Fix: Complete SSH bootstrap hardening

This commit is contained in:
Gabriel Brown
2026-08-27 05:19:58 -04:00
parent 98e29eb8f3
commit fc8f226747
9 changed files with 852 additions and 186 deletions
+10 -4
View File
@@ -138,10 +138,16 @@ steps, each checked and skipped when already true:
exists, use it. exists, use it.
2. Ensure the user has a password (needed for sudo) — `passwd` interactively 2. Ensure the user has a password (needed for sudo) — `passwd` interactively
if none is set. if none is set.
3. Copy root's `authorized_keys` to the user when the user has none. 3. Copy root's `authorized_keys` only after OpenSSH parses every key line. The
4. Offer sshd hardening (yes/no, default yes): drop-in under destination directory and file are created as the target UID at `0700/0600`
`/etc/ssh/sshd_config.d/` with `PermitRootLogin no`, and revalidated before hardening is available.
`PasswordAuthentication no`, then reload sshd. Skipped when already set. 4. Offer sshd hardening (yes/no, default yes): atomically install
`/etc/ssh/sshd_config.d/00-panama.conf` with `PermitRootLogin no`,
`PasswordAuthentication no`, and `KbdInteractiveAuthentication no`. Run
`sshd -t` plus effective root/target `sshd -T -C` checks before reloading the
detected unit. Restore a prior regular file with metadata on failure. A
missing unit or unsupported existing drop-in leaves SSH unchanged and
bootstrap continues. The later binding transaction design owns the details.
5. Move/clone the checkout under the user's home, chown it, and re-exec 5. Move/clone the checkout under the user's home, chown it, and re-exec
`install --server` as that user. `install --server` as that user.
+15 -6
View File
@@ -99,10 +99,19 @@ Before it offers SSH hardening, it copies a safe root key when possible or
verifies the target key. The target user's `.ssh` must be owned by that user at verifies the target key. The target user's `.ssh` must be owned by that user at
`0700`, and `authorized_keys` must be owned by that user at `0600`. Without a `0700`, and `authorized_keys` must be owned by that user at `0600`. Without a
verified target key, SSH hardening is unavailable and the bootstrap continues. verified target key, SSH hardening is unavailable and the bootstrap continues.
Every non-comment key line must parse with `ssh-keygen`. Root-key destination
creation and writing run as the target UID, followed by the same owner, mode,
and key checks. Do not replace that with root writes or assume the user's
primary group matches the username.
Accepted hardening uses an atomic same-directory `sshd_config.d` drop-in, Accepted hardening uses atomic same-directory `00-panama.conf` with exactly
runs `sshd -t`, then reloads the detected SSH unit. Validation or reload `PermitRootLogin no`, `PasswordAuthentication no`, and
failure restores the previous drop-in before it retries validation and reload; `KbdInteractiveAuthentication no`. A pre-existing symlink or non-regular
failed recovery stops the handoff with manual recovery instructions. The object makes hardening unavailable, as does a missing SSH unit. Panama runs
fixture contracts exercise those branches. `panama test --safe` never reloads `sshd -t`, then checks effective root and target-user policy with `sshd -T -C`
a live daemon, so it is not live-host proof. before reloading the detected unit. Validation or reload failure restores a
prior regular file with its metadata before it retries validation and reload.
Failed recovery stops the handoff with instructions that distinguish a prior
file from no prior file. The fixture contracts also cover declined hardening
and interrupted preparation. `panama test --safe` never reloads a live daemon,
so it is not live-host proof.
+17 -8
View File
@@ -45,15 +45,24 @@ That command also works from a brand-new VPS's **root** login. It creates or
reuses your sudo-enabled user, then copies a safe root key when it can or reuses your sudo-enabled user, then copies a safe root key when it can or
verifies the target key before offering SSH hardening. A verified target key verifies the target key before offering SSH hardening. A verified target key
means the target user owns `.ssh` with mode `0700` and `authorized_keys` with means the target user owns `.ssh` with mode `0700` and `authorized_keys` with
mode `0600`. SSH hardening is unavailable without a verified target key, and mode `0600`. Every non-comment line in `authorized_keys` must be valid OpenSSH
the install continues without it. key material that `ssh-keygen` can parse. Root-key destination writes run as
the target user, and Panama rechecks the resulting owner, modes, and keys. SSH
hardening is unavailable without a verified target key or installed SSH unit,
and the install continues without it. Declining hardening also leaves SSH
unchanged.
When you accept hardening, Panama makes an atomic same-directory drop-in, When you accept hardening, Panama uses an atomic same-directory drop-in named
validates the complete SSH configuration with `sshd -t`, then reloads the `00-panama.conf`, whose early filename gives it safer precedence. A pre-existing
detected SSH unit. If validation or reload fails, it restores the previous symlink or non-regular object makes hardening unavailable. The effective policy
drop-in and validates and reloads that restored configuration; recovery that is exactly `PermitRootLogin no`, `PasswordAuthentication no`, and
cannot complete stops the handoff and prints the manual recovery command. The `KbdInteractiveAuthentication no`. Panama validates syntax with `sshd -t` and
fixture contracts exercise these branches. No real daemon reload runs under checks `sshd -T -C` for both root and target-user contexts before it reloads
the detected SSH unit. If validation or reload fails, it restores the previous
drop-in regular file with its metadata and validates and reloads that restored
configuration. Recovery that cannot complete stops the handoff and prints the
right manual command for either a prior file or no prior file. The fixture
contracts test these branches. No real daemon reload runs under
`panama test --safe`, so that suite is not live-host proof. `panama test --safe`, so that suite is not live-host proof.
After that, it hands off to a normal install as the new user. After that, it hands off to a normal install as the new user.
+136 -75
View File
@@ -48,6 +48,20 @@ system_path() {
printf '%s%s\n' "$BOOT_ROOT" "$path" printf '%s%s\n' "$BOOT_ROOT" "$path"
} }
valid_authorized_keys() {
local keys="$1" line saw_key=0
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]]; then
continue
fi
if ! ssh-keygen -l -f /dev/stdin >/dev/null 2>&1 <<<"$line"; then
return 1
fi
saw_key=1
done <"$keys"
(( saw_key ))
}
safe_authorized_keys() { safe_authorized_keys() {
local username="$1" user_home="$2" uid ssh_dir keys local username="$1" user_home="$2" uid ssh_dir keys
uid="$(id -u "$username")" || return 1 uid="$(id -u "$username")" || return 1
@@ -57,7 +71,7 @@ safe_authorized_keys() {
[[ -d "$ssh_dir" && ! -L "$ssh_dir" && -f "$keys" && ! -L "$keys" ]] || return 1 [[ -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' "$ssh_dir")" == "$uid:700" ]] || return 1
[[ "$(stat -Lc '%u:%a' "$keys")" == "$uid:600" ]] || return 1 [[ "$(stat -Lc '%u:%a' "$keys")" == "$uid:600" ]] || return 1
grep -qEv '^[[:space:]]*(#|$)' "$keys" valid_authorized_keys "$keys"
} }
safe_root_authorized_keys() { safe_root_authorized_keys() {
@@ -65,7 +79,7 @@ safe_root_authorized_keys() {
keys="$(system_path /root/.ssh/authorized_keys)" || return 1 keys="$(system_path /root/.ssh/authorized_keys)" || return 1
[[ -f "$keys" && ! -L "$keys" ]] || return 1 [[ -f "$keys" && ! -L "$keys" ]] || return 1
[[ "$(stat -Lc '%u:%a' "$keys")" == '0:600' ]] || return 1 [[ "$(stat -Lc '%u:%a' "$keys")" == '0:600' ]] || return 1
grep -qEv '^[[:space:]]*(#|$)' "$keys" valid_authorized_keys "$keys"
} }
detect_ssh_unit() { detect_ssh_unit() {
@@ -81,8 +95,9 @@ detect_ssh_unit() {
restore_ssh_dropin() { restore_ssh_dropin() {
local restore local restore
if [[ -n "${ssh_backup:-}" && -e "$ssh_backup" ]]; then if (( ssh_had_prior )); then
restore="$(mktemp --tmpdir="$sshd_dir" .90-panama.XXXXXX.restore)" || return 1 [[ -n "$ssh_backup" && -f "$ssh_backup" && ! -L "$ssh_backup" ]] || return 1
restore="$(mktemp --tmpdir="$sshd_dir" .00-panama.XXXXXX.restore)" || return 1
if ! cp -a -- "$ssh_backup" "$restore"; then if ! cp -a -- "$ssh_backup" "$restore"; then
remove_ssh_artifact "$restore" || true remove_ssh_artifact "$restore" || true
return 1 return 1
@@ -106,7 +121,7 @@ restore_ssh_transaction_traps() {
remove_ssh_artifact() { remove_ssh_artifact() {
local artifact="$1" local artifact="$1"
[[ -n "$artifact" && -e "$artifact" ]] || return 0 [[ -n "$artifact" && ( -e "$artifact" || -L "$artifact" ) ]] || return 0
if rm -f -- "$artifact"; then if rm -f -- "$artifact"; then
return 0 return 0
fi fi
@@ -115,121 +130,164 @@ remove_ssh_artifact() {
return 1 return 1
} }
print_ssh_recovery() {
if (( ssh_had_prior )); then
printf 'SSH rollback needs manual recovery. Backup: %s\n' "$ssh_backup" >&2
printf ' cp -a -- %q %q\n' "$ssh_backup" "$ssh_dropin" >&2
else
printf 'SSH rollback needs manual recovery. No prior drop-in existed.\n' >&2
printf ' rm -f -- %q\n' "$ssh_dropin" >&2
fi
printf ' sshd -t\n' >&2
printf ' systemctl reload %s\n' "$ssh_unit" >&2
}
policy_is_no() {
local policy="$1" setting="$2"
awk -v setting="$setting" '
$1 == setting { count += 1; if ($2 != "no") bad = 1 }
END { exit count != 1 || bad }
' <<<"$policy"
}
effective_ssh_policy_is_hardened() {
local username="$1" root_policy target_policy context
context='host=localhost,addr=127.0.0.1'
root_policy="$(sshd -T -C "user=root,$context")" || return 1
policy_is_no "$root_policy" permitrootlogin || return 1
policy_is_no "$root_policy" passwordauthentication || return 1
policy_is_no "$root_policy" kbdinteractiveauthentication || return 1
target_policy="$(sshd -T -C "user=$username,$context")" || return 1
policy_is_no "$target_policy" passwordauthentication || return 1
policy_is_no "$target_policy" kbdinteractiveauthentication
}
rollback_ssh_transaction() {
local reload_restored="$1" rollback_failed=0
restore_ssh_dropin || rollback_failed=1
sshd -t || rollback_failed=1
if (( reload_restored )); then
systemctl reload "$ssh_unit" || rollback_failed=1
fi
ssh_transaction_state=""
restore_ssh_transaction_traps
if (( rollback_failed )); then
print_ssh_recovery
else
remove_ssh_artifact "$ssh_backup" || true
fi
return 1
}
handle_ssh_transaction_exit() {
if [[ "$ssh_transaction_state" == preparing \
|| ( "$ssh_transaction_state" == activating && -e "$ssh_candidate" ) ]]; then
remove_ssh_artifact "$ssh_candidate" || true
remove_ssh_artifact "$ssh_backup" || true
elif [[ "$ssh_transaction_state" == activating || "$ssh_transaction_state" == activated ]]; then
restore_ssh_dropin || true
fi
}
handle_ssh_transaction_signal() { handle_ssh_transaction_signal() {
local signal_status="$1" local signal_status="$1"
trap - INT TERM trap - INT TERM
if [[ -n "$ssh_candidate" && -e "$ssh_candidate" ]]; then if [[ "$ssh_transaction_state" == preparing \
ssh_transaction_active=0 || ( "$ssh_transaction_state" == activating && -e "$ssh_candidate" ) ]]; then
ssh_transaction_state=""
restore_ssh_transaction_traps restore_ssh_transaction_traps
remove_ssh_artifact "$ssh_candidate" || true remove_ssh_artifact "$ssh_candidate" || true
remove_ssh_artifact "$ssh_backup" || true remove_ssh_artifact "$ssh_backup" || true
exit "$signal_status"
fi
rollback_failed=0
restore_ssh_dropin || rollback_failed=1
sshd -t || rollback_failed=1
systemctl reload "$ssh_unit" || rollback_failed=1
ssh_transaction_active=0
restore_ssh_transaction_traps
if (( rollback_failed )); then
printf 'SSH rollback needs manual recovery. Backup: %s\n' "$ssh_backup" >&2
printf ' cp -a -- %q %q\n' "$ssh_backup" "$ssh_dropin" >&2
printf ' sshd -t\n' >&2
printf ' systemctl reload %s\n' "$ssh_unit" >&2
else else
remove_ssh_artifact "$ssh_backup" || true rollback_ssh_transaction 1 || true
fi fi
exit "$signal_status" exit "$signal_status"
} }
harden_server_ssh() { harden_server_ssh() {
local username="$1" user_home="$2" sshd_dir ssh_dropin harden ssh_unit local username="$1" user_home="$2" sshd_dir ssh_dropin harden ssh_unit
local ssh_candidate="" ssh_backup="" rollback_failed=0 local ssh_candidate="" ssh_backup="" ssh_had_prior=0
local ssh_transaction_active=0 local ssh_transaction_state=""
local ssh_saved_exit_trap ssh_saved_int_trap ssh_saved_term_trap local ssh_saved_exit_trap ssh_saved_int_trap ssh_saved_term_trap
sshd_dir="$(system_path /etc/ssh/sshd_config.d)" || return 1 sshd_dir="$(system_path /etc/ssh/sshd_config.d)" || return 1
ssh_dropin="$sshd_dir/90-panama.conf" ssh_dropin="$sshd_dir/00-panama.conf"
printf 'Harden sshd (disable root login and password auth)? [Y/n]: ' if [[ -L "$ssh_dropin" || ( -e "$ssh_dropin" && ! -f "$ssh_dropin" ) ]]; then
printf 'SSH hardening unavailable: %s is not a regular file\n' "$ssh_dropin" >&2
return 2
fi
if ! ssh_unit="$(detect_ssh_unit)"; then
echo "SSH hardening unavailable: neither sshd.service nor ssh.service is installed" >&2
return 2
fi
printf 'Harden sshd (disable root, password, and keyboard-interactive authentication)? [Y/n]: '
read -r harden </dev/tty || harden="" read -r harden </dev/tty || harden=""
if [[ "$harden" =~ ^[Nn] ]]; then if [[ "$harden" =~ ^[Nn] ]]; then
return 0 return 0
fi fi
if ! ssh_unit="$(detect_ssh_unit)"; then ssh_saved_exit_trap="$(trap -p EXIT)"
echo "SSH hardening failed: neither sshd.service nor ssh.service exists" >&2 ssh_saved_int_trap="$(trap -p INT)"
ssh_saved_term_trap="$(trap -p TERM)"
ssh_transaction_state=preparing
trap 'handle_ssh_transaction_exit' EXIT
trap 'handle_ssh_transaction_signal 130' INT
trap 'handle_ssh_transaction_signal 143' TERM
if ! ssh_candidate="$(umask 077; mktemp --tmpdir="$sshd_dir" .00-panama.XXXXXX.tmp)"; then
ssh_transaction_state=""
restore_ssh_transaction_traps
return 1 return 1
fi fi
if ! printf 'PermitRootLogin no\nPasswordAuthentication no\nKbdInteractiveAuthentication no\n' >"$ssh_candidate"; then
ssh_candidate="$(umask 077; mktemp --tmpdir="$sshd_dir" .90-panama.XXXXXX.tmp)" || return 1 ssh_transaction_state=""
if ! printf 'PermitRootLogin no\nPasswordAuthentication no\n' >"$ssh_candidate"; then restore_ssh_transaction_traps
remove_ssh_artifact "$ssh_candidate" || true remove_ssh_artifact "$ssh_candidate" || true
return 1 return 1
fi fi
if [[ -e "$ssh_dropin" ]]; then if [[ -e "$ssh_dropin" ]]; then
ssh_backup="$(umask 077; mktemp --tmpdir="$sshd_dir" .90-panama.XXXXXX.backup)" || { ssh_had_prior=1
if ! ssh_backup="$(umask 077; mktemp --tmpdir="$sshd_dir" .00-panama.XXXXXX.backup)"; then
ssh_transaction_state=""
restore_ssh_transaction_traps
remove_ssh_artifact "$ssh_candidate" || true remove_ssh_artifact "$ssh_candidate" || true
return 1 return 1
} fi
if ! cat -- "$ssh_dropin" >"$ssh_backup"; then if ! cp -a -- "$ssh_dropin" "$ssh_backup"; then
ssh_transaction_state=""
restore_ssh_transaction_traps
remove_ssh_artifact "$ssh_candidate" || true remove_ssh_artifact "$ssh_candidate" || true
remove_ssh_artifact "$ssh_backup" || true remove_ssh_artifact "$ssh_backup" || true
return 1 return 1
fi fi
fi fi
ssh_saved_exit_trap="$(trap -p EXIT)" ssh_transaction_state=activating
ssh_saved_int_trap="$(trap -p INT)"
ssh_saved_term_trap="$(trap -p TERM)"
trap 'if [[ "${ssh_transaction_active:-0}" == 1 ]]; then restore_ssh_dropin || true; fi' EXIT
trap 'handle_ssh_transaction_signal 130' INT
trap 'handle_ssh_transaction_signal 143' TERM
ssh_transaction_active=1
if ! mv -f -- "$ssh_candidate" "$ssh_dropin"; then if ! mv -f -- "$ssh_candidate" "$ssh_dropin"; then
ssh_transaction_active=0 ssh_transaction_state=""
restore_ssh_transaction_traps restore_ssh_transaction_traps
remove_ssh_artifact "$ssh_candidate" || true remove_ssh_artifact "$ssh_candidate" || true
remove_ssh_artifact "$ssh_backup" || true remove_ssh_artifact "$ssh_backup" || true
return 1 return 1
fi fi
ssh_candidate="" ssh_candidate=""
ssh_transaction_state=activated
if ! sshd -t; then if ! sshd -t || ! effective_ssh_policy_is_hardened "$username"; then
restore_ssh_dropin || rollback_failed=1 rollback_ssh_transaction 0 || true
sshd -t || rollback_failed=1
ssh_transaction_active=0
restore_ssh_transaction_traps
if (( rollback_failed )); then
printf 'SSH rollback needs manual recovery. Backup: %s\n' "$ssh_backup" >&2
printf ' cp -a -- %q %q\n' "$ssh_backup" "$ssh_dropin" >&2
printf ' sshd -t\n' >&2
printf ' systemctl reload %s\n' "$ssh_unit" >&2
else
remove_ssh_artifact "$ssh_backup" || true
fi
return 1 return 1
fi fi
if ! systemctl reload "$ssh_unit"; then if ! systemctl reload "$ssh_unit"; then
restore_ssh_dropin || rollback_failed=1 rollback_ssh_transaction 1 || true
sshd -t || rollback_failed=1
systemctl reload "$ssh_unit" || rollback_failed=1
ssh_transaction_active=0
restore_ssh_transaction_traps
if (( rollback_failed )); then
printf 'SSH rollback needs manual recovery. Backup: %s\n' "$ssh_backup" >&2
printf ' cp -a -- %q %q\n' "$ssh_backup" "$ssh_dropin" >&2
printf ' sshd -t\n' >&2
printf ' systemctl reload %s\n' "$ssh_unit" >&2
else
remove_ssh_artifact "$ssh_backup" || true
fi
return 1 return 1
fi fi
ssh_transaction_active=0 ssh_transaction_state=""
restore_ssh_transaction_traps restore_ssh_transaction_traps
remove_ssh_artifact "$ssh_backup" || return 1 remove_ssh_artifact "$ssh_backup" || return 1
echo "Wrote $ssh_dropin; make sure your key works before logging out." echo "Wrote $ssh_dropin; make sure your key works before logging out."
@@ -305,7 +363,6 @@ if [[ "$(id -u)" -eq 0 ]]; then
&& ! -L "$user_ssh_dir" ]] && safe_root_authorized_keys; then && ! -L "$user_ssh_dir" ]] && safe_root_authorized_keys; then
copy_root_key=0 copy_root_key=0
if [[ ! -e "$user_ssh_dir" ]]; then if [[ ! -e "$user_ssh_dir" ]]; then
mkdir -p "$user_ssh_dir"
copy_root_key=1 copy_root_key=1
elif [[ ! -d "$user_ssh_dir" \ elif [[ ! -d "$user_ssh_dir" \
|| "$(stat -Lc '%u:%a' "$user_ssh_dir")" != "$(id -u "$username"):700" ]]; then || "$(stat -Lc '%u:%a' "$user_ssh_dir")" != "$(id -u "$username"):700" ]]; then
@@ -315,15 +372,19 @@ if [[ "$(id -u)" -eq 0 ]]; then
fi fi
if (( copy_root_key )); then if (( copy_root_key )); then
echo "Copying root's authorized_keys to $username" echo "Copying root's authorized_keys to $username"
cp "$(system_path /root/.ssh/authorized_keys)" "$user_keys" root_keys="$(system_path /root/.ssh/authorized_keys)"
chmod 700 "$user_ssh_dir" if ! runuser -u "$username" -- install -d -m 0700 -- "$user_ssh_dir" \
chmod 600 "$user_keys" || ! runuser -u "$username" -- install -m 0600 -- /dev/stdin "$user_keys" \
chown "$username:$username" "$user_ssh_dir" "$user_keys" <"$root_keys"; then
echo "SSH hardening unavailable: could not install root's key for $username" >&2
fi
fi fi
fi fi
if safe_authorized_keys "$username" "$user_home"; then if safe_authorized_keys "$username" "$user_home"; then
if ! harden_server_ssh "$username" "$user_home"; then harden_status=0
harden_server_ssh "$username" "$user_home" || harden_status=$?
if (( harden_status != 0 && harden_status != 2 )); then
echo "SSH hardening failed; stopping before install handoff." >&2 echo "SSH hardening failed; stopping before install handoff." >&2
exit 1 exit 1
fi fi
@@ -14,9 +14,21 @@
- Preserve the public `boot --server` command and the non-root desktop bootstrap path. - 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. - 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. - 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. - 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. - 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. - `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`. - 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. - Preserve the user's unstaged `/home/gib/.local/share/Panama/config/bash/.bashrc` change outside this worktree.
@@ -75,6 +87,9 @@ Run one table row per unsafe state:
missing missing
empty empty
comment-only comment-only
malformed-key
mixed-valid-and-malformed-key
malformed-root-key
ssh-directory-symlink ssh-directory-symlink
authorized-keys-symlink authorized-keys-symlink
directory-wrong-mode directory-wrong-mode
@@ -124,11 +139,22 @@ safe_authorized_keys() {
[[ -d "$ssh_dir" && ! -L "$ssh_dir" && -f "$keys" && ! -L "$keys" ]] || return 1 [[ -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' "$ssh_dir")" == "$uid:700" ]] || return 1
[[ "$(stat -Lc '%u:%a' "$keys")" == "$uid:600" ]] || return 1 [[ "$(stat -Lc '%u:%a' "$keys")" == "$uid:600" ]] || return 1
grep -qEv '^[[:space:]]*(#|$)' "$keys" valid_authorized_keys "$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. `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** - [ ] **Step 4: Add the hermetic manifest entry and run focused checks**
@@ -181,12 +207,20 @@ Extend the command state with `SSHD_RESULTS` and `RELOAD_RESULTS`, consumed one
success-without-prior-dropin: validate=0 reload=0 success-without-prior-dropin: validate=0 reload=0
success-replaces-prior-dropin: validate=0 reload=0 success-replaces-prior-dropin: validate=0 reload=0
candidate-invalid: validate=1,0 reload=<none> 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 candidate-reload-fails: validate=0,0 reload=1,0
rollback-validation-fails: validate=0,1 reload=1 rollback-validation-fails: validate=0,1 reload=1
rollback-reload-fails: validate=0,0 reload=1,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. 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** - [ ] **Step 2: Run the focused contract and confirm it fails on current code**
@@ -212,9 +246,9 @@ detect_ssh_unit() {
} }
restore_ssh_dropin() { restore_ssh_dropin() {
if [[ -n "${ssh_backup:-}" && -e "$ssh_backup" ]]; then if (( ssh_had_prior )); then
local restore local restore
restore="$(mktemp --tmpdir="$sshd_dir" .90-panama.XXXXXX.restore)" || return 1 restore="$(mktemp --tmpdir="$sshd_dir" .00-panama.XXXXXX.restore)" || return 1
cp -a -- "$ssh_backup" "$restore" cp -a -- "$ssh_backup" "$restore"
mv -f -- "$restore" "$ssh_dropin" mv -f -- "$restore" "$ssh_dropin"
else else
@@ -223,7 +257,15 @@ restore_ssh_dropin() {
} }
``` ```
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. 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: After activation:
@@ -233,6 +275,11 @@ if ! sshd -t; then
sshd -t || rollback_failed=1 sshd -t || rollback_failed=1
return 1 return 1
fi 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 if ! systemctl reload "$ssh_unit"; then
restore_ssh_dropin restore_ssh_dropin
sshd -t || rollback_failed=1 sshd -t || rollback_failed=1
@@ -241,7 +288,13 @@ if ! systemctl reload "$ssh_unit"; then
fi 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. `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** - [ ] **Step 4: Verify failure status, cleanup, and old public behavior**
@@ -281,7 +334,11 @@ git commit -m "Fix: Roll back failed SSH hardening"
- [ ] **Step 1: Write the documentation assertions first** - [ ] **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. 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** - [ ] **Step 2: Run the README contract and confirm the old prose fails**
@@ -295,7 +352,11 @@ Expected: nonzero until README describes the transactional behavior.
- [ ] **Step 3: Update the documentation without claiming a live reload** - [ ] **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`. 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** - [ ] **Step 4: Run the plan gate**
@@ -33,8 +33,11 @@ real firewall, install a real package, start a service, or apply changes to a se
- Safe target SSH state means a non-root account, an absolute home, a real `.ssh` - Safe target SSH state means a non-root account, an absolute home, a real `.ssh`
directory owned by the target UID with mode `0700`, and a nonempty regular directory owned by the target UID with mode `0700`, and a nonempty regular
`authorized_keys` file owned by the target UID with mode `0600`. Symlinks are `authorized_keys` file owned by the target UID with mode `0600`. Symlinks are
refused. Panama may create and normalize files it copied from root, but it does refused. Every nonblank, non-comment key line must parse with OpenSSH tooling.
not take ownership of an unsafe pre-existing target path. Panama may create and normalize files it copied from root, but final destination
creation and writing run as the target UID. It does not take ownership of an
unsafe pre-existing target path or assume that the user's primary group has the
same name as the user.
- Failed SSH reload rollback includes restored-config validation and a reload of the - Failed SSH reload rollback includes restored-config validation and a reload of the
restored configuration, because a command can apply state and still return restored configuration, because a command can apply state and still return
nonzero. nonzero.
@@ -92,16 +95,21 @@ Before offering hardening, Panama verifies:
absolute and nonempty; absolute and nonempty;
- neither the home-relative `.ssh` path nor `authorized_keys` is a symlink; - neither the home-relative `.ssh` path nor `authorized_keys` is a symlink;
- `.ssh` and `authorized_keys` have the exact ownership and modes in Decisions; - `.ssh` and `authorized_keys` have the exact ownership and modes in Decisions;
- `authorized_keys` contains at least one nonblank, non-comment line; - `authorized_keys` contains at least one nonblank, non-comment line, and OpenSSH
- exactly one installed SSH unit is detected, preferring `sshd.service` and falling parses every such line as a public key;
back to `ssh.service` only when the first unit is absent. - an installed SSH unit is detected, preferring `sshd.service` and falling back to
`ssh.service` only when the first unit is absent.
If the target has no key and root has a safe regular key file, Panama copies only that If the target has no key and root has a safe regular key file, Panama copies only that
file, creates `.ssh`, applies `0700/0600`, and changes ownership only on those two file. The target UID creates or normalizes `.ssh` at `0700` and writes
paths. It does not recursively take ownership of an existing directory tree. `authorized_keys` at `0600` through an already-open root-key input. Panama then
revalidates exact UID ownership, modes, and OpenSSH key parsing. It does not chown a
target-controlled path or assume a same-named primary group.
If the preconditions fail, Panama prints why hardening is unavailable, keeps If the key preconditions fail, no SSH unit is installed, or the Panama drop-in path
root/password authentication unchanged, and continues the clone/install handoff. already names a symlink or non-regular object, Panama prints why hardening is
unavailable, keeps root/password authentication unchanged, and continues the
clone/install handoff. Declining the prompt has the same unchanged-state outcome.
### Transaction ### Transaction
@@ -110,22 +118,32 @@ The desired drop-in is exactly:
```text ```text
PermitRootLogin no PermitRootLogin no
PasswordAuthentication no PasswordAuthentication no
KbdInteractiveAuthentication no
``` ```
Panama creates the candidate with `umask 077` and `mktemp` in Panama creates the candidate with `umask 077` and `mktemp` in
`/etc/ssh/sshd_config.d`. Its temporary name does not end in `.conf`, so the normal `/etc/ssh/sshd_config.d`. Its temporary name does not end in `.conf`, so the normal
include glob cannot activate it early. It preserves an existing include glob cannot activate it early. It refuses a pre-existing Panama path unless
`90-panama.conf` in the same directory, arms EXIT/INT/TERM rollback, and atomically it is a non-symlink regular file. It preserves an existing `00-panama.conf`, including
renames the candidate over the final path. its ownership, mode, timestamps, ACLs, and extended attributes, in the same directory.
State-aware EXIT/INT/TERM cleanup is armed before the first candidate or backup
artifact, and the candidate is atomically renamed over the final path.
It then runs `sshd -t` against the complete active configuration and reloads only the It then runs `sshd -t` against the complete active configuration. Before reload,
detected unit. Success disarms rollback and removes the backup. `sshd -T -C` must report `permitrootlogin no`, `passwordauthentication no`, and
`kbdinteractiveauthentication no` for the root context. The target-user context must
report both authentication directives as `no`. This fails closed when an earlier
main-config directive wins despite the precedence-safe filename. Panama reloads only
the detected unit after every check passes. Success disarms rollback and removes the
backup.
On validation failure, Panama restores or removes the new drop-in, validates the On validation failure, Panama restores or removes the new drop-in, validates the
restored configuration, and returns nonzero without reloading the rejected candidate. restored configuration, and returns nonzero without reloading the rejected candidate.
On reload failure, Panama restores the previous drop-in, validates it, reloads the On reload failure, Panama restores the previous drop-in, validates it, reloads the
restored unit, and returns nonzero. A rollback validation/reload failure preserves the restored unit, and returns nonzero. A rollback validation/reload failure preserves the
backup and prints its path plus exact recovery commands. backup and prints its path plus exact recovery commands. When no prior file existed,
recovery instead instructs the operator to remove `00-panama.conf`, run `sshd -t`,
and reload the detected unit.
Existing drop-ins go through the same desired-content, validation, and reload path; Existing drop-ins go through the same desired-content, validation, and reload path;
mere existence is not treated as proof of hardening. mere existence is not treated as proof of hardening.
@@ -143,12 +161,18 @@ and `systemctl`, and provides temporary account and filesystem state.
Required cases are: Required cases are:
- missing, empty, comment-only, symlinked, wrong-owner, and wrong-mode target keys; - missing, empty, comment-only, malformed, mixed valid/malformed, symlinked,
wrong-owner, and wrong-mode target keys;
- safe root-key copy and safe existing target key; - safe root-key copy and safe existing target key;
- target-UID copy normalization with a primary group whose name differs from the user;
- declined hardening and no installed SSH unit;
- pre-existing symlink, directory, and FIFO Panama drop-ins;
- successful initial install and replacement of an existing drop-in; - successful initial install and replacement of an existing drop-in;
- invalid candidate rollback; - invalid syntax and conflicting effective-policy rollback;
- failed reload rollback, including restored validation and reload; - failed reload rollback, including restored validation and reload;
- rollback failure retaining its recovery artifact; - rollback failure retaining its recovery artifact or printing no-prior-file removal;
- INT/TERM during candidate and backup preparation as well as after activation;
- actual-root rejection of `PANAMA_BOOT_FIXTURE_ROOT` in a user namespace;
- exact command ordering and no install handoff after a transactional failure. - exact command ordering and no install handoff after a transactional failure.
## Verified bootstrap and installer inputs ## Verified bootstrap and installer inputs
+2 -1
View File
@@ -271,7 +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. # Root bootstrap uses a temporary filesystem, PATH adapters, real public-key
# parsing, and a user namespace for the actual-root fixture guard.
hermetic tests/setup/root-server-bootstrap-contract 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
+16
View File
@@ -57,18 +57,28 @@ assert_bootstrap_probe_rejected() {
} }
target_key_requirement='target user owns[^.]*\.ssh[^.]*mode[^.]*([^0-9]|^)0700([^0-9]|$)[^.]*authorized_keys[^.]*mode[^.]*([^0-9]|^)0600([^0-9]|$)' target_key_requirement='target user owns[^.]*\.ssh[^.]*mode[^.]*([^0-9]|^)0700([^0-9]|$)[^.]*authorized_keys[^.]*mode[^.]*([^0-9]|^)0600([^0-9]|$)'
parseable_key_requirement='(every|each)[^.]*non-?comment[^.]*authorized_keys[^.]*(OpenSSH|ssh-keygen)[^.]*(parse|valid)|(OpenSSH|ssh-keygen)[^-]*parse[^.]*every[^.]*non-?comment'
hardening_continues_requirement='hardening[[:space:]]+is[[:space:]]+unavailable[^.]*without[^.]*verified[^.]*key[^.]*install[[:space:]]+continues[^.]*without[[:space:]]+(it|SSH[[:space:]]+hardening)' hardening_continues_requirement='hardening[[:space:]]+is[[:space:]]+unavailable[^.]*without[^.]*verified[^.]*key[^.]*install[[:space:]]+continues[^.]*without[[:space:]]+(it|SSH[[:space:]]+hardening)'
atomic_dropin_requirement='atomic[[:space:]]+same-directory[[:space:]]+drop-in' atomic_dropin_requirement='atomic[[:space:]]+same-directory[[:space:]]+drop-in'
rollback_requirement='validation[^.]*reload[^.]*fail[^.]*(restor|rollback)[^.]*previous[[:space:]]+drop-in' rollback_requirement='validation[^.]*reload[^.]*fail[^.]*(restor|rollback)[^.]*previous[[:space:]]+drop-in'
effective_policy_requirement='sshd -T[^.]*root[^.]*target|sshd -T[^.]*target[^.]*root'
require_bootstrap_doc "$target_key_requirement" \ require_bootstrap_doc "$target_key_requirement" \
'the root bootstrap docs do not require target-user ownership with exact 0700/0600 SSH modes' 'the root bootstrap docs do not require target-user ownership with exact 0700/0600 SSH modes'
require_bootstrap_doc "$parseable_key_requirement" \
'the root bootstrap docs do not require OpenSSH to parse every non-comment key entry'
require_bootstrap_doc "$hardening_continues_requirement" \ require_bootstrap_doc "$hardening_continues_requirement" \
'the root bootstrap docs do not say bootstrap continues without unavailable SSH hardening' 'the root bootstrap docs do not say bootstrap continues without unavailable SSH hardening'
require_bootstrap_doc 'sshd -t' \ require_bootstrap_doc 'sshd -t' \
'the root bootstrap docs do not name sshd -t validation' 'the root bootstrap docs do not name sshd -t validation'
require_bootstrap_doc "$effective_policy_requirement" \
'the root bootstrap docs do not name sshd -T checks for root and target contexts'
require_bootstrap_doc "$atomic_dropin_requirement" \ require_bootstrap_doc "$atomic_dropin_requirement" \
'the root bootstrap docs do not describe the atomic same-directory drop-in' 'the root bootstrap docs do not describe the atomic same-directory drop-in'
require_bootstrap_doc '00-panama\.conf' \
'the root bootstrap docs do not name the precedence-safe 00-panama.conf drop-in'
require_bootstrap_doc 'PermitRootLogin[^.]*no[^.]*PasswordAuthentication[^.]*no[^.]*KbdInteractiveAuthentication[^.]*no' \
'the root bootstrap docs do not state all three effective authentication denials'
require_bootstrap_doc 'detected (SSH )?unit.*reload|reload.*detected (SSH )?unit' \ require_bootstrap_doc 'detected (SSH )?unit.*reload|reload.*detected (SSH )?unit' \
'the root bootstrap docs do not describe reloading the detected SSH unit' 'the root bootstrap docs do not describe reloading the detected SSH unit'
require_bootstrap_doc "$rollback_requirement" \ require_bootstrap_doc "$rollback_requirement" \
@@ -85,6 +95,10 @@ assert_bootstrap_probe_rejected 'exact SSH modes' "$target_key_requirement" \
"${bootstrap_doc//0700/700}" "${bootstrap_doc//0700/700}"
assert_bootstrap_probe_rejected 'target-user ownership' "$target_key_requirement" \ assert_bootstrap_probe_rejected 'target-user ownership' "$target_key_requirement" \
"${bootstrap_doc//target user owns/someone owns}" "${bootstrap_doc//target user owns/someone owns}"
weakened_key_doc="${bootstrap_doc//OpenSSH/text tooling}"
weakened_key_doc="${weakened_key_doc//ssh-keygen/text parser}"
assert_bootstrap_probe_rejected 'OpenSSH key parsing' "$parseable_key_requirement" \
"$weakened_key_doc"
assert_bootstrap_probe_rejected 'hardening availability' "$hardening_continues_requirement" \ assert_bootstrap_probe_rejected 'hardening availability' "$hardening_continues_requirement" \
"${bootstrap_doc//unavailable/available}" "${bootstrap_doc//unavailable/available}"
assert_bootstrap_probe_rejected 'hardening continuation' "$hardening_continues_requirement" \ assert_bootstrap_probe_rejected 'hardening continuation' "$hardening_continues_requirement" \
@@ -95,6 +109,8 @@ assert_bootstrap_probe_rejected 'rollback after failure' "$rollback_requirement"
"${bootstrap_doc//restores /keeps }" "${bootstrap_doc//restores /keeps }"
assert_bootstrap_probe_rejected 'rollback trigger' "$rollback_requirement" \ assert_bootstrap_probe_rejected 'rollback trigger' "$rollback_requirement" \
"${bootstrap_doc//fails/works}" "${bootstrap_doc//fails/works}"
assert_bootstrap_probe_rejected 'effective target policy' "$effective_policy_requirement" \
"${bootstrap_doc//target/root}"
if grep -qiE 'merely writes? (the )?(SSH )?(drop-in|file)|reload failure.*ignored|ignores? .*reload failure' <<<"$bootstrap_doc"; then if grep -qiE 'merely writes? (the )?(SSH )?(drop-in|file)|reload failure.*ignored|ignores? .*reload failure' <<<"$bootstrap_doc"; then
note 'the root bootstrap docs weaken the transaction by treating the write or reload failure as harmless' note 'the root bootstrap docs weaken the transaction by treating the write or reload failure as harmless'
+542 -63
View File
@@ -21,8 +21,10 @@ import fcntl
import os import os
import pty import pty
import re import re
import select
import signal import signal
import shutil import shutil
import stat as stat_module
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
@@ -45,6 +47,22 @@ def write_executable(path: Path, contents: str) -> None:
path.chmod(0o755) path.chmod(0o755)
def generate_public_key(label: str) -> str:
key_path = work / label
subprocess.run(
["ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", label, "-f", key_path],
check=True,
)
public_key = key_path.with_suffix(".pub").read_text()
key_path.unlink()
key_path.with_suffix(".pub").unlink()
return public_key
TARGET_PUBLIC_KEY = generate_public_key("panama-target-fixture")
ROOT_PUBLIC_KEY = generate_public_key("panama-root-fixture")
def make_stubs(stub_dir: Path, fixture_root: Path, calls: Path) -> None: def make_stubs(stub_dir: Path, fixture_root: Path, calls: Path) -> None:
common = f'''#!/usr/bin/env bash common = f'''#!/usr/bin/env bash
set -u set -u
@@ -77,7 +95,7 @@ case "${1:-}" in
*) exit 97 ;; *) exit 97 ;;
esac esac
;; ;;
-nG) [[ "${2:-}" == gib ]] || exit 97; printf 'gib wheel\n' ;; -nG) [[ "${2:-}" == gib ]] || exit 97; printf 'operators wheel\n' ;;
*) exit 97 ;; *) exit 97 ;;
esac esac
''') ''')
@@ -90,7 +108,7 @@ printf 'gib PS\n'
log getent "$@" log getent "$@"
[[ "${1:-}" == passwd && "${2:-}" == gib ]] || exit 97 [[ "${1:-}" == passwd && "${2:-}" == gib ]] || exit 97
home="$(<"$PANAMA_BOOT_FIXTURE_ROOT/state/home")" home="$(<"$PANAMA_BOOT_FIXTURE_ROOT/state/home")"
printf 'gib:x:1000:1000::%s:/bin/bash\n' "$home" printf 'gib:x:1000:2000::%s:/bin/bash\n' "$home"
''') ''')
write_executable(stub_dir / "stat", common + r''' write_executable(stub_dir / "stat", common + r'''
log stat "$@" log stat "$@"
@@ -106,7 +124,23 @@ esac
log runuser "$@" log runuser "$@"
[[ "${1:-}" == -u && "${2:-}" == gib && "${3:-}" == -- ]] || exit 97 [[ "${1:-}" == -u && "${2:-}" == gib && "${3:-}" == -- ]] || exit 97
shift 3 shift 3
if [[ "${1:-}" == install && "${2:-}" == -d && "${3:-}" == -m && "${4:-}" == 0700 && "${5:-}" == -- ]]; then
/usr/bin/install "${@:2}"
printf '%s:700\n' "$(<"$PANAMA_BOOT_FIXTURE_ROOT/state/target-uid")" \
>"$PANAMA_BOOT_FIXTURE_ROOT/state/target-dir-meta"
exit 0
fi
if [[ "${1:-}" == install && "${2:-}" == -m && "${3:-}" == 0600 && "${4:-}" == -- ]]; then
/usr/bin/install "${@:2}"
printf '%s:600\n' "$(<"$PANAMA_BOOT_FIXTURE_ROOT/state/target-uid")" \
>"$PANAMA_BOOT_FIXTURE_ROOT/state/target-key-meta"
exit 0
fi
"$@" "$@"
''')
write_executable(stub_dir / "ssh-keygen", common + r'''
log ssh-keygen "$@"
exec /usr/bin/ssh-keygen "$@"
''') ''')
write_executable(stub_dir / "git", common + r''' write_executable(stub_dir / "git", common + r'''
log git "$@" log git "$@"
@@ -126,12 +160,29 @@ log dnf "$@"
''') ''')
write_executable(stub_dir / "sshd", common + r''' write_executable(stub_dir / "sshd", common + r'''
log sshd "$@" log sshd "$@"
[[ "$#" -eq 1 && "$1" == -t ]] || exit 97 case "${1:-}" in
for artifact in "$PANAMA_BOOT_FIXTURE_ROOT/etc/ssh/sshd_config.d"/.90-panama.*; do -t)
[[ -e "$artifact" ]] || continue [[ "$#" -eq 1 ]] || exit 97
log ssh-artifact "$artifact" "$(/usr/bin/stat -c %a -- "$artifact")" for artifact in "$PANAMA_BOOT_FIXTURE_ROOT/etc/ssh/sshd_config.d"/.00-panama.*; do
done [[ -e "$artifact" ]] || continue
consume_result SSHD_RESULTS log ssh-artifact "$artifact" "$(/usr/bin/stat -c %a -- "$artifact")"
done
consume_result SSHD_RESULTS
;;
-T)
[[ "$#" -eq 3 && "$2" == -C ]] || exit 97
case "$3" in
user=root,host=localhost,addr=127.0.0.1)
cat "$PANAMA_BOOT_FIXTURE_ROOT/state/ROOT_POLICY"
;;
user=gib,host=localhost,addr=127.0.0.1)
cat "$PANAMA_BOOT_FIXTURE_ROOT/state/TARGET_POLICY"
;;
*) exit 97 ;;
esac
;;
*) exit 97 ;;
esac
''') ''')
write_executable(stub_dir / "systemctl", common + r''' write_executable(stub_dir / "systemctl", common + r'''
log systemctl "$@" log systemctl "$@"
@@ -141,6 +192,40 @@ case "${1:-}:${2:-}" in
reload:sshd.service|reload:ssh.service) consume_result RELOAD_RESULTS ;; reload:sshd.service|reload:ssh.service) consume_result RELOAD_RESULTS ;;
*) exit 97 ;; *) exit 97 ;;
esac esac
''')
write_executable(stub_dir / "mktemp", common + r'''
log mktemp "$@"
artifact="$(/usr/bin/mktemp "$@")" || exit
case "$artifact" in
*.tmp)
if [[ -e "$PANAMA_BOOT_FIXTURE_ROOT/state/HOLD_CANDIDATE_PREPARATION" ]]; then
: >"$PANAMA_BOOT_FIXTURE_ROOT/state/CANDIDATE_PREPARING"
while [[ ! -e "$PANAMA_BOOT_FIXTURE_ROOT/state/RELEASE_CANDIDATE_PREPARATION" ]]; do
/usr/bin/sleep 0.01
done
fi
;;
*.backup)
if [[ -e "$PANAMA_BOOT_FIXTURE_ROOT/state/HOLD_BACKUP_MKTEMP" ]]; then
: >"$PANAMA_BOOT_FIXTURE_ROOT/state/BACKUP_MKTEMP_RUNNING"
while [[ ! -e "$PANAMA_BOOT_FIXTURE_ROOT/state/RELEASE_BACKUP_MKTEMP" ]]; do
/usr/bin/sleep 0.01
done
fi
;;
esac
printf '%s\n' "$artifact"
''')
write_executable(stub_dir / "cp", common + r'''
log cp "$@"
destination="${@: -1}"
if [[ "$destination" == *.backup && -e "$PANAMA_BOOT_FIXTURE_ROOT/state/HOLD_BACKUP_PREPARATION" ]]; then
: >"$PANAMA_BOOT_FIXTURE_ROOT/state/BACKUP_PREPARING"
while [[ ! -e "$PANAMA_BOOT_FIXTURE_ROOT/state/RELEASE_BACKUP_PREPARATION" ]]; do
/usr/bin/sleep 0.01
done
fi
exec /usr/bin/cp "$@"
''') ''')
write_executable(stub_dir / "mv", common + r''' write_executable(stub_dir / "mv", common + r'''
log mv "$@" "source-mode=$(/usr/bin/stat -c %a -- "${3:-}")" log mv "$@" "source-mode=$(/usr/bin/stat -c %a -- "${3:-}")"
@@ -176,9 +261,14 @@ if [[ "${1:-}" == -f && "${2:-}" == -- && "${3:-}" == *.tmp ]]; then
result=$? result=$?
(( result == 0 )) || exit "$result" (( result == 0 )) || exit "$result"
fi fi
if [[ "${1:-}" == -f && "${2:-}" == -- && "${3:-}" == */00-panama.conf ]]; then
consume_result RM_DROPIN_RESULTS
result=$?
(( result == 0 )) || exit "$result"
fi
exec /usr/bin/rm "$@" exec /usr/bin/rm "$@"
''') ''')
for command in ("useradd", "usermod"): for command in ("chown", "useradd", "usermod"):
write_executable(stub_dir / command, common + f'''\nlog {command} "$@"\nexit 97\n''') write_executable(stub_dir / command, common + f'''\nlog {command} "$@"\nexit 97\n''')
@@ -190,11 +280,24 @@ def configure_case(
mv_activation_results: tuple[int, ...] = (), mv_activation_results: tuple[int, ...] = (),
rm_backup_results: tuple[int, ...] = (), rm_backup_results: tuple[int, ...] = (),
rm_candidate_results: tuple[int, ...] = (), rm_candidate_results: tuple[int, ...] = (),
rm_dropin_results: tuple[int, ...] = (),
prior_dropin: bytes | None = None, prior_dropin: bytes | None = None,
prior_dropin_kind: str = "regular",
root_policy: str = (
"permitrootlogin no\n"
"passwordauthentication no\n"
"kbdinteractiveauthentication no\n"
),
target_policy: str = (
"passwordauthentication no\n"
"kbdinteractiveauthentication no\n"
),
sshd_unit: bool = True, sshd_unit: bool = True,
ssh_unit: bool = True, ssh_unit: bool = True,
hold_activation: bool = False, hold_activation: bool = False,
hold_before_activation: bool = False, hold_before_activation: bool = False,
hold_candidate_preparation: bool = False,
hold_backup_preparation: bool = False,
) -> tuple[Path, Path]: ) -> tuple[Path, Path]:
fixture_root = work / name / "root" fixture_root = work / name / "root"
stub_dir = work / name / "bin" stub_dir = work / name / "bin"
@@ -224,22 +327,45 @@ def configure_case(
(state / "RM_CANDIDATE_RESULTS").write_text( (state / "RM_CANDIDATE_RESULTS").write_text(
"".join(f"{result}\n" for result in rm_candidate_results) "".join(f"{result}\n" for result in rm_candidate_results)
) )
(state / "RM_DROPIN_RESULTS").write_text(
"".join(f"{result}\n" for result in rm_dropin_results)
)
(state / "ROOT_POLICY").write_text(root_policy)
(state / "TARGET_POLICY").write_text(target_policy)
(state / "SSHD_UNIT").write_text("present\n" if sshd_unit else "absent\n") (state / "SSHD_UNIT").write_text("present\n" if sshd_unit else "absent\n")
(state / "SSH_UNIT").write_text("present\n" if ssh_unit else "absent\n") (state / "SSH_UNIT").write_text("present\n" if ssh_unit else "absent\n")
if hold_activation: if hold_activation:
(state / "HOLD_ACTIVATION").touch() (state / "HOLD_ACTIVATION").touch()
if hold_before_activation: if hold_before_activation:
(state / "HOLD_BEFORE_ACTIVATION").touch() (state / "HOLD_BEFORE_ACTIVATION").touch()
if hold_candidate_preparation:
(state / "HOLD_CANDIDATE_PREPARATION").touch()
if hold_backup_preparation:
(state / "HOLD_BACKUP_PREPARATION").touch()
(fixture_root / "stub-install").write_text( (fixture_root / "stub-install").write_text(
"#!/usr/bin/env bash\nprintf 'install-handoff %s\\n' \"${PANAMA_PATH:-unset}\" >> \"$PANAMA_BOOT_FIXTURE_ROOT/calls\"\n" "#!/usr/bin/env bash\nprintf 'install-handoff %s\\n' \"${PANAMA_PATH:-unset}\" >> \"$PANAMA_BOOT_FIXTURE_ROOT/calls\"\n"
) )
(fixture_root / "stub-install").chmod(0o755) (fixture_root / "stub-install").chmod(0o755)
make_stubs(stub_dir, fixture_root, calls) make_stubs(stub_dir, fixture_root, calls)
if prior_dropin is not None: dropin = fixture_root / "etc/ssh/sshd_config.d/00-panama.conf"
dropin = fixture_root / "etc/ssh/sshd_config.d/90-panama.conf" if prior_dropin_kind != "regular":
if prior_dropin_kind == "symlink":
symlink_target = fixture_root / "unsupported-panama-dropin"
symlink_target.write_bytes(prior_dropin or b"unsupported symlink target\n")
dropin.symlink_to(symlink_target)
elif prior_dropin_kind == "directory":
dropin.mkdir()
elif prior_dropin_kind == "fifo":
os.mkfifo(dropin)
else:
raise ValueError(prior_dropin_kind)
elif prior_dropin is not None:
dropin.write_bytes(prior_dropin) dropin.write_bytes(prior_dropin)
dropin.chmod(0o600) dropin.chmod(0o640)
os.utime(dropin, ns=(1_700_000_000_123_456_789, 1_700_000_000_123_456_789))
os.setxattr(dropin, b"user.panama-contract", b"preserve-me")
subprocess.run(["setfacl", "-m", "u:65534:r--", dropin], check=True)
target_keys = ssh_dir / "authorized_keys" target_keys = ssh_dir / "authorized_keys"
root_keys = root_ssh_dir / "authorized_keys" root_keys = root_ssh_dir / "authorized_keys"
@@ -256,31 +382,42 @@ def configure_case(
(fixture_root / "home/gib/.ssh").symlink_to(alternate) (fixture_root / "home/gib/.ssh").symlink_to(alternate)
elif name == "authorized-keys-symlink": elif name == "authorized-keys-symlink":
alternate = fixture_root / "unsafe-authorized-keys" alternate = fixture_root / "unsafe-authorized-keys"
alternate.write_text("ssh-ed25519 unsafe\n") alternate.write_text(TARGET_PUBLIC_KEY)
target_keys.symlink_to(alternate) target_keys.symlink_to(alternate)
elif name == "malformed-key":
target_keys.write_text("this is not OpenSSH key material\n")
elif name == "mixed-valid-and-malformed-key":
target_keys.write_text(TARGET_PUBLIC_KEY + "this is not OpenSSH key material\n")
elif name == "malformed-root-key":
root_keys.write_text("this is not OpenSSH key material\n")
elif name == "directory-wrong-mode": elif name == "directory-wrong-mode":
target_keys.write_text("ssh-ed25519 target\n") target_keys.write_text(TARGET_PUBLIC_KEY)
(state / "target-dir-meta").write_text("1000:755\n") (state / "target-dir-meta").write_text("1000:755\n")
elif name == "root-copy-directory-wrong-mode": elif name == "root-copy-directory-wrong-mode":
root_keys.write_text("ssh-ed25519 root\n") root_keys.write_text(ROOT_PUBLIC_KEY)
(state / "target-dir-meta").write_text("1000:755\n") (state / "target-dir-meta").write_text("1000:755\n")
elif name == "file-wrong-mode": elif name == "file-wrong-mode":
target_keys.write_text("ssh-ed25519 target\n") target_keys.write_text(TARGET_PUBLIC_KEY)
(state / "target-key-meta").write_text("1000:644\n") (state / "target-key-meta").write_text("1000:644\n")
elif name == "directory-wrong-owner": elif name == "directory-wrong-owner":
target_keys.write_text("ssh-ed25519 target\n") target_keys.write_text(TARGET_PUBLIC_KEY)
(state / "target-dir-meta").write_text("0:700\n") (state / "target-dir-meta").write_text("0:700\n")
elif name == "file-wrong-owner": elif name == "file-wrong-owner":
target_keys.write_text("ssh-ed25519 target\n") target_keys.write_text(TARGET_PUBLIC_KEY)
(state / "target-key-meta").write_text("0:600\n") (state / "target-key-meta").write_text("0:600\n")
elif name == "root-target-account": elif name == "root-target-account":
target_keys.write_text("ssh-ed25519 target\n") target_keys.write_text(TARGET_PUBLIC_KEY)
(state / "target-uid").write_text("0\n") (state / "target-uid").write_text("0\n")
elif name == "relative-home": elif name == "relative-home":
target_keys.write_text("ssh-ed25519 target\n") target_keys.write_text(TARGET_PUBLIC_KEY)
(state / "home").write_text("home/gib\n") (state / "home").write_text("home/gib\n")
elif name in ( elif name in (
"safe-existing-key", "safe-existing-key",
"declines-hardening",
"missing-ssh-unit",
"preexisting-dropin-symlink",
"preexisting-dropin-directory",
"preexisting-dropin-fifo",
"success-without-prior-dropin", "success-without-prior-dropin",
"success-replaces-prior-dropin", "success-replaces-prior-dropin",
"candidate-invalid", "candidate-invalid",
@@ -289,6 +426,9 @@ def configure_case(
"candidate-reload-fails-without-prior", "candidate-reload-fails-without-prior",
"rollback-validation-fails", "rollback-validation-fails",
"rollback-reload-fails", "rollback-reload-fails",
"rollback-removal-fails-without-prior",
"effective-root-policy-conflict",
"effective-target-policy-conflict",
"success-backup-cleanup-fails", "success-backup-cleanup-fails",
"rollback-backup-cleanup-fails", "rollback-backup-cleanup-fails",
"signal-int-restores-prior", "signal-int-restores-prior",
@@ -297,10 +437,15 @@ def configure_case(
"signal-int-before-activation-prior", "signal-int-before-activation-prior",
"signal-term-before-activation-no-prior", "signal-term-before-activation-no-prior",
"signal-int-before-activation-cleanup-fails", "signal-int-before-activation-cleanup-fails",
"signal-int-during-candidate-preparation",
"signal-term-during-backup-preparation",
): ):
target_keys.write_text("ssh-ed25519 target\n") target_keys.write_text(TARGET_PUBLIC_KEY)
elif name == "safe-root-key-copy": elif name == "safe-root-key-copy":
root_keys.write_text("ssh-ed25519 root\n") shutil.rmtree(ssh_dir)
(state / "target-dir-meta").write_text("missing\n")
(state / "target-key-meta").write_text("missing\n")
root_keys.write_text(ROOT_PUBLIC_KEY)
else: else:
raise ValueError(name) raise ValueError(name)
return fixture_root, stub_dir return fixture_root, stub_dir
@@ -311,6 +456,9 @@ def run_case(
*, *,
signal_after_activation: int | None = None, signal_after_activation: int | None = None,
signal_before_activation: int | None = None, signal_before_activation: int | None = None,
signal_during_candidate_preparation: int | None = None,
signal_during_backup_preparation: int | None = None,
harden_answer: str = "Y",
prior_traps: bool = False, prior_traps: bool = False,
**configuration: object, **configuration: object,
) -> tuple[int, str, str, Path, int]: ) -> tuple[int, str, str, Path, int]:
@@ -318,6 +466,8 @@ def run_case(
name, name,
hold_activation=signal_after_activation is not None, hold_activation=signal_after_activation is not None,
hold_before_activation=signal_before_activation is not None, hold_before_activation=signal_before_activation is not None,
hold_candidate_preparation=signal_during_candidate_preparation is not None,
hold_backup_preparation=signal_during_backup_preparation is not None,
**configuration, **configuration,
) )
master, slave = pty.openpty() master, slave = pty.openpty()
@@ -358,8 +508,28 @@ fi
preexec_fn=attach_terminal, preexec_fn=attach_terminal,
) )
os.close(slave) os.close(slave)
os.write(master, b"gib\nY\n") os.write(master, f"gib\n{harden_answer}\n".encode())
if signal_before_activation is not None: if signal_during_candidate_preparation is not None:
marker = fixture_root / "state/CANDIDATE_PREPARING"
deadline = time.monotonic() + 5
while not marker.exists() and process.poll() is None and time.monotonic() < deadline:
time.sleep(0.01)
if not marker.exists():
note(f"{name}: fixture did not observe candidate preparation before signaling")
else:
os.kill(process.pid, signal_during_candidate_preparation)
(fixture_root / "state/RELEASE_CANDIDATE_PREPARATION").touch()
elif signal_during_backup_preparation is not None:
marker = fixture_root / "state/BACKUP_PREPARING"
deadline = time.monotonic() + 5
while not marker.exists() and process.poll() is None and time.monotonic() < deadline:
time.sleep(0.01)
if not marker.exists():
note(f"{name}: fixture did not observe backup preparation before signaling")
else:
os.kill(process.pid, signal_during_backup_preparation)
(fixture_root / "state/RELEASE_BACKUP_PREPARATION").touch()
elif signal_before_activation is not None:
armed = fixture_root / "state/TRANSACTION_ARMED" armed = fixture_root / "state/TRANSACTION_ARMED"
deadline = time.monotonic() + 5 deadline = time.monotonic() + 5
while not armed.exists() and process.poll() is None and time.monotonic() < deadline: while not armed.exists() and process.poll() is None and time.monotonic() < deadline:
@@ -380,7 +550,19 @@ fi
os.kill(process.pid, signal_after_activation) os.kill(process.pid, signal_after_activation)
(fixture_root / "state/RELEASE_ACTIVATION").touch() (fixture_root / "state/RELEASE_ACTIVATION").touch()
chunks: list[bytes] = [] chunks: list[bytes] = []
deadline = time.monotonic() + 8
timed_out = False
while True: while True:
readable, _, _ = select.select([master], [], [], 0.1)
if not readable:
if process.poll() is not None:
break
if time.monotonic() >= deadline:
timed_out = True
os.killpg(process.pid, signal.SIGKILL)
process.wait()
continue
continue
try: try:
chunk = os.read(master, 4096) chunk = os.read(master, 4096)
except OSError as error: except OSError as error:
@@ -392,15 +574,53 @@ fi
chunks.append(chunk) chunks.append(chunk)
os.close(master) os.close(master)
status = process.wait() status = process.wait()
if timed_out:
note(f"{name}: bootstrap timed out, likely while reading an unsupported object")
calls = (fixture_root / "calls").read_text() calls = (fixture_root / "calls").read_text()
output = b"".join(chunks).decode(errors="replace") output = b"".join(chunks).decode(errors="replace")
return status, output, calls, fixture_root, process.pid return status, output, calls, fixture_root, process.pid
guard_root = work / "actual-root-fixture-guard"
guard_root.mkdir()
guard_env = {
**os.environ,
"PANAMA_BOOT_FIXTURE_ROOT": str(guard_root),
"HOME": str(guard_root),
}
if os.geteuid() == 0:
guard_command = ["bash", boot, "--server"]
else:
guard_command = ["unshare", "--user", "--map-root-user", "--", "bash", boot, "--server"]
try:
guard_result = subprocess.run(
guard_command,
env=guard_env,
capture_output=True,
text=True,
timeout=5,
)
except (FileNotFoundError, subprocess.TimeoutExpired) as error:
note(f"actual-root-fixture-guard: could not create a hermetic root process: {error}")
else:
if guard_result.returncode != 1:
note(
"actual-root-fixture-guard: actual root did not reject "
f"PANAMA_BOOT_FIXTURE_ROOT with status 1: {guard_result.returncode}"
)
if "PANAMA_BOOT_FIXTURE_ROOT is test-only" not in guard_result.stderr:
note("actual-root-fixture-guard: rejection diagnostic was missing")
if any(guard_root.iterdir()):
note("actual-root-fixture-guard: boot mutated its rejected fixture root")
unsafe_cases = ( unsafe_cases = (
"missing", "missing",
"empty", "empty",
"comment-only", "comment-only",
"malformed-key",
"mixed-valid-and-malformed-key",
"malformed-root-key",
"ssh-directory-symlink", "ssh-directory-symlink",
"authorized-keys-symlink", "authorized-keys-symlink",
"directory-wrong-mode", "directory-wrong-mode",
@@ -421,7 +641,7 @@ for case in unsafe_cases:
note(f"{case}: unsafe login path validated sshd") note(f"{case}: unsafe login path validated sshd")
if "systemctl reload" in calls: if "systemctl reload" in calls:
note(f"{case}: unsafe login path reloaded SSH") note(f"{case}: unsafe login path reloaded SSH")
if (fixture_root / "etc/ssh/sshd_config.d/90-panama.conf").exists(): if (fixture_root / "etc/ssh/sshd_config.d/00-panama.conf").exists():
note(f"{case}: unsafe login path changed the SSH drop-in") note(f"{case}: unsafe login path changed the SSH drop-in")
if case == "root-copy-directory-wrong-mode" and ( if case == "root-copy-directory-wrong-mode" and (
fixture_root / "home/gib/.ssh/authorized_keys" fixture_root / "home/gib/.ssh/authorized_keys"
@@ -430,26 +650,132 @@ for case in unsafe_cases:
if "install-handoff " not in calls: if "install-handoff " not in calls:
note(f"{case}: unsafe login path did not hand off to install") note(f"{case}: unsafe login path did not hand off to install")
desired_dropin = (
b"PermitRootLogin no\n"
b"PasswordAuthentication no\n"
b"KbdInteractiveAuthentication no\n"
)
prior_dropin = b"# prior Panama settings\nPasswordAuthentication yes\n"
for case in ("safe-existing-key", "safe-root-key-copy"): for case in ("safe-existing-key", "safe-root-key-copy"):
status, output, calls, fixture_root, _ = run_case(case) status, output, calls, fixture_root, _ = run_case(case)
if status != 0: if status != 0:
note(f"{case}: safe login path stopped with status {status}: {output.strip()}") note(f"{case}: safe login path stopped with status {status}: {output.strip()}")
if "SSH hardening unavailable" in output: if "SSH hardening unavailable" in output:
note(f"{case}: safe login path was rejected") note(f"{case}: safe login path was rejected: {output.strip()} | {calls.strip()}")
if "systemctl reload" not in calls: if "systemctl reload" not in calls:
note(f"{case}: safe login path did not reach SSH hardening") note(f"{case}: safe login path did not reach SSH hardening")
if "install-handoff " not in calls: if "install-handoff " not in calls:
note(f"{case}: safe login path did not hand off to install") note(f"{case}: safe login path did not hand off to install")
dropin = fixture_root / "etc/ssh/sshd_config.d/90-panama.conf" dropin = fixture_root / "etc/ssh/sshd_config.d/00-panama.conf"
if (dropin.read_text() if dropin.exists() else "") != "PermitRootLogin no\nPasswordAuthentication no\n": if (dropin.read_bytes() if dropin.exists() else None) != desired_dropin:
note(f"{case}: safe login path did not write the expected SSH drop-in") note(f"{case}: safe login path did not write the expected SSH drop-in")
if case == "safe-root-key-copy": if case == "safe-root-key-copy":
keys = fixture_root / "home/gib/.ssh/authorized_keys" keys = fixture_root / "home/gib/.ssh/authorized_keys"
if not keys.exists() or keys.read_text() != "ssh-ed25519 root\n": ssh_dir = keys.parent
if not keys.exists() or keys.read_text() != ROOT_PUBLIC_KEY:
note("safe-root-key-copy: root key was not copied to the target account") note("safe-root-key-copy: root key was not copied to the target account")
if keys.exists() and (
ssh_dir.stat().st_mode & 0o777 != 0o700
or keys.stat().st_mode & 0o777 != 0o600
):
note("safe-root-key-copy: destination modes were not normalized to 0700/0600")
call_lines = calls.splitlines()
install_dir = (
f"runuser -u gib -- install -d -m 0700 -- {ssh_dir} "
)
install_key = (
"runuser -u gib -- install -m 0600 -- "
f"/dev/stdin {keys} "
)
if install_dir not in call_lines or install_key not in call_lines:
note("safe-root-key-copy: destination creation and writing did not run as the target user")
else:
validation_indices = [
index
for index, line in enumerate(call_lines)
if line.startswith("ssh-keygen -l -f ")
]
if not validation_indices or max(validation_indices) < call_lines.index(install_key):
note("safe-root-key-copy: copied key validity was not rechecked after installation")
if any(line.startswith("chown ") for line in call_lines):
note("safe-root-key-copy: bootstrap still assumes the primary group matches the username")
status, output, calls, fixture_root, _ = run_case(
"declines-hardening",
harden_answer="n",
)
declined_dropin = fixture_root / "etc/ssh/sshd_config.d/00-panama.conf"
if status != 0 or "install-handoff " not in calls:
note("declines-hardening: declining did not continue to install")
if declined_dropin.exists() or "sshd " in calls or "systemctl reload " in calls:
note("declines-hardening: declining changed or validated SSH state")
status, output, calls, fixture_root, _ = run_case(
"missing-ssh-unit",
sshd_unit=False,
ssh_unit=False,
)
missing_unit_dropin = fixture_root / "etc/ssh/sshd_config.d/00-panama.conf"
if status != 0 or "install-handoff " not in calls:
note("missing-ssh-unit: unavailable hardening did not continue to install")
if "SSH hardening unavailable" not in output:
note("missing-ssh-unit: missing units did not explain that hardening was unavailable")
if missing_unit_dropin.exists() or "sshd " in calls or "systemctl reload " in calls:
note("missing-ssh-unit: unavailable hardening changed or validated SSH state")
unsupported_dropins = {
"preexisting-dropin-symlink": "symlink",
"preexisting-dropin-directory": "directory",
"preexisting-dropin-fifo": "fifo",
}
for case, kind in unsupported_dropins.items():
status, output, calls, fixture_root, _ = run_case(
case,
prior_dropin=prior_dropin,
prior_dropin_kind=kind,
)
dropin = fixture_root / "etc/ssh/sshd_config.d/00-panama.conf"
if status != 0 or "install-handoff " not in calls:
note(f"{case}: unsupported object did not continue to install")
if "SSH hardening unavailable" not in output:
note(f"{case}: unsupported object did not explain that hardening was unavailable")
if "sshd " in calls or "systemctl reload " in calls:
note(f"{case}: unsupported object reached SSH validation or reload")
if kind == "symlink" and not dropin.is_symlink():
note(f"{case}: pre-existing symlink was changed")
if kind == "directory" and not dropin.is_dir():
note(f"{case}: pre-existing directory was changed")
if kind == "fifo" and not stat_module.S_ISFIFO(dropin.lstat().st_mode):
note(f"{case}: pre-existing FIFO was changed")
def regular_metadata(path: Path) -> tuple[object, ...]:
metadata = path.stat()
xattrs = tuple((name, os.getxattr(path, name)) for name in sorted(os.listxattr(path)))
acl = subprocess.check_output(["getfacl", "-cp", path])
return (
stat_module.S_IMODE(metadata.st_mode),
metadata.st_uid,
metadata.st_gid,
metadata.st_mtime_ns,
xattrs,
acl,
)
metadata_reference = work / "prior-dropin-metadata-reference"
metadata_reference.write_bytes(prior_dropin)
metadata_reference.chmod(0o640)
os.utime(
metadata_reference,
ns=(1_700_000_000_123_456_789, 1_700_000_000_123_456_789),
)
os.setxattr(metadata_reference, b"user.panama-contract", b"preserve-me")
subprocess.run(["setfacl", "-m", "u:65534:r--", metadata_reference], check=True)
expected_prior_metadata = regular_metadata(metadata_reference)
desired_dropin = b"PermitRootLogin no\nPasswordAuthentication no\n"
prior_dropin = b"# prior Panama settings\nPasswordAuthentication yes\n"
transaction_cases = { transaction_cases = {
"success-without-prior-dropin": { "success-without-prior-dropin": {
"sshd_results": (0,), "sshd_results": (0,),
@@ -517,17 +843,53 @@ transaction_cases = {
"succeeds": False, "succeeds": False,
"rollback_fails": True, "rollback_fails": True,
}, },
"rollback-removal-fails-without-prior": {
"sshd_results": (1, 1),
"reload_results": (),
"rm_dropin_results": (1,),
"prior_dropin": None,
"sshd_unit": True,
"ssh_unit": True,
"succeeds": False,
"rollback_fails": True,
"settled_dropin": desired_dropin,
},
"effective-root-policy-conflict": {
"sshd_results": (0, 0),
"reload_results": (),
"prior_dropin": prior_dropin,
"root_policy": (
"permitrootlogin yes\n"
"passwordauthentication no\n"
"kbdinteractiveauthentication no\n"
),
"sshd_unit": True,
"ssh_unit": True,
"succeeds": False,
},
"effective-target-policy-conflict": {
"sshd_results": (0, 0),
"reload_results": (),
"prior_dropin": prior_dropin,
"target_policy": (
"passwordauthentication no\n"
"kbdinteractiveauthentication yes\n"
),
"sshd_unit": True,
"ssh_unit": True,
"succeeds": False,
},
} }
for case, expected in transaction_cases.items(): for case, expected in transaction_cases.items():
configuration = { configuration = {
key: value key: value
for key, value in expected.items() for key, value in expected.items()
if key not in {"succeeds", "rollback_fails"} if key not in {"succeeds", "rollback_fails", "settled_dropin"}
} }
status, output, calls, fixture_root, _ = run_case(case, **configuration) status, output, calls, fixture_root, _ = run_case(case, **configuration)
call_lines = calls.splitlines() call_lines = calls.splitlines()
dropin = fixture_root / "etc/ssh/sshd_config.d/90-panama.conf" dropin = fixture_root / "etc/ssh/sshd_config.d/00-panama.conf"
sshd_dir = dropin.parent sshd_dir = dropin.parent
validations = [index for index, line in enumerate(call_lines) if line.startswith("sshd -t ")] validations = [index for index, line in enumerate(call_lines) if line.startswith("sshd -t ")]
reloads = [ reloads = [
@@ -539,7 +901,7 @@ for case, expected in transaction_cases.items():
(index, line) (index, line)
for index, line in enumerate(call_lines) for index, line in enumerate(call_lines)
if re.fullmatch( if re.fullmatch(
rf"mv -f -- {re.escape(str(sshd_dir))}/\.90-panama\.[A-Za-z0-9]+\.tmp " rf"mv -f -- {re.escape(str(sshd_dir))}/\.00-panama\.[A-Za-z0-9]+\.tmp "
rf"{re.escape(str(dropin))} source-mode=600 ", rf"{re.escape(str(dropin))} source-mode=600 ",
line, line,
) )
@@ -548,8 +910,8 @@ for case, expected in transaction_cases.items():
index index
for index, line in enumerate(call_lines) for index, line in enumerate(call_lines)
if re.fullmatch( if re.fullmatch(
rf"mv -f -- {re.escape(str(sshd_dir))}/\.90-panama\.[A-Za-z0-9]+\.restore " rf"mv -f -- {re.escape(str(sshd_dir))}/\.00-panama\.[A-Za-z0-9]+\.restore "
rf"{re.escape(str(dropin))} source-mode=600 ", rf"{re.escape(str(dropin))} source-mode=640 ",
line, line,
) )
] ]
@@ -570,31 +932,86 @@ for case, expected in transaction_cases.items():
if not succeeds and "install-handoff " in calls: if not succeeds and "install-handoff " in calls:
note(f"{case}: failed transaction handed off to install") note(f"{case}: failed transaction handed off to install")
wanted_contents = desired_dropin if succeeds else expected["prior_dropin"] wanted_contents = (
desired_dropin
if succeeds
else expected.get("settled_dropin", expected["prior_dropin"])
)
actual_contents = dropin.read_bytes() if dropin.exists() else None actual_contents = dropin.read_bytes() if dropin.exists() else None
if actual_contents != wanted_contents: if actual_contents != wanted_contents:
note(f"{case}: SSH drop-in contents were not {'activated' if succeeds else 'restored'}") note(f"{case}: SSH drop-in contents were not {'activated' if succeeds else 'restored'}")
if len(activation_lines) != 1: if len(activation_lines) != 1:
note(f"{case}: candidate was not activated once through a restrictive same-directory rename") note(f"{case}: candidate was not activated once through a restrictive same-directory rename")
root_policy_lines = [
index
for index, line in enumerate(call_lines)
if line == r"sshd -T -C user=root\,host=localhost\,addr=127.0.0.1 "
]
target_policy_lines = [
index
for index, line in enumerate(call_lines)
if line == r"sshd -T -C user=gib\,host=localhost\,addr=127.0.0.1 "
]
if case in {
"candidate-invalid",
"candidate-invalid-without-prior",
"rollback-removal-fails-without-prior",
}:
expected_policy_users: tuple[str, ...] = ()
elif case == "effective-root-policy-conflict":
expected_policy_users = ("root",)
else:
expected_policy_users = ("root", "gib")
if len(root_policy_lines) != (1 if "root" in expected_policy_users else 0):
note(f"{case}: effective root policy validation count was wrong")
if len(target_policy_lines) != (1 if "gib" in expected_policy_users else 0):
note(f"{case}: effective target policy validation count was wrong")
if succeeds: if succeeds:
if len(validations) != 1 or len(reloads) != 1: if len(validations) != 1 or len(reloads) != 1:
note(f"{case}: success did not validate once and reload once") note(f"{case}: success did not validate once and reload once")
elif activation_lines and not activation_lines[0][0] < validations[0] < reloads[0]: elif root_policy_lines and target_policy_lines and activation_lines and not (
note(f"{case}: success did not activate, validate, then reload") activation_lines[0][0]
elif case in {"candidate-invalid", "candidate-invalid-without-prior"}: < validations[0]
if len(validations) != 2 or reloads: < root_policy_lines[0]
note(f"{case}: invalid candidate did not validate candidate and restoration without reload") < target_policy_lines[0]
elif activation_lines and rollback_lines and not ( < reloads[0]
activation_lines[0][0] < validations[0] < rollback_lines[0] < validations[1]
): ):
note(f"{case}: rollback command order was wrong") note(f"{case}: success did not activate, validate syntax and effective policy, then reload")
elif case in {
"candidate-invalid",
"candidate-invalid-without-prior",
"rollback-removal-fails-without-prior",
"effective-root-policy-conflict",
"effective-target-policy-conflict",
}:
if len(validations) != 2 or reloads:
note(f"{case}: rejected candidate did not validate candidate and restoration without reload")
elif activation_lines and rollback_lines:
policy_order = [
*root_policy_lines,
*target_policy_lines,
]
if not (
activation_lines[0][0]
< validations[0]
< (policy_order[0] if policy_order else rollback_lines[0])
and all(
left < right
for left, right in zip(policy_order, [*policy_order[1:], rollback_lines[0]])
)
and rollback_lines[0] < validations[1]
):
note(f"{case}: rollback command order was wrong")
else: else:
if len(validations) != 2 or len(reloads) != 2: if len(validations) != 2 or len(reloads) != 2:
note(f"{case}: reload failure did not validate and reload the restored configuration") note(f"{case}: reload failure did not validate and reload the restored configuration")
elif activation_lines and rollback_lines and not ( elif activation_lines and rollback_lines and root_policy_lines and target_policy_lines and not (
activation_lines[0][0] activation_lines[0][0]
< validations[0] < validations[0]
< root_policy_lines[0]
< target_policy_lines[0]
< reloads[0] < reloads[0]
< rollback_lines[0] < rollback_lines[0]
< validations[1] < validations[1]
@@ -607,6 +1024,9 @@ for case, expected in transaction_cases.items():
if not succeeds: if not succeeds:
if len(rollback_lines) != 1: if len(rollback_lines) != 1:
note(f"{case}: pre-transaction SSH state was not restored exactly once") note(f"{case}: pre-transaction SSH state was not restored exactly once")
if expected["prior_dropin"] is not None and dropin.exists():
if regular_metadata(dropin) != expected_prior_metadata:
note(f"{case}: rollback did not restore complete regular-file metadata")
detected_unit = "ssh.service" if case == "success-replaces-prior-dropin" else "sshd.service" detected_unit = "ssh.service" if case == "success-replaces-prior-dropin" else "sshd.service"
other_unit = "sshd.service" if detected_unit == "ssh.service" else "ssh.service" other_unit = "sshd.service" if detected_unit == "ssh.service" else "ssh.service"
@@ -628,20 +1048,26 @@ for case, expected in transaction_cases.items():
): ):
note(f"{case}: did not fall back from absent sshd.service to ssh.service") note(f"{case}: did not fall back from absent sshd.service to ssh.service")
artifacts = list(sshd_dir.glob(".90-panama.*")) artifacts = list(sshd_dir.glob(".00-panama.*"))
rollback_fails = bool(expected.get("rollback_fails", False)) rollback_fails = bool(expected.get("rollback_fails", False))
if not rollback_fails and artifacts: if not rollback_fails and artifacts:
note(f"{case}: successful or cleanly rolled-back transaction left temporary artifacts") note(f"{case}: successful or cleanly rolled-back transaction left temporary artifacts")
if rollback_fails: if rollback_fails:
backups = [artifact for artifact in artifacts if artifact.name.endswith(".backup")] backups = [artifact for artifact in artifacts if artifact.name.endswith(".backup")]
if len(backups) != 1: if expected["prior_dropin"] is None:
if backups:
note(f"{case}: no-prior-file recovery retained a nonexistent backup")
expected_remove = f"rm -f -- {dropin.resolve()}"
if expected_remove not in output or "cp -a --" in output:
note(f"{case}: no-prior-file recovery did not instruct removal of the installed drop-in")
elif len(backups) != 1:
note(f"{case}: rollback failure did not retain exactly one backup") note(f"{case}: rollback failure did not retain exactly one backup")
else: else:
backup = backups[0] backup = backups[0]
if backup.parent != sshd_dir or backup.read_bytes() != prior_dropin: if backup.parent != sshd_dir or backup.read_bytes() != prior_dropin:
note(f"{case}: retained backup was not a same-directory byte copy") note(f"{case}: retained backup was not a same-directory copy")
if backup.stat().st_mode & 0o777 != 0o600: if regular_metadata(backup) != expected_prior_metadata:
note(f"{case}: retained backup permissions were not restrictive") note(f"{case}: retained backup did not preserve complete regular-file metadata")
if str(backup.resolve()) not in output: if str(backup.resolve()) not in output:
note(f"{case}: recovery output omitted the absolute backup path") note(f"{case}: recovery output omitted the absolute backup path")
if "sshd -t" not in output or f"systemctl reload {detected_unit}" not in output: if "sshd -t" not in output or f"systemctl reload {detected_unit}" not in output:
@@ -650,7 +1076,7 @@ for case, expected in transaction_cases.items():
artifact_logs = [line for line in call_lines if line.startswith("ssh-artifact ")] artifact_logs = [line for line in call_lines if line.startswith("ssh-artifact ")]
if expected["prior_dropin"] is not None and not any( if expected["prior_dropin"] is not None and not any(
re.fullmatch( re.fullmatch(
rf"ssh-artifact {re.escape(str(sshd_dir))}/\.90-panama\.[A-Za-z0-9]+\.backup 600 ", rf"ssh-artifact {re.escape(str(sshd_dir))}/\.00-panama\.[A-Za-z0-9]+\.backup 640 ",
line, line,
) )
for line in artifact_logs for line in artifact_logs
@@ -684,8 +1110,8 @@ for case, expected in cleanup_failure_cases.items():
rm_backup_results=expected["rm_backup_results"], rm_backup_results=expected["rm_backup_results"],
prior_dropin=prior_dropin, prior_dropin=prior_dropin,
) )
dropin = fixture_root / "etc/ssh/sshd_config.d/90-panama.conf" dropin = fixture_root / "etc/ssh/sshd_config.d/00-panama.conf"
backups = list(dropin.parent.glob(".90-panama.*.backup")) backups = list(dropin.parent.glob(".00-panama.*.backup"))
if status == 0: if status == 0:
note(f"{case}: cleanup failure returned success") note(f"{case}: cleanup failure returned success")
if "install-handoff " in calls: if "install-handoff " in calls:
@@ -696,20 +1122,27 @@ for case, expected in cleanup_failure_cases.items():
note(f"{case}: cleanup failure validation count was wrong") note(f"{case}: cleanup failure validation count was wrong")
if calls.count("systemctl reload sshd.service \n") != expected["expected_reloads"]: if calls.count("systemctl reload sshd.service \n") != expected["expected_reloads"]:
note(f"{case}: cleanup failure reload count was wrong") note(f"{case}: cleanup failure reload count was wrong")
expected_policy_checks = 1 if case == "success-backup-cleanup-fails" else 0
if calls.count("sshd -T -C user=root\\,host=localhost\\,addr=127.0.0.1 \n") != expected_policy_checks:
note(f"{case}: cleanup failure root policy validation count was wrong")
if calls.count("sshd -T -C user=gib\\,host=localhost\\,addr=127.0.0.1 \n") != expected_policy_checks:
note(f"{case}: cleanup failure target policy validation count was wrong")
if len(backups) != 1: if len(backups) != 1:
note(f"{case}: failed cleanup did not retain exactly one backup") note(f"{case}: failed cleanup did not retain exactly one backup")
else: else:
backup = backups[0] backup = backups[0]
if str(backup.resolve()) not in output or "rm -f --" not in output: if str(backup.resolve()) not in output or "rm -f --" not in output:
note(f"{case}: retained backup was not reported with an actionable cleanup command") note(f"{case}: retained backup was not reported with an actionable cleanup command")
if regular_metadata(backup) != expected_prior_metadata:
note(f"{case}: cleanup failure backup lost regular-file metadata")
status, output, calls, fixture_root, _ = run_case( status, output, calls, fixture_root, _ = run_case(
"candidate-cleanup-fails", "candidate-cleanup-fails",
mv_activation_results=(1,), mv_activation_results=(1,),
rm_candidate_results=(1,), rm_candidate_results=(1,),
) )
dropin = fixture_root / "etc/ssh/sshd_config.d/90-panama.conf" dropin = fixture_root / "etc/ssh/sshd_config.d/00-panama.conf"
candidates = list(dropin.parent.glob(".90-panama.*.tmp")) candidates = list(dropin.parent.glob(".00-panama.*.tmp"))
if status == 0: if status == 0:
note("candidate-cleanup-fails: activation cleanup failure returned success") note("candidate-cleanup-fails: activation cleanup failure returned success")
if dropin.exists(): if dropin.exists():
@@ -745,13 +1178,13 @@ for case, expected in signal_cases.items():
reload_results=(0,), reload_results=(0,),
prior_dropin=expected["prior_dropin"], prior_dropin=expected["prior_dropin"],
) )
dropin = fixture_root / "etc/ssh/sshd_config.d/90-panama.conf" dropin = fixture_root / "etc/ssh/sshd_config.d/00-panama.conf"
actual_dropin = dropin.read_bytes() if dropin.exists() else None actual_dropin = dropin.read_bytes() if dropin.exists() else None
if status != expected["status"]: if status != expected["status"]:
note(f"{case}: signal returned status {status}, expected {expected['status']}") note(f"{case}: signal returned status {status}, expected {expected['status']}")
if actual_dropin != expected["prior_dropin"]: if actual_dropin != expected["prior_dropin"]:
note(f"{case}: signal did not restore the pre-transaction SSH state") note(f"{case}: signal did not restore the pre-transaction SSH state")
if list(dropin.parent.glob(".90-panama.*")): if list(dropin.parent.glob(".00-panama.*")):
note(f"{case}: signal left transaction residue") note(f"{case}: signal left transaction residue")
if "install-handoff " in calls: if "install-handoff " in calls:
note(f"{case}: signal reached install handoff") note(f"{case}: signal reached install handoff")
@@ -768,8 +1201,8 @@ for case, expected in signal_cases.items():
if ( if (
expected["prior_dropin"] is not None expected["prior_dropin"] is not None
and re.fullmatch( and re.fullmatch(
rf"mv -f -- {re.escape(str(dropin.parent))}/\.90-panama\.[A-Za-z0-9]+\.restore " rf"mv -f -- {re.escape(str(dropin.parent))}/\.00-panama\.[A-Za-z0-9]+\.restore "
rf"{re.escape(str(dropin))} source-mode=600 ", rf"{re.escape(str(dropin))} source-mode=640 ",
line, line,
) )
) )
@@ -788,6 +1221,8 @@ for case, expected in signal_cases.items():
and rollback_indices[0] < validation_indices[0] < reload_indices[0] and rollback_indices[0] < validation_indices[0] < reload_indices[0]
): ):
note(f"{case}: signal did not restore, validate, then reload in order") note(f"{case}: signal did not restore, validate, then reload in order")
if expected["prior_dropin"] is not None and regular_metadata(dropin) != expected_prior_metadata:
note(f"{case}: signal rollback did not restore complete regular-file metadata")
pre_activation_signal_cases = { pre_activation_signal_cases = {
"signal-int-before-activation-prior": { "signal-int-before-activation-prior": {
@@ -819,10 +1254,10 @@ for case, expected in pre_activation_signal_cases.items():
prior_dropin=expected["prior_dropin"], prior_dropin=expected["prior_dropin"],
rm_candidate_results=rm_candidate_results, rm_candidate_results=rm_candidate_results,
) )
dropin = fixture_root / "etc/ssh/sshd_config.d/90-panama.conf" dropin = fixture_root / "etc/ssh/sshd_config.d/00-panama.conf"
actual_dropin = dropin.read_bytes() if dropin.exists() else None actual_dropin = dropin.read_bytes() if dropin.exists() else None
candidates = list(dropin.parent.glob(".90-panama.*.tmp")) candidates = list(dropin.parent.glob(".00-panama.*.tmp"))
backups = list(dropin.parent.glob(".90-panama.*.backup")) backups = list(dropin.parent.glob(".00-panama.*.backup"))
if status != expected["status"]: if status != expected["status"]:
note(f"{case}: signal returned status {status}, expected {expected['status']}") note(f"{case}: signal returned status {status}, expected {expected['status']}")
if actual_dropin != expected["prior_dropin"]: if actual_dropin != expected["prior_dropin"]:
@@ -848,6 +1283,50 @@ for case, expected in pre_activation_signal_cases.items():
if ".restore " in calls or f"rm -f -- {dropin} " in calls: if ".restore " in calls or f"rm -f -- {dropin} " in calls:
note(f"{case}: pre-activation signal rewrote the unchanged final drop-in") note(f"{case}: pre-activation signal rewrote the unchanged final drop-in")
preparation_signal_cases = {
"signal-int-during-candidate-preparation": {
"signal": signal.SIGINT,
"status": 130,
"prior_dropin": None,
"phase": "candidate",
},
"signal-term-during-backup-preparation": {
"signal": signal.SIGTERM,
"status": 143,
"prior_dropin": prior_dropin,
"phase": "backup",
},
}
for case, expected in preparation_signal_cases.items():
signal_arguments = (
{"signal_during_candidate_preparation": expected["signal"]}
if expected["phase"] == "candidate"
else {"signal_during_backup_preparation": expected["signal"]}
)
status, output, calls, fixture_root, boot_pid = run_case(
case,
prior_traps=True,
prior_dropin=expected["prior_dropin"],
**signal_arguments,
)
dropin = fixture_root / "etc/ssh/sshd_config.d/00-panama.conf"
actual_dropin = dropin.read_bytes() if dropin.exists() else None
if status != expected["status"]:
note(f"{case}: signal returned status {status}, expected {expected['status']}")
if actual_dropin != expected["prior_dropin"]:
note(f"{case}: preparation signal changed the final drop-in")
if list(dropin.parent.glob(".00-panama.*")):
note(f"{case}: preparation signal left candidate or backup residue")
if "install-handoff " in calls:
note(f"{case}: preparation signal reached install handoff")
if f"prior-exit {boot_pid}\n" not in calls:
note(f"{case}: preparation signal suppressed the saved EXIT trap")
if "sshd " in calls or "systemctl reload " in calls:
note(f"{case}: preparation signal validated or reloaded unchanged SSH state")
if expected["prior_dropin"] is not None and regular_metadata(dropin) != expected_prior_metadata:
note(f"{case}: preparation signal changed prior regular-file metadata")
if findings: if findings:
print(f"root server bootstrap: {len(findings)} finding(s)", file=sys.stderr) print(f"root server bootstrap: {len(findings)} finding(s)", file=sys.stderr)
for finding in findings: for finding in findings: