# How to give AI coding agents AWS and Google Cloud access with OIDC instead of stored keys

A coding agent swaps a 15-minute OIDC token for AWS credentials, with no stored key. Real commands and output, what AWS rejected, and how other agents compare.

- Canonical: https://replicas.dev/resources/oidc-for-ai-agents

A coding agent that deploys code, reads logs, or checks a bucket needs cloud access. The usual way to give it that is to paste an AWS access key or a Google service account file into its environment variables. Then anything the agent runs can read the key, including a bad dependency or instructions hidden in an issue it was asked to fix. If the key leaks, it keeps working until someone notices and rotates it.

OpenID Connect (OIDC) removes the stored key. The agent's machine asks for a signed token that says which organization, environment, and workspace it belongs to. AWS or Google Cloud checks the signature against the issuer's public keys, compares the token with a trust policy you set up once, and returns credentials that expire within the hour. [GitHub Actions](https://docs.github.com/en/actions/concepts/security/openid-connect) has worked this way for years, and several cloud coding agents now do too.

How an agent gets AWS credentials without a stored key.

| Step | Who | Where | What happens | Detail |
| --- | --- | --- | --- | --- |
| 1 | Agent | Workspace | Asks for a token for one service | `replicas identity token --audience sts.amazonaws.com` |
| 2 | Replicas | api.replicas.dev | Signs a token that lasts 15 minutes | `sub: org:…:env:…:workspace:…` |
| 3 | AWS STS | Your AWS account | Checks the signature, audience and subject | Against the keys at /.well-known/jwks.json and your role’s trust policy |
| 4 | Agent | Workspace | Gets temporary credentials for one role | Keys that start with ASIA and expire in 1 hour |

| Where | What is set up |
| --- | --- |
| In the workspace | A CLI profile that runs the exchange, and no AWS key |
| In your AWS account, once | An OIDC provider for api.replicas.dev and a role whose trust policy names your organization |

Google Cloud and your own APIs follow the same steps, with Google’s STS or your service in step 3.

The steps below run that exchange with Replicas [workspace identity](https://docs.replicas.dev/features/workspaces/workspace-identity) and show the real output.

## How we tested

An AI agent, Claude Code running in a Replicas workspace, ran every command on this page on November 7, 2026 and wrote the guide. A person at Replicas reviewed it before it was published.

The AWS exchange used our own AWS account and an IAM role we had made earlier to test this feature. The role has no permissions attached, so it can prove who it is and do nothing else. We created no AWS resources for this guide. We had no Google Cloud project to test with, so the Google Cloud exchange is marked as not run. In the output below, IDs, account numbers, and email addresses are replaced with placeholders, and no full token is shown.

## Step 1: Get a token and check its signature

Inside a workspace, the `replicas` CLI asks for a token for one audience, which is the service that will receive it. For AWS, the audience is `sts.amazonaws.com`:

```bash
replicas identity token --audience sts.amazonaws.com > aws.jwt
```

That took 0.9 seconds. A JWT has three base64url parts: a header, the claims, and a signature. Decoded, the first two were:

```json
{ "alg": "RS256", "typ": "JWT", "kid": "0TktkOrKK4BeVPVe_MKXzs8SGYmtA9Z4kyhfhecVlrg" }
```

```json
{
  "iss": "https://api.replicas.dev",
  "sub": "org:<uuid>:env:<uuid>:workspace:<uuid>",
  "aud": "sts.amazonaws.com",
  "iat": 1794039286,
  "nbf": 1794039286,
  "exp": 1794040186,
  "jti": "<uuid>",
  "organization_id": "<uuid>",
  "workspace_id": "<uuid>",
  "environment_id": "<uuid>",
  "creator_id": "<uuid>",
  "creator_email": "<redacted>"
}
```

`exp` minus `iat` is 900 seconds, so the token lasts 15 minutes. AWS trust policies match on `sub`. The other claims are for services that can read them, such as Google Cloud and your own APIs. The [docs](https://docs.replicas.dev/features/workspaces/workspace-identity#token-claims) describe each one.

Anyone can write JSON that decodes like this, so the claims mean nothing until the signature checks out. The issuer publishes its public keys at fixed addresses:

```bash
curl -s https://api.replicas.dev/.well-known/openid-configuration \
  | jq '{issuer, jwks_uri, id_token_signing_alg_values_supported}'
```

```json
{
  "issuer": "https://api.replicas.dev",
  "jwks_uri": "https://api.replicas.dev/.well-known/jwks.json",
  "id_token_signing_alg_values_supported": ["RS256"]
}
```

`jwks.json` held one 2048-bit RSA key with the same `kid` as the token. We converted it to PEM and checked the signature with OpenSSL, without any Replicas code:

```bash
curl -s https://api.replicas.dev/.well-known/jwks.json -o jwks.json
node -e 'const c=require("crypto"),f=require("fs");const k=JSON.parse(f.readFileSync("jwks.json")).keys[0];f.writeFileSync("replicas-pub.pem",c.createPublicKey({key:k,format:"jwk"}).export({type:"spki",format:"pem"}))'
cut -d. -f1,2 aws.jwt | tr -d '\n' > signed.txt
cut -d. -f3 aws.jwt | tr -d '\n' | tr '_-' '/+' \
  | awk '{l=length($0)%4; if(l==2)$0=$0"=="; else if(l==3)$0=$0"="; print}' | base64 -d > sig.bin
openssl dgst -sha256 -verify replicas-pub.pem -signature sig.bin signed.txt
```

```text
Verified OK
```

Changing one character of `signed.txt` gave `Verification failure`. AWS, Google, and your own service all run this same check.

## Step 2: Trust the issuer in AWS

Someone with IAM access does this once. We didn't repeat it for this guide because the provider and role already existed in our account. [AWS's guide](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html) covers the console steps. It comes down to two things:

1. An IAM OIDC identity provider with the URL `https://api.replicas.dev` and the audience `sts.amazonaws.com`.
2. A role whose trust policy accepts tokens from that provider. This is the trust policy on the role we used, with our IDs replaced and a second pattern for an older token format left out:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Federated": "arn:aws:iam::<account>:oidc-provider/api.replicas.dev" },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": { "api.replicas.dev:aud": "sts.amazonaws.com" },
        "StringLike": { "api.replicas.dev:sub": "org:<organization_id>:env:*:workspace:*" }
      }
    }
  ]
}
```

For a provider like this, a trust policy can match the token's `aud` and `sub`, but not custom claims such as `creator_email` ([AWS](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html#condition-keys-wif)). So `sub` is where you narrow access. Put an environment ID in place of `env:*` to accept only workspaces from that environment. What the agent can do once it has the role is set by the role's permission policies, as with any role. Ours has none.

## Step 3: Exchange the token for AWS credentials

In the workspace, an AWS CLI profile runs the exchange whenever the CLI or an SDK needs credentials:

```bash
aws configure set credential_process \
  'replicas identity aws-credentials --role-arn arn:aws:iam::<account>:role/replicas-oidc-test --region us-east-1' \
  --profile replicas
