Files
OpenBSD-Notes/security/audit.ksh
T
2026-09-22 04:37:10 +03:00

286 lines
7.2 KiB
Bash

#!/bin/ksh
#
# First-login security audit for OpenBSD.
#
# This script is intentionally diagnostic: it does not repair, install, or
# reconfigure the host. OpenBSD security(8) may rotate its normal comparison
# state in /var/backups, just as it does during the daily security run.
# This script cannot prove that a host is clean.
# A root-level attacker may alter the kernel, commands, logs, or this script's
# view of the system. Treat warnings as reasons to investigate and credible
# evidence of root compromise as a reason to isolate and reinstall the host.
#
# Exit status:
# 0 No automatic red flags were found (not proof that the host is clean).
# 1 One or more findings require manual review.
# 2 The audit could not run correctly.
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin
export PATH
umask 077
WARNINGS=0
ERRORS=0
heading()
{
printf '\n===== %s =====\n' "$1"
}
pass()
{
printf '[PASS] %s\n' "$1"
}
info()
{
printf '[INFO] %s\n' "$1"
}
warn()
{
printf '[WARN] %s\n' "$1"
WARNINGS=$((WARNINGS + 1))
}
error()
{
printf '[ERROR] %s\n' "$1" >&2
ERRORS=$((ERRORS + 1))
}
print_file()
{
if [ -s "$1" ]; then
sed 's/^/ /' "$1"
fi
}
if [ "$(id -u)" -ne 0 ]; then
printf 'Run this audit as root, for example:\n' >&2
printf ' doas %s\n' "$0" >&2
exit 2
fi
if [ "$(uname -s)" != "OpenBSD" ]; then
printf 'This script is intended only for OpenBSD.\n' >&2
exit 2
fi
AUDIT_TMPDIR=$(mktemp -d /tmp/openbsd-audit.XXXXXXXXXX) || {
printf 'Cannot create temporary audit directory.\n' >&2
exit 2
}
cleanup()
{
case "$AUDIT_TMPDIR" in
/tmp/openbsd-audit.*)
/bin/rm -rf "$AUDIT_TMPDIR"
;;
esac
}
case "$AUDIT_TMPDIR" in
/tmp/openbsd-audit.*)
trap cleanup EXIT
trap 'exit 2' HUP INT TERM
;;
*)
printf 'Refusing unexpected temporary path: %s\n' "$AUDIT_TMPDIR" >&2
exit 2
;;
esac
heading "IMPORTANT LIMITATION"
cat <<'EOF'
This report can find common integrity and persistence problems, but it cannot
certify that the host is unhacked. A sufficiently privileged attacker can
make a running system lie. For strong assurance, inspect from trusted boot
media or reinstall from verified OpenBSD media and rotate exposed secrets.
EOF
heading "SYSTEM IDENTITY"
printf 'Date (UTC): %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
printf 'Hostname: %s\n' "$(hostname)"
printf 'Kernel: %s\n' "$(uname -a)"
printf 'Architecture: %s\n' "$(machine 2>/dev/null || uname -m)"
printf 'Uptime: %s\n' "$(uptime)"
if [ -n "${SSH_CONNECTION:-}" ]; then
set -- $SSH_CONNECTION
printf 'SSH source: %s:%s\n' "$1" "$2"
printf 'SSH destination: %s:%s\n' "$3" "$4"
else
printf 'SSH connection: not detected\n'
fi
SECURELEVEL=$(sysctl -n kern.securelevel 2>/dev/null)
if [ -z "$SECURELEVEL" ]; then
error "Could not read kern.securelevel"
elif [ "$SECURELEVEL" -ge 1 ] 2>/dev/null; then
pass "kern.securelevel is $SECURELEVEL"
else
warn "kern.securelevel is $SECURELEVEL; review why it is below 1"
fi
heading "OPENBSD SECURITY(8)"
SECURITY_OUT="$AUDIT_TMPDIR/security.out"
/usr/libexec/security >"$SECURITY_OUT" 2>&1
SECURITY_RC=$?
if [ "$SECURITY_RC" -ne 0 ]; then
warn "security(8) exited with status $SECURITY_RC"
print_file "$SECURITY_OUT"
elif [ -s "$SECURITY_OUT" ]; then
warn "security(8) produced findings; review every line"
print_file "$SECURITY_OUT"
else
pass "security(8) produced no findings"
fi
heading "BASE SYSTEM PATCHES"
printf '%s\n' "Installed syspatches:"
syspatch -l 2>&1 | sed 's/^/ /'
SYSPATCH_OUT="$AUDIT_TMPDIR/syspatch.out"
syspatch -c >"$SYSPATCH_OUT" 2>&1
SYSPATCH_RC=$?
if [ "$SYSPATCH_RC" -ne 0 ]; then
warn "Could not reliably check for missing syspatches (status $SYSPATCH_RC)"
print_file "$SYSPATCH_OUT"
elif [ -s "$SYSPATCH_OUT" ]; then
warn "Available syspatches are not installed"
print_file "$SYSPATCH_OUT"
else
pass "No missing syspatches were reported"
fi
heading "INSTALLED PACKAGE INTEGRITY"
PKG_OUT="$AUDIT_TMPDIR/pkg-check.out"
pkg_check -n -F -x >"$PKG_OUT" 2>&1
PKG_RC=$?
if [ "$PKG_RC" -ne 0 ]; then
warn "pkg_check reported an error (status $PKG_RC)"
print_file "$PKG_OUT"
elif [ -s "$PKG_OUT" ]; then
warn "pkg_check produced output; review package checksums and extra objects"
print_file "$PKG_OUT"
else
pass "Installed package files and checksums passed pkg_check"
fi
heading "PRIVILEGED ACCOUNTS"
UID0_OUT="$AUDIT_TMPDIR/uid0.out"
awk -F: '$3 == 0 { print $1 ":" $7 }' /etc/master.passwd >"$UID0_OUT"
printf '%s\n' "Accounts with UID 0:"
print_file "$UID0_OUT"
UID0_COUNT=$(wc -l <"$UID0_OUT" | tr -d ' ')
if [ "$UID0_COUNT" -eq 1 ] && grep -q '^root:' "$UID0_OUT"; then
pass "Only root has UID 0"
else
warn "Expected exactly one UID 0 account named root"
fi
heading "SSH AUTHORIZED KEYS"
KEY_LIST="$AUDIT_TMPDIR/authorized-keys.list"
find /root /home -type f -path '*/.ssh/authorized_keys' -print \
2>/dev/null >"$KEY_LIST"
if [ ! -s "$KEY_LIST" ]; then
info "No authorized_keys files were found"
else
while IFS= read -r KEY_FILE; do
printf '%s\n' "File: $KEY_FILE"
ls -ld "$KEY_FILE" | sed 's/^/ /'
ssh-keygen -lf "$KEY_FILE" 2>&1 | sed 's/^/ /'
done <"$KEY_LIST"
info "Confirm that every SSH key fingerprint belongs to an authorized person"
fi
heading "RC SERVICES"
printf '%s\n' "Enabled services:"
rcctl ls on 2>&1 | sed 's/^/ /'
printf '%s\n' "Currently started services:"
rcctl ls started 2>&1 | sed 's/^/ /'
FAILED_RC="$AUDIT_TMPDIR/rc-failed.out"
rcctl ls failed >"$FAILED_RC" 2>&1
FAILED_RC_STATUS=$?
if [ "$FAILED_RC_STATUS" -ne 0 ]; then
warn "Could not query failed rc services"
print_file "$FAILED_RC"
elif [ -s "$FAILED_RC" ]; then
warn "Some enabled services are in a failed state"
print_file "$FAILED_RC"
else
pass "No failed rc services were reported"
fi
heading "CRON PERSISTENCE LOCATIONS"
if [ -d /var/cron/tabs ]; then
find /var/cron/tabs -type f -exec ls -ld {} \; 2>/dev/null
else
warn "/var/cron/tabs does not exist"
fi
info "Review the content of every expected user crontab manually"
heading "RECENT LOGINS"
last -20 2>&1
heading "RECENT SSH AUTHENTICATION EVENTS"
if [ -r /var/log/authlog ]; then
grep -Ei 'accepted|failed|invalid user|authentication' /var/log/authlog \
2>/dev/null | tail -50
else
warn "Cannot read /var/log/authlog"
fi
heading "RUNNING PROCESSES"
ps auxww
heading "INTERNET SOCKETS AND CONNECTIONS"
FSTAT_NET="$AUDIT_TMPDIR/fstat-network.out"
fstat -n 2>/dev/null | grep -E 'internet|internet6' >"$FSTAT_NET"
if [ -s "$FSTAT_NET" ]; then
cat "$FSTAT_NET"
info "Confirm that every listener and remote connection is expected"
else
info "No internet sockets were reported by fstat"
fi
heading "PF STATUS AND RULES"
pfctl -si 2>&1
printf '\nFilter rules:\n'
pfctl -sr 2>&1
printf '\nNAT and redirection rules:\n'
pfctl -sn 2>&1
heading "FILESYSTEMS"
mount
df -h
heading "AUDIT RESULT"
if [ "$ERRORS" -gt 0 ]; then
printf '[ERROR] Audit incomplete: %d error(s), %d warning(s).\n' \
"$ERRORS" "$WARNINGS"
printf 'Do not treat this host as verified.\n'
exit 2
elif [ "$WARNINGS" -gt 0 ]; then
printf '[WARN] Manual review required: %d warning(s).\n' "$WARNINGS"
printf 'Warnings are not proof of compromise, but they must be explained.\n'
exit 1
else
printf '[PASS] No automatic red flags were found.\n'
printf 'This increases confidence but does not prove that the host is clean.\n'
exit 0
fi