DevOps · Cicd

GitHub Actions Secrets Management

GitHub Actions Secrets Management is the work that turns a deploy into a system. The deployment is one moment; the system is the next 18 months of uptime, incidents, and.

John Kihiu12 min read

Secrets management in GitHub Actions is mostly about scoping: deciding who can read a value, at what level it's stored, and how the pipeline gets short-lived cloud credentials instead of long-lived ones sitting in a settings page. The masking GitHub gives you for free is not a security boundary by itself — it's a convenience that stops accidental echoes, and it's worth understanding exactly where it does and doesn't help.

Where secrets can live

GitHub Actions secrets can be defined at three levels: repository, environment, and organization. Repository secrets are available to any workflow in that repo. Organization secrets can be shared across many repos, optionally restricted to a chosen subset. Environment secrets are the most useful for anything resembling a real deployment pipeline, because they're only exposed to a job once that job targets the environment (jobs.<id>.environment: production) — and an environment can carry protection rules, including required reviewers, so a workflow run pauses for manual approval before it can read the production secrets at all. That's the mechanism that turns "anyone who can push a branch can deploy" into "someone still has to click approve."

OIDC federation instead of long-lived cloud keys

The biggest single upgrade most pipelines can make is dropping static AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY pairs entirely. GitHub's OIDC provider lets a workflow request a short-lived token that a cloud provider trusts directly, with no long-lived secret stored anywhere. You add permissions: id-token: write to the job, configure an identity provider and trust policy on the AWS/Azure/GCP side that trusts tokens from your specific repo (and optionally branch or environment), and use an action like aws-actions/configure-aws-credentials to exchange the OIDC token for temporary STS credentials scoped to a role.

YAML · OIDC TO AWS
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/gha-deploy
          aws-region: us-east-1
      - run: aws s3 sync ./dist s3://my-bucket/

No AWS secret ever sits in GitHub's secret store. The role's trust policy is scoped to the repo and branch, the token is valid for a single job run, and there's nothing to rotate because there's nothing long-lived to rotate.

Masking behavior — and where it still leaks

GitHub automatically redacts any registered secret value it sees in step logs, replacing it with ***. That masking is a literal string match on the raw secret value, which means it has real gaps: it doesn't catch a secret that's been base64-encoded, split across multiple log lines, transformed, or reconstructed by concatenation. echo "${{ secrets.API_KEY }}" gets masked; a script that reads the key from the environment and prints Bearer ${key:0:4}... or logs it after a transformation might not be. Treat masking as a safety net for accidental slip-ups, not a guarantee — the actual control is not printing secrets in the first place, and not passing them to steps that don't need them.

GITHUB_TOKEN is a secret too

The automatic GITHUB_TOKEN is scoped to the triggering repo and expires at the end of the job, but its default permissions can be broad. Set an explicit least-privilege permissions: block at the workflow or job level (e.g. contents: read, and only add pull-requests: write or similar if a step actually needs it) rather than relying on the repository-wide default.

Pulling secrets from Vault or AWS Secrets Manager

For organizations that already centralize secrets outside GitHub, actions like hashicorp/vault-action or the AWS Secrets Manager CLI/SDK let a workflow authenticate (often via the same OIDC flow) and fetch secrets at run time instead of duplicating them into GitHub's secret store. This keeps one source of truth and one audit trail for who accessed what, and it means rotating a secret in Vault doesn't require also updating it in every repo or org secret that referenced a copy.

ScopeWho can read it
Repository secretAny workflow run in that repo
Environment secretJobs targeting that environment, gated by required reviewers if configured
Organization secretRepos it's shared with, org admin controls the list
OIDC-issued cloud credentialNobody stores it — it's minted per run and expires

The pattern that holds up under audit is: environment secrets with required reviewers for anything that touches production, OIDC instead of static cloud keys wherever the provider supports it, an explicit least-privilege permissions: block on every workflow, and no secret value ever deliberately printed to a log — masking is the backstop, not the plan.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.