AWS Workload Identity Federation

Identity Federation lets an attached VM mint short-lived exe.dev OIDC tokens. AWS trusts those tokens through an IAM OIDC provider, then exchanges them for temporary role credentials with AssumeRoleWithWebIdentity. Use this instead of storing AWS access keys on the VM.

Create the exe.dev integration in the web UI. Run the AWS commands from a machine with the AWS CLI.

Setup

Set values

One-time AWS account setup values. Choose these once for the AWS account:

export AWS_ACCOUNT_ID=123456789012

export AWS_AUDIENCE=sts.amazonaws.com

Per-role and per-exe.dev-integration values. Choose these for each workload:

export INTEGRATION_NAME=awswif
export ROLE_NAME=exe-dev-demo

export AWS_ROLE_ARN="arn:aws:iam::${AWS_ACCOUNT_ID}:role/${ROLE_NAME}"
export AWS_TRUST_POLICY_FILE="/tmp/exe-${ROLE_NAME}-trust-policy.json"

Add the exe.dev integration

Open the Integrations page, choose Identity Federation, and select AWS.

  • Name: awswif
  • AWS role ARN: the AWS_ROLE_ARN value from above
  • Attach to: the VM or tag that should use this role

Copy the generated Issuer and Subject, then click Run. The AWS provider and role trust policy below must use those exact values.

Identity Federation modal configured for an AWS role

Set the copied values in your shell before running the AWS commands. Replace these example values with the exact Issuer and Subject from the integration:

export EXE_WIF_ISSUER=https://exe.dev/issuer/example-team-workload
export EXE_WIF_SUBJECT=sub-ABCDEFGHIJKLMNOPQRSTUVWXYZ

export AWS_OIDC_CONDITION_PREFIX="${EXE_WIF_ISSUER#https://}"
export AWS_OIDC_PROVIDER_ARN="arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/${AWS_OIDC_CONDITION_PREFIX}"

The condition prefix and provider ARN use the issuer host and path, without the https:// scheme.

Configure AWS

Create the IAM OIDC provider once per exe.dev user or team in the AWS account:

aws iam create-open-id-connect-provider \
  --url "$EXE_WIF_ISSUER" \
  --client-id-list "$AWS_AUDIENCE"

Create the role trust policy:

cat > "$AWS_TRUST_POLICY_FILE" <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "${AWS_OIDC_PROVIDER_ARN}"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "${AWS_OIDC_CONDITION_PREFIX}:aud": "${AWS_AUDIENCE}",
          "${AWS_OIDC_CONDITION_PREFIX}:sub": "${EXE_WIF_SUBJECT}"
        }
      }
    }
  ]
}
EOF

aws iam create-role \
  --role-name "$ROLE_NAME" \
  --assume-role-policy-document "file://${AWS_TRUST_POLICY_FILE}"

Grant the role only the permissions your workload needs. For example:

export BUCKET_NAME=my-app-bucket
export AWS_PERMISSIONS_POLICY_FILE="/tmp/exe-${ROLE_NAME}-s3-read-policy.json"

cat > "$AWS_PERMISSIONS_POLICY_FILE" <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::${BUCKET_NAME}"
    },
    {
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::${BUCKET_NAME}/*"
    }
  ]
}
EOF

aws iam put-role-policy \
  --role-name "$ROLE_NAME" \
  --policy-name exe-dev-demo-s3-read \
  --policy-document "file://${AWS_PERMISSIONS_POLICY_FILE}"

You do not need to attach a policy just to run aws sts get-caller-identity as a smoke test after assumption.

Use it from the VM

From a VM that has the integration attached, use /metadata to read the AWS role ARN you entered in the integration:

export INTEGRATION_NAME=awswif

export EXE_WIF_URL="https://${INTEGRATION_NAME}.int.exe.xyz"
export EXE_WIF_METADATA_FILE="/tmp/exe-${INTEGRATION_NAME}-aws-wif-metadata.json"
export AWS_WEB_IDENTITY_TOKEN_FILE="/tmp/exe-${INTEGRATION_NAME}-aws-web-identity-token"
export AWS_ROLE_SESSION_NAME="exe-${INTEGRATION_NAME}"

curl -fsS "$EXE_WIF_URL/metadata" > "$EXE_WIF_METADATA_FILE"
curl -fsS "$EXE_WIF_URL/token" | jq -r .token > "$AWS_WEB_IDENTITY_TOKEN_FILE"

For a team integration, use https://${INTEGRATION_NAME}.team.exe.xyz for EXE_WIF_URL instead.

export AWS_ROLE_ARN="$(jq -r .role_arn "$EXE_WIF_METADATA_FILE")"

aws sts get-caller-identity

The AWS CLI and AWS SDKs read AWS_ROLE_ARN and AWS_WEB_IDENTITY_TOKEN_FILE, then call AWS STS with the token file. exe.dev does not put AWS access keys on the VM. For long-running processes, refresh AWS_WEB_IDENTITY_TOKEN_FILE before the SDK needs to call STS again.

Use cases

Common things to do from a VM with an assumed role. Each assumes AWS_ROLE_ARN and AWS_WEB_IDENTITY_TOKEN_FILE are exported as shown above.

S3: artifacts and data

Sync build outputs, datasets, or static sites to a bucket:

aws s3 sync ./dist "s3://${BUCKET_NAME}/"

The permissions example in Configure AWS grants scoped S3 read access; add s3:PutObject on the bucket for writes.

ECR: push and pull container images

Build images on the VM and push them to a private registry:

export ECR_REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"

aws ecr get-login-password | docker login --username AWS --password-stdin "$ECR_REGISTRY"
docker push "${ECR_REGISTRY}/${REPO_NAME}:${TAG}"

Grant AmazonEC2ContainerRegistryPowerUser or a policy scoped to the repository ARN.

CodeArtifact: private package registries

Install from and publish to private npm, PyPI, or Maven repositories with a temporary token:

aws codeartifact login --tool npm \
  --domain "$CA_DOMAIN" --repository "$CA_REPO"

npm install
npm publish

Use --tool pip or --tool twine for Python. Grant codeartifact:GetAuthorizationToken, sts:GetServiceBearerToken, and the read or publish actions the workload needs.

Bedrock: model inference

Call Claude and other models with temporary credentials:

aws bedrock-runtime converse \
  --model-id "$MODEL_ID" \
  --messages '[{"role":"user","content":[{"text":"Hello"}]}]'

AWS SDKs pick up the web identity env vars automatically, so application code needs no extra configuration. Grant bedrock:InvokeModel scoped to the model ARNs you use.

Terraform / IaC: state and deploys

Run terraform plan and terraform apply with an S3 state backend and DynamoDB (or S3 native) locking:

terraform init
terraform plan
terraform apply

Terraform's AWS provider reads the same AWS_ROLE_ARN and AWS_WEB_IDENTITY_TOKEN_FILE env vars. Grant access to the state bucket and lock table, plus whatever the configuration manages.

Keep it narrow

  • Use one exe.dev WIF integration per AWS role or workload.
  • Attach the integration only to the VM or tag that needs it.
  • Bind sts:AssumeRoleWithWebIdentity to the exact exe.dev audience and subject.
  • Give the AWS role only the permissions the workload needs.
  • Do not create or store AWS access keys as a fallback.

Additional integrations owned by the same exe.dev user or team reuse its IAM OIDC provider. A different exe.dev user or team has a different issuer and needs its own provider. AWS accounts have a default quota of 100 IAM OIDC providers.

AWS references: