This repository demonstrates how secrets isolation, permission lifting, and secret precedence work in GitHub Actions reusable workflows (workflow_call).
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:.
There are two ways to lift the secret isolation and grant a reusable workflow access to secrets:
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 valueWith 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
The caller grants blanket access to all secrets:
jobs:
call-reusable:
uses: ./.github/workflows/reusable_workflow_with_inherit.yaml
with:
github_environment: dev
secrets: inheritWith 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
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 |
- 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.
| 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 |
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:
- Without inherit — passes secrets explicitly, including one hardcoded override.
- With inherit — uses
secrets: inheritfor blanket access.
reusable_workflow.yaml — Explicit Secrets
Declares required secrets under on.workflow_call.secrets. Tests:
GH_ENV_SECRET_ONLY— exists only at environment levelSECRET_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.
reusable_workflow_with_inherit.yaml — Inherited Secrets
Does not declare secrets. Tests the same scenarios but with secrets: inherit:
- All secrets are accessible without explicit declaration.
SECRET_OVERRIDE_BY_VALUEis not overridden (no hardcoded value), so the environment value is used.- A second job without
environment:to show repo-level fallback.
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 |
| 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 |
| 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 |
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) |