Allow secrets in step-level env: bindings under strict mode#25779
Allow secrets in step-level env: bindings under strict mode#25779
Conversation
Strict mode blocks all secrets.* in user-defined steps, but step-level env: bindings are controlled, masked surfaces — functionally equivalent to what the framework's own generated jobs do. The validation now classifies secrets by location within each step: - env: bindings → allowed (masked by Actions, scoped to step) - run:, with:, other fields → still blocked (inline interpolation) Add classifyStepSecrets() helper, update validateStepsSectionSecrets(), improve error message to suggest env: bindings as alternative. Refs: #25747 Agent-Logs-Url: https://github.com/github/gh-aw/sessions/cbccafb7-fb26-495b-85dc-786315599518 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
|
@copilot steps are not allowed to reference $GITHUB_ENV to avoid leaking secrets in env |
Steps must not reference $GITHUB_ENV to avoid leaking secrets from env: bindings to subsequent steps. Updated test examples to use tool invocation patterns that consume secrets only within the step. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/de761e54-6d1f-4961-8f74-865ec8a05cc9 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Fixed in 22cd2b0 — removed all |
There was a problem hiding this comment.
Pull request overview
Enables strict mode workflows to use ${{ secrets.* }} in step-level env: bindings while still blocking secrets in other step fields (e.g., run, with) to reduce accidental secret leakage into the agent job.
Changes:
- Updated strict-mode steps validation to allow secrets referenced only within step
env:maps. - Added
classifyStepSecrets()helper to partition secret refs into env-bound vs unsafe refs (with malformedenvtreated as unsafe). - Added/updated unit tests and introduced integration tests covering compilation behavior; added an ADR documenting the decision.
Show a summary per file
| File | Description |
|---|---|
| pkg/workflow/strict_mode_steps_validation.go | Implements env-vs-unsafe secret classification and updates strict/non-strict enforcement & messaging. |
| pkg/workflow/strict_mode_steps_validation_test.go | Updates existing tests and adds unit tests for classifyStepSecrets(). |
| pkg/workflow/strict_mode_steps_secrets_integration_test.go | Adds integration coverage for strict-mode compilation allowing env-bound secrets while blocking unsafe secrets. |
| docs/adr/0001-allow-secrets-in-step-env-bindings-under-strict-mode.md | Documents the design decision and normative behavior for secret classification/enforcement. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 4/4 changed files
- Comments generated: 5
| if c.strictMode { | ||
| // In strict mode, secrets in step-level env: bindings are allowed | ||
| // (controlled binding, masked by GitHub Actions). Only block secrets | ||
| // found in other fields (run, with, etc.). | ||
| if len(unsafeSecretRefs) == 0 { | ||
| strictModeValidationLog.Printf("All secrets in %s section are in env bindings (allowed in strict mode)", sectionName) | ||
| return nil |
There was a problem hiding this comment.
Allowing secrets in step-level env under strict mode introduces a new, easy-to-miss leak path: a step can persist an env-bound secret to later steps (including the agent step) via "$GITHUB_ENV" (or equivalent), which defeats the “step-only” scoping described in the PR. Consider adding strict-mode validation that rejects steps which both (a) contain env-bound secrets and (b) reference GITHUB_ENV / write to it in run (and/or documenting this explicitly in the compiler error/warning output).
| unsafeSecretRefs = sliceutil.Deduplicate(unsafeSecretRefs) | ||
| return fmt.Errorf( | ||
| "strict mode: secrets expressions detected in '%s' section may be leaked to the agent job. Found: %s. "+ | ||
| "Operations requiring secrets must be moved to a separate job outside the agent job", | ||
| sectionName, strings.Join(secretRefs, ", "), | ||
| "Operations requiring secrets must be moved to a separate job outside the agent job, "+ | ||
| "or use step-level env: bindings instead", | ||
| sectionName, strings.Join(unsafeSecretRefs, ", "), | ||
| ) |
There was a problem hiding this comment.
The ordering of secret refs in error/warning messages can be nondeterministic because refs are collected via Go map iteration (stepMap iteration, nested map traversal, and ExtractSecretsFromValue returns a map). This can lead to unstable error strings and flaky tests/assertions downstream. Consider producing deterministic output by sorting the deduplicated ref slices (unsafeSecretRefs / allSecretRefs) before strings.Join.
| # ADR-0001: Allow Secrets in Step-Level env: Bindings Under Strict Mode | ||
|
|
||
| **Date**: 2026-04-11 | ||
| **Status**: Draft | ||
| **Deciders**: pelikhan, Copilot |
There was a problem hiding this comment.
This ADR is titled/numbered "ADR-0001" but the repository already contains another ADR-0001 file (docs/adr/0001-conditional-oidc-env-var-forwarding-to-mcp-gateway.md). ADR IDs should be unique; please renumber this document (and its filename/header) to the next available number to avoid ambiguity in references.
| 1. Implementations **MUST** classify each `${{ secrets.* }}` reference within a step by the name of the YAML field in which it appears. | ||
| 2. A secret reference found inside the `env:` field of a step **MUST** be classified as an *env-bound* reference. | ||
| 3. 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. | ||
| 4. A step value that is not a YAML map (e.g., a raw string) **MUST** treat all secret references within it as *unsafe* references. | ||
| 5. Implementations **MUST NOT** treat an *env-bound* reference as *unsafe*, nor vice versa. |
There was a problem hiding this comment.
Normative spec mismatch with implementation: the code intentionally treats malformed env: values (string/slice) as unsafe to prevent bypass, but the spec currently says any secret reference in env: MUST be env-bound and MUST NOT be classified as unsafe. Update the spec to explicitly scope “env-bound” to well-formed env mappings (or otherwise reconcile with the malformed-env-as-unsafe behavior).
| // Filter out the built-in GITHUB_TOKEN: it is already present in every runner | ||
| // environment and is not a user-defined secret that could be accidentally leaked. | ||
| secretRefs = filterBuiltinTokens(secretRefs) | ||
| unsafeSecretRefs = filterBuiltinTokens(unsafeSecretRefs) | ||
| envSecretRefs = filterBuiltinTokens(envSecretRefs) | ||
|
|
There was a problem hiding this comment.
filterBuiltinTokens currently drops any ref whose expression string contains the substring secrets.GITHUB_TOKEN. This can unintentionally exempt user secrets like secrets.GITHUB_TOKEN_SUFFIX, and (more critically) can cause other secrets to be ignored when they appear in the same expression as GITHUB_TOKEN (because the entire expression gets filtered). Consider filtering more precisely by parsing secret names from the expression and only removing refs when the expression contains only GITHUB_TOKEN (or filter by secret name before collapsing to expression strings).
✅ Design Decision Gate — ADR VerifiedThe implementation in this PR aligns with the stated Architecture Decision Record. ADR reviewed: ADR-0001: Allow Secrets in Step-Level env: Bindings Under Strict Mode Verification Summary
The code faithfully implements every MUST and MUST NOT requirement in the ADR's normative specification. 🏗️
|
There was a problem hiding this comment.
Implementation verified: code aligns with the linked Architecture Decision Record (ADR-0001: Allow Secrets in Step-Level env: Bindings Under Strict Mode).
All MUST and MUST NOT normative requirements are satisfied. Two housekeeping items remain before merge:
- Rename the ADR file to
0002-*.mdto resolve the numbering collision with the existing0001-conditional-oidc-env-var-forwarding-to-mcp-gateway.md. - Update the ADR Status from
DrafttoAccepted.
🧪 Test Quality Sentinel ReportTest Quality Score: 70/100
Test Classification Details
Suggestions — Not Blocking
|
There was a problem hiding this comment.
✅ Test Quality Sentinel: 70/100. Test quality is acceptable — 0% of new tests are implementation tests (threshold: 30%). All 6 test functions verify observable behavioral contracts, both files have correct build tags, and no forbidden mock libraries are used. Minor suggestions posted in the comment above.
|
@copilot review comments |
…, ADR renumbering, precise token filter - Add GITHUB_ENV leak detection: steps with env-bound secrets that reference GITHUB_ENV in run: are reclassified as unsafe - Sort secret refs before joining for deterministic error messages - Renumber ADR from 0001 to 0002 to avoid conflict with existing ADR - Update ADR spec to scope "env-bound" to well-formed env mappings and document GITHUB_ENV reclassification rule - Fix filterBuiltinTokens to use precise secret name parsing instead of substring matching (secrets.GITHUB_TOKEN_SUFFIX no longer exempt) - Add tests for all new behaviors Agent-Logs-Url: https://github.com/github/gh-aw/sessions/8b6d69ab-d376-4ef9-a150-520c283b33b4 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Addressed all 5 review comments in f47d1f2:
|
Strict mode blocks all
secrets.*in user-defined steps, forcing workflows that need tool credentials (API tokens, OAuth keys) to disable strict mode entirely viastrict: false, losing all other protections. Step-levelenv:bindings are controlled, masked surfaces — functionally equivalent to what the framework's own generated jobs already do.Important: Secrets in
env:bindings must be consumed only within the step itself. Steps that reference$GITHUB_ENVwhile using env-bound secrets are blocked to prevent leaking secrets to subsequent steps (including the agent step).Changes
classifyStepSecrets()— new helper partitions a step's secret refs intounsafeRefs(run, with, etc.) andenvRefs(env bindings). Malformed env values (string, slice) are treated as unsafe to prevent bypass. Steps that reference$GITHUB_ENVwhile having env-bound secrets are reclassified as entirely unsafe.validateStepsSectionSecrets()— now only errors onunsafeRefsin strict mode; env-only secrets pass throughfilterBuiltinTokens()— now parses secret names precisely via regex instead of substring matching;secrets.GITHUB_TOKEN_SUFFIXand mixed expressions are no longer wrongly exemptstepReferencesGitHubEnv()/valueReferencesGitHubEnv()— new helpers detect$GITHUB_ENVreferences in non-env step fields to prevent secret leakageenv:bindings as alternativefilterBuiltinTokensprecision (5 cases),stepReferencesGitHubEnv(3 cases), updated validation tests (9 new cases), integration tests for full compilationExample
Now compiles under strict mode (secret scoped to the step only):
Still blocked (inline secret in run):
Also blocked (env-bound secret leaked via
$GITHUB_ENV):