Deploying or publishing needs credentials, and leaking or over-privileging them is the biggest CI security risk.
Secrets are encrypted values stored at organization, repository or environment level. Read them as ${{ secrets.NAME }} and pass them to steps through env or with. GitHub masks secret values in logs, printing *** if they appear verbatim. Masking cannot catch transformed values (base64 encoded, split), so never print or echo secrets.
Every job also receives a GITHUB_TOKEN, an installation token created for that job that expires when the job finishes. It authenticates git push, the gh CLI and the REST API against the current repository: ${{ secrets.GITHUB_TOKEN }} or ${{ github.token }}.
Its rights are set by the permissions key, at workflow or job level. Once you specify permissions, every scope you don't list becomes none. Best practice is to start from contents: read and grant per job only what is needed, for example pull-requests: write to comment on PRs or packages: write to push images to GHCR. New repositories default to a read-only token.
Security rules worth remembering:
- Pull requests from forks get no secrets and a read-only token.
- Events created with the GITHUB_TOKEN (such as a push) do not trigger new workflow runs, which prevents infinite loops. Use a GitHub App token if you need that.
- Secrets are not available in
if:conditions directly; check them via an env variable.
Example
permissions:
contents: read # default for every job in this workflow
jobs:
label:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write # only this job may write to PRs
steps:
- run: gh pr comment "$PR" --body "Build passed"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR: ${{ github.event.pull_request.html_url }}
- run: ./deploy.sh
env:
API_KEY: ${{ secrets.DEPLOY_API_KEY }} # shown as *** if printedRemember
- secrets.* are encrypted, masked in logs and absent for fork PRs
- GITHUB_TOKEN is per-job and short-lived; scope it with permissions
- Listing permissions sets every unlisted scope to none
Watch out for
- Relying on masking: an encoded or partial secret is printed in clear text.