From 20b41c39762f07cc7668cdcac950f71e3690ea7b Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Thu, 27 Aug 2026 03:28:19 -0400 Subject: [PATCH] Fix: Gate SSH hardening on a verified key --- boot | 119 ++++++-- tests/contracts.manifest | 2 + tests/setup/root-server-bootstrap-contract | 300 +++++++++++++++++++++ 3 files changed, 395 insertions(+), 26 deletions(-) create mode 100755 tests/setup/root-server-bootstrap-contract diff --git a/boot b/boot index 48eb65d..676dfa1 100755 --- a/boot +++ b/boot @@ -33,6 +33,60 @@ for arg in "$@"; do esac done +# The public bootstrap contract runs this branch as an ordinary user with a +# stubbed root identity. Keep its filesystem adapter unavailable to a real root +# shell so it cannot redirect a real installation by accident. +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" +} + +safe_root_authorized_keys() { + local keys + keys="$(system_path /root/.ssh/authorized_keys)" || return 1 + [[ -f "$keys" && ! -L "$keys" ]] || return 1 + [[ "$(stat -Lc '%u:%a' "$keys")" == '0:600' ]] || return 1 + grep -qEv '^[[:space:]]*(#|$)' "$keys" +} + +harden_server_ssh() { + local username="$1" user_home="$2" sshd_dir sshd_dropin harden + sshd_dir="$(system_path /etc/ssh/sshd_config.d)" || return 1 + sshd_dropin="$sshd_dir/90-panama.conf" + + if [[ -f "$sshd_dropin" ]]; then + echo "sshd is already hardened ($sshd_dropin)" + return 0 + fi + + printf 'Harden sshd (disable root login and password auth)? [Y/n]: ' + read -r harden "$sshd_dropin" + systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || true + echo "Wrote $sshd_dropin; make sure your key works before logging out." + fi +} + # Panama assumes Fedora's repositories and package names. if ! grep -qi '^ID=fedora' /etc/os-release 2>/dev/null; then echo "This looks like something other than Fedora; Panama only supports Fedora." >&2 @@ -84,33 +138,46 @@ if [[ "$(id -u)" -eq 0 ]]; then passwd "$username" "$SSHD_DROPIN" - systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || true - echo "Wrote $SSHD_DROPIN; make sure your key works before logging out." + user_ssh_dir="$user_home/.ssh" + user_keys="$user_ssh_dir/authorized_keys" + if [[ -n "$user_home" && ! -e "$user_keys" && ! -L "$user_keys" \ + && ! -L "$user_ssh_dir" ]] && safe_root_authorized_keys; then + copy_root_key=0 + if [[ ! -e "$user_ssh_dir" ]]; then + mkdir -p "$user_ssh_dir" + copy_root_key=1 + elif [[ ! -d "$user_ssh_dir" \ + || "$(stat -Lc '%u:%a' "$user_ssh_dir")" != "$(id -u "$username"):700" ]]; then + echo "SSH hardening unavailable: $username has no safe authorized_keys" >&2 + else + copy_root_key=1 fi + if (( copy_root_key )); then + echo "Copying root's authorized_keys to $username" + cp "$(system_path /root/.ssh/authorized_keys)" "$user_keys" + chmod 700 "$user_ssh_dir" + chmod 600 "$user_keys" + chown "$username:$username" "$user_ssh_dir" "$user_keys" + fi + fi + + if safe_authorized_keys "$username" "$user_home"; then + harden_server_ssh "$username" "$user_home" + else + echo "SSH hardening unavailable: $username has no safe authorized_keys" >&2 fi if ! command -v git >/dev/null 2>&1; then @@ -121,13 +188,13 @@ if [[ "$(id -u)" -eq 0 ]]; then # Cloned straight into the user's home and owned by them: this is the # checkout `panama update` will pull from for the life of the machine, and # a root-owned .git in a user's home is a wound that never heals. - PANAMA_PATH="$user_home/.local/share/Panama" + PANAMA_PATH="$bootstrap_home/.local/share/Panama" if [[ -d "$PANAMA_PATH/.git" ]]; then echo "Panama is already cloned at $PANAMA_PATH; updating" runuser -u "$username" -- git -C "$PANAMA_PATH" pull --ff-only \ || echo "Could not fast-forward; installing from the clone as it is" >&2 else - runuser -u "$username" -- mkdir -p "$user_home/.local/share" + runuser -u "$username" -- mkdir -p "$bootstrap_home/.local/share" runuser -u "$username" -- git clone "$REPO_URL" "$PANAMA_PATH" fi diff --git a/tests/contracts.manifest b/tests/contracts.manifest index 04468fc..e9b5c8f 100644 --- a/tests/contracts.manifest +++ b/tests/contracts.manifest @@ -271,6 +271,8 @@ hermetic tests/setup/package-lists-contract hermetic tests/setup/projects-contract hermetic tests/setup/readme-contract hermetic tests/setup/role-contract +# Root bootstrap runs entirely against a temporary filesystem and PATH adapters. +hermetic tests/setup/root-server-bootstrap-contract hermetic tests/setup/skills-contract hermetic tests/setup/test-runner-contract hermetic tests/setup/update-command-contract diff --git a/tests/setup/root-server-bootstrap-contract b/tests/setup/root-server-bootstrap-contract new file mode 100755 index 0000000..b33f2cb --- /dev/null +++ b/tests/setup/root-server-bootstrap-contract @@ -0,0 +1,300 @@ +#!/usr/bin/env bash + +# `boot --server` is deliberately public and must be safe before it reaches the +# cloned repository. Exercise its root branch through a PTY, against only a +# temporary filesystem and PATH adapters. + +set -uo pipefail + +repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +boot="$repo_dir/boot" + +[[ -x "$boot" ]] || { + printf 'root server bootstrap: %s is not executable\n' "$boot" >&2 + exit 1 +} + +python3 - "$boot" <<'PY' +import atexit +import errno +import fcntl +import os +import pty +import shutil +import subprocess +import sys +import tempfile +import termios +from pathlib import Path + +boot = sys.argv[1] +work = Path(tempfile.mkdtemp()) +atexit.register(shutil.rmtree, work, ignore_errors=True) +findings: list[str] = [] + + +def note(message: str) -> None: + findings.append(message) + + +def write_executable(path: Path, contents: str) -> None: + path.write_text(contents) + path.chmod(0o755) + + +def make_stubs(stub_dir: Path, fixture_root: Path, calls: Path) -> None: + common = f'''#!/usr/bin/env bash +set -u +calls={str(calls)!r} +log() {{ + local argument + {{ for argument in "$@"; do printf '%q ' "$argument"; done; printf '\\n'; }} >>"$calls" +}} +''' + + write_executable(stub_dir / "id", common + r''' +log id "$@" +case "${1:-}" in + -u) + case "${2:-}" in + '') printf '0\n' ;; + root) printf '0\n' ;; + gib) cat "$PANAMA_BOOT_FIXTURE_ROOT/state/target-uid" ;; + *) exit 97 ;; + esac + ;; + -nG) [[ "${2:-}" == gib ]] || exit 97; printf 'gib wheel\n' ;; + *) exit 97 ;; +esac +''') + write_executable(stub_dir / "passwd", common + r''' +log passwd "$@" +[[ "${1:-}" == -S && "${2:-}" == gib ]] || exit 97 +printf 'gib PS\n' +''') + write_executable(stub_dir / "getent", common + r''' +log getent "$@" +[[ "${1:-}" == passwd && "${2:-}" == gib ]] || exit 97 +home="$(<"$PANAMA_BOOT_FIXTURE_ROOT/state/home")" +printf 'gib:x:1000:1000::%s:/bin/bash\n' "$home" +''') + write_executable(stub_dir / "stat", common + r''' +log stat "$@" +[[ "${1:-}" == -Lc && "${2:-}" == '%u:%a' ]] || exit 97 +case "${3:-}" in + "$PANAMA_BOOT_FIXTURE_ROOT/home/gib/.ssh") cat "$PANAMA_BOOT_FIXTURE_ROOT/state/target-dir-meta" ;; + "$PANAMA_BOOT_FIXTURE_ROOT/home/gib/.ssh/authorized_keys") cat "$PANAMA_BOOT_FIXTURE_ROOT/state/target-key-meta" ;; + "$PANAMA_BOOT_FIXTURE_ROOT/root/.ssh/authorized_keys") cat "$PANAMA_BOOT_FIXTURE_ROOT/state/root-key-meta" ;; + *) exit 97 ;; +esac +''') + write_executable(stub_dir / "runuser", common + r''' +log runuser "$@" +[[ "${1:-}" == -u && "${2:-}" == gib && "${3:-}" == -- ]] || exit 97 +shift 3 +"$@" +''') + write_executable(stub_dir / "git", common + r''' +log git "$@" +case "${1:-}" in + clone) + mkdir -p "$3/.git" + cp "$PANAMA_BOOT_FIXTURE_ROOT/stub-install" "$3/install" + chmod +x "$3/install" + ;; + -C) [[ "${3:-}" == pull && "${4:-}" == --ff-only ]] || exit 97 ;; + *) exit 97 ;; +esac +''') + write_executable(stub_dir / "dnf", common + r''' +log dnf "$@" +[[ "${1:-}" == install && "${2:-}" == -y && "${3:-}" == git ]] || exit 97 +''') + write_executable(stub_dir / "sshd", common + r''' +log sshd "$@" +exit 97 +''') + write_executable(stub_dir / "systemctl", common + r''' +log systemctl "$@" +case "${1:-}:${2:-}" in + reload:sshd|reload:ssh) exit 0 ;; + *) exit 97 ;; +esac +''') + for command in ("useradd", "usermod"): + write_executable(stub_dir / command, common + f'''\nlog {command} "$@"\nexit 97\n''') + + +def configure_case(name: str) -> tuple[Path, Path]: + fixture_root = work / name / "root" + stub_dir = work / name / "bin" + calls = fixture_root / "calls" + state = fixture_root / "state" + ssh_dir = fixture_root / "home/gib/.ssh" + root_ssh_dir = fixture_root / "root/.ssh" + (fixture_root / "etc/ssh/sshd_config.d").mkdir(parents=True) + ssh_dir.mkdir(parents=True) + root_ssh_dir.mkdir(parents=True) + stub_dir.mkdir(parents=True) + state.mkdir() + calls.touch() + (state / "target-uid").write_text("1000\n") + (state / "home").write_text("/home/gib\n") + (state / "target-dir-meta").write_text("1000:700\n") + (state / "target-key-meta").write_text("1000:600\n") + (state / "root-key-meta").write_text("0:600\n") + (fixture_root / "stub-install").write_text( + "#!/usr/bin/env bash\nprintf 'install-handoff %s\\n' \"${PANAMA_PATH:-unset}\" >> \"$PANAMA_BOOT_FIXTURE_ROOT/calls\"\n" + ) + (fixture_root / "stub-install").chmod(0o755) + make_stubs(stub_dir, fixture_root, calls) + + target_keys = ssh_dir / "authorized_keys" + root_keys = root_ssh_dir / "authorized_keys" + if name == "missing": + pass + elif name == "empty": + target_keys.touch() + elif name == "comment-only": + target_keys.write_text("# no usable key\n\n") + elif name == "ssh-directory-symlink": + shutil.rmtree(ssh_dir) + alternate = fixture_root / "unsafe-ssh" + alternate.mkdir() + (fixture_root / "home/gib/.ssh").symlink_to(alternate) + elif name == "authorized-keys-symlink": + alternate = fixture_root / "unsafe-authorized-keys" + alternate.write_text("ssh-ed25519 unsafe\n") + target_keys.symlink_to(alternate) + elif name == "directory-wrong-mode": + target_keys.write_text("ssh-ed25519 target\n") + (state / "target-dir-meta").write_text("1000:755\n") + elif name == "root-copy-directory-wrong-mode": + root_keys.write_text("ssh-ed25519 root\n") + (state / "target-dir-meta").write_text("1000:755\n") + elif name == "file-wrong-mode": + target_keys.write_text("ssh-ed25519 target\n") + (state / "target-key-meta").write_text("1000:644\n") + elif name == "directory-wrong-owner": + target_keys.write_text("ssh-ed25519 target\n") + (state / "target-dir-meta").write_text("0:700\n") + elif name == "file-wrong-owner": + target_keys.write_text("ssh-ed25519 target\n") + (state / "target-key-meta").write_text("0:600\n") + elif name == "root-target-account": + target_keys.write_text("ssh-ed25519 target\n") + (state / "target-uid").write_text("0\n") + elif name == "relative-home": + target_keys.write_text("ssh-ed25519 target\n") + (state / "home").write_text("home/gib\n") + elif name == "safe-existing-key": + target_keys.write_text("ssh-ed25519 target\n") + elif name == "safe-root-key-copy": + root_keys.write_text("ssh-ed25519 root\n") + else: + raise ValueError(name) + return fixture_root, stub_dir + + +def run_case(name: str) -> tuple[int, str, str, Path]: + fixture_root, stub_dir = configure_case(name) + master, slave = pty.openpty() + + def attach_terminal() -> None: + fcntl.ioctl(0, termios.TIOCSCTTY, 0) + + env = { + **os.environ, + "PATH": f"{stub_dir}:/usr/bin:/bin", + "PANAMA_BOOT_FIXTURE_ROOT": str(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, + preexec_fn=attach_terminal, + ) + os.close(slave) + os.write(master, b"gib\nY\n") + chunks: list[bytes] = [] + while True: + try: + chunk = os.read(master, 4096) + except OSError as error: + if error.errno == errno.EIO: + break + raise + if not chunk: + break + chunks.append(chunk) + os.close(master) + status = process.wait() + calls = (fixture_root / "calls").read_text() + output = b"".join(chunks).decode(errors="replace") + return status, output, calls, fixture_root + + +unsafe_cases = ( + "missing", + "empty", + "comment-only", + "ssh-directory-symlink", + "authorized-keys-symlink", + "directory-wrong-mode", + "root-copy-directory-wrong-mode", + "file-wrong-mode", + "directory-wrong-owner", + "file-wrong-owner", + "root-target-account", + "relative-home", +) +for case in unsafe_cases: + status, output, calls, fixture_root = run_case(case) + if status != 0: + note(f"{case}: bootstrap stopped with status {status}: {output.strip()}") + if "SSH hardening unavailable" not in output: + note(f"{case}: unsafe login path did not explain why hardening was unavailable") + if "sshd -t" in calls: + note(f"{case}: unsafe login path validated sshd") + if "systemctl reload" in calls: + note(f"{case}: unsafe login path reloaded SSH") + if (fixture_root / "etc/ssh/sshd_config.d/90-panama.conf").exists(): + note(f"{case}: unsafe login path changed the SSH drop-in") + if case == "root-copy-directory-wrong-mode" and ( + fixture_root / "home/gib/.ssh/authorized_keys" + ).exists(): + note("root-copy-directory-wrong-mode: copied a root key into an unsafe SSH directory") + if "install-handoff " not in calls: + note(f"{case}: unsafe login path did not hand off to install") + +for case in ("safe-existing-key", "safe-root-key-copy"): + status, output, calls, fixture_root = run_case(case) + if status != 0: + note(f"{case}: safe login path stopped with status {status}: {output.strip()}") + if "SSH hardening unavailable" in output: + note(f"{case}: safe login path was rejected") + if "systemctl reload" not in calls: + note(f"{case}: safe login path did not reach SSH hardening") + if "install-handoff " not in calls: + note(f"{case}: safe login path did not hand off to install") + dropin = fixture_root / "etc/ssh/sshd_config.d/90-panama.conf" + if (dropin.read_text() if dropin.exists() else "") != "PermitRootLogin no\nPasswordAuthentication no\n": + note(f"{case}: safe login path did not write the expected SSH drop-in") + if case == "safe-root-key-copy": + keys = fixture_root / "home/gib/.ssh/authorized_keys" + if not keys.exists() or keys.read_text() != "ssh-ed25519 root\n": + note("safe-root-key-copy: root key was not copied to the target account") + +if findings: + print(f"root server bootstrap: {len(findings)} finding(s)", file=sys.stderr) + for finding in findings: + print(f" - {finding}", file=sys.stderr) + raise SystemExit(1) + +print("root server bootstrap: PASS") +PY