fix(ci): relocate Bedrock credentials out of the reviewer's env - #8754
fix(ci): relocate Bedrock credentials out of the reviewer's env#8754chenmingwei23 wants to merge 2 commits into
Conversation
Design Review (Fable 5) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS Sound staged plumbing, honestly scoped to #8789; but the shipped comment contradicts the PR body on whether the defence-in-depth layer is verified. Watch
[DESIGN-REVIEWED] ca7ef90 |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of I have everything I need: the contract, the intent, the patch, the resulting workflow file, the sibling trusted-PR lane, and the test helpers. The evidence is consistent — the diff is purely additive against base (the "deleted" First-Principles-Verdict: CONCERNS The core plumbing is honest and earns its place; what rides along is a possibly-inert env-filter block pinned by four tests, plus a history-narrating tombstone. What this change shipsIntent: keep Bedrock credentials out of the process an attacker-prompted model drives, as staging for the real UID-boundary fix (#8789). A FIX by type, declared plumbing by the author.
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] ca7ef90 |
GPT 5.6 Review — 🔴 changes requested (blocking)GPT 5.6 found at least one blocking issue that must be resolved before merging This comment is updated in place on each push. BLOCKING -- .github/workflows/fork-gpt-review.yml:453 -- Credential file remains reachable by the untrusted reviewer Adjudication (Opus 4.8) — is blocking on each finding proportionate?The adjudicable block is empty; ruling only on the one fenced finding F1. F1 — unbounded (credential exposure). Confirmed conditions from code opened this run: the minted Bedrock credentials are written to [ADJUDICATION] ca7ef90 total=0 uphold=0 downgrade=0 False positive or not applicable? A repository writer can comment: |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsI've validated the discovery pass's conclusion against the diff and the changed files directly. The change is a defensive CI hardening: it relocates minted Bedrock credentials into a Checking for a groundable defect on the changed lines:
Nothing survives falsification, and I could ground no new Step-2 finding at 80+. No findings. [OPUS-REVIEWED] ca7ef90 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
UX Review (Fable 5) — ⏭️ skippedRevision |
3ee9e51 to
afac82e
Compare
afac82e to
bd27a28
Compare
The fork GPT lane mints Bedrock credentials into the job environment and then runs codex exec, whose shell tool inherits them wholesale, and publishes that model's output to a public PR comment. A fork author controls the diff the reviewer reads, so they control the prompt, and the model picks the encoding -- so the lane's existing shape and literal-value redaction cannot contain it. Scope the environment at the exec boundary instead, via the config.toml the lane already writes: shell_environment_policy governs spawned subprocesses and not the CLI itself, so Bedrock still authenticates while the model's shell sees no keys. How the job authenticates is unchanged. Refs #8671
bd27a28 to
0ab5ce2
Compare
The first attempt scoped the environment the reviewer's shell INHERITS, and this lane's own GPT reviewer correctly blocked it: the shell runs under the same UID, so /proc/<ppid>/environ returns whatever the parent holds. The second attempt added a config-level deny on the credential file, and the same reviewer blocked that too: both codex calls pass the read-only sandbox flag on the command line, and that flag selects the legacy sandbox instead of a named permissions profile, so the deny was silently inert. So this drops the deny rather than the flag. The flag governs today, so deleting the config changes the effective sandbox by exactly nothing and needs no fact about the pinned CLI version; removing the flag instead would hand control to whatever 0.150.1 falls back to, which could be more permissive. What remains: both codex steps blank the credential variables in their own env blocks, and authentication moves to a profile file staged per pass. On an ephemeral runner the file and the environment are both same-UID reachable and both die with the runner, so this RELOCATES the credential rather than reducing the exposure. It is a prerequisite for putting the credential behind a UID boundary -- which is the only thing that closes this -- not a mitigation. configure-aws-credentials is untouched. The reviewer keeps its shell: measured at 83 bash -lc calls after the change against a 53-call baseline, with no sandbox denials on paths it previously read. Refs #8671
0ab5ce2 to
ca7ef90
Compare
Closing as folded into #8789, not abandonedThe diff is preserved on branch Why fold rather than land. This PR's own body claims relocation and The invariant, and three sound fixes defeated by itA secret reachable by a process is reachable by any same-UID sibling the model
Attempts 1 and 3 were each caught by this repository's own fork GPT reviewer, on What was measured, and is worth keepingThe relocation neither broke authentication nor narrowed the reviewer. On the The reviewer needs its shell, and that was measured rather than assumed. One Schema verification, at the pinned tag. A correction worth recordingEarlier revisions of this PR body listed four config keys as "statically The acceptance criterion, and why it was the right oneThe bar was not a green board: a real verdict had to LAND and the shell had to |
Problem / Motivation
The fork GPT reviewer holds Bedrock credentials in the process that gives an
untrusted-prompt-driven model a shell, and then publishes that model's output to
a public PR comment.
This is demonstrated, not inferred. Earlier revisions argued it from the YAML;
the job log says it outright. From run
33970715459:and the model step's own
env:block, in the same log:A fork PR author controls the diff the reviewer reads, so they control the prompt.
The read-only sandbox restricts writes; it does not restrict reads.
Why it matters
The lane already redacts credential shapes and literal values before publishing,
and those filters are correct as far as they go, but an attacker who controls the
prompt controls the output encoding -- base64, reversed, interleaved -- and no
shape or literal matcher sees it. The value has to be absent, not filtered.
What this PR claims, stated before the diff
This PR does not close #8671, and it is not hardening. It relocates the
credential from the process environment to a profile file, and deletes a config
block that claimed an authority it did not have.
On an ephemeral runner both the environment and the file die with the runner, and
both are reachable by any same-UID sibling the model controls. So this is a change
of shape, not a reduction in exposure. Its value is as a prerequisite: the
credential has to leave codex's environment before it can be moved behind a
privilege boundary, which is the only thing that closes this. That work is
#8789.
If you are reviewing this for security value, review it as plumbing.
Three attempts, three defeats, one unmoved invariant
The invariant: a secret reachable by a process is reachable by any same-UID
sibling the model controls.
shell_environment_policywithheld the credentials from the model's shell/proc/<ppid>/environreturns what the parent holdsenv:; authenticate from a profile filedefault_permissions+ a[permissions.*]deny on that fileAttempts 1 and 3 were each caught by this lane's own GPT reviewer, on the PR that
introduced them. This PR keeps attempt 2 and deletes attempt 3.
What changed
One workflow file.
configure-aws-credentialsis untouched -- all three stepsand their least-privilege roles are byte-identical, asserted by test. How the job
authenticates does not change; what the model step holds does.
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEYandAWS_SESSION_TOKENin their ownenv:blocks. Per-step, so later steps thatlegitimately need them -- the Opus adjudication -- still see them.
pass re-assumes the role and the prior session has expired. Staged under
$RUNNER_TEMPand selected withAWS_SHARED_CREDENTIALS_FILErather thanwritten to the SDK's global default path: reachability is identical -- same UID
either way -- but writing the default would clobber an existing credentials file
on a non-ephemeral or self-hosted runner. Raised as an advisory by this repo's
Design Review lane.
default_permissions/[permissions.review-readonly]block is deleted.A comment in its place records why, so nobody re-adds it.
shell_environment_policyis kept as defence in depth.Why deleting the deny, rather than deleting the flag
The reviewer's prescribed fix was to remove the sandbox flag so the config would
govern. This does the inverse, deliberately: the flag governs today, so deleting
the config changes the effective sandbox by exactly nothing and requires no
fact about the pinned CLI version. Removing the flag instead would hand control to
whatever 0.150.1 falls back to when it is absent -- which I cannot verify, and
whose failure direction is more permissive than read-only.
The reviewer keeps its shell, and that was measured
Removing the shell was considered and refuted by measurement. One real review
makes 53
bash -lccalls across 16 paths: 19 paging this repo's own reviewrulebooks in chunks, 14 cross-referencing changed symbols against surrounding
code, 10 reading
CLAUDE.mdand the convention docs, 6 runninggit applyon thepatch, 3 reading the patch itself. The patch reaches the model through the shell.
The lane's own prompt settles it: "A finding is grounded in what the code DOES
when executed, never in what text in the diff says about it." A shell-less
reviewer could ground findings only in diff text, which its instructions forbid.
The
claude-code-actionlanes need no shell because they haveRead,GrepandGlobnatively; codex has no native file tools here.The four publishing paths
c791f0f1dblockedfork-gpt-review.yml:973, body at:974clearfork-gpt-review.yml:975blockedfork-opus-review.yml:465, body at:466clearfork-opus-review.yml:467, body at:471Refs, notCloses. The Opus paths are out of reach only because those threeclaude-code-actioncalls grant no shell -- a premise this PR asserts in a testrather than assumes, so granting Bash to any of them fails CI.
Tests
test/test_ai_review_workflows.py::TestModelShellDoesNotInheritCredentials,13 cases. The load-bearing one is provable by reading this repository: the
credential variables are blank in both model steps, a profile is named, the region
survives. Others assert the provider resolves from a profile and the name matches
both steps; the credentials are re-staged once per pass; all three
credential-minting steps are unchanged with roles intact; the config write precedes
both exec boundaries; and no
claude-code-actioncall grants a shell.One test is the inverse of a mistake this PR made: it fails if anyone re-adds a
permissions profile while the sandbox flag is still passed, because that
combination produces a deny that parses fine and enforces nothing. An earlier
revision also had
default_permissionsland inside a table, silently becomingmodel_providers.amazon-bedrock.aws.default_permissions-- same failure shape,different cause.
Not vacuous, measured: re-adding the inert
default_permissionsmakes that guardfail with
config.toml selects a permissions profile while --sandbox is passed on the command line, which overrides it -- the deny is inert. Whole touched filepasses 360.
isortandflake8clean on the changed file.black --checkreports the testfile unformatted both at
origin/mainand here (exit 1 either way), so it isin the gate's shrinking baseline, not introduced by this diff.
Manual verification
Verified by consequence.
model_providers.amazon-bedrock.aws.profileishonoured by 0.150.1: a verdict landed with the credential variables blanked and
zero auth-failure lines in the log, leaving the profile file as the only
possible credential source.
Verified from primary source, at the pinned tag. An earlier revision of this
body listed
shell_environment_policy.filtersandignore_default_excludesas unverifiable becausenpm installon my host failsE401against an authenticated registry. That conflated cannot install thepackage with cannot verify, and the second does not follow: the CLI is open
source.
codex-rs/config/src/shell_environment_policy.rsat tagrust-v0.150.1definesShellEnvironmentPolicyTomlwithinherit,ignore_default_excludes,exclude,set,include_only, andfiltersasBTreeMap<String, ShellEnvironmentPolicyFilter>-- a map, exactly the formused here -- alongside
validate_shell_environment_policy_filter_config, so thefield is actively validated rather than vestigial. Every key this PR writes
exists on the pinned version.
That also refutes a concern worth recording rather than quietly dropping:
Opus 4.8 reasoned that on 0.150.1 the exclusion field might be
exclude(a globarray) rather than the
filtersmap, which would have left two mechanisms inert.It was a reasonable inference and it is wrong. Two practical notes for anyone
repeating the check: the tag is
rust-v0.150.1, notv0.150.1, andunauthenticated code search returns 401 while an authenticated
gh api search/codeworks.Proven inert, and therefore deleted: the
permissions.*deny-read profile.The read-only sandbox flag on the command line selects the legacy sandbox instead
of a named profile, so the deny never applied.
Acceptance evidence from the lanes on this PR, which are the change under test.
Real verdicts landed bound to the head, and the shell still works: 83
bash -lccalls against a 53-call baseline on the revision that introduced the profile
file, still reading the rulebooks,
CLAUDE.md,src/,website/srcanddocs/,with zero sandbox denials.
authentic.patchreads dropped to zero, which iswhat a denial looks like -- but
gitcalls rose from 6 to 29, so it reads the diffthrough git now. A count falling to zero was checked, not assumed. The 53-call
baseline came from a run reviewing a different PR, so cross-revision counts are
not strictly comparable; the denial check is the load-bearing evidence.
Pattern harvest
Rule candidate: when three sound fixes are each routed around, stop patching and
name the invariant they all sit below. Here it is same-UID reachability --
environment, procfs and filesystem are three doors to one secret, and closing them
one at a time is not progress.
Rule candidate: a config that asserts an authority it does not have is worse than
no config, because it reads to a human as a closed hole. Prefer deleting the false
claim over trusting an unverified fallback that would make it true.
Not generalizable: the sandbox-flag-versus-permissions-profile precedence, and
Codex's default of keeping KEY/SECRET/TOKEN variables rather than dropping them.
Both are properties of this CLI.
Refs #8671
Refs #8789