Jan 15, 2025
 ]

Best Practices for Managing Secrets in GitHub Actions

Aditya Jayaprakash
TL;DR
Rotate GitHub Actions secrets regularly (30-90 days), use OIDC over long-lived tokens, implement environment-based access controls with approval workflows, avoid hardcoded secrets, and use descriptive naming conventions, or supply chain attackers will own your deployment pipeline.
Get started!
Try us Free

GitHub Actions workflows frequently handle sensitive data like API keys, database credentials, and access tokens that power CI/CD pipelines. Recent supply chain attacks have increasingly targeted CI systems, with compromised secrets being at the heart of these breaches. While GitHub Actions excels at workflow automation, improper handling of these secrets can expose your infrastructure to unauthorized access, leading to supply chain compromises that affect not just your pipeline but your entire software delivery chain.

Whether you're deploying to production or integrating with third-party services, proper secrets management is crucial if it's the difference between a secure pipeline and a potential entry point for attackers. As development teams scale and workflows grow in complexity, implementing structured and secure secrets management becomes critical for maintaining the integrity of your software.

In this post, we will walk through tried and tested practices for managing secrets in GitHub Actions, from basic repository-level secrets to advanced environment-specific configurations. We'll explore how to implement robust security measures that protect your sensitive data in CI.

Understanding GitHub Actions secrets

GitHub Actions secrets are encrypted environment variables that securely store sensitive information needed in your CI/CD workflows. These secrets can include API keys, access tokens, database credentials, or any confidential data in your workflows that shouldn't be exposed in your code.

GitHub provides three distinct levels of secret management:

  1. Repository Secrets: Repository secrets are specific to a single repository and are available to all workflows within that repository. These are ideal for project-specific credentials like deployment keys or service-specific API tokens. Only repository owners and collaborators with appropriate permissions can access and modify these secrets.
  2. Environment Secrets: Environment secrets add an extra layer of security by being specific to designated environments (like development, staging, or production). Their key advantage is that they can require approval from designated reviewers before workflows can access them.
  3. Organization Secrets: Organization secrets are shared across multiple repositories within an organization. They're particularly useful for managing common credentials that multiple teams or projects need to access. Organization administrators can control which repositories can access these secrets through detailed access policies.

While these different types of secrets provide flexibility in managing sensitive data, their effectiveness relies heavily on GitHub's underlying security infrastructure. Next, let's understand how GitHub ensures your secrets are protected.

How secrets work in GitHub Actions

Here's how GitHub protects secrets at each level:

  1. Encryption: GitHub uses Libsodium sealed boxes to encrypt secrets before they reach GitHub's servers. This ensures that secrets remain encrypted until they're actually needed in a workflow. For a detailed understanding of how GitHub does this, refer to GitHub's official documentation on security hardening for GitHub Actions.
  2. Access Control: Secrets are only accessible to workflows that explicitly reference them. Even then, they're only decrypted when the workflow runs and are automatically redacted from any logs.
  3. Inheritance and Precedence: Secrets follow a specific precedence order, Environment secrets take priority over repository secrets, which in turn take precedence over organization secrets.
  4. Runtime Protection: When workflows run, GitHub automatically masks any secrets that accidentally get printed to the logs, providing an additional layer of protection against accidental exposure.

Now that we understand how secrets work in GitHub Actions, let's explore the best practices that will help you manage them effectively and securely.

Best practices for managing secrets

Proper secret management practices is crucial as CI/CD workflows are at the center of supply chain attacks. Attackers can extract secrets by modifying workflows to send data to external servers, exploiting vulnerable third-party actions, or executing malicious code during workflow runs. With access to these secrets, attackers can potentially access your cloud infrastructure, modify repository contents, or even steal the GITHUB_TOKEN to perform unauthorized actions. Let's explore things you could do to protect your secrets and minimize these security risks in GitHub Actions.

Access control

The principle of least privilege is fundamental when managing secrets in GitHub Actions. Each secret should be accessible only to workflows and users who absolutely need them - no more, no less. Here's how to implement strong access controls:

