Secrets Management in GitHub Actions
What are GitHub Secrets?
GitHub Secrets are encrypted environment variables stored securely in GitHub repositories, organizations, or environments. They protect sensitive data like API keys, passwords, tokens, and certificates from exposure in workflow files and logs.
Secrets enable secure automation without hardcoding credentials in code or workflows.
Why Secrets Management Matters
- Security - Prevents credential exposure in version control
- Access Control - Limits who can view and modify secrets
- Audit Trail - Tracks secret usage and modifications
- Compliance - Meets security standards and regulations
- Rotation - Facilitates regular credential updates
Secret Scopes
graph TD
A["GitHub Secrets"] -->B["Repository Secrets"]
A -->C["Environment Secrets"]
A -->D["Organization Secrets"]
B -->E["Available to
all workflows
in repository"]
C -->F["Available only in
specific environment
with approvals"]
D -->G["Shared across
multiple repositories
in organization"]
style B fill:#e1f5ff
style C fill:#fff3e0
style D fill:#e8f5e9
Creating Repository Secrets
Via GitHub UI
- Navigate to repository Settings
- Click Secrets and variables > Actions
- Click New repository secret
- Enter name and value
- Click Add secret
Using GitHub CLI
# Set repository secret
gh secret set API_KEY --body "your-secret-value"
# Set secret from file
gh secret set SSH_KEY < ~/.ssh/id_rsa
# Set secret interactively
gh secret set DATABASE_PASSWORD
Using Secrets in Workflows
name: Deploy Application
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to server
run: |
echo "Deploying application..."
curl -X POST https://api.example.com/deploy \
-H "Authorization: Bearer ${{ secrets.DEPLOY_TOKEN }}"
env:
API_KEY: ${{ secrets.API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
Important: Secrets are redacted in logs automatically.
Secret Naming Conventions
# Good naming practices
secrets.PROD_DATABASE_URL
secrets.STAGING_API_KEY
secrets.AWS_ACCESS_KEY_ID
secrets.SLACK_WEBHOOK_URL
# Avoid generic names
secrets.KEY
secrets.TOKEN
secrets.PASSWORD
Best practices:
- Use UPPERCASE with underscores
- Include environment prefix (PROD_, STAGING_)
- Be descriptive and specific
- Follow organizational naming standards
Environment-Specific Secrets
Environments provide deployment protection and environment-scoped secrets.
Creating Environment Secrets
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging
steps:
- name: Deploy
run: |
echo "Deploying to staging"
# Uses staging environment secrets
env:
API_KEY: ${{ secrets.API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
deploy-production:
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy
run: |
echo "Deploying to production"
# Uses production environment secrets
env:
API_KEY: ${{ secrets.API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
Behavior:
- Environment secrets override repository secrets with the same name
- Requires approval if environment protection rules are configured
- Provides clear separation between environments
Secret Resolution Order
graph TD
A["Workflow requests secret"] -->B{Environment
configured?}
B -->|Yes| C{Environment
secret exists?}
B -->|No| F{Repository
secret exists?}
C -->|Yes| D["Use environment secret"]
C -->|No| F
F -->|Yes| G["Use repository secret"]
F -->|No| H["Secret not found
empty value"]
style D fill:#ccffcc
style G fill:#fff3e0
style H fill:#ffcccc
Organization Secrets
Share secrets across multiple repositories in an organization.
Creating Organization Secrets
- Navigate to Organization Settings
- Click Secrets and variables > Actions
- Click New organization secret
- Select repository access policy
- Add secret
Repository Access Policies
# Organization secret visibility options:
# - All repositories
# - Private repositories only
# - Selected repositories
Using Organization Secrets
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo "Using org secret"
env:
ORG_API_KEY: ${{ secrets.ORG_API_KEY }}
Passing Secrets to Reusable Workflows
Secrets must be explicitly passed to reusable workflows.
Reusable Workflow with Secrets
# .github/workflows/reusable-deploy.yml
name: Reusable Deploy
on:
workflow_call:
secrets:
DEPLOY_TOKEN:
required: true
SLACK_WEBHOOK:
required: false
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: |
curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
-d '{"text":"Deployment started"}'
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
Calling Workflow with Secrets
jobs:
deploy:
uses: ./.github/workflows/reusable-deploy.yml
secrets:
DEPLOY_TOKEN: ${{ secrets.PROD_DEPLOY_TOKEN }}
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
Inheriting All Secrets
jobs:
deploy:
uses: ./.github/workflows/reusable-deploy.yml
secrets: inherit
Secret Flow in Reusable Workflows
sequenceDiagram
participant Caller as Caller Workflow
participant Reusable as Reusable Workflow
participant Runner as GitHub Runner
Caller->>Caller: Access repository secrets
Caller->>Reusable: Pass secrets explicitly
Reusable->>Runner: Use secrets in steps
Runner->>Runner: Redact in logs
Note over Runner: Secrets never exposed
Secret Security Best Practices
Never Log Secrets
# BAD - Never do this
steps:
- run: echo "API Key is ${{ secrets.API_KEY }}"
# GOOD - GitHub redacts automatically
steps:
- run: |
curl -H "Authorization: Bearer ${{ secrets.API_KEY }}" \
https://api.example.com
Use Short-Lived Tokens
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActions
aws-region: us-east-1
Benefits:
- No long-lived credentials stored as secrets
- OIDC token automatically expires
- Better security posture
Limit Secret Access
- Use environment secrets for production credentials
- Configure repository access for organization secrets
- Implement approval gates for sensitive environments
- Rotate secrets regularly
OIDC (OpenID Connect) Integration
Use OIDC to authenticate with cloud providers without storing credentials.
AWS OIDC Configuration
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: us-east-1
- run: aws s3 ls
Azure OIDC Configuration
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- run: az account show
Google Cloud OIDC Configuration
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: 'projects/123/locations/global/workloadIdentityPools/pool/providers/provider'
service_account: 'github-actions@project.iam.gserviceaccount.com'
- run: gcloud compute instances list
OIDC Benefits
graph LR
A["GitHub Actions"] -->|Request token| B["GitHub OIDC Provider"]
B -->|Issue short-lived token| A
A -->|Present token| C["Cloud Provider"]
C -->|Verify & grant access| D["Cloud Resources"]
style A fill:#e1f5ff
style B fill:#fff3e0
style C fill:#e8f5e9
style D fill:#ccffcc
Advantages:
- No stored credentials
- Automatic token expiration
- Granular permission control
- Audit trail in cloud provider logs
Managing Secrets with GitHub CLI
List Secrets
# List repository secrets
gh secret list
# List organization secrets
gh secret list --org my-org
Set Secrets
# Set from value
gh secret set API_KEY --body "abc123"
# Set from file
gh secret set CERTIFICATE < certificate.pem
# Set for organization
gh secret set API_KEY --org my-org --visibility all
Delete Secrets
# Delete repository secret
gh secret remove API_KEY
# Delete organization secret
gh secret remove API_KEY --org my-org
Encrypted Files in Repository
For large secrets (certificates, configuration files), use encrypted files.
Encrypt File
# Encrypt file with GPG
gpg --symmetric --cipher-algo AES256 certificate.pem
# Creates certificate.pem.gpg
# Commit encrypted file
git add certificate.pem.gpg
git commit -m "Add encrypted certificate"
Decrypt in Workflow
steps:
- uses: actions/checkout@v4
- name: Decrypt certificate
run: |
gpg --quiet --batch --yes --decrypt \
--passphrase="$DECRYPT_PASSPHRASE" \
--output certificate.pem \
certificate.pem.gpg
env:
DECRYPT_PASSPHRASE: ${{ secrets.DECRYPT_PASSPHRASE }}
- name: Use certificate
run: |
# Certificate now available as certificate.pem
echo "Certificate decrypted successfully"
Secret Rotation Strategy
graph TD
A["Generate new secret"] -->B["Add as NEW_SECRET"]
B -->C["Update workflows
to use NEW_SECRET"]
C -->D["Deploy and verify"]
D -->E["Remove OLD_SECRET"]
style A fill:#e1f5ff
style C fill:#fff3e0
style E fill:#ffcccc
Zero-Downtime Rotation
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Try new key first
id: new-key
continue-on-error: true
run: |
curl -H "Authorization: Bearer ${{ secrets.API_KEY_NEW }}" \
https://api.example.com/status
- name: Fall back to old key
if: steps.new-key.outcome == 'failure'
run: |
curl -H "Authorization: Bearer ${{ secrets.API_KEY }}" \
https://api.example.com/status
Common Secret Patterns
Database Connection Strings
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
# Format: postgres://user:password@host:5432/database
Multi-Part Credentials
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
API Keys with Multiple Environments
jobs:
deploy-staging:
environment: staging
steps:
- run: echo "Using staging API key"
env:
API_KEY: ${{ secrets.API_KEY }}
deploy-production:
environment: production
steps:
- run: echo "Using production API key"
env:
API_KEY: ${{ secrets.API_KEY }}
Security Checklist
Real-World Example: Multi-Environment Deployment
name: Multi-Environment Deploy
on:
push:
branches:
- main
- develop
jobs:
deploy-staging:
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- name: Configure AWS
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- name: Deploy application
run: |
aws s3 sync ./dist s3://${{ secrets.S3_BUCKET }}
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.CLOUDFRONT_ID }} \
--paths "/*"
- name: Notify Slack
if: always()
run: |
curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
-d '{"text":"Staging deployment ${{ job.status }}"}'
deploy-production:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Configure AWS with OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- name: Deploy application
run: |
aws s3 sync ./dist s3://${{ secrets.S3_BUCKET }}
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.CLOUDFRONT_ID }} \
--paths "/*"
- name: Notify team
if: success()
run: |
curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
-d '{"text":"Production deployment successful!"}'
Key Takeaways
- GitHub Secrets store encrypted credentials for workflows
- Three scopes exist: repository, environment, and organization secrets
- Environment secrets provide deployment protection and approval gates
- Secrets are automatically redacted in workflow logs
- OIDC eliminates the need for storing long-lived cloud credentials
- Pass secrets explicitly to reusable workflows or use
secrets: inherit - Use GitHub CLI for programmatic secret management
- Encrypt large files with GPG and decrypt in workflows
- Rotate secrets regularly using zero-downtime strategies
- Follow naming conventions with environment prefixes
- Never log or expose secrets in workflow output
- Implement approval gates for production environments
Next Steps: Audit existing secrets, implement OIDC for cloud providers, configure environment-specific secrets with approval gates, and establish a secret rotation schedule.