```

You can ask the agent to set this up in plain words, or put the command in the environment's [start hook](https://docs.replicas.dev/features/environments#start-hooks) so every workspace starts with it. Any agent in the workspace uses the profile the same way, whether it's Claude Code, Codex, or a script, because it's an ordinary AWS CLI profile.

We removed the AWS keys from the shell first, so nothing else could be used:

```text
$ aws sts get-caller-identity
Unable to locate credentials. You can configure credentials by running "aws configure".

$ aws sts get-caller-identity --profile replicas
{
    "UserId": "AROA…:replicas",
    "Account": "<account>",
    "Arn": "arn:aws:sts::<account>:assumed-role/replicas-oidc-test/replicas"
}
```

The first call took 3.0 seconds, including the exchange. The second took 1.4 seconds, because the helper caches the credentials and only exchanges a new token when they have less than 15 minutes left. The cache is a file only the workspace user can read:

```json
{
  "Version": 1,
  "AccessKeyId": "ASIA…",
  "SecretAccessKey": "<hidden>",
  "SessionToken": "<hidden>",
  "Expiration": "2026-11-07T09:15:10Z"
}
```

Keys that start with `ASIA` are temporary. Long-lived IAM user keys start with `AKIA`. These expired one hour after the exchange.

The helper calls STS `AssumeRoleWithWebIdentity`. You can make the same call yourself, and it needs no AWS credentials:

```bash
aws sts assume-role-with-web-identity --region us-east-1 \
  --role-arn arn:aws:iam::<account>:role/replicas-oidc-test \
  --role-session-name demo --web-identity-token file://aws.jwt
