185 lines
11 KiB
Bash
185 lines
11 KiB
Bash
#!/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/platform-host-baseline.lock
|
|
exec 9>>/run/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
|