286 lines
7.2 KiB
Bash
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
|
|
|