← All articles
SECURITY Homelab Network Security Audit: Scanning, Testing, a... 2026-02-09 · 17 min read · security · nmap · openvas

Homelab Network Security Audit: Scanning, Testing, and Hardening

Security 2026-02-09 · 17 min read security nmap openvas suricata audit

Running a homelab means running a network, and running a network means you're responsible for its security. That Docker container you exposed to the internet last month? Is it still running? That SSH port you opened for testing? Did you ever close it? The default password on your router's admin panel? Yeah, about that.

A security audit isn't something you do once and forget about — it's a regular practice that catches the things you forgot, the things that changed, and the things you didn't know were vulnerable. This guide walks you through a complete audit of your homelab network using real tools: Nmap for discovery and port scanning, OpenVAS for vulnerability assessment, Suricata for traffic analysis, and a hardening checklist to fix what you find.

Nmap logo

Why Audit Your Homelab?

"It's just my homelab — who would hack it?"

The answer: automated bots. Your public IP gets scanned thousands of times a day by automated tools looking for open ports, default credentials, and known vulnerabilities. If you've exposed anything to the internet — even briefly — it's been probed.

Common homelab security mistakes:

A security audit finds these before someone else does.

Phase 1: Network Discovery with Nmap

Nmap is the Swiss Army knife of network scanning. Start here to understand what's actually running on your network.

Installation

# Debian/Ubuntu
sudo apt install nmap

# Fedora
sudo dnf install nmap

# Check version (you want 7.90+)
nmap --version

Host Discovery (What's on My Network?)

# Ping scan — discover all live hosts on your subnet
sudo nmap -sn 192.168.1.0/24

# ARP scan (more reliable on local networks)
sudo nmap -sn -PR 192.168.1.0/24

# If you have multiple VLANs, scan each one
sudo nmap -sn 192.168.1.0/24   # Management VLAN
sudo nmap -sn 192.168.10.0/24  # Server VLAN
sudo nmap -sn 192.168.20.0/24  # IoT VLAN
sudo nmap -sn 192.168.30.0/24  # Guest VLAN

Example output:

Nmap scan report for router.homelab.local (192.168.1.1)
Host is up (0.0010s latency).
MAC Address: AA:BB:CC:DD:EE:01 (Ubiquiti Networks)

Nmap scan report for proxmox.homelab.local (192.168.1.10)
Host is up (0.0005s latency).
MAC Address: AA:BB:CC:DD:EE:02 (Dell)

Nmap scan report for nas.homelab.local (192.168.1.20)
Host is up (0.0003s latency).
MAC Address: AA:BB:CC:DD:EE:03 (ASRock)

Record every host you find. If there's a device you don't recognize, investigate immediately.

Port Scanning (What's Exposed?)

# Quick scan — top 1000 ports on all discovered hosts
sudo nmap -sS -T4 192.168.1.0/24

# Full port scan — all 65535 TCP ports (takes longer)
sudo nmap -sS -p- -T4 192.168.1.10

# UDP scan — important for DNS, SNMP, VPN (much slower)
sudo nmap -sU --top-ports 100 192.168.1.0/24

# Combined TCP + UDP scan on a specific host
sudo nmap -sS -sU -p T:1-65535,U:53,67,68,123,161,500,1194,4500,51820 \
  192.168.1.10

Service Detection (What's Running?)

# Detect services and versions on open ports
sudo nmap -sV -sC 192.168.1.10

# Example output:
# PORT     STATE SERVICE     VERSION
# 22/tcp   open  ssh         OpenSSH 9.6p1 Ubuntu 3ubuntu13
# 80/tcp   open  http        nginx 1.24.0
# 443/tcp  open  ssl/http    nginx 1.24.0
# 3000/tcp open  http        Grafana v10.4.0
# 8006/tcp open  ssl/http    Proxmox VE API
# 8080/tcp open  http-proxy  Vaultwarden
# 9090/tcp open  http        Prometheus

The -sC flag runs Nmap's default scripts, which will identify specific software versions, check for common misconfigurations, and extract useful information like SSL certificate details.

OS Detection

# Detect operating systems
sudo nmap -O 192.168.1.0/24

# Combine everything into a comprehensive scan
sudo nmap -A -T4 192.168.1.0/24
# -A = OS detection + version detection + script scanning + traceroute

Nmap Scripting Engine (NSE) for Specific Checks

# Check for known vulnerabilities
sudo nmap --script vuln 192.168.1.10

# Check SSL/TLS configuration
sudo nmap --script ssl-enum-ciphers -p 443 192.168.1.10

# Check for default credentials on HTTP services
sudo nmap --script http-default-accounts -p 80,443,8080,8443 192.168.1.10

# Check SMB for known vulnerabilities
sudo nmap --script smb-vuln* -p 445 192.168.1.20

# Check SSH configuration
sudo nmap --script ssh2-enum-algos -p 22 192.168.1.10

# Check DNS for zone transfer (should be denied)
sudo nmap --script dns-zone-transfer -p 53 192.168.1.1

Saving and Comparing Scan Results

# Save results in all formats
sudo nmap -A -T4 192.168.1.0/24 \
  -oA /home/user/security-audits/scan-$(date +%Y%m%d)

# This creates three files:
# scan-20260209.nmap    (human-readable)
# scan-20260209.xml     (machine-readable)
# scan-20260209.gnmap   (greppable)

# Compare with a previous scan to find changes
ndiff /home/user/security-audits/scan-20260109.xml \
      /home/user/security-audits/scan-20260209.xml

# Example output:
# -Nmap scan report for 192.168.1.50
# -Host is up.
# +Nmap scan report for 192.168.1.50
# +Host is up.
# +PORT    STATE SERVICE
# +8888/tcp open  sun-answerbook    <-- NEW PORT OPEN

Phase 2: Vulnerability Scanning with OpenVAS/Greenbone

Nmap finds what's open. OpenVAS (now called Greenbone Community Edition) tests what's open for known vulnerabilities.

Deploy OpenVAS with Docker

# docker-compose.yml
services:
  openvas:
    image: greenbone/openvas-scanner:latest
    container_name: openvas
    restart: unless-stopped
    ports:
      - "127.0.0.1:9392:9392"
    volumes:
      - openvas-data:/var/lib/openvas
      - gvm-data:/var/lib/gvm
    environment:
      - GVM_ADMIN_PASSWORD=${OPENVAS_PASSWORD}

volumes:
  openvas-data:
  gvm-data:

For easier deployment, use the all-in-one Greenbone Community image:

# Pull and run the Greenbone Community Edition
docker run -d \
  --name greenbone \
  -p 127.0.0.1:9392:9392 \
  -v greenbone-data:/data \
  -e PASSWORD="your-secure-password" \
  greenbone/gsad

# Wait for feed sync to complete (this takes 30-60 minutes on first run)
docker logs -f greenbone
# Look for "Feed sync complete"

# Access the web interface
# https://localhost:9392
# Username: admin
# Password: your-secure-password

Running a Vulnerability Scan

  1. Log into the Greenbone web interface
  2. Go to Scans > Tasks > New Task
  3. Configure:
    • Name: "Homelab Full Scan - Feb 2026"
    • Scan Targets: Create a new target with your subnet (192.168.1.0/24)
    • Scanner: OpenVAS Default
    • Scan Config: "Full and fast" (good balance)
  4. Click Start

The scan will take 30 minutes to several hours depending on how many hosts and ports are being tested.

Interpreting Results

OpenVAS categorizes findings by severity:

Severity CVSS Score Action
Critical 9.0 - 10.0 Fix immediately
High 7.0 - 8.9 Fix within a week
Medium 4.0 - 6.9 Fix within a month
Low 0.1 - 3.9 Fix when convenient
Log 0.0 Informational only

Common findings in homelabs:

Critical:
- Default credentials on management interfaces
- Unpatched services with known RCE vulnerabilities
- Exposed IPMI/iDRAC/iLO without TLS

High:
- Outdated OpenSSH with known vulnerabilities
- Weak SSL/TLS configurations (TLS 1.0/1.1)
- SMBv1 enabled

Medium:
- SSH password authentication enabled
- Missing HTTP security headers
- Self-signed certificates (expected in homelab, but flagged)

Low:
- ICMP timestamp responses
- SSH banner information disclosure
- SNMP community string "public"

Automating Regular Scans

# Use the GVM CLI to schedule scans
# Install gvm-cli
pip install gvm-tools

# Create a scheduled scan via the API
gvm-cli socket --socketpath /run/gvmd/gvmd.sock \
  --xml '<create_schedule>
    <name>Monthly Homelab Scan</name>
    <icalendar>BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
DTSTART:20260301T020000Z
RRULE:FREQ=MONTHLY;INTERVAL=1
DURATION:PT12H
END:VEVENT
END:VCALENDAR</icalendar>
    <timezone>America/Los_Angeles</timezone>
  </create_schedule>'

Phase 3: Traffic Analysis with Suricata IDS

Suricata is an Intrusion Detection System (IDS) that monitors your network traffic in real time, looking for malicious patterns, exploit attempts, and policy violations.

Installation

# Debian/Ubuntu
sudo apt install suricata suricata-update

# Fedora
sudo dnf install suricata

# Check the version
suricata --build-info

Configuration

# /etc/suricata/suricata.yaml (key sections)

# Define your home network
vars:
  address-groups:
    HOME_NET: "[192.168.1.0/24, 192.168.10.0/24, 10.0.0.0/8]"
    EXTERNAL_NET: "!$HOME_NET"
    HTTP_SERVERS: "$HOME_NET"
    DNS_SERVERS: "[192.168.1.1]"

  port-groups:
    HTTP_PORTS: "80"
    SHELLCODE_PORTS: "!80"
    SSH_PORTS: "22"

# Network interface to monitor
af-packet:
  - interface: eth0
    cluster-id: 99
    cluster-type: cluster_flow
    defrag: yes

# Enable the EVE JSON log (structured logging)
outputs:
  - eve-log:
      enabled: yes
      filetype: regular
      filename: eve.json
      types:
        - alert:
            payload: yes
            payload-printable: yes
            http-body: yes
            http-body-printable: yes
        - http:
            extended: yes
        - dns:
            query: yes
            answer: yes
        - tls:
            extended: yes
        - files:
            force-magic: yes
        - ssh
        - flow

# Rule sources
default-rule-path: /var/lib/suricata/rules
rule-files:
  - suricata.rules

Updating Rules

# Update Suricata rules (ET Open ruleset — free)
sudo suricata-update

# Enable additional rule sources
sudo suricata-update list-sources

# Enable Abuse.ch SSLBL (malicious SSL certificates)
sudo suricata-update enable-source sslbl/ssl-fp-blacklist

# Enable Abuse.ch URLhaus (malicious URLs)
sudo suricata-update enable-source et/open

# Update again to pull new rules
sudo suricata-update

# Restart Suricata to load new rules
sudo systemctl restart suricata

Running Suricata

# Test the configuration
sudo suricata -T -c /etc/suricata/suricata.yaml

# Start Suricata
sudo systemctl enable --now suricata

# Check it's running
sudo systemctl status suricata

# Watch alerts in real time
sudo tail -f /var/log/suricata/eve.json | jq 'select(.event_type == "alert")'

Analyzing Suricata Alerts

# Count alerts by severity
sudo cat /var/log/suricata/eve.json | \
  jq -r 'select(.event_type == "alert") | .alert.severity' | \
  sort | uniq -c | sort -rn

# Top 10 alert signatures
sudo cat /var/log/suricata/eve.json | \
  jq -r 'select(.event_type == "alert") | .alert.signature' | \
  sort | uniq -c | sort -rn | head -10

# Alerts from external sources
sudo cat /var/log/suricata/eve.json | \
  jq -r 'select(.event_type == "alert" and (.src_ip | startswith("192.168") | not)) |
    "\(.timestamp) \(.src_ip):\(.src_port) -> \(.dest_ip):\(.dest_port) \(.alert.signature)"'

# DNS queries to suspicious domains
sudo cat /var/log/suricata/eve.json | \
  jq -r 'select(.event_type == "dns") | "\(.timestamp) \(.src_ip) -> \(.dns.rrname)"' | \
  sort | uniq -c | sort -rn | head -20

Custom Rules for Homelab

# /var/lib/suricata/rules/local.rules

# Alert on any traffic to IPMI ports from outside the management VLAN
alert tcp !192.168.1.0/24 any -> $HOME_NET 623 (msg:"POLICY Non-management access to IPMI"; sid:1000001; rev:1;)

# Alert on plaintext HTTP to sensitive services
alert http any any -> $HOME_NET any (msg:"POLICY Unencrypted HTTP to sensitive service"; content:"Proxmox"; http_header; sid:1000002; rev:1;)

# Alert on SSH brute force (more than 5 failed auth in 60 seconds)
alert ssh any any -> $HOME_NET $SSH_PORTS (msg:"BRUTE-FORCE SSH rapid auth attempts"; flow:to_server; threshold:type both, track by_src, count 5, seconds 60; sid:1000003; rev:1;)

# Alert on outbound connections to known malicious ports
alert tcp $HOME_NET any -> $EXTERNAL_NET [4444,5555,6666,7777,8888,9999] (msg:"POLICY Outbound connection to suspicious port"; sid:1000004; rev:1;)

# Alert on DNS queries to known bad TLDs
alert dns any any -> any any (msg:"POLICY DNS query to suspicious TLD"; dns.query; content:".tk"; endswith; sid:1000005; rev:1;)
alert dns any any -> any any (msg:"POLICY DNS query to suspicious TLD"; dns.query; content:".xyz"; endswith; sid:1000006; rev:1;)
# Test your custom rules
sudo suricata -T -c /etc/suricata/suricata.yaml

# Reload rules without restarting
sudo suricatasc -c reload-rules

Integrating Suricata with Your Monitoring Stack

If you run Grafana + Loki or an ELK stack:

# Filebeat configuration for shipping Suricata logs to Elasticsearch
# /etc/filebeat/filebeat.yml

filebeat.inputs:
  - type: log
    paths:
      - /var/log/suricata/eve.json
    json.keys_under_root: true
    json.add_error_key: true

output.elasticsearch:
  hosts: ["http://elasticsearch:9200"]
  index: "suricata-%{+yyyy.MM.dd}"

Or use Promtail for Loki:

# /etc/promtail/config.yml
scrape_configs:
  - job_name: suricata
    static_configs:
      - targets:
          - localhost
        labels:
          job: suricata
          __path__: /var/log/suricata/eve.json
    pipeline_stages:
      - json:
          expressions:
            event_type: event_type
            alert_signature: alert.signature
            src_ip: src_ip
            dest_ip: dest_ip
      - labels:
          event_type:

Phase 4: Password Auditing

Weak passwords are the number one way homelabs get compromised.

Check for Default Credentials

# Common default credentials to check:
# Router admin panels: admin/admin, admin/password, admin/1234
# IPMI/iDRAC/iLO: admin/admin, ADMIN/ADMIN, root/calvin (Dell iDRAC)
# Proxmox: root/<whatever you set during install>
# TrueNAS: root/<whatever you set>
# Docker Portainer: admin/<first-run password>
# Grafana: admin/admin (defaults on first launch)

# Use hydra to test SSH for weak passwords (test your own machines only!)
# Create a test password list
cat > /tmp/test-passwords.txt << 'EOF'
password
admin
root
123456
homelab
changeme
letmein
default
EOF

# Test SSH (ONLY on machines you own)
hydra -l root -P /tmp/test-passwords.txt \
  -t 4 -V 192.168.1.10 ssh

# Test HTTP basic auth
hydra -l admin -P /tmp/test-passwords.txt \
  -t 4 -V 192.168.1.10 http-get /admin

# Clean up
rm /tmp/test-passwords.txt

SSH Configuration Audit

# Check SSH config for security issues
ssh -G 192.168.1.10 | grep -E "passwordauthentication|permitrootlogin|pubkeyauthentication"

# What you want to see:
# passwordauthentication no
# permitrootlogin no  (or prohibit-password)
# pubkeyauthentication yes

# Scan all hosts for SSH configuration
for host in 192.168.1.{10,20,30,40,50}; do
    echo "=== $host ==="
    ssh -o ConnectTimeout=3 -o BatchMode=yes $host \
      "grep -E '^(Password|PermitRoot|PubkeyAuth)' /etc/ssh/sshd_config" 2>/dev/null
done

Phase 5: SSL/TLS Certificate Checking

# Check certificate details for all HTTPS services
check_cert() {
    local host=$1
    local port=${2:-443}
    echo "=== $host:$port ==="
    echo | openssl s_client -connect "$host:$port" -servername "$host" 2>/dev/null | \
        openssl x509 -noout -dates -subject -issuer 2>/dev/null
    echo ""
}

check_cert proxmox.homelab.local 8006
check_cert nas.homelab.local
check_cert vault.homelab.local
check_cert grafana.homelab.local 3000

# Check for weak TLS versions
testssl() {
    local host=$1
    local port=${2:-443}
    echo "=== $host:$port ==="
    nmap --script ssl-enum-ciphers -p $port $host | grep -E "TLSv|SSLv|WARNING|WEAK"
}

testssl proxmox.homelab.local 8006
testssl nas.homelab.local 443

For thorough TLS testing, use testssl.sh:

# Install testssl.sh
git clone https://github.com/drwetter/testssl.sh.git
cd testssl.sh

# Test a specific service
./testssl.sh https://vault.homelab.local

# Test all your services
for host in proxmox nas vault grafana; do
    ./testssl.sh --quiet https://$host.homelab.local >> tls-audit-results.txt
done

Phase 6: Firewall Rule Review

# Review iptables rules
sudo iptables -L -n -v --line-numbers
sudo iptables -t nat -L -n -v --line-numbers

# Review nftables rules
sudo nft list ruleset

# Review UFW rules (if using UFW)
sudo ufw status verbose

# Check for overly permissive rules
# These patterns are red flags:
# - ACCEPT all from 0.0.0.0/0 to 0.0.0.0/0 (allows everything)
# - No DROP/REJECT default policy
# - DNAT rules forwarding to internal services without restrictions

Firewall Audit Checklist

#!/bin/bash
# firewall-audit.sh

echo "=== Firewall Audit ==="
echo ""

# Check default policies
echo "--- Default Policies ---"
sudo iptables -S | grep "\-P"
# Expected: -P INPUT DROP, -P FORWARD DROP, -P OUTPUT ACCEPT

echo ""
echo "--- Open Inbound Ports (from internet) ---"
sudo iptables -L INPUT -n | grep ACCEPT | grep -v "192.168\|10\.\|172\.16"

echo ""
echo "--- NAT/Port Forwarding Rules ---"
sudo iptables -t nat -L PREROUTING -n | grep DNAT

echo ""
echo "--- Forward Rules ---"
sudo iptables -L FORWARD -n | grep ACCEPT

echo ""
echo "--- Hosts with RELATED,ESTABLISHED ---"
sudo iptables -L -n | grep RELATED
# This is normal and expected, but make sure it's not too broad

Phase 7: Docker Container Security

Docker containers are a common attack surface in homelabs.

Scanning Container Images with Trivy

# Install Trivy
sudo apt install wget apt-transport-https gnupg
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | \
  gpg --dearmor | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | \
  sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt update && sudo apt install trivy

# Scan a specific image
trivy image vaultwarden/server:latest

# Scan all running container images
docker ps --format '{{.Image}}' | sort -u | while read img; do
    echo "=== Scanning: $img ==="
    trivy image --severity HIGH,CRITICAL "$img"
    echo ""
done

# Scan with a specific severity threshold
trivy image --severity CRITICAL --exit-code 1 nginx:latest
# Exit code 1 if CRITICAL vulnerabilities found

Docker Security Checklist

#!/bin/bash
# docker-security-audit.sh

echo "=== Docker Security Audit ==="

echo ""
echo "--- Containers running as root ---"
docker ps -q | while read cid; do
    USER=$(docker inspect --format '{{.Config.User}}' $cid)
    NAME=$(docker inspect --format '{{.Name}}' $cid)
    if [ -z "$USER" ] || [ "$USER" = "root" ] || [ "$USER" = "0" ]; then
        echo "WARNING: $NAME runs as root"
    fi
done

echo ""
echo "--- Containers with host networking ---"
docker ps -q | while read cid; do
    NET=$(docker inspect --format '{{.HostConfig.NetworkMode}}' $cid)
    NAME=$(docker inspect --format '{{.Name}}' $cid)
    if [ "$NET" = "host" ]; then
        echo "WARNING: $NAME uses host networking"
    fi
done

echo ""
echo "--- Containers with privileged mode ---"
docker ps -q | while read cid; do
    PRIV=$(docker inspect --format '{{.HostConfig.Privileged}}' $cid)
    NAME=$(docker inspect --format '{{.Name}}' $cid)
    if [ "$PRIV" = "true" ]; then
        echo "CRITICAL: $NAME runs in privileged mode"
    fi
done

echo ""
echo "--- Containers with writable root filesystem ---"
docker ps -q | while read cid; do
    RO=$(docker inspect --format '{{.HostConfig.ReadonlyRootfs}}' $cid)
    NAME=$(docker inspect --format '{{.Name}}' $cid)
    if [ "$RO" = "false" ]; then
        echo "INFO: $NAME has writable root fs (consider read-only)"
    fi
done

echo ""
echo "--- Containers with mounted Docker socket ---"
docker ps -q | while read cid; do
    MOUNTS=$(docker inspect --format '{{range .Mounts}}{{.Source}} {{end}}' $cid)
    NAME=$(docker inspect --format '{{.Name}}' $cid)
    if echo "$MOUNTS" | grep -q "docker.sock"; then
        echo "WARNING: $NAME has Docker socket mounted (container escape risk)"
    fi
done

echo ""
echo "--- Images with known CRITICAL vulnerabilities ---"
docker ps --format '{{.Image}}' | sort -u | while read img; do
    VULNS=$(trivy image --severity CRITICAL --quiet --format json "$img" 2>/dev/null | \
      jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL")] | length')
    if [ "$VULNS" -gt 0 ]; then
        echo "CRITICAL: $img has $VULNS critical vulnerabilities"
    fi
done

Phase 8: Open Port Inventory

Create a complete inventory of every open port and what it's for:

#!/bin/bash
# port-inventory.sh — Generate a port inventory for your homelab

echo "| Host | Port | Protocol | Service | Purpose | Exposed to Internet? |"
echo "|------|------|----------|---------|---------|---------------------|"

for host in 192.168.1.{1,10,20,30,40,50}; do
    HOSTNAME=$(dig +short -x $host 2>/dev/null | sed 's/\.$//' || echo "$host")

    # Quick scan — just open ports
    PORTS=$(nmap -sS -T4 --open -p- $host 2>/dev/null | \
      grep "^[0-9]" | awk '{print $1 "|" $3}')

    while IFS='|' read -r port service; do
        if [ -n "$port" ]; then
            echo "| $HOSTNAME | $port | TCP | $service | TODO | TODO |"
        fi
    done <<< "$PORTS"
done

Fill in the "Purpose" and "Exposed to Internet?" columns manually. Every open port should have a documented purpose. If you can't explain why a port is open, close it.

Expected vs. Unexpected Ports

Port Expected On Purpose If Unexpected
22 Servers SSH Check who configured it
53 DNS server/router DNS Make sure it's not open to internet
80/443 Web servers, reverse proxy HTTP/HTTPS Check what's being served
445 NAS SMB file sharing Should only be on NAS
631 Print server CUPS/IPP Disable if no printer
3000 Monitoring server Grafana Should be behind reverse proxy
5432 Database server PostgreSQL Should NOT be exposed externally
6379 Cache server Redis Should NOT be exposed (no auth by default)
8006 Proxmox host Proxmox UI Should NOT be on internet
8080 Various HTTP alt Investigate — could be anything
9090 Monitoring server Prometheus Should be internal only
9100 Monitored hosts Node Exporter Should be internal only

The Security Hardening Checklist

After scanning and testing, work through this checklist to harden your homelab:

Network Level

[ ] Default firewall policy is DROP (not ACCEPT)
[ ] Only necessary ports are forwarded from the internet
[ ] VLANs separate management, servers, IoT, and guest traffic
[ ] Inter-VLAN routing has explicit allow rules (not allow-all)
[ ] DNS queries from IoT VLAN are restricted to your DNS server
[ ] UPnP is disabled on your router
[ ] IPv6 firewall rules match IPv4 rules (or IPv6 is disabled if unused)
[ ] WiFi uses WPA3 (or WPA2 with a strong password)
[ ] Guest WiFi is isolated from your LAN

SSH

[ ] Password authentication disabled (key-only)
[ ] Root login disabled (or key-only)
[ ] SSH on non-default port (optional, reduces log noise)
[ ] Fail2ban or similar for brute force protection
[ ] SSH keys are Ed25519 or RSA 4096-bit
[ ] Unused SSH keys removed from authorized_keys
[ ] SSH agent forwarding disabled (unless needed)
# Recommended /etc/ssh/sshd_config settings
cat >> /etc/ssh/sshd_config.d/hardening.conf << 'EOF'
PasswordAuthentication no
PermitRootLogin prohibit-password
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
PermitEmptyPasswords no
X11Forwarding no
MaxAuthTries 3
AllowAgentForwarding no
AllowTcpForwarding no
ClientAliveInterval 300
ClientAliveCountMax 2
EOF

sudo systemctl restart sshd

Services

[ ] All management interfaces behind VPN or internal-only
[ ] No services running with default credentials
[ ] All web services use HTTPS (self-signed minimum)
[ ] Unused services stopped and disabled
[ ] Services run as non-root users where possible
[ ] Docker containers don't run in privileged mode (unless required)
[ ] Docker containers don't mount the Docker socket (unless required)
[ ] Container images are regularly updated
[ ] Databases are not exposed outside localhost/Docker network

Updates

[ ] Automatic security updates enabled (unattended-upgrades or dnf-automatic)
[ ] Container images updated at least monthly
[ ] Firmware updates applied (router, IPMI/iDRAC, drive firmware)
[ ] A process exists for tracking CVEs relevant to your stack
# Enable automatic security updates on Debian/Ubuntu
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

# Enable on Fedora
sudo dnf install dnf5-plugin-automatic
sudo systemctl enable --now dnf5-automatic.timer

Monitoring and Logging

[ ] Failed SSH login attempts are logged and monitored
[ ] Firewall deny logs are enabled
[ ] Suricata or similar IDS is running
[ ] Logs are shipped to a central location (not just local)
[ ] Alerting is configured for critical security events
[ ] Log retention is at least 30 days

Scheduling Regular Audits

Security isn't a one-time thing. Schedule regular audits:

Audit Type Frequency Tool Time Required
Port scan Monthly Nmap 15 minutes
Vulnerability scan Monthly OpenVAS 1-2 hours (automated)
Certificate check Monthly openssl/testssl.sh 10 minutes
Docker image scan Weekly Trivy 5 minutes (automated)
Firewall review Quarterly Manual 30 minutes
Password audit Quarterly Manual/Hydra 30 minutes
Full security review Annually All of the above Half day

Automated Monthly Scan Script

#!/bin/bash
# monthly-security-audit.sh

AUDIT_DIR="/home/user/security-audits/$(date +%Y-%m)"
mkdir -p "$AUDIT_DIR"

echo "=== Monthly Security Audit: $(date) ===" | tee "$AUDIT_DIR/summary.txt"

# 1. Network scan
echo ""
echo "--- Running Nmap scan ---"
sudo nmap -A -T4 192.168.1.0/24 -oA "$AUDIT_DIR/nmap-full" 2>&1 | tail -5

# 2. Compare with last month
LAST_MONTH=$(date -d "last month" +%Y-%m)
if [ -f "/home/user/security-audits/$LAST_MONTH/nmap-full.xml" ]; then
    echo ""
    echo "--- Changes since last month ---"
    ndiff "/home/user/security-audits/$LAST_MONTH/nmap-full.xml" \
          "$AUDIT_DIR/nmap-full.xml" | tee "$AUDIT_DIR/changes.txt"
fi

# 3. Docker image vulnerabilities
echo ""
echo "--- Docker image vulnerabilities ---"
docker ps --format '{{.Image}}' | sort -u | while read img; do
    trivy image --severity HIGH,CRITICAL --quiet "$img" 2>/dev/null
done | tee "$AUDIT_DIR/docker-vulns.txt"

# 4. Certificate expiration check
echo ""
echo "--- Certificate expiration ---"
for host in vault.homelab.local grafana.homelab.local; do
    EXPIRY=$(echo | openssl s_client -connect "$host:443" -servername "$host" 2>/dev/null | \
      openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
    echo "$host: expires $EXPIRY"
done | tee "$AUDIT_DIR/cert-check.txt"

echo ""
echo "Audit complete. Results saved to $AUDIT_DIR/"
# Schedule it with cron
# Run on the 1st of every month at 3 AM
echo "0 3 1 * * /home/user/scripts/monthly-security-audit.sh" | crontab -

Responding to Findings

When your audit finds something, don't panic. Work through it methodically:

  1. Classify: Is it critical (actively exploitable), important (should fix soon), or informational?
  2. Understand: What's the actual risk? A medium-severity finding on an internal-only service is different from a medium finding on an internet-facing service.
  3. Prioritize: Fix critical and internet-facing issues first.
  4. Fix: Apply the fix (patch, configuration change, firewall rule).
  5. Verify: Re-scan to confirm the fix worked.
  6. Document: Record what you found and what you did about it.

Keep a simple log:

## 2026-02-09 Audit Findings

### Critical
- None

### High
- [FIXED] Grafana running on port 3000 accessible from internet (closed firewall rule)
- [FIXED] PostgreSQL on nas-01 accepting connections from any IP (bound to localhost)

### Medium
- [FIXED] SSH password auth still enabled on proxmox-01 (disabled, key-only now)
- [ACCEPTED] Self-signed certificates on internal services (expected, internal only)

### Low
- [FIXED] SNMP community string "public" on router (changed to random string)

Final Thoughts

A security audit sounds intimidating, but it's really just answering the question: "What's running on my network, and should it be?" Nmap answers the first part, vulnerability scanners and IDS answer the second, and the hardening checklist gives you a concrete list of things to fix.

The most important takeaway: do this regularly. A monthly Nmap scan takes 15 minutes and will catch the port you forgot to close, the service you forgot was running, and the container that updated itself to a vulnerable version. Make it a habit, and your homelab will be significantly more secure than most.