Plan secure bootstrap and privileged installation

This commit is contained in:
Gabriel Brown
2026-08-27 03:11:00 -04:00
parent b361db8486
commit bfb37afd69
4 changed files with 1322 additions and 6 deletions
@@ -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"
```
@@ -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=<none>
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"
```
@@ -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"
```
@@ -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.