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.
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:
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.
Here's how GitHub protects secrets at each level:
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.
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.
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
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
Regular access reviews
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
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
This approach significantly reduces the attack surface compared to storing long-lived credentials as secrets, while providing better auditability and access control.
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.
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
.env.example
for documentation:
.gitignore
:Additional prevention measures
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:
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.
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:
AWS_PROD_ACCESS_KEY
or DOCKER_REGISTRY_TOKEN
STAGING_DB_PASSWORD or PROD_DB_ KEY
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.
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:
However, there are a few scenarios to watch out for:
Proactive protection measures
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.