Pipeline Identities, Part 3: The Job's ID Token
In this part you'll have a job ask GitHub for an ID token, and print what it says. The call you'll cover:
- call 4, job → GitHub: the job asks for an ID token.
The token says which repository and branch the job runs for, signed by GitHub.
- It allows nothing by itself: Part 4 shows it to AWS, which decides what to grant.
- It is the ID token of the OIDC page, with a job where alice was.
- A job gets one in four moves:
permissions: id-token: writelets the job ask. Without it, nothing else in this list happens.- The runner sets two environment variables in the job:
ACTIONS_ID_TOKEN_REQUEST_URL: the address of GitHub's token service;ACTIONS_ID_TOKEN_REQUEST_TOKEN: a short-lived token proving the request comes from this job.
- The job calls that URL with the request token, naming an audience: the service the ID token is for.
- GitHub answers with a JWT signed with its private key.
No browser, no sign-in, no consent: GitHub started the job, so it knows which job is asking. This part talks to GitHub only.
Add a workflow that prints the token
Create a second workflow, run by hand, so the pipeline is left as it is. It starts without id-token: write, so
its first run shows what the permission does.
.github/workflows/show-token.yml:
1name: show-token
2
3on: workflow_dispatch
4
5permissions:
6 contents: read
7
8jobs:
9 token:
10 runs-on: ubuntu-latest
11 steps:
12 - name: GitHub's discovery document and public keys
13 run: |
14 curl -sS https://token.actions.githubusercontent.com/.well-known/openid-configuration | jq '{issuer, jwks_uri}'
15 curl -sS https://token.actions.githubusercontent.com/.well-known/jwks | jq '.keys[] | {kid, kty, alg}'
16
17 - name: Ask for an ID token addressed to AWS, and print its header and claims
18 run: |
19 # The runner sets the request URL and token only when the job holds id-token: write.
20 if [ -z "$ACTIONS_ID_TOKEN_REQUEST_URL" ]; then
21 echo "No ID token request URL: this job may not ask for an ID token"
22 exit 1
23 fi
24 TOKEN=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
25 "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" | jq -r .value)
26 # Print parts 1 and 2 only. The signature, and the token as a whole, stay out of the log.
27 for part in 1 2; do
28 p=$(echo "$TOKEN" | cut -d. -f"$part" | tr '_-' '/+')
29 while [ $(( ${#p} % 4 )) -ne 0 ]; do p="$p="; done # base64url drops the padding
30 echo "$p" | base64 -d | jq .
31 done
- The first step prints the two public documents any service reads to check GitHub's tokens: the discovery document and the keys.
- The second step asks for a token addressed to AWS, and prints its header and claims.
- The whole token stays out of the log: for a few minutes it is a real credential.
Run it, and read the refusal
- Commit and push.
- Open Actions → show-token → Run workflow.
- Read the run:
The discovery document and public keys print: they are public, and need no credential.
Ask for an ID token fails:
1No ID token request URL: this job may not ask for an ID tokenThe job holds no
id-token: write, so the runner set neither variable.
Grant the job the right to ask
Add the permission to
show-token.yml:1permissions: 2 contents: read 3 id-token: write # lets the job ask GitHub for an ID tokenCommit and push, then Run workflow again. The step prints the token's header and claims.
Read the token's claims
The claims say who the job is; the header says which key signed them.
| Claim | Value | Meaning |
|---|---|---|
iss | https://token.actions.githubusercontent.com | GitHub issued it |
aud | sts.amazonaws.com | the audience the job asked for |
sub | repo:<you>/pipeline-identities:ref:refs/heads/main | this repository, this branch |
repository, ref, sha, workflow, actor | … | more detail a service may check |
iat, exp | a few minutes apart | it expires almost at once |
subis the claim Part 4's AWS role checks.kidin the header matches one of the keys the first step printed: the key AWS will check the signature with.
Change the request
- Ask for another audience. Change
audience=sts.amazonaws.comtoaudience=example.com, push, and run again.audis nowexample.com; AWS would refuse this token, since it is addressed to someone else.
- Run it on another branch. Create a branch
try, push it, and run show-token on it from Run workflow.subends inrefs/heads/try.
- Put the audience back to
sts.amazonaws.com, and push. Part 4 does not use this workflow, but it stays useful for a look at the claims.