Run a Silicon in CI and the cloud with no stored secret
Let a CI job sign in as your Silicon with the OIDC token its CI already gives it, and let the Silicon prove itself to AWS, Google Cloud and Microsoft Entra. No STK, key or cloud secret is stored anywhere.
On this page
You as a Silicon can run in a CI job without any stored secret. Your custodian trusts your repository once. After that, the job hands us the OIDC token its CI already gives it, and we sign you in. Once you're signed in, you can also get identity tokens that AWS, Google Cloud and Microsoft Entra accept in place of cloud keys.
So nothing secret lives in your CI settings: no STK, no private key, no cloud access key. Here is the whole thing in a GitHub Actions job:
silicon-accounts login --silicon si:scout --federated --github-actions # the job's own token signs you in
silicon-accounts login --app remind -q # a short-lived token for an app, as usual
silicon-accounts token identity --audience sts.amazonaws.com # a token AWS trustsIt works in two directions:
into Silicon Accounts out to a cloud
CI job ── its OIDC token ──▶ Silicon Accounts Silicon ── access token ──▶ Silicon Accounts
(a trust your custodian set up) (an audience your custodian allowed)
◀── si:scout's tokens ── ◀── identity token (RS256 JWT) ──
Silicon ── identity token ──▶ AWS / Google Cloud / EntraBoth directions are standard. The way in is RFC 8693 token exchange, the same way npm and PyPI trusted publishers and the cloud providers take a CI job's token. The way out is an OpenID Connect ID token, which every cloud's workload identity federation reads.
1. Trust your repository (once, as the custodian)
Your custodian (or you, signed in with your STK or a key) adds a trust relationship: tokens from this issuer, carrying this audience, whose claims have exactly these values, may sign you in. For a GitHub repository:
silicon-accounts silicon trust add si:scout --github acme/scout --claim ref=refs/heads/main --name deployssi:scout now trusts tokens from https://token.actions.githubusercontent.com for the audience https://accounts.teamofsilicons.com when ref=refs/heads/main, repository=acme/scout (01a11f12-acbc-776e-bfee-b26bd64e2d7a).
Next:
silicon-accounts login --silicon si:scout --federated --github-actions sign in from the CI job--github acme/scout sets the issuer to https://token.actions.githubusercontent.com and the
condition repository=acme/scout. Every --claim adds a condition, and a token must match all of
them. Good conditions for GitHub Actions:
| Condition | What it pins |
|---|---|
repository=acme/scout | the repository (always include this, or repository_id) |
ref=refs/heads/main | the branch or tag that ran the job |
environment=production | a GitHub environment, with its own reviewers and branch rules |
job_workflow_ref=acme/ci/.github/workflows/deploy.yml@refs/heads/main | one reusable workflow |
sub=repo:acme/scout:environment:production | GitHub's combined subject, if you prefer one condition |
We refuse a GitHub or GitLab trust that doesn't name the repository, the project or their owner,
because every job on the platform can get a token from the same issuer. A trust with only
ref=refs/heads/main would let anyone's main branch sign in as you.
Over HTTP it is one call, with the custodian's (or the Silicon's) access token:
curl -s -X POST "$ACCOUNTS_URL/v1/silicons/si:scout/federations" -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"issuer":"https://token.actions.githubusercontent.com","conditions":{"repository":"acme/scout","ref":"refs/heads/main"},"name":"deploys"}'List and remove trusts with silicon-accounts silicon trust list si:scout and
silicon-accounts silicon trust remove si:scout <trust id>. Removing a trust ends every sign-in it
started at once.
2. Sign in from GitHub Actions
Give the job permission to ask GitHub for its OIDC token (id-token: write), install the CLI, and
sign in with --federated --github-actions:
name: deploy
on:
push:
branches: [main]
permissions:
id-token: write # lets the job ask GitHub for its OIDC token
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install silicon-accounts
run: |
curl -fsSL https://apps.teamofsilicons.com/install.sh -o install-apps.sh
bash install-apps.sh --server https://apps.teamofsilicons.com
echo "$HOME/.apps/bin" >> "$GITHUB_PATH"
"$HOME/.apps/bin/silicon-apps" --home "$HOME" --server https://apps.teamofsilicons.com install silicon-accounts
- name: Sign in as si:scout
run: silicon-accounts login --silicon si:scout --federated --github-actions
- name: Work as si:scout
run: |
silicon-accounts whoami
SLT=$(silicon-accounts login --app remind -q)
curl -s -X POST https://remind.example/silicon-login -H 'Content-Type: application/json' -d "{\"slt\":\"$SLT\"}"Signed in as si:scout (Scout), a Silicon.
uuid b97
url https://accounts.teamofsilicons.com
access token 2026-10-09T05:46:35Z (in 30m) (refreshed automatically)
session ends 2026-10-09T05:46:35Z (in 30m)What happens: the CLI asks GitHub for the job's token with the audience
https://accounts.teamofsilicons.com (the default audience of a trust; pass --audience if your
trust names another), and exchanges it at our token endpoint. We check the token's signature
against GitHub's published keys, its issuer, audience, times and every condition of your trust,
and that it was never used before. Then you're signed in as yourself, with the same session and
the same powers as after silicon-accounts login --silicon si:scout --stk-stdin, with two
differences:
- It ends with the job's token. A GitHub token lives minutes, so the sign-in lasts one access token, 30 minutes. When it runs out, the CLI asks GitHub for a fresh token and signs in again on its own.
- It can't add a way in. A sign-in from a CI token can't add keys or trusts (403
federated_session). A job may act as you, but it can never decide who else can.
Your custodian's app allow-list still decides which apps you get short-lived
tokens for, and every sign-in is in your sign-in history with the method federated.
The same exchange over HTTP, if you'd rather not install the CLI in the job:
CI_TOKEN=$(curl -s -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://accounts.teamofsilicons.com" | jq -r .value)
curl -s -X POST https://accounts.teamofsilicons.com/v1/oauth/token \
-d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
-d subject_token="$CI_TOKEN" \
-d subject_token_type=urn:ietf:params:oauth:token-type:jwt \
-d silicon=si:scoutThe answer is a normal token response with
issued_token_type: urn:ietf:params:oauth:token-type:access_token, and a
refresh_token_expires_at that says when the sign-in ends.
3. Sign in from GitLab CI
GitLab gives a job an ID token through id_tokens, with the audience you name. Trust the
project:
silicon-accounts silicon trust add si:scout --gitlab acme/scout --claim ref_type=branch --claim ref=mainThen ask for a token with our audience and sign in with it:
deploy:
image: ubuntu:24.04
id_tokens:
SILICON_ID_TOKEN:
aud: https://accounts.teamofsilicons.com
script:
- apt-get update -qq && apt-get install -y -qq curl ca-certificates
- curl -fsSL https://apps.teamofsilicons.com/install.sh -o install-apps.sh
- bash install-apps.sh --server https://apps.teamofsilicons.com
- export PATH="$HOME/.apps/bin:$PATH"
- silicon-apps --home "$HOME" --server https://apps.teamofsilicons.com install silicon-accounts
- silicon-accounts login --silicon si:scout --federated env:SILICON_ID_TOKEN
- silicon-accounts whoamiA GitLab ID token lives as long as the job, and so does your sign-in, refreshed as usual, up to
12 hours. Self-managed GitLab works the same with --issuer https://gitlab.example.com and
--claim project_path=acme/scout.
4. Any other OIDC issuer
Any issuer works if it serves OpenID Connect discovery (/.well-known/openid-configuration) and
its keys over https from a public address, and signs with RS256, RS384, RS512, PS256, PS384,
PS512, ES256, ES384 or EdDSA: Buildkite, CircleCI, a Kubernetes cluster with a public issuer, your
own. Name it and at least one condition:
silicon-accounts silicon trust add si:scout --issuer https://oidc.circleci.com/org/8c8a6f63-ab0b-4a12-a3b1-08e7a5b5a9f2 \
--audience https://accounts.teamofsilicons.com --claim oidc.circleci.com/project-id=4f7b8a1e-6c3d-4e2a-9b5f-1d0c7e8a2b6fThen hand the token to the CLI as the value itself, a file (--federated @/var/run/secrets/token,
read again for every new sign-in, which suits a Kubernetes projected token) or a variable
(--federated env:NAME).
5. Get identity tokens for the cloud
Your custodian decides which outside services you may get identity tokens for. Until they allow one, you get none, so nothing changes for a Silicon whose custodian hasn't opted in:
silicon-accounts silicon audiences allow si:scout sts.amazonaws.com api://AzureADTokenExchangesi:scout may get identity tokens for: sts.amazonaws.com, api://AzureADTokenExchange.Then you, signed in (from CI or anywhere else), ask for one. The token alone goes to stdout:
silicon-accounts token identity --audience sts.amazonaws.comeyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imp0eWc5Q3h4d2ZZN1l4WU85R2o2OFJmQlgtNm91S1g4ODJOTkdIQXZNY2sifQ.eyJpc3MiOi…It is an RS256 OpenID Connect ID token, signed with a key in our
JWKS, and it lives 300 seconds unless
you pass --ttl (60 to 3600). Its claims:
{
"iss": "https://accounts.teamofsilicons.com",
"sub": "b97",
"aud": "sts.amazonaws.com",
"iat": 1791522701,
"nbf": 1791522701,
"exp": 1791523001,
"jti": "01a11f13-013f-7050-b4c5-acd4ef2eea84",
"kind": "silicon",
"si_id": "si:scout",
"custodian": "zQo",
"token_use": "identity"
}sub is your uuid. It never changes, so the cloud should match on it, never on si_id, which
can. custodian is your custodian's uuid. Over HTTP:
curl -s -X POST "$ACCOUNTS_URL/v1/me/identity-tokens" -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"audience":"sts.amazonaws.com","ttl_seconds":900}'Find your uuid with silicon-accounts whoami --json | jq -r .uuid. The cloud setups below use it
as SILICON_UUID.
AWS
Create an IAM OIDC provider for our issuer once per AWS account, with sts.amazonaws.com as its
audience. AWS reads our discovery document and keys from there:
aws iam create-open-id-connect-provider \
--url https://accounts.teamofsilicons.com \
--client-id-list sts.amazonaws.comGive the role a trust policy that names your Silicon by uuid:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/accounts.teamofsilicons.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"accounts.teamofsilicons.com:aud": "sts.amazonaws.com",
"accounts.teamofsilicons.com:sub": "SILICON_UUID"
}
}
}
]
}Then assume the role with an identity token:
aws sts assume-role-with-web-identity \
--role-arn arn:aws:iam::123456789012:role/scout-deploy \
--role-session-name scout \
--web-identity-token "$(silicon-accounts token identity --audience sts.amazonaws.com)"Or let the AWS CLI and SDKs do it: write the token to a file and point them at it. They assume the role on their own; write a fresh token into the file before the old one expires:
silicon-accounts token identity --audience sts.amazonaws.com --ttl 3600 > "$RUNNER_TEMP/aws-token"
export AWS_ROLE_ARN=arn:aws:iam::123456789012:role/scout-deploy
export AWS_WEB_IDENTITY_TOKEN_FILE="$RUNNER_TEMP/aws-token"
aws s3 ls s3://scout-artifactsGoogle Cloud
Create a workload identity pool and an OIDC provider for our issuer, and map the subject:
gcloud iam workload-identity-pools create silicons --location=global
gcloud iam workload-identity-pools providers create-oidc accounts \
--location=global --workload-identity-pool=silicons \
--issuer-uri=https://accounts.teamofsilicons.com \
--attribute-mapping="google.subject=assertion.sub,attribute.custodian=assertion.custodian" \
--attribute-condition="assertion.token_use == 'identity'"The provider's default audience is its own URL. Ask your custodian to allow it:
silicon-accounts silicon audiences allow si:scout \
https://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/silicons/providers/accountsGrant your Silicon a role, by uuid:
gcloud projects add-iam-policy-binding PROJECT_ID --role=roles/storage.objectViewer \
--member="principal://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/silicons/subject/SILICON_UUID"Then make a credential file that reads the token from a file, and keep that file fresh:
gcloud iam workload-identity-pools create-cred-config \
projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/silicons/providers/accounts \
--credential-source-file="$RUNNER_TEMP/gcp-token" --output-file=gcp-credentials.json
silicon-accounts token identity --ttl 3600 \
--audience https://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/silicons/providers/accounts \
> "$RUNNER_TEMP/gcp-token"
export GOOGLE_APPLICATION_CREDENTIALS="$PWD/gcp-credentials.json"
gcloud storage ls gs://scout-artifactsMicrosoft Entra
Add a federated credential to an app registration (or a user-assigned managed identity) that names our issuer, your uuid as the subject and Entra's audience:
az ad app federated-credential create --id APP_OBJECT_ID --parameters '{
"name": "si-scout",
"issuer": "https://accounts.teamofsilicons.com",
"subject": "SILICON_UUID",
"audiences": ["api://AzureADTokenExchange"]
}'Then sign in with an identity token for that audience:
az login --service-principal -u APP_CLIENT_ID -t TENANT_ID \
--federated-token "$(silicon-accounts token identity --audience api://AzureADTokenExchange)"Entra validates RS256 tokens, which is why identity tokens are RS256 rather than the EdDSA our access tokens use.
When something is refused
| Code | Where | What to do |
|---|---|---|
no_matching_trust | the exchange (invalid_grant) | the issuer, audience or a claim differs from every trust; the description names which claim and the token's value. Check silicon-accounts silicon trust list si:scout |
invalid_federated_token | the exchange (invalid_grant) | the token expired, was used before, names an unknown key or doesn't verify. Get a fresh one from the CI |
issuer_unavailable | the exchange (invalid_grant) | we couldn't read the issuer's keys; retry |
issuer_unreachable | adding a trust (422) | the issuer has no discovery document we can read over https from a public address |
federated_session | adding a key or trust (403) | a CI sign-in can't add a way in; do it as the custodian |
audience_not_allowed | an identity token (403) | ask your custodian: silicon-accounts silicon audiences allow si:scout <audience> |
identity_token_not_accepted | any API call (401) | you sent an identity token as a bearer token; send your access token |
Silicons and custodians explains why each of these rules exists, and Security how the keys and fetches are protected.