Workflow-Level access

  • Explicitly define which workflows can access specific secrets using environment protection rules
  • Create separate environments (like 'production', 'staging') with different access levels
  • Require approval from specific team members before workflows can access sensitive production secrets
  • Use conditional steps in workflows to limit when secrets are accessed, even within approved workflows

Example of environment protection in a workflow:

name: Deploy to Production
on: [push]
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production    # This environment requires approval
    steps:
      - uses: actions/checkout@v3
      - name: Deploy
        env:
          AWS_ACCESS_KEY: ${{ secrets.PROD_AWS_ACCESS_KEY }}
        run: |
          # Deploy code only after environment approval

Repository and organization scoping

  • For organization secrets, avoid organization-wide access by default
  • Use repository access policies to explicitly grant access to specific repositories
  • Consider creating repository groups based on team ownership or project boundaries
  • Implement CODEOWNERS to ensure workflow changes that use secrets are properly reviewed

Regular access reviews

  • Conduct monthly audits of secret access patterns
  • Remove access for completed projects or deprecated workflows
  • Track and document why each secret needs access to specific workflows
  • Use GitHub's audit logs to monitor secret usage and detect unusual patterns

OpenID Connect (OIDC) integration

While storing cloud credentials as GitHub Secrets is common, it poses significant security risks - from potential secret leaks to credential rotation headaches. OIDC provides a more secure alternative by enabling your GitHub Actions workflows to authenticate directly with cloud providers using short-lived tokens.

How OIDC works in GitHub Actions

  1. Your workflow requests a JWT token from GitHub's OIDC Provider
  2. This token contains claims about your workflow (repository, branch, environment)
  3. Your cloud provider validates this token and exchanges it for temporary credentials
  4. These credentials are only valid for that specific workflow run

Here's an example for using an OIDC token with AWS:

# .github/workflows/deploy.yml
jobs:
  deploy:
    runs-on: ubuntu-latest
    # Required permissions for OIDC
    permissions:
      id-token: write   # Required for OIDC
      contents: read    # Required for actions/checkout
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
          aws-region: us-east-1
          
      # Now you can use AWS CLI/SDK with temporary credentials
      - name: Deploy to AWS
        run: |
          aws s3 sync ./dist s3://my-bucket

Here's the corresponding AWS IAM Role configuration:

{
    "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:octo-org/octo-repo:ref:refs/heads/main"
                }
            }
        }
    ]
}

Security Benefits

The key benefit is that no long-lived credentials are stored on GitHub. moreover, tokens are automatically rotated for each workflow run. You're also able to get fine-grained control over which repositories/branches can assume roles. You would also be able to get an audit trail automatically from your cloud provider logs on token issuance and usage.

Best Practices

  • Restrict role permissions to only what workflows need
  • Use condition keys to limit access to specific branches or environments
  • Regularly audit OIDC trust relationships and role permissions
  • Consider implementing additional controls like requiring specific GitHub Actions environments

This approach significantly reduces the attack surface compared to storing long-lived credentials as secrets, while providing better auditability and access control.

Secret rotation

Regular secret rotation helps limit potential damage from compromised credentials. Here's a detailed, actionable approach to implement secret rotation in your GitHub Actions workflows. We recommend that critical secrets like prod database credentials are rotated every 30 days, high risk secrets are rotated every 60 days and other secrets are rotated every 90 days.

It also makes sense to run mock emergency rotation drills to prepare for the event where a secret(s) has been compromised and you want to get them rotated as quickly as possible.

Avoiding hardcoded secrets

Hardcoding secrets in your codebase or workflow files is one of the most common causes of security breaches. Let's explore comprehensive practices to prevent secret exposure:

Common anti-patterns to avoid:

# ❌ BAD: Hardcoding secrets in workflows
jobs:
  deploy:
    steps:
      - run: |
          AWS_ACCESS_KEY=AKIA1234567890
          AWS_SECRET_KEY=abcdef1234567890

# ❌ BAD: Storing secrets in plain text files
database:
  password: "prod_password_123"

Good Implementation Patterns:

# GOOD: Using GitHub Secrets context
jobs:
  deploy:
    steps:
      - name: Deploy to AWS
        env:
          AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }}
          AWS_SECRET_KEY: ${{ secrets.AWS_SECRET_KEY }}

