Acceldata
ADOC

Cross-account S3 Access Point authentication (IRSA or EKS Pod Identity)

Use this guide when the ADOC Data Plane runs on EKS in one AWS account (Account A) and reads data through an S3 Access Point hosted in a different AWS account (Account B). The bucket and the Access Point live in Account B. Pods in Account A use one dedicated IAM role, and ADOC connects to the Access Point as the datasource.

Complete the shared setup once, then choose exactly one authentication option:

  • Option 1: IRSA
  • Option 2: EKS Pod Identity

For a scenario where the cluster, bucket, Access Point, and IAM role are all in the same AWS account, see Same-account S3 Access Point authentication. This is also a different mechanism from Cross‑Account Access Setup, which grants direct cross-account bucket access without an Access Point.

Required values

Token

Meaning

<region>

Same region as EKS, the bucket, and the Access Point

<account-a-id>

The account that hosts EKS

<account-b-id>

The account that hosts the bucket and the Access Point

<cluster>

EKS cluster name in Account A

<namespace>

The Data Plane's live namespace

<bucket>

Bucket name in Account B

<ap-name>

Access Point name in Account B

<ap-alias>

Access Point alias, assigned after creation. Ends with -s3alias

<ap-arn>

arn:aws:s3:<region>:<account-b-id>:accesspoint/<ap-name>

<prefix>

Allowed folder, for example allowed/

<iam-role>

Dedicated IAM role in Account A. Create a new role — do not reuse a same-account Access Point role

<oidc-host>

OIDC host with no https://

<analysis-standalone-deploy>

The deployment that uses service account analysis-standalone-service

<spark-history-bucket>

Bucket in Account A used for Spark event logs

Confirm service account names with kubectl -n <namespace> get sa. There is no service account named dataplane-spark. Skip any service account that does not exist. Skip any deployment that is already failing, for example CrashLoopBackOff.

Service accounts in scope

  • analysis-service
  • analysis-standalone-service
  • spark-scheduler
  • torch-monitors
  • analysis-sql-service
  • spark-history-server
  • acceldata-dataplane-spark

Shared setup

Step 1: Create the bucket in Account B

  1. Log in to Account B.
  2. Create bucket <bucket> in region <region>.
  3. Keep Block Public Access enabled.
  4. Upload sample files under <prefix>, for example allowed/sample.csv.
  5. Leave the bucket policy empty until the IAM role in Account A exists.

Step 2: Create the Access Point in Account B

  1. In S3, open bucket <bucket> and create Access Point <ap-name>.
  2. Choose the same bucket <bucket>.
  3. Set network to Internet, or the VPC used by the cluster.
  4. Keep Block Public Access enabled.
  5. Copy the Access Point ARN and alias.

Do not put a prefix condition on s3:ListBucket. ADOC Test Connection uses HeadBucket and does not send a prefix.

Step 3: Create the IAM role and inline S3 policy in Account A

Create IAM role <iam-role> in Account A. Set the trust relationship later, in Option 1 or Option 2. Attach this inline S3 policy to the role:

Json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "HeadAndList",
      "Effect": "Allow",
      "Action": ["s3:ListBucket", "s3:GetBucketLocation"],
      "Resource": [
        "arn:aws:s3:<region>:<account-b-id>:accesspoint/<ap-name>",
        "arn:aws:s3:::<bucket>"
      ]
    },
    {
      "Sid": "ReadObjects",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:GetObjectVersion"],
      "Resource": [
        "arn:aws:s3:<region>:<account-b-id>:accesspoint/<ap-name>/object/<prefix>*",
        "arn:aws:s3:::<bucket>/<prefix>*"
      ]
    }
  ]
}

Cross-account access requires both the Access Point ARN and the bucket ARN on the IAM role.

Step 4: Add the Access Point policy in Account B

On Access Point <ap-name>, allow only the Account A role:

Json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowListBucketViaAp",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::<account-a-id>:role/<iam-role>"
      },
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:<region>:<account-b-id>:accesspoint/<ap-name>"
    },
    {
      "Sid": "AllowGetObjectViaAp",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::<account-a-id>:role/<iam-role>"
      },
      "Action": ["s3:GetObject", "s3:GetObjectVersion"],
      "Resource": "arn:aws:s3:<region>:<account-b-id>:accesspoint/<ap-name>/object/<prefix>*"
    }
  ]
}

Step 5: Add the bucket policy in Account B

The Access Point policy works only if the bucket policy also allows the request. Add one of the following patterns and preserve any unrelated existing statements.

