Files
Panama/docs/superpowers/specs/2026-08-27-secure-bootstrap-privileged-installation-design.md
T

540 lines
26 KiB
Markdown

# 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-<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`:
```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.