AWS IRSA Setup for EKS Deployments
This guide walks through setting up an Unstract on-prem deployment on Amazon EKS to access Amazon S3 (for storage) and Amazon Bedrock (for LLM inference) using IAM Roles for Service Accounts (IRSA).
This guide is EKS-specific. For non-EKS Kubernetes clusters, use static credentials as described in the Infrastructure Requirements.
IRSA for S3 storage is available from Unstract v0.158.4 onwards. Bedrock IAM Role / Instance Profile authentication (Step 6.4) requires v0.159.3 or later.
Why IRSA
With IRSA, each Kubernetes service account is bound to a dedicated IAM role via OIDC federation. Pods get short-lived AWS credentials that are scoped to that role.
What this gives you:
- Pod-scoped permissions — only Unstract pods get S3 and Bedrock access; other workloads on the same cluster are unaffected.
- Per-workload audit trail — CloudTrail attributes calls to the IRSA role, so you can trace AWS activity directly to Unstract.
- No static keys, no rotation — credentials come from a JWT token that the EKS Pod Identity Webhook injects and rotates automatically.
- Workload isolation — different services on the same cluster can have different AWS permissions.
The setup is a one-time effort: an OIDC provider, an IAM role with a trust policy, and a service account annotation. After that, it is hands-off.
Prerequisites
- An Amazon EKS cluster (Kubernetes 1.29+ to match the Unstract platform requirement; IRSA itself requires only 1.14+).
kubectlconfigured against the cluster.- AWS CLI with permissions to create IAM OIDC providers, IAM policies, IAM roles, and S3 buckets.
eksctlinstalled (recommended for OIDC provider setup).- Unstract Helm chart available locally or from your registry.
- A Kubernetes namespace created in the cluster where Unstract will be deployed.
Set these environment variables — they are used throughout the guide:
export CLUSTER_NAME=<your-eks-cluster-name>
export AWS_REGION=<your-region> # e.g., ap-south-1
export AWS_ACCOUNT_ID=<your-account-id>
export NAMESPACE=<your-namespace>
export SERVICE_ACCOUNT=unstract-irsa # fixed by the Unstract Helm chart
export S3_BUCKET=<your-s3-bucket-name>
Step 1 — Create the S3 bucket
aws s3 mb s3://${S3_BUCKET} --region ${AWS_REGION}
S3 bucket names are globally unique. Pick a region close to your EKS cluster.
Step 2 — Define and create the IAM policies
Both policies are reusable — you create them once and attach them to the IRSA role.
2.1 S3 policy
Save as unstract-s3-policy.json:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListBucket",
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::<s3-bucket-name>"
]
},
{
"Sid": "ObjectAccess",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject"
],
"Resource": [
"arn:aws:s3:::<s3-bucket-name>/*"
]
},
{
"Sid": "ListAllBuckets",
"Effect": "Allow",
"Action": [
"s3:ListAllMyBuckets"
],
"Resource": "*"
}
]
}
s3:ListAllMyBuckets is required by Unstract's storage adapter for connectivity validation.
Replace <s3-bucket-name> with your actual bucket name before running:
aws iam create-policy \
--policy-name UnstractS3Access \
--policy-document file://unstract-s3-policy.json
2.2 Bedrock policy
Save as unstract-bedrock-policy.json (replace <aws-account-id>):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "BedrockInvoke",
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream"
],
"Resource": [
"arn:aws:bedrock:*::foundation-model/*",
"arn:aws:bedrock:*:<aws-account-id>:inference-profile/*"
]
},
{
"Sid": "BedrockList",
"Effect": "Allow",
"Action": [
"bedrock:ListFoundationModels",
"bedrock:GetFoundationModel",
"bedrock:ListInferenceProfiles"
],
"Resource": "*"
}
]
}
The wildcard region in arn:aws:bedrock:*::foundation-model/* and the inference-profile/* resource are required for cross-region inference profiles — model IDs prefixed with us., eu., apac., or global. (the global. prefix is supported for select models only). These route across multiple regions and break with single-region scoping.
aws iam create-policy \
--policy-name UnstractBedrockAccess \
--policy-document file://unstract-bedrock-policy.json
Step 3 — Associate an OIDC provider with the cluster
IRSA uses OIDC federation. Each EKS cluster has a unique OIDC issuer URL that must be registered as an IAM identity provider.
3.1 Get the cluster's OIDC issuer URL
OIDC_URL=$(aws eks describe-cluster \
--name "$CLUSTER_NAME" \
--region "$AWS_REGION" \
--query 'cluster.identity.oidc.issuer' \
--output text)
echo "$OIDC_URL"
# Example: https://oidc.eks.<aws-region>.amazonaws.com/id/<oidc-id>
# Strip the https:// prefix for use in trust policies
OIDC_ISSUER=$(echo "$OIDC_URL" | sed 's|^https://||')
echo "$OIDC_ISSUER"
3.2 Check whether an OIDC provider already exists
: "${OIDC_ISSUER:?run Step 3.1 first to set OIDC_ISSUER}"
OIDC_ID=$(echo "$OIDC_ISSUER" | awk -F'/' '{print $NF}')
if ! aws iam list-open-id-connect-providers \
--query "OpenIDConnectProviderList[?contains(Arn, '$OIDC_ID')].Arn" \
--output text; then
echo "AWS call failed — check credentials and IAM permissions" >&2
exit 1
fi
If the command prints an ARN, an OIDC provider is already registered for this cluster — skip to Step 4. If the output is empty (and the command itself succeeded), no provider is registered yet — continue to Step 3.3.
3.3 Create the OIDC provider
eksctl utils associate-iam-oidc-provider \
--cluster "$CLUSTER_NAME" \
--region "$AWS_REGION" \
--approve
If you do not have eksctl, you can use the AWS CLI directly — but eksctl handles thumbprint discovery automatically and is the recommended approach.
Step 4 — Create the IAM role for Unstract
This is the role that Unstract pods will assume.
4.1 Service account name
The Unstract Helm chart creates a service account named unstract-irsa in the install namespace. This name is fixed by the chart — the trust policy must reference it exactly.
export SERVICE_ACCOUNT=unstract-irsa
4.2 Build the trust policy
The trust policy binds the IAM role to specific Kubernetes service accounts. AWS will refuse to issue credentials unless the JWT in the pod matches the sub condition.
The OIDC issuer string appears in two distinct forms — the script below assembles both correctly:
- IAM ARN form (
arn:aws:iam::<account>:oidc-provider/<issuer>) — used asPrincipal.Federated. - Bare-issuer form — used as the prefix of both the
:audand:subcondition keys.
EOF is intentionally left unquoted so the placeholders are expanded into the JSON. Run the variable check first to fail fast if any are unset:
: "${AWS_ACCOUNT_ID:?re-export from Prerequisites}"
: "${OIDC_ISSUER:?run Step 3.1 again to set OIDC_ISSUER}"
: "${NAMESPACE:?not set}"
: "${SERVICE_ACCOUNT:?not set}"
Now write the trust policy:
cat > unstract-irsa-trust-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/${OIDC_ISSUER}"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"${OIDC_ISSUER}:aud": "sts.amazonaws.com",
"${OIDC_ISSUER}:sub": "system:serviceaccount:${NAMESPACE}:${SERVICE_ACCOUNT}"
}
}
}
]
}
EOF
Inspect the rendered file before running the next step:
cat unstract-irsa-trust-policy.json
Verify no placeholder variables remain literal and no fields are empty before continuing. A malformed JSON here surfaces as a confusing IAM error in Step 4.3 rather than at policy-write time.
4.3 Create the role
aws iam create-role \
--role-name UnstractIRSARole \
--assume-role-policy-document file://unstract-irsa-trust-policy.json \
--description "IRSA role for Unstract pods on EKS"