diff --git a/docs/superpowers/specs/2026-08-27-secure-bootstrap-privileged-installation-design.md b/docs/superpowers/specs/2026-08-27-secure-bootstrap-privileged-installation-design.md new file mode 100644 index 0000000..e19e353 --- /dev/null +++ b/docs/superpowers/specs/2026-08-27-secure-bootstrap-privileged-installation-design.md @@ -0,0 +1,510 @@ +# Secure bootstrap and privileged installation design + +**Status:** Approved in chat on 2026-08-27 + +**Parent program:** `docs/superpowers/specs/2026-08-26-repository-audit-remediation-design.md` + +## Purpose + +This document refines Package 2 after repository-wide reconnaissance found that the +same trust boundary extends beyond the three examples named in the parent design. +Panama must not turn mutable network content into root-capable code. That includes +the initial root bootstrap, package-manager repository bootstraps, downloaded +installers and artifacts, and user-level processes that run while the installer +keeps a live sudo credential. + +The package still has three independently testable parts: + +1. transactional SSH hardening in `boot --server`; +2. verified bootstrap and installer inputs; +3. transactional server firewall policy. + +All three are fixture-tested. This branch does not reload a real SSH daemon, mutate a +real firewall, install a real package, start a service, or apply changes to a server. + +## Decisions + +- Keep automatic installation when an official signature or a reviewed immutable + digest is available. Otherwise make the component explicit and optional. +- Keep the public `boot --server` interface, but remove the mutable + `bash <(curl .../main/boot)` installation path from the documentation. +- A missing or unsafe target SSH key makes hardening unavailable but does not stop + bootstrap. Validation or reload failure rolls back and stops bootstrap. +- Safe target SSH state means a non-root account, an absolute home, a real `.ssh` + directory owned by the target UID with mode `0700`, and a nonempty regular + `authorized_keys` file owned by the target UID with mode `0600`. Symlinks are + refused. Panama may create and normalize files it copied from root, but it does + not take ownership of an unsafe pre-existing target path. +- Failed SSH reload rollback includes restored-config validation and a reload of the + restored configuration, because a command can apply state and still return + nonzero. +- Firewalld is required. Setup attempts to enable and start it, then verifies both + states before any rule mutation. +- Ports 80 and 443 are public only through Cloudflare source ipsets. Port 81 is open + only in a unique WireGuard-only zone. With no WireGuard zone it stays closed and + setup reports that fact; ambiguity or an unsafe mixed-interface zone fails before + mutation. +- Panama removes only its own rules and the exact legacy direct `80/tcp`, `443/tcp`, + and `81/tcp` rules created by the old server installer. Other broad rules are + reported as conflicts, not silently deleted. +- There is no privileged firewall timer. Server installation and upgrades are the + refresh entry points. +- Use narrow verification helpers, not a generalized installer framework. + +## Shared trust model + +`install` obtains one sudo credential and refreshes it until the run ends. Therefore +code executed as the target user during that window is root-capable in practice. +Panama treats fetched shell, binaries, RPM scriptlets, npm lifecycle code, container +entrypoints, and sourced repository build definitions as executable inputs. + +Fetched JSON, repository descriptors, keys, checksums, HTML, icons, and CIDR lists are +data only while Panama parses them without executing them. They still require strict +shape, size, signer, and destination checks before they can authorize an executable +transaction. + +Every download follows this order: + +1. create a checked private temporary directory; +2. download without sudo with redirects, a 10-second connection timeout, a + 600-second total timeout, and the reviewed per-artifact byte limit; +3. verify the expected fingerprint, signature, or reviewed SHA-256; +4. stage the complete result on the destination filesystem; +5. use sudo only for the narrow final package/repository operation that needs it; +6. atomically replace user-owned installed artifacts; +7. remove temporary material on success, failure, INT, and TERM. + +Verification failure never falls back to the fetched artifact and never removes a +known-good installed version. + +## SSH hardening transaction + +### Public flow + +The root `boot --server` branch keeps account creation, password setup, key copy, +clone, and target-user handoff in the standalone `boot` file. It adds one private +function, `harden_server_ssh USER HOME`, because no repository helper exists before +the clone. + +Before offering hardening, Panama verifies: + +- the target account exists, has a numeric UID other than 0, and its `getent` home is + absolute and nonempty; +- neither the home-relative `.ssh` path nor `authorized_keys` is a symlink; +- `.ssh` and `authorized_keys` have the exact ownership and modes in Decisions; +- `authorized_keys` contains at least one nonblank, non-comment line; +- exactly one installed SSH unit is detected, preferring `sshd.service` and falling + back to `ssh.service` only when the first unit is absent. + +If the target has no key and root has a safe regular key file, Panama copies only that +file, creates `.ssh`, applies `0700/0600`, and changes ownership only on those two +paths. It does not recursively take ownership of an existing directory tree. + +If the preconditions fail, Panama prints why hardening is unavailable, keeps +root/password authentication unchanged, and continues the clone/install handoff. + +### Transaction + +The desired drop-in is exactly: + +```text +PermitRootLogin no +PasswordAuthentication no +``` + +Panama creates the candidate with `umask 077` and `mktemp` in +`/etc/ssh/sshd_config.d`. Its temporary name does not end in `.conf`, so the normal +include glob cannot activate it early. It preserves an existing +`90-panama.conf` in the same directory, arms EXIT/INT/TERM rollback, and atomically +renames the candidate over the final path. + +It then runs `sshd -t` against the complete active configuration and reloads only the +detected unit. Success disarms rollback and removes the backup. + +On validation failure, Panama restores or removes the new drop-in, validates the +restored configuration, and returns nonzero without reloading the rejected candidate. +On reload failure, Panama restores the previous drop-in, validates it, reloads the +restored unit, and returns nonzero. A rollback validation/reload failure preserves the +backup and prints its path plus exact recovery commands. + +Existing drop-ins go through the same desired-content, validation, and reload path; +mere existence is not treated as proof of hardening. + +### Hermetic adapter + +`PANAMA_BOOT_FIXTURE_ROOT` exists only for the contract. `boot` accepts it only when +the real effective UID is nonzero while the stubbed `id -u` reports root; an actual +root process that sets it exits before mutation. Absolute system paths are resolved +beneath that fixture root. Command adapters remain ordinary PATH stubs. + +The public test seam is the real `boot --server` command under a PTY. The fixture +stubs `id`, `passwd`, `getent`, `useradd`, `usermod`, `runuser`, `git`, `dnf`, `sshd`, +and `systemctl`, and provides temporary account and filesystem state. + +Required cases are: + +- missing, empty, comment-only, symlinked, wrong-owner, and wrong-mode target keys; +- safe root-key copy and safe existing target key; +- successful initial install and replacement of an existing drop-in; +- invalid candidate rollback; +- failed reload rollback, including restored validation and reload; +- rollback failure retaining its recovery artifact; +- exact command ordering and no install handoff after a transactional failure. + +## Verified bootstrap and installer inputs + +### Initial Panama bootstrap + +README installation instructions use a two-file trust assertion: a full 40-character +Git commit and the SHA-256 of `boot` at that commit. The command downloads the raw file +from the commit URL with a 10-second connection timeout, 30-second total timeout, and +256 KiB maximum, checks SHA-256, and only then executes it. + +The verified revision is passed as `PANAMA_BOOT_REVISION`. `boot` requires a full +lowercase hexadecimal commit, fetches that exact object, verifies +`git rev-parse HEAD^{commit}` equality, and only then executes `install`. A fresh +clone creates local `main` at that commit with `origin/main` as its upstream. An +existing checkout must be clean and may only fast-forward to the verified commit; +Panama never resets or overwrites local work. A mismatch, dirty checkout, divergent +history, or fetch failure stops before handoff. + +This uses two commits when refreshing the documented bootstrap: the implementation is +committed first; a following documentation commit records the preceding full commit +and `git show COMMIT:boot | sha256sum`. A hermetic contract recomputes the documented +digest from that committed object. A digest copied from the same hosting origin is not +a substitute for a long-lived publisher signing key, so Package 3 owns the stronger +authenticated-update/release-manifest design. + +### Verification helpers and provenance data + +Create `setup/lib/artifact-provenance` with only these public shell functions: + +```text +key_fingerprint_matches FILE EXPECTED_FINGERPRINT +download_sha256 URL EXPECTED_SHA256 DESTINATION +verify_detached_signature KEY_FILE SIGNATURE_FILE CONTENT_FILE +rpm_signature_matches PACKAGE_FILE KEY_FILE EXPECTED_FINGERPRINT +``` + +`rpm_signature_matches` imports only the expected key into a temporary RPM database +and requires `rpmkeys --dbpath ... --checksig` to report a valid package signature; +it does not trust or modify the host RPM keyring. + +Create `setup/provenance/installers.conf` as non-executable data. It uses one +`NAME=value` assignment per line, rejects unknown/duplicate keys, and is parsed without +`source` or `eval`. Per-architecture URLs, SHA-256 values, and maximum byte counts are +explicit. + +Vendored public keys live under `setup/provenance/keys/`. The accompanying README +records the official source URL, retrieval date, complete primary fingerprint, +verification command, and rotation policy. Runtime verification compares the complete +primary fingerprint, not a short key ID. + +As reviewed on 2026-08-27, the trust anchors are: + +| Source | Primary fingerprint | +|---|---| +| Terra 44 | `AE09157A4DE88B497EA1D5D300CDAB43DE226D6F` | +| Anthropic Claude Code | `31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE` | +| Bun releases | `F3DCC08A8572C0749B3E18888EAB4D40A7B22B59` | +| RPM Fusion free | `E9A491A3DE247814E7E067EAE06F8ECDD651FF2E` | +| RPM Fusion nonfree | `79BDB88F9BBF73910FD4095B6A2AF96194843C65` | +| lionheartp/Hyprland COPR | `97E23476C89635135407C7D5E9BA41342C4B2995` | +| Flathub | `6E5C05D979C76DAF93C081354184DD4D907A7CAE` | + +The initial reviewed artifact pins are: + +| Artifact | Architecture | SHA-256 | +|---|---|---| +| Bun 1.4.0 | x86_64 | `2d03fb5fb83ac8b567aca0a281b2ce1a1a19d488f56c2968d88c3f25e92fe452` | +| Bun 1.4.0 | aarch64 | `4b1a332ee861983eb93bcfe6f770fff94e3e31b2c388bdaea3c8ed35e58eed0e` | +| Node 24.20.0 | x86_64 | `2f2c0da162318f0de47665410c7c8c2ed3d36c8f3105de4bbc61176c70a7cbf2` | +| Node 24.20.0 | aarch64 | `5f4ddab610c1ab2016b3c227cebdbf6d9495161487e4739c7b90090595f465f7` | +| Codex 0.150.1 package | x86_64 musl | `00aba704f029f6dc0d948be407a756e0c97cc840132fd691353b2c6b0a505b17` | +| Codex 0.150.1 package | aarch64 musl | `1ecac3f87823efb98153233b076ea3d6e34a7a8cebe43c5285dc5f79e1514639` | +| RustDesk 1.4.9 RPM | x86_64 | `eb1b053ac5b2f774f2271f7fbbfd2ea475899f7a55135c5e172bc54b9388f108` | + +Unsupported architectures fail before download. Updating any version, URL, digest, or +key is an explicit reviewed repository change. + +The provenance README cites these publisher-controlled records: + +- Terra package instructions, key, and bootstrap limitation: + `https://github.com/terrapkg/packages/blob/frawhide/README.md`, + `https://repos.fyralabs.com/terra44/key.asc`, and + `https://github.com/terrapkg/packages/discussions/7736`; +- Anthropic setup and signing key: `https://code.claude.com/docs/en/setup` and + `https://downloads.claude.ai/keys/claude-code.asc`; +- Bun release and release-key usage: + `https://github.com/oven-sh/bun/releases/tag/bun-v1.4.0` and + `https://github.com/oven-sh/bun/blob/main/dockerhub/distroless/Dockerfile`; +- RPM Fusion keys: + `https://download1.rpmfusion.org/free/fedora/RPM-GPG-KEY-rpmfusion-free-fedora-2020` + and + `https://download1.rpmfusion.org/nonfree/fedora/RPM-GPG-KEY-rpmfusion-nonfree-fedora-2020`; +- Hyprland COPR key: + `https://download.copr.fedorainfracloud.org/results/lionheartp/Hyprland/pubkey.gpg`; +- Flathub descriptor: `https://flathub.org/repo/flathub.flatpakrepo`; +- Node release and verification instructions: + `https://github.com/nodejs/node/releases/tag/v24.20.0` and + `https://github.com/nodejs/node/blob/main/README.md`; +- Codex release and signing workflow: + `https://github.com/openai/codex/releases/tag/rust-v0.150.1` and + `https://github.com/openai/codex/blob/main/.github/workflows/rust-release.yml`; +- RustDesk release: `https://github.com/rustdesk/rustdesk/releases/tag/1.4.9`. + +### Repository roots + +- RPM Fusion release RPMs are downloaded first, verified against the vendored free + and nonfree keys, then installed as local files with + `--setopt=localpkg_gpgcheck=1`. +- Terra installs no package with `--nogpgcheck`. Panama verifies the Terra 44 key, + installs it atomically, and uses `terra.pkg_gpgcheck=1`, + `terra.repo_gpgcheck=1`, and the local pinned key with the official repository. + Failure stops before initial, desktop, or Hyprland package transactions. +- Hyprland COPR configuration is written from reviewed local data with the exact + base URL, `gpgcheck=1`, `repo_gpgcheck=1`, and pinned project key. Panama does not + use interactive/TOFU `dnf copr enable -y`. +- Flathub's descriptor is downloaded as data. Panama decodes and verifies its embedded + primary key and requires GPG verification before adding or retaining the remote. + A mismatch preserves an existing remote and skips Flathub transactions. + +The exact Terra DNF command receives a disposable Fedora 44 container smoke test. It +is never tried on the daily-driver host. If the signed bootstrap cannot be made to +work, Terra is reported unavailable and desktop installation stops before packages +that depend on it. + +### User tools + +- Node 24.20.0 is installed into the nvm version layout from the reviewed per-arch + archive and selected as the default. `nvm install --lts` is removed. +- pnpm comes from Fedora's signed `pnpm` package. If unavailable, Panama reports a + soft failure; it does not run npm or a remote installer as fallback. +- Bun is installed from the reviewed archive after SHA-256 verification; the remote + install script is removed. +- Claude Code uses Anthropic's signed RPM repository after exact key and repository + validation; `claude.ai/install.sh` is removed. +- Codex uses the reviewed per-arch package archive and an atomic user-owned install; + unversioned `npm install -g` is removed. +- RustDesk uses the reviewed versioned RPM URL and SHA-256 before the narrow sudo DNF + install. It never resolves `latest` at runtime. +- Claude Desktop repository setup is never downloaded or executed. If an existing + repository has the expected base URL, key, `gpgcheck=1`, and `repo_gpgcheck=1`, + Panama may install from it. Otherwise it prints one optional manual step and + continues successfully. +- Vicinae extensions use `npm ci`; lock mismatch fails the extension build without + modifying the tracked lockfile. + +Changing installer code, provenance data, keys, or package lists invalidates the +installer-stage hash so the next upgrade re-runs verification. + +### Hermetic contract + +`tests/setup/package-provenance-contract` runs real `gpg`/`sha256sum` against a test +key and signed tiny fixture manifest. It stubs curl, sudo, DNF, rpmkeys, Git, and +Flatpak. It proves: + +- correct signature/digest success and verify-before-install ordering; +- wrong fingerprint, wrong signer, bad signature, absent checksum, truncation, + tampering, unsupported architecture, and interrupted download refusal; +- known-good target preservation and temporary cleanup; +- exact RPM Fusion, Terra, COPR, Flathub, Claude Code, and Claude Desktop policies; +- no `curl | shell`, unpinned npm global install, moving `latest` lookup, or + unverified root package input remains in the base installer; +- both x86_64 and aarch64 select only their reviewed URL and digest. + +The existing `boot`, README, package-list, desktop-first, apps, launcher-search, and +update-command contracts are updated where their old expectations contradict the new +trust boundary. + +## Server firewall transaction + +### Files and policy + +Create `server/scripts/update-firewall` as a standard-library Python executable and +invoke it from the real `setup/scripts/setup-server` stage. It is server-only and does +not reuse the desktop `panama-firewall` helper. + +The repository carries LF-terminated canonical CIDR files: + +- `server/firewall/cloudflare-v4.cidrs` +- `server/firewall/cloudflare-v6.cidrs` + +They begin with the current official ranges published at +`https://www.cloudflare.com/ips-v4` and `https://www.cloudflare.com/ips-v6`. + +Each refresh uses a 10-second connection timeout and 30-second total timeout, and +downloads at most 64 KiB per response without sudo. It requires: + +- valid UTF-8/ASCII with one value per line; +- no blanks, whitespace, comments, or trailing fields; +- `ipaddress.ip_network(value, strict=True)` success; +- matching file family, canonical string form, no `/0`, no duplicate, and nonempty + IPv4 and IPv6 sets. + +The pair is indivisible. One invalid response changes nothing. With an existing valid +Panama policy, refresh failure warns and preserves it. On first setup, the validated +committed pair is the fallback. + +### Desired state + +Content-addressed ipsets are named: + +```text +panama-cf4- +panama-cf6- +``` + +The selected public zone receives exactly four Panama-owned rich rules: IPv4 and IPv6 +sources for each of `80/tcp` and `443/tcp`. The selected WireGuard zone receives only +`81/tcp`. Panama adds no service-specific port. + +Zone selection is stored without shell evaluation in +`${XDG_CONFIG_HOME:-$HOME/.config}/panama/server-firewall.conf`: + +```text +public_zone=public +wireguard_zone=wireguard +``` + +On first run, the public zone is firewalld's default zone. The WireGuard zone is the +unique active zone whose interfaces are all named `wg*`. Zero matches leaves port 81 +closed and omits the setting. More than one match, identical public/WireGuard zones, +or a candidate containing a non-WireGuard interface fails before mutation. Explicit +stored zones must still exist and satisfy those invariants. + +Before mutation, Panama refuses unrelated services, port ranges, ACCEPT zone targets, +or rich rules that broadly admit 80/443. It prints exact inspection commands. It +removes the exact direct 80/443/81 legacy rules only during the documented migration. + +### Recoverable transaction + +The updater acquires a nonblocking user-state lock and writes a checked journal to +`${XDG_STATE_HOME:-$HOME/.local/state}/panama/firewall/pending.json` containing only +Panama-owned rules/ipsets, the legacy direct ports, the WireGuard 81 rule, and selected +zones. + +It then: + +1. creates and fills the new permanent generation ipsets; +2. adds the four new public rules; +3. adds WireGuard 81 when a valid zone exists; +4. removes old Panama rules, exact legacy ports, and unreferenced Panama ipsets; +5. runs `firewall-cmd --check-config`; +6. performs one reload; +7. reads permanent and runtime state back; +8. writes selected-zone configuration and removes the journal only after equivalence. + +Old live rules remain active until the single reload. A partial permanent transaction +therefore does not create an outage. Any error restores the snapshot and reloads. +Rollback success still returns nonzero. Rollback failure retains the journal and +prints its path plus the exact retry command. A later run restores a pending journal +before considering new inputs. + +`setup-server` attempts `sudo systemctl enable --now firewalld`, then requires both +`is-enabled` and `is-active`. Missing commands or failed postconditions stop before +the updater. The updater itself is never run through sudo; only its narrow systemctl +and firewall-cmd mutations use sudo. + +### Stateful contract + +`tests/setup/server-firewall-contract` invokes the real `setup-server`. Its stateful +stubs keep separate permanent/runtime JSON and exact argv logs. `firewall-cmd` supports +only the queried zone, service, port, rich-rule, ipset, check-config, and reload +operations. Reload copies permanent to runtime. Exact one-shot failure selectors leave +state unchanged. + +Required cases are: + +- missing/inactive firewalld that cannot become enabled and active; +- first setup with legacy rules, exact desired ipsets/rules, WireGuard-only 81, + unrelated SSH/manual rules preserved, and one reload; +- every malformed CIDR class and indivisible-pair behavior; +- no WireGuard zone, ambiguous zones, identical zones, and mixed interfaces; +- unrelated broad exposure conflict refusal; +- reload failure with successful rollback and rollback-reload failure with journal; +- identical second run with no mutation/reload and valid two-family refresh; +- interrupted journal recovery before new evaluation. + +## Scope ledger amendments + +Reconnaissance added findings that the parent ledger did not name. Ownership is: + +| Finding | Package | +|---|---| +| Mutable root bootstrap and mutable initial checkout | 2 | +| Unverified RPM Fusion/COPR/Flathub trust roots | 2 | +| Moving Node/pnpm/Bun/Claude/Codex/RustDesk inputs | 2 | +| Mutable npm install during Vicinae extension build | 2 | +| Unauthenticated fetched revision used by `panama update` | 3 | +| Mutable source-app build inputs | 5 | +| Mutable Neovim bootstrap/plugin graph | 5 | + +Package 3 must define an authenticated approved revision or signed release manifest +before fetched Panama code reaches `install --upgrade`. Package 5 must pin or refuse +the ChatGPT source application and Neovim bootstrap/plugin graph. Package 3 retains +container image trust, and Package 4 retains the Whisper image/model pins already +assigned by the parent design. + +## Documentation + +Package 2 updates: + +- README bootstrap commands and root-server narrative; +- README/server README firewall policy and port exposure; +- installer comments that currently defend moving or unsigned inputs; +- package/provenance documentation and key-rotation procedure; +- Panama development/operator skills when command behavior changes. + +Documentation never claims a real host was cut over or a live reload succeeded. + +## Verification + +Each implementation plan uses red-green cycles through public interfaces. The package +gate includes: + +```bash +bash -n boot install setup/scripts/install-packages setup/scripts/setup-server \ + setup/scripts/link-vicinae-scripts setup/lib/artifact-provenance \ + tests/setup/root-server-bootstrap-contract \ + tests/setup/package-provenance-contract \ + tests/setup/server-firewall-contract + +python3 -m py_compile server/scripts/update-firewall + +tests/setup/root-server-bootstrap-contract +tests/setup/package-provenance-contract +tests/setup/server-firewall-contract +tests/setup/boot-contract +tests/setup/role-contract +tests/setup/readme-contract +tests/setup/package-lists-contract +tests/setup/desktop-first-contract +tests/setup/launcher-search-contract +tests/setup/update-command-contract + +./bin/panama test --safe +git diff --check +``` + +If available, ShellCheck covers every touched shell file. The signed Terra command +also receives one disposable Fedora 44 container smoke test. The container has no host +mounts, host package database, system bus, SSH daemon, firewall access, or production +credentials. + +## Completion criteria + +Package 2 is complete when: + +- no unverified mutable network response is executed by root or while relying on the + installer's sudo keepalive; +- the initial Panama revision and boot digest are checked before handoff; +- every automatic third-party executable input is signature-verified or pinned by a + reviewed immutable SHA-256; +- untrusted Claude Desktop setup is optional and never automatic; +- SSH hardening cannot remove the available login path and rolls back every tested + validation/reload failure; +- server firewall setup establishes the documented Cloudflare/WireGuard policy or + returns nonzero without losing the last known good policy; +- the full hermetic gate passes without live capability grants; +- independent package review has no unresolved Critical or Important finding.