> ## Documentation Index
> Fetch the complete documentation index at: https://www.ravion.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# AWS credentials in GitHub Actions jobs

> How sandbox jobs get AWS credentials, why Docker containers can't reach them, and how to pass credentials to containers without printing them in the job log.

> [!NOTE]
>
> GitHub Actions runners are in private preview. [Contact support](https://www.ravion.com/docs/contact-support.md) to request access.

This page covers jobs that run on sandboxes (`type=sandbox`). It explains where a job's AWS credentials come from, what can and can't reach them, and how to get credentials into Docker containers safely.

## How sandbox jobs get AWS credentials

Each job runs in its own Firecracker microVM. For each job, Ravion creates an IAM role in your execution environment's AWS account. The role's name starts with `rvn-ci-`, and its path is `/rvn-ci/`.

The sandbox host assumes that role with AWS STS and serves the credentials inside the microVM at the standard instance metadata endpoint, `169.254.169.254`. The paths match EC2's instance metadata service (IMDS). Each STS session lasts 15 minutes, and Ravion refreshes it automatically for as long as the job runs.

The AWS CLI and AWS SDKs running directly on the runner find these credentials with no setup:

```yaml theme={null}
jobs:
  whoami:
    runs-on: ravion/ee=github-actions/type=sandbox/cpu=auto/memory=auto/disk=auto/id=${{ github.run_id }}-${{ github.run_attempt }}-whoami-${{ strategy.job-index || 0 }}
    env:
      AWS_REGION: us-east-1
      AWS_DEFAULT_REGION: us-east-1
    steps:
      - run: aws sts get-caller-identity
```

Keep these in mind:

* **IMDSv2 only.** Requests must first get a session token with `PUT /latest/api/token`. IMDSv1 requests, which send no token, get `401 Unauthorized`. Current AWS CLI and SDK versions use IMDSv2 automatically.
* **No region.** The endpoint doesn't serve the instance's placement or region. Set `AWS_REGION` and `AWS_DEFAULT_REGION` yourself, as in the example above.
* **No policies by default.** The job role has no IAM policies attached by default. Calls such as `aws sts get-caller-identity` work, but calls that need permissions return `AccessDenied`. For jobs that need AWS access, use [GitHub OIDC](#use-github-oidc).

## Docker containers can't reach the endpoint

Docker containers in the job can't reach `169.254.169.254`. That includes:

* `services:` containers;
* `container:` jobs;
* `docker run` with the default network;
* `docker compose` stacks;
* any other Docker bridge network.

Requests from these containers don't fail with an error. They time out. Firecracker's metadata service sends every reply with an IP time to live (TTL) of 1, and the hop from the microVM's network interface to the Docker bridge drops it. Unlike the EC2 hop limit, this isn't configurable.

Containers started with `docker run --network host` share the microVM's network, so they do reach the endpoint.

| Where the code runs | Reaches the job's credentials |
| - | - |
| A `run` step on the runner | Yes |
| `docker run --network host` | Yes |
| `services:` and `container:` | No (times out) |
| `docker run` and `docker compose` on a bridge network | No (times out) |

AWS SDKs inside containers probe the endpoint when they look for credentials, and wait for that probe to time out. For example, the AWS SDK for Ruby probes synchronously, with timeouts of about 1 second to open the connection and 1 second to read it. That can make an application slow to start, or make its first AWS call hang.

## Get credentials into containers

### Stop SDKs from probing the endpoint

In every container that can't reach the endpoint, set `AWS_EC2_METADATA_DISABLED=true`. SDKs then skip the metadata endpoint instead of waiting for it to time out. Use this together with one of the options below to give the container credentials.

```yaml theme={null}
services:
  app:
    image: ghcr.io/acme/app:latest
    env:
      AWS_EC2_METADATA_DISABLED: "true"
      AWS_REGION: us-east-1
```

### Run services with Docker Compose

`services:` containers start before any step runs, so a step can't hand them credentials. Instead, start the containers with `docker compose` in a step, and pass the job's credentials in an env file:

```yaml theme={null}
jobs:
  e2e:
    runs-on: ravion/ee=github-actions/type=sandbox/cpu=auto/memory=auto/disk=auto/id=${{ github.run_id }}-${{ github.run_attempt }}-e2e-${{ strategy.job-index || 0 }}
    env:
      AWS_REGION: us-east-1
      AWS_DEFAULT_REGION: us-east-1
    steps:
      - uses: actions/checkout@v4

      - name: Export the job's AWS credentials
        run: |
          umask 077
          AWS_ENV_FILE="$RUNNER_TEMP/aws.env"
          aws configure export-credentials --format env-no-export > "$AWS_ENV_FILE"
          echo "AWS_ENV_FILE=$AWS_ENV_FILE" >> "$GITHUB_ENV"

      - name: Start the stack
        run: docker compose up -d --wait

      - run: npm run test:e2e

      - name: Remove the credentials file
        if: always()
        run: rm -f "$AWS_ENV_FILE"
```

```yaml compose.yaml theme={null}
services:
  app:
    image: ghcr.io/acme/app:latest
    env_file:
      - ${AWS_ENV_FILE:?}
    environment:
      AWS_REGION: us-east-1
      AWS_EC2_METADATA_DISABLED: "true"
```

The keys never appear in the workflow file or the job log. Don't run `docker compose config` or print the env file, because both show the values.

> [!WARNING]
>
> Exported credentials don't refresh inside the container. They expire within 15 minutes of export, and possibly sooner, because Ravion issues a new session before the old one expires. `AWS_CREDENTIAL_EXPIRATION` in the env file shows when. For containers that run longer, export again and restart them, or use [GitHub OIDC](#use-github-oidc) with a longer session.

Write the file under `$RUNNER_TEMP` and remove it when the job ends, as above, so it isn't kept in the job's [disk cache](https://www.ravion.com/docs/github-actions/caching.md#disk-cache).

### Use the host network for one-off containers

A `docker run` container on the host network reaches the endpoint directly, and SDKs inside it refresh credentials as usual. This example passes `AWS_REGION` from the job's `env`:

```yaml theme={null}
- run: docker run --rm --network host -e AWS_REGION amazon/aws-cli sts get-caller-identity
```

The container shares the microVM's network, so its ports are the runner's ports and it can reach anything the runner can.

### Use GitHub OIDC

For roles that depend on the branch, environment or repository, use [GitHub's OpenID Connect provider](https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services) with [`aws-actions/configure-aws-credentials`](https://github.com/aws-actions/configure-aws-credentials). This works on Ravion runners the same way as on GitHub-hosted runners, and lets you attach exactly the policies the job needs.

```yaml theme={null}
permissions:
  id-token: write
  contents: read

jobs:
  integration:
    runs-on: ravion/ee=github-actions/type=sandbox/cpu=auto/memory=auto/disk=auto/id=${{ github.run_id }}-${{ github.run_attempt }}-integration-${{ strategy.job-index || 0 }}
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/ci-integration
          aws-region: us-east-1

      - name: Run a container with the OIDC credentials
        run: |
          docker run --rm \
            -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN \
            -e AWS_REGION -e AWS_EC2_METADATA_DISABLED=true \
            ghcr.io/acme/app:latest npm run test:integration
```

The action exports the credentials as environment variables for later steps. Those take precedence over the job role from the metadata endpoint. The action also masks the values in the log.

To forward them to a container, pass each variable's name only (`-e AWS_ACCESS_KEY_ID`), or write them to an env file for `docker compose` as in [Run services with Docker Compose](#run-services-with-docker-compose). In a `container:` job, steps already run inside the container, so the action's credentials reach later steps there directly.

### Keep keys out of the job log

Values you write into `services.*.env`, `container.env` or a `docker run -e NAME=value` argument can show up in the job log. Masking only covers values GitHub knows are secret.

* Pass credentials with env files, or with `-e NAME` without a value.
* Reference credentials stored as GitHub secrets with `${{ secrets.NAME }}`. GitHub masks them.
* Mask any other value before anything can print it:

  ```bash theme={null}
  echo "::add-mask::$AWS_SECRET_ACCESS_KEY"
  echo "::add-mask::$AWS_SESSION_TOKEN"
  ```

## Compared with other runners

| Runner | Ambient AWS role | Reachable from Docker containers |
| - | - | - |
| Ravion sandbox | A role per job, served over IMDSv2 | No. See [the options above](#get-credentials-into-containers). |
| RunsOn | The EC2 instance role, shared by every job on the stack, over IMDSv2 with hop limit 2 | Yes |
| GitHub-hosted | None. Use GitHub OIDC. | Not applicable |

If you're moving from RunsOn, workflows whose `services:` or `container:` code relied on the instance role need one of the [options above](#get-credentials-into-containers). AWS calls made directly in steps find the job role instead of the instance role. The job role has [no policies by default](#how-sandbox-jobs-get-aws-credentials), so give those steps the permissions they need with [GitHub OIDC](#use-github-oidc).

## Next steps

> [card] [Runners](https://www.ravion.com/docs/github-actions/runners.md)
>
> Labels, sizing and requirements.

> [card] [Sandboxes](https://www.ravion.com/docs/concepts/sandboxes.md)
>
> What runs inside a sandbox.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.