Skip to content

SECURITY: a plane never receives a credential's bytes (credential-field refs, template refs); a member's env resolves at spawn - #727

Merged
MattJackson merged 27 commits into
predevfrom
lane-sec-plane-secret-refs
Oct 13, 2026
Merged

MattJackson merged 27 commits into
predevfrom
lane-sec-plane-secret-refs

Conversation

@MattJackson

@MattJackson MattJackson commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

SECURITY. A door plane is handed a secret reference where it used to be handed a credential's plaintext. Carried onto predev from the orphaned #684 (it sat on #599 → #559 → #554 → #546 → p6-d2-kernel, whose #526 closed unmerged). Supersedes #684.

Authority. BUSBAR-1.6.0.md Part 0, THE DESIGN §6 "Trust boundary" (l.828-834 at predev 9c0a394): "A plane never receives secret bytes. Logs, audit records and metrics carry a secret's reference, never its value." Part 3 §4 (l.2415): "the plane passes a ref and NEVER holds plaintext". SECURITY rulings, 2026-10-07:

  • a SecretRef appears only at a credential field;
  • url, command, args, cwd and token_url keep load-time interpolation (no narrowing from 1.5.5);
  • a credential filled in part becomes a template reference;
  • a member's env resolves at the spawn.

What was wrong on predev. ${VAR} is spliced into the config text before it is parsed. Its plaintext then reached a door plane at every crossing:

  • the door's validate;
  • the facing probe;
  • open settings and owned sections;
  • the config-apply refresh;
  • the stage-3g validate blob.

A member's env was also resolved at the declare, not at the spawn.

Changes

  • busbar-kernel config::filled:
    • The interpolation's structural check records each scalar ${VAR} it filled. A record holds the path, the variables, the value as written and a per-process keyed digest. It never holds the value.
    • plane_bound puts a reference at each credential position: {env: VAR} for a whole value, a template reference for a part. Only a value that still matches its digest is replaced.
  • Bound crossings on predev:
    • plane/door.rs settings_of, used by the door validate (both paths) and the facing probe;
    • DoorOwned::bytes;
    • root/boot.rs validate_dealt (a plane's verb seats);
    • root/serve.rs handed_settings, used by open and the config-apply refresh;
    • the owned sections at open.
  • busbar-contract: SecretRef::template / template_text / resolve_template, and Program::secret_env.
  • busbar-core-connector: install_spawn_secrets. Connection::spawn resolves a member's secret_env at the spawn, and nowhere earlier.
  • plugin-loader: install_member_secrets and MemberSecretFn are removed. The root installs the linked secret resolver into the connector instead.
  • The plane-driver test plane writes the blobs it is handed at open and refresh to disk, and its refresh now returns a snapshot.

What changed from #684's diff. #684 bound the crossings that P1b (#670) had routed through the deal's strip. P1b is not on predev, so this binds the crossings predev has: handed_settings and the door's settings_of. It does not strip reserved keys; that is P1b, which is its own task. Its test plane drops the pool_affinity blob, which belongs to P1b-a.

REDs

  • root an_interpolated_credential_reaches_a_door_plane_as_its_reference_never_its_bytes: the open, owned and refresh blobs carry no plaintext and no resolved reference;
  • kernel filled_tests;
  • connector a_members_secret_environment_resolves_at_the_spawn and a_member_whose_secret_environment_does_not_resolve_is_not_spawned;
  • contract a_template_reference_resolves_each_variable_in_place and a_programs_secret_environment_is_held_as_its_reference.

Proof (Latchkey, this tree)

Check Job Result
cargo check --workspace --all-targets cli-1679dbe9 0
clippy -p busbar-contract --all-targets -D warnings cli-bbab9e6a 0
clippy -p busbar-core-connector --all-targets -D warnings cli-ef45f9fe 0
clippy -p busbar-kernel --all-targets -D warnings cli-55b74cc6 0
clippy -p busbar-plugin-loader --all-targets -D warnings cli-9f4f58c1 0
clippy -p busbar --all-targets -D warnings cli-2841384a 0
test -p busbar-contract cli-0df868b7 1286 passed, 0 failed
test -p busbar-kernel cli-80e1d39c 2884 passed, 0 failed
test -p busbar-core-connector cli-1ca1921d 244 passed; 7 failed: universal_needs, each "a ... door is built beside the test binary" (the CI cdylib step; not built on a Latchkey runner)
test -p busbar-plugin-loader cli-02632d77 639 passed; 2 failed: the both-ways secret cells, "the busbar_secret_env_plugin cdylib is not built" (same cause)
test -p busbar cli-8e178202 653 passed; 23 failed: predev's own 22 on this runner (same set at predev 9c0a394, job cli-e6de4be3: the neutral frame door cdylib is not built, and the leaked serve-framed-wild publication under one-process cargo test). The 23rd is the new cell, refused by that same leaked publication ("admin route ... overlaps serve-framed-wild"); CI's test:workspace runs under nextest, one process per test

RED: shown by #684 on its own base (an_interpolated_credential... read the plaintext at all three blobs before the binding). On a Latchkey runner the one-test filter does not build the example cdylib, so the red/green pair for this cell is CI's.

…} filled crosses as its secret reference; a member's env resolves at spawn

