Skip to content

pmalarme/github-secrets

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

39 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GitHub Secrets: Permission Lifting & Precedence in Reusable Workflows

This repository demonstrates how secrets isolation, permission lifting, and secret precedence work in GitHub Actions reusable workflows (workflow_call).

Key Concepts

1. Secrets Isolation in Reusable Workflows

By default, reusable workflows are sandboxed — they cannot access any secrets from the caller, even if the secret exists at the repository or environment level. This is a security-by-design decision by GitHub to prevent untrusted reusable workflows from silently accessing sensitive data.

Important

This isolation applies only to secrets (secrets.*). Environment variables (vars.*) are not isolated and are automatically accessible when the job declares an environment:.

2. Lifting Secret Isolation

There are two ways to lift the secret isolation and grant a reusable workflow access to secrets:

Option A: Explicit Secret Passing

The caller explicitly maps each secret to the called workflow. The caller can either forward a secret reference or hardcode a value:

jobs:
  call-reusable:
    uses: ./.github/workflows/reusable_workflow.yaml
    with:
      github_environment: dev
    secrets:
      GH_ENV_SECRET_ONLY: ${{ secrets.GH_ENV_SECRET_ONLY }}         # Forward secret reference
      SECRET_AT_REPO_AND_GH_ENV_LEVELS: ${{ secrets.SECRET_AT_REPO_AND_GH_ENV_LEVELS }}
      SECRET_OVERRIDE_BY_VALUE: "CallerWorkflow"                     # Hardcode a value

With explicit passing:

  • The called workflow must declare each secret it expects under on.workflow_call.secrets.
  • The caller controls exactly which secrets are shared — principle of least privilege.
  • The caller can override a secret's value by hardcoding it (e.g., "CallerWorkflow").

Note

When the caller writes GH_ENV_SECRET_ONLY: ${{ secrets.GH_ENV_SECRET_ONLY }}, it does not pass the actual value if the secret only exists at the environment level and the caller job has no environment: set. Instead, it lifts the isolation, allowing the called workflow to resolve the secret from its own environment: declaration.

The following diagram illustrates how a secret is resolved when using explicit passing:

flowchart TD
    A[Caller passes secret to reusable workflow] --> B{Is the value hardcoded in the caller?}
    B -- Yes --> C[✅ Use hardcoded value from caller]
    B -- No --> D{Does the called workflow job declare environment?}
    D -- Yes --> E{Does the secret exist at environment level?}
    D -- No --> F{Does the secret exist at repository level?}
    E -- Yes --> G[✅ Use environment secret]
    E -- No --> F
    F -- Yes --> H[✅ Use repository secret]
    F -- No --> I{Does the secret exist at organization level?}
    I -- Yes --> J[✅ Use organization secret]
    I -- No --> K[❌ Secret is empty]

    style C fill:#2da44e,color:#fff
    style G fill:#2da44e,color:#fff
    style H fill:#2da44e,color:#fff
    style J fill:#2da44e,color:#fff
    style K fill:#cf222e,color:#fff
Loading

Option B: secrets: inherit

The caller grants blanket access to all secrets:

jobs:
  call-reusable:
    uses: ./.github/workflows/reusable_workflow_with_inherit.yaml
    with:
      github_environment: dev
    secrets: inherit

With secrets: inherit:

  • The called workflow does not need to declare secrets under on.workflow_call.secrets.
  • All secrets from organization, repository, and environment levels become available.
  • The caller cannot override individual secret values — it's all-or-nothing.

The following diagram illustrates how a secret is resolved when using secrets: inherit:

flowchart TD
    A[Caller uses secrets: inherit] --> B{Does the called workflow job declare environment?}
    B -- Yes --> C{Does the secret exist at environment level?}
    B -- No --> D{Does the secret exist at repository level?}
    C -- Yes --> E[✅ Use environment secret]
    C -- No --> D
    D -- Yes --> F[✅ Use repository secret]
    D -- No --> G{Does the secret exist at organization level?}
    G -- Yes --> H[✅ Use organization secret]
    G -- No --> I[❌ Secret is empty]

    style E fill:#2da44e,color:#fff
    style F fill:#2da44e,color:#fff
    style H fill:#2da44e,color:#fff
    style I fill:#cf222e,color:#fff
Loading

3. Secret Precedence

When a secret with the same name exists at multiple levels, GitHub resolves it using this precedence order (highest to lowest):

Priority Source Description
1 (highest) Caller hardcoded value Value explicitly set in the caller (e.g., SECRET: "CallerWorkflow")
2 Environment secret Secret defined in the GitHub environment, when the job has environment: set
3 Repository secret Secret defined at the repository level
4 (lowest) Organization secret Secret defined at the organization level

Precedence Rules:

  • Environment secrets override repository secrets when the job declares environment:.
  • Caller hardcoded values override everything, because the called workflow receives the literal value instead of a reference.
  • Without environment: on the job, environment-level secrets are inaccessible, and the repository-level value is used instead.

4. Variables vs Secrets

Aspect vars.* (Variables) secrets.* (Secrets)
Sensitivity Non-sensitive configuration Sensitive credentials
Visible in logs Yes (printed as-is) No (masked with ***)
Isolated in reusable workflows No — available automatically Yes — requires inherit or explicit passing
Accessed via environment: Automatically, no lifting needed Only after isolation is lifted

Repository Structure

This repository contains three workflow files that demonstrate these concepts:

test_workflow.yaml — Caller Workflow

The entry point. Dispatched manually with a github_environment input. It calls two reusable workflows:

  1. Without inherit — passes secrets explicitly, including one hardcoded override.
  2. With inherit — uses secrets: inherit for blanket access.

reusable_workflow.yaml — Explicit Secrets

Declares required secrets under on.workflow_call.secrets. Tests:

  • GH_ENV_SECRET_ONLY — exists only at environment level
  • SECRET_AT_REPO_AND_GH_ENV_LEVELS — exists at both repo and environment level (environment wins)
  • SECRET_OVERRIDE_BY_VALUE — overridden by hardcoded value from caller ("CallerWorkflow" wins)
  • A second job without environment: to show that repo-level secrets are used when no environment is set.

Does not declare secrets. Tests the same scenarios but with secrets: inherit:

  • All secrets are accessible without explicit declaration.
  • SECRET_OVERRIDE_BY_VALUE is not overridden (no hardcoded value), so the environment value is used.
  • A second job without environment: to show repo-level fallback.

Test Scenarios & Expected Results

Assuming the following secret configuration:

Secret Name Organization Repository Environment (dev)
GH_ENV_SECRET_ONLY Environment
SECRET_AT_REPO_AND_GH_ENV_LEVELS Repository Environment
SECRET_OVERRIDE_BY_VALUE Environment

Expected Results — Explicit Passing (no inherit)

Secret With environment: dev Without environment:
GH_ENV_SECRET_ONLY Environment N/A
SECRET_AT_REPO_AND_GH_ENV_LEVELS Environment (env overrides repo) Repository
SECRET_OVERRIDE_BY_VALUE CallerWorkflow (hardcoded overrides env) N/A

Expected Results — secrets: inherit

Secret With environment: dev Without environment:
GH_ENV_SECRET_ONLY Environment N/A
SECRET_AT_REPO_AND_GH_ENV_LEVELS Environment (env overrides repo) Repository
SECRET_OVERRIDE_BY_VALUE Environment (no hardcoded override) N/A

When to Use Which Approach

Important

Recommendation: Following the principle of least privilege, it is recommended to explicitly declare and pass secrets to reusable workflows rather than using secrets: inherit. This ensures each workflow only has access to the secrets it actually needs, reducing the blast radius in case of a compromised or misconfigured workflow.

Scenario Recommended Approach
Called workflow resolves its own environment secrets secrets: inherit
Internal/trusted reusable workflows secrets: inherit (simpler)
Third-party/external reusable workflows Explicit passing (safer)
Need to override a secret value Explicit passing (only option)
Security-critical pipelines Explicit passing (least privilege)

References

About

No description, website, or topics provided.

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors