Handling Secrets

Orbs often need credentials for package registries, cloud providers, databases, and internal services. You can store a secret in Amp settings or let the orb use its temporary OIDC identity.

Use OIDC when the service supports it. OIDC gives the orb a temporary credential with a narrow scope, so you do not have to store a permanent key in Amp. Use Amp secrets for services that need a fixed token or password.

Secrets and Environment Variables

Add values from one of these settings pages:

  • Workspace settings apply to every orb in the workspace, including orbs for personal projects.
  • Project settings apply only to orbs for that project.
  • Personal settings apply only to your orbs.

Personal values override project values with the same name. Project values override workspace values.

Use an environment variable for configuration that is safe to display, such as a region or feature flag. Mark passwords, API tokens, private keys, and URLs with embedded credentials as secrets. Amp hides secret values after you save them.

You can also ask Amp to manage values or use amp secrets from the CLI. Run this command to see the available operations:

amp secrets --help

Amp records who created and last changed each entry. The settings pages and amp secrets history show this history without showing the values.

Refresh Secrets

New orbs receive the current values when they start. To refresh a running orb, run this command from the orb’s Terminal:

amp orb restart-processes

Amp waits for the current command to finish. It then reloads the values and restarts the executor and managed services.

OIDC

How It Works

Amp orbs can mint signed OpenID Connect ID tokens. A cloud provider or private service can verify the token and grant temporary access based on the workspace, project, user, and thread that owns the orb.

Inside an orb, mint a token for one intended service:

amp orb id-token --audience https://logs.example.com

The command prints only the token to standard output, so it works with command substitution and credential helpers. The default lifetime is 10 minutes. You can request between 60 seconds and one hour:

amp orb id-token \
  --audience https://logs.example.com \
  --ttl-seconds 3600

The token uses RS256 and these public endpoints:

  • Issuer: https://ampcode.com/api/workload-identity
  • Discovery document: https://ampcode.com/api/workload-identity/.well-known/openid-configuration
  • JWKS: https://ampcode.com/api/workload-identity/jwks.json

The token contains the standard iss, aud, sub, iat, exp, and jti claims. It also contains these Amp claims:

ClaimMeaning
workspace_idThe Amp workspace. It is absent for a personal thread.
project_idThe Amp project. It is absent when the thread has no project.
user_idThe user who created the thread.
thread_idThe thread and orb identity.
email, email_verifiedThe current account email and its verification state.
token_useThis is exchanged for tokens printed by amp orb id-token.

The default subject scope is thread. For a workspace-owned project, its subject is:

workspace:<workspace-id>:project:<project-id>:user:<user-id>:thread:<thread-id>

Each broader scope removes the narrower layers on the right:

ScopeSubject
threadworkspace:<workspace-id>:project:<project-id>:user:<user-id>:thread:<thread-id>
userworkspace:<workspace-id>:project:<project-id>:user:<user-id>
projectworkspace:<workspace-id>:project:<project-id>
workspaceworkspace:<workspace-id>

Amp omits unavailable workspace or project layers. The project scope for a personal project uses the owner-qualified subject described below.

For example, some providers only match the standard sub claim, such as Entra ID federated credentials with exact subject matching. Use --subject-scope workspace to produce a subject that is identical across all orbs in the same workspace:

amp orb id-token \
  --audience api://AzureADTokenExchange \
  --subject-scope workspace

The resulting sub is:

workspace:<workspace-id>

The workspace_id, project_id, user_id, and thread_id claims are still present as custom claims on the token. Only the sub shape changes.

Every orb in the same workspace can mint a token with this subject, including orbs for personal projects. The token does not encode the user or thread, so a provider that matches only sub cannot distinguish which member or orb produced it. Scope the provider’s role assignment to the minimum resource set.

Revocation is delayed: the token exchange endpoint trusts the orb’s existing request credential and does not recheck workspace membership at exchange time. A member removed from the workspace can continue minting workspace-scoped tokens until their request credential expires, which can take up to 24 hours.

Use --subject-scope project for a stable identity shared by all orbs for one project:

amp orb id-token \
  --audience https://logs.example.com \
  --subject-scope project

The subject includes the project owner so personal and workspace-owned projects cannot collide:

workspace:<workspace-id>:project:<project-id>
user:<owner-user-id>:project:<project-id>

Amp checks current project access each time it mints a project-scoped token. Moving a project to a different owner changes its subject. Update the identity configured at the receiving service when project ownership changes.

