Skip to main content

Architecture Overview

Xpander self-hosted uses a three-tier IAM role architecture for connector authentication:
┌─────────────────────────────────────────────────────┐
│  EKS Pod (xpander service account)                  │
│                                                     │
│  Pod Identity ──► Base Role (no permissions)        │
│                       │                             │
│                       ├──► Redshift Connector Role  │
│                       ├──► EKS Connector Role       │
│                       ├──► S3 Connector Role        │
│                       └──► [Any Future Connector]   │
└─────────────────────────────────────────────────────┘
RolePurposePermissions
Base RoleAttached to pods via EKS Pod Identity. Has no direct permissions — only assumes connector-specific roles.sts:AssumeRole + sts:TagSession on each connector role
Connector RolesOne per data source/service. Contains only the permissions needed for that connector.Service-specific (e.g., redshift-data:*, eks:*) + self-assume
Cross-Account TrustEach connector role can optionally trust the xpander platform for cloud-managed operations.Trust with ExternalId condition

Why Three Tiers?

  1. Least privilege — Each connector only has access to its specific service. A Redshift connector can’t access EKS and vice versa.
  2. Auditability — CloudTrail shows exactly which connector role accessed which resource. No shared credentials.
  3. Independent lifecycle — Add or remove connectors without touching other roles. Revoke one connector’s access without affecting others.
  4. Role chaining — The AI Gateway assumes the appropriate connector role per request. This is standard AWS role chaining, fully supported by xpander.
If you’re using a single IAM role for all connectors (the setup from AWS Operator Setup), the instructions on this page show how to split that into per-connector roles for better isolation and auditability.

Step 1: Base Role

The base role is the only role attached to pods. It has no service permissions — it can only assume other roles.
aws iam create-role \
  --role-name xpander-base-role \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": { "Service": "pods.eks.amazonaws.com" },
      "Action": ["sts:AssumeRole", "sts:TagSession"]
    }]
  }'
Attach the assume permissions (update this list as you add connectors):
aws iam put-role-policy \
  --role-name xpander-base-role \
  --policy-name AssumeConnectorRoles \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Action": ["sts:AssumeRole", "sts:TagSession"],
      "Resource": [
        "arn:aws:iam::<ACCOUNT_ID>:role/xpander-redshift-access",
        "arn:aws:iam::<ACCOUNT_ID>:role/xpander-eks-connector"
      ]
    }]
  }'
Associate with the xpander service account via Pod Identity:
aws eks create-pod-identity-association \
  --cluster-name <CLUSTER_NAME> \
  --namespace xpander \
  --service-account xpander \
  --role-arn arn:aws:iam::<ACCOUNT_ID>:role/xpander-base-role \
  --region <REGION>

Step 2: Connector Roles

Each connector role needs a trust policy with four statements:
StatementPurposeExternalId Required?
PodIdentityDirect pod access (fallback)No
CrossAccountWithExternalIdxpander cloud platform operationsYes
SelfAssumeAI Gateway internal re-assume for credential refreshNo
AllowFromBaseRoleRole chaining from the Pod Identity base roleNo

Trust Policy Template

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PodIdentity",
      "Effect": "Allow",
      "Principal": { "Service": "pods.eks.amazonaws.com" },
      "Action": ["sts:AssumeRole", "sts:TagSession"]
    },
    {
      "Sid": "CrossAccountWithExternalId",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::<ACCOUNT_ID>:root"
      },
      "Action": ["sts:AssumeRole", "sts:TagSession"],
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "<ORGANIZATION_ID>"
        }
      }
    },
    {
      "Sid": "SelfAssume",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::<ACCOUNT_ID>:role/<THIS_ROLE_NAME>"
      },
      "Action": ["sts:AssumeRole", "sts:TagSession"]
    },
    {
      "Sid": "AllowFromBaseRole",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::<ACCOUNT_ID>:role/xpander-base-role"
      },
      "Action": ["sts:AssumeRole", "sts:TagSession"]
    }
  ]
}
The SelfAssume and AllowFromBaseRole statements must not have an ExternalId condition. The AI Gateway’s internal AssumeRole calls do not pass an external ID. If you add an ExternalId condition to these statements, the connector will fail with AccessDenied.

Permission Policies Per Connector

Each connector role needs its own service permissions plus self-assume.

Self-Assume + Session Tagging (required for all connector roles)

aws iam put-role-policy \
  --role-name <CONNECTOR_ROLE> \
  --policy-name SelfAssumeAndTagSession \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Action": ["sts:AssumeRole", "sts:TagSession"],
      "Resource": "arn:aws:iam::<ACCOUNT_ID>:role/<CONNECTOR_ROLE>"
    }]
  }'
aws iam put-role-policy \
  --role-name xpander-redshift-access \
  --policy-name RedshiftCredentials \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Action": [
        "redshift:GetClusterCredentials",
        "redshift:GetClusterCredentialsWithIAM",
        "redshift:DescribeClusters"
      ],
      "Resource": [
        "arn:aws:redshift:<REGION>:<ACCOUNT_ID>:cluster:<CLUSTER_ID>",
        "arn:aws:redshift:<REGION>:<ACCOUNT_ID>:dbname:<CLUSTER_ID>/<DB_NAME>",
        "arn:aws:redshift:<REGION>:<ACCOUNT_ID>:dbuser:<CLUSTER_ID>/*"
      ]
    }]
  }'

