GadaaLabs
GitHub for Developers — Collaboration, CI/CD & Open Source
Lesson 8

Secrets, Environments & Security Best Practices

22 min

Why Secret Management Matters

A secret is any piece of sensitive information your application or CI/CD pipeline needs to function: API keys, database passwords, TLS private keys, OAuth client secrets, deployment credentials, and similar values. Handling secrets incorrectly is one of the most common and consequential security mistakes in software development.

The risks are real: credentials committed to a public GitHub repository are indexed by search engines within minutes and by specialized credential-hunting services within seconds. GitHub's own security team has documented cases where compromised API keys have led to data breaches, cryptocurrency mining on cloud accounts generating five-figure bills in hours, and complete infrastructure takeovers. These are not theoretical risks.

The fundamental rule is simple: secrets never live in your code, your configuration files, your documentation, or your repository in any form. They are injected at runtime by a secrets management system. GitHub's built-in secrets system is the secrets management layer for your GitHub Actions workflows.


What GitHub Secrets Are

GitHub Secrets are encrypted key-value pairs stored at the repository, environment, or organization level. Their key security properties:

  • Encrypted at rest using libsodium public-key cryptography. Even GitHub employees cannot read the stored values.
  • Masked in logs: if a secret's value appears in a workflow log, it is replaced with ***. This is not foolproof (if you base64-encode a secret it may not be masked) but provides a safety net.
  • Scoped access: secrets are only accessible to workflows running on the repository or organization they are stored in.
  • Not accessible to forked repositories by default: workflows triggered by pull requests from forks cannot read secrets (a critical security boundary for open source projects).

Repository Secrets

Repository secrets are available to all workflows in a specific repository.

Creating Repository Secrets

Go to SettingsSecrets and variablesActionsNew repository secret.

Enter:

  • Name: conventionally uppercase with underscores — GROQ_API_KEY, DATABASE_URL, STRIPE_SECRET_KEY
  • Value: the actual secret value

Click Add secret.

Using Repository Secrets in Workflows

yaml
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to production
        env:
          API_KEY: ${{ secrets.GROQ_API_KEY }}
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
        run: |
          ./deploy.sh

      - name: Notify Slack
        uses: slackapi/slack-github-action@v1
        with:
          payload: '{"text": "Deployed to production"}'
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Note: the ${{ secrets.NAME }} syntax is only valid inside workflow YAML. Never include it in scripts committed to your repository as a template — the ${} will error or be interpreted as a shell variable substitution.

Updating and Deleting Secrets

Secrets can be updated (but the current value is never shown — updating always replaces). Navigate to the secret and click Update.

To delete: navigate to the secret and click Remove.

The GITHUB_TOKEN Secret

GitHub automatically creates a special secret called GITHUB_TOKEN for every workflow run. It is a short-lived token with permissions scoped to the repository and valid only for the duration of the workflow run.

