Self-sufficient zero-trust perimeter bootstrap (+ ADR-0009/0010)#62
Merged
Conversation
…-0009, ADR-0010) Adds the vault-egress sidecar that separates L3 network-layer policy (kernel nftables RFC1918 drop + pinned DoT resolver) from vault-proxy's L7 application policy. vault-proxy loses its external-net attachment and routes outbound through vault-egress; vault-egress holds NET_ADMIN but no secrets and runs no application code. - compose.yml: vault-egress service + egress-net (10.230.0.0/24); vault-proxy upstream-mode through egress; vault-proxy no longer on external-net - docs/adr/0009-five-container-perimeter.md, 0010-pinned-resolver-dns.md - tests/orchestrator-check.sh: assert five-container topology + vault-proxy has no external-net attachment - docs/* + diagrams: five-container narrative - components/opencli-container, components/openskill-forge: submodule refs - ci.yml: release title Lobster-TrApp -> OpenTrApp Status per ADRs: Tier 2 (L7 destination-IP check) landed and tested; vault-egress container config landed but activation/end-to-end verification is pending a dedicated session. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the host-dependent `podman compose` shell-out (the v0.4.1 first-launch failure: podman delegated to an un-pinned docker-compose-v2) with code we own that depends only on `podman`. - resources/perimeter.yml: the runtime topology contract, embedded into the binary via include_str! so it is covered by the AppImage signature — the perimeter cannot be altered without a rebuild + re-sign. Image refs by repo:tag (digests/signatures come from a CI-generated overlay, not here); secrets referenced by name only; policy files declared as verified `resource` mounts (no writable host bind-mounts of policy — ADR-0009). - orchestrator/perimeter.rs: typed parser + start_order() topological sort + 6 security-invariant tests (only egress holds NET_ADMIN, agent maximally contained, no inlined secrets, deps respected). - orchestrator/podman.rs: pure container_run_args/network_create_args builders (seccomp + policy files resolve under the verified resource dir, never a source-tree path) and up/down/ensure_networks/wait_healthy. Image trust is behind an ImageVerifier trait (DevVerifier now; cosign+digest verifier in a later step). 6 arg-translation tests + live podman network round-trip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Repoints all 17 run_compose/run_compose_with_runtime call sites to the podman
orchestrator and removes the compose shell-out entirely. Splits the
source-tree-assuming `monorepo_root` into `runtime_data_dir` (~/.opentrapp/);
`.env` now lives there, not in the source tree.
- orchestrator/podman.rs: lifecycle façade (perimeter_up/down/stop, shell_up,
service_up, ensure_images) + runtime_data_dir/resource_dir/load_runtime_env
- lib.rs: find_monorepo_root (walked for a components/ dir) → runtime_data_dir
- lifecycle.rs: bring_perimeter_up/down → orchestrator; delete run_compose
- bootstrap/mod.rs: step 4 build → ensure_images (no on-host build); step 5
pull → verify; step 6 → shell_up; vault-egress joins SHELL_SERVICES (ADR-0009);
delete run_compose_with_runtime + images_already_built
- commands/{lifecycle,credentials,prerequisites,manifest_cmds}.rs,
bootstrap/auto_activate.rs, status_aggregator.rs: rename field; repoint calls
cargo build clean; 84 lib tests pass. Note: discover_components still reads a
components/ dir (returns empty without a source tree) — fixed in the
container-embedded manifest step.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a build-images job (tag builds only) that builds the four OpenTrApp container images in CI — never on the user's machine — then per image: cosign keyless-signs it, pushes to GHCR by digest, and exports an OCI tarball. Emits a signed image-digests.json overlay (per-image digest + signer identity regexp + OIDC issuer) that the runtime verifies before `podman run`, plus a SLSA build-provenance attestation over the bundled tarballs. vault-proxy (upstream mitmproxy) is digest-pinned as source=external and exported too so first launch is fully offline. The Linux build-and-release job now downloads these into app/src-tauri/resources/perimeter/images/ before the Tauri build (bundle.resources wiring lands in the next step). build-and-release tolerates a skipped build-images on non-tag pushes via a !cancelled() gate. Also fixes broken release-notes download links (Lobster-TrApp_* -> OpenTrApp_*), which pointed at filenames the build no longer produces. Verification deferred to a tag-triggered Actions run. Locally confirmed the never-before-built vault-egress Containerfile assembles. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The trust keystone of the zero-trust bootstrap. Images and policy files are bundled inside the cosign-signed AppImage; at launch the app loads them and pins every image to the digest in the signed image-digests.json overlay, refusing any mismatch. No runtime cosign (Karen has none; keyless verify needs network) — the cosign keyless signatures on GHCR are the public/audit axis. - orchestrator/podman.rs: BundleVerifier (digest-pinned, refuses unknown/ mismatched images), ImageDigestOverlay parser, repo_key helper, make_verifier (bundle in prod, DevVerifier in dev), stage_resources_from_bundle + load_bundled_images. Façades select the verifier; 4 new unit tests including the unknown-image-refused tamper guard. - build.rs: stage policy files (seccomp x2, vault-proxy.py, allowlist.txt, resolv.conf) from the submodules into resources/perimeter/ for bundling. - tauri.conf.json: bundle.resources packages resources/perimeter/ → perimeter/. - bootstrap/mod.rs: prepare_bundle() restages policy files (self-healing) and podman-loads the signed image tarballs from the read-only bundle mount before the pipeline runs; no-op in dev. - ci.yml: image-digests overlay now records each entry's tarball name. - .gitignore: resources/perimeter/ is generated, not committed. 88 lib tests pass; build clean. Live bundle/load/tamper verification runs in the clean-box E2E step (needs a real AppImage build). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
discover_components walked a components/ source tree and returned empty on any installed AppImage, so the UI couldn't render dashboards. Now manifests are bundled inside the signed AppImage and discovered from there — available even before the perimeter is up (install/bootstrapping screens), with no podman exec latency and no source clone. - discovery.rs: discover_first(candidates) tries the bundled manifests dir, the runtime-staged copy, then the dev source tree; discover_components_at(dir) factored out. Test: bundled manifests parse into 3 components without a tree. - manifest_cmds.rs: list_components/get_component take AppHandle (auto-injected; frontend invoke unchanged) and resolve candidates via the resource dir. - build.rs: stage each component.yml into resources/perimeter/manifests/<c>/. 89 lib tests pass. Note: component_dir still feeds host-side command execution (workflow/stream/config); running those via `podman exec` is a separate follow-up — not on the Karen install/Telegram critical path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two #[ignore] integration tests (run with --ignored; require local images + podman) that exercise the real code paths string assertions can't: - live_forge_brings_up_and_tears_down: the real container_run_args + network_create_args produce a podman run that podman accepts; the container comes up with the io.opentrapp.service label, then tears down cleanly. - live_tampered_digest_is_refused: BundleVerifier refuses an image pinned to a wrong digest (the tamper guard, end to end). Both pass locally against the built vault-forge image. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the architectural flaw surfaced by the v0.4.0/v0.4.1 Karen E2E: the install path ran a developer pipeline (assumed a source clone + host
podman compose), so first launch failed on every clean machine. The app now builds nothing on the user's machine — it loads pre-built, signed, digest-pinned images and orchestrates containers with code we own that depends only onpodman.Includes the previously-uncommitted ADR-0009 (five-container L7/L3 perimeter split +
vault-egress) and ADR-0010 (pinned DoT resolver) as the base commit.Zero-trust bootstrap (6 commits on top of ADR-0009)
perimeter.yml, compile-time embedded → covered by the AppImage signature) + a native podman orchestrator replacing allpodman composeshellouts.monorepo_root→runtime_data_dir(~/.opentrapp/);.envmoves there; no source-tree assumption.build-imagesjob: builds the 4 images, cosign keyless-signs, pushes to GHCR by digest, exports OCI tarballs, emits a signedimage-digests.jsonoverlay + SLSA attestation. Bundled into the Linux AppImage.BundleVerifier: loads images from the signed bundle and refuses any image whose digest doesn't match the overlay (offline-first; no runtime cosign needed — Karen has none).Trust model
Runtime trust = digest-pinning against the overlay baked into the cosign-signed AppImage. Cosign keyless on GHCR is the public/audit axis.
Test plan
--ignoredpodman bring-up + digest-tamper refusal)orchestrator-check.sh48/48 (ADR-0009 five-container checks)v0.5.0-rc1tag): signed GHCR images + bundled rc AppImage + clean-box launch reachingOk+ AppImage-level tamper test🤖 Generated with Claude Code