Skip to content
ExponaBLOG
GIT & GITHUB FOR PROFESSIONAL DEVELOPERS · DAY 096 / 100

Environment secrets vs repository secrets for production

Scope secrets correctly.

Expona Admin7 min read

Environment secrets vs repository secrets for production

When you're deploying services to production, deciding where and how to store credentials isn't a technical detail you can defer—it directly affects your blast radius if a secret leaks. Understanding the differences between environment secrets vs repository secrets is the foundation of a secure CI/CD pipeline. We'll walk through when to use each, how they interact with approvals, and why cloud-native approaches like OIDC often eliminate the secret-storage problem entirely.

Repository secrets for all workflows

Repository secrets are stored at the repo level in GitHub and are available to all workflows in that repository. They're the most straightforward option for credentials that need to exist in a single place but don't require extra approval gates.

When to use repository secrets:

  • Database credentials for staging environments
  • API keys for third-party services (Stripe test keys, SendGrid, etc.)
  • Docker registry credentials for pulling private images
  • SSH deploy keys for non-production targets
  • Signing certificates that aren't sensitive

The advantage is simplicity: you define it once, reference it everywhere. Every workflow that needs that credential can access it without additional configuration.

Example: storing a test database password

bash
name: Run integration tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
      - name: Install dependencies
        run: npm ci
      - name: Run tests
        env:
          TEST_DB_PASSWORD: ${{ secrets.TEST_DB_PASSWORD }}
          TEST_DB_HOST: localhost
        run: npm run test:integration

Repository secrets are visible to anyone with write access to the repo—this is an important trust boundary. If you can push code, you can add a workflow that echoes secrets to logs (though GitHub masks output). For most teams, this is acceptable for non-production credentials.

Environment secrets require environment approval

Environment secrets are scoped to specific environments (like production, staging, or canary) and add a critical layer of control: required reviewers. Before a workflow can access an environment secret, designated team members must approve the deployment.

Why this matters in production:

A junior engineer might accidentally trigger a destructive migration on production. Without environment protection, that runs immediately. With environment secrets and required reviewers, the workflow pauses, a senior engineer reviews the changes, and either approves or rejects. This is your last line of defense before sensitive operations touch production data.

Setting up environment secrets in GitHub:

  1. Go to your repository settings → Environments
  2. Create a new environment (e.g., production)
  3. Enable "Require reviewers" and add team members who should approve
  4. Add secrets specific to this environment (production database password, production API keys)
  5. Optionally restrict to specific branches (e.g., only main can deploy to production)

Example: production deployment with approvals

bash
name: Deploy to production
on:
  workflow_dispatch:  # manual trigger only
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://api.example.com
    steps:
      - uses: actions/checkout@v4
      - name: Build application
        run: npm run build
      - name: Deploy with credentials
        env:
          PROD_DB_PASSWORD: ${{ secrets.PROD_DB_PASSWORD }}
          PROD_API_KEY: ${{ secrets.PROD_API_KEY }}
          AWS_ROLE_ARN: ${{ secrets.PROD_AWS_ROLE_ARN }}
        run: npm run deploy:prod

When this workflow runs, GitHub automatically pauses at the deploy job and notifies the required reviewers. They see what branch is being deployed, what changes are included, and can review the code before approving. This is significantly more secure than having any developer push production credentials.

The trade-off is latency: deployments now require human approval. For critical services, this is the right trade-off. For internal tooling that auto-deploys from main, you might skip it.

Organization secrets for shared values

Organization secrets exist at the organization level and are available to all repositories that are granted access. Use these sparingly, for truly shared infrastructure secrets that multiple repos need.

Good use cases:

  • Shared monitoring API keys (Datadog, New Relic)
  • Organization-wide artifact registry credentials
  • Central logging service tokens
  • Shared notification webhooks

Risks:

  • Blast radius is huge—if the secret leaks, it affects all repos
  • Harder to audit which repos actually use it
  • Rotating the secret requires coordination across teams
  • Less visibility: developers might not realize they're using a shared secret

Our advice: minimize organization secrets. If you have multiple services that need to push logs to a central stack, consider instead using OIDC tokens (covered below) so each service authenticates with its own identity, not a shared password.

Example organization secret in a workflow:

bash
- name: Push metrics
  env:
    DATADOG_API_KEY: ${{ secrets.DATADOG_API_KEY }}
  run: |
    curl -X POST https://api.datadoghq.com/api/v1/series \
      -H "DD-API-KEY: $DATADOG_API_KEY" \
      -d @metrics.json

This works, but now every repo in the org shares the same Datadog API key. A better approach: have Datadog create a read-only key per service, store those as environment secrets in each repo, and rotate them independently.

Prefer OIDC over secrets for cloud access

The best secret is one you don't have to store. GitHub Actions supports OpenID Connect (OIDC), which lets your workflows authenticate to AWS, Azure, GCP, and other cloud providers using short-lived tokens instead of long-lived credentials.

How OIDC works:

  1. Your workflow requests a token from GitHub's OIDC provider
  2. GitHub issues a token bound to your repo, branch, and commit
  3. Your workflow exchanges that token for temporary cloud credentials (AWS STS, Azure Workload Identity, etc.)
  4. The temporary credentials expire after minutes, not years

Why this is superior to storing secrets:

  • No long-lived credentials to leak or rotate
  • Each workflow gets credentials scoped to that specific job
  • Audit logs show exactly which repo/branch triggered the access
  • Cloud provider controls the trust relationship, not GitHub
  • If a token is compromised, it expires in minutes

Setting up OIDC with AWS:

First, create a GitHub OIDC provider in AWS IAM:

bash
aws iam create-open-id-connect-provider \
  --url https://token.actions.githubusercontent.com \
  --client-id-list sts.amazonaws.com \
  --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1

Then create an IAM role that trusts this provider, scoped to your repo:

bash
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:ref:refs/heads/main"
        }
      }
    }
  ]
}

Then in your workflow, use the AWS OIDC action:

bash
name: Deploy to AWS
on: [push]
permissions:
  id-token: write
  contents: read
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Assume AWS role
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-role
          aws-region: us-east-1
      - name: Deploy
        run: |
          aws s3 sync ./dist s3://my-bucket/
          aws lambda update-function-code \
            --function-name my-function \
            --s3-bucket my-bucket \
            --s3-key app.zip

No AWS credentials stored anywhere. The workflow authenticates, deploys, and the token expires when the job finishes. This is the gold standard for CI/CD security.

Common mistakes

Mistake 1: Storing production secrets in repository secrets

Repository secrets are accessible to anyone with write access, including via malicious workflows. Always use environment secrets for production, with required reviewers.

Mistake 2: Not rotating environment secrets

Environment secrets live in GitHub's database. Set a rotation schedule (quarterly is reasonable) and update them. With OIDC, rotation happens automatically on your cloud provider's schedule.

Mistake 3: Using the same secret across multiple environments

Never use the same database password for staging and production. Each environment should have independent credentials so a staging breach doesn't compromise production.

Mistake 4: Forgetting to mask secrets in logs

GitHub masks known secrets when they appear in logs, but if you pipe a secret through a tool that transforms it (base64 encoding, hashing), the mask might not catch it. Be explicit about what's sensitive.

Mistake 5: Checking secrets into version control

Even if you delete them later, they remain in git history. Use .gitignore for local .env files and educate team members that secrets should never touch the repo.

Key takeaways

  • Repository secrets are for non-sensitive values needed across all workflows; they're accessed by anyone with write access.
  • Environment secrets are for production-grade credentials; pair them with required reviewers to enforce approval gates before deployment.
  • Organization secrets minimize blast radius if shared across repos, but create audit and rotation headaches—use sparingly.
  • OIDC tokens eliminate the need to store long-lived credentials; each workflow gets temporary, scoped credentials that expire in minutes.
  • Always use environment-specific secrets; never share production credentials with staging.
  • Rotate secrets quarterly at minimum; prefer OIDC for cloud access so rotation is automatic.
  • Restrict production deployments to specific branches (e.g., main only) and require approval from senior engineers before access to environment secrets.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles