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


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

  1. Navigate to repository Settings
  2. Click Secrets and variables > Actions
  3. Click New repository secret
  4. Enter name and value
  5. 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:


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:


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

  1. Navigate to Organization Settings
  2. Click Secrets and variables > Actions
  3. Click New organization secret
  4. Select repository access policy
  5. 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:

Limit Secret Access


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:


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

Next Steps: Audit existing secrets, implement OIDC for cloud providers, configure environment-specific secrets with approval gates, and establish a secret rotation schedule.