Skip to content

Commit e9ddcd7

Browse files
authored
Strict mode: allow secrets.* in step-level with: for uses: action steps (#25871)
1 parent aa99024 commit e9ddcd7

File tree

5 files changed

+387
-91
lines changed

5 files changed

+387
-91
lines changed

docs/adr/0002-allow-secrets-in-step-env-bindings-under-strict-mode.md

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ The workflow compiler enforces a "strict mode" that restricts what GitHub Action
1414

1515
### Decision
1616

17-
We will introduce a per-field classification of secret references within a step, distinguishing "safe" bindings (`env:` fields, which are automatically masked by GitHub Actions) from "unsafe" inline interpolations (`run:`, `with:`, and all other step fields). In strict mode, only unsafe secret references will be treated as errors; secrets that appear exclusively in `env:` bindings will be permitted. We implement this via a new `classifyStepSecrets()` helper that partitions a step's secret references, and update `validateStepsSectionSecrets()` to only block the unsafe partition under strict mode.
17+
We will introduce a per-field classification of secret references within a step, distinguishing "safe" bindings (`env:` fields, which are automatically masked by GitHub Actions, and `with:` inputs for `uses:` action steps, which are passed to external actions and masked by the runner) from "unsafe" inline interpolations (`run:` and all other step fields). In strict mode, only unsafe secret references will be treated as errors; secrets that appear exclusively in safe bindings will be permitted. We implement this via a new `classifyStepSecrets()` helper that partitions a step's secret references, and update `validateStepsSectionSecrets()` to only block the unsafe partition under strict mode.
1818

1919
### Alternatives Considered
2020

@@ -33,19 +33,20 @@ A third option was to keep secrets blocked by default but allow authors to annot
3333
### Consequences
3434

3535
#### Positive
36-
- Workflows that supply tool credentials via `env:` bindings no longer need to disable strict mode entirely, preserving all other strict-mode protections.
36+
- Workflows that supply tool credentials via `env:` bindings or `with:` inputs (for `uses:` action steps) no longer need to disable strict mode entirely, preserving all other strict-mode protections.
3737
- The enforcement policy now mirrors how the framework's own generated jobs handle secrets, making the security model internally consistent.
38-
- The error message for blocked secrets now explicitly suggests `env:` bindings as an alternative, improving the developer experience.
39-
- The behavior aligns with GitHub Actions' native masking guarantee: `env:` bindings are masked by the runner before command execution.
38+
- The error message for blocked secrets now explicitly suggests `env:` bindings and `with:` inputs as alternatives, improving the developer experience.
39+
- The behavior aligns with GitHub Actions' native masking guarantee: `env:` bindings and `with:` inputs are masked by the runner before command execution.
40+
- Enterprise workflows that use centralized secret managers (e.g., Conjur, HashiCorp Vault) via dedicated GitHub Actions can now use strict mode, passing authentication credentials via `with:` inputs.
4041

4142
#### Negative
42-
- The security policy is now more nuanced: reviewers must understand the `env:` vs. inline distinction rather than a simple blanket rule, increasing the surface area to reason about.
43+
- The security policy is now more nuanced: reviewers must understand the `env:`/`with:` vs. inline distinction rather than a simple blanket rule, increasing the surface area to reason about.
4344
- The `classifyStepSecrets()` function must be kept accurate as the step data model evolves; an incorrectly classified field could silently downgrade a secret from "unsafe" to "safe".
44-
- Non-strict mode still emits a warning for all secrets (including `env:`-bound ones), which may be slightly misleading now that `env:` bindings are considered safe in strict mode.
45+
- Non-strict mode still emits a warning for all secrets (including safe-bound ones), which may be slightly misleading now that safe bindings are permitted in strict mode.
4546

4647
#### Neutral
47-
- Existing workflows that used `strict: false` specifically to allow `env:` bindings can now remove that override and adopt strict mode, but this migration is voluntary.
48-
- Unit and integration tests must now cover both the "env-only" allowed path and the "mixed env + run" blocked path to maintain adequate coverage.
48+
- Existing workflows that used `strict: false` specifically to allow safe bindings can now remove that override and adopt strict mode, but this migration is voluntary.
49+
- Unit and integration tests must now cover both the "safe-binding-only" allowed path and the "mixed safe + run" blocked path to maintain adequate coverage.
4950

5051
---
5152

@@ -56,18 +57,21 @@ A third option was to keep secrets blocked by default but allow authors to annot
5657
### Secret Classification
5758

5859
1. Implementations **MUST** classify each `${{ secrets.* }}` reference within a step by the name of the YAML field in which it appears.
59-
2. A secret reference found inside a well-formed `env:` mapping (i.e., the `env:` value is a YAML map of key-value pairs) **MUST** be classified as an *env-bound* reference.
60+
2. A secret reference found inside a well-formed `env:` mapping (i.e., the `env:` value is a YAML map of key-value pairs) **MUST** be classified as a *safe* reference.
6061
3. A secret reference found inside a malformed `env:` value (e.g., `env:` is a bare string or a YAML sequence) **MUST** be classified as an *unsafe* reference, because the runner cannot apply per-variable masking to such values.
61-
4. A secret reference found in any other step field (including but not limited to `run:`, `with:`, `name:`, `if:`) **MUST** be classified as an *unsafe* reference.
62-
5. A step value that is not a YAML map (e.g., a raw string) **MUST** treat all secret references within it as *unsafe* references.
63-
6. When a step contains *env-bound* secret references AND any non-`env:` field references `GITHUB_ENV`, implementations **MUST** reclassify all *env-bound* references in that step as *unsafe*, because writing to `GITHUB_ENV` persists secrets to subsequent steps (including the agent step).
62+
4. A secret reference found inside a well-formed `with:` mapping in a step that also has a `uses:` field **MUST** be classified as a *safe* reference, because `with:` inputs are passed to the external action (not interpolated into shell scripts) and the runner masks values derived from secrets.
63+
5. A secret reference found inside a `with:` mapping in a step that does NOT have a `uses:` field **MUST** be classified as an *unsafe* reference.
64+
6. A secret reference found inside a malformed `with:` value (e.g., `with:` is a bare string or a YAML sequence) **MUST** be classified as an *unsafe* reference.
65+
7. A secret reference found in any other step field (including but not limited to `run:`, `name:`, `if:`) **MUST** be classified as an *unsafe* reference.
66+
8. A step value that is not a YAML map (e.g., a raw string) **MUST** treat all secret references within it as *unsafe* references.
67+
9. When a step contains *safe* secret references AND any non-`env:`/non-`with:` field references `GITHUB_ENV`, implementations **MUST** reclassify all *safe* references in that step as *unsafe*, because writing to `GITHUB_ENV` persists secrets to subsequent steps (including the agent step).
6468

6569
### Strict Mode Enforcement
6670

6771
1. When strict mode is active, implementations **MUST** return an error if one or more *unsafe* secret references are found in a `pre-steps`, `steps`, or `post-steps` section.
68-
2. When strict mode is active, implementations **MUST NOT** return an error solely because *env-bound* secret references are present in a section.
69-
3. The error message for blocked secrets in strict mode **SHOULD** suggest the use of step-level `env:` bindings as an alternative to inline interpolation.
70-
4. The built-in `GITHUB_TOKEN` **MUST** be filtered out from both *unsafe* and *env-bound* reference lists before strict-mode enforcement, as it is present in every runner environment by default.
72+
2. When strict mode is active, implementations **MUST NOT** return an error solely because *safe* secret references are present in a section.
73+
3. The error message for blocked secrets in strict mode **SHOULD** suggest the use of step-level `env:` bindings (for `run:` steps) or `with:` inputs (for `uses:` action steps) as alternatives to inline interpolation.
74+
4. The built-in `GITHUB_TOKEN` **MUST** be filtered out from both *unsafe* and *safe* reference lists before strict-mode enforcement, as it is present in every runner environment by default.
7175

7276
### Non-Strict Mode Behavior
7377

@@ -77,7 +81,7 @@ A third option was to keep secrets blocked by default but allow authors to annot
7781

7882
### Conformance
7983

80-
An implementation is considered conformant with this ADR if it satisfies all **MUST** and **MUST NOT** requirements above. Failure to meet any **MUST** or **MUST NOT** requirement constitutes non-conformance. In particular: permitting *unsafe* secret references in strict mode, or blocking *env-bound* references in strict mode, are both non-conformant behaviors.
84+
An implementation is considered conformant with this ADR if it satisfies all **MUST** and **MUST NOT** requirements above. Failure to meet any **MUST** or **MUST NOT** requirement constitutes non-conformance. In particular: permitting *unsafe* secret references in strict mode, or blocking *safe* references in strict mode, are both non-conformant behaviors.
8185

8286
---
8387

pkg/workflow/strict_mode_steps_secrets_integration_test.go

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,50 @@ post-steps:
136136
# Post-Steps Env Secret Test
137137
138138
Post-steps with secrets in env bindings.
139+
`,
140+
},
141+
{
142+
name: "secrets in with for uses action step compile in strict mode",
143+
content: `---
144+
on: workflow_dispatch
145+
permissions:
146+
contents: read
147+
issues: read
148+
pull-requests: read
149+
engine: copilot
150+
steps:
151+
- uses: my-org/secrets-action@v2
152+
with:
153+
username: ${{ secrets.VAULT_USERNAME }}
154+
password: ${{ secrets.VAULT_PASSWORD }}
155+
secret_map: static-value
156+
---
157+
158+
# With Secrets in Uses Action Test
159+
160+
Action steps with secrets in with: inputs should compile in strict mode.
161+
`,
162+
},
163+
{
164+
name: "mixed env and with secrets for uses action step compile in strict mode",
165+
content: `---
166+
on: workflow_dispatch
167+
permissions:
168+
contents: read
169+
issues: read
170+
pull-requests: read
171+
engine: copilot
172+
steps:
173+
- uses: my-org/secrets-action@v2
174+
env:
175+
EXTRA_TOKEN: ${{ secrets.EXTRA_TOKEN }}
176+
with:
177+
username: ${{ secrets.VAULT_USERNAME }}
178+
---
179+
180+
# Mixed Env and With Secrets Test
181+
182+
Both env: and with: secrets in a uses: action step should compile.
139183
`,
140184
},
141185
}
@@ -165,8 +209,8 @@ Post-steps with secrets in env bindings.
165209
}
166210

167211
// TestStrictModeStepUnsafeSecretsBlocked tests that secrets in non-env step
168-
// fields (run, with, etc.) are still blocked in strict mode during full
169-
// compilation.
212+
// fields (run, etc.) are still blocked in strict mode during full compilation.
213+
// Note: secrets in with: for uses: action steps are now allowed (safe binding).
170214
func TestStrictModeStepUnsafeSecretsBlocked(t *testing.T) {
171215
tests := []struct {
172216
name string
@@ -194,7 +238,7 @@ This should fail because secrets are used inline in run.
194238
errorMsg: "strict mode: secrets expressions detected in 'steps' section",
195239
},
196240
{
197-
name: "secret in with field is blocked in strict mode",
241+
name: "secret in with field without uses is blocked in strict mode",
198242
content: `---
199243
on: workflow_dispatch
200244
permissions:
@@ -203,14 +247,15 @@ permissions:
203247
pull-requests: read
204248
engine: copilot
205249
steps:
206-
- uses: some/action@v1
250+
- name: Step with with but no uses
207251
with:
208252
token: ${{ secrets.MY_API_TOKEN }}
253+
run: echo hi
209254
---
210255
211-
# Unsafe With Secret Test
256+
# Unsafe With Secret Test (no uses)
212257
213-
This should fail because secrets are used in with field.
258+
This should fail because with: without uses: is not a safe binding.
214259
`,
215260
errorMsg: "strict mode: secrets expressions detected in 'steps' section",
216261
},
@@ -295,6 +340,9 @@ Check that error suggests env bindings.
295340
if !strings.Contains(err.Error(), "env: bindings") {
296341
t.Errorf("Expected error to suggest env: bindings, got: %v", err)
297342
}
343+
if !strings.Contains(err.Error(), "with: inputs") {
344+
t.Errorf("Expected error to suggest with: inputs for action steps, got: %v", err)
345+
}
298346
}
299347

300348
// TestNonStrictModeStepSecretsAllowedWithWarning verifies that in non-strict

0 commit comments

Comments
 (0)