Homelab Network Security Audit: Scanning, Testing, and Hardening
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.

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:
- Exposed management interfaces (Proxmox, TrueNAS, IPMI) to the internet
- Services with default credentials (admin/admin on routers, NAS devices)
- Outdated software with known CVEs
- Unnecessary open ports
- Weak SSH configurations (password auth, root login)
- Docker containers running as root with host networking
- No firewall rules between VLANs (flat network)
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
- Log into the Greenbone web interface
- Go to Scans > Tasks > New Task
- 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)
- 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:
- Classify: Is it critical (actively exploitable), important (should fix soon), or informational?
- 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.
- Prioritize: Fix critical and internet-facing issues first.
- Fix: Apply the fix (patch, configuration change, firewall rule).
- Verify: Re-scan to confirm the fix worked.
- 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.