GitHub Actions Secrets and Environments — Security Guide 2026

Sanjeev SharmaSanjeev Sharma
7 min read

Advertisement

Introduction

Why This Matters

Leaked CI/CD credentials are one of the most common vectors for cloud infrastructure breaches. In 2026, supply chain attacks targeting GitHub Actions workflows — stealing secrets via malicious third-party actions or pull request triggers — have become a standard attack pattern.

GitHub Actions provides multiple layers of secrets management: repository secrets, environment secrets, organization secrets, and OIDC-based keyless authentication. Understanding how each layer works, and when to use which, is essential for any team deploying production workloads through GitHub Actions.

GitHub Actions Secrets Hierarchy

GitHub offers four scopes of secrets, from broadest to narrowest:

ScopeWhere setWho can access
Organization secretsOrg SettingsSelected or all repos in the org
Repository secretsRepo SettingsAll jobs in any workflow
Environment secretsRepo Settings > EnvironmentsOnly jobs referencing that environment
Codespaces secretsUser/Org SettingsDevelopment codespaces only

Rule of thumb: always store secrets at the narrowest scope that meets your needs. Production database credentials should be environment secrets, not repository secrets.

Setting Secrets via the GitHub UI

Navigate to your repository's Settings → Secrets and variables → Actions, then:

  1. Click New repository secret
  2. Enter the name (e.g., DATABASE_URL) — uppercase with underscores is conventional
  3. Enter the value
  4. Click Add secret

Secrets are encrypted at rest and masked in logs. You cannot read a secret's value after saving it — only overwrite it.

Setting Secrets via the GitHub CLI

# Set a repository secret
gh secret set DATABASE_URL --body "postgres://user:pass@host:5432/db"
 
# Set a secret from a file (useful for kubeconfig, private keys)
gh secret set KUBECONFIG < ~/.kube/config
 
# Set an environment secret
gh secret set PROD_API_KEY --env production --body "sk-live-abc123"
 
# List secrets (names only — values are never shown)
gh secret list
gh secret list --env production
 
# Delete a secret
gh secret delete OLD_SECRET

Using Secrets in Workflows

Secrets are accessed via the secrets context in workflow YAML:

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy application
        run: |
          ./scripts/deploy.sh
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
          API_KEY: ${{ secrets.API_KEY }}
 
      - name: Configure kubectl
        run: |
          mkdir -p $HOME/.kube
          echo "${{ secrets.KUBECONFIG }}" | base64 -d > $HOME/.kube/config

Important: secrets are automatically masked in logs — any step that prints the secret value will show *** instead. However, avoid logging secrets intentionally, and never echo them in scripts that might be committed.

Environments and Protection Rules

Environments add a deployment approval gate between CI and production. Configure them under Settings → Environments.

jobs:
  deploy-production:
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://myapp.example.com
    steps:
      - name: Deploy
        run: helm upgrade --install myapp ./charts/myapp --atomic
        env:
          KUBECONFIG: ${{ secrets.PRODUCTION_KUBECONFIG }}

Environment protection rules you should enable:

  • Required reviewers — list team members or teams who must approve the deployment
  • Wait timer — add a delay (e.g., 10 minutes) before deployment can proceed, giving time to cancel if needed
  • Deployment branches — restrict the environment to specific branches (e.g., main only)

Keyless Authentication With OIDC

The most secure pattern in 2026 is to eliminate long-lived secrets entirely using OIDC federation. GitHub Actions can request short-lived cloud provider credentials automatically.

AWS OIDC integration:

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write   # Required for OIDC
      contents: read
 
    steps:
      - uses: actions/checkout@v4
 
      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
          aws-region: us-east-1
 
      - name: Deploy to EKS
        run: |
          aws eks update-kubeconfig --name my-cluster --region us-east-1
          kubectl apply -f k8s/

AWS IAM role trust policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
          "token.actions.githubusercontent.com:sub": "repo:myorg/myrepo:environment:production"
        }
      }
    }
  ]
}

GCP Workload Identity federation:

      - name: Authenticate to Google Cloud via OIDC
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: projects/123/locations/global/workloadIdentityPools/github/providers/github
          service_account: deployer@my-project.iam.gserviceaccount.com

Preventing Secret Leaks in Pull Requests

Pull requests from forked repositories do not have access to repository secrets by default — this is a critical security boundary. However, pull_request_target runs in the context of the base repository and DOES have access to secrets. Misusing it is dangerous.

# SAFE: secrets are NOT available to PRs from forks
on: pull_request
 
# DANGEROUS: secrets ARE available; only use when required for PR labeling/commenting
on: pull_request_target

Never run untrusted code (e.g., npm test) in a pull_request_target workflow with secrets available.

Restrict pull_request_target to safe operations only:

on: pull_request_target
 
jobs:
  label:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    steps:
      - name: Add label
        uses: actions/labeler@v5
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}

Rotating and Auditing Secrets

# Rotate a secret (overwrite the existing value)
gh secret set DATABASE_URL --body "postgres://user:newpass@host:5432/db"
 
# Audit which workflows use a secret
grep -r "secrets.DATABASE_URL" .github/workflows/
 
# View secret usage in the GitHub audit log (org admins only)
gh api /orgs/myorg/audit-log --jq '.[] | select(.action == "secret.access")'

Set a calendar reminder to rotate long-lived secrets every 90 days. Use OIDC wherever possible to eliminate the rotation burden entirely.

Common Mistakes

  • Using pull_request_target to run tests from forks — this exposes secrets to untrusted code; use pull_request (no secrets) for testing fork contributions.
  • Storing secrets as repository secrets when environment secrets suffice — repository secrets are accessible to all workflows; use environment secrets to limit blast radius.
  • Base64-encoding secrets thinking it adds security — base64 is encoding, not encryption; GitHub will still mask it when the decoded value is in the secrets store.
  • Not restricting OIDC role trust policies — use the sub condition to limit which repo, branch, and environment can assume an AWS/GCP role.
  • Checking secrets.* into workflow conditions — never use if: secrets.MY_SECRET != '' to conditionally run steps; it partially reveals the secret in error messages.

Best Practices

  • Prefer OIDC over long-lived secrets — no rotation, no storage, no accidental commit risk; available for AWS, GCP, Azure, and HashiCorp Vault.
  • Scope OIDC trust to environments — include :environment:production in the sub claim condition so only the production environment workflow can assume production roles.
  • Enable environment protection rules — required reviewers for production deployments prevent accidental or malicious deployments.
  • Audit secret access in CI logs — ensure no step accidentally prints secrets; inspect logs after initial setup.
  • Use ${{ secrets.GITHUB_TOKEN }} — it is automatically provided, scoped to the repository, and expires after the workflow run; prefer it over PATs for GitHub API calls.

Key Takeaways

  • GitHub Actions has four secret scopes: organization, repository, environment, and codespaces; always use the narrowest scope that meets your needs.
  • Environment secrets are only accessible to jobs that explicitly reference the environment — use them for production credentials.
  • OIDC federation lets GitHub Actions workflows assume cloud provider roles without storing long-lived credentials anywhere.
  • Restrict OIDC IAM trust policies by repository and environment using the sub claim to prevent privilege escalation across repos.
  • The pull_request_target event has access to secrets and runs in the base repo context; never run untrusted fork code in this event.
  • GitHub automatically masks secret values in workflow logs — any output matching a stored secret is replaced with ***.
  • Environment protection rules (required reviewers, wait timers, branch restrictions) add a human approval gate before production deployments.
  • Rotate long-lived secrets every 90 days; switch to OIDC-based keyless authentication to eliminate rotation overhead entirely.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading