# 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
export AWS_OIDC_PROVIDER_ARN="arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/exe.dev"
```

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](/integrations), 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 `Unique generated subject`, then click `Run`. The AWS role trust
policy below must use that exact `sub`.

<img src="/docs/integrations/aws-wif/identity-federation-add-aws.png" alt="Identity Federation modal configured for an AWS role" width="100%"/>

Set the copied subject in your shell before running the AWS commands:

```
export EXE_WIF_SUBJECT=sub-copy-from-the-integration
```

### Configure AWS

Create the IAM OIDC provider once per AWS account:

```
aws iam create-open-id-connect-provider \
  --url https://exe.dev \
  --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": {
          "exe.dev:aud": "${AWS_AUDIENCE}",
          "exe.dev: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](#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.

For additional exe.dev WIF integrations in the same AWS account, you usually
reuse the same IAM OIDC provider. Use the same `sts.amazonaws.com` audience,
create the new exe.dev integration, copy its `Unique generated subject`, and
grant that subject access to the role it should assume. Create a new provider
only when you want a separate trust boundary.

AWS references:

- [Create an IAM OIDC provider](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html)
- [Create a role for an OIDC identity provider](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html)
- [AssumeRoleWithWebIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html)
- [AWS SDK web identity token credentials](https://docs.aws.amazon.com/sdkref/latest/guide/feature-assume-role-credentials.html)
