Fix: Gate SSH hardening on a verified key
This commit is contained in:
@@ -33,6 +33,60 @@ for arg in "$@"; do
|
|||||||
esac
|
esac
|
||||||
done
|
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 </dev/tty || harden=""
|
||||||
|
if [[ ! "$harden" =~ ^[Nn] ]]; then
|
||||||
|
printf 'PermitRootLogin no\nPasswordAuthentication no\n' >"$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.
|
# Panama assumes Fedora's repositories and package names.
|
||||||
if ! grep -qi '^ID=fedora' /etc/os-release 2>/dev/null; then
|
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
|
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" </dev/tty
|
passwd "$username" </dev/tty
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# The key that reached root is the key that should reach the user, or the
|
# Do not close root/password access until the account's key is an exact,
|
||||||
# next SSH login has no way in once root logins are closed below.
|
# usable login path. The fixture adapter resolves these logical system paths
|
||||||
user_home="$(getent passwd "$username" | cut -d: -f6)"
|
# beneath a temporary root; ordinary execution receives the original paths.
|
||||||
if [[ -s /root/.ssh/authorized_keys && ! -s "$user_home/.ssh/authorized_keys" ]]; then
|
logical_user_home="$(getent passwd "$username" | cut -d: -f6)"
|
||||||
echo "Copying root's authorized_keys to $username"
|
user_home=""
|
||||||
mkdir -p "$user_home/.ssh"
|
if [[ "$logical_user_home" == /* ]]; then
|
||||||
cp /root/.ssh/authorized_keys "$user_home/.ssh/authorized_keys"
|
user_home="$(system_path "$logical_user_home")" || true
|
||||||
chmod 700 "$user_home/.ssh"
|
fi
|
||||||
chmod 600 "$user_home/.ssh/authorized_keys"
|
bootstrap_home="$user_home"
|
||||||
chown -R "$username:$username" "$user_home/.ssh"
|
if [[ -z "$bootstrap_home" ]]; then
|
||||||
|
bootstrap_home="$(system_path "/home/$username")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Offered rather than imposed, defaulting to yes: a VPS keeps its provider's
|
user_ssh_dir="$user_home/.ssh"
|
||||||
# web console, so locking password and root logins out of sshd is
|
user_keys="$user_ssh_dir/authorized_keys"
|
||||||
# recoverable even when it goes wrong. Written as a drop-in so it never
|
if [[ -n "$user_home" && ! -e "$user_keys" && ! -L "$user_keys" \
|
||||||
# fights the distribution's own sshd_config.
|
&& ! -L "$user_ssh_dir" ]] && safe_root_authorized_keys; then
|
||||||
SSHD_DROPIN=/etc/ssh/sshd_config.d/90-panama.conf
|
copy_root_key=0
|
||||||
if [[ -f "$SSHD_DROPIN" ]]; then
|
if [[ ! -e "$user_ssh_dir" ]]; then
|
||||||
echo "sshd is already hardened ($SSHD_DROPIN)"
|
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
|
else
|
||||||
printf 'Harden sshd (disable root login and password auth)? [Y/n]: '
|
copy_root_key=1
|
||||||
read -r harden </dev/tty || harden=""
|
|
||||||
if [[ ! "$harden" =~ ^[Nn] ]]; then
|
|
||||||
printf 'PermitRootLogin no\nPasswordAuthentication no\n' >"$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
|
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
|
fi
|
||||||
|
|
||||||
if ! command -v git >/dev/null 2>&1; then
|
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
|
# 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
|
# 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.
|
# 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
|
if [[ -d "$PANAMA_PATH/.git" ]]; then
|
||||||
echo "Panama is already cloned at $PANAMA_PATH; updating"
|
echo "Panama is already cloned at $PANAMA_PATH; updating"
|
||||||
runuser -u "$username" -- git -C "$PANAMA_PATH" pull --ff-only \
|
runuser -u "$username" -- git -C "$PANAMA_PATH" pull --ff-only \
|
||||||
|| echo "Could not fast-forward; installing from the clone as it is" >&2
|
|| echo "Could not fast-forward; installing from the clone as it is" >&2
|
||||||
else
|
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"
|
runuser -u "$username" -- git clone "$REPO_URL" "$PANAMA_PATH"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -271,6 +271,8 @@ hermetic tests/setup/package-lists-contract
|
|||||||
hermetic tests/setup/projects-contract
|
hermetic tests/setup/projects-contract
|
||||||
hermetic tests/setup/readme-contract
|
hermetic tests/setup/readme-contract
|
||||||
hermetic tests/setup/role-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/skills-contract
|
||||||
hermetic tests/setup/test-runner-contract
|
hermetic tests/setup/test-runner-contract
|
||||||
hermetic tests/setup/update-command-contract
|
hermetic tests/setup/update-command-contract
|
||||||
|
|||||||
Executable
+300
@@ -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
|
||||||
Reference in New Issue
Block a user