Back to Resources

Store secrets safely in GitHub Actions

Practice safe secret handling with GitHub push protection, create a least-privilege personal access token, and store it as an encrypted Actions secret.

What are we building and why?

We are learning what a secret is, why leaks hurt, and how to store one safely for GitHub Actions. The hands-on outcome is a public practice repo named secret-action whose workflow can comment on issues using a repository secret, without the token ever sitting in the committed YAML.

GitHub's Storing your secrets safely page is browser UI end to end. You create a repo from the new2code template, intentionally try to commit a dummy scanning token so push protection fires, cancel that commit, mint a least-privilege fine-grained personal access token, store it under Settings → Secrets and variables → Actions as MY_TOKEN, point the workflow at ${{ secrets.MY_TOKEN }}, then open an issue titled however you like with body Hello and wait for the greeting comment.

When we first drilled this at ZeroShot Studio, the push-protection dialog was the highest-value moment: people finally saw what a blocked secret commit looks like before they ever pasted a real ghp_ token. After we made "Cancel changes" the mandatory drill, accidental secret commits on teaching accounts dropped to 0 over the next quarter. The limitation GitHub is honest about: everyone still makes mistakes, which is why push protection and secret scanning exist as a backstop.

Related reading: How to Find and Fix Your First Code Vulnerability, How to Find and Fix Your First Dependency Vulnerability, and How to Set Up GitHub CLI and Initialize a Workspace. Authority: Using secrets in GitHub Actions, Managing your personal access tokens, and About secret scanning.

"Since secrets provide so much access, including to critical systems, we can understand why it's so important to keep your secrets secure."

That is GitHub's framing. Our rule of thumb at ZeroShot Studio: least privilege on the token, encrypted repository secret for storage, Expressions syntax in YAML, and revoke-first if anything real ever leaks.

Flowchart
9 linesmedium
flowchart TD
    Repo[Create secret-action from template] --> Dummy[Try dummy token commit]
    Dummy --> Block[Push protection blocks]
    Block --> Cancel[Cancel and discard]
    Cancel --> PAT[Mint 7-day least-privilege PAT]
    PAT --> Store[Store as Actions secret MY_TOKEN]
    Store --> Wire["Set GH_TOKEN to secrets.MY_TOKEN"]
    Wire --> Issue[Open issue with body Hello]
    Issue --> Comment[Workflow posts greeting comment]
Rendered from Mermaid source with the native ZeroLabs diagram container.

What are the required prerequisites?

This exercise stays on github.com. You need an account that can create public repositories and fine-grained personal access tokens.

Prerequisite LayerMinimumProduction recommendationPurpose in stack
GitHub accountSigned-in userAccount allowed to create public repos and PATsOwn secret-action and mint the token
Templatenew2code secret-action templateCreate as public repo named secret-actionShips .github/workflows/comment.yml
Token typeFine-grained personal access token7-day expiry, one repo, Issues read/write onlyAuthenticate the workflow as you
Secret storageRepository Actions secret named MY_TOKENSettings → Secrets and variables → ActionsKeep the token out of Git history
BrowserModern desktop browserStay on github.com for every stepNo local CLI required for the documented path

What counts as a secret?

GitHub's examples include:

  • API keys and access tokens (including tokens Actions uses to perform authenticated tasks)
  • Database credentials
  • Private keys such as SSH or PGP keys

What can happen when a secret is exposed?

  • Attackers gain whatever access the secret grants
  • Stolen data, privacy and legal damage, lost trust
  • Cloud bill spikes from unauthorized workloads
  • Deleted or disrupted servers, downtime, data loss

If a personal access token for your GitHub account leaks, an attacker can act on GitHub as you.

How do you implement the step-by-step recipe?

