#!/usr/bin/env bash # Debian host baseline. No identity reset, package upgrade, reboot or firewall rewrite. set -euo pipefail export LC_ALL=C fail() { printf 'ERROR: %s\n' "$*" >&2; return 1; } trusted() { local path=$1 while [[ "$path" != / ]]; do [[ ! -L "$path" ]] || { fail "Symlink: $path"; return 1; } if [[ -e "$path" ]]; then [[ $(stat -c %u "$path") == 0 ]] && (( (8#$(stat -c %a "$path") & 0022) == 0 )) || { fail "Untrusted path: $path"; return 1; } fi path=$(dirname -- "$path") done } sysctl_conflicts() { # Local custom tuning must be reviewed before adopting the small managed profile. local file directory=$1 legacy=$2 shopt -s nullglob for file in "$directory"/*.conf "$legacy"; do [[ -e "$file" ]] || continue [[ "$file" == "$directory/90-platform-baseline.conf" ]] && continue if grep -Eq '^[[:space:]]*[^#;[:space:]]' "$file"; then fail "Unreviewed local sysctl configuration: $file"; return 1 fi done } render_sysctl() { printf '%s\n' '# Managed by platform host baseline v1' 'net.core.default_qdisc = fq' 'net.ipv4.tcp_congestion_control = bbr' } late_sysctl_conflicts() { local directory file shopt -s nullglob for directory in /usr/lib/sysctl.d /usr/local/lib/sysctl.d /lib/sysctl.d; do for file in "$directory"/*.conf; do if [[ ${file##*/} > 90-platform-baseline.conf ]] && grep -Eq '^[[:space:]-]*net[./](core[./]default_qdisc|ipv4[./]tcp_congestion_control)[[:space:]]*=' "$file"; then fail "Later vendor sysctl override requires review: $file"; return 1 fi done done } result() { local key=$1 status=$2 detail=$3 jq -cn --arg check "$key" --arg status "$status" --arg detail "$detail" '{check:$check,status:$status,detail:$detail}' [[ "$status" != fail ]] || failures=$((failures + 1)) } collect_info() { local key=$1 value shift if value=$("$@" 2>&1); then result "$key" info "$value"; else result "$key" fail "Collection failed: $value"; fi } verify_ssh_receipt() { local state=/var/lib/platform-bootstrap/ssh file for file in confirmed.json confirmation.SHA256SUMS ssh-confirm.sh firewall-confirm.sh; do trusted "$state/$file" || return 1 [[ -f "$state/$file" && $(stat -c %h "$state/$file") == 1 ]] || return 1 done # Reuse the pinned SSH implementation for config/include/service/firewall drift. # Receipt existence alone cannot certify that the validated state still holds. (cd "$state" && sha256sum --status -c confirmation.SHA256SUMS) || return 1 bash "$state/ssh-confirm.sh" --check } audit() { local value config ports failures=0 if sysctl_conflicts /etc/sysctl.d /etc/sysctl.conf && sysctl_conflicts /run/sysctl.d /dev/null && late_sysctl_conflicts; then result sysctl_ownership pass 'No unreviewed local sysctl files' else result sysctl_ownership fail 'Review local sysctl owners before applying a managed profile'; fi value=$(sysctl -n net.ipv4.tcp_congestion_control) if [[ "$value" == bbr ]]; then result tcp pass "$value"; else result tcp fail "$value; expected bbr"; fi value=$(sysctl -n net.core.default_qdisc) if [[ "$value" == fq ]]; then result default_qdisc pass "$value"; else result default_qdisc fail "$value; expected fq"; fi if [[ -f /etc/sysctl.d/90-platform-baseline.conf ]] && cmp -s <(render_sysctl) /etc/sysctl.d/90-platform-baseline.conf && [[ -f /etc/modules-load.d/platform-bbr.conf ]] && [[ $(cat /etc/modules-load.d/platform-bbr.conf) == tcp_bbr ]]; then result network_persistence pass 'Managed sysctl and module configuration match' else result network_persistence fail 'Managed network configuration missing or changed'; fi # default_qdisc does not replace existing qdiscs. Report the live state without claiming it did. collect_info live_qdisc tc qdisc show config=$(sshd -T) || return 1 ports=$(awk '$1 == "port" {print $2}' <<< "$config") if [[ "$ports" == 61789 ]] && grep -qx 'passwordauthentication no' <<< "$config" && grep -qx 'kbdinteractiveauthentication no' <<< "$config" && grep -qx 'pubkeyauthentication yes' <<< "$config" && grep -Eq '^permitrootlogin (prohibit-password|without-password)$' <<< "$config"; then result ssh_configuration pass '61789, public key only; external transport verification is separate' else result ssh_configuration fail 'SSH port/authentication baseline incomplete'; fi if verify_ssh_receipt; then result ssh_confirmation pass 'Pinned confirmation checks current SSH inputs, listener and exact firewall; periodic external probes still required' else result ssh_confirmation fail 'SSH confirmation missing, pending, untrusted or drifted'; fi if systemctl is-active --quiet platform-firewall.service && [[ ! -e /run/platform-firewall/pending.nft ]] && nft list table inet platform_host >/dev/null 2>&1; then result firewall pass 'Managed firewall active, no pending transaction; external access tests remain separate' else result firewall fail 'Firewall inactive or pending'; fi value=$(timedatectl show --property=NTPSynchronized --value) if [[ "$value" == yes ]]; then result time_sync pass synchronized; else result time_sync fail 'Clock not synchronized'; fi value=$(systemctl --failed --no-legend --plain) if [[ -z "$value" ]]; then result services pass 'No failed systemd units'; else result services fail "$value"; fi if [[ -e /var/run/reboot-required ]]; then result reboot fail 'Maintenance reboot required'; else result reboot info 'No reboot-required marker; kernel/package maintenance is separately reviewed'; fi collect_info storage df -P / /var collect_info inodes df -Pi / /var collect_info root_residue find /root -mindepth 1 -maxdepth 1 -type f \( -name '*.sh' -o -name '*.bundle' -o -name '*.tar' -o -name '*.log' \) -printf '%f\n' jq -cn --arg hostname "$name" --arg release "$release" --argjson failures "$failures" '{baseline:"debian13-v1",hostname:$hostname,release:$release,failures:$failures,scope:"host-local; external network, storage capacity policy and role acceptance remain separate"}' [[ "$failures" == 0 ]] || return 2 } preflight() { sysctl_conflicts /etc/sysctl.d /etc/sysctl.conf sysctl_conflicts /run/sysctl.d /dev/null late_sysctl_conflicts modinfo tcp_bbr >/dev/null || { fail 'Kernel BBR module unavailable'; return 1; } modinfo sch_fq >/dev/null || { fail 'Kernel fq module unavailable'; return 1; } local file for file in /etc/sysctl.d/90-platform-baseline.conf /etc/modules-load.d/platform-bbr.conf; do trusted "$file" [[ ! -e "$file" || ( -f "$file" && $(stat -c %h "$file") == 1 ) ]] || { fail 'Nonregular managed configuration'; return 1; } done for file in /etc/sysctl.d/.platform-baseline.pending /etc/modules-load.d/.platform-bbr.pending; do [[ ! -e "$file" && ! -L "$file" ]] || { fail 'Interrupted network baseline found; inspect backup before retrying'; return 1; } done if [[ -e /etc/sysctl.d/90-platform-baseline.conf ]]; then cmp -s <(render_sysctl) /etc/sysctl.d/90-platform-baseline.conf || { fail 'Managed sysctl drift; review before overwrite'; return 1; } fi if [[ -e /etc/modules-load.d/platform-bbr.conf ]]; then [[ $(cat /etc/modules-load.d/platform-bbr.conf) == tcp_bbr ]] || { fail 'Managed module drift'; return 1; } fi } main() { local action='' name='' release='' failures=0 argument while (($#)); do case "$1" in --help|-h) echo 'Usage: host-baseline.sh --check|--preflight|--apply-network --hostname NAME --release FULL_COMMIT'; return 0;; --check|--preflight|--apply-network) [[ -z "$action" ]] || return 1; action=$1; shift;; --hostname|--release) (($# >= 2)) || return 1; case "$1" in --hostname) name=$2;; --release) release=$2;; esac; shift 2;; *) fail "Unknown argument: $1"; return 1;; esac done [[ "$name" =~ ^(dev|test|stage|prod)-[a-z0-9-]+-[0-9]{2}$ && "$release" =~ ^[a-f0-9]{40}$ ]] || { fail 'Explicit hostname and release required'; return 1; } [[ $EUID == 0 && -d /run/systemd/system && ! -e /.dockerenv && $(hostname) == "$name" ]] || { fail 'Exact native root host required'; return 1; } grep -Eq '^ID="?debian"?$' /etc/os-release && grep -Eq '^VERSION_ID="?13"?$' /etc/os-release || { fail 'Debian 13 required'; return 1; } for argument in jq sysctl sshd tc nft timedatectl flock modinfo modprobe; do command -v "$argument" >/dev/null || { fail "Missing prerequisite: $argument"; return 1; }; done if [[ "$action" == --check ]]; then audit; return; fi [[ "$action" == --preflight || "$action" == --apply-network ]] || return 1 preflight if [[ "$action" == --preflight ]]; then echo 'Network baseline preflight passed; no configuration changed.'; return; fi umask 077 trusted /run/lock/platform-host-baseline.lock exec 9>>/run/lock/platform-host-baseline.lock; flock -n 9 || { fail 'Baseline maintenance already running'; return 1; } [[ ! -e /var/lib/platform-bootstrap/ssh/pending && ! -e /run/platform-firewall/pending.nft ]] || { fail 'Resolve pending SSH/firewall maintenance before network tuning'; return 1; } preflight if [[ $(sysctl -n net.ipv4.tcp_congestion_control) == bbr && $(sysctl -n net.core.default_qdisc) == fq && -f /etc/sysctl.d/90-platform-baseline.conf && -f /etc/modules-load.d/platform-bbr.conf ]]; then jq -cn --arg release "$release" '{network_applied:true,changed:false,release:$release,full_baseline_verified:false,live_qdisc_replaced:false}' return fi local backup old_tcp old_qdisc trusted /var/backups/platform install -d -m 0700 /var/backups/platform backup=$(mktemp -d /var/backups/platform/baseline-XXXXXXXX) old_tcp=$(sysctl -n net.ipv4.tcp_congestion_control); old_qdisc=$(sysctl -n net.core.default_qdisc) [[ ! -f /etc/sysctl.d/90-platform-baseline.conf ]] || cp -p /etc/sysctl.d/90-platform-baseline.conf "$backup/sysctl.conf" [[ ! -f /etc/modules-load.d/platform-bbr.conf ]] || cp -p /etc/modules-load.d/platform-bbr.conf "$backup/modules.conf" printf '%s\n' "$old_tcp" "$old_qdisc" > "$backup/runtime-before" # Subshell isolates failures so restoration is attempted even under set -e. if ( set -e modprobe tcp_bbr && modprobe sch_fq && render_sysctl > "$backup/candidate.conf" && printf 'tcp_bbr\n' > "$backup/candidate-modules.conf" && install -m 0644 "$backup/candidate.conf" /etc/sysctl.d/.platform-baseline.pending && install -m 0644 "$backup/candidate-modules.conf" /etc/modules-load.d/.platform-bbr.pending && mv -fT /etc/sysctl.d/.platform-baseline.pending /etc/sysctl.d/90-platform-baseline.conf && mv -fT /etc/modules-load.d/.platform-bbr.pending /etc/modules-load.d/platform-bbr.conf && sysctl -p /etc/sysctl.d/90-platform-baseline.conf && [[ $(sysctl -n net.ipv4.tcp_congestion_control) == bbr && $(sysctl -n net.core.default_qdisc) == fq ]] ); then jq -cn --arg release "$release" --arg backup "$backup" '{network_applied:true,release:$release,backup:$backup,full_baseline_verified:false,live_qdisc_replaced:false}' else rm -f /etc/sysctl.d/.platform-baseline.pending /etc/modules-load.d/.platform-bbr.pending if [[ -f "$backup/sysctl.conf" ]]; then cp -p "$backup/sysctl.conf" /etc/sysctl.d/90-platform-baseline.conf; else rm -f /etc/sysctl.d/90-platform-baseline.conf; fi if [[ -f "$backup/modules.conf" ]]; then cp -p "$backup/modules.conf" /etc/modules-load.d/platform-bbr.conf; else rm -f /etc/modules-load.d/platform-bbr.conf; fi sysctl -w "net.ipv4.tcp_congestion_control=$old_tcp" "net.core.default_qdisc=$old_qdisc" fail "Network baseline failed; restored previous values. Backup: $backup"; return 1 fi } if [[ ${BASH_SOURCE[0]:-} == "$0" || -z ${BASH_SOURCE[0]:-} ]]; then main "$@"; fi