CB-633: constrain a member's environment with an allow-list — the denylist misses a whole secret file #144
Open
opened 2026-08-23 06:20:51 +02:00 by ltms
·
4 comments
No Branch/Tag Specified
main
worker/fleetd-612-unita-87807e-1
worker/612-b3-mcpwirings-da2b58-3
worker/612-b2-cb185-176d3a-2
worker/612-b1-completion-457459-1
worker/612-agaps-73a926-2
worker/608-sleeps-3a64ff-3
worker/621-b4520b-1
worker/618-b83894-2
worker/fleetd-615-e05481-5
worker/lead-autocompact-5f1ab2-3
worker/fleetd-613-f85deb-3
worker/fleetd-608-flaky-nudge-test-d0c2d1-3
worker/lead-context-gauge-ad404f-1
worker/gauge-wiring-9158c1-4
worker/redeploy-slowstart-ead0e5-5
worker/charter-bytes-13668c-6
worker/rollover-outcome-291483-2
worker/589-f64303-2
worker/593-1a8025-5
worker/589-fcd2aa-1
worker/568-9fdaa2-3
worker/571-attempted-outcome-5739f7-2
worker/581-completionresolver-cas-sites-0542b7-6
worker/562-loop-health-wiring-test-99611c-5
worker/562-surface-loop-health-7df5cc-4
worker/575-waiter-cleanup-sites-62ad80-1
worker/572-answer-lock-release-46a9ae-5
worker/567-probe-channel-leak-a38fc5-6
worker/551-record-before-send-7cbf56-1
worker/561-listener-fanout-survives-a-throw-61d538-2
worker/555-redeploy-main-flow-seam-65c2f5-2
worker/556-injector-owns-registration-e027a5-1
worker/552-post-restart-mktemp-abort-bc2672-4
worker/553-onstatus-completion-leak-0da881-2
worker/550-shasum-linux-196132-1
worker/538-loop-dies-on-error-4a5eeb-6
worker/426-health-coverage-ef1fd4-4
worker/504-failed-reported-clean-3cfd66-3
worker/537-capturedlog-close-e4c437-2
worker/459-broken-link-targets-cadc17-5
worker/535-appender-leak-fe74c1-1
worker/512-part2-shutdown-detection-434701-9
worker/529-logger-level-sweep-2a5533-8
worker/528-drain-gate-call-site-5de83d-7
charter/forge-mcp-vs-token
worker/521-swap-guard-unpinned-28e931-5
worker/519-probe-test-harness-d25ab8-4
worker/525-logger-level-leak-1b4eb0-6
worker/518-fleetmcp-resolver-wiring-8ef96c-1
worker/512-drain-complete-line-7edd71-3
worker/517-abort-branch-and-jar-id-41b641-2
worker/500-9e52c9-3
worker/509-4912f4-2
worker/511-9a4b23-1
worker/493-479f45-2
worker/505-03f8b2-1
worker/492-followup-detect-unclear
worker/501-a31fa0-7
worker/498-451d1c-5
worker/494-1015ce-2
worker/492-209647-1
worker/489-001902-2
worker/480-relative-handover-path-906323-1
worker/480-b-handover-skill-45bf1f-5
worker/474-followup-source-pin-f54a55-17
worker/474-charter-check-on-reload-f54a55-17
worker/466-quarantine-repeatcount-report
worker/393-opencode-skill-seeding-71854b-13
worker/469-canonical-tool-names-2a472a-16
worker/466-quarantine-escalation-5ae9c1-15
worker/446-hot-exhausted-pattern-0af580-6
worker/464-charter-tool-name-guard-a85635-12
worker/463-listfleet-default-fails-open-f1c76c-11
worker/458-invariant-5-by-purpose-862f9a-10
worker/439-coordinator-row-gate-bc032a-8
worker/449-herdr-protocol-576015-4
worker/450-abstract-spawn-599e1c-5
worker/437-ack-refuses-177d91-1
worker/444-placement-window-feb56a-2
worker/440-helddurable-derived-d462d7-13
worker/425-rework-placement-resolve-c58ba1-9
worker/421-lead-peek-held-msgs-cdbad2-10
worker/435-fixed-policy-cap-fe11de-12
worker/422-gate-state-observability-9e79d6-11
worker/431-memberregistry-live-readers-cdbad2-10
worker/424-architect-slot-hot-038b41-7
worker/422-model-gate-spawn-c29f48-6
worker/425-default-profile-live-f55534-8
worker/415-coverage-wording-2cbf9c-5
worker/416-3ad1da-1
worker/418-588283-3
worker/deterministic-stamp-race-409-3cb7b6-10
worker/armed-reads-live-config-404-ed931f-9
worker/reply-peer-refusal-391-5a34bd-7
worker/models-allowlist-aa9e9b-3
worker/ttl-stamp-race-399-f1122f-8
worker/scrub-receipt-400-316b3e-5
worker/exhaustion-detection-395-105105-6
worker/scrub-abort-394-316b3e-5
fix/scrub-uid-abort
worker/task-scrub-517574-2
worker/t386-clock-bd5b78-4
worker/t384-scrub-813790-5
worker/t381-cc-748314-2
worker/t373-336973-2
worker/t365-3920c5-3
worker/t358-6e989b-1
worker/t355-8b321c-1
worker/fleetd-369-hermetic-git-tests-e8b19a-3
worker/fleetd-368-stale-lead-binding-f5682e-2
worker/fleetd-360-deploy-units-0d3793-1
worker/359-dead-lead-tabs-f1253b-4
worker/362-worktree-skills-c03e51-3
worker/361-coord-visibility-655144-1
362-plugin-visibility-and-drift
worker/errscan-bed2ca-2
worker/amqp-log-identity-bed2ca-2
worker/withdefaults-guard-561704
worker/sleepguard-82076d-1
worker/fd334-9ee1b6-5
worker/fd348-f1ab27-4
worker/fd335-a71c35-1
worker/fd342-174a17-2
worker/fd345-490d0f-3
worker/fleetd-337-5ec7d4-21
worker/fleetd-341-af5a6b-24
worker/fleetd-339-5ca0a2-23
worker/fleetd-338-83a4a1-22
worker/fleetd-333-281f46-18
worker/fleetd-329-11bdbb-16
worker/fleetd-330-2770fb-17
worker/fix-326-50506e-15
worker/fix-324-3e9bbf-14
worker/fix-323-b8287d-13
worker/fix-316b-bd0860-11
worker/fix-318-76ca36-9
worker/fix-317-486aec-8
worker/fix-315-ce47c5-6
worker/fix-307-275890-6
worker/fix-308-b4f664-7
worker/fix-309-ec3939-8
worker/fix-310-7a3974-9
worker/fix-302-52ad0e-9
worker/fix-298-ce1acb-8
worker/fix-297-66bd11-7
worker/fix-296-104622-6
worker/fix-293-bare-closetab-eb22b5-3
worker/fix-280-gone-ask-lapse-bca98e-2
worker/fix-290-reapidle-guard-coverage-9b0dd1-1
worker/fix-285-trust-seed-8f3565-10
worker/fix-284-backend-error-seat-85912c-11
worker/fix-282-chained-ask-e6d0bb-8
worker/fix-283-teardown-leaks-f40dfa-9
worker/fix-281-pin-handler-actions-4921ac-7
worker/audit-rendezvous-lifecycle-d072ae-2
worker/audit-health-placement-1a2476-6
worker/audit-teardown-exits-e207a5-3
worker/audit-launcher-asymmetry-27e370-4
worker/audit-rest-authz-6ca53c-5
worker/investigate-275-abandon-asking-fdef52-8
worker/fix-274-worktree-leak-b0095d-7
worker/fix-273-exhausted-pattern-9665b5-6
worker/fleetd-267-model-check-bd8068-1
worker/fleetd-131-archunit-18b834-7
worker/fleetd-266-sshagent-rename-a014ff-6
worker/fleetd-184-uid-claim-8e1f31-4
worker/fleetd-184-warn-b381ee-10
worker/fleetd-184-docs-be1d12-9
worker/fleetd-257-9bf010-7
worker/fleetd-103-23a113-6
worker/fleetd-247-342356-5
worker/fleetd-116-04dea8-4
worker/fleetd-252-a830e0-3
worker/fleetd-111-7e8673-9
worker/fleetd-155c-f8ef4b-8
worker/fleetd-176-b928ca-3
worker/fleetd-249-7a7878-2
worker/cb248-composition-root-b-9acdf7-15
worker/cb148-envrc-default-fa6c82-12
worker/cb201-unit5-wiring-6c12e6-8
worker/cb241-fallback-echo-1175e9-11
worker/cb149-trust-dialog-2392a5-9
worker/cb134-148-overlay-visible-c9b986-10
worker/cb234-session-id-keyed-04e1fc-1
worker/cb201-unit3-nudge-abdf5c-6
worker/cb201-unit2-policy-c1102c-5
worker/cb201-unit4-outcome-a13bfa-7
worker/cb201-unit1-classifier-91b9b1-4
worker/cb201-227-refine-831980-3
worker/cb175-model-readback-0f085f-1
worker/cb222-charter-tmpdir-17f013-1
worker/cb226-architect-slot-race-cd3aa8-3
worker/cb224-worktree-root-group-024523-2
worker/cb-123-role-demotion-c600f7-2
worker/cb-219-opencode-roots-1f677e-1
worker/cb214-claude-session-id-b9eab4-4
worker/cb213-zdotdir-wrong-process-dd6de4-3
worker/cb211-exhaustion-classification-9546e0-2
worker/cb137-ambiguous-task-4df3d8-4
worker/cb209-agentsessionid-4dfdb6-2
worker/cb185-hostenvnames-2692b5-3
worker/cb206-opencode-sqlite-128718-2
worker/cb185-worktree-group-fc0c99-1
worker/cb-137-ask-ticket-e7760c-2
worker/cb-172-broker-uri-d36ae4-4
worker/cb-175-model-readback-76ead6-3
worker/cb-161-pane-ancestry-293510-1
worker/cb-164-rebase-885863-8
worker/cb-164-empty-scrape-false-success-1a80af-3
fix/cb-197-ticket-ttl-from-completion
worker/cb-189-remote-url-coverage-4692f3-1
worker/cb-185-blockers-027756-4
worker/cb-192-gap-log-11b631-2
worker/cb-633-fix-5f4396-3
worker/cb185-router-d6436d-3
worker/cb185-router-routing-gaps-9e9d33-3
worker/cb185-paneids-992586-2
worker/cb-633-allow-list-union-ed374b-1
worker/cb-157-credential-in-remote-url-496e44-2
worker/cb-641-health-herdr-evidence-8f1f54-6
worker/cb-640-health-msg-evidence-99c9cd-1
worker/cb-642-fleets-status-skill-bbbc40-5
cb-634-ide-mcp
worker/lead-comms-wiring-c014b9-7
worker/lead-mailbox-c19577-6
worker/autocompact-window-82bc2f-5
worker/cb-634-probe-18056f-4
worker/cb635-broker-urienv
worker/cb-632-config-retry-8e0efa-7
lead/cb-622e-claude-md
lead/cb-622-followup
worker/cb-622a-165dff-1
lead/cb-622d-opencode-mount
worker/cb-622b-717c67-2
worker/cb-622c-ab7759-3
worker/cb-617b2-20ca4b-3
worker/cb-617a-5c2f4a-1
worker/cb596-4e49ef-3
worker/cb586-10500c-1
worker/cb-606-b9343a-25
worker/cb604-1445f8-24
worker/cb582-477374-21
worker/cb584-8c2281-22
worker/cb600-e6b9a9-20
worker/cb602-ce257f-19
worker/cb601-b42837-18
worker/cb598-6c7ba7-17
worker/cb599-740fe4-16
worker/cb597-282224-15
worker/cb590fix-185e9a-10
worker/cb528-recovery-race
worker/cb594-96bead-8
worker/cb590-916766-2
worker/cb527-997d99-3
worker/cb592-env-leak-3cbf9c-1
worker/cb588-async-ticket-nudge-3218f7-5
worker/cb578b-9dcb13-6
worker/cb581-d24826-5
worker/m2-u5-ef8c42-15
worker/cb578a-516499-2
worker/cb576-01a04b-17
worker/cb579-lead-tab-acba06-20
worker/cb580-terminal-health-ed6058-21
worker/cb577-f36fdc-18
worker/cb573b-3db06f-16
worker/cb568c-f36fdc-18
worker/cb568-drop-cause-c3ac1c
worker/cb575-cancelled-notification-c3ac1c
worker/m4-sol-a2cbec-3
worker/cb574-async-ask-c3ac1c
worker/cb573-health-model-8ca857-14
worker/cb572-unknown-target-7f2e35-13
worker/u4-700706-9
worker/u3-b9fcb6-6
worker/u2-ef5b68-4
worker/u1-469dce-1-clean
worker/u1-469dce-1
worker/cb-564-health-events-70cf7e-2
worker/cb-565-recycle-drops-role-98e58f-3
worker/cb-563-missing-reply-df2866-1
worker/cb-562-readiness-gate-silent-6c23c9-3
worker/cb-560-architect-presence-da8155-1
worker/cb-561-architect-silent-off-a71cab-2
worker/cb-548-bind-architect-slot-fe1b8c-1
worker/parity-overlay-settings-5fb711-1
secrets-central-store
cb-559-hot-key-correction
cb-557-fleet-role-pools
worker/cb-553-maxload-explicit-spawn-305ee3-6
worker/cb-551-idle-lead-heartbeat-f1633c-1
worker/cb-544-drain-preserves-worktree-925fad-3
worker/cb-552-docs-sync-1cb9cf-4
worker/cb-548-rendezvous-guard-rebased
worker/cb-548-rendezvous-guard-116b53-10
worker/cb-548-authz-v2-586df6-8
worker/cb-548-authz-264363-5
salvage/cb-528b-codex-home
salvage/cb-528a-codex-launcher
CB-518-primary-flow
feature/peer-launcher-spi
cb-103-injector
v1.1.0
v1.0.0
Labels
Clear labels
blocked
needs-live-proof
ready-to-delegate
silent-default
Cannot start until something else lands. The body says what.
Merged and green, but never shown working on the running daemon. Not the same as done.
Scope, files and acceptance criteria are written. A worker can be briefed from the body alone.
A feature that compiles, passes tests, and ships turned off. Nine recurrences and counting.
No Label
Milestone
No items
No Milestone
Projects
Clear projects
No project
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: fleet/fleetd#144
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of #125 (CB-621). Supersedes the approach in #141 (CB-631).
What is wrong
The fleet decides what credentials a member may see with a denylist: 29 names in
memberCredentials.knowninbridged.yaml, mirrored by hand into aBRIDGED_MEMBER-guardedblock in
${SHARED_ENV}/tools/secrets.sh. The comment in that file says it plainly:Two hand-written lists that must agree is the defect family we already keep hitting (#113).
But the real problem is worse than drift. A denylist can only name what somebody remembered
to enumerate, and the exposure is not a file — it is an environment.
Measured, not assumed (2026-08-23)
${SHARED_ENV}/.ltmssources the secret files in this order:Nothing exports after line 41. Simulating a member pane exactly as herdr starts one:
So 7 credentials reach every member pane in full. Four of them
(
CONFLUENCE_*,GITLAB_*) are already named in thesecrets.shguard — the guard blanksthem, and then
mgnlSecrets.shexports them again one line later. The guard is beaten byordering, not by an incomplete list.
These are work credentials (Magnolia): a GitLab personal access token, a Confluence API token,
AWS keys, Jenkins auth. A member on a remote-model profile sends its context to a third-party
provider, so this is a real exposure and not a tidiness issue.
This also corrects #141:
N8N_WEBHOOK_TOKENis blocked, not leaking. The four names in#141 were three genuine leaks plus one false positive.
Why the current design cannot be fixed by adding names
Adding
AWS_*andJENKINS_MCP_AUTHto the two lists closes today's hole and nothing else.The next secret file, or the next
exportin an existing one, reopens it silently. Theenumeration was of a file; the exposure is the environment (#113 family).
What to do instead
Invert it. In a member pane, blank everything that looks like a credential except an
explicit allow-list of what a member actually needs:
Acceptance criteria:
memberCredentials:in config gainspolicy: allow-listwith anallow:list.known:becomes reporting-only — it stops being a control, so it stops being a thing to keep in sync.
(
gitTokenEnv->GITEA_TOKEN,gitHostEnv->GITEA_HOST,tokenEnv, and each profile'senv:map). Build the allow-list from those, so adding a profile cannot break a spawn.Getting this wrong breaks every spawn, so it must not be a hand-typed list either.
asking the operator to hand-mirror a list. One source, no drift.
and a sample of other credential-shaped names is blanked. Testing the launcher's env map is
not enough — the launcher's map is what the login shell overwrites (#113 again: measure the
side the failure is on).
allowed N of M credential-shaped variables, notblocked N.Stop-gap already staged
A
BRIDGED_MEMBER-guarded block that blanks the 7 measured names, appended to the end of.ltmsso it runs aftermgnlSecrets.sh. It is a denylist and is explicitly marked as astop-gap in its own comment. It is limited to names no member needs, so it cannot break a spawn.
Not yet applied — it edits a file outside this repo.
The
vmslead reproduced this independently and found two things that change the ticket.1. The argument in this ticket was too weak
I wrote that "a denylist can only name what somebody remembered to enumerate". That is true but
it is not what happened here. Four of the seven leaking names are already on the guard's
list. The guard ran, blanked them correctly, and then
.ltmsline 41 sourcedmgnlSecrets.sh, which handed the real values straight back.So a correct and complete denylist still failed. The defect is not an incomplete list — it is
that the control lives inside a sourced file, and a file sourced later can always undo it.
That is host-independent: it would happen the same way on a fresh vhost.
This sharpens acceptance criterion 2. The allow-list must be applied at the spawn boundary,
where the launcher builds the child environment and nothing runs after it. An allow-list that is
itself a block inside a sourced shell file inherits the exact defect it is meant to fix.
Independent confirmation, by comparing SHA-256 prefixes of each value between a normal login
shell and a
BRIDGED_MEMBER=1login shell (prefixes only, never values):Three different credentials collapsing to the same hash prefix is what a working guard looks
like. Comparing hashes is a better probe than the sentinel-string check I used, because it needs
no knowledge of what the sentinel is.
2. The AWS key is not a limited one — raise the priority
AWS_ACCESS_KEY_IDcarriesAdministratorAccessvia theadminsgroup on account891377113284. Created 2024-03-20, never rotated, last used three days ago.That makes this the most serious of the seven by a wide margin, and it changes the ordering:
rotate the AWS key first, ahead of the other six and ahead of building anything here. A
full-admin, never-rotated key reached every member pane we have ever spawned on a remote-model
profile.
Rotation is the operator's call and is not part of this ticket. Noting it here so the ticket
records the real weight of what leaked, rather than treating all seven as equivalent.
3. The vhost does not replace this
Considered and rejected. Members on a separate host do not inherit the operator's Mac login
environment, so the vhost shrinks the blast radius — but it does not fix the defect, for the
reason in section 1: source ordering is host-independent. It also does not help while the lead
still runs on the Mac. Build the allow-list; treat the vhost as a separate win.
Stop-gap applied by the operator, and verified here independently
The operator fixed it a better way than my staged block: reorder the two sources so
mgnlSecrets.shruns before the guardedsecrets.sh, plus add the three missing names to theguard list. My block was not applied and is not needed.
Checked myself rather than taking the report —
.ltmsnow reads:and a member login shell started from a clean parent (
env -i HOME=... BRIDGED_MEMBER=1 zsh -l, so nothing is inherited from my own session):A full sweep of every credential-shaped name in that shell leaves exactly the three intended
pass-throughs and nothing else:
The reorder is safe because
mgnlSecrets.shcontains no variable references, so nothing in itdepended on
secrets.shhaving run first. Confirmed by reading it.This ticket stays open
The exposure is closed. The defect is not fixed, and the fix makes that clearer rather than
less clear: the control is still a block inside a sourced file, and it now works only because
of the order two
sourcelines happen to be in. Add a third secret file after line 41 — or moveone line — and it reopens silently, with no test anywhere that would notice.
That is the same shape as the original bug. The reorder buys time; it is not the answer.
Acceptance criteria unchanged: apply the allow-list at the spawn boundary, in the launcher,
where nothing runs afterwards.
One extra criterion this episode earns:
parent and assert the surviving set equals the allow-list exactly — an equality assertion,
not a "these names are blocked" assertion. A blocked-name list is the very thing that fails
here, because it can only check names somebody already thought of.
The seam exists after all —
ZDOTDIR. Measured on this host, not reasoned about.This overturns the conclusion in
7930a31("CB-596 round 2: the exec-time argv-prefix fix has no seam — stop and report"). That commit was right about what it checked and wrong about what it concluded. It checked herdr's protocol:agent.starttakes a fixedkind(herdr resolves the executable) plus trailing args, and onlytab.create/pane.splitcarry anenvmap. All still true — I re-readAgentControl.startandWorkspaceControl.createTabtoday.But "no control point in the herdr protocol" is not "no control point". The shell itself has one, and we already reach it:
tab.create's env map runs before the shell starts, and zsh reads its startup files from$ZDOTDIR. So the daemon can redirect where the member's shell looks for its own rc files, and put its scrub in the file that runs last.Why this beats every earlier attempt
Startup order for a login interactive zsh is
.zshenv→.zprofile→.zshrc→.zlogin. The operator's chain (.zshrc→.ltms→mgnlSecrets.sh+secrets.sh) all happens inside the first three..zloginruns after all of it. Nothing the operator sources can undo it, and the fix depends on no file outside this repo — which is the exact property the reorder stop-gap lacks.Measured
ZDOTDIRis honoured by a login zsh started from a clean parent:Then the full design: four generated files in
$ZDOTDIR, each sourcing the operator's real$HOME/.z*counterpart, with the scrub appended to.zlogin. Run as an interactive login shell from a clean parent (env -i HOME=… ZDOTDIR=… /bin/zsh -l -i), so nothing is inherited from my session:That is the AWS
AdministratorAccesskey blocked by the daemon's own file, with the operator's.ltmsreordering removed from the equation entirely.I also checked that no operator rc file sets
ZDOTDIR(grepacross.zshenv .zprofile .zshrc .zlogin .zlogout→ no match), so the daemon's value is not overwritten.The one thing this measurement exposes about the pattern list
CONFLUENCE_USERNAMEwas not blocked. It matches none of*TOKEN* *KEY* *SECRET* *PASSWORD* *AUTH* *CREDENTIAL*, because a username is the other half of a credential and is not shaped like one. That is this ticket's own defect in miniature: a pattern list is an enumeration, and it can only catch what somebody's pattern happened to describe.So the policy should be a real allow-list: blank every variable that is neither on the derived allow-list nor on a small infrastructure passthrough set (
PATH,HOME,SHELL,TERM,LANG,TMPDIR,USER,PWD,SSH_AUTH_SOCK² …). Then a credential nobody has thought of is blocked because it is new, not because it matched. Credential-shaped patterns stay only as a warning signal in the log, never as the control.²
SSH_AUTH_SOCKis #110 (CB-607) — it is a handle to the operator's agent, not a value in any secret file, which is why three credential tickets missed it. Decide it explicitly here rather than letting it pass through by silence.Revised acceptance criteria
memberCredentials:gainspolicy: allow-list.known:becomes reporting-only.gitTokenEnv,gitHostEnv,tokenEnv, and every profile'senv:map — plus the infrastructure passthrough set. Not hand-typed: a hand-typed list breaks every spawn the first time a profile adds a variable.$ZDOTDIRdirectory per spawn and passesZDOTDIRintab.create's env map. No operator-owned file is edited, and nothing needs keeping in sync. Each generated file sources its$HOMEcounterpart first soPATHand the agent binaries still resolve.ZDOTDIRdoes nothing and the member is unprotected. Detect it and refuse the spawn, or log a loud WARN — do not fail silently. Say which was chosen and why.allowed N of M, neverblocked N(#113).What this does not change
The
.ltmsreorder stays as defence in depth. Rotating the AWS key is still first and still the operator's call.The control is built, merged, tested — and switched off on the live daemon
Checked in the code on 2026-08-28, not inferred from the ticket.
fleetd.yamlline 198 readspolicy: deny-by-default. SoHerdrPeerLauncher.applyEnvironmentAllowListPolicyreturnsnullat its first branch, noZDOTDIRis generated, andapplyMemberCredentialPolicyfalls back tooverlayBlockedCredentials— the CB-596 pre-shell env overlay. That is the exact control this ticket measured failing, because a file sourced later re-exports over it.The daemon says so itself at every boot. Tonight, after a restart at 22:31:
AWS_ACCESS_KEY_IDis theAdministratorAccesskey from the comment above. So the exposure this ticket was raised for is live, and the fix for it has been sitting merged and inert.This is the defect family the ticket already names, one level up: not "a control that misses names", but a control that is complete and turned off, with nothing that fails when it is. Nothing in the test suite asserts which policy the shipped config selects, and nothing could — the config is gitignored, so no worker and no CI run has ever seen it.
Why the flip is not a one-line config change
MemberEnvAllowList.derivebuilds the kept set from every profile'sgitTokenEnv,gitHostEnv,tokenEnvandenv:keys, plusINFRASTRUCTURE_PASSTHROUGH.applyEnvironmentAllowListPolicythen unions in this launch's own env-map keys and, if configured,SSH_AUTH_SOCK.It never reads
memberCredentials.allow:. The live config lists four names there that are consumed by the member's own binaries, not by fleetd, and none of them is derived:GITEA_HOSTgitHostEnv:OPENCODE_AUTOMODE_MODELCONTEXT7_TOKENCLAUDE_CODE_MESSAGING_TOKENAll four resolve on this host. Flipping the policy today blanks all four. Two of them are secrets, so the documented escape hatch — put the name in a profile's
env:map — is not available: that map holds values, and using it would write a secret into config.The gap is in the design, not in the config. Derivation covers what fleetd injects. It has no way to express what the member's binary needs from the inherited environment. The config already holds exactly the right thing for that — a list of names — and the code ignores it.
What is being done
memberCredentials.allow:into the allowed set underpolicy: allow-list. Derivation stays the base; the operator's list is additive, so it can only widen the set and cannot break another spawn. That is not the hand-mirrored list this ticket rejected — what was rejected was a hand-typed list replacing derivation.logCredentialGap. Underallow-listits "inherits them UNBLOCKED" sentence is false: a name on neither list is blanked because it is not on the derived set. A control that reports a false exposure teaches operators to skim warnings (#115).gitHostEnv: GITEA_HOSTto each profile that hasgitTokenEnv, restart, and prove it with a live spawn: read the pane's ownallowed N of Mreport, confirm the AWS key is blank, and confirm a member can still push and open a PR.sshAuthSockstays unset, which meansSSH_AUTH_SOCKis blocked — that is #110's answer, and it is safe here because members push over the repo-scoped HTTPS token, not the operator's agent.One acceptance criterion this episode earns