```

```json
{
  "Credentials": {
    "AccessKeyId": "ASIA…",
    "SecretAccessKey": "<hidden>",
    "SessionToken": "<hidden>",
    "Expiration": "2026-11-07T09:15:32Z"
  },
  "SubjectFromWebIdentityToken": "org:<uuid>:env:<uuid>:workspace:<uuid>",
  "AssumedRoleUser": {
    "AssumedRoleId": "AROA…:demo",
    "Arn": "arn:aws:sts::<account>:assumed-role/replicas-oidc-test/demo"
  },
  "PackedPolicySize": 24,
  "Provider": "arn:aws:iam::<account>:oidc-provider/api.replicas.dev",
  "Audience": "sts.amazonaws.com"
}
```

### What AWS accepted and rejected

We then sent tokens and requests that should fail. AWS refused all of them except the expired token, which it accepted for up to five minutes after it expired:

| What we sent | What AWS returned |
|---|---|
| A token minted for `https://api.example.com` instead of `sts.amazonaws.com` | `InvalidIdentityToken` |
| A real token with `sub` edited to another organization | `InvalidIdentityToken`, because the signature no longer matched |
| A token that had expired 2 minutes 20 seconds earlier | New credentials. AWS accepted it |
| The same token, 4 minutes 50 seconds after it expired | New credentials again |
| The same token, 5 minutes 47 seconds after it expired | `ExpiredTokenException: Token expired: current date/time 1794040233 must be before the expiration date/time 1794040186` |
| A valid token with `--source-identity`, on a role whose trust policy doesn't allow `sts:SetSourceIdentity` | `AccessDenied` |
| `aws s3 ls` with the role's credentials | `AccessDenied`: no identity-based policy allows `s3:ListAllMyBuckets` |

The token proves which workspace is calling, and the role decides what that workspace may do. Ours allows nothing, so S3 refused. If you turn on [source identity](https://docs.replicas.dev/features/workspaces/workspace-identity#aws), AWS records the workspace ID on every action in CloudTrail. The role's trust policy has to allow it first, as the sixth row shows.

The expired token surprised us. The "current date/time" in AWS's error was exactly 300 seconds behind the real time, which suggests STS allows five minutes for clock differences. At AWS, a token that lasts 15 minutes can be exchanged for up to 20, and the credentials it returns last another hour. We found no AWS page that documents this window.

## Google Cloud (not run end to end)

Google Cloud uses [Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers). Someone with IAM access creates a workload identity pool with an OIDC provider for `https://api.replicas.dev`, maps `google.subject` to `assertion.workspace_id`, adds a condition such as `assertion.organization_id == '<organization_id>'`, and grants roles. Unlike AWS, Google's conditions can read any claim, so you can also limit access by `environment_id` or `creator_email`. [Our docs](https://docs.replicas.dev/features/workspaces/workspace-identity#google-cloud) have the commands.

In the workspace, `gcloud` and Google's client libraries run a command to get a token whenever they need one. This is the command they run, and what it printed for us:

```bash
replicas identity token --format gcp \
  --audience "https://iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/replicas/providers/replicas"
```

```json
{
  "version": 1,
  "success": true,
  "token_type": "urn:ietf:params:oauth:token-type:jwt",
  "id_token": "eyJhbGciOiJS…<redacted>",
  "expiration_time": 1794040250
}
```

That is the format Google expects from an executable credential source. We had no Google Cloud project for this test, so we didn't send the token to Google's STS.

## Your own API or MCP server

The same token works for any service that can check a JWT. As a stand-in for an internal deploy API, we ran this Node server in the workspace. It has no dependencies, keeps the issuer's keys for 5 minutes, and fetches them again sooner when a token names a key it doesn't have:

```js
import http from 'node:http';
import { createPublicKey, createVerify } from 'node:crypto';

const ISSUER = 'https://api.replicas.dev';
const AUDIENCE = 'https://deploys.internal.example';
const ALLOWED_ORGS = new Set(process.env.ALLOWED_ORGS.split(','));

let keys = new Map();
let keysFetchedAt = 0;
let refresh = null;

async function fetchKeys() {
  const config = await (await fetch(`${ISSUER}/.well-known/openid-configuration`)).json();
  if (!config.jwks_uri.startsWith(`${ISSUER}/`)) throw new Error('Unexpected jwks_uri');
  const jwks = await (await fetch(config.jwks_uri)).json();
  keys = new Map(jwks.keys.map((jwk) => [jwk.kid, createPublicKey({ key: jwk, format: 'jwk' })]));
  keysFetchedAt = Date.now();
}

async function publicKey(kid) {
  const age = Date.now() - keysFetchedAt;
  if (age > 300_000 || (!keys.has(kid) && age > 60_000)) {
    refresh ??= fetchKeys().finally(() => { refresh = null; });
    await refresh;
  }
  return keys.get(kid);
}

const decode = (part) => JSON.parse(Buffer.from(part, 'base64url'));

async function verify(token) {
  const [header, payload, signature] = token.split('.');
  const { alg, kid } = decode(header);
  const key = alg === 'RS256' && (await publicKey(kid));
  const signed = createVerify('RSA-SHA256').update(`${header}.${payload}`);
  if (!key || !signed.verify(key, Buffer.from(signature, 'base64url'))) {
    throw new Error('Invalid signature');
  }
  const claims = decode(payload);
  const now = Date.now() / 1000;
  if (claims.iss !== ISSUER) throw new Error('Wrong issuer');
  if (claims.aud !== AUDIENCE) throw new Error('Wrong audience');
  if (claims.exp + 30 < now || claims.nbf - 30 > now) throw new Error('Expired or not yet valid');
  if (!ALLOWED_ORGS.has(claims.organization_id)) throw new Error('Organization not allowed');
  return claims;
}

http.createServer(async (req, res) => {
  const token = req.headers.authorization?.match(/^Bearer (\S+)$/)?.[1];
  if (!token) return res.writeHead(401).end('Missing token');
  try {
    const { workspace_id, environment_id, creator_email } = await verify(token);
    res.end(JSON.stringify({ workspace_id, environment_id, creator_email }));
  } catch (error) {
    res.writeHead(401).end(error.message);
  }
}).listen(Number(process.env.PORT ?? 8787));
```

We saved it as `server.mjs` and started it with our organization ID as the only allowed organization:

```bash
ALLOWED_ORGS=<organization_id> node server.mjs
```

The agent called it with a fresh token for that audience:

```bash
replicas identity exec --audience https://deploys.internal.example -- \
  sh -c 'curl -s -H "Authorization: Bearer $REPLICAS_WORKSPACE_TOKEN" http://localhost:8787/deploys'
```

| What we sent | Response |
|---|---|
| A token for `https://deploys.internal.example` | `200` with the workspace, environment, and creator's email |
| No token | `401 Missing token` |
| A token minted for `https://api.example.com` | `401 Wrong audience` |
| A real token with `organization_id` edited | `401 Invalid signature` |
| A token with `"alg": "none"` and no signature | `401 Invalid signature` |
| A valid token, with our organization left off the allowlist | `401 Organization not allowed` |
| A token that had expired 53 seconds earlier | `401 Expired or not yet valid` |

For an MCP server, you don't need the CLI. When you add an HTTP MCP server to a Replicas environment, choose **Workspace identity token** as its authentication, and Replicas attaches a current token to every request ([docs](https://docs.replicas.dev/features/workspaces/workspace-identity#mcp-servers)). The server checks it the same way. We didn't test the MCP path for this guide.

## How other cloud coding agents get cloud credentials

Every cloud coding agent we checked can store secrets and hand them to the agent's machine. Five of the nine below also issue their own OIDC token, so you can set up keyless access to AWS or Google Cloud the way this guide does, and GitHub Copilot can use GitHub Actions' token. If you use more than one agent, each one with its own issuer needs its own provider and trust policy in your cloud account.

We checked each vendor's own docs on November 7, 2026. Agents are in alphabetical order:

| Agent | Stored secrets | Own OIDC token |
|---|---|---|
| Claude Code on the web | Environment variables, which anyone who uses the environment can read. Network secrets, which Anthropic's proxy adds to requests for hosts you list, so the session never sees them ([Anthropic](https://code.claude.com/docs/en/cloud-environments)) | Not documented for Anthropic-hosted environments. Self-hosted environments, in beta on Team and Enterprise, run on your machines, where a wrapper script can provision short-lived credentials per session ([Anthropic](https://code.claude.com/docs/en/self-hosted-environments-configuration)) |
| Codex cloud | Environment variables, and network secrets that a proxy swaps in on HTTPS requests ([OpenAI](https://learn.chatgpt.com/docs/environments/cloud-environments)) | Yes, set per environment. OpenAI lists supported services for AWS, Google Cloud, and Azure. RDS and Cloud SQL IAM logins, and Azure Resource Manager, Blob Storage, and Container Registry, are not yet supported ([OpenAI](https://learn.chatgpt.com/docs/environments/cloud-environments#access-cloud-resources-with-oidc)) |
| Cursor cloud agents | Secrets as environment variables. Runtime secrets are redacted from transcripts and commits but can still be read in the terminal ([Cursor](https://cursor.com/docs/cloud-agent/security-network)) | Yes. Issuer `https://api.cursor.com`, tokens last 5 minutes. AWS has a worked example. Google Cloud, Azure, and Vault are listed as compatible ([Cursor](https://cursor.com/docs/cloud-agent/identity)) |
| Devin | Secrets given to commands as environment variables when needed, scoped to the organization, a person, a repository, or a session ([Cognition](https://docs.devin.ai/product-guides/secrets)) | Yes. Issuer `https://app.devin.ai`, with setup actions for AWS, Google Cloud, Vault, and JFrog. Token lifetime isn't published ([Cognition](https://docs.devin.ai/product-guides/oidc)) |
| Factory | Personal and organization secrets, which become environment variables. Not available during environment builds ([Factory](https://docs.factory.com/environments/prerequisites)) | None documented |
| GitHub Copilot cloud agent | Agents secrets, given to the agent as environment variables ([GitHub](https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/customize-cloud-agent/configure-secrets-and-variables)) | Through GitHub Actions. When the agent runs on Actions, its setup steps can request Actions' OIDC token. GitHub documents an Azure login this way ([GitHub](https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/configure-mcp-servers)) |
| Google Jules | Repository environment variables, turned on per task ([Google](https://jules.google/docs/changelog/2025-10-01)) | None documented |
| Ona | Organization, project, and user secrets ([Ona](https://ona.com/docs/ona/configuration/secrets/overview)) | Yes. Issuer `https://app.gitpod.io`, with AWS and Google Cloud guides ([Ona](https://ona.com/docs/ona/identity/aws-oidc)). Enterprise plan ([Ona](https://ona.com/docs/ona/configuration/oidc)) |
| Replicas | Environment variables, which can also sync from Doppler or Infisical ([Replicas](https://docs.replicas.dev/features/environments#variables)) | Yes. Issuer `https://api.replicas.dev`, tokens last 15 minutes. AWS and Google Cloud guides, and MCP servers. Team and Enterprise plans, and trials ([Replicas](https://docs.replicas.dev/features/workspaces/workspace-identity)) |

### Where Replicas falls short

Workspace identity is only on Team and Enterprise plans and trials. Our tokens last 15 minutes, three times as long as Cursor's 5, and an issued token can't be revoked before it expires. Our docs cover AWS and Google Cloud but not Azure, and we haven't tested Azure.

For an API that only accepts a fixed key, Replicas puts the key in an environment variable the agent can read. Claude Code on the web and Codex cloud can keep that kind of key in a proxy outside the agent's machine, and Replicas has no equivalent yet. The [security page](/security) lists the controls Replicas does have, by plan.

## What OIDC doesn't protect against

Short-lived tokens remove one risk: a long-lived key sitting in the agent's environment. They don't make the agent trustworthy, and they don't limit what it does with access you gave it.

| Risk | Does a short-lived token help? | What to do |
|---|---|---|
| The agent misuses access it was given, for example after reading instructions planted in an issue | No. It can do whatever the role allows | Give the role only what the task needs, read-only where you can. Use a separate role for each environment |
| Credentials are copied out of the workspace during a session | Partly. AWS accepts the token for up to 5 minutes after it expires, and the credentials it returns last an hour. Both work from anywhere until then | If you suspect a leak, [revoke the role's active sessions](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_revoke-sessions.html) in IAM |
| Anyone who can start a workspace in your organization gets a token that matches an organization-wide trust policy | No | Match one environment in `sub`, and limit who can use that environment |
| You remove someone's access, but they already have a token | Partly. Replicas stops issuing tokens right away, but a token already issued works until it expires | Archive the workspace to stop new tokens, and revoke the role's sessions in AWS if it matters |
| Data the agent reads goes to its model provider | No | Don't give the role access to data you can't send to your model provider. Our [HIPAA guide](/resources/ai-coding-agents-hipaa) covers where that data goes |
| A service only accepts a fixed API key | No | That key still lives in the workspace. Scope it and rotate it |

If you're rolling this out across a company, [Replicas Enterprise](/enterprise) adds dedicated or self-hosted deployment and SCIM provisioning. The [workspace identity docs](https://docs.replicas.dev/features/workspaces/workspace-identity) have the full setup for AWS, Google Cloud, MCP servers, and your own APIs.