yaml
- name: Create GitHub Release
  uses: softprops/action-gh-release@v2
  with:
    files: dist/*.zip
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

GITHUB_TOKEN is the right tool for:

  • Creating releases and uploading release assets
  • Commenting on PRs from a workflow
  • Creating commits (e.g., updating a changelog)
  • Interacting with the GitHub API from within a workflow

The token's permissions are set in the workflow with a permissions block:

yaml
permissions:
  contents: write       # needed to create releases and push commits
  pull-requests: write  # needed to comment on PRs
  issues: write         # needed to create or update issues
  packages: write       # needed to push to GitHub Container Registry

Use the principle of least privilege: only grant the permissions the workflow actually needs.


Organization Secrets

Organization secrets are available to multiple repositories within a GitHub organization, eliminating the need to duplicate the same secret in every repository.

Creating Organization Secrets

Go to the organization's SettingsSecrets and variablesActionsNew organization secret.

In addition to name and value, configure Repository access:

  • All repositories — every repository in the organization can use this secret
  • Private repositories — only private repositories (useful for keeping secrets away from public forks)
  • Selected repositories — explicitly choose which repositories can access the secret

Example: Shared Deployment Credentials

A deployment key used across all services in an organization:

Organization secret: AWS_DEPLOY_KEY_ID
Organization secret: AWS_DEPLOY_KEY_SECRET
Repository access: Selected repositories
  - api-service
  - web-app
  - data-pipeline

Any workflow in those repositories can use ${{ secrets.AWS_DEPLOY_KEY_ID }} without each repository maintaining its own copy.


Environments

GitHub Environments add a governance layer on top of secrets. An environment represents a deployment target — typically production, staging, and preview — and can have:

  • Environment-specific secrets — secrets that differ per environment (e.g., different database URLs for staging vs production)
  • Environment variables — non-secret configuration that differs per environment
  • Protection rules — require human approval, impose wait timers, or restrict which branches can deploy

Creating Environments

Go to SettingsEnvironmentsNew environment. Name it (e.g., production, staging).

Environment Secrets

After creating an environment, add secrets and variables to it:

SettingsEnvironmentsproductionAdd secret

Environment secrets override repository secrets of the same name for workflows deploying to that environment. This allows you to use the same secret name (e.g., DATABASE_URL) in workflows but have different values per environment.

Using Environments in Workflows

yaml
jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - name: Deploy to staging
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
          # This resolves to the STAGING database URL
        run: ./deploy.sh staging

  deploy-production:
    runs-on: ubuntu-latest
    environment: production
    needs: deploy-staging
    steps:
      - name: Deploy to production
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
          # This resolves to the PRODUCTION database URL
        run: ./deploy.sh production

Environment Protection Rules

Protection rules add human gates and timing controls to deployments.

Required reviewers: Before a job targeting this environment can run, the listed people or teams must approve it. Up to 6 reviewers/teams can be required.

Environment: production
Required reviewers: @org/release-team (any 1 reviewer must approve)

When a workflow reaches a job targeting production, it pauses and sends approval requests. Reviewers see the pending deployment and can approve or reject it.

Wait timer: Adds a mandatory delay (1-43,200 minutes) after a deployment is triggered before it actually runs. Useful for canary deployment patterns where you want to observe metrics before proceeding.

Deployment branches: Restrict which branches can deploy to this environment.

Environment: production
Allowed branches: main, release/*

This prevents a feature/experiment branch from accidentally deploying to production even if someone triggers the workflow manually.


Dependabot Secrets

Dependabot is GitHub's automated dependency update service (covered more in the security section). When Dependabot opens a PR to update a dependency, any CI workflow triggered by that PR cannot access regular repository secrets (for security reasons — Dependabot is effectively an external contributor).

Dependabot secrets are a separate set of secrets specifically available to Dependabot-triggered workflows:

SettingsSecrets and variablesDependabotNew repository secret

yaml
# This workflow runs when Dependabot opens a PR
on:
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run integration tests
        env:
          API_KEY: ${{ secrets.INTEGRATION_TEST_API_KEY }}
          # This must be a Dependabot secret, not a regular secret,
          # for it to be available in Dependabot-triggered workflows
        run: npm run test:integration

Never Committing Secrets

The best way to handle secret leakage is prevention. Use these tools to catch secrets before they are committed.

git-secrets (AWS)

git-secrets is a pre-commit hook that scans staged changes for patterns matching known secret formats:

bash
# Install (macOS)
brew install git-secrets

# Set up in a repository
cd your-repo
git secrets --install
git secrets --register-aws

# Now any commit containing AWS credentials will be rejected
git secrets --add-provider -- git secrets --aws-provider

gitleaks

gitleaks is a more comprehensive secret scanning tool with detection rules for hundreds of secret types:

bash
# Install (macOS)
brew install gitleaks

# Scan the current repository for secrets
gitleaks detect --source . --verbose

# Scan a specific commit range
gitleaks detect --source . --log-opts "HEAD~10..HEAD"

# Use as a pre-commit hook
gitleaks protect --staged

trufflehog

trufflehog specializes in finding high-entropy strings (random-looking tokens) and known secret formats:

bash
# Install
pip install trufflehog

# Scan a GitHub repository
trufflehog git https://github.com/your-org/your-repo.git

# Scan local repository
trufflehog filesystem --directory=.

Pre-commit Framework

The pre-commit framework manages pre-commit hooks declaratively:

yaml
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: detect-private-key
      - id: detect-aws-credentials
bash
# Install pre-commit
pip install pre-commit

# Install the hooks
pre-commit install

# Now hooks run automatically on git commit
# Run manually on all files
pre-commit run --all-files

What To Do If You Commit a Secret

If you accidentally commit a secret, act immediately. The clock is ticking.

Step 1: Revoke the Secret First

Before doing anything with the repository, go to whatever service issued the credential and revoke or rotate it. Change the password, revoke the API key, delete and recreate the credentials. This is the most urgent step — removing the secret from the repository history does not help if it was already scraped.

Step 2: Remove from Repository History

Use git filter-repo (the modern replacement for git filter-branch):

bash
# Install git-filter-repo
pip install git-filter-repo

# Remove a specific file from all history
git filter-repo --path secrets.txt --invert-paths

# Replace a specific string throughout all history
git filter-repo --replace-text <(echo 'AKIAIOSFODNN7EXAMPLE==>REPLACED')

Step 3: Force Push and Notify

After rewriting history, force push all branches and delete all cached views:

bash
git push origin --force --all
git push origin --force --tags

Contact GitHub support to purge cached data if the repository is public.

Step 4: Rotate Again

Rotate the credential one more time after cleanup. Assume the original value was compromised and treat any access logs as potentially tainted.


GitHub Secret Scanning

GitHub automatically scans every commit pushed to repositories for patterns matching known secret formats from over 200 service providers (AWS, Google Cloud, Stripe, Twilio, etc.).

For Public Repositories

Secret scanning is enabled by default and free for all public repositories. When a secret is detected:

  1. GitHub alerts the repository owner.
  2. GitHub notifies the service provider directly (for participating providers), who can immediately revoke the exposed credential.
  3. A security alert appears in SecuritySecret scanning.

For Private Repositories

Secret scanning for private repositories requires GitHub Advanced Security (GHAS), which is included in GitHub Enterprise Cloud and available as an add-on for organizations.

Push Protection

Push protection takes secret scanning one step further: it blocks pushes that contain secrets before they reach GitHub.

Enable for a repository: SettingsCode security and analysisSecret scanningPush protection: Enable.

When push protection is active and a secret is detected in a push:

bash
git push origin main
# remote: error: GH013: Repository rule violations found for refs/heads/main.
# remote:
# remote: - GITHUB PUSH PROTECTION
# remote: —————————————————————————————————
# remote:   Resolve the following secrets before pushing again.
# remote:
# remote: (?) AWS Access Key ID:
# remote:     —————————————————————————————————————————————————————
# remote:      Location:     config/deploy.yml:8
# remote:      Commit:       a1b2c3d
# remote:
# remote:     To push, remove secret from commit(s) or proceed with bypass.

The push is rejected. The developer must remove the secret from the commit before pushing.


GitHub Advanced Security Overview

GitHub Advanced Security (GHAS) is a suite of security features for organizations:

  • Secret scanning (with push protection) — covered above
  • Code scanning — static analysis using CodeQL or third-party tools to find security vulnerabilities in code
  • Dependency review — shows the security impact of dependency changes in PRs (which new vulnerabilities are being introduced)
  • Dependabot alerts — notifications when your dependencies have known CVEs
  • Dependabot security updates — automatically opens PRs to fix vulnerable dependencies
  • Dependabot version updates — automatically opens PRs to keep dependencies up to date

Enabling Dependabot Alerts and Updates

These are free for all repositories:

SettingsCode security and analysis:

  • Enable Dependabot alerts
  • Enable Dependabot security updates
  • Enable Dependabot version updates (requires a .github/dependabot.yml configuration)

Dependabot Configuration

yaml
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: npm
    directory: /
    schedule:
      interval: weekly
      day: monday
      time: "09:00"
      timezone: "America/New_York"
    open-pull-requests-limit: 10
    labels:
      - dependencies
    reviewers:
      - org/backend-team
    commit-message:
      prefix: chore
      prefix-development: chore

  - package-ecosystem: docker
    directory: /
    schedule:
      interval: monthly
    labels:
      - dependencies
      - docker

This configuration tells Dependabot to open PRs every Monday morning for outdated npm dependencies, and once a month for Docker base image updates.


Practical Exercises

Exercise 1 — Create and Use Repository Secrets

  1. In a repository, create two secrets: TEST_API_KEY with value test-key-12345 and TEST_DB_URL with value postgres://localhost/testdb.
  2. Create a workflow that uses both secrets in environment variables and prints a masked version (e.g., echo "Key length: ${#TEST_API_KEY}").
  3. Run the workflow and check the logs — verify the actual values are masked.

Exercise 2 — Set Up Environments

  1. Create two environments: staging and production.
  2. Add a secret DATABASE_URL to each with different values.
  3. Add a required reviewer to the production environment.
  4. Create a workflow with two jobs targeting each environment.
  5. Run the workflow — observe that the production job waits for approval.

Exercise 3 — Install Gitleaks

  1. Install gitleaks locally.
  2. Run gitleaks detect on a repository.
  3. Deliberately add a fake secret (e.g., an AWS-format access key) to a file and run gitleaks detect --staged before committing.
  4. Verify gitleaks catches it.
  5. Remove the fake secret.

Exercise 4 — Configure Dependabot

  1. In a repository with a package.json (or requirements.txt), create .github/dependabot.yml.
  2. Configure Dependabot to check for updates weekly.
  3. Verify that Dependabot alerts are enabled in the security settings.
  4. If you have outdated dependencies, check the Security tab for any existing alerts.