Skip to content
Innovialabs

GitHub Actions Secrets and Environments: Safe Deploy Setup

Practice4 min

In short

Put production credentials in environment secrets behind required reviewers and branch rules, replace long-lived cloud keys with OIDC, give GITHUB_TOKEN minimal permissions, and never run untrusted fork code with access to secrets.

The short answer

A safe deploy setup in GitHub Actions rests on four ideas:

  1. Scope secrets narrowly: production credentials live in a production environment, not at repository level.
  2. Gate production: the environment requires a reviewer and accepts deploys only from protected branches.
  3. No long-lived cloud keys: the workflow gets short-lived credentials through OIDC.
  4. Assume logs and forks are hostile: masking is not a guarantee, and untrusted code must never run next to secrets.

Three levels of secrets

LevelWho can use itUse it for
OrganizationRepositories you select in the access policyShared values: registry tokens, notification webhooks
RepositoryAny workflow in the repositoryNon-critical values used across jobs
EnvironmentOnly jobs that reference the environment, and only after its protection rules passDeploy credentials for staging and production

When names collide, the more specific level wins: environment over repository, repository over organization. Secrets are read with ${{ secrets.NAME }} and should be passed to steps through env, not pasted into inline scripts.

Protecting production

In Settings → Environments → production configure:

  • Required reviewers — a person or team must approve each deploy job before it starts and before it gets the secrets.
  • Deployment branches and tags — allow only main or release tags. A feature branch then cannot deploy even if someone edits the workflow.
  • Wait timer if you want a pause before deploys.
  • Prevent self-review where available, so the author of a change cannot approve their own deploy.

Combine this with branch protection or rulesets on main: required pull request reviews, required status checks and no force pushes. Otherwise anyone with write access can push a modified workflow straight to the protected branch.

OIDC instead of cloud keys

Static access keys stored as secrets live for months and work from anywhere. With OIDC, GitHub issues a signed token for each run; the cloud provider exchanges it for temporary credentials, but only if the token's claims match the trust policy (repository, branch, environment).

name: deploy
on:
  push:
    branches: [main]

permissions:
  contents: read
  id-token: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-deploy
          aws-region: eu-central-1
      - run: ./scripts/deploy.sh

On the cloud side, restrict the trust policy to the exact subject, for example the production environment of a specific repository. A wildcard over the whole organization defeats the purpose. Major clouds and Vault support this flow.

How secrets leak

Through logs:

  • GitHub masks exact secret values, but not transformed ones: base64-encoded, split, URL-encoded or embedded in JSON.
  • set -x, verbose CLI flags and debug logging can print headers and tokens.
  • Register derived values with echo "::add-mask::$VALUE" before using them.

Through forks and untrusted code:

  • Workflows triggered by pull_request from forks do not receive secrets — keep it that way.
  • pull_request_target and workflow_run run with the base repository's secrets. Checking out and running the PR's code in them hands your secrets to any outside contributor.
  • Inputs like PR titles and branch names inserted directly into run: can lead to script injection. Pass them through env and quote them.

Through dependencies:

  • Third-party actions run with your job's permissions. Pin them to a full commit SHA and review what they do.

Least-privilege checklist

  • Set permissions: at the workflow level; start from contents: read and add only what is needed.
  • Keep production secrets only in the production environment.
  • Rotate any secret that was ever printed, committed or shared.
  • Remove unused secrets and environments.
  • Review who has admin rights: admins can change protection rules.

FAQ

Yes, when the environment has protection rules. The job gets the secrets only after reviewers approve and the branch rule matches, while repository secrets are available to any workflow in the repo.

We can help with this

If you'd rather get the result than figure it all out yourself — this is what we do every day.

Discuss a project

Read next

Have an idea or a task? Tell us about it

We'll reply within one business day and propose a solution. The consultation is free.