Secrets and pipeline security (gh secret, OIDC)
The pipeline is an attack surface too
CI/CD has access to the code, the secrets, and often to production environments. A leaked workflow token can be more dangerous than a bug in the application. This lesson clarifies how to keep secrets and how to get rid of them entirely with OIDC.
Secrets in GitHub Actions
Secrets are encrypted and injected into the workflow as variables. They never end up in logs (GitHub masks their values). You define them at the level of the repo, the environment, or the organization.
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy
env:
API_TOKEN: ${{ secrets.API_TOKEN }}
run: ./scripts/deploy.sh
Management via gh CLI
gh secret lets you set secrets from the terminal — faster than clicking in the
panel and easy to automate.
# Set a repository secret (from a file or interactively)
gh secret set API_TOKEN
# From a value in a variable / file
gh secret set API_TOKEN --body "$TOKEN"
gh secret set SERVICE_JSON < service-account.json
# A secret for a specific environment
gh secret set DB_PASSWORD --env production
# List and delete
gh secret list
gh secret delete API_TOKEN
Tip:
gh secret listshows names and dates, never values — a secret cannot be read back after being saved, it can only be overwritten. If you lose the value, generate a new one and rotate.
Best practice: OIDC instead of long-lived keys
Classically, deploying to the cloud (AWS, Azure, GCP) meant keeping long-lived keys in secrets. That is a risk — such a key, if leaked, works for months. OIDC (OpenID Connect) eliminates the problem: GitHub issues a short-lived identity token that the cloud exchanges for temporary permissions. No fixed keys in the repo.
permissions:
id-token: write # lets the workflow request an OIDC token
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Login do chmury przez OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/github-deploy
aws-region: eu-central-1
- run: ./deploy.sh
On the cloud side you configure a trust policy that trusts tokens from a specific repo and branch. The token lives for minutes, so even if intercepted it expires quickly.
Pipeline security rules
| Rule | Why |
|---|---|
Minimal permissions | By default restrict GITHUB_TOKEN to read |
| Pin actions to a version/SHA | @v4 or a specific SHA, not @main |
| Per-environment secrets | Production separated from preview |
| OIDC instead of keys | No long-lived credentials |
Watch out for pull_request_target | It can expose secrets to forks |
Limiting token permissions
The default GITHUB_TOKEN can be too powerful. Set the minimum at the top of the workflow:
permissions:
contents: read
Expand only where a specific job truly needs more (e.g.
id-token: write for OIDC, packages: write for publishing).
Summary
Keep secrets in GitHub Actions (encrypted, masked), manage them via
gh secret, and rotate them after incidents. Wherever possible, replace long-lived keys
with OIDC — short-lived tokens without fixed credentials. Restrict
permissions and pin actions to versions. This is the minimum hygiene of secure CI/CD.