Security: What I Actually Do
No theoretical checklist—this is what’s in place and why.
SSH: keys only, no passwords
On every box that has SSH (Desktop Host Windows, NAS Host, all four Ubuntu VMs, and the Proxy Host):
- Password auth disabled. Only public-key auth. Same key pair across all hosts; I use one SSH config and one key path.
- Windows (Desktop Host): OpenSSH Server with a custom firewall rule: TCP 22 allowed only from the LAN subnet. The default “OpenSSH” rule is disabled so the scope is explicit. Authorized keys live in the usual Windows path for admin accounts.
- NAS Host (Ubuntu): SSH enabled, password auth off, key-based only. UFW restricts access to LAN only. fail2ban sshd jail active.
- Ubuntu VMs and Proxy Host: Same key; fail2ban on top (see below). SSH is not exposed to the internet.
So even if something on the LAN is compromised, an attacker still needs the key. And nothing is listening for SSH from the internet.
Router (MikroTik hEX)
- No management from WAN. SSH and Winbox/WebFig are only reachable from the LAN. No port forwards for the router itself.
- Firewall: Strict default-deny on both input and forward chains. Only established connections and explicitly port-forwarded traffic are allowed inbound. Management protocols are restricted to LAN only.
- Brute-force protection: Multi-stage address lists. After a few failed SSH attempts from an IP, that IP is added to a temporary blacklist (e.g. 24 hours). So even from the LAN, repeated guessing gets blocked.
- Source NAT: Masquerade for outbound is restricted to the LAN subnet. That way when traffic is port-forwarded in, the source IP isn’t rewritten from the router’s perspective and nginx sees the real client IP—which I use for allow/deny rules.
TLS and nginx
- Protocols: TLS 1.2 and 1.3 only; no TLS 1.0/1.1. Set in the main
nginx.conf. - Ciphers: Modern suite (ECDHE, AES-GCM, CHACHA20-POLY1305). No legacy ciphers.
- Headers: HSTS, X-Content-Type-Options, X-Frame-Options, Content-Security-Policy where it makes sense. Same pattern for all public server blocks.
- LAN-only blocks: Admin UIs (Pi-hole, Uptime Kuma, Grafana) use
allow <LAN subnet>; deny all;so those hostnames return 403 from the internet. server_tokens off: nginx doesn’t reveal its version number in error pages or theServerresponse header.- Rate limiting on Vaultwarden auth: Authentication endpoints are rate-limited per IP to prevent credential stuffing.
- Bad-path blocking (444): A shared snippet included in every public server block silently drops common scanner and exploit probes — no response, no version information.
- WPAD suppression: Windows/macOS clients probe
/wpad.daton every HTTP request (proxy auto-detection). nginx serves a direct “no proxy” PAC response at the HTTP level so requests never redirect to HTTPS and never show as 404s in the access log.
Fail2ban on all Ubuntu hosts
On all six Ubuntu hosts — four VMs on the Desktop Host (Docker host, nginx proxy, Minecraft, Stoat Chat), the Proxy Host, and the NAS Host:
- SSH jail (
sshd): On all hosts. Repeated failed login attempts trigger a temporary IP ban. - nginx 4xx jail (
nginx-4xx): On nginx hosts. Bans IPs generating excessive client errors in a short window. Catches scanners probing for common exploit paths. LAN IPs are always exempt. - Minecraft connection flood jail (
minecraft-conn): On proxy hosts. Bans IPs flooding the game port with excessive connections. - Ignore: LAN subnet and localhost are in
ignoreipso I never ban my own machines. - Config: Tiered
jail-*.localtemplates in the repo; each deployed based on the host’s role.
Keepalived
VRRP is authenticated so a random device on the LAN can’t claim the VIP. The secret is in my local values file, not in the repo. If the primary node’s nginx goes down, the VIP fails over to the secondary automatically.
Container hardening
- Docker image pinning: All Docker Compose services use pinned image tags — no
:latestin production. This prevents silent image changes on restart. - Resource limits: Every Compose service defines memory and CPU limits. A runaway container can’t OOM-kill its neighbors.
iptables: falseon the Proxy Host: Docker is configured not to manipulate iptables, so it can’t bypass UFW firewall rules. All port exposure is explicit.- Security options: Containers that don’t need elevated privileges use
cap_drop: ALLandsecurity_opt: no-new-privileges:true.
Secrets and repo
- No real values in git. All configs and docs in the repo use placeholders (
detellem.com,<SUBNET>, etc.). Real values live invalues.yaml.local, which is gitignored. A validation script (make test) checks that no value from that file appears in any committed file—so I can’t accidentally commit an IP or key. - Passwords and keys: Stored in Vaultwarden or in the local values file. Nothing sensitive in the repo.
- Backup passphrases: GPG passphrases for encrypted backups live on the source hosts only, not in the repo or values file.
Future improvements
- VLAN segmentation — Isolate guest and IoT traffic from infrastructure.
- Source-IP restrictions on management access — Tighten SSH and admin UI access beyond LAN-only.
- VPN for remote administration — Secure remote access to the LAN.
- TLS for all internal management interfaces — Extend the wildcard cert to cover every internal UI.