Skip to content

Self-sufficient zero-trust perimeter bootstrap (+ ADR-0009/0010)#62

Merged
albertdobmeyer merged 7 commits into
mainfrom
feat/zero-trust-bootstrap
May 20, 2026
Merged

Self-sufficient zero-trust perimeter bootstrap (+ ADR-0009/0010)#62
albertdobmeyer merged 7 commits into
mainfrom
feat/zero-trust-bootstrap

Conversation

@albertdobmeyer
Copy link
Copy Markdown
Owner

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 on podman.

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)

  • Signed perimeter spec (perimeter.yml, compile-time embedded → covered by the AppImage signature) + a native podman orchestrator replacing all podman compose shellouts.
  • monorepo_rootruntime_data_dir (~/.opentrapp/); .env moves there; no source-tree assumption.
  • CI build-images job: builds the 4 images, cosign keyless-signs, pushes to GHCR by digest, exports OCI tarballs, emits a signed image-digests.json overlay + 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).
  • Bundled component manifests: UI renders on a clean box with no source clone.

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

  • 91 Rust tests pass (incl. live --ignored podman bring-up + digest-tamper refusal)
  • orchestrator-check.sh 48/48 (ADR-0009 five-container checks)
  • All 4 Containerfiles build locally
  • CI-gated (post-merge, via v0.5.0-rc1 tag): signed GHCR images + bundled rc AppImage + clean-box launch reaching Ok + AppImage-level tamper test

🤖 Generated with Claude Code

albertdobmeyer and others added 7 commits May 19, 2026 22:35
…-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>
@albertdobmeyer albertdobmeyer enabled auto-merge May 20, 2026 14:49
@albertdobmeyer albertdobmeyer merged commit 6aa5d5a into main May 20, 2026
14 checks passed
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