#!/usr/bin/env bash # ============================================================================= # audit-netboot.sh # ----------------------------------------------------------------------------- # READ-ONLY audit of the reference node "pfv-netboot". # # Purpose: gather enough information to replicate its Pi-hole, Technitium DNS, # and NTP services onto pfv-netinfra-01 and pfv-netinfra-02. # # Guarantees: # * NO writes, NO installs, NO service restarts, NO network changes. # * Every command below is read-only (status, cat, ls, ss, ps, sqlite3 SELECT). # # Intended to be run as the "localuser" account on pfv-netboot: # # ssh localuser@pfv-netboot 'bash -s' < audit-netboot.sh > netboot-audit.txt # # or, if sudo is needed for a few reads, the script will try `sudo -n` for # specific files that are normally root-readable only. It will NEVER use sudo # to write or modify anything. # ============================================================================= set -u AUDIT_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" # Helper: print a clearly delimited section header. section() { printf '\n========================================================================\n' printf 'SECTION: %s\n' "$1" printf '========================================================================\n' } # Helper: read a file with sudo if needed, silently skip if missing. read_file() { local path="$1" if [ -r "$path" ]; then printf '--- %s (uid-readable) ---\n' "$path" cat "$path" 2>/dev/null elif sudo -n true 2>/dev/null; then if sudo -n test -e "$path"; then printf '--- %s (via sudo -n) ---\n' "$path" sudo -n cat "$path" 2>/dev/null else printf '--- %s : NOT FOUND ---\n' "$path" fi else printf '--- %s : NOT READABLE (no passwordless sudo) ---\n' "$path" fi printf '\n' } list_dir() { local path="$1" printf '--- ls -la %s ---\n' "$path" ls -la "$path" 2>/dev/null || sudo -n ls -la "$path" 2>/dev/null || printf '(cannot list %s)\n' "$path" printf '\n' } printf '##### AUDIT START %s #####\n' "$AUDIT_DATE" printf 'Audit host: %s\n' "$(hostname -f 2>/dev/null || hostname)" printf 'Audit user: %s\n' "$(id -un 2>/dev/null)" printf 'Script: audit-netboot.sh (READ-ONLY)\n' # ----------------------------------------------------------------------------- # 1. System basics # ----------------------------------------------------------------------------- section "1. SYSTEM BASICS" echo "-- uname --"; uname -a echo "-- /etc/os-release --"; cat /etc/os-release 2>/dev/null echo "-- uptime --"; uptime echo "-- timezone --"; timedatectl 2>/dev/null || cat /etc/timezone 2>/dev/null || date echo "-- arch --"; dpkg --print-architecture 2>/dev/null || uname -m echo "-- memory --"; free -h 2>/dev/null echo "-- disk --"; df -h / 2>/dev/null echo "-- cpu count --"; nproc 2>/dev/null # ----------------------------------------------------------------------------- # 2. Network configuration # ----------------------------------------------------------------------------- section "2. NETWORK" echo "-- hostname --"; hostname; hostname -f 2>/dev/null; hostname -I 2>/dev/null echo "-- ip addr --"; ip -br addr 2>/dev/null; echo; ip addr 2>/dev/null echo "-- default route --"; ip route 2>/dev/null echo "-- /etc/resolv.conf --"; cat /etc/resolv.conf 2>/dev/null echo "-- /etc/hosts --"; cat /etc/hosts 2>/dev/null echo "-- listening TCP/UDP sockets --" ss -tlnup 2>/dev/null || sudo -n ss -tlnup 2>/dev/null echo "-- /etc/network/interfaces --"; cat /etc/network/interfaces 2>/dev/null echo "-- netplan --"; ls -la /etc/netplan/ 2>/dev/null; for f in /etc/netplan/*.yaml; do [ -e "$f" ] && { echo "--- $f ---"; cat "$f"; }; done 2>/dev/null echo "-- systemd-networkd --"; ls -la /etc/systemd/network/ 2>/dev/null; networkctl status 2>/dev/null | head -40 # ----------------------------------------------------------------------------- # 3. DNS / DHCP / NTP related systemd units # ----------------------------------------------------------------------------- section "3. RELEVANT SYSTEMD UNITS" echo "-- all units matching dns|pihole|dnsmasq|technitium|ftl|ntp|chrony|timesync --" systemctl list-units --type=service --all --no-pager 2>/dev/null \ | grep -Ei 'pihole|dnsmasq|ftl|technitium|dns|ntp|chrony|timesync|resolv|resolved' || true echo "-- unit files (enabled state) --" systemctl list-unit-files --no-pager 2>/dev/null \ | grep -Ei 'pihole|dnsmasq|ftl|technitium|dns|ntp|chrony|timesync|resolv|resolved' || true echo "-- installed packages of interest --" dpkg -l 2>/dev/null | grep -Ei 'pihole|dnsmasq|ftl|technitium|ntp|chrony|timesync|unbound|resolved|resolvconf' || true # ----------------------------------------------------------------------------- # 4. NTP service details # ----------------------------------------------------------------------------- section "4. NTP" echo "-- chrony --" systemctl status chrony --no-pager 2>/dev/null | head -15 || echo "(no chrony unit)" read_file /etc/chrony/chrony.conf echo "-- chronyc sources/stats (if available) --" chronyc -n sources 2>/dev/null || sudo -n chronyc -n sources 2>/dev/null || true chronyc -n tracking 2>/dev/null || sudo -n chronyc -n tracking 2>/dev/null || true echo "-- ntpsec / ntp classic --" systemctl status ntp --no-pager 2>/dev/null | head -15 || echo "(no ntp unit)" systemctl status ntpsec --no-pager 2>/dev/null | head -15 || true read_file /etc/ntp.conf read_file /etc/ntpsec/ntp.conf echo "-- systemd-timesyncd --" systemctl status systemd-timesyncd --no-pager 2>/dev/null | head -15 || echo "(no timesyncd)" read_file /etc/systemd/timesyncd.conf echo "-- openntpd --" systemctl status openntpd --no-pager 2>/dev/null | head -15 || true read_file /etc/openntpd/ntpd.conf # ----------------------------------------------------------------------------- # 5. Docker (Pi-hole and/or Technitium may be containerized) # ----------------------------------------------------------------------------- section "5. DOCKER" if command -v docker >/dev/null 2>&1; then echo "-- docker version --"; docker version 2>/dev/null || sudo -n docker version 2>/dev/null || true echo "-- containers (running) --"; docker ps 2>/dev/null || sudo -n docker ps 2>/dev/null || true echo "-- containers (all) --"; docker ps -a 2>/dev/null || sudo -n docker ps -a 2>/dev/null || true echo "-- images --"; docker images 2>/dev/null || sudo -n docker images 2>/dev/null || true echo "-- volumes --"; docker volume ls 2>/dev/null || sudo -n docker volume ls 2>/dev/null || true echo "-- networks --"; docker network ls 2>/dev/null || sudo -n docker network ls 2>/dev/null || true else echo "(docker not installed / not on PATH)" fi echo "-- compose files in common locations --" for d in /etc/docker-compose /opt/docker-compose /root/docker-compose /home/localuser/docker-compose /srv/docker-compose /opt/pihole /opt/technitium /opt; do if sudo -n test -d "$d" 2>/dev/null || [ -d "$d" ]; then echo "### $d ###" sudo -n ls -la "$d" 2>/dev/null || ls -la "$d" 2>/dev/null || true for f in docker-compose.yml docker-compose.yaml compose.yml compose.yaml; do if sudo -n test -f "$d/$f" 2>/dev/null; then read_file "$d/$f" elif [ -f "$d/$f" ]; then read_file "$d/$f" fi done fi done # ----------------------------------------------------------------------------- # 6. Pi-hole # ----------------------------------------------------------------------------- section "6. PI-HOLE" if command -v pihole >/dev/null 2>&1; then echo "-- pihole version --"; pihole -v 2>/dev/null || sudo -n pihole -v 2>/dev/null || true echo "-- pihole status --"; pihole status 2>/dev/null || sudo -n pihole status 2>/dev/null || true else echo "(pihole command not on PATH)" fi echo "-- /etc/pihole listing --" list_dir /etc/pihole # Key Pi-hole config files for f in \ /etc/pihole/setupVars.conf \ /etc/pihole/pihole-FTL.conf \ /etc/pihole/adlists.list \ /etc/pihole/whitelist.txt \ /etc/pihole/blacklist.txt \ /etc/pihole/regex.list \ /etc/pihole/custom.list \ /etc/pihole/local.list \ /etc/pihole/hostnameMappings.txt \ /etc/pihole/dhcp.leases \ /etc/pihole/static_ip.conf \ /etc/pihole/GitHubVersions \ /etc/pihole/macvendor.db ; do read_file "$f" done echo "-- /etc/pihole/*.conf (all) --" for f in /etc/pihole/*.conf; do [ -e "$f" ] && read_file "$f"; done 2>/dev/null echo "-- /etc/dnsmasq.d listing --" list_dir /etc/dnsmasq.d for f in /etc/dnsmasq.d/*; do [ -e "$f" ] && read_file "$f"; done 2>/dev/null echo "-- /etc/dnsmasq.conf (if present) --" read_file /etc/dnsmasq.conf echo "-- gravity.db schema + row counts (Pi-hole v5+) --" GRAVITY="" for g in /etc/pihole/gravity.db /etc/pihole/gravity.db.*; do if [ -e "$g" ]; then GRAVITY="$g"; break; fi done if [ -n "${GRAVITY:-}" ]; then echo "gravity.db = $GRAVITY" if command -v sqlite3 >/dev/null 2>&1; then sqlite3 -readonly "$GRAVITY" ".tables" 2>/dev/null || sudo -n sqlite3 -readonly "$GRAVITY" ".tables" 2>/dev/null || true for tbl in adlist adlist_by_group domainlist domainlist_by_group client client_by_group group info; do cnt=$(sqlite3 -readonly "$GRAVITY" "SELECT COUNT(*) FROM $tbl;" 2>/dev/null || sudo -n sqlite3 -readonly "$GRAVITY" "SELECT COUNT(*) FROM $tbl;" 2>/dev/null || echo "n/a") printf 'gravity.%s count = %s\n' "$tbl" "$cnt" done echo "-- adlist entries (url, enabled, comment) --" sqlite3 -readonly "$GRAVITY" "SELECT id,address,enabled,comment FROM adlist;" 2>/dev/null \ || sudo -n sqlite3 -readonly "$GRAVITY" "SELECT id,address,enabled,comment FROM adlist;" 2>/dev/null || true echo "-- domainlist sample (first 50) --" sqlite3 -readonly "$GRAVITY" "SELECT id,type,domain,enabled,comment FROM domainlist LIMIT 50;" 2>/dev/null \ || sudo -n sqlite3 -readonly "$GRAVITY" "SELECT id,type,domain,enabled,comment FROM domainlist LIMIT 50;" 2>/dev/null || true echo "-- client list --" sqlite3 -readonly "$GRAVITY" "SELECT id,ip,comment FROM client;" 2>/dev/null \ || sudo -n sqlite3 -readonly "$GRAVITY" "SELECT id,ip,comment FROM client;" 2>/dev/null || true echo "-- group list --" sqlite3 -readonly "$GRAVITY" "SELECT id,name,enabled,comment FROM 'group';" 2>/dev/null \ || sudo -n sqlite3 -readonly "$GRAVITY" "SELECT id,name,enabled,comment FROM 'group';" 2>/dev/null || true echo "-- info table --" sqlite3 -readonly "$GRAVITY" "SELECT * FROM info;" 2>/dev/null \ || sudo -n sqlite3 -readonly "$GRAVITY" "SELECT * FROM info;" 2>/dev/null || true else echo "(sqlite3 not installed; gravity.db present at $GRAVITY)" fi else echo "(no gravity.db found)" fi echo "-- lighttpd / pihole web admin --" systemctl status lighttpd --no-pager 2>/dev/null | head -15 || true read_file /etc/lighttpd/lighttpd.conf for f in /etc/lighttpd/conf-enabled/*; do [ -e "$f" ] && read_file "$f"; done 2>/dev/null echo "-- pihole-FTL service --" systemctl status pihole-FTL --no-pager 2>/dev/null | head -20 || true # ----------------------------------------------------------------------------- # 7. Technitium DNS # ----------------------------------------------------------------------------- section "7. TECHNITIUM DNS" echo "-- technitium systemd unit --" systemctl status technitium-dns --no-pager 2>/dev/null | head -20 || echo "(no technitium-dns unit)" echo "-- any unit containing technitium --" systemctl list-units --all --no-pager 2>/dev/null | grep -i technitium || true echo "-- unit file path --" sudo -n systemctl cat technitium-dns 2>/dev/null || systemctl cat technitium-dns 2>/dev/null || true # Technitium common install locations for d in /etc/technitium /etc/technitium/dns /opt/technitium /opt/technitium/dns /var/lib/technitium /usr/local/technitium; do echo "### checking $d ###" if sudo -n test -d "$d" 2>/dev/null || [ -d "$d" ]; then list_dir "$d" # recurse one level for config files for sub in "$d" "$d"/*; do [ -e "$sub" ] || continue if [ -f "$sub" ] && echo "$sub" | grep -Eq '\.(xml|json|conf|config|txt)$'; then read_file "$sub" fi done fi done echo "-- technitium config.xml / dnsServer.conf (search) --" sudo -n find /etc/technitium /opt/technitium /var/lib/technitium -maxdepth 4 \ \( -name 'config.xml' -o -name '*.config' -o -name 'dnsServer.conf' -o -name 'blockList.txt' \) \ -print 2>/dev/null || true # ----------------------------------------------------------------------------- # 8. Firewall / SELinux / misc # ----------------------------------------------------------------------------- section "8. FIREWALL & MISC" echo "-- nftables --"; sudo -n nft list ruleset 2>/dev/null | head -80 || true echo "-- iptables --"; sudo -n iptables -S 2>/dev/null | head -40 || true echo "-- ufw --"; sudo -n ufw status verbose 2>/dev/null || true echo "-- firewalld --"; systemctl status firewalld --no-pager 2>/dev/null | head -8 || true echo "-- selinux/apparmor --"; getenforce 2>/dev/null || echo "(SELinux not present)"; aa-status 2>/dev/null | head -5 || true # ----------------------------------------------------------------------------- # 9. Cron / timers that maintain these services # ----------------------------------------------------------------------------- section "9. CRON & TIMERS" echo "-- root crontab --"; sudo -n crontab -l 2>/dev/null || echo "(n/a)" echo "-- localuser crontab --"; crontab -l 2>/dev/null || echo "(none)" echo "-- /etc/cron.d --"; ls -la /etc/cron.d 2>/dev/null for f in /etc/cron.d/*pihole* /etc/cron.d/*technitium* /etc/cron.d/*gravity*; do [ -e "$f" ] && read_file "$f" done 2>/dev/null echo "-- pihole timer --"; systemctl list-timers --all --no-pager 2>/dev/null | grep -Ei 'pihole|gravity|technitium' || true printf '\n##### AUDIT END %s #####\n' "$AUDIT_DATE"