Security Hardening Guide¶
Security configuration to protect against script kiddies, malware, and DoS attacks.
Threat Model¶
| Threat | Likelihood | Mitigation |
|---|---|---|
| Script kiddies | High | Fail2ban, rate limiting, no exposed ports |
| Malware/botnets | Medium | 2FA, updates, network segmentation |
| DoS attacks | Medium | Cloudflare, rate limiting, geo-blocking |
| Credential stuffing | Medium | 2FA, strong passwords, Vaultwarden |
| Doxxing | Low | WHOIS privacy, Cloudflare proxy |
Attack Surface Minimization¶
Public Exposure¶
Multiple services on the VPS are exposed to the internet via Caddy reverse proxy. All fixed homelab services are behind Tailscale (no public ports).
Internet Access (VPS — Caddy reverse proxy):
├── hs.cronova.dev (Headscale) ← Tailscale coordination
│ └── Port 443 (HTTPS)
│ └── Port 3478 (STUN/DERP)
├── status.cronova.dev (Uptime Kuma) ← Public status page
├── notify.cronova.dev (ntfy) ← Push notifications
├── cronova.dev (Landing page) ← Static HTML
│
└── Everything else via Tailscale mesh (no public ports)
├── Home Assistant
├── Jellyfin
├── Vaultwarden
├── Frigate
└── All internal services
VPS Firewall (UFW)¶
# Reset and configure UFW
sudo ufw reset
sudo ufw default deny incoming
sudo ufw default allow outgoing
# SSH (consider changing port)
sudo ufw allow 22/tcp
# Headscale only
sudo ufw allow 443/tcp
sudo ufw allow 3478/udp
# Tailscale (auto-managed, but explicit)
sudo ufw allow in on tailscale0
# Enable
sudo ufw enable
sudo ufw status verbose
Fixed Homelab Firewall (OPNsense)¶
All traffic filtered through OPNsense VM (gateway since 2026-02-21):
- WAN: Block all inbound (no port forwards)
- LAN: Allow outbound, block inter-VLAN
- IoT VLAN (10): Configured, rules pending
- Guest VLAN (20): Configured, rules pending
- Access via Tailscale only
Authelia Forward Auth — Deployed¶
Authelia (Okẽ) provides SSO + TOTP 2FA for services behind Caddy on Docker VM:
- Protected: Ysyry (Dozzle), Kuatia (BentoPDF), Mbyja (Homepage), Papa (Grafana), Aranduka (Paperless-ngx)
- Own auth (not protected): Jara (HA), Taguato (Frigate), Vaultwarden, Vera (Immich), Forgejo, Yrasema (Jellyfin — mobile/TV clients can't handle redirects)
- Notifier: Filesystem (writes codes to
/data/notification.txt), not SMTP - 2FA: TOTP via Authy app
Two-Factor Authentication (2FA)¶
Hardware Key¶
YubiKey 5C NFC available for hardware-based 2FA:
- USB-C + NFC for phone/laptop
- Supports FIDO2, WebAuthn, TOTP
- Use for most critical accounts (Vaultwarden master, Cloudflare, GitHub)
Service 2FA Matrix¶
| Service | 2FA Method | Priority | Status |
|---|---|---|---|
| Vaultwarden | TOTP or YubiKey | Critical | Available |
| Authelia (Okẽ) | TOTP via Authy | Critical | Active (protects 6 services) |
| Headscale | OIDC + 2FA | Critical | Pending (CLI-only for now) |
| Proxmox | TOTP or YubiKey | Critical | Available |
| Home Assistant | TOTP | High | Available |
| OPNsense | TOTP | High | Pending |
| Start9 | TOTP | High | Pending |
| Jellyfin | None (behind Authelia) | Low | Protected via forward auth |
| *arr stack | None (internal only) | Low | — |
Vaultwarden 2FA Setup¶
Enforce 2FA for all users¶
Add to docker-compose.yml environment:
Headscale OIDC Integration¶
For web-based admin with 2FA, integrate with an OIDC provider.
Option 1: Authelia (self-hosted)¶
# docker-compose.yml addition
authelia:
image: authelia/authelia:latest
volumes:
- ./authelia:/config
environment:
- TZ=America/Asuncion
Option 2: Use pre-auth keys only (simpler)¶
No web admin exposed. Manage via CLI:
# All admin via SSH + CLI
docker exec headscale headscale users list
docker exec headscale headscale nodes list
Proxmox 2FA Setup¶
- Datacenter → Permissions → Two Factor
- Add TOTP for root user
- Require 2FA for all admin users
Home Assistant 2FA¶
- Profile → Multi-factor Authentication
- Enable "Authenticator app"
- Scan QR code with authenticator
OPNsense 2FA¶
- System → Access → Servers → Add
- Type: Local + Timebased One-time Password
- System → Access → Users → Edit
- OTP seed: Generate new
Fail2ban Configuration¶
VPS Installation¶
SSH Protection¶
# /etc/fail2ban/jail.local
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 3
banaction = ufw
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 24h
Headscale Protection¶
# /etc/fail2ban/jail.local
[headscale]
enabled = true
port = 443
filter = headscale
logpath = /var/log/headscale/headscale.log
maxretry = 5
bantime = 1h
# /etc/fail2ban/filter.d/headscale.conf
[Definition]
failregex = ^.*Failed authentication.*from <HOST>.*$
^.*Invalid token.*from <HOST>.*$
ignoreregex =
Docker Container Protection¶
For services in Docker, use fail2ban with Docker logs:
# /etc/fail2ban/jail.local
[vaultwarden]
enabled = true
port = 443
filter = vaultwarden
logpath = /var/lib/docker/containers/*vaultwarden*/*-json.log
maxretry = 3
bantime = 24h
# /etc/fail2ban/filter.d/vaultwarden.conf
[Definition]
failregex = ^.*"Username or password is incorrect".*"Client":\s*"<HOST>".*$
ignoreregex =
Fail2ban Commands¶
# Check status
sudo fail2ban-client status
# Check specific jail
sudo fail2ban-client status sshd
# Unban IP
sudo fail2ban-client set sshd unbanip 1.2.3.4
# View banned IPs
sudo fail2ban-client get sshd banned
Rate Limiting¶
Caddy Rate Limiting¶
# VPS Caddyfile - rate limit Headscale
hs.cronova.dev {
rate_limit {
zone headscale {
key {remote_host}
events 100
window 1m
}
}
reverse_proxy localhost:8080
}
OPNsense Rate Limiting¶
- Firewall → Settings → Advanced
- Enable "Firewall Adaptive Timeouts"
- Firewall → Aliases → Add
- Name: rate_limit_block
- Type: URL Table (IPs)
DNS Privacy¶
Pi-hole Upstream (DNS-over-HTTPS)¶
Use Cloudflared for encrypted DNS:
# Add to Pi-hole docker-compose.yml
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
command: proxy-dns
environment:
- TUNNEL_DNS_UPSTREAM=https://1.1.1.1/dns-query,https://1.0.0.1/dns-query
- TUNNEL_DNS_PORT=5053
- TUNNEL_DNS_ADDRESS=0.0.0.0
networks:
- pihole-net
Pi-hole configuration:
- Custom upstream DNS:
cloudflared#5053
Alternative: Unbound with DoT¶
# /etc/unbound/unbound.conf.d/dns-over-tls.conf
forward-zone:
name: "."
forward-tls-upstream: yes
forward-addr: 1.1.1.1@853#cloudflare-dns.com
forward-addr: 1.0.0.1@853#cloudflare-dns.com
IP Privacy & Anti-Doxxing¶
Cloudflare Proxy¶
All public domains use Cloudflare proxy (orange cloud):
| Domain | Proxy | Notes |
|---|---|---|
| cronova.dev | Yes | Static site |
| hs.cronova.dev | No | Headscale needs direct IP |
| Yes | When purchased |
For Headscale: IP is exposed, but:
- Only serves Tailscale clients
- Fail2ban protects against abuse
- Can geo-block if needed
WHOIS Privacy¶
Ensure WHOIS privacy is enabled:
- Cloudflare Registrar - Privacy included free
- Other registrars - Enable WHOIS privacy/redaction
Verify:
Email Privacy¶
- Don't use personal email in public configs
- Use domain email:
<admin@cronova.dev> - Forward to personal email privately
Git Privacy¶
Check for exposed info:
# Search for emails in repo
git log --all --format='%ae' | sort -u
# Search for potential secrets
grep -r "password\|secret\|key\|token" --include="*.yml" --include="*.md"
Container Security¶
Docker Hardening¶
# docker-compose.yml security options
services:
app:
security_opt:
- no-new-privileges:true
read_only: true # Where possible
user: "1000:1000" # Non-root
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Only if needed
Image Security¶
# Use specific tags, not :latest in production
image: vaultwarden/server:1.30.1
# Scan images for vulnerabilities
docker scout cves vaultwarden/server:latest
Network Isolation¶
# Separate networks per service group
networks:
frontend:
internal: false
backend:
internal: true # No internet access
Update Strategy¶
Automatic Security Updates¶
# Install unattended-upgrades
sudo apt install unattended-upgrades
# Configure
sudo dpkg-reconfigure unattended-upgrades
# /etc/apt/apt.conf.d/50unattended-upgrades
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
};
Unattended-Upgrade::Automatic-Reboot "false";
Container Updates¶
# Weekly container update script
#!/bin/bash
cd /opt/homelab/docker/vps
docker compose pull
docker compose up -d
# Notify
curl -d "VPS containers updated" https://notify.cronova.dev/cronova-info
Watchtower (Automated) — Deployed¶
# Deployed on Docker VM (maintenance stack)
watchtower:
image: nicholas-fedor/watchtower:1.14.2 # Maintained fork (containrrr abandoned/Docker 29+ incompatible)
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_SCHEDULE=0 0 4 * * 0 # Sundays 4 AM
- WATCHTOWER_LABEL_ENABLE=true # Opt-in via container labels
Update strategy (opt-in via com.centurylinklabs.watchtower.enable=true label):
| Category | Services | Strategy |
|---|---|---|
| Pinned (manual bump) | victoriametrics, vmagent, vmalert, alertmanager, grafana, authelia, paperless-ngx | Version pinned in compose — Watchtower label present but no-op |
| Excluded (no label) | vaultwarden, frigate, homeassistant, immich-db, immich-valkey, paperless-db, paperless-redis | No Watchtower label — manual only |
| Excluded (label=false) | caddy (Docker VM) | Explicitly disabled — custom build with Cloudflare plugin |
| Auto-updated | dozzle, bentopdf, homepage, cadvisor, glances, sonarr, radarr, prowlarr, qbittorrent, jellyfin, mosquitto, pihole, immich-server, immich-ml + backup sidecars | Low-risk or stateless — Watchtower updates on schedule |
Monitoring & Alerts¶
Security Alerts in Uptime Kuma¶
Add monitors for security events:
| Monitor | Type | Alert |
|---|---|---|
| VPS SSH | TCP 22 | If down, possible attack |
| Fail2ban status | Push | On ban events |
| UFW logs | Push | On blocked connections |
Log Monitoring¶
# Watch auth failures
sudo tail -f /var/log/auth.log | grep -i "failed\|invalid"
# Watch UFW blocks
sudo tail -f /var/log/ufw.log
Fail2ban Notifications¶
# /etc/fail2ban/action.d/ntfy.conf
[Definition]
actionban = curl -d "Banned <ip> from <name> jail" https://notify.cronova.dev/cronova-warning
actionunban = curl -d "Unbanned <ip> from <name> jail" https://notify.cronova.dev/cronova-info
Backup Security¶
Encrypted Backups¶
Restic encrypts by default:
Backup Key Storage¶
- Store restic password in Vaultwarden
- Paper backup in secure location
- Never commit password to git
Offsite Encryption¶
rclone crypt adds additional layer:
# Google Drive data is encrypted client-side
rclone config
# Type: crypt
# Remote: gdrive:homelab-backup
# Password: (different from restic password)
Incident Response¶
If VPS Compromised¶
- Isolate: Remove from Tailscale
- Revoke: Invalidate all Headscale auth keys
- Rotate: Change all passwords/keys
- Rebuild: Fresh VPS from backup
- Audit: Check access logs
If Credentials Leaked¶
- Change Vaultwarden master password
- Rotate all stored passwords
- Revoke all Tailscale pre-auth keys
- Check for unauthorized devices in mesh
If DoS Attack¶
- Cloudflare: Enable "Under Attack" mode
- Geo-block: Block attacking countries
- Rate limit: Increase restrictions
- Report: To VPS provider
Security Checklist¶
Initial Setup¶
- [ ] UFW configured and enabled
- [ ] SSH key-only authentication
- [ ] SSH root login disabled
- [ ] Fail2ban installed and configured
- [ ] 2FA enabled on Vaultwarden
- [ ] 2FA enabled on Proxmox
- [ ] WHOIS privacy verified
- [ ] Cloudflare proxy enabled (where applicable)
Monthly Review¶
- [ ] Check fail2ban ban logs
- [ ] Review Uptime Kuma alerts
- [ ] Verify backups are encrypted
- [ ] Check for system updates
- [ ] Review Tailscale device list
- [ ] Rotate any compromised credentials
After Incident¶
- [ ] Document what happened
- [ ] Identify root cause
- [ ] Implement fixes
- [ ] Update this document
Quick Reference¶
Emergency Commands¶
# Block IP immediately
sudo ufw deny from 1.2.3.4
# Check active connections
sudo netstat -tulpn
# Kill suspicious process
sudo kill -9 <pid>
# Check for rootkits
sudo rkhunter --check
# View recent logins
last -n 20
# Check for unauthorized SSH keys
cat ~/.ssh/authorized_keys
Security Tools¶
| Tool | Purpose | Install |
|---|---|---|
| fail2ban | Ban brute forcers | apt install fail2ban |
| ufw | Firewall | apt install ufw |
| rkhunter | Rootkit detection | apt install rkhunter |
| lynis | Security audit | apt install lynis |
| clamav | Antivirus | apt install clamav |