Local development security

  1. Use .env.example for documentation:
  2. Implement strict .gitignore:

Additional prevention measures

  1. Configure branch protection rules to require reviews
  2. Use secret detection tools in your CI pipeline. There are a number of services that do this:

Environment-based secret management

Environment secrets provide granular control over who can access secrets in different deployment stages. Unlike repository secrets, they require explicit approval from designated reviewers before workflows can access them.

Setting up environment protection rules

# Example of a protected environment configuration
name: Production
protection_rules:
  - required_reviewers:
      - security-team-lead
      - devops-lead
  - wait_timer: 30
  - allowed_branches:
      - main
      - release/*

Implementing environment secrets

# .github/workflows/deploy.yml
name: Deploy Application
on:
  push:
    branches: [main]

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/checkout@v3
      - name: Deploy to Staging
        env:
          API_KEY: ${{ secrets.STAGING_API_KEY }}
          DB_PASSWORD: ${{ secrets.STAGING_DB_PASSWORD }}

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production  # Requires approval
    steps:
      - uses: actions/checkout@v3
      - name: Deploy to Production
        env:
          API_KEY: ${{ secrets.PROD_API_KEY }}
          DB_PASSWORD: ${{ secrets.PROD_DB_PASSWORD }}

Here are some benefits of doing this:

  • Separate secrets for different environments (dev, staging, prod)
  • Required approvals before accessing production secrets
  • Audit trail of secret access and approvals
  • Branch restrictions for production deployments
  • Automatic secret redaction in logs

This approach also ensures that production secrets are only accessed through approved workflows, reducing the risk of unauthorized access or accidental deployments. On a lighter note, we all know or have been that one intern who accidentally deployed to prod.

Naming conventions

Having clear and consistent naming conventions are pretty essential for managing secrets when you have a large codebase. Instead of generic names like API_KEY or TOKEN, use descriptive names that indicate the service, environment, and purpose:

  • Use prefixes to indicate the service: AWS_PROD_ACCESS_KEY or DOCKER_REGISTRY_TOKEN
  • Include environment indicators: STAGING_DB_PASSWORD or PROD_DB_ KEY
  • Add purpose suffixes when needed: GCP_DEPLOY_SERVICE_ACCOUNT or SLACK_NOTIFICATION_WEBHOOK

This naming structure helps teams quickly identify secrets' purposes and reduces the risk of using incorrect credentials in workflows.

Security measures in GitHub Actions

GitHub Actions provides built-in security mechanisms to protect sensitive information, but understanding their limitations is crucial for maintaining security.

Built-in Redaction

GitHub automatically redacts secrets from workflow logs in several ways  through pattern matching:

# This will be masked: ghp_123456789abcdef
# This won't be masked: base64_encode(ghp_123456789abcdef)

The masking applies to:

  • Configured GitHub secrets
  • OAuth, JWT, Bearer tokens, etc
  • Database connection strings
  • GitHub tokens (PATs and installation tokens)
  • Personal access tokens
  • SSH keys

However, there are a few scenarios to watch out for:

  1. Debug Commands Exposing Secrets
  2. Base64 Encoding Bypassing Masking
  3. JSON/YAML Structured Secrets:

Proactive protection measures

  1. Implement Runtime Masking: Mask dynamically generated secrets and derived values that GitHub's automatic masking won't catch, like tokens generated during workflow execution.
  2. Log Protection: Prevent accidental secret exposure by redirecting error outputs and implementing careful error handling in commands that might expose secrets in their response.
  3. Environment Isolation: Use environment protection rules to ensure secrets are only available after required approvals, providing an additional layer of security for sensitive operations

Secrets management

We also wanted to point that external secret management solutions offer advantages over GitHub's native secrets manager like centralized management across multiple platforms, automated secret rotation, audit logging and granular access controls. If you're a larger enterprise with a complex use case, we recommend HashiCorp Vault. We're also fans of Infisical, an open-source solution. They have a pretty active community, with over 16K GitHub stars and 170+ contributors. Another solution we like is Doppler, which is used by over 50k companies. We use Doppler at Blacksmith and are fans of what they have built.

World globe

Start with 3,000 free minutes per month or book a live demo with our engineers