Files
Panama/docs/superpowers/plans/2026-08-27-ssh-hardening-transaction.md
T

13 KiB

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:

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:

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

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:

# Root bootstrap runs entirely against a temporary filesystem and PATH adapters.
hermetic tests/setup/root-server-bootstrap-contract

Run:

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

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:

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:

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:

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

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