You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add the registrar-redteam Docker scenario and gate every pull request on it
Context
The registrar holds only an mTLS client certificate and key for the daemon's Unix socket. It holds no OpenBao token, role_id, or secret_id; the daemon's separate bootroot-internal certificate is the only material that can obtain the authority used to mint service identities. This scenario must prove that boundary against a live OpenBao rather than a mock that could reproduce the same authorization bug.
This is a credential-level test: it models registrar material copied from the host. Its attacker input is a bounded, read-only registrar-leak bundle: the regular files supplied to the registrar client for this scenario, namely its client certificate and key plus its socket/endpoint configuration and certificate-pin material. It deliberately excludes the daemon's private credential, its configuration, and unrelated host files. A manifest defines the bundle, and the scenario verifies that the staged file set equals that manifest before recursively scanning the bundle for OpenBao or AppRole credentials. It does not model an exploited root request handler, a compromised control plane, or root access to the bootroot host.
The acceptance suite also has open cargo-only and endurance arms. Cargo-only cases, including ordinary live-listener and wire round trips that do not need Docker or root, remain in the cargo arm. Certificate renewal and no-AppRole-during-renewal checks, whose cost is waiting past expiry, remain in the endurance arm. This issue owns the real-OpenBao, process-boundary, and root-owned-file cases that complete after one stack bring-up, plus support data used exclusively by this scenario.
Scope
Add scripts/impl/run-registrar-redteam.sh and its ignored wrapper tests/docker_e2e_registrar_redteam.rs, following the existing run-two-instance-isolation.sh convention. Wire the scenario into scripts/preflight/ci/e2e-matrix.sh and add a registrar-redteam entry, with a ci-registrar-redteam artifact, to test-docker-e2e-matrix in .github/workflows/ci.yml.
Define and document one launcher contract for the script: it takes no positional arguments and requires absolute BOOTROOT_PROJECT_DIR, BOOTROOT_BIN, and ARTIFACT_DIR environment variables. BOOTROOT_PROJECT_DIR is the checked-out project root from which the scenario reads its compose files, scripts, fixtures, and policy data; BOOTROOT_BIN is the executable built from that checkout and used for all bootroot commands; ARTIFACT_DIR is a unique writable directory where the script records its logs, generated inputs, and failure evidence. The script validates each input before it is used for scenario setup. RUN_TOKEN is the only optional control input; it overrides the sanitized run identifier used solely for run-scoped names. The wrapper passes env!("CARGO_MANIFEST_DIR"), env!("CARGO_BIN_EXE_bootroot"), and its unique artifact path; the matrix entry, docs-only companion, and preflight matrix pass the checkout root, target/debug/bootroot, and their own ci-registrar-redteam-<run-id> artifact directory.
Make the CI dispatch explicit for code-changing pull requests: exclude registrar-redteam from the generic lifecycle runner and add a dedicated matrix step selected only for that label. The step creates the run-scoped ARTIFACT_DIR, supplies BOOTROOT_PROJECT_DIR and BOOTROOT_BIN according to the launcher contract, invokes run-registrar-redteam.sh, and on failure prints the phase, run, cargo-test, init, and compose logs that exist before failing the job.
Preserve the literal every-pull-request gate despite the existing matrix job's docs_only skip: add a focused companion CI job that runs only when needs.changes.outputs.docs_only == 'true'. It performs the same build and dedicated red-team invocation, artifact upload, timeout, and failure-log handling as the matrix entry, but does not start the unrelated Docker matrix scenarios. Thus a documentation-only pull request runs the companion job, and every other pull request runs the matrix entry, with exactly one red-team gate per pull request.
Put common registrar Docker bring-up in one sourced helper under scripts/impl/lib/. This scenario owns and uses that helper so the endurance arm can source it rather than duplicate setup. Reuse scripts/impl/lib/audit-log.sh for OpenBao audit-device checks.
Make runs safe beside a developer installation: derive run-scoped container and instance names, select free loopback ports, and remove created identities, roles, policies, containers, and temporary files on success and failure.
Stage the bounded registrar-leak bundle from the manifest as a read-only attacker filesystem. Assert its regular-file set matches the manifest, then scan only that staged bundle for OpenBao tokens, role_id, and secret_id; do not scan the whole container or the daemon's private material.
Against a live TLS OpenBao, prove escalation is denied with only the staged registrar-leak bundle: a registrar-leaf auth/cert/login is refused; the bundle contains no OpenBao token, role_id, or secret_id; unauthenticated attempts to create an AppRole with a privileged policy and to write an ACL policy both fail.
Drive the AppRole attack from tests/e2e/registrar/privileged-policies.txt. The newline-delimited file lists exactly the string values of every POLICY_BOOTROOT_* constant in src/commands/init/constants.rs, including the bootroot-internal policy. Add an ordinary, non-ignored binary-crate test that fails if the constants and file differ, runs under cargo test --bin bootroot and bare cargo test, and needs neither Docker nor root. The script must not keep a second policy-name list.
Prove allowed registrar behavior is narrowly contained: mint and deregister over the socket succeed and are idempotent; each minted role has only its derived bootroot-service-<registration_id> policy; unknown socket operations and use of the registrar leaf as a service identity are refused. Direct access with registrar material to the CA trust, responder-HMAC, agent-EAB, and a newly minted service's KV paths is refused, while a legitimate mint demonstrates the daemon can perform the required reads and writes with its internal credential. Build paths from existing constants and the scenario's KV mount.
Exercise audit and capacity behavior against the real stack: retain the required OpenBao file audit device; a refused mint produces paired verb-level intent/outcome records and no OpenBao role, policy, or KV write; an exhausted scenario-local tmpfs audit store refuses a mint before creating anything; and scenario-local low-water settings make the endpoint report registrar_health.audit_capacity.state = low_water before exhausted.
Exercise the root-dependent socket boundary inside the container: assert that the socket is numeric 0:0 and mode exactly 0700, and that its parent directory is numeric 0:0 with the group-write and other-write bits clear (mode & 0o022 == 0) — a bitmask assertion, not an equality, because the deployed socket unit provisions the directory with RuntimeDirectoryMode=0755 (systemd/bootroot-registrar.socket), which is conformant: what the directory must guarantee is that nobody but root can replace the socket path; preserve the socket inode and path across a daemon restart; and show an unprivileged process cannot connect to, create, or replace the path while the daemon is stopped or after a failed start. Drive the inherited-listener contract; do not substitute a daemon that binds the path itself.
Add deterministic negative tests for peer credential and endpoint pinning. Construct the peer-credential case in this exact container-local order: (1) as root, create the socket directory owned by the unprivileged test UID with mode 0700; (2) as that UID, bind and listen on the socket path in that directory; (3) as root, chown 0:0 and chmod 0700 both the directory and the socket file. Assert the final directory and socket are each root:root mode 0700, then prove the caller rejects the still-live listener because its connected peer credentials identify the unprivileged UID rather than root. The post-bind ownership and mode transition preserves the bound, accepting listener, makes the final path metadata indistinguishable from the deployed layout, and removes any timing race; no ACL or attack-realism setup is part of this fixture. A caller must reject both a wrong-name endpoint leaf and a fingerprint mismatch.
Document the threat-model boundary and the split between this per-PR scenario, the cargo-only arm, and the renewal endurance arm in the script header and wrapper documentation.
Acceptance criteria
Every pull request has exactly one green registrar-redteam gate: ordinary pull requests run the test-docker-e2e-matrixregistrar-redteam entry and documentation-only pull requests run the focused companion job. Both publish ci-registrar-redteam artifacts.
The matrix label is excluded from the lifecycle step and its dedicated step executes run-registrar-redteam.sh with the required BOOTROOT_PROJECT_DIR, BOOTROOT_BIN, and ARTIFACT_DIR inputs and failure-log handling; the docs-only companion executes the equivalent dedicated invocation rather than the unrelated matrix.
The attacker filesystem is a read-only registrar-leak bundle whose regular files match its manifest and include all files supplied to the registrar client, but exclude daemon-private material. Credential scanning is recursive over this bundle and nowhere broader.
The live-OpenBao escalation test proves the staged registrar material cannot log in through auth/cert, contains no OpenBao or AppRole credential, and cannot create either a privileged AppRole or an ACL policy unauthenticated.
privileged-policies.txt is the sole policy input to the AppRole attack and a non-Docker test fails for both a missing and an extraneous POLICY_BOOTROOT_* value.
A successful, idempotent mint/deregister flow produces only the derived service policy, while every listed direct KV access and every non-verb use of the registrar credential is refused.
Refused and storage-exhausted mint attempts leave the required audit evidence and no corresponding OpenBao state; the low-water health state is observable before exhaustion.
The root-only socket tests assert that the socket is root:root mode exactly 0700 and its parent directory is root:root with mode & 0o022 == 0 (a bitmask, not an equality — the deployed RuntimeDirectoryMode=0755 directory is conformant), inherited-listener inode preservation, path-occupation refusal, the ordered post-bind peer-credential fixture and its unprivileged-peer refusal, and both endpoint-pinning failures.
The scenario is rerunnable without colliding with a developer installation or leaving backend, container, or temporary-file residue.
Both CI paths complete within the existing 20-minute timeout, with the scenario wall-clock recorded in the pull request.
Constraints
Assert observable behavior against a live backend; do not add production test hooks, fault-injection seams, or inspection-only APIs. If an assertion exposes a product defect, fix only the smallest correct defect rather than weakening the test.
Do not make documentation-only pull requests an exception to the red-team gate, and do not run every unrelated Docker scenario on them just to satisfy this requirement.
Do not move cargo-only cases into this Docker scenario, or move any security assertion to the non-PR endurance tier.
Do not widen production visibility merely to place a test. Keep the policy-list guard with this scenario's data even though that guard itself runs without Docker.
Do not copy common Docker setup into the endurance scenario; maintain the sourced helper under scripts/impl/lib/.
Do not hard-code OpenBao paths or duplicate policy lists where repository constants or the scenario data file supply them.
Do not broaden the credential-leak model with a whole-container scan or silently narrow it by omitting a file provided to the registrar client from the manifest.
Do not infer the project checkout or binary from the process working directory in one launch path while another supplies it differently; every launcher must satisfy the same explicit script contract.
Out of scope
In-process derivation, identity-shape and safe-set refusals, collision and serialization races, ordinary endpoint handshake/round-trip tests, codec tests, and wire-visible error-shape tests. These require neither Docker nor root and belong to the cargo-only acceptance arm.
Certificate renewal past expiry and proving the renewal path reads no AppRole credentials. These require wall-clock waiting and belong to the extended endurance arm.
Production implementation of the registrar endpoint, credentials, audit store, limiter, capacity probe, or renewal loop, except for a minimal correction exposed by this scenario.
Performance or load testing and changes outside this repository.
Test plan
Run the non-ignored policy-list guard with cargo test --bin bootroot; temporarily add and remove a POLICY_BOOTROOT_* constant to demonstrate that the guard fails before Docker is involved.
Run scripts/preflight/ci/e2e-matrix.sh and scripts/preflight/run-all.sh; if passwordless sudo is unavailable, run the matrix until its root prerequisite blocks it and record that CI remains the gate for that arm. Local macOS cannot provide native setpriv or passwordless sudo; the Linux Docker E2E (registrar-redteam) CI job remains the gate.
Verify the wrapper, preflight matrix, code-changing matrix entry, and docs-only companion each invoke the script with no positional arguments and the required project-root, binary, and unique artifact-directory inputs. Verify both CI dispatch paths: a code-changing pull request invokes the dedicated registrar-redteam matrix step, and a documentation-only pull request invokes only the focused companion job. Inspect the artifact on failure and record the scenario wall-clock in the pull request.
Run cargo fmt -- --check --config group_imports=StdExternalCrate and cargo clippy --all-targets -- -D warnings.
Dependencies
Depends on the registrar endpoint, registrar mTLS client identity, bootroot-internal OpenBao certificate credential, audit record and capacity reporting, and the per-service mint/deregister verbs being available in the checkout — including mint actually served through the endpoint (#912): the functionality-preserved cases prove a successful mint and deregister over the socket, and until #912 lands the endpoint refuses every wire spec with GrammarNotSettled (src/registrar/endpoint/production.rs), so this scenario cannot pass. It is part of the registrar acceptance suite. The endurance arm consumes the shared helper produced here but remains responsible for its own renewal cases.
Pointers
scripts/impl/run-two-instance-isolation.sh and tests/docker_e2e_two_instance_isolation.rs — scenario shape, run-scoped names, and free-port handling
scripts/impl/run-registrar-verbs-e2e.sh — a focused OpenBao-backed registrar scenario
Add the registrar-redteam Docker scenario and gate every pull request on it
Context
The registrar holds only an mTLS client certificate and key for the daemon's Unix socket. It holds no OpenBao token,
role_id, orsecret_id; the daemon's separate bootroot-internal certificate is the only material that can obtain the authority used to mint service identities. This scenario must prove that boundary against a live OpenBao rather than a mock that could reproduce the same authorization bug.This is a credential-level test: it models registrar material copied from the host. Its attacker input is a bounded, read-only registrar-leak bundle: the regular files supplied to the registrar client for this scenario, namely its client certificate and key plus its socket/endpoint configuration and certificate-pin material. It deliberately excludes the daemon's private credential, its configuration, and unrelated host files. A manifest defines the bundle, and the scenario verifies that the staged file set equals that manifest before recursively scanning the bundle for OpenBao or AppRole credentials. It does not model an exploited root request handler, a compromised control plane, or root access to the bootroot host.
The acceptance suite also has open cargo-only and endurance arms. Cargo-only cases, including ordinary live-listener and wire round trips that do not need Docker or root, remain in the cargo arm. Certificate renewal and no-AppRole-during-renewal checks, whose cost is waiting past expiry, remain in the endurance arm. This issue owns the real-OpenBao, process-boundary, and root-owned-file cases that complete after one stack bring-up, plus support data used exclusively by this scenario.
Scope
scripts/impl/run-registrar-redteam.shand its ignored wrappertests/docker_e2e_registrar_redteam.rs, following the existingrun-two-instance-isolation.shconvention. Wire the scenario intoscripts/preflight/ci/e2e-matrix.shand add aregistrar-redteamentry, with aci-registrar-redteamartifact, totest-docker-e2e-matrixin.github/workflows/ci.yml.BOOTROOT_PROJECT_DIR,BOOTROOT_BIN, andARTIFACT_DIRenvironment variables.BOOTROOT_PROJECT_DIRis the checked-out project root from which the scenario reads its compose files, scripts, fixtures, and policy data;BOOTROOT_BINis the executable built from that checkout and used for allbootrootcommands;ARTIFACT_DIRis a unique writable directory where the script records its logs, generated inputs, and failure evidence. The script validates each input before it is used for scenario setup.RUN_TOKENis the only optional control input; it overrides the sanitized run identifier used solely for run-scoped names. The wrapper passesenv!("CARGO_MANIFEST_DIR"),env!("CARGO_BIN_EXE_bootroot"), and its unique artifact path; the matrix entry, docs-only companion, and preflight matrix pass the checkout root,target/debug/bootroot, and their ownci-registrar-redteam-<run-id>artifact directory.registrar-redteamfrom the generic lifecycle runner and add a dedicated matrix step selected only for that label. The step creates the run-scopedARTIFACT_DIR, suppliesBOOTROOT_PROJECT_DIRandBOOTROOT_BINaccording to the launcher contract, invokesrun-registrar-redteam.sh, and on failure prints the phase, run, cargo-test, init, and compose logs that exist before failing the job.docs_onlyskip: add a focused companion CI job that runs only whenneeds.changes.outputs.docs_only == 'true'. It performs the same build and dedicated red-team invocation, artifact upload, timeout, and failure-log handling as the matrix entry, but does not start the unrelated Docker matrix scenarios. Thus a documentation-only pull request runs the companion job, and every other pull request runs the matrix entry, with exactly one red-team gate per pull request.scripts/impl/lib/. This scenario owns and uses that helper so the endurance arm can source it rather than duplicate setup. Reusescripts/impl/lib/audit-log.shfor OpenBao audit-device checks.role_id, andsecret_id; do not scan the whole container or the daemon's private material.auth/cert/loginis refused; the bundle contains no OpenBao token,role_id, orsecret_id; unauthenticated attempts to create an AppRole with a privileged policy and to write an ACL policy both fail.tests/e2e/registrar/privileged-policies.txt. The newline-delimited file lists exactly the string values of everyPOLICY_BOOTROOT_*constant insrc/commands/init/constants.rs, including the bootroot-internal policy. Add an ordinary, non-ignored binary-crate test that fails if the constants and file differ, runs undercargo test --bin bootrootand barecargo test, and needs neither Docker nor root. The script must not keep a second policy-name list.bootroot-service-<registration_id>policy; unknown socket operations and use of the registrar leaf as a service identity are refused. Direct access with registrar material to the CA trust, responder-HMAC, agent-EAB, and a newly minted service's KV paths is refused, while a legitimate mint demonstrates the daemon can perform the required reads and writes with its internal credential. Build paths from existing constants and the scenario's KV mount.tmpfsaudit store refuses a mint before creating anything; and scenario-local low-water settings make the endpoint reportregistrar_health.audit_capacity.state = low_waterbeforeexhausted.0:0and mode exactly0700, and that its parent directory is numeric0:0with the group-write and other-write bits clear (mode & 0o022 == 0) — a bitmask assertion, not an equality, because the deployed socket unit provisions the directory withRuntimeDirectoryMode=0755(systemd/bootroot-registrar.socket), which is conformant: what the directory must guarantee is that nobody but root can replace the socket path; preserve the socket inode and path across a daemon restart; and show an unprivileged process cannot connect to, create, or replace the path while the daemon is stopped or after a failed start. Drive the inherited-listener contract; do not substitute a daemon that binds the path itself.0700; (2) as that UID, bind and listen on the socket path in that directory; (3) as root,chown 0:0andchmod 0700both the directory and the socket file. Assert the final directory and socket are eachroot:rootmode0700, then prove the caller rejects the still-live listener because its connected peer credentials identify the unprivileged UID rather than root. The post-bind ownership and mode transition preserves the bound, accepting listener, makes the final path metadata indistinguishable from the deployed layout, and removes any timing race; no ACL or attack-realism setup is part of this fixture. A caller must reject both a wrong-name endpoint leaf and a fingerprint mismatch.Acceptance criteria
test-docker-e2e-matrixregistrar-redteamentry and documentation-only pull requests run the focused companion job. Both publishci-registrar-redteamartifacts.run-registrar-redteam.shwith the requiredBOOTROOT_PROJECT_DIR,BOOTROOT_BIN, andARTIFACT_DIRinputs and failure-log handling; the docs-only companion executes the equivalent dedicated invocation rather than the unrelated matrix.auth/cert, contains no OpenBao or AppRole credential, and cannot create either a privileged AppRole or an ACL policy unauthenticated.privileged-policies.txtis the sole policy input to the AppRole attack and a non-Docker test fails for both a missing and an extraneousPOLICY_BOOTROOT_*value.root:rootmode exactly0700and its parent directory isroot:rootwithmode & 0o022 == 0(a bitmask, not an equality — the deployedRuntimeDirectoryMode=0755directory is conformant), inherited-listener inode preservation, path-occupation refusal, the ordered post-bind peer-credential fixture and its unprivileged-peer refusal, and both endpoint-pinning failures.Constraints
scripts/impl/lib/.Out of scope
Test plan
cargo test --bin bootroot; temporarily add and remove aPOLICY_BOOTROOT_*constant to demonstrate that the guard fails before Docker is involved.scripts/preflight/ci/e2e-matrix.shandscripts/preflight/run-all.sh; if passwordlesssudois unavailable, run the matrix until its root prerequisite blocks it and record that CI remains the gate for that arm. Local macOS cannot provide nativesetprivor passwordlesssudo; the LinuxDocker E2E (registrar-redteam)CI job remains the gate.registrar-redteammatrix step, and a documentation-only pull request invokes only the focused companion job. Inspect the artifact on failure and record the scenario wall-clock in the pull request.cargo fmt -- --check --config group_imports=StdExternalCrateandcargo clippy --all-targets -- -D warnings.Dependencies
Depends on the registrar endpoint, registrar mTLS client identity, bootroot-internal OpenBao certificate credential, audit record and capacity reporting, and the per-service mint/deregister verbs being available in the checkout — including mint actually served through the endpoint (#912): the functionality-preserved cases prove a successful mint and deregister over the socket, and until #912 lands the endpoint refuses every wire
specwithGrammarNotSettled(src/registrar/endpoint/production.rs), so this scenario cannot pass. It is part of the registrar acceptance suite. The endurance arm consumes the shared helper produced here but remains responsible for its own renewal cases.Pointers
scripts/impl/run-two-instance-isolation.shandtests/docker_e2e_two_instance_isolation.rs— scenario shape, run-scoped names, and free-port handlingscripts/impl/run-registrar-verbs-e2e.sh— a focused OpenBao-backed registrar scenarioscripts/impl/lib/audit-log.sh— reusable audit-device assertionscripts/preflight/ci/e2e-matrix.shand.github/workflows/ci.yml— per-PR Docker matrix wiring and docs-only filtersrc/commands/init/constants.rs— privileged policy and KV-path constantssrc/commands/service/approle.rsandsrc/trust_bootstrap.rs— derived service policy and per-service KV pathssrc/registrar/endpoint/— inherited socket activation, peer authentication, and endpoint pinning