SECURITY rulings (2026-10-07):
- a SecretRef only at a credential field;
- url, command, args, cwd and token_url keep load-time interpolation (no 1.5.5 narrowing);
- a credential filled in part is a template reference;
- a member's env resolves at the spawn.
Carried onto predev from the orphaned stack (#684 on #599 on #559): it no longer rides P1b. Every
crossing predev hands a door plane is bound: the door validate and facing probe, DoorOwned::bytes, the stage-3g validate blob, serve.rs handed_settings (open and the apply refresh) and the owned sections at open. The test plane records the open and refresh blobs it is handed (its refresh now answers a snapshot).

- busbar-kernel config::filled. The interpolation's structural check walks the real and placeholder
  trees in step; it records each scalar `${VAR}` filled. A record holds the path, the variables,
  whole or part, the value as written and a per-process keyed digest; it never holds the value.
- plane_bound replaces a value only at a CREDENTIAL position, and only while the value keeps its
  digest. A credential position is any of:
  - a credential key (api_key, token, client_secret, subject_token, password, secret, private_key,
    authorization, bearer, and the other token names);
  - an entry of a program's `env` map;
  - anything inside the kernel-owned `upstream_credentials` block.
  A whole value becomes `{ env: VAR }`. A part becomes a template reference
  (`{ module: template, settings: { text: "Bearer ${VAR}" } }`). Every other position crosses as
  interpolated. Nothing is refused.
- Bound blobs: the door's validate settings and facing probe, DoorOwned::bytes, the stage-3g
  validate blob of a plane's verb sections, and serve.rs `crossed` (open and both refresh paths)
  plus the owned sections.
- busbar-contract:
  - SecretRef::template / template_text / resolve_template (SECRET_MODULE_TEMPLATE);
  - Program::secret_env holds a program's `env` references as references.
- busbar-core-connector: install_spawn_secrets. Connection::spawn resolves `secret_env` there and
  nowhere earlier, a template's every `${VAR}` filled in place. A reference that does not resolve is
  refused, naming the variable and the source, never a value.
- plugin-loader: member_programs no longer resolves `env` at the declare (MemberSecretFn and
  install_member_secrets removed). The root installs the linked secret resolver into the connector
  instead.

REDs:
- root planes_tests `an_interpolated_credential_reaches_a_door_plane_as_its_reference_never_its_bytes`;
- kernel filled_tests;
- connector `a_members_secret_environment_resolves_at_the_spawn` and
  `a_member_whose_secret_environment_does_not_resolve_is_not_spawned`;
- contract `a_template_reference_resolves_each_variable_in_place` and
  `a_programs_secret_environment_is_held_as_its_reference`.
…gram resolved at the spawn, not a RootCfg type
@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

promote into predev: all green @d39006c97

Already red on predev, not this PR's: 15
gate row detail
test:xtask-selftest ::xtask selftest kind-isolation
test:xtask-selftest ::xtask selftest plane-purity
test:xtask-selftest ::xtask selftest no-deferral-strict-done
test:xtask-selftest ::xtask selftest plane-purity-strict
test:xtask-selftest ::xtask selftest unconstructed
test:xtask-selftest ::xtask selftest structure-lint
test:xtask-selftest ::xtask selftest plane-transport-neutrality
construction one-pick-site 3 production call site(s) of 'pick_among(' (ceiling 2): crates/busbar-kernel-egress/src/walk.rs:358; crates/busbar-llm/src/engine/exhaustion/fallback.rs:120; crates/busbar-llm/src/engine/pipeline.rs:8
kind-isolation kind-isolation:deps 3 finding(s), 92 shipped edge instance(s) over 29 class(es), 92 declaration(s); 55 '[[dep]]' row(s), 0 question(s): unlisted-dep-edge legacy -> plane busbar-llm -> busbar-plane-llm is a shipped edge w
kind-isolation-ship kind-isolation:deps 8 finding(s) over 92 shipped edge(s): ship-edge kernel -> store busbar-kernel -> busbar-store-memory is 'not-allowed': the architecture grants no kernel -> store edge, and the ship criterion is the ar
kind-isolation-ship kind-isolation:test-deps 5 finding(s) over 31 test edge(s): ship-edge cleanliness -> export busbar-core-admin -> busbar-export-prometheus is 'not-allowed': the architecture grants no cleanliness -> export edge, and the ship c
kind-isolation-ship kind-isolation:faces 2 finding(s) over 40 crate(s): foreign-entry crates/busbar busbar is kind 'root' and implements 'Transport' 1 time(s) in shipped source — the entry face of kind 'transport'. A trait implementation is
kind-isolation-ship kind-isolation:legacy-drain 3 finding(s): transitional-live qa/kind-isolation.toml 'busbar-a2a -> busbar-core-admin' (legacy drain: the retiring A2A engine's tests drive the admin surface that drained into the cleanliness tier)
ship-ready ship-ready:ship-twin 'kind-isolation-ship' is not green: kind-isolation:deps (a dependency the architecture does not grant is still in the graph); kind-isolation:test-deps (a dependency the architecture does not grant is
structure-lint structure-lint:plane-dup:unledgered 22 finding(s): PLANE-DUPLICATE (module): 'config.rs' — a2a:crates/busbar-a2a/src/a2a/config.rs decisions:crates/busbar-plane-decisions/src/config.rs voice:crates/busbar-voice/src/config.rs (the ledger

tests passed: 24853, failed: 0. Run: https://github.com/GetBusbar/busbar/actions/runs/38348625115 . Artifact verdict-d39006c974a6bb30975ce674e42da480090a9e28 (failures.json, junit.xml, raw.log; 90 days).

…utralize test URLs

kind-isolation:law0 (18 -> 24, back to 18):
- Program::secret_env -> Program::env_refs. `secret_env` spells the secret-env
  plugin's kind-qualified id: the new busbar-core-connector x secret cell and
  the busbar-contract / busbar-kernel / busbar-plugin-loader x secret rises.
- SECRET_MODULE_TEMPLATE is "env-template", not "template": as a module-name
  constant it made every "template" literal a secret instance name, including
  the kernel's prompt `template` field in the config-schema snapshot.
- The kernel's new filled.rs / filled_tests.rs build `{ env: VAR }`, the
  template reference and the program `env` key through the contract
  (SecretRef, SECRET_MODULE_ENV, PROGRAM_KEYS) rather than as literals
  (busbar-kernel x instance:secret back to 117).
- busbar-plugin-loader x instance:secret 111 -> 110: dispatch/plugin.rs no
  longer edits a member's `env` map in place (law0-slack, lowered as the row
  prescribes).

config-schema:snapshot-drift / selftest config-schema: the snapshot gains the
new `type SpawnSecretFn` alias (busbar-core-connector compose.rs).

instance-noun-neutrality:mcp: the filled / serve_planes test URLs end /v1, not /mcp.

Proved on Latchkey: gate kind-isolation (law0 at 18 findings, all standing
base offenders), gate config-schema (snapshot-drift PASS; baseline rows red
only because the dev checkout has no v1.5.3 tag), gate
instance-noun-neutrality (green), selftest config-schema (only failures are
the same missing-v1.5.3 baseline), busbar-kernel test target compiles. The
Latchkey run budget ran out before kind-isolation-ship (same law0 rule as
kind-isolation) and the contract/connector/loader/root test runs.
@MattJackson MattJackson added fixing and removed fixing labels Oct 10, 2026
…ation:law0 30 -> 21)

The law0 figure is hits off the [[law0]] ceilings, not findings. Against the
base (50a31fb) this PR added 9 instance:secret hits: busbar +5 (four "env"
literals in serve_planes.rs, and the fixture's new `extern "C" fn refresh`,
whose "C" is the `c` secret alias), busbar-contract +4 (three "env" literals
in conn_tests.rs and the SECRET_MODULE_TEMPLATE "env-template" declaration).

- busbar-contract: the member program's `env` registration key is now one
  constant, conn::PROGRAM_ENV_KEY; PROGRAM_KEYS and Program::from_settings
  read it instead of spelling "env".
- Tests write the `{ env: VAR }` reference through SECRET_MODULE_ENV and the
  member env key through PROGRAM_ENV_KEY (serve_planes.rs, conn_tests.rs,
  connector.rs).

Net per cell against the base: busbar x instance:secret 37 -> 37,
busbar-contract x instance:secret 133 -> 133; the remaining +1 in each (the
fixture's `extern "C"`, the "env-template" declaration) is offset by the
literals the constant replaces. No ceiling changes.

Proof: `busbar-release dev cargo xtask gate kind-isolation` was refused by the
Latchkey run budget (4000/4000). The counts above come from a line-for-line
port of the gate's literal scanner (instances.rs literals/decoded_line/
join_adjacent_literals) run on base and candidate; it reproduces CI's own
per-cell readings exactly (busbar 37 -> 42, busbar-contract 133 -> 137).
Compilation and the touched tests are left to CI.
…et-refs

# Conflicts:
#	crates/busbar-kernel/src/plane/door.rs
@MattJackson MattJackson added fixing and removed fixing labels Oct 10, 2026
@MattJackson
MattJackson added this pull request to the merge queue Oct 10, 2026
@MattJackson
MattJackson removed this pull request from the merge queue due to a manual request Oct 10, 2026
@MattJackson MattJackson added fixing and removed fixing labels Oct 10, 2026
…not the declare

predev's member_secret_tests.rs (plugin-loader) and root/tests/secret_source.rs (busbar) still
installed the loader's declare-time resolver (install_member_secrets), which this branch replaced
with the connector's spawn-time one (install_spawn_secrets); neither compiled (E0425), which broke
build:check/clippy/workspace-compile and every test row that builds them.

- member_secret_tests.rs: brings its own env_reference (the linked env source, as before); the
  member is declared with its reference unresolved (Program::env_refs, no value, nothing of the
  source's refusal handed to the table); the test table models the connector's spawn (a reference
  that does not resolve refuses the open), and the plugin still reads only ConnError::Refused's
  fixed text, nothing of the source.
- secret_source.rs: installs the spawn resolver; the request-time cases are unchanged (the member
  is never spawned, the caller's render is clean). Across an apply, the program spawned while its
  reference resolved keeps serving the unchanged member (its value lives only in the child), and
  the caller's render stays clean of the source.

Not proved on Latchkey: the run budget is spent (RUN_CAP), so the PR's CI is the proof. The
xtask selftest rows (blocking-ffi, tracing, response-header, settings-leak) show no failure
detail in the run log beyond the compile errors; they are expected to clear with the build.
kind-isolation:law0 reads the same 11 hits / 5 findings as predev (#751); its armed red is the
whole-row judgement of the turnstile build CI ran, which busbar-release 5a1380d replaced with the
per-cell one (the one cell this branch moves, busbar-plugin-loader x instance:secret, sits at
its new row of 110).
@MattJackson MattJackson added fixing and removed fixing labels Oct 11, 2026
@MattJackson

Copy link
Copy Markdown
Collaborator Author

Claim released by busbar-release: the fixing label had no live agent behind it: no push and no new claim for 15h57m (the longest agent's hard timeout is 2h00m), so it is off and the PR is back in the pipeline.

…program through its carrier (host io.spawn), so a member's env references resolve inside the host's spawn and never cross into the carrier's destination
…kind-qualified secret id in the neutral connector: kind-isolation:law0 busbar-core-connector x secret back to none)
…dev's bound() hands it (no-silent-skip: a missing cdylib fails the cell, never skips it)
…lane the settings settings_of builds (the pools-map plane keyed at the root, any other at its section) and adds the pools-map plane's refresh crossing, so every crossing binds its credential positions at the settings' own path (settings_path): open, the config-apply refresh and the pools-map refresh
…instance:secret = 109 (predev OpenOnce -1 plus this PR's env-resolve -1, both cited)
@MattJackson
MattJackson merged commit 350e107 into predev Oct 13, 2026
10 checks passed
@MattJackson
MattJackson deleted the lane-sec-plane-secret-refs branch October 13, 2026 05:08
MattJackson added a commit that referenced this pull request Oct 13, 2026
Brings predev 350e107 (#727). Conflict: qa/kind-isolation.toml, busbar-plugin-loader x instance:secret: predev's row (109, its two drains cited) kept, with this PR's own delta applied: the extern-ABI scanner fix, -86 as measured on #787's tree (111 -> 25), so 109 - 86 = 23. Neither predev drain is an extern ABI string. The earlier 25 assumed #671's drain was one; predev's cite names it as the OpenOnce move, so it was not.
MattJackson added a commit that referenced this pull request Oct 13, 2026
Brings predev 350e107 (#727). Conflict: qa/kind-isolation.toml, busbar-plugin-loader x instance:secret: predev's row (109) kept, with this PR's own delta applied: the raw JSON-lane loader and cold auth lane deleted, -2, so 107.
MattJackson added a commit that referenced this pull request Oct 13, 2026
Brings predev 350e107 (#727: a plane never receives secret bytes; open and handed_settings take the settings path). Conflict: crates/busbar/src/root/tests/serve_planes.rs, where both sides appended a test: this branch's catalog-route cell and predev's plane-bound credential cell are both kept; every open call there takes predev's path argument.
MattJackson added a commit that referenced this pull request Oct 13, 2026
…erged tree's shapes

- main.rs config-apply DoorReach lends the shared resolver as Arc<dyn SecretResolve>, as boot does;
- root tests door_steps own_reach, the a2a door's root cells: DoorReach names models: None;
- serve_door_far: DoorFar names shown: None;
- serve_planes: refreshed takes the resolver as Arc<dyn SecretResolve>;
- kernel tests/plane_driver: Pick::Exhausted states its detail; REFUSAL_KERNEL is no longer used.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant