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

26 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:

  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:

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:

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:

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:

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