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

16 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, missing SSH unit, declined prompt, or unsupported pre-existing Panama drop-in 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, malformed non-comment lines, and foreign ownership are refused.
  • The installed policy is 00-panama.conf with PermitRootLogin no, PasswordAuthentication no, and KbdInteractiveAuthentication no.
  • Before reload, sshd -t and root/target sshd -T -C checks must prove the desired effective policy. Earlier main-config precedence therefore fails closed.
  • Root-key destination creation and writing run as the target UID. Revalidation follows, and no same-named primary group is assumed.
  • Existing regular drop-ins retain complete metadata on rollback. Symlinks, directories, FIFOs, and other non-regular objects make hardening unavailable.
  • Transaction traps are armed before the first candidate or backup artifact.
  • 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
malformed-key
mixed-valid-and-malformed-key
malformed-root-key
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
  valid_authorized_keys "$keys"
}

valid_authorized_keys skips blank/comment lines, requires at least one remaining line, and runs ssh-keygen -l on every remaining line. 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, whose non-comment lines all parse. 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. Run final directory creation and key writing as the target UID, then revalidate ownership, modes, and key parsing. Do not chown the destination or assume the user's primary group is named after the user. 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>
effective-root-policy-conflict: syntax=0 root-policy=conflict rollback-validate=0 reload=<none>
effective-target-policy-conflict: syntax=0 root-policy=safe target-policy=conflict rollback-validate=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 three-line content; syntax and root/target effective validation before reload; only the detected unit; content and complete metadata restoration; restored validation/reload ordering; nonzero status and no install handoff on every transactional failure; no *.tmp/*.backup residue on success; and retained backup or no-prior-file removal instructions when rollback fails. Add declined-hardening, missing-unit, symlink/directory/FIFO drop-in, target-UID normalization, candidate and backup preparation signals, and actual-root fixture-guard cases.

  • 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 (( ssh_had_prior )); then
    local restore
    restore="$(mktemp --tmpdir="$sshd_dir" .00-panama.XXXXXX.restore)" || return 1
    cp -a -- "$ssh_backup" "$restore"
    mv -f -- "$restore" "$ssh_dropin"
  else
    rm -f -- "$ssh_dropin"
  fi
}

Reject an existing 00-panama.conf unless it is a non-symlink regular file. Save the prior EXIT, INT, and TERM traps and arm state-aware preparation cleanup before creating any artifact. Create the candidate with umask 077; mktemp --tmpdir="$sshd_dir" .00-panama.XXXXXX.tmp, write the exact three-line desired content, and preserve an existing final file with cp -a in a collision-safe same-directory mktemp name ending .backup, not .conf. Atomically activate with mv -f. Preparation signals remove known artifacts without touching the final path; activated signals restore, validate, and reload. 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 ! effective_ssh_policy_is_hardened "$username"; 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

effective_ssh_policy_is_hardened uses sshd -T -C for root and target contexts. Root must report all three denials; target must report both authentication denials. On clean success clear the transaction state, restore traps, and remove the backup. On rollback failure with a prior file, keep the metadata-preserving backup and print its absolute path plus validation/reload commands. With no prior file, print rm -f -- /etc/ssh/sshd_config.d/00-panama.conf, sshd -t, and the detected reload command. Do not continue to clone/install after a transactional failure.

  • 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: OpenSSH-parsed target keys, 00-panama.conf, all three denials, sshd -t, root/target sshd -T, atomic installation, 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 every target key line with OpenSSH, offers hardening only with exact safe ownership/modes and a supported regular drop-in, validates syntax and effective root/target policy, reloads the detected unit, and restores the previous file with metadata 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"