# 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. Every nonblank, non-comment key line must parse with OpenSSH tooling. Panama may create and normalize files it copied from root, but final destination creation and writing run as the target UID. It does not take ownership of an unsafe pre-existing target path or assume that the user's primary group has the same name as the user. - 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, and OpenSSH parses every such line as a public key; - an 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. The target UID creates or normalizes `.ssh` at `0700` and writes `authorized_keys` at `0600` through an already-open root-key input. Panama then revalidates exact UID ownership, modes, and OpenSSH key parsing. It does not chown a target-controlled path or assume a same-named primary group. If the key preconditions fail, no SSH unit is installed, or the Panama drop-in path already names a symlink or non-regular object, Panama prints why hardening is unavailable, keeps root/password authentication unchanged, and continues the clone/install handoff. Declining the prompt has the same unchanged-state outcome. ### Transaction The desired drop-in is exactly: ```text PermitRootLogin no PasswordAuthentication no KbdInteractiveAuthentication 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 refuses a pre-existing Panama path unless it is a non-symlink regular file. It preserves an existing `00-panama.conf`, including its ownership, mode, timestamps, ACLs, and extended attributes, in the same directory. State-aware EXIT/INT/TERM cleanup is armed before the first candidate or backup artifact, and the candidate is atomically renamed over the final path. It then runs `sshd -t` against the complete active configuration. Before reload, `sshd -T -C` must report `permitrootlogin no`, `passwordauthentication no`, and `kbdinteractiveauthentication no` for the root context. The target-user context must report both authentication directives as `no`. This fails closed when an earlier main-config directive wins despite the precedence-safe filename. Panama reloads only the detected unit after every check passes. 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. When no prior file existed, recovery instead instructs the operator to remove `00-panama.conf`, run `sshd -t`, and reload the detected unit. 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, malformed, mixed valid/malformed, symlinked, wrong-owner, and wrong-mode target keys; - safe root-key copy and safe existing target key; - target-UID copy normalization with a primary group whose name differs from the user; - declined hardening and no installed SSH unit; - pre-existing symlink, directory, and FIFO Panama drop-ins; - successful initial install and replacement of an existing drop-in; - invalid syntax and conflicting effective-policy rollback; - failed reload rollback, including restored validation and reload; - rollback failure retaining its recovery artifact or printing no-prior-file removal; - INT/TERM during candidate and backup preparation as well as after activation; - actual-root rejection of `PANAMA_BOOT_FIXTURE_ROOT` in a user namespace; - 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 MAX_BYTES 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` | | Claude Desktop Extra | `825A7D15D78BABE45646D5DF382409F597908867` | 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`; - Claude Desktop Extra key: `https://patrickjaja.github.io/claude-desktop-extra/gpg-key.asc`; - 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`, and the pinned project key. The publisher does not provide `repomd.xml.asc`, so this repository has an explicit `repo_gpgcheck=0` exception; package signatures remain mandatory. 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, vendored-key fingerprint, `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.