AWS SSO credentials in containers

harnessed works fully without AWS. This guide is for operators who use aws-sso-cli on the host and want their stacks to make authenticated AWS calls — without baking any credential into an image, copying ~/.aws-sso into the container, or leaking a long-lived SSO token past the host.

It is opt-in per stack and env-only, the same posture as secrets and the git-credential forward: a container gets AWS authority only when the stack asks for it, and only short-lived STS credentials ever cross the boundary.

Why the obvious approach doesn’t work

The profiles aws-sso-cli writes into ~/.aws/config look like:

[profile pinnacle-dev]
credential_process = aws-sso -S "pinnacle" process --arn arn:aws:iam::…:role/AWSAdministratorAccess
region = us-east-1

That credential_process needs the aws-sso binary, the ~/.aws-sso/ config, the encrypted secure store, and a live SSO token — all inside the container. Mounting that surface into a container is exactly what harnessed forbids: it hands the container standing authority to mint credentials for every role you can assume, and puts secret material one image layer away.

The mechanism: the aws-sso ECS server (a credential proxy)

aws-sso-cli ships a proxy built for this. aws-sso ecs server runs on the host and speaks the same HTTP protocol AWS uses to inject ECS task-role credentials. Any AWS SDK/CLI that sees AWS_CONTAINER_CREDENTIALS_FULL_URI fetches short-lived STS credentials from that endpoint — no profile, no binary, no SSO token required in the client.

host:  aws-sso ecs server         long-running; holds STS creds for the loaded role in memory
host:  aws-sso ecs load            you pick a role → it fills the server's "default slot"
ctr:   AWS SDK → http://host.containers.internal:4144/ → temporary STS creds

This is the exact analog of harnessed’s SSH-agent forward: the container gets a reachable endpoint, never the key material. The server itself has no access to the secure store — the only credentials it can serve are those you explicitly aws-sso ecs load into it.

Quickstart

1. Start the credential server on the host

harnessed aws-sso serve

On first run this generates a random bearer token, loads it into the aws-sso secure store, and records it at ~/.config/harnessed/aws-sso-ecs.token (mode 0600) so the launcher can inject it. Then it starts aws-sso ecs server in the foreground — leave this terminal running.

Prerequisite: aws-sso must be on your PATH (see the aws-sso-cli install docs) and your SSO instances configured in ~/.aws-sso/config.yaml.

2. Load the role the containers should use

In another terminal:

aws-sso ecs load          # interactive role picker → fills the default slot

Every container consuming the server uses whichever role is loaded here. To switch the role for all containers, just run aws-sso ecs load again and pick another — no relaunch needed.

3. Opt the stack in

In the stack’s stack.yaml:

name: my-stack
harness: claude
forward_aws_sso: true

4. Launch

harnessed my-stack

Your project’s AWS code (boto3, aws-sdk-js/go/rust, …) picks up the credentials automatically — every AWS SDK reads AWS_CONTAINER_CREDENTIALS_FULL_URI natively, so no application code change is needed. The SDK is whatever your project already depends on.

To confirm from a shell you need the aws CLI, which is not in the base image. Either check from Python if boto3 is available:

python -c "import boto3; print(boto3.client('sts').get_caller_identity()['Arn'])"

or add the CLI once (mise-managed) and rebuild — see Getting the aws CLI in the container below:

aws sts get-caller-identity

Either should report the role you loaded in step 2.

Getting the aws CLI in the container

The container never needs aws-sso, and it only needs an AWS SDK if your workload calls AWS — that SDK ships with your project’s own dependencies (pnpm add @aws-sdk/…, uv add boto3), so it is already present at runtime and reads the injected endpoint with no extra setup.

The aws CLI is a separate binary and is not baked into the base image. If you want it for ad-hoc shell commands, add one line to your user-owned extra-tools list and rebuild:

echo 'aws-cli   # AWS CLI (aqua:aws/aws-cli)' >> ~/.config/harnessed/extra-tools.txt
harnessed build            # rebuild the base image with the extra tool

How it works

When a stack sets forward_aws_sso: true, the launcher reads the bearer token from ~/.config/harnessed/aws-sso-ecs.token and injects two environment variables into the pod (env only — never written to a profile, an image layer, or a repo file):

AWS_CONTAINER_CREDENTIALS_FULL_URI=http://host.containers.internal:4144/
AWS_CONTAINER_AUTHORIZATION_TOKEN=Bearer <token>
  • host.containers.internal is how a harnessed pod reaches host services (the same name the hatago MCP hub uses to proxy host processes). The server must therefore bind an address the container can reach — harnessed aws-sso serve defaults to --bind-ip 0.0.0.0 for exactly this reason. 127.0.0.1 keeps it host-only but is then unreachable from containers.
  • The bearer token gates the endpoint. Because the server listens off-loopback so containers can reach it, the token is what stops any other local (or on-network) process from using your credentials. harnessed aws-sso serve sets one up automatically before starting the server.
  • No-op when unconfigured. If the token file is absent (you never ran harnessed aws-sso serve), the launcher injects nothing — a forward_aws_sso stack still launches fine, the AWS SDK simply finds no credentials. So the flag is safe to commit in a shared catalog.

The token is a single source of truth: harnessed aws-sso serve writes it, the launcher reads it, and it also lives in the aws-sso secure store where the server reads it. You never type or paste it.

Security model

  • Short-lived, but not touch-gated. The endpoint serves temporary STS credentials, not long-lived keys, and only for the one role currently loaded. But unlike the SSH signing agent (which prompts for a 1Password approval / YubiKey touch on every use), these credentials are usable by anything that can reach the endpoint with the token. That is why AWS forwarding is opt-in per stack, whereas the SSH agent is auto-forwarded whenever it is live.
  • The bearer token is the boundary. Choose to bind loopback-only (--bind-ip 127.0.0.1, no container access) or accept off-loopback binding gated by a strong random token — which is what the wizard generates. Treat the token file as a secret; it is stored 0600.
  • Nothing is baked. No aws-sso binary, no ~/.aws-sso, no SSO token, and no long-lived AWS key ever enters a container or an image. The credentials arrive as a per-launch -e and expire.
  • TLS caveat. aws-sso-cli supports TLS on the ECS server, but a bug in the AWS SDK prevents the SDK from trusting anything but a publicly-CA-signed cert on this endpoint. In practice the endpoint is bearer-token-over-HTTP on a host-local interface — acceptable for a developer laptop; do not expose the bind IP to an untrusted network.

Switching and unloading roles

aws-sso ecs load              # change the default-slot role (all containers follow)
aws-sso ecs profile           # print the currently loaded role
aws-sso ecs unload            # remove the default-slot credentials

Multiple roles at once (advanced)

harnessed wires containers to the server’s default slot (http://host.containers.internal:4144/), so one role is active at a time across all forward_aws_sso stacks — switch it host-side with aws-sso ecs load. aws-sso-cli can also hold several roles in named slots (aws-sso ecs load --slotted, consumed via …/slot/<ProfileName>); harnessed does not target named slots today. If you need that, set AWS_CONTAINER_CREDENTIALS_FULL_URI yourself in the stack’s env and file an issue.

Troubleshooting

AWS calls inside the container fail with “unable to locate credentials” or hang:

  • Server not running? The harnessed aws-sso serve terminal must stay open. Check the host: curl -H "Authorization: Bearer $(cat ~/.config/harnessed/aws-sso-ecs.token)" http://127.0.0.1:4144/ should return a credentials JSON (run it on the host, where loopback works).
  • No role loaded? Run aws-sso ecs load. The server exposes an unauthenticated GET /healthcheck that returns 200 only when the default slot has valid credentials.
  • Flag not set? The stack needs forward_aws_sso: true. Confirm the env reached the container: podman exec <instance> printenv AWS_CONTAINER_CREDENTIALS_FULL_URI.
  • Token file missing? If you started the server by hand instead of via harnessed aws-sso serve, the launcher has no token to inject. Run the wizard once to generate and record it.
  • Bind IP unreachable? If you started the server with --bind-ip 127.0.0.1, containers can’t reach it. Restart with the default 0.0.0.0 (token-gated).

Reference

PieceLocation
Stack opt-in flagforward_aws_sso: true in stack.yaml
Host commandharnessed aws-sso serve [--port 4144] [--bind-ip 0.0.0.0]
Bearer token file~/.config/harnessed/aws-sso-ecs.token (0600)
Injected into containerAWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_CONTAINER_AUTHORIZATION_TOKEN
Underlying toolaws-sso-cli ECS server