Option A: Access Point only. Use this when the role should access the bucket only through the Access Point, and no other service uses the raw bucket name.

Json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowListViaAccessPoint",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::<account-a-id>:role/<iam-role>"
      },
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::<bucket>",
      "Condition": {
        "StringEquals": {
          "s3:DataAccessPointArn": "arn:aws:s3:<region>:<account-b-id>:accesspoint/<ap-name>"
        }
      }
    },
    {
      "Sid": "AllowGetObjectViaAccessPoint",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::<account-a-id>:role/<iam-role>"
      },
      "Action": ["s3:GetObject", "s3:GetObjectVersion"],
      "Resource": "arn:aws:s3:::<bucket>/*",
      "Condition": {
        "StringEquals": {
          "s3:DataAccessPointArn": "arn:aws:s3:<region>:<account-b-id>:accesspoint/<ap-name>"
        }
      }
    }
  ]
}

Option B: Access Point and raw bucket. Use this when the same role must also access s3://<bucket> or s3a://<bucket>/... directly.

Json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowRoleOnBucket",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::<account-a-id>:role/<iam-role>"
      },
      "Action": [
        "s3:ListBucket",
        "s3:GetBucketLocation",
        "s3:GetObject",
        "s3:GetObjectVersion"
      ],
      "Resource": [
        "arn:aws:s3:::<bucket>",
        "arn:aws:s3:::<bucket>/*"
      ]
    }
  ]
}

Option 1: IRSA

Confirm or create the OIDC provider

aws eks describe-cluster --name <cluster> --region <region> --query \
  'cluster.identity.oidc.issuer' --output text

Strip https:// from the result. That value is <oidc-host>. If the result is empty, create the provider:

eksctl utils associate-iam-oidc-provider --cluster <cluster> --region <region> --approve

Set the IRSA trust policy

Update the trust relationship on role <iam-role>:

Json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::<account-a-id>:oidc-provider/<oidc-host>"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "<oidc-host>:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "<oidc-host>:sub": [
            "system:serviceaccount:<namespace>:analysis-service",
            "system:serviceaccount:<namespace>:analysis-standalone-service",
            "system:serviceaccount:<namespace>:spark-scheduler",
            "system:serviceaccount:<namespace>:torch-monitors",
            "system:serviceaccount:<namespace>:analysis-sql-service",
            "system:serviceaccount:<namespace>:spark-history-server",
            "system:serviceaccount:<namespace>:acceldata-dataplane-spark"
          ]
        }
      }
    }
  ]
}

Remove Pod Identity associations for these service accounts, if any

If a service account has both Pod Identity and IRSA, Pod Identity wins. For an IRSA-only setup, remove those associations first.

aws eks list-pod-identity-associations --cluster-name <cluster> --region <region> \
  --query 'associations[?namespace==`<namespace>`].[associationId,serviceAccount,roleArn]' --output table

Delete each matching association:

aws eks delete-pod-identity-association --cluster-name <cluster> \
  --region <region> --association-id <association-id>
NS=<namespace>
ROLE=arn:aws:iam::<account-a-id>:role/<iam-role>

Annotate the service accounts

for sa in analysis-service analysis-standalone-service spark-scheduler \
 torch-monitors analysis-sql-service spark-history-server acceldata-dataplane-spark
do
 kubectl -n "$NS" annotate sa "$sa" eks.amazonaws.com/role-arn="$ROLE" --overwrite
done
kubectl -n <namespace> rollout restart deploy

Restart deployments

Wait until the deployments are ready. If needed, find the standalone deployment:

kubectl -n <namespace> get deploy | grep analysis-standalone
kubectl -n <namespace> exec deploy/<analysis-standalone-deploy> -- env \
 | grep -E 'AWS_ROLE_ARN|AWS_WEB_IDENTITY_TOKEN_FILE|AWS_CONTAINER_CREDENTIALS'

Confirm IRSA on a pod

Expected result for IRSA:AWS_ROLE_ARN=arn:aws:iam::<account-a-id>:role/<iam-role> is present and AWS_CONTAINER_CREDENTIALS_FULL_URI is not present.

Option 2: EKS Pod Identity

Confirm the Pod Identity agent

kubectl -n kube-system get ds eks-pod-identity-agent

The agent must be running.

Set the Pod Identity trust policy

In the role, add this trust policy statement:

Json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "pods.eks.amazonaws.com"
      },
      "Action": [
        "sts:AssumeRole",
        "sts:TagSession"
      ]
    }
  ]
}

