Plan secure bootstrap and privileged installation
This commit is contained in:
@@ -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"
|
||||||
|
```
|
||||||
+11
-6
@@ -181,7 +181,7 @@ Create `setup/lib/artifact-provenance` with only these public shell functions:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
key_fingerprint_matches FILE EXPECTED_FINGERPRINT
|
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
|
verify_detached_signature KEY_FILE SIGNATURE_FILE CONTENT_FILE
|
||||||
rpm_signature_matches PACKAGE_FILE KEY_FILE EXPECTED_FINGERPRINT
|
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` |
|
| RPM Fusion nonfree | `79BDB88F9BBF73910FD4095B6A2AF96194843C65` |
|
||||||
| lionheartp/Hyprland COPR | `97E23476C89635135407C7D5E9BA41342C4B2995` |
|
| lionheartp/Hyprland COPR | `97E23476C89635135407C7D5E9BA41342C4B2995` |
|
||||||
| Flathub | `6E5C05D979C76DAF93C081354184DD4D907A7CAE` |
|
| Flathub | `6E5C05D979C76DAF93C081354184DD4D907A7CAE` |
|
||||||
|
| Claude Desktop Extra | `825A7D15D78BABE45646D5DF382409F597908867` |
|
||||||
|
|
||||||
The initial reviewed artifact pins are:
|
The initial reviewed artifact pins are:
|
||||||
|
|
||||||
@@ -245,6 +246,8 @@ The provenance README cites these publisher-controlled records:
|
|||||||
- Hyprland COPR key:
|
- Hyprland COPR key:
|
||||||
`https://download.copr.fedorainfracloud.org/results/lionheartp/Hyprland/pubkey.gpg`;
|
`https://download.copr.fedorainfracloud.org/results/lionheartp/Hyprland/pubkey.gpg`;
|
||||||
- Flathub descriptor: `https://flathub.org/repo/flathub.flatpakrepo`;
|
- 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:
|
- Node release and verification instructions:
|
||||||
`https://github.com/nodejs/node/releases/tag/v24.20.0` and
|
`https://github.com/nodejs/node/releases/tag/v24.20.0` and
|
||||||
`https://github.com/nodejs/node/blob/main/README.md`;
|
`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.
|
`terra.repo_gpgcheck=1`, and the local pinned key with the official repository.
|
||||||
Failure stops before initial, desktop, or Hyprland package transactions.
|
Failure stops before initial, desktop, or Hyprland package transactions.
|
||||||
- Hyprland COPR configuration is written from reviewed local data with the exact
|
- 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
|
base URL, `gpgcheck=1`, and the pinned project key. The publisher does not provide
|
||||||
use interactive/TOFU `dnf copr enable -y`.
|
`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
|
- 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.
|
primary key and requires GPG verification before adding or retaining the remote.
|
||||||
A mismatch preserves an existing remote and skips Flathub transactions.
|
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
|
- RustDesk uses the reviewed versioned RPM URL and SHA-256 before the narrow sudo DNF
|
||||||
install. It never resolves `latest` at runtime.
|
install. It never resolves `latest` at runtime.
|
||||||
- Claude Desktop repository setup is never downloaded or executed. If an existing
|
- 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`,
|
repository has the expected base URL, vendored-key fingerprint, `gpgcheck=1`, and
|
||||||
Panama may install from it. Otherwise it prints one optional manual step and
|
`repo_gpgcheck=1`, Panama may install from it. Otherwise it prints one optional
|
||||||
continues successfully.
|
manual step and continues successfully.
|
||||||
- Vicinae extensions use `npm ci`; lock mismatch fails the extension build without
|
- Vicinae extensions use `npm ci`; lock mismatch fails the extension build without
|
||||||
modifying the tracked lockfile.
|
modifying the tracked lockfile.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user