aws iam put-role-policy \
  --role-name xpander-redshift-access \
  --policy-name RedshiftDataAPI \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Action": [
        "redshift-data:ExecuteStatement",
        "redshift-data:GetStatementResult",
        "redshift-data:DescribeStatement",
        "redshift-data:ListStatements",
        "redshift-data:CancelStatement",
        "redshift-data:BatchExecuteStatement",
        "redshift-data:ListDatabases",
        "redshift-data:ListSchemas",
        "redshift-data:ListTables",
        "redshift-data:DescribeTable"
      ],
      "Resource": "*"
    }]
  }'
aws iam put-role-policy \
  --role-name xpander-eks-connector \
  --policy-name EKSAccess \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Action": [
          "eks:DescribeCluster",
          "eks:ListClusters",
          "eks:ListNodegroups",
          "eks:DescribeNodegroup",
          "eks:ListInsights",
          "eks:DescribeInsight",
          "eks:AccessKubernetesApi"
        ],
        "Resource": "*"
      },
      {
        "Effect": "Allow",
        "Action": [
          "logs:GetLogEvents",
          "logs:FilterLogEvents",
          "logs:DescribeLogGroups",
          "logs:DescribeLogStreams"
        ],
        "Resource": "*"
      }
    ]
  }'
The EKS connector also needs Kubernetes RBAC:
aws eks create-access-entry \
  --cluster-name <CLUSTER_NAME> \
  --principal-arn arn:aws:iam::<ACCOUNT_ID>:role/xpander-eks-connector \
  --type STANDARD \
  --region <REGION>

aws eks associate-access-policy \
  --cluster-name <CLUSTER_NAME> \
  --principal-arn arn:aws:iam::<ACCOUNT_ID>:role/xpander-eks-connector \
  --policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy \
  --access-scope type=cluster \
  --region <REGION>

Step 3: Configure in xpander UI

In the connector settings, set the IAM Role ARN to the connector-specific role (not the base role):
ConnectorRole ARN
Redshiftarn:aws:iam::<ACCOUNT_ID>:role/xpander-redshift-access
EKSarn:aws:iam::<ACCOUNT_ID>:role/xpander-eks-connector

Adding a New Connector

When adding a new AWS connector (e.g., S3, DynamoDB, Athena):
1

Create the connector role

Use the trust policy template above.
2

Attach service-specific permissions

Add the service permissions + the self-assume policy.
3

Update the base role

Add the new role ARN to the AssumeConnectorRoles policy on the base role.
4

Configure in xpander UI

Set the connector’s IAM Role ARN.
5

Restart the AI Gateway

kubectl rollout restart deployment xpander-ai-gateway -n xpander
No Pod Identity changes needed — the base role association stays the same.

Role Creation Order

IAM roles can’t reference themselves during creation. Follow this order:
  1. Create the role with only PodIdentity + CrossAccountWithExternalId trust statements
  2. Wait 10 seconds for IAM propagation
  3. Update the trust policy to add SelfAssume + AllowFromBaseRole statements
# Step 1: Create without self-reference
aws iam create-role --role-name <ROLE> --assume-role-policy-document file://initial-trust.json

# Step 2: Wait for propagation
sleep 10

# Step 3: Update with self-assume
aws iam update-assume-role-policy --role-name <ROLE> --policy-document file://full-trust.json

Troubleshooting

Cause: Missing sts:TagSession in either the trust policy or the permission policy.Fix: Ensure both:
  • Trust policy allows sts:TagSession from the caller
  • Permission policy includes sts:TagSession on the role’s own ARN
Cause: The SelfAssume trust statement has an ExternalId condition, or the self-assume permission policy is missing.Fix: The SelfAssume and AllowFromBaseRole statements must NOT have conditions. The AI Gateway does not pass an ExternalId during internal role operations.
Cause: Base role doesn’t have permission to assume the connector role, or the connector role’s trust policy doesn’t list the base role.Fix: Check both sides:
  • Base role permission policy lists the connector role ARN
  • Connector role trust policy has AllowFromBaseRole statement
Cause: In cloud mode, the xpander platform assumes the role directly with the ExternalId. In self-hosted mode, the AI Gateway chains through the base role without an ExternalId.Fix: Add the AllowFromBaseRole trust statement to the connector role.

Security Recommendations

  1. Scope resources — Use specific ARNs in permission policies, not "Resource": "*" where possible
  2. Separate roles per data source — Don’t combine Redshift + EKS permissions in one role
  3. Rotate ExternalId — The organization ID is used as ExternalId. If compromised, rotate it in the xpander platform
  4. Monitor with CloudTrail — Each connector role appears separately in CloudTrail, making it easy to audit which connector accessed which resource
  5. Tag roles — Tag all roles with Environment, Project, ManagedBy for governance
  6. Restrict base role — The base role should ONLY have sts:AssumeRole + sts:TagSession. Never attach service permissions directly to it