If you already completed IRSA setup, add this statement to the existing trust policy and keep the OIDC statement. sts:TagSession is required.

Recreate service account associations

List current associations:

aws eks list-pod-identity-associations --cluster-name <cluster> --region <region> \
  --query 'associations[?namespace==`<namespace>`].[associationId,serviceAccount,roleArn]' --output table

If a target service account already has an association, delete it before creating the new one:

aws eks delete-pod-identity-association --cluster-name <cluster> \
  --region <region> --association-id <association-id>

Create the associations:

CLUSTER=<cluster>
REGION=<region>
NS=<namespace>
ROLE=arn:aws:iam::<account-a-id>:role/<iam-role>
for sa in analysis-service analysis-standalone-service spark-scheduler \
  torch-monitors analysis-sql-service spark-history-server acceldata-dataplane-spark
do
  aws eks create-pod-identity-association --cluster-name "$CLUSTER" \
    --region "$REGION" --namespace "$NS" --service-account "$sa" --role-arn \
    "$ROLE"
done

You can leave the IRSA annotation on the service accounts. When both exist, Pod Identity is used instead of IRSA.

Restart deployments

kubectl -n <namespace> rollout restart deploy

Wait until the deployments are ready. Spark job pods pick this up on the next crawl.

Confirm Pod Identity on a pod

kubectl -n <namespace> exec deploy/<analysis-standalone-deploy> -- env \
 | grep -E 'AWS_ROLE_ARN|AWS_WEB_IDENTITY_TOKEN_FILE|AWS_CONTAINER_CREDENTIALS'

Expected result for Pod Identity:AWS_CONTAINER_CREDENTIALS_FULL_URI is present and AWS_ROLE_ARN is absent.

Verify access from the cluster

Run these checks after completing either Option 1 or Option 2. Delete any leftover ap-head pod first. Create a new pod each time. Read logs after the pod reaches Completed. Do not exec into it.

1. HeadBucket using the Access Point ARN

kubectl -n <namespace> delete pod ap-head --ignore-not-found
kubectl -n <namespace> run ap-head --restart=Never \
 --image=public.ecr.aws/aws-cli/aws-cli:2.17.55 --overrides='{"spec":
  {"serviceAccountName":"analysis-standalone-service"}}' --command -- aws \
 s3api head-bucket --bucket <ap-arn> --region <region>
kubectl -n <namespace> logs ap-head

Pass condition: the logs return JSON that includes BucketRegion.

2. HeadObject using the Access Point alias

kubectl -n <namespace> delete pod ap-head --ignore-not-found
kubectl -n <namespace> run ap-head --restart=Never \
 --image=public.ecr.aws/aws-cli/aws-cli:2.17.55 --overrides='{"spec":
  {"serviceAccountName":"analysis-standalone-service"}}' --command -- aws \
 s3api head-object --bucket <ap-alias> --key <prefix>sample.csv --region \
 <region>
kubectl -n <namespace> logs ap-head

Pass condition: the logs return JSON that includes ContentLength or ETag.

Configure the ADOC datasource

In ADOC, add a new AWS S3 datasource. For the general steps to open the Add Data Source wizard, seeAmazon S3. Use these values for a cross-account Access Point connection.

Connection field

Value

Region

<region>

Authentication

Option 1: AWS IAM Roles For Service Accounts. Option 2: EKS Pod Identity

Bucket Name

<ap-alias>, or <ap-arn> if the alias fails. Do not use s3a://

Data Plane

This cluster's Data Plane in <namespace>

Select Test Connection. A successful result shows the datasource connected.

Asset or Observability configuration

Field

Value

Asset Name

Any label

Path Expression

s3a://<ap-alias>/<prefix>*

File Type / delimiter

Match the files under <prefix>

Crawler schedule / notify / cadence

Optional

Run the crawl, then run the profile.

GET /validate on analysis-service is a health probe. The relevant jobs for datasource testing use type CONNECTION_VALIDATION.

Troubleshooting

  • If ADOC Test Connection fails on bucket access, verify that the bucket policy in Account B allows the role from Account A, in addition to the Access Point policy.
  • If HeadBucket fails but HeadObject works, recheck that no prefix condition was added to s3:ListBucket.
  • If IRSA is configured but pods still use container credentials, look for existing Pod Identity associations on the same service account.
  • If Pod Identity is configured but credentials are missing, confirm the eks-pod-identity-agent daemonset is running and the trust policy includes sts:TagSession.
  • If the alias fails in ADOC, use <ap-arn> for connection testing, then confirm alias resolution separately.