The workload identity request credential is available at /run/amp/workload-identity-request-token during project pre-clone, pre-setup, and .agents/setup scripts. During setup it can mint project- or workspace-scoped tokens only. Amp removes the file after setup and before saving a project snapshot. When Amp restores the snapshot, it writes a fresh runtime request credential before .agents/resume runs.

Project lifecycle scripts are trusted code. Amp removes its request credential, but it does not find or remove tokens, provider credentials, or login caches that a script copies elsewhere. Do not save exchanged tokens or provider login state in files that become part of the snapshot.

Design the Trust Policy

Choose a different audience for each service that trusts Amp. Configure Amp and the service with the exact same value. Use a URL or URN that names the integration, such as https://logs.example.com or urn:example:aws:logs:123456789012.

An audience is an identifier, not a password. Any orb can request the same audience, so the service must also restrict the identity claims:

  • Match workspace_id for workspace access.
  • Match workspace_id and project_id for one project.
  • Add user_id when only specific users should have access.
  • Match thread_id for one orb.
  • Require token_use to equal exchanged when the provider supports custom claim conditions.

Use immutable IDs instead of email for access rules. The sub claim works with providers that only expose standard OIDC claims. It identifies one thread by default. Use --subject-scope user, --subject-scope project, or --subject-scope workspace to share an identity across threads.

Inspect a token before you configure the provider:

TOKEN="$(amp orb id-token --audience test-only)"
TOKEN="$TOKEN" python3 - <<'PY'
import base64
import json
import os

payload = os.environ['TOKEN'].split('.')[1]
payload += '=' * (-len(payload) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(payload)), indent=2))
PY
unset TOKEN

This command decodes the payload but does not verify it. A service must verify the signature, issuer, audience, and expiry before it trusts any claim.

Google Cloud

Google Cloud Workload Identity Federation can exchange an Amp ID token for Google credentials. The following example gives every orb in one Amp project read only access to logs and metrics without a service account key.

Run this setup from a trusted workstation where you are signed in as a Google Cloud IAM administrator:

export GCP_IDENTITY_PROJECT_ID='<project-that-owns-the-pool>'
export GCP_IDENTITY_PROJECT_NUMBER='<numeric-project-number>'
export GCP_RESOURCE_PROJECT_ID='<project-containing-logs-and-metrics>'
export POOL_ID='amp-orbs'
export PROVIDER_ID='amp'
export AMP_WORKSPACE_ID='<amp-workspace-uuid>'
export AMP_PROJECT_ID='<amp-project-uuid>'
export AMP_GCP_AUDIENCE="urn:amp:gcp:${GCP_RESOURCE_PROJECT_ID}"

Create a pool and an OIDC provider. thread_id is used as google.subject because it is unique and stays below Google Cloud’s 127-byte subject limit; Amp’s composite sub can be longer.

gcloud iam workload-identity-pools create "$POOL_ID" \
  --project="$GCP_IDENTITY_PROJECT_ID" \
  --location=global \
  --display-name='Amp orbs'

gcloud iam workload-identity-pools providers create-oidc "$PROVIDER_ID" \
  --project="$GCP_IDENTITY_PROJECT_ID" \
  --location=global \
  --workload-identity-pool="$POOL_ID" \
  --issuer-uri='https://ampcode.com/api/workload-identity' \
  --allowed-audiences="$AMP_GCP_AUDIENCE" \
  --attribute-mapping='google.subject=assertion.thread_id,attribute.workspace_id=assertion.workspace_id,attribute.project_id=assertion.project_id,attribute.user_id=assertion.user_id' \
  --attribute-condition="assertion.workspace_id == '${AMP_WORKSPACE_ID}' && assertion.project_id == '${AMP_PROJECT_ID}' && assertion.token_use == 'exchanged'"

Grant only the roles the orbs need. Use the number of the project that owns the identity pool, even when the resources are in another project:

AMP_PROJECT_PRINCIPAL_SET="principalSet://iam.googleapis.com/projects/${GCP_IDENTITY_PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_ID}/attribute.project_id/${AMP_PROJECT_ID}"

gcloud projects add-iam-policy-binding "$GCP_RESOURCE_PROJECT_ID" \
  --member="$AMP_PROJECT_PRINCIPAL_SET" \
  --role=roles/logging.viewer

gcloud projects add-iam-policy-binding "$GCP_RESOURCE_PROJECT_ID" \
  --member="$AMP_PROJECT_PRINCIPAL_SET" \
  --role=roles/monitoring.viewer

Commit a reviewed credential helper so Google client libraries can refresh the Amp token. For example, create scripts/amp-gcp-identity:

#!/usr/bin/env bash
set -euo pipefail

: "${AMP_GCP_AUDIENCE:?Set AMP_GCP_AUDIENCE}"
token="$(amp orb id-token --audience "$AMP_GCP_AUDIENCE")"

AMP_ID_TOKEN="$token" python3 - <<'PY'
import base64
import json
import os

token = os.environ['AMP_ID_TOKEN']
payload = token.split('.')[1]
payload += '=' * (-len(payload) % 4)
expiration = json.loads(base64.urlsafe_b64decode(payload))['exp']
print(json.dumps({
    'version': 1,
    'success': True,
    'token_type': 'urn:ietf:params:oauth:token-type:id_token',
    'id_token': token,
    'expiration_time': expiration,
}))
PY

Make it executable and create an external account credential file:

chmod +x scripts/amp-gcp-identity
mkdir -p .amp

export AMP_GCP_AUDIENCE='urn:amp:gcp:<resource-project-id>'
export GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES=1
export GOOGLE_APPLICATION_CREDENTIALS="$PWD/.amp/gcp-external-account.json"

gcloud iam workload-identity-pools create-cred-config \
  "projects/<identity-project-number>/locations/global/workloadIdentityPools/amp-orbs/providers/amp" \
  --executable-command="$PWD/scripts/amp-gcp-identity" \
  --subject-token-type=urn:ietf:params:oauth:token-type:id_token \
  --output-file="$GOOGLE_APPLICATION_CREDENTIALS"

gcloud auth login --cred-file="$GOOGLE_APPLICATION_CREDENTIALS"
gcloud logging read 'severity>=ERROR' \
  --project='<resource-project-id>' \
  --limit=20

Application Default Credentials libraries use GOOGLE_APPLICATION_CREDENTIALS directly. Set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES=1 only for a helper you control because the external account file grants permission to run that command.

If a Google API does not support direct federated principals, use service account impersonation. Grant the principal set roles/iam.workloadIdentityUser on a narrow service account and pass its email with --service-account=<service-account-email> when you create the credential file.

AWS

An AWS IAM OIDC provider can let selected Amp orbs assume roles with temporary credentials. This example gives every orb in one Amp project read only access to CloudWatch Logs without an AWS access key.

AWS IAM ignores custom claims in OIDC tokens, so its trust policy must restrict the standard aud and sub claims. Set these values on an authenticated administrator workstation:

export AWS_ACCOUNT_ID='<12-digit-account-id>'
export AWS_OIDC_AUDIENCE="urn:amp:aws:logs:${AWS_ACCOUNT_ID}"
export AMP_WORKSPACE_ID='<amp-workspace-uuid>'
export AMP_PROJECT_ID='<amp-project-uuid>'

Create the OIDC provider. The thumbprint list is optional in current IAM; IAM validates the issuer’s TLS certificate chain against trusted root certificate authorities.

aws iam create-open-id-connect-provider \
  --url 'https://ampcode.com/api/workload-identity' \
  --client-id-list "$AWS_OIDC_AUDIENCE"

An AWS account can have only one OIDC provider for an issuer. If the Amp provider already exists, add the audience to it:

aws iam add-client-id-to-open-id-connect-provider \
  --open-id-connect-provider-arn "arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/ampcode.com/api/workload-identity" \
  --client-id "$AWS_OIDC_AUDIENCE"

Create amp-logs-trust.json. AWS condition keys omit the https:// prefix but keep the issuer path:

cat > amp-logs-trust.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/ampcode.com/api/workload-identity"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "ampcode.com/api/workload-identity:aud": "${AWS_OIDC_AUDIENCE}",
          "ampcode.com/api/workload-identity:sub": "workspace:${AMP_WORKSPACE_ID}:project:${AMP_PROJECT_ID}"
        }
      }
    }
  ]
}
EOF

Create the role and attach the managed read only policy for CloudWatch Logs:

aws iam create-role \
  --role-name AmpOrbLogsReadOnly \
  --assume-role-policy-document file://amp-logs-trust.json

aws iam attach-role-policy \
  --role-name AmpOrbLogsReadOnly \
  --policy-arn arn:aws:iam::aws:policy/CloudWatchLogsReadOnlyAccess

The managed policy can read all CloudWatch Logs in the account. Replace it with an inline policy for specific log groups when the orbs need less access.

Inside an orb, mint a token into a private temporary file. AWS CLI v2 and the AWS SDK credential chain can use it with AssumeRoleWithWebIdentity:

TOKEN_FILE="$(mktemp)"
trap 'rm -f "$TOKEN_FILE"' EXIT
chmod 600 "$TOKEN_FILE"

export AWS_ACCOUNT_ID='<12-digit-account-id>'
export AWS_OIDC_AUDIENCE="urn:amp:aws:logs:${AWS_ACCOUNT_ID}"

amp orb id-token \
  --audience "$AWS_OIDC_AUDIENCE" \
  --subject-scope project \
  > "$TOKEN_FILE"

export AWS_ROLE_ARN="arn:aws:iam::${AWS_ACCOUNT_ID}:role/AmpOrbLogsReadOnly"
export AWS_ROLE_SESSION_NAME='amp-orb-logs'
export AWS_WEB_IDENTITY_TOKEN_FILE="$TOKEN_FILE"
export AWS_REGION='<region>'
export LOG_GROUP_NAME='<log-group-name>'

aws sts get-caller-identity
aws logs tail "$LOG_GROUP_NAME" \
  --region "$AWS_REGION" \
  --since 1h \
  --format short

Do not turn on shell tracing around the token command. Mint a new token into the same temporary file before the next AWS credential refresh when the work lasts longer than the token.

Tailscale

Tailscale can exchange an Amp token for a temporary node registration credential. This example lets orbs from one Amp project join as temporary tagged nodes. A developer group can then reach a demo service on port 3000.

Open Trust credentials in the Tailscale admin console. Create an OpenID Connect credential with these values:

  • Choose Custom issuer.
  • Set the issuer URL to https://ampcode.com/api/workload-identity.
  • Set the subject pattern to workspace:<workspace-id>:project:<project-id>.
  • Match the exact workspace_id and project_id claims and require token_use=exchanged.
  • Set the scope to auth_keys.
  • Allow the tag tag:amp-demo.

Copy the generated client ID and audience. They are identifiers and do not need to be stored as secrets. Keep the generated audience exactly as Tailscale displays it.

Add a narrow rule to the tailnet policy:

{
	"groups": {
		"group:developers": ["alice@example.com", "bob@example.com"]
	},
	"tagOwners": {
		"tag:amp-demo": ["autogroup:admin"]
	},
	"grants": [
		{
			"src": ["group:developers"],
			"dst": ["tag:amp-demo"],
			"ip": ["tcp:3000"]
		}
	]
}

Install Tailscale 1.90.1 or newer in the orb. E2B microVM orbs expose only a link-local IPv4 interface, causing Tailscale netmon to treat all links as down and pause control traffic, which leaves the daemon in NeedsLogin. Before you start tailscaled, configure it to treat the orb network as available:

sudo install -d /etc/systemd/system/tailscaled.service.d
sudo tee /etc/systemd/system/tailscaled.service.d/amp-orb.conf >/dev/null <<'EOF'
[Service]
Environment=TS_ASSUME_NETWORK_UP_FOR_TEST=true
EOF

sudo systemctl daemon-reload
sudo systemctl enable tailscaled
sudo systemctl restart tailscaled

The systemd drop-in keeps the setting across restarts and package upgrades without editing the vendor unit.

Register the orb with an allowed tag:

export TAILSCALE_CLIENT_ID='<generated-client-id>'
export TAILSCALE_AUDIENCE='<generated-audience>'

IDENTITY_TOKEN="$(amp orb id-token \
  --audience "$TAILSCALE_AUDIENCE" \
  --subject-scope project)"
sudo tailscale up \
  --client-id="${TAILSCALE_CLIENT_ID}?ephemeral=true&preauthorized=true" \
  --id-token="$IDENTITY_TOKEN" \
  --advertise-tags=tag:amp-demo \
  --hostname=amp-demo
unset IDENTITY_TOKEN

Tailscale removes temporary nodes after they go offline. Run sudo tailscale logout to remove one right away. Bind the service to an address available through Tailscale, but do not open its port to the public internet. Use the tailnet policy to control access.

Do not turn on shell tracing around this command because the Tailscale CLI receives the ID token as an argument. Tailscale verifies the token’s signature, audience, subject pattern, and custom claims before issuing its own short-lived credential.