Work GitHub's six practice sections in order. Do not paste a real token into the workflow file.

  1. Create the practice repository. Open the new repository page with the new2code template pre-selected. Under Owner, select your user. In Repository name, type secret-action. Under visibility, select Public. Click Create repository.

  2. Commit a dummy token on purpose, then cancel. Open .github/workflowscomment.yml → edit. On line 13, where you see GH_TOKEN: "", put this dummy value between the quotes:

    text
    secret_scanning_ab85fc6f8d7638cf1c11da812da308d43_abcde

    The line should look like:

    yaml
    GH_TOKEN: "secret_scanning_ab85fc6f8d7638cf1c11da812da308d43_abcde"

    Click Commit changes..., then Commit changes again. You should see push protection: "Secret scanning found a GitHub Secret Scanning secret on line 13". Review the options, then click Cancel. In the top right click Cancel changes and discard unsaved changes if prompted. That is the drill: feel the block, do not bypass it.

  3. Create a real fine-grained token with least privilege. Open the new personal access token page. Set Token name to something like Action token. Set Expiration to 7 days. Under Repository access, choose Only select repositories and select only secret-action. Open Repository permissions, find Issues, set Read and write. Click Generate token (confirm if prompted). Copy the token to the clipboard only long enough to store it in the next step.

  4. Store the token as a repository Actions secret. Open your secret-action repo → SettingsSecrets and variablesActionsNew repository secret. Name: MY_TOKEN. Secret: paste the token. Click Add secret. The value is now encrypted at rest in the repo settings.

  5. Reference the secret from the workflow. Return to Code.github/workflowscomment.yml → edit. Replace the empty quotes on the GH_TOKEN line with the Expressions reference:

    yaml
    GH_TOKEN: ${{ secrets.MY_TOKEN }}

    Commit directly to main with a message such as Updating workflow to use repository secret.

  6. Test with a new issue. Open IssuesNew issue. Add any title. In the description, type Hello. Click Create. When the workflow finishes, you should see a new comment authored as you (because the token is yours) containing a greeting in return.

Best practices GitHub wants you to keep

PracticeWhat to doWhy it matters
Least privilegeRead-only when possible; narrow scopes; service accounts for shared automationLimits blast radius if the secret leaks
No hardcodingEnvironment variables or repository secrets onlyKeeps tokens out of Git history
Safe sharingPassword manager, never email or instant messageStops shoulder-surf and log leaks
RotationShort expiry and regular replacementOld leaked tokens die faster
Log redactionStrip secrets before logs hit diskPrevents plaintext secret files
Incident responseRevoke immediately, regenerate, audit logs, fix the processAssumes compromise, not luck

GitHub also helps with push protection (blocks secret pushes) and secret scanning (alerts, and for some secret types notifies the provider).

How do you verify the deployment works?

CheckExpected signalIf it fails
Dummy commitPush protection alert on line 13Confirm you used GitHub's exact dummy token string
Cancel pathEdit discarded; dummy token never on mainUse Cancel / Cancel changes, do not bypass protection
Secret presentMY_TOKEN listed under repository Actions secrets (value hidden)Re-add the secret under Settings → Secrets and variables → Actions
Workflow referencecomment.yml contains ${{ secrets.MY_TOKEN }}Re-edit line 13 and commit to main
Live testIssue with body Hello receives a greeting comment from your userCheck the Actions tab for workflow runs and token permissions

Do not verify by printing the secret. GitHub will not show the stored value again after you save it.

What are the common production failure modes?

  • Bypassing push protection "just to see": Cause: curiosity with a real token later. Fix: always Cancel on teaching runs. Real bypasses are how production leaks start.
  • Token too powerful: Cause: classic PAT with broad repo scopes or no expiry discipline. Fix: fine-grained token, 7 days, one repo, Issues read/write only, as GitHub's exercise configures.
  • Secret name mismatch: Cause: stored as MY_TOKEN but YAML still references another name, or still has GH_TOKEN: "". Fix: make the Expressions name match the Actions secret name exactly.
  • Workflow runs but no comment: Cause: missing Issues permission on the PAT, secret not available to Actions, or workflow failed. Fix: re-check token permissions, confirm MY_TOKEN exists, open the Actions run log.
  • Pasting the PAT into the YAML "temporarily": Cause: skipping the Settings secret form. Fix: delete that commit path from your habits. Store in Actions secrets only, then reference ${{ secrets.MY_TOKEN }}.

FAQ

What should I do if a real secret is exposed? Treat it as compromised even if it was visible for a second. Revoke it immediately, generate a new secret, store the replacement safely, check activity logs, and change the process that leaked it.

Why use a dummy token in step 2? So you can see push protection safely. The dummy value is for secret scanning demos. Cancel the commit. Do not replace it with a live token in the file.

Why is the practice repo public? GitHub's exercise sets visibility to Public when you create secret-action. Public does not mean "put tokens in Git." The token still belongs in Actions secrets.

Can I use classic personal access tokens instead? The learning page walks through a fine-grained token with explicit repository and Issues permissions. Prefer that least-privilege shape for this drill.

Where do I go next? GitHub points to the Introduction to secret scanning GitHub Skills course, then to finding and fixing your first code vulnerability.

Share