24 KiB
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:
- transactional SSH hardening in
boot --server; - verified bootstrap and installer inputs;
- 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 --serverinterface, but remove the mutablebash <(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
.sshdirectory owned by the target UID with mode0700, and a nonempty regularauthorized_keysfile owned by the target UID with mode0600. 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, and81/tcprules 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:
- create a checked private temporary directory;
- download without sudo with redirects, a 10-second connection timeout, a 600-second total timeout, and the reviewed per-artifact byte limit;
- verify the expected fingerprint, signature, or reviewed SHA-256;
- stage the complete result on the destination filesystem;
- use sudo only for the narrow final package/repository operation that needs it;
- atomically replace user-owned installed artifacts;
- 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
getenthome is absolute and nonempty; - neither the home-relative
.sshpath norauthorized_keysis a symlink; .sshandauthorized_keyshave the exact ownership and modes in Decisions;authorized_keyscontains at least one nonblank, non-comment line;- exactly one installed SSH unit is detected, preferring
sshd.serviceand falling back tossh.serviceonly 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:
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:
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, andhttps://github.com/terrapkg/packages/discussions/7736; - Anthropic setup and signing key:
https://code.claude.com/docs/en/setupandhttps://downloads.claude.ai/keys/claude-code.asc; - Bun release and release-key usage:
https://github.com/oven-sh/bun/releases/tag/bun-v1.4.0andhttps://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-2020andhttps://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.0andhttps://github.com/nodejs/node/blob/main/README.md; - Codex release and signing workflow:
https://github.com/openai/codex/releases/tag/rust-v0.150.1andhttps://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 usesterra.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/TOFUdnf 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 --ltsis removed. - pnpm comes from Fedora's signed
pnpmpackage. 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.shis removed. - Codex uses the reviewed per-arch package archive and an atomic user-owned install;
unversioned
npm install -gis removed. - RustDesk uses the reviewed versioned RPM URL and SHA-256 before the narrow sudo DNF
install. It never resolves
latestat runtime. - Claude Desktop repository setup is never downloaded or executed. If an existing
repository has the expected base URL, key,
gpgcheck=1, andrepo_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, movinglatestlookup, 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.cidrsserver/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:
panama-cf4-<first 12 SHA-256 hex>
panama-cf6-<first 12 SHA-256 hex>
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:
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:
- creates and fills the new permanent generation ipsets;
- adds the four new public rules;
- adds WireGuard 81 when a valid zone exists;
- removes old Panama rules, exact legacy ports, and unreferenced Panama ipsets;
- runs
firewall-cmd --check-config; - performs one reload;
- reads permanent and runtime state back;
- 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 -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.