The Build That Packed Its Own Key: Recovering GITHUB_TOKEN From an Actions Artifact
The challenge
Kelpforge's CI shared its whole checkout. Find the auth header git kept, decode it. Bring back the token the build job ran with.
What you'll learn
- Explain where actions/checkout stores the job's token and why
- Find hidden files and folders inside a CI artifact
- Decode an HTTP basic credential from base64
- Tell a GitHub Actions token (ghs_) from a personal token (ghp_)
- Harden a workflow: persist-credentials, artifact paths, permissions
Skills tested
Prerequisites
- Basic shell commands (ls, cat, grep)
- What a CI pipeline is
How it works
Every GitHub Actions job gets a short-lived GITHUB_TOKEN (prefix ghs_), scoped to its repository with the permissions the workflow grants. actions/checkout writes it into the checkout's .git/config by default (persist-credentials: true), as an http.https://github.com/.extraheader carrying AUTHORIZATION: basic and the base64 of x-access-token:<token>, so that later git push steps just work.
Base64 is an encoding, not encryption: anyone who reads the file gets the token. In 2024 Unit 42 found public repositories whose workflows uploaded the whole working directory as an artifact (they called it ArtiPACKED). Artifacts of public repositories can be downloaded by any signed-in user, and since actions/upload-artifact@v4 they are available while the job is still running, while the token is still valid.
Common mistakes
- Using plain
ls..git,.githuband.npmrcare dotfiles: usels -a,findorgrep -r. - Answering the ghp_ token. The commented line in
.npmrcis an old personal access token (ghp_). The job ran with aghs_token. - Answering the base64 value. It is the encoded
user:passwordpair; decode it and keep only the part afterx-access-token:. - Taking
${{ secrets.GITHUB_TOKEN }}literally. The workflow only names the secret; its value is in the checkout's config. - Waiting for git. This box has no git installed:
cat .git/configreads the same thinggit config --listwould.
How to defend against it
- Set
persist-credentials: falseonactions/checkout, and pass a token explicitly to the one step that pushes. - Upload only the build output (
path: dist), never., and leaveinclude-hidden-filesat its defaultfalse. - Declare the minimum
permissions:for the job (contents: readunless it must push). - Scan workflows for these patterns (zizmor flags ArtiPACKED) and rotate any long-lived secret that ever landed in an artifact.