feat(host): add guarded Debian baseline and SSH transactions
This commit is contained in:
184
host-baseline.sh
Normal file
184
host-baseline.sh
Normal file
@@ -0,0 +1,184 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user