From bfb37afd69c56cc3e061fde83bae258c133224cf Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Thu, 27 Aug 2026 03:11:00 -0400 Subject: [PATCH] Plan secure bootstrap and privileged installation --- .../2026-08-27-server-firewall-transaction.md | 406 ++++++++++++ .../2026-08-27-ssh-hardening-transaction.md | 319 ++++++++++ .../2026-08-27-trusted-installation-inputs.md | 586 ++++++++++++++++++ ...ootstrap-privileged-installation-design.md | 17 +- 4 files changed, 1322 insertions(+), 6 deletions(-) create mode 100644 docs/superpowers/plans/2026-08-27-server-firewall-transaction.md create mode 100644 docs/superpowers/plans/2026-08-27-ssh-hardening-transaction.md create mode 100644 docs/superpowers/plans/2026-08-27-trusted-installation-inputs.md diff --git a/docs/superpowers/plans/2026-08-27-server-firewall-transaction.md b/docs/superpowers/plans/2026-08-27-server-firewall-transaction.md new file mode 100644 index 0000000..1fe51c2 --- /dev/null +++ b/docs/superpowers/plans/2026-08-27-server-firewall-transaction.md @@ -0,0 +1,406 @@ +# Server firewall transaction implementation plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make `setup-server` establish Cloudflare-scoped public web access and WireGuard-only admin access as one recoverable firewalld transaction. + +**Architecture:** Put validation, desired-state calculation, journaling, and rollback in one standard-library Python command. Keep `setup-server` as the public orchestrator that verifies firewalld and invokes the updater; test both through stateful curl/systemd/firewalld command adapters with separate permanent/runtime state. Production endpoint URLs stay hardcoded, so no environment variable can redirect policy input. + +**Tech Stack:** Python 3 standard library (`argparse`, `dataclasses`, `fcntl`, `hashlib`, `ipaddress`, `json`, `os`, `pathlib`, `signal`, `subprocess`), Bash 5, curl/firewalld/systemd command adapters, Panama contract runner. + +**Spec:** `docs/superpowers/specs/2026-08-27-secure-bootstrap-privileged-installation-design.md` + +## Global constraints + +- Do not call the host's `firewall-cmd`, mutate firewalld/systemd, contact Cloudflare from a contract, or apply a real server policy. +- `setup-server` must fail unless firewalld is installed, enabled, and active after one `enable --now` attempt. +- Validate both Cloudflare families completely before any privileged mutation. One bad family changes nothing. +- Public 80/443 must use dedicated Panama ipsets and exactly four rich rules. Port 81 belongs only to a unique WireGuard-only zone; no WireGuard zone leaves it closed. +- Never remove unrelated user-managed rules or service-specific ports. Only exact old direct 80/443/81 rules are eligible for the documented legacy migration. +- Keep old runtime rules until one successful reload. Roll back every failed transaction and retain the journal when rollback cannot be verified. +- Run the repository updater as the normal user. Only narrow `systemctl`/`firewall-cmd` mutations use sudo. +- No privileged timer or background service is added. +- The new contract is hermetic and runs under `panama test --safe`. +- Preserve `/home/gib/.local/share/Panama/config/bash/.bashrc` outside this worktree. + +## File map + +- `server/scripts/update-firewall`: parser, range validation, zone/policy inspection, desired state, journal, mutation, reload/readback, rollback. +- `server/firewall/cloudflare-v4.cidrs`: committed current canonical IPv4 ranges. +- `server/firewall/cloudflare-v6.cidrs`: committed current canonical IPv6 ranges. +- `tests/setup/server-firewall-contract`: stateful public `setup-server` fixture. +- `tests/setup/fixtures/firewall/`: valid/invalid/refreshed CIDR response data. +- `setup/scripts/setup-server`: required firewalld postconditions and updater call. +- `setup/packages/server-packages`: document firewalld as a required package, not a best-effort extra. +- `tests/contracts.manifest`: one new hermetic contract. +- `README.md`, `server/README.md`: exact exposure, refresh, and rollback policy. + +--- + +### Task 1: Require firewalld and establish the first desired policy + +**Files:** +- Create: `server/scripts/update-firewall` +- Create: `server/firewall/cloudflare-v4.cidrs` +- Create: `server/firewall/cloudflare-v6.cidrs` +- Create: `tests/setup/server-firewall-contract` +- Create: `tests/setup/fixtures/firewall/valid-v4.cidrs` +- Create: `tests/setup/fixtures/firewall/valid-v6.cidrs` +- Modify: `setup/scripts/setup-server:40-60` +- Modify: `setup/packages/server-packages` +- Modify: `tests/contracts.manifest` + +**Interfaces:** +- Consumes: `server/scripts/update-firewall apply`, `server/scripts/update-firewall --validate-only DIRECTORY`, `XDG_CONFIG_HOME`, `XDG_STATE_HOME`, PATH adapters, and committed bootstrap CIDRs. +- Produces: `main(argv) -> int`, `parse_cidrs(text, family) -> tuple[str, ...]`, `generation_name(family, cidrs) -> str`, `run_firewall(args, permanent=True)`, validation-only mode, and first-run policy application. + +- [ ] **Step 1: Create current canonical range fixtures** + +Use the official 2026-08-27 Cloudflare list as both committed bootstrap and initial valid fixture. + +`cloudflare-v4.cidrs` must contain exactly: + +```text +103.21.244.0/22 +103.22.200.0/22 +103.31.4.0/22 +104.16.0.0/13 +104.24.0.0/14 +108.162.192.0/18 +131.0.72.0/22 +141.101.64.0/18 +162.158.0.0/15 +172.64.0.0/13 +173.245.48.0/20 +188.114.96.0/20 +190.93.240.0/20 +197.234.240.0/22 +198.41.128.0/17 +``` + +`cloudflare-v6.cidrs` must contain exactly: + +```text +2400:cb00::/32 +2405:8100::/32 +2405:b500::/32 +2606:4700::/32 +2803:f800::/32 +2a06:98c0::/29 +2c0f:f248::/32 +``` + +The production refresh endpoints are exactly `https://www.cloudflare.com/ips-v4` and `https://www.cloudflare.com/ips-v6`. The contract's PATH `curl` adapter returns fixture bytes for only those exact URLs; no environment variable or config can replace them, and the contract never contacts either endpoint. + +- [ ] **Step 2: Build the stateful command model and red first-run cases** + +Store fixture state in JSON: + +```json +{ + "systemctl": {"installed": true, "enabled": true, "active": true}, + "permanent": { + "defaultZone": "public", + "zones": { + "public": {"interfaces": ["eth0"], "target": "default", "services": ["ssh"], "ports": ["80/tcp", "443/tcp", "81/tcp", "2222/tcp"], "richRules": []}, + "wireguard": {"interfaces": ["wg0"], "target": "default", "services": [], "ports": [], "richRules": []} + }, + "ipsets": {} + }, + "runtime": {} +} +``` + +The Python `firewall-cmd` stub must support only the exact query/mutation argv used by the updater, append JSON argv arrays to `calls.jsonl`, keep permanent/runtime separate, copy permanent to runtime on reload, and reject unknown argv with status `97`. The curl stub accepts only `--fail --silent --show-error --location --connect-timeout 10 --max-time 30 --max-filesize 65536 --output PATH URL`, logs argv, and copies the selected fixture response. `systemctl` supports `list-unit-files`, `is-enabled`, `is-active`, and `enable --now`. `sudo` logs and execs. Stub linger, sysctl, podman, and user-systemctl paths so the real `setup-server` finishes without host access. + +Write red cases: + +```text +firewalld missing -> nonzero; zero firewall mutations +enable/start failure -> nonzero; zero firewall mutations +postcheck inactive/disabled -> nonzero; zero firewall mutations +valid first run -> content-addressed v4/v6 ipsets, four rich rules, 81 only in wireguard, legacy direct 80/443/81 removed, ssh and 2222 preserved, one check-config and one reload +no wireguard zone -> 80/443 policy succeeds, 81 absent, exact closed-port diagnostic +``` + +- [ ] **Step 3: Run the contract and confirm current setup is red** + +```bash +bash -n setup/scripts/setup-server tests/setup/server-firewall-contract +tests/setup/server-firewall-contract +``` + +Expected: nonzero because current setup treats inactive firewalld as success and opens all three ports directly. + +- [ ] **Step 4: Implement validation, desired state, and first apply** + +Start the updater with: + +```python +def parse_cidrs(text: str, family: int) -> tuple[str, ...]: + if len(text.encode("utf-8")) > 65536: + raise PolicyError("CIDR response exceeds 65536 bytes") + lines = text.splitlines() + if not lines: + raise PolicyError("CIDR list is empty") + parsed: list[str] = [] + for raw in lines: + if not raw or raw != raw.strip() or any(ch.isspace() for ch in raw): + raise PolicyError(f"invalid CIDR line: {raw!r}") + network = ipaddress.ip_network(raw, strict=True) + if network.version != family or network.prefixlen == 0 or str(network) != raw: + raise PolicyError(f"invalid IPv{family} CIDR: {raw}") + parsed.append(raw) + if len(parsed) != len(set(parsed)): + raise PolicyError("CIDR list contains duplicates") + return tuple(parsed) + +def generation_name(family: int, cidrs: tuple[str, ...]) -> str: + digest = hashlib.sha256(("\n".join(cidrs) + "\n").encode()).hexdigest()[:12] + return f"panama-cf{family}-{digest}" +``` + +Build exactly these rich-rule strings, substituting the content-addressed generation name: + +```text +rule family="ipv4" source ipset="V4_GENERATION" port port="80" protocol="tcp" accept +rule family="ipv4" source ipset="V4_GENERATION" port port="443" protocol="tcp" accept +rule family="ipv6" source ipset="V6_GENERATION" port port="80" protocol="tcp" accept +rule family="ipv6" source ipset="V6_GENERATION" port port="443" protocol="tcp" accept +``` + +Implement `main(argv)` with only two accepted forms: `apply` and `--validate-only DIRECTORY`. Validation-only mode reads `DIRECTORY/cloudflare-v4.cidrs` and `DIRECTORY/cloudflare-v6.cidrs`, runs the same strict parser used by apply, prints the two generation names, and exits without consulting systemd, the network, config/state directories, sudo, or firewalld. Unknown or combined arguments exit nonzero with usage. + +Query default/public and WireGuard zones before mutation. Apply new permanent ipsets/rules, exact legacy removal, `--check-config`, one `--reload`, and readback. The first task may use an in-memory snapshot; Task 3 adds durable recovery. + +Replace the old port loop in `setup-server` with exact firewalld installation/state checks: `command -v firewall-cmd`, `systemctl list-unit-files firewalld.service`, one `sudo systemctl enable --now firewalld.service`, then successful `systemctl is-enabled firewalld.service` and `systemctl is-active firewalld.service`. Only after those postconditions invoke: + +```bash +"$PANAMA_PATH/server/scripts/update-firewall" apply +``` + +- [ ] **Step 5: Add the manifest entry and verify first-run behavior** + +```text +# Server firewall policy runs against stateful systemctl/firewall-cmd adapters. +hermetic tests/setup/server-firewall-contract +``` + +Run: + +```bash +bash -n setup/scripts/setup-server tests/setup/server-firewall-contract +python3 -m py_compile server/scripts/update-firewall +server/scripts/update-firewall --validate-only server/firewall +tests/setup/server-firewall-contract +tests/setup/contract-manifest-contract +./bin/panama test --safe server-firewall +git diff --check +``` + +- [ ] **Step 6: Commit the required first policy** + +```bash +git add server/scripts/update-firewall server/firewall tests/setup/fixtures/firewall \ + tests/setup/server-firewall-contract setup/scripts/setup-server \ + setup/packages/server-packages tests/contracts.manifest +git commit -m "Fix: Establish scoped server firewall policy" +``` + +--- + +### Task 2: Reject bad ranges, unsafe zones, and foreign exposure + +**Files:** +- Modify: `server/scripts/update-firewall` +- Modify: `tests/setup/server-firewall-contract` +- Create: `tests/setup/fixtures/firewall/refreshed-v4.cidrs` +- Create: `tests/setup/fixtures/firewall/refreshed-v6.cidrs` +- Create: `tests/setup/fixtures/firewall/invalid-*.cidrs` + +**Interfaces:** +- Consumes: `parse_cidrs`, content generations, first-run apply, the exact PATH curl adapter, and stored zone config. +- Produces: `load_zone_config`, `select_zones`, `find_exposure_conflicts`, refresh fallback, and idempotent convergence. + +- [ ] **Step 1: Add exhaustive red validation and selection tables** + +Fixture-invalid classes are exact: + +```text +empty +blank-line +leading-space +trailing-token +comment +host-bits-set +wrong-family +slash-zero +duplicate +non-UTF8 +oversized-65537-bytes +valid-v4-plus-invalid-v6 +invalid-v4-plus-valid-v6 +``` + +For each, assert byte-for-byte permanent/runtime/config/journal equality and zero mutation argv. Add zone cases for multiple `wg*` zones, public equal to WireGuard, stored missing zone, and WireGuard zone containing `wg0` plus `eth1`; all fail before mutation. Zero WireGuard candidates remains the safe success from Task 1. + +Add conflicts: public `http`/`https` service, port range covering 80/443, ACCEPT zone target, and unrelated rich rule accepting 80/443. Assert a diagnostic with exact `firewall-cmd --zone=ZONE --list-all` inspection command and no deletion. + +- [ ] **Step 2: Add red idempotence and two-family refresh cases** + +Run the same valid policy twice. The second run must have no mutating `firewall-cmd`, check-config, or reload calls. Then provide valid refreshed v4/v6 lists and assert both generations change in one transaction; one changed plus one invalid changes neither. + +Simulate curl network failure. With a valid installed policy, assert warning plus no changes. With no policy, assert the committed bootstrap pair is used. + +- [ ] **Step 3: Run the contract and confirm missing validation behavior** + +```bash +tests/setup/server-firewall-contract +``` + +Expected: nonzero on malformed, conflict, refresh, and idempotence cases not yet implemented. + +- [ ] **Step 4: Implement strict refresh, zone config, conflict detection, and no-op convergence** + +Fetch both hardcoded official endpoints without sudo using curl argv `--fail --silent --show-error --location --connect-timeout 10 --max-time 30 --max-filesize 65536 --output PART URL`. Use a private destination, verify its actual size is at most 65,536 bytes, and decode with strict ASCII before calling `parse_cidrs`. Treat the pair as one candidate and remove partial files on every exit/signal. Parse config with explicit `key=value` names and no shell execution. + +Before mutation, compute normalized current and desired Panama-owned state. If identical, print `Server firewall policy already current` and return without check-config/reload. Detect foreign broad exposure but allow unrelated ports/services such as SSH and 2222. Remove only exact direct legacy ports. + +Write selected-zone config to a private `.part` and rename it only after successful policy readback. Omit `wireguard_zone` when no candidate exists. + +- [ ] **Step 5: Run focused checks and commit** + +```bash +python3 -m py_compile server/scripts/update-firewall +tests/setup/server-firewall-contract +./bin/panama test --safe server-firewall +git diff --check +``` + +```bash +git add server/scripts/update-firewall tests/setup/server-firewall-contract \ + tests/setup/fixtures/firewall +git commit -m "Fix: Validate firewall ranges and ownership" +``` + +--- + +### Task 3: Journal and roll back interrupted firewall transactions + +**Files:** +- Modify: `server/scripts/update-firewall` +- Modify: `tests/setup/server-firewall-contract` + +**Interfaces:** +- Consumes: normalized current/desired state and mutation adapter from Tasks 1-2. +- Produces: `PolicySnapshot`, `write_pending`, `restore_snapshot`, `verify_snapshot`, signal-safe rollback, and pending-journal recovery. + +- [ ] **Step 1: Add red failure injection and journal cases** + +The firewall stub accepts `FAIL_ONCE_JSON`, an exact normalized argv array. Inject one failure at each mutation boundary: create ipset, add entry, add rich rule, add 81, remove legacy port, remove old rule, remove old ipset, check-config, and reload. Separately inject permanent/runtime readback mismatch, selected-zone config write/rename failure, and pending-journal deletion failure after reload; each is still a failed transaction and must restore the snapshot. + +For every case assert updater nonzero, permanent/runtime restored byte-for-byte, rollback reload performed when needed, and journal removed only after verified restoration. Add rollback-reload failure: journal remains with mode `0600`, diagnostic names its absolute path and exact retry command. + +Seed a valid `pending.json` before invocation and assert restoration happens before curl or new-policy evaluation. Seed malformed, symlinked, foreign-owned, and mode-wrong journals; assert fail closed without mutation. + +Start an apply process, block one mutation, send TERM to the updater's exact PID, release the stub, and require status `143`, restored state, and no orphan process. + +- [ ] **Step 2: Run the contract and confirm current failure paths are red** + +```bash +tests/setup/server-firewall-contract +``` + +Expected: nonzero because the updater has no durable journal or exact-PID signal rollback. + +- [ ] **Step 3: Implement checked journal and rollback ownership** + +Use a frozen dataclass with JSON round-trip methods: + +```python +@dataclasses.dataclass(frozen=True) +class PolicySnapshot: + public_zone: str + wireguard_zone: str | None + panama_ipsets: dict[str, tuple[str, ...]] + panama_rich_rules: tuple[str, ...] + legacy_ports: tuple[str, ...] + wireguard_has_81: bool +``` + +Acquire `${state}/firewall/lock` with `fcntl.flock(LOCK_EX | LOCK_NB)`. Reject symlinked state components. Write `pending.json.part` with mode `0600`, fsync file and directory, then rename before the first mutation. + +Start each mutating sudo/firewalld subprocess in its own process group and track its leader PID. INT/TERM handlers signal that exact group, wait for it, call `restore_snapshot`, verify permanent and runtime equality after rollback reload, then exit 130/143. The contract asserts the recorded child is gone before it returns. Normal completion clears handlers before deleting the journal. + +At startup, a valid pending journal is restored and verified before network/config evaluation. Invalid journal ownership/mode/shape fails closed and prints inspection instructions. + +- [ ] **Step 4: Verify all injected failures and signals** + +```bash +python3 -m py_compile server/scripts/update-firewall +tests/setup/server-firewall-contract +./bin/panama test --safe server-firewall +git diff --check +``` + +- [ ] **Step 5: Commit recovery behavior** + +```bash +git add server/scripts/update-firewall tests/setup/server-firewall-contract +git commit -m "Fix: Roll back firewall transactions" +``` + +--- + +### Task 4: Document policy and run the complete firewall gate + +**Files:** +- Modify: `README.md:56-64` +- Modify: `server/README.md:1-90` +- Modify: `.claude/skills/panama/SKILL.md` +- Modify: `skills/panama-sudo/SKILL.md` if it documents server setup +- Modify: `tests/setup/readme-contract` +- Modify: `tests/server/containers-shape-contract` only if it asserts the old global 81 policy + +**Interfaces:** +- Consumes: final updater behavior and its exact operator diagnostics. +- Produces: accurate exposure/refresh/rollback documentation and Package 2 firewall verification evidence. + +- [ ] **Step 1: Write documentation assertions before prose** + +Require README/server README to state: firewalld required, Cloudflare-only 80/443, dedicated IPv4/IPv6 ipsets, WireGuard-only 81, no automatic service ports, invalid refresh preserves last known good, one recoverable transaction, and no privileged timer. Reject the old statement that setup simply opens 80/443/81. + +- [ ] **Step 2: Confirm old documentation fails** + +```bash +tests/setup/readme-contract +``` + +- [ ] **Step 3: Update docs without live-cutover claims** + +Explain that `setup-server` and server upgrades refresh the policy; first setup may use committed official ranges; missing WireGuard leaves 81 closed; conflicts require manual inspection; fixture tests model rollback but do not apply host rules. + +- [ ] **Step 4: Run the firewall plan gate** + +```bash +bash -n setup/scripts/setup-server tests/setup/server-firewall-contract +python3 -m py_compile server/scripts/update-firewall +server/scripts/update-firewall --validate-only server/firewall +tests/setup/server-firewall-contract +tests/setup/role-contract +tests/setup/readme-contract +tests/server/containers-shape-contract +./bin/panama test --safe +git diff --check +``` + +Expected: 134 hermetic contracts pass after the SSH, provenance, and firewall contracts exist; non-hermetic skip counts remain unchanged. + +- [ ] **Step 5: Commit documentation and final fixture adjustments** + +```bash +git add README.md server/README.md .claude/skills/panama/SKILL.md \ + skills/panama-sudo/SKILL.md tests/setup/readme-contract \ + tests/server/containers-shape-contract +git commit -m "Docs: Explain the server firewall transaction" +``` diff --git a/docs/superpowers/plans/2026-08-27-ssh-hardening-transaction.md b/docs/superpowers/plans/2026-08-27-ssh-hardening-transaction.md new file mode 100644 index 0000000..e590491 --- /dev/null +++ b/docs/superpowers/plans/2026-08-27-ssh-hardening-transaction.md @@ -0,0 +1,319 @@ +# SSH hardening transaction implementation plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make `boot --server` refuse unsafe SSH hardening and atomically roll back failed validation or reloads. + +**Architecture:** Keep production logic in the standalone `boot` file because it runs before the repository exists. Exercise the public `boot --server` command through a PTY, a temporary filesystem root, and PATH command adapters; never call private functions directly or touch host SSH state. + +**Tech Stack:** Bash 5, Python 3 standard library for the PTY driver, OpenSSH/systemd command adapters, Panama contract runner. + +**Spec:** `docs/superpowers/specs/2026-08-27-secure-bootstrap-privileged-installation-design.md` + +## Global constraints + +- Preserve the public `boot --server` command and the non-root desktop bootstrap path. +- Do not reload a real SSH service, edit `/etc/ssh`, create a real account, or run a live privileged check. +- A missing or unsafe target key keeps root/password access unchanged and continues bootstrap. +- Candidate validation or reload failure restores the prior drop-in and stops before clone/install handoff. +- Exact safe state is target ownership plus `.ssh` mode `0700` and `authorized_keys` mode `0600`; symlinks, root UID, relative homes, blank/comment-only keys, and foreign ownership are refused. +- `PANAMA_BOOT_FIXTURE_ROOT` is accepted only by a real non-root process whose stubbed `id -u` reports root. Real root plus that variable must fail closed. +- The new public contract is hermetic and must run under `panama test --safe`. +- Preserve the user's unstaged `/home/gib/.local/share/Panama/config/bash/.bashrc` change outside this worktree. + +## File map + +- `boot`: account/key preconditions, test-only filesystem adapter, SSH unit detection, atomic drop-in transaction, rollback. +- `tests/setup/root-server-bootstrap-contract`: PTY and stateful command/filesystem fixture for the real public command. +- `tests/contracts.manifest`: one `hermetic` entry for the new executable contract. +- `tests/setup/boot-contract`: retain the non-root clone/handoff behavior; adjust only if the later verified-bootstrap plan changes it. +- `tests/setup/role-contract`: retain role/stage assertions. +- `README.md`: describe key-gated transactional hardening without claiming a live reload was tested. + +--- + +### Task 1: Gate hardening on a verified login path + +**Files:** +- Create: `tests/setup/root-server-bootstrap-contract` +- Modify: `boot:52-115` +- Modify: `tests/contracts.manifest` +- Test: `tests/setup/root-server-bootstrap-contract` + +**Interfaces:** +- Consumes: public `boot --server`, `PANAMA_BOOT_FIXTURE_ROOT`, PTY answers `gib`, password already set, and `Y` for hardening. +- Produces: `system_path ABSOLUTE_PATH`, `safe_authorized_keys USER HOME`, and `harden_server_ssh USER HOME`; later tasks extend the last function with the transaction. + +- [ ] **Step 1: Build the public fixture and write the failing precondition table** + +Create a checked temporary tree with `root/etc/ssh/sshd_config.d`, `root/root/.ssh`, `root/home/gib/.ssh`, `bin`, `calls`, and a stub cloned installer. Use Python's `pty.openpty()` to start: + +```python +env = { + **os.environ, + "PATH": f"{stub_dir}:/usr/bin:/bin", + "PANAMA_BOOT_FIXTURE_ROOT": fixture_root, + "PANAMA_PATH": f"{fixture_root}/home/gib/.local/share/Panama", + "HOME": f"{fixture_root}/root", +} +process = subprocess.Popen( + ["bash", boot, "--server"], + stdin=slave, + stdout=slave, + stderr=slave, + env=env, + start_new_session=True, +) +os.write(master, b"gib\nY\n") +``` + +PATH stubs must log one shell-escaped argv vector per line. `id -u` with no username reports `0`; `id -u root` reports `0`; `id -u gib` reports `1000`; `id -nG gib` prints `gib wheel`; `passwd -S gib` prints `gib PS`; `getent passwd gib` prints the logical absolute home `/home/gib`; `runuser` materializes the clone/install handoff without changing users. `boot` resolves that logical home beneath the fixture root through `system_path`. Stub `stat` for fixture ownership metadata plus `dnf`, `git`, `sshd`, and `systemctl`; any unexpected command exits `97`. + +Run one table row per unsafe state: + +```text +missing +empty +comment-only +ssh-directory-symlink +authorized-keys-symlink +directory-wrong-mode +file-wrong-mode +directory-wrong-owner +file-wrong-owner +root-target-account +relative-home +``` + +For every row assert status `0`, a diagnostic containing `SSH hardening unavailable`, no `sshd -t`, no `systemctl reload`, no drop-in change, and a recorded install handoff. Add safe existing-key and safe root-key-copy rows that currently reach the unsafe direct-write path and therefore fail the new expected command ordering. + +- [ ] **Step 2: Run the contract and confirm the red behavior** + +Run: + +```bash +bash -n tests/setup/root-server-bootstrap-contract +tests/setup/root-server-bootstrap-contract +``` + +Expected: nonzero findings showing current `boot` writes/reloads without the required key checks and has no fixture-root support. + +- [ ] **Step 3: Add the guarded filesystem adapter and key checks** + +Add these shapes near the root branch, using `stat -Lc` only after rejecting symlinks: + +```bash +BOOT_ROOT="${PANAMA_BOOT_FIXTURE_ROOT:-}" +if [[ -n "$BOOT_ROOT" && "$EUID" -eq 0 ]]; then + echo "boot: PANAMA_BOOT_FIXTURE_ROOT is test-only" >&2 + exit 1 +fi + +system_path() { + local path="$1" + [[ "$path" == /* ]] || return 2 + printf '%s%s\n' "$BOOT_ROOT" "$path" +} + +safe_authorized_keys() { + local username="$1" user_home="$2" uid ssh_dir keys + uid="$(id -u "$username")" || return 1 + [[ "$uid" =~ ^[0-9]+$ && "$uid" != 0 && "$user_home" == /* ]] || return 1 + ssh_dir="$user_home/.ssh" + keys="$ssh_dir/authorized_keys" + [[ -d "$ssh_dir" && ! -L "$ssh_dir" && -f "$keys" && ! -L "$keys" ]] || return 1 + [[ "$(stat -Lc '%u:%a' "$ssh_dir")" == "$uid:700" ]] || return 1 + [[ "$(stat -Lc '%u:%a' "$keys")" == "$uid:600" ]] || return 1 + grep -qEv '^[[:space:]]*(#|$)' "$keys" +} +``` + +Resolve the target home, `/root/.ssh/authorized_keys`, and `/etc/ssh/sshd_config.d` through `system_path`. A copyable root key must be a non-symlinked regular file owned by UID 0, mode `0600`, with at least one nonblank/non-comment line. Never overwrite an existing `authorized_keys`. When it is absent, either create a missing `.ssh` or require an existing `.ssh` to already be a real directory owned by the target UID with mode `0700`; then copy only the key and apply ownership/mode to those two paths. Never use `chown -R`. If `safe_authorized_keys` still fails, print the unavailable message and skip the prompt/transaction. + +- [ ] **Step 4: Add the hermetic manifest entry and run focused checks** + +Insert the sorted manifest line with a directly preceding comment: + +```text +# Root bootstrap runs entirely against a temporary filesystem and PATH adapters. +hermetic tests/setup/root-server-bootstrap-contract +``` + +Run: + +```bash +bash -n boot tests/setup/root-server-bootstrap-contract +tests/setup/contract-manifest-contract +tests/setup/root-server-bootstrap-contract +tests/setup/boot-contract +tests/setup/role-contract +./bin/panama test --safe root-server-bootstrap +git diff --check +``` + +Expected: all pass; the public fixture proves unsafe keys do not invoke `sshd` or reload while bootstrap still hands off. + +- [ ] **Step 5: Commit the precondition gate** + +```bash +git add boot tests/setup/root-server-bootstrap-contract tests/contracts.manifest +git commit -m "Fix: Gate SSH hardening on a verified key" +``` + +--- + +### Task 2: Make the drop-in transaction recoverable + +**Files:** +- Modify: `boot:42-137` +- Modify: `tests/setup/root-server-bootstrap-contract` +- Test: `tests/setup/root-server-bootstrap-contract` + +**Interfaces:** +- Consumes: `safe_authorized_keys`, `system_path`, the fixture's `sshd`/`systemctl` state files, and a detected service name. +- Produces: `detect_ssh_unit`, `restore_ssh_dropin`, and a complete `harden_server_ssh USER HOME` transaction returning 0 only after validation and reload. + +- [ ] **Step 1: Add failing success and rollback scenarios** + +Extend the command state with `SSHD_RESULTS` and `RELOAD_RESULTS`, consumed one result per call. Add exact cases: + +```text +success-without-prior-dropin: validate=0 reload=0 +success-replaces-prior-dropin: validate=0 reload=0 +candidate-invalid: validate=1,0 reload= +candidate-reload-fails: validate=0,0 reload=1,0 +rollback-validation-fails: validate=0,1 reload=1 +rollback-reload-fails: validate=0,0 reload=1,1 +``` + +Assert the desired two-line content, validation before reload, only the detected unit, byte-for-byte restoration, restored validation/reload ordering, nonzero status and no install handoff on every transactional failure, no `*.tmp`/`*.backup` residue on success, and retained backup plus recovery commands when rollback fails. + +- [ ] **Step 2: Run the focused contract and confirm it fails on current code** + +Run: + +```bash +tests/setup/root-server-bootstrap-contract +``` + +Expected: nonzero because current code writes the final path directly, never validates, guesses units through reload failure, and reports success after failed reload. + +- [ ] **Step 3: Implement unit detection, atomic install, and rollback** + +Use explicit transaction state and traps. The core shape is: + +```bash +detect_ssh_unit() { + local unit + for unit in sshd.service ssh.service; do + systemctl cat "$unit" >/dev/null 2>&1 && { printf '%s\n' "$unit"; return 0; } + done + return 1 +} + +restore_ssh_dropin() { + if [[ -n "${ssh_backup:-}" && -e "$ssh_backup" ]]; then + local restore + restore="$(mktemp --tmpdir="$sshd_dir" .90-panama.XXXXXX.restore)" || return 1 + cp -a -- "$ssh_backup" "$restore" + mv -f -- "$restore" "$ssh_dropin" + else + rm -f -- "$ssh_dropin" + fi +} +``` + +Create the candidate with `umask 077; mktemp --tmpdir="$sshd_dir" .90-panama.XXXXXX.tmp`, write exact desired content, and preserve an existing final file in a collision-safe same-directory `mktemp` name ending `.backup`, not `.conf`. Save any prior `EXIT`, `INT`, and `TERM` traps, then arm transaction traps before `mv -f` activates the candidate. The EXIT handler restores only while `ssh_transaction_active=1`; every success or handled failure path restores the prior traps before returning. + +After activation: + +```bash +if ! sshd -t; then + restore_ssh_dropin + sshd -t || rollback_failed=1 + return 1 +fi +if ! systemctl reload "$ssh_unit"; then + restore_ssh_dropin + sshd -t || rollback_failed=1 + systemctl reload "$ssh_unit" || rollback_failed=1 + return 1 +fi +``` + +On clean success set `ssh_transaction_active=0`, clear the local traps, and remove the backup. On rollback failure keep the backup and print its absolute path plus `sshd -t` and `systemctl reload UNIT` recovery commands. Do not continue to clone/install after a hardening transaction returns nonzero. + +- [ ] **Step 4: Verify failure status, cleanup, and old public behavior** + +Run: + +```bash +bash -n boot tests/setup/root-server-bootstrap-contract +tests/setup/root-server-bootstrap-contract +tests/setup/boot-contract +tests/setup/role-contract +./bin/panama test --safe root-server-bootstrap +git diff --check +``` + +Expected: all pass; every rollback scenario preserves the previous drop-in and the safe success path validates before one reload. + +- [ ] **Step 5: Commit the transaction** + +```bash +git add boot tests/setup/root-server-bootstrap-contract +git commit -m "Fix: Roll back failed SSH hardening" +``` + +--- + +### Task 3: Synchronize operator documentation and close the SSH plan + +**Files:** +- Modify: `README.md:40-48` +- Modify: `.claude/skills/panama/SKILL.md` +- Modify: `skills/panama-desktop/SKILL.md` only if it describes root bootstrap +- Test: `tests/setup/readme-contract` + +**Interfaces:** +- Consumes: the landed `boot --server` behavior from Tasks 1-2. +- Produces: accurate user-facing preconditions, skip behavior, rollback behavior, and no claim of live-host proof. + +- [ ] **Step 1: Write the documentation assertions first** + +Extend `tests/setup/readme-contract` to require nearby root-bootstrap prose containing all of: verified target key, `sshd -t`, atomic drop-in, reload rollback, and hardening unavailable without a key. Reject wording that says Panama merely writes the file or that reload failure is ignored. + +- [ ] **Step 2: Run the README contract and confirm the old prose fails** + +Run: + +```bash +tests/setup/readme-contract +``` + +Expected: nonzero until README describes the transactional behavior. + +- [ ] **Step 3: Update the documentation without claiming a live reload** + +State plainly that Panama copies or verifies the target key, offers hardening only with exact safe ownership/modes, validates the complete config, reloads the detected unit, and restores the previous drop-in on failure. State that fixture contracts test these paths and no real daemon reload runs under `panama test --safe`. + +- [ ] **Step 4: Run the plan gate** + +```bash +bash -n boot tests/setup/root-server-bootstrap-contract +tests/setup/root-server-bootstrap-contract +tests/setup/boot-contract +tests/setup/role-contract +tests/setup/readme-contract +./bin/panama test --safe +git diff --check +``` + +Expected: 132 hermetic contracts pass after adding the new contract; non-hermetic skip counts remain unchanged. + +- [ ] **Step 5: Commit the synchronized documentation** + +```bash +git add README.md .claude/skills/panama/SKILL.md skills/panama-desktop/SKILL.md tests/setup/readme-contract +git commit -m "Docs: Explain transactional SSH hardening" +``` diff --git a/docs/superpowers/plans/2026-08-27-trusted-installation-inputs.md b/docs/superpowers/plans/2026-08-27-trusted-installation-inputs.md new file mode 100644 index 0000000..772d5e9 --- /dev/null +++ b/docs/superpowers/plans/2026-08-27-trusted-installation-inputs.md @@ -0,0 +1,586 @@ +# Trusted installation inputs implementation plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Ensure every automatic executable input is publisher-signed or pinned by a reviewed SHA-256 before Panama executes or installs it. + +**Architecture:** Add four small provenance helpers plus declarative reviewed pins. Keep vendor-specific decisions in `install-packages`, drive them through one hermetic public contract, and preserve existing installations when verification fails. Finish with a commit-pinned, digest-checked Panama bootstrap. + +**Tech Stack:** Bash 5, GnuPG, rpmkeys with a temporary database, SHA-256, DNF5/Flatpak/Git command adapters, Panama contract runner. + +**Spec:** `docs/superpowers/specs/2026-08-27-secure-bootstrap-privileged-installation-design.md` + +## Global constraints + +- Never execute fetched shell, install an unverified RPM, or accept a moving `latest` response during automatic setup. +- Download without sudo into a private checked temporary directory. Verify before any sudo, extraction, execution, or target replacement. +- Preserve a known-good installed version on every verification/download failure. +- Use exact complete fingerprints and reviewed per-architecture SHA-256 values from the approved spec. +- Use `curl --connect-timeout 10 --max-time 600`; enforce each configured maximum byte count before verification. +- Parse `setup/provenance/installers.conf` as data. Do not `source`, `eval`, or shell-expand it. +- Claude Desktop repository setup is optional and never automatic. +- No task may mutate the host package database, repository configuration, Flatpak remotes, or live installed tools. +- The Terra command gets at most one disposable Fedora 44 container smoke test with no host mounts, credentials, services, or production state. +- Package 3 owns authenticated `panama update`; Package 5 owns mutable source-app and Neovim inputs. +- Preserve `/home/gib/.local/share/Panama/config/bash/.bashrc` outside this worktree. + +## File map + +- `setup/lib/artifact-provenance`: fingerprint, digest-download, detached-signature, and temporary-RPM-keyring helpers. +- `setup/provenance/installers.conf`: strict reviewed versions, URLs, SHA-256 values, fingerprints, and maximum sizes. +- `setup/provenance/keys/*`: reviewed ASCII-armored public keys. +- `setup/provenance/README.md`: source, retrieval date, verification command, and rotation notes. +- `tests/setup/package-provenance-contract`: real cryptographic fixture plus stateful curl/sudo/DNF/Flatpak/rpm command adapters. +- `tests/setup/fixtures/provenance/*`: test-only GPG key, tiny signed manifest, good/tampered artifacts, and trusted/untrusted repo data. +- `setup/scripts/install-packages`: vendor-specific verified repository/artifact flows. +- `install`: include provenance and installer behavior in the package-stage hash. +- `setup/scripts/link-vicinae-scripts`: use `npm ci` against the tracked lock. +- `boot`, `README.md`, `tests/setup/boot-contract`, `tests/setup/readme-contract`: verified initial Panama revision and boot digest. +- `tests/contracts.manifest`: one new hermetic contract. + +--- + +### Task 1: Build and prove the provenance helpers + +**Files:** +- Create: `setup/lib/artifact-provenance` +- Create: `setup/provenance/installers.conf` +- Create: `setup/provenance/keys/terra44.asc` +- Create: `setup/provenance/keys/claude-code.asc` +- Create: `setup/provenance/keys/bun.asc` +- Create: `setup/provenance/keys/rpmfusion-free.asc` +- Create: `setup/provenance/keys/rpmfusion-nonfree.asc` +- Create: `setup/provenance/keys/hyprland-copr.asc` +- Create: `setup/provenance/keys/flathub.asc` +- Create: `setup/provenance/keys/claude-desktop.asc` +- Create: `setup/provenance/README.md` +- Create: `tests/setup/package-provenance-contract` +- Create: `tests/setup/fixtures/provenance/` +- Modify: `tests/contracts.manifest` + +**Interfaces:** +- Consumes: `curl`, `gpg`, `sha256sum`, `rpmkeys`, `stat`, and strict `NAME=value` provenance data. +- Produces: `load_installer_provenance FILE`, `key_fingerprint_matches FILE EXPECTED`, `download_sha256 URL EXPECTED MAX_BYTES DEST`, `verify_detached_signature KEY SIGNATURE CONTENT`, and `rpm_signature_matches PACKAGE KEY EXPECTED`. + +- [ ] **Step 1: Create cryptographic fixtures and write failing helper cases** + +Generate a test-only key in a temporary `GNUPGHOME`, export its public key, sign a four-line `SHASUMS256.txt`, and commit only the public key, content, detached signature, a good tiny artifact, and a one-byte-tampered artifact. The private key must never enter the repository; the contract does not need it after fixture creation. + +Write contract cases that source only `setup/lib/artifact-provenance` and assert: + +```text +known fingerprint -> 0 +wrong fingerprint -> nonzero +valid detached signature -> 0 +wrong content or signature -> nonzero +matching digest/size -> atomic destination created +wrong digest, oversized body, interrupted curl -> destination absent or original bytes preserved +valid RPM signed by fixture key -> 0 through a temporary rpmdb +unsigned/wrong-key RPM -> nonzero and host rpmdb untouched +``` + +Add provenance-parser cases for unknown key, duplicate key, missing required key, whitespace around the name, shell expansion text, and unsupported architecture. + +- [ ] **Step 2: Run the new contract and confirm the missing-helper failure** + +```bash +bash -n tests/setup/package-provenance-contract +tests/setup/package-provenance-contract +``` + +Expected: nonzero because the helper/config do not exist. + +- [ ] **Step 3: Implement exact helpers** + +Use these signatures and behaviors: + +```bash +key_fingerprint_matches() { + local file="$1" expected="$2" actual + actual="$(gpg --batch --with-colons --import-options show-only --import "$file" 2>/dev/null \ + | awk -F: '$1 == "fpr" { print $10; exit }')" + [[ "$actual" == "$expected" ]] +} + +verify_detached_signature() { + local key="$1" signature="$2" content="$3" home + home="$(mktemp -d)" || return 1 + chmod 700 "$home" + GNUPGHOME="$home" gpg --batch --quiet --import "$key" >/dev/null 2>&1 \ + && GNUPGHOME="$home" gpg --batch --verify "$signature" "$content" >/dev/null 2>&1 + local status=$? + rm -rf -- "$home" + return "$status" +} +``` + +`download_sha256` downloads to `DEST.part`, passes `--max-filesize MAX_BYTES`, verifies `stat -c %s <= MAX_BYTES`, compares a lowercase 64-hex digest, then `mv -f` atomically. Its EXIT/INT/TERM cleanup removes only the checked `.part` path. + +The installer may define one private `download_bounded URL MAX_BYTES DEST` wrapper for publisher-signed RPMs whose trust assertion is the later RPM signature rather than a reviewed digest. It uses the same curl timeouts, `.part` cleanup, post-download size check, and atomic rename as `download_sha256`; it does not execute or install the result before `rpm_signature_matches` succeeds. + +`rpm_signature_matches` creates a private temporary rpmdb, imports only `KEY`, verifies the complete expected primary fingerprint before import, and requires `rpmkeys --dbpath DB --checksig PACKAGE` success with an OpenPGP signature line. It never imports into the host keyring. + +`load_installer_provenance` reads with `IFS='=' read -r name value`, accepts only an explicit name allowlist, rejects duplicate/empty values and any line without exactly one `=`, and exports nothing. Store values in one associative array named `INSTALLER_PROVENANCE`. + +- [ ] **Step 4: Add reviewed keys and config** + +Fetch each key from the exact source URL in the spec to a temporary directory, verify its full fingerprint, and add its exact ASCII-armored content with `apply_patch`. Fill `installers.conf` with these reviewed values and conservative byte caps: + +```text +BUN_VERSION=1.4.0 +BUN_X86_64_URL=https://github.com/oven-sh/bun/releases/download/bun-v1.4.0/bun-linux-x64.zip +BUN_X86_64_SHA256=2d03fb5fb83ac8b567aca0a281b2ce1a1a19d488f56c2968d88c3f25e92fe452 +BUN_X86_64_MAX_BYTES=67108864 +BUN_AARCH64_URL=https://github.com/oven-sh/bun/releases/download/bun-v1.4.0/bun-linux-aarch64.zip +BUN_AARCH64_SHA256=4b1a332ee861983eb93bcfe6f770fff94e3e31b2c388bdaea3c8ed35e58eed0e +BUN_AARCH64_MAX_BYTES=67108864 +NODE_VERSION=24.20.0 +NODE_X86_64_URL=https://nodejs.org/dist/v24.20.0/node-v24.20.0-linux-x64.tar.xz +NODE_X86_64_SHA256=2f2c0da162318f0de47665410c7c8c2ed3d36c8f3105de4bbc61176c70a7cbf2 +NODE_X86_64_MAX_BYTES=67108864 +NODE_AARCH64_URL=https://nodejs.org/dist/v24.20.0/node-v24.20.0-linux-arm64.tar.xz +NODE_AARCH64_SHA256=5f4ddab610c1ab2016b3c227cebdbf6d9495161487e4739c7b90090595f465f7 +NODE_AARCH64_MAX_BYTES=67108864 +CODEX_VERSION=0.150.1 +CODEX_X86_64_URL=https://github.com/openai/codex/releases/download/rust-v0.150.1/codex-package-x86_64-unknown-linux-musl.tar.gz +CODEX_X86_64_SHA256=00aba704f029f6dc0d948be407a756e0c97cc840132fd691353b2c6b0a505b17 +CODEX_X86_64_MAX_BYTES=134217728 +CODEX_AARCH64_URL=https://github.com/openai/codex/releases/download/rust-v0.150.1/codex-package-aarch64-unknown-linux-musl.tar.gz +CODEX_AARCH64_SHA256=1ecac3f87823efb98153233b076ea3d6e34a7a8cebe43c5285dc5f79e1514639 +CODEX_AARCH64_MAX_BYTES=134217728 +RUSTDESK_VERSION=1.4.9 +RUSTDESK_X86_64_URL=https://github.com/rustdesk/rustdesk/releases/download/1.4.9/rustdesk-1.4.9-0.x86_64.rpm +RUSTDESK_X86_64_SHA256=eb1b053ac5b2f774f2271f7fbbfd2ea475899f7a55135c5e172bc54b9388f108 +RUSTDESK_X86_64_MAX_BYTES=134217728 +FEDORA_RELEASE=44 +RPMFUSION_FREE_RELEASE_URL=https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-44.noarch.rpm +RPMFUSION_FREE_RELEASE_MAX_BYTES=4194304 +RPMFUSION_NONFREE_RELEASE_URL=https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-44.noarch.rpm +RPMFUSION_NONFREE_RELEASE_MAX_BYTES=4194304 +TERRA_BASEURL=https://repos.fyralabs.com/terra44 +HYPRLAND_COPR_BASEURL=https://download.copr.fedorainfracloud.org/results/lionheartp/Hyprland/fedora-$releasever-$basearch/ +FLATHUB_DESCRIPTOR_URL=https://flathub.org/repo/flathub.flatpakrepo +FLATHUB_DESCRIPTOR_MAX_BYTES=1048576 +CLAUDE_CODE_BASEURL=https://downloads.claude.ai/claude-code/rpm/stable +CLAUDE_DESKTOP_BASEURL=https://patrickjaja.github.io/claude-desktop-extra/rpm/ +TERRA_FINGERPRINT=AE09157A4DE88B497EA1D5D300CDAB43DE226D6F +CLAUDE_CODE_FINGERPRINT=31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE +BUN_FINGERPRINT=F3DCC08A8572C0749B3E18888EAB4D40A7B22B59 +RPMFUSION_FREE_FINGERPRINT=E9A491A3DE247814E7E067EAE06F8ECDD651FF2E +RPMFUSION_NONFREE_FINGERPRINT=79BDB88F9BBF73910FD4095B6A2AF96194843C65 +HYPRLAND_COPR_FINGERPRINT=97E23476C89635135407C7D5E9BA41342C4B2995 +FLATHUB_FINGERPRINT=6E5C05D979C76DAF93C081354184DD4D907A7CAE +CLAUDE_DESKTOP_FINGERPRINT=825A7D15D78BABE45646D5DF382409F597908867 +``` + +Use the exact hashes and fingerprints from the spec; no value may be resolved through `latest`. The provenance README must list every source URL and the command used to verify it on 2026-08-27. + +- [ ] **Step 5: Add the manifest entry and run the focused gate** + +```text +# Provenance uses local signed fixtures and stubs every network/package operation. +hermetic tests/setup/package-provenance-contract +``` + +Run: + +```bash +bash -n setup/lib/artifact-provenance tests/setup/package-provenance-contract +tests/setup/package-provenance-contract +tests/setup/contract-manifest-contract +./bin/panama test --safe package-provenance +git diff --check +``` + +Expected: all pass and no host GPG/RPM state changes. + +- [ ] **Step 6: Commit the provenance foundation** + +```bash +git add setup/lib/artifact-provenance setup/provenance tests/setup/fixtures/provenance \ + tests/setup/package-provenance-contract tests/contracts.manifest +git commit -m "Test: Add installer provenance boundary" +``` + +--- + +### Task 2: Verify third-party repository roots + +**Files:** +- Modify: `setup/scripts/install-packages:218-276,339-370,396-456` +- Modify: `tests/setup/package-provenance-contract` +- Test: `tests/setup/desktop-first-contract` +- Test: `tests/setup/package-lists-contract` + +**Interfaces:** +- Consumes: `INSTALLER_PROVENANCE`, the four helper functions, vendored keys, and command adapters. +- Produces: `install_rpmfusion_repositories`, `install_terra_repository`, `configure_hyprland_repository`, `ensure_flathub_remote`, `install_claude_code`, and `install_claude_desktop_if_trusted`. + +- [ ] **Step 1: Add public installer cases for every repository** + +Run a fixture copy of `install-packages` with temporary HOME/state and stubbed `sudo`, `dnf`, `rpm`, `rpmkeys`, `curl`, `flatpak`, and `gpg`. Assert exact command-log order and policy: + +```text +RPM Fusion: exact Fedora 44 URL -> size cap -> RPM signature -> localpkg_gpgcheck=1 install +Terra: exact F44 key -> pkg_gpgcheck=1 -> repo_gpgcheck=1 -> local gpgkey -> terra-release +COPR: exact baseurl/local key, package gpgcheck=1, explicit metadata-signature exception; no `dnf copr enable` +Flathub: decoded embedded key fingerprint and GPG-enabled remote; mismatch preserves existing remote +Claude Code: exact Anthropic key/repo checks before DNF +Claude Desktop absent/untrusted: one manual message, no download, no DNF, overall success +Claude Desktop trusted existing repo: DNF install only +``` + +Inject wrong keys, wrong base URLs, GPG flags off, signature failure, and DNF failure. Assert nothing downstream in the dependent transaction runs after a trust-root failure. Require `rpm -E %fedora` to equal the reviewed `FEDORA_RELEASE`; any other release fails before a third-party download or repository mutation. + +- [ ] **Step 2: Run the contract and confirm current unsafe paths fail** + +```bash +tests/setup/package-provenance-contract +``` + +Expected: nonzero findings for `--nogpgcheck`, TOFU COPR, unvalidated Flathub, remote-script Claude Desktop, and unverified RPM Fusion URLs. + +- [ ] **Step 3: Implement signed repository setup** + +Source the helper and load the config from `PANAMA_PATH` at installer start. Download RPM Fusion release RPMs, verify signatures with the matching vendored key, then call: + +```bash +sudo dnf install -y --setopt=localpkg_gpgcheck=1 "$free_rpm" "$nonfree_rpm" +``` + +Replace Terra with `--repofrompath terra,https://repos.fyralabs.com/terra44` plus: + +```text +--setopt=terra.pkg_gpgcheck=1 +--setopt=terra.repo_gpgcheck=1 +--setopt=terra.gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-terra44-panama +``` + +Stage repo/key files completely before atomic sudo install. Write the COPR repo from reviewed local values rather than `dnf copr enable`, with: + +```ini +[panama-hyprland] +name=Panama reviewed Hyprland COPR +baseurl=https://download.copr.fedorainfracloud.org/results/lionheartp/Hyprland/fedora-$releasever-$basearch/ +enabled=1 +gpgcheck=1 +repo_gpgcheck=0 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-panama-hyprland +``` + +The `repo_gpgcheck=0` line is the single audited exception: the publisher returns no `repodata/repomd.xml.asc`, while its RPMs are signed by the pinned project key. The contract rejects this exception for every other repository and still requires package signatures. + +Parse the Flathub descriptor as INI data, base64-decode `GPGKey`, verify its fingerprint, and reject `NoGPGVerify=true` or equivalent disabled state. Write Claude Code's stable repository with the vendored local key, `gpgcheck=1`, and `repo_gpgcheck=1`; its publisher provides signed metadata. + +For Claude Desktop, inspect only an already configured repo. Trust it only when its base URL equals `https://patrickjaja.github.io/claude-desktop-extra/rpm/`, both GPG checks are `1`, and its `gpgkey` is an existing local file whose complete fingerprint matches the vendored `claude-desktop.asc`. Never curl or run the community setup script. The host's current remote-key configuration is therefore treated as untrusted until an operator configures a local reviewed key. Untrusted/absent configuration logs an optional manual instruction and returns success. + +- [ ] **Step 4: Verify ordering and regression contracts** + +```bash +bash -n setup/scripts/install-packages tests/setup/package-provenance-contract +tests/setup/package-provenance-contract +tests/setup/desktop-first-contract +tests/setup/package-lists-contract +./bin/panama test --safe package-provenance +git diff --check +``` + +- [ ] **Step 5: Commit repository trust roots** + +```bash +git add setup/scripts/install-packages tests/setup/package-provenance-contract +git commit -m "Fix: Verify third-party package repositories" +``` + +--- + +### Task 3: Pin language runtimes and agent tools + +**Files:** +- Modify: `setup/scripts/install-packages:100-177,334-394` +- Modify: `tests/setup/package-provenance-contract` +- Modify: `tests/quickshell/declared-dependencies-contract` + +**Interfaces:** +- Consumes: `download_sha256`, loaded reviewed pins, supported `uname -m` values `x86_64` and `aarch64`. +- Produces: `install_node`, `install_bun`, `install_claude_code`, `install_codex`, and `install_rustdesk` with verified staging and known-good preservation. + +- [ ] **Step 1: Add failing per-architecture and preservation cases** + +For each artifact, assert exact URL/digest selection for x86_64 and aarch64, unsupported-architecture refusal before curl, digest mismatch preserving a seeded old version, interrupted download cleanup, atomic replacement, and already-installed exact-version no-op. + +Require source scans and public command logs to reject: + +```text +curl ... | bash +nvm install --lts +npm install -g pnpm +npm install -g @openai/codex +releases/latest +api.github.com/.../releases/latest +``` + +RustDesk supports only the reviewed x86_64 RPM. aarch64 records a deliberate soft failure without downloading. pnpm must come from signed Fedora DNF; no network-script/npm fallback is allowed. + +- [ ] **Step 2: Run the focused contract and confirm it fails on moving inputs** + +```bash +tests/setup/package-provenance-contract +``` + +Expected: nonzero findings naming each current moving or piped installer. + +- [ ] **Step 3: Implement verified atomic installs** + +Map architecture once: + +```bash +case "$(uname -m)" in + x86_64) artifact_arch=X86_64 ;; + aarch64) artifact_arch=AARCH64 ;; + *) log "Unsupported architecture: $(uname -m)"; return 1 ;; +esac +``` + +Install Node 24.20.0 into `$NVM_DIR/versions/node/v24.20.0` from a sibling staging directory, reject archive entries outside the expected single top-level directory, require staged `bin/node --version` to print `v24.20.0`, then rename and set nvm's default alias to `24.20.0` without `nvm install`. + +For Bun, reject unexpected archive paths, stage the binary at `$HOME/.bun/versions/1.4.0/bin/bun`, require `--version` to print `1.4.0`, then atomically replace a temporary symlink at `$HOME/.bun/bin/bun`. For Codex, reject absolute/parent-traversal tar members, stage the release's `codex` binary at `$HOME/.local/lib/panama/codex/0.150.1/codex`, require `--version` to identify `0.150.1`, then atomically replace `$HOME/.local/bin/codex` through a temporary symlink. Existing version directories must match the reviewed binary/version or cause a soft failure; never delete and recreate an unverified collision. + +Install Claude Code through the signed stable repository from Task 2. Download RustDesk's versioned RPM, verify SHA-256, then pass only that local path to DNF. Install pnpm through the signed Fedora package transaction and record a soft failure if unavailable. Every archive extracts into a checked private directory first; no archive writes directly into its final prefix. + +Every helper failure appends the same component name to `softly_failed`; `report_soft_failures` keeps the package hash unstamped. + +- [ ] **Step 4: Run focused and dependency checks** + +```bash +bash -n setup/scripts/install-packages tests/setup/package-provenance-contract +tests/setup/package-provenance-contract +tests/quickshell/declared-dependencies-contract +tests/setup/desktop-first-contract +./bin/panama test --safe package-provenance +git diff --check +``` + +- [ ] **Step 5: Commit pinned user tools** + +```bash +git add setup/scripts/install-packages tests/setup/package-provenance-contract \ + tests/quickshell/declared-dependencies-contract +git commit -m "Fix: Pin runtime and agent artifacts" +``` + +--- + +### Task 4: Invalidate stale installer state and lock npm installs + +**Files:** +- Modify: `install:67-106` +- Modify: `setup/scripts/link-vicinae-scripts:79-97` +- Modify: `tests/setup/launcher-search-contract` +- Modify: `tests/setup/update-command-contract` +- Modify: `tests/setup/package-provenance-contract` + +**Interfaces:** +- Consumes: tracked package lists, installer, provenance helper/config/keys, and extension lockfile. +- Produces: `hash_packages` covering every installer trust input and Vicinae `npm ci` behavior. + +- [ ] **Step 1: Write failing hash and lockfile assertions** + +Run `hash_packages` from a disposable installer copy and assert the digest changes independently when each of these changes: + +```text +setup/packages/core-packages +setup/scripts/install-packages +setup/lib/artifact-provenance +setup/provenance/installers.conf +one setup/provenance/keys file +``` + +In the Vicinae fixture, stub npm and require argv `ci`, not `install`. Seed a lock mismatch and assert nonzero extension-build status with the lockfile byte-for-byte unchanged. + +- [ ] **Step 2: Confirm current hash and npm behavior fail** + +```bash +tests/setup/package-provenance-contract +tests/setup/launcher-search-contract +``` + +Expected: current hash ignores installer/provenance changes and extension setup invokes `npm install`. + +- [ ] **Step 3: Hash exact inputs and switch to `npm ci`** + +Replace the current `find ... -maxdepth 1` stream with a sorted NUL-safe list containing top-level package files, `setup/scripts/install-packages`, `setup/lib/artifact-provenance`, and every regular file under `setup/provenance`. Hash file paths plus contents so renames change the digest. + +Change only the extension dependency command to `npm ci`; do not add lockfile repair or update behavior. + +- [ ] **Step 4: Verify upgrade and extension behavior** + +```bash +bash -n install setup/scripts/link-vicinae-scripts +tests/setup/package-provenance-contract +tests/setup/launcher-search-contract +tests/setup/update-command-contract +./bin/panama test --safe package-provenance +git diff --check +``` + +- [ ] **Step 5: Commit state invalidation and npm locking** + +```bash +git add install setup/scripts/link-vicinae-scripts tests/setup/launcher-search-contract \ + tests/setup/update-command-contract tests/setup/package-provenance-contract +git commit -m "Fix: Re-run verified installer inputs" +``` + +--- + +### Task 5: Verify the initial Panama revision before handoff + +**Files:** +- Modify: `boot:20-166` +- Modify: `tests/setup/boot-contract` +- Modify: `README.md:1-48` +- Modify: `tests/setup/readme-contract` +- Modify: `.claude/skills/panama/SKILL.md` +- Modify: `skills/panama-desktop/SKILL.md` + +**Interfaces:** +- Consumes: `PANAMA_BOOT_REVISION` as a full lowercase 40-hex commit and `PANAMA_BOOT_SHA256` as a lowercase 64-hex digest. +- Produces: verified fresh clone at that revision, fast-forward-only clean existing checkout, and documentation pinned to the implementation commit immediately preceding its documentation commit. + +- [ ] **Step 1: Replace old permissive boot-contract expectations with red trust cases** + +The public fixture must assert: + +```text +missing/malformed revision -> no git clone/fetch and no install +fresh clone -> fetch exact revision, resolve HEAD^{commit}, equality, handoff +HEAD mismatch -> nonzero, no install +existing clean ancestor -> fast-forward to exact revision, then install +existing dirty or divergent checkout -> nonzero, no reset, no install +fetch failure -> nonzero, no install +``` + +Delete the old assertion that a failed pull proceeds with the checkout as-is. Add README assertions rejecting `bash <(curl .../main/boot)` and requiring a commit URL, 40-hex revision, 64-hex SHA-256, `sha256sum -c`, 10-second connect timeout, 30-second total timeout, and 256 KiB maximum. + +- [ ] **Step 2: Run boot and README contracts to prove they fail** + +```bash +tests/setup/boot-contract +tests/setup/readme-contract +``` + +Expected: both fail on the mutable branch bootstrap and permissive pull fallback. + +- [ ] **Step 3: Implement exact-revision clone/handoff and commit it** + +Validate inputs before Git: + +```bash +[[ "${PANAMA_BOOT_REVISION:-}" =~ ^[0-9a-f]{40}$ ]] || exit 1 +[[ "${PANAMA_BOOT_SHA256:-}" =~ ^[0-9a-f]{64}$ ]] || exit 1 +actual_boot_sha="$(sha256sum "${BASH_SOURCE[0]}" | cut -d' ' -f1)" +[[ "$actual_boot_sha" == "$PANAMA_BOOT_SHA256" ]] || exit 1 +``` + +For a fresh destination, initialize/fetch the exact commit, verify `git rev-parse HEAD^{commit}` equality, create local `main` at that commit, and set `branch.main.remote=origin` plus `branch.main.merge=refs/heads/main`. For an existing checkout, require empty `git status --porcelain`, fetch the exact commit, require `git merge-base --is-ancestor HEAD REVISION`, and fast-forward only. Never use reset or execute after mismatch/failure. + +Run focused tests, then commit only implementation and contract changes: + +```bash +bash -n boot tests/setup/boot-contract +tests/setup/boot-contract +tests/setup/root-server-bootstrap-contract +git add boot tests/setup/boot-contract tests/setup/root-server-bootstrap-contract +git commit -m "Fix: Verify the initial Panama revision" +``` + +- [ ] **Step 4: Compute the committed boot pin and write the documented command** + +Use the implementation commit just created: + +```bash +bootstrap_commit="$(git rev-parse HEAD)" +bootstrap_sha="$(git show "$bootstrap_commit:boot" | sha256sum | cut -d' ' -f1)" +``` + +Write README commands that download +`https://git.gbrown.org/gib/Panama/raw/commit/$bootstrap_commit/boot` to a checked temporary file with `curl --connect-timeout 10 --max-time 30 --max-filesize 262144`, compare `$bootstrap_sha` through `sha256sum -c`, then invoke with both environment values. Use the same verified command for desktop and `--server`; never pipe the response to Bash. + +The README contract must parse those literal values, run `git cat-file -e COMMIT^{commit}`, and require: + +```bash +test "$(git show "$commit:boot" | sha256sum | cut -d' ' -f1)" = "$documented_sha" +``` + +- [ ] **Step 5: Update operator skills and run the complete installer plan gate** + +```bash +bash -n boot install setup/scripts/install-packages setup/scripts/link-vicinae-scripts \ + setup/lib/artifact-provenance tests/setup/package-provenance-contract \ + tests/setup/boot-contract +tests/setup/package-provenance-contract +tests/setup/boot-contract +tests/setup/root-server-bootstrap-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 +``` + +Expected: 133 hermetic contracts pass after the SSH and provenance contracts exist; non-hermetic skip counts remain unchanged. + +- [ ] **Step 6: Commit the pinned documentation** + +```bash +git add README.md tests/setup/readme-contract .claude/skills/panama/SKILL.md \ + skills/panama-desktop/SKILL.md setup/provenance/README.md +git commit -m "Docs: Pin the verified Panama bootstrap" +``` + +--- + +### Task 6: Smoke-test signed Terra bootstrap in a disposable Fedora 44 container + +**Files:** +- Modify: `setup/provenance/README.md` +- Modify: `tests/setup/package-provenance-contract` only if the smoke test exposes a fixture gap + +**Interfaces:** +- Consumes: exact Terra key/repo command landed in Task 2. +- Produces: recorded disposable proof or a fail-closed Terra-unavailable implementation; never host installation. + +- [ ] **Step 1: Preflight the disposable target** + +Require rootless Podman, no bind mounts, a fresh `registry.fedoraproject.org/fedora:44` container, and no forwarded credentials or host sockets. The command may download repository metadata and the `terra-release` package only inside the disposable container. + +- [ ] **Step 2: Run the exact key and DNF verification path** + +Copy only the vendored Terra key into the container, verify its full fingerprint, then run the exact `--repofrompath` and three `terra.*` GPG settings from Task 2. Query the resulting repo file and package signature settings. Remove the container on exit. + +Expected: DNF installs `terra-release` with both package and metadata verification enabled and without `--nogpgcheck`. + +- [ ] **Step 3: Apply the fail-closed result** + +If the exact command fails, do not weaken GPG settings. Change the installer to print Terra unavailable and exit before initial/desktop/Hyprland transactions; update the fixture expectation to that branch. If it succeeds, make no production change. + +- [ ] **Step 4: Record proof and rerun the hermetic gate** + +Document the container image, date, exact command, exit status, key fingerprint, and inspected repo settings in `setup/provenance/README.md`. Do not claim host installation. + +```bash +tests/setup/package-provenance-contract +./bin/panama test --safe +git diff --check +``` + +- [ ] **Step 5: Commit the provenance proof** + +```bash +git add setup/provenance/README.md setup/scripts/install-packages \ + tests/setup/package-provenance-contract +git commit -m "Docs: Record signed Terra bootstrap proof" +``` diff --git a/docs/superpowers/specs/2026-08-27-secure-bootstrap-privileged-installation-design.md b/docs/superpowers/specs/2026-08-27-secure-bootstrap-privileged-installation-design.md index e19e353..602aaa4 100644 --- a/docs/superpowers/specs/2026-08-27-secure-bootstrap-privileged-installation-design.md +++ b/docs/superpowers/specs/2026-08-27-secure-bootstrap-privileged-installation-design.md @@ -181,7 +181,7 @@ Create `setup/lib/artifact-provenance` with only these public shell functions: ```text key_fingerprint_matches FILE EXPECTED_FINGERPRINT -download_sha256 URL EXPECTED_SHA256 DESTINATION +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 ``` @@ -211,6 +211,7 @@ As reviewed on 2026-08-27, the trust anchors are: | RPM Fusion nonfree | `79BDB88F9BBF73910FD4095B6A2AF96194843C65` | | lionheartp/Hyprland COPR | `97E23476C89635135407C7D5E9BA41342C4B2995` | | Flathub | `6E5C05D979C76DAF93C081354184DD4D907A7CAE` | +| Claude Desktop Extra | `825A7D15D78BABE45646D5DF382409F597908867` | The initial reviewed artifact pins are: @@ -245,6 +246,8 @@ The provenance README cites these publisher-controlled records: - 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`; @@ -263,8 +266,10 @@ The provenance README cites these publisher-controlled records: `terra.repo_gpgcheck=1`, and the local pinned key with the official repository. Failure stops before initial, desktop, or Hyprland package transactions. - Hyprland COPR configuration is written from reviewed local data with the exact - base URL, `gpgcheck=1`, `repo_gpgcheck=1`, and pinned project key. Panama does not - use interactive/TOFU `dnf copr enable -y`. + 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. @@ -289,9 +294,9 @@ that depend on it. - RustDesk uses the reviewed versioned RPM URL and SHA-256 before the narrow sudo DNF install. It never resolves `latest` at runtime. - Claude Desktop repository setup is never downloaded or executed. If an existing - repository has the expected base URL, key, `gpgcheck=1`, and `repo_gpgcheck=1`, - Panama may install from it. Otherwise it prints one optional manual step and - continues successfully. + 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.