Sanchez Home Network — Rod-Server Infrastructure
Version 1.3.1 — 2026-02-18 — Classification: INTERNAL
This plan transforms the Sanchez Home Network from a flat, trust-all architecture into a segmented, zero-trust infrastructure capable of safely hosting public web services while protecting sensitive internal systems.
10.0.1.6.*.sanchezfamily.ca -> 10.0.1.6) so clients using router DNS still resolve internal subdomains.forwardAuth + full route protection tests.cloudflared on Rod-Server after full DMZ tunnel cutover to 10.0.1.5.| Asset | Sensitivity | Location | Impact if Compromised |
|---|---|---|---|
| Vaultwarden passwords | CRITICAL | Rod-Server | Total identity compromise |
| SSH keys (agent mesh) | CRITICAL | Rod-Server ~/.ssh/ | Lateral movement to all hosts |
| Docker socket | CRITICAL | Rod-Server | Container escape = root on host |
| MariaDB databases | HIGH | Rod-Server Docker | Data breach (Nextcloud, n8n, crypto) |
| PostgreSQL (LibroSynth) | HIGH | Rod-Server Docker | Research data loss |
| Cloudflare API keys | HIGH | _rod/.env | Domain hijacking |
| Omada controller | HIGH | Rod-Server Docker | Full network control |
| Home Assistant | MEDIUM | Rod-Server Docker | Physical home access (locks, cameras) |
| Personal files | MEDIUM | Rod-Server /media/rod/ | Privacy breach |
| Actor | Motivation | Capability | Likely Attack Vector |
|---|---|---|---|
| Automated bots | Crypto mining, DDoS recruitment | Low-Medium | Scanning open ports, default creds, known CVEs |
| Script kiddies | Defacement, bragging | Low | Public-facing services, weak passwords |
| Targeted attacker | Data theft, ransomware | Medium-High | Cloudflare bypass, supply chain, phishing |
| Insider (compromised IoT) | Lateral movement | Medium | Flat network, no segmentation |
*.sanchezfamily.ca, including phpMyAdmin (database admin), Portainer (Docker control), Terminal (shell access), and n8n (arbitrary code execution). This plan eliminates all unprotected public access.
Tracked vulnerabilities with current remediation status as of the 2026-02-15 lockdown.
| # | Vulnerability | Severity | Mitigation | Status |
|---|---|---|---|---|
| 1 | Admin panels public (Portainer, phpMyAdmin, Dockge, Terminal) | CRITICAL | lan-only middleware (immediate) + Authelia 2FA (Phase 1) |
MITIGATED |
| 2 | Flat network — no segmentation | HIGH | VLANs + WireGuard (Phase 3-4) | Open |
| 3 | No MFA on any service | HIGH | Authelia TOTP (Phase 1) | Open |
| 4 | Dual cloudflared conflict | HIGH | Consolidate to TinyDesk (Phase 2) | Open |
| 5 | UFW too permissive (DNS, Omada from anywhere) | HIGH | UFW hardening (Phase 2) | FIXED |
| 6 | No IDS/IPS | MEDIUM | CrowdSec (Phase 1) | Open |
| 7 | Data at rest unencrypted | MEDIUM | LUKS encryption (Phase 3) | Open |
| 8 | Docker socket exposed to containers | HIGH | Restrict to Traefik + Portainer only | Open |
| 9 | Hardcoded credentials in compose files | MEDIUM | Move to .env / Docker secrets (Phase 3) | FIXED |
| 10 | No centralized logging | MEDIUM | Grafana + Loki (Phase 4) | Open |
| 11 | DOCKER-USER chain empty (Docker bypasses UFW) | CRITICAL | Populate with DROP rules | FIXED |
| 12 | No HTTP→HTTPS redirect | HIGH | https-redirect middleware |
FIXED |
| 13 | Missing security headers | MEDIUM | security-headers middleware |
FIXED |
| 14 | Cloudflare real-IP plugin broken | HIGH | Populate trustedCIDRs |
FIXED |
| 15 | Database connections unencrypted | HIGH | Enable TLS on MariaDB/PostgreSQL | FIXED — MariaDB: require_secure_transport=ON, TLS_AES_256_GCM_SHA384; PostgreSQL: TLSv1.3, TLS_AES_256_GCM_SHA384 |
| 16 | IoT DNS pinhole to Infrastructure | MEDIUM | Rate-limit + restrict to UDP 53 only; consider secondary DNS forwarder | Open — see Section 7.5 |
| 17 | Container images pulled by tag (mutable) | MEDIUM | Pin images by digest (SHA256) for immutability | FIXED — 11 images pinned by SHA256 (8 hosting + 3 LibroSynth) |
| Item | Status | Details |
|---|---|---|
| ER605 firmware | CURRENT | v2.3.3 — patches CVE-2025-7850 (CVSS 9.3), CVE-2025-7851 (CVSS 8.7) |
| Vaultwarden signups | FIXED | SIGNUPS_ALLOWED=false, INVITATIONS_ALLOWED=false |
| Vaultwarden network | ISOLATED | Dedicated vault_isolated network (172.30.0.0/28) — only 2 containers |
| Vaultwarden filesystem | HARDENED | read_only: true, no-new-privileges:true |
| Admin panel | LAN ONLY | /admin blocked from internet via Traefik ipAllowList |
| Brute force protection | ACTIVE | Login: 3 attempts/60s, Admin: 3 attempts/60s |
| Security headers | APPLIED | CSP, HSTS (1 year), Permissions-Policy, X-Frame DENY |
| Item | Status | Details |
|---|---|---|
lan-only middleware | APPLIED | 19 admin routers — Portainer, phpMyAdmin, Dockge, Terminal, Traefik dashboard, Pi-hole, Omada, MCP, Filebrowser, Frigate, go2rtc, Ollama, Grafana, qBittorrent, Dashboard |
cloudflare-only middleware | APPLIED | 18 public routers — Nextcloud, n8n, Home Assistant, Forgejo, Kavita, Audiobooks, Plex, etc. |
https-redirect middleware | APPLIED | All 47 HTTP routers redirect 301 to HTTPS |
security-headers middleware | APPLIED | All 36+ HTTPS routers — HSTS, XSS, nosniff, frame options, referrer, permissions |
cloudflare-real-ip fix | FIXED | Populated trustedCIDRs with 15 Cloudflare IP ranges |
| UFW DNS restriction | FIXED | Port 53 restricted to LAN-only (10.0.1.0/24) |
| UFW Omada restriction | FIXED | Ports 29810-29814 restricted to LAN-only (10.0.1.0/24) |
| DOCKER-USER iptables | APPLIED | DROP rules for ports 5433, 5434, 15672, 9002, 9003, 9091, 8089, 8053, 1984 |
| DOCKER-USER persistence | ACTIVE | systemd service docker-user-rules.service — rules survive reboot |
| VPN credentials | FIXED | Hardcoded VPN creds moved from docker-compose.yml to .env |
| Item | Status | Details |
|---|---|---|
| Traefik rate limiting | APPLIED | auth-rate-limit middleware: 5 req/s sustained, burst 10 |
| Traefik body limit | APPLIED | body-limit middleware: maxRequestBodyBytes 2MB |
| Traefik circuit breaker | APPLIED | circuit-breaker middleware: trips at >25% 5xx errors |
| Secure middleware chain | APPLIED | secure-chain compositions combining security-headers, rate-limit, body-limit, circuit-breaker |
| TLS hardening | APPLIED | sniStrict: true, ECDSA cipher suites: TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 |
| serversTransport timeouts | APPLIED | dialTimeout: 5s, responseHeaderTimeout: 30s, idleConnTimeout: 30s (Slowloris defense) |
| MariaDB TLS | ACTIVE | require_secure_transport=ON, cipher: TLS_AES_256_GCM_SHA384 |
| PostgreSQL TLS | ACTIVE | TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, certs in persistent volume |
| Trivy container scanning | ACTIVE | v0.69.1 installed, weekly cron scan Sundays 3AM |
| Pi-hole blocklists | ACTIVE | 1,442,496 gravity domains — 6 new C2/malware/phishing feeds from abuse.ch, NoTracking, RPiList |
| Image digest pinning | APPLIED | 11 images pinned by SHA256 (8 hosting + 3 LibroSynth) — prevents supply chain substitution |
.sanchezfamily.ca but NOT .home routes. Authelia protects both, works offline, and is self-hosted with no external dependency.
.env file, the attacker cannot authenticate without the TOTP device. This is the highest practical authorization level.
Beyond the Tunnel and Access already in the plan, the Cloudflare One platform offers additional components that could replace or complement local tools:
| Component | Replaces | Phase | Benefit | Trade-off |
|---|---|---|---|---|
| Tunnel (cloudflared) | Port forwarding | Phase 2 (active) | Zero inbound ports, encrypted QUIC | Cloudflare dependency |
| Access (Zero Trust) | Edge auth | Phase 2 (active) | SSO/MFA at edge, identity-aware | Does not protect .home routes |
| WARP (device client) | Tailscale mesh | Phase 5 (evaluate) | Secure remote access without VPN ports | Requires WARP client on devices |
| Gateway (DNS filtering) | Pi-hole (for IoT) | Phase 5 (evaluate) | Edge-based DNS filtering, works off-network | Loses local DNS cache + ad blocking |
| Browser-Rendered SSH | wetty Terminal containers | Phase 2 (evaluate) | Eliminates 3 terminal containers, centralizes SSH | Requires internet for local SSH |
| R2 (object storage) | Local backup storage | Phase 5 | Off-site encrypted backups, S3-compatible, $0 egress | Monthly storage cost |
| Level | Requirement | Applied To |
|---|---|---|
two_factor | Password + TOTP (mandatory) | All admin/management panels |
one_factor | Password only | Low-risk services (Kavita, Frigate camera feeds) |
bypass | No authentication | Services with own auth (Nextcloud, Plex, Vaultwarden), health endpoints |
| Service | Auth Level | Why |
|---|---|---|
| Portainer | two_factor | Full Docker control = root equivalent access |
| phpMyAdmin | two_factor | Direct SQL access to all databases |
| Dockge | two_factor | Docker compose editor = arbitrary container deployment |
| Terminal (wetty) | two_factor | Shell access to the server |
| Traefik dashboard | two_factor | Reveals entire routing topology and internal IPs |
| Pi-hole admin | two_factor | DNS manipulation enables phishing attacks |
| n8n | two_factor | Workflow automation = arbitrary code execution |
| Grafana | two_factor | May expose internal metrics and secrets |
| Home Assistant | two_factor | Physical home control (locks, lights, cameras) |
| Omada controller | two_factor | Full network management and configuration |
| MCP Proxy | two_factor | Database, Docker, and LLM access proxy |
| Filebrowser | two_factor | Filesystem read/write access |
| Ollama | two_factor | LLM API — resource abuse and data exfiltration |
| Prometheus | two_factor | Internal metrics exposure |
| Setting | Value |
|---|---|
| User Store | File-based (users_database.yml) |
| Session Backend | File-based (no external Redis) |
| Storage Backend | SQLite (encrypted at rest via LUKS) |
| Second Factor | TOTP (RFC 6238) |
| Session Duration | 12 hours active, 30 days remember-me |
| Session Cookie | Encrypted, Secure, SameSite=Lax |
| Password Hashing | Argon2id (64MB, 3 iterations, 4 parallelism) |
Cloudflare Access and Authelia serve complementary roles and can coexist without conflict:
| Layer | Tool | Function | Scope |
|---|---|---|---|
| Edge (internet) | Cloudflare Access | Authentication — Who are you? | *.sanchezfamily.ca only |
| Ingress (local) | Authelia | Authorization + 2FA — What can you do? | Both *.sanchezfamily.ca and *.home |
Cf-Access-Jwt-Assertion header — Traefik/Authelia can optionally validate this to trust the edge identity.home access where Cloudflare has no visibility| Connection | Protocol | Status |
|---|---|---|
| Internet → Cloudflare | TLS 1.3 | ACTIVE |
| Cloudflare → TinyDesk | Cloudflare Tunnel (QUIC+TLS) | ACTIVE |
| TinyDesk → Rod-Server | WireGuard (ChaCha20-Poly1305) | PHASE 3 |
| Browser → *.sanchezfamily.ca | TLS 1.2+ (Let's Encrypt) | ACTIVE |
| Browser → *.home | TLS 1.2+ (Local CA) | ACTIVE |
| Apps → MariaDB | TLS 1.2+ (self-signed, TLS_AES_256_GCM_SHA384) | ACTIVE |
| Apps → PostgreSQL | TLSv1.3 (self-signed, TLS_AES_256_GCM_SHA384) | ACTIVE |
| SSH between hosts | SSH (Ed25519) | ACTIVE |
| Data | Method | Status |
|---|---|---|
| Vaultwarden DB (passwords) | LUKS2 + AES-256-XTS | PHASE 3 |
| MariaDB data directory | LUKS2 + AES-256-XTS | PHASE 3 |
| TLS certificates & private keys | LUKS2 + AES-256-XTS | PHASE 3 |
| .env credentials file | LUKS2 (same volume) | PHASE 3 |
| Backups | GPG symmetric (AES-256) | PHASE 4 |
Target: Create an encrypted volume at /home/rod/_rod/_hosting/encrypted/ holding Vaultwarden DB, MariaDB data, Traefik certs, and .env credentials. Auto-unlock via systemd + TPM or passphrase keyslot at boot.
| VLAN | Name | Subnet | Gateway | Purpose |
|---|---|---|---|---|
| 1 | Home | 10.0.1.0/24 | 10.0.1.1 | Personal devices (laptops, phones) |
| 10 | DMZ | 10.0.10.0/24 | 10.0.10.1 | TinyDesk (public-facing) |
| 20 | Infrastructure | 10.0.20.0/24 | 10.0.20.1 | Rod-Server (internal) |
| 30 | IoT | 10.0.30.0/24 | 10.0.30.1 | Smart home devices (lights, cameras, sensors) |
| # | Direction | Action | Rationale |
|---|---|---|---|
| 1 | VLAN 10 → VLAN 20 | DENY | DMZ cannot reach internal (WireGuard bypasses at L3) |
| 2 | VLAN 10 → VLAN 1 | DENY | DMZ cannot reach home devices |
| 3 | VLAN 1 → VLAN 20 | ALLOW | Home devices access *.home services |
| 4 | VLAN 20 → VLAN 1 | ALLOW | Rod-Server provides DNS, monitoring |
| 5 | VLAN 1 → VLAN 10 | DENY | Home devices don't need DMZ access |
| 6 | ALL → WAN | ALLOW | All VLANs can reach internet |
| 7 | VLAN 1 → VLAN 30 | ALLOW | Home can control IoT devices |
| 8 | VLAN 30 → VLAN 1 | DENY | IoT cannot initiate to Home (lateral movement prevention) |
| 9 | VLAN 30 → VLAN 20 | DENY | IoT cannot reach Infrastructure |
| 10 | VLAN 20 → VLAN 30 | ALLOW | Infrastructure can monitor IoT (HA, DNS) |
Default: DENY incoming, ALLOW outgoing ALLOW 51820/udp FROM 10.0.20.2 # WireGuard from Rod-Server ALLOW 22/tcp FROM 10.0.1.0/24 # SSH from Home LAN DENY everything else inbound
Default: DENY incoming, ALLOW outgoing # WireGuard ALLOW 51820/udp FROM 10.0.10.2 # WireGuard from TinyDesk # Database access via WireGuard tunnel ONLY ALLOW 3306/tcp FROM 10.0.99.1 # MariaDB ALLOW 5432/tcp FROM 10.0.99.1 # PostgreSQL ALLOW 6379/tcp FROM 10.0.99.1 # Redis # LAN services ALLOW 80,443/tcp FROM 10.0.1.0/24 # Traefik (*.home) ALLOW 53 FROM 10.0.1.0/24 # Pi-hole DNS ALLOW 22/tcp FROM 10.0.1.0/24 # SSH ALLOW 8043 FROM 10.0.1.0/24 # Omada controller # Docker internal ALLOW FROM 172.16.0.0/12 # Docker subnet (internal) DENY everything else
| Option | Approach | Complexity | Risk Reduction |
|---|---|---|---|
| A (Best) | Secondary Pi-hole instance on DMZ (TinyDesk) for IoT DNS; Rod-Server Pi-hole for VLAN 1/20 only | Medium | High — no IoT→Infra path |
| B (Acceptable) | Strict ACL: IoT → Rod-Server UDP 53 only (no TCP, no other ports) + Pi-hole rate limiting (100 queries/min per IP) | Low | Medium — limits but doesn't eliminate path |
| C (Best effort) | Use Cloudflare Gateway as upstream DNS for IoT VLAN (no local Pi-hole dependency) | Low | Medium — moves trust to Cloudflare |
VLAN 30 → VLAN 20: DENY) must include an exception for UDP 53 only.
| Network | Subnet | Type | Containers |
|---|---|---|---|
vault_isolated | 172.30.0.0/28 | Bridge | Traefik + Vaultwarden ONLY DONE |
web_public | 172.25.0.0/24 | Bridge | Traefik + public-facing services |
web_admin | 172.25.1.0/24 | Bridge | Traefik + Authelia + admin services |
db_backend | 172.26.0.0/24 | Internal | MariaDB, Redis (no external access) |
librosynth | 172.28.0.0/24 | Internal | LibroSynth microservices |
The Docker socket (/var/run/docker.sock) grants root-equivalent access. Currently exposed to Traefik, Portainer, and Dockge.
Mitigation: Dual Docker socket proxy architecture (tecnativa/docker-socket-proxy):
| Proxy | For | CONTAINERS | IMAGES | NETWORKS | SERVICES | VOLUMES | POST | EXEC |
|---|---|---|---|---|---|---|---|---|
| Read-only proxy | Traefik | 1 | 0 | 1 | 1 | 0 | 0 | 0 |
| Management proxy | Portainer + Dockge | 1 | 1 | 1 | 1 | 1 | 1 | 1* |
* Portainer requires EXEC=1; Dockge does not. Management proxy restricted to web_admin network only.
# Read-only proxy (Traefik)
docker-socket-proxy-ro:
image: tecnativa/docker-socket-proxy
environment:
CONTAINERS: 1
IMAGES: 0
NETWORKS: 1
SERVICES: 1
POST: 0 # Read-only
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- socket_proxy
# Management proxy (Portainer + Dockge)
docker-socket-proxy-mgmt:
image: tecnativa/docker-socket-proxy
environment:
CONTAINERS: 1
IMAGES: 1
NETWORKS: 1
SERVICES: 1
VOLUMES: 1
POST: 1 # Write access for management
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- web_admin # Only accessible from admin network
exec and events endpoints by design. However, for single-host deployment the agent adds complexity without significant benefit over the dual-proxy approach. Revisit if deploying Portainer to manage multiple Docker hosts.
POST=1 still allows container creation/deletion (Portainer needs this)POST /containers/start but deny POST /containers/create)lan-only middleware (already applied) reduces risk to authenticated, local attackers onlyread_only: true + explicit tmpfs mountssecurity_opt: [no-new-privileges:true]cap_drop: [ALL] + explicit cap_add only when neededprivileged: true unless required (only Home Assistant)| Kill Chain Phase | Traefik Log Pattern | CrowdSec Action |
|---|---|---|
| Reconnaissance (scanning) |
High 403/404 volume from single ClientAddr; scanner User-Agent strings (Nmap, Nikto, sqlmap); sequential requests to /admin, /wp-login, /.env, /api/v1/debug |
Ban IP for 4 hours |
| Weaponization/Delivery (exploitation) |
Spike in 500 errors; RequestPath containing UNION SELECT, <script>, ../; ResponseCodeRatio(500,600,0,600) > 0.25 |
Ban IP for 24 hours; Circuit Breaker trips |
| Installation (persistence) |
POST requests to admin endpoints from unknown IPs; new User-Agent strings on authenticated sessions |
Ban IP for 7 days |
| C2 (command & control) |
"Low and slow" beaconing: regular-interval requests to same endpoint; consistent small RequestContentSize/DownstreamContentSize; connections at odd hours |
Alert + ban IP for 7 days |
| Exfiltration | Unusually large DownstreamContentSize responses; sustained high Duration connections to external IPs |
Alert + immediate ban |
| Known malicious IP (crowdsourced) |
N/A — preemptive block | Block preemptively |
All Traefik hardening middleware has been applied to dynamic.yml and traefik.yml as of 2026-02-15:
auth-rate-limit:
rateLimit:
average: 5 # 5 requests/second sustained
burst: 10 # allow spike of 10
# Apply to login/auth endpoints
body-limit:
buffering:
maxRequestBodyBytes: 2097152 # 2MB max request body
maxResponseBodyBytes: 10485760 # 10MB max response body
memRequestBodyBytes: 1048576 # 1MB in-memory buffer
memResponseBodyBytes: 1048576
circuit-breaker:
circuitBreaker:
expression: "ResponseCodeRatio(500, 600, 0, 600) > 0.25"
# Trips when >25% of responses are 5xx errors
In traefik.yml static config:
serversTransport:
forwardingTimeouts:
dialTimeout: 5s
responseHeaderTimeout: 30s
idleConnTimeout: 30s
secure-chain:
chain:
middlewares:
- security-headers
- auth-rate-limit
- body-limit
- circuit-breaker
tls:
options:
default:
minVersion: VersionTLS12
cipherSuites:
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
sniStrict: true
ResponseCodeRatio(500, 600, 0, 600) > 0.25 trips when more than 25% of responses are server errors.
lan-only middleware to 19 admin routers (2026-02-15)cloudflare-only middleware to 18 public routers (2026-02-15)https-redirect middleware to all 47 HTTP routers (2026-02-15)security-headers middleware to all 36+ HTTPS routers (2026-02-15)cloudflare-real-ip plugin: populated trustedCIDRs with 15 Cloudflare IP ranges (2026-02-15)Traefik hardening middleware:
auth-rate-limit middleware (avg: 5/s, burst: 10) to login endpoints (2026-02-15)body-limit middleware (maxRequestBodyBytes: 2MB) to all routers (2026-02-15)circuit-breaker middleware (trip at >25% 5xx) to all backends (2026-02-15)serversTransport timeouts: dialTimeout 5s, responseHeaderTimeout 30s, idleConnTimeout 30s (2026-02-15)secure-chain middleware compositions (2026-02-15)sniStrict: true (2026-02-15)Database TLS (moved from Phase 3):
require_secure_transport=ON, cipher TLS_AES_256_GCM_SHA384 (2026-02-15)Supply chain & DNS:
Authelia (2FA) — remaining:
forwardAuth middleware pointing to Autheliatwo_factor policy (see Section 5.2)rodchemist with TOTP enrollmentCrowdSec (IDS) — remaining:
Monitoring (Grafana) — remaining:
Key Prometheus queries for security monitoring:
# DDoS / scanning spike detection
rate(traefik_entrypoint_requests_total[5m])
# Backend crash detection (exploitation indicator)
rate(http_requests_total{status="500"}[5m])
# Resource abuse (crypto-miner detection)
process_resident_memory_bytes
rate(node_cpu_seconds_total[5m])
Key Loki queries:
# Authelia brute force detection
{compose_service="authelia"} | logfmt | level="error"
# Traefik 4xx/5xx by source IP
{compose_service="traefik"} | json | DownstreamStatus >= 400
f49ac13d-2108-4170-8052-457167417bae
VLANs:
IoT VLAN 30 implementation:
Sanchez_IoT mapped to VLAN 30Monitoring:
Docker isolation:
wg0) — the DMZ↔Internal choke point catches non-HTTP attacks (DB buffer overflows, SSH exploits, data exfiltration) that CrowdSec's log-based detection misses| Service | Auth | Network | Encryption |
|---|---|---|---|
| Vaultwarden | Own auth + isolated | vault_isolated /28 | LUKS at rest, TLS in transit |
| Service | Auth Level | Public? | Network |
|---|---|---|---|
| Portainer | Authelia 2FA | No — LAN only | web_admin |
| phpMyAdmin | Authelia 2FA | No — LAN only | web_admin |
| Dockge | Authelia 2FA | No — LAN only | web_admin |
| Terminal (wetty) | Authelia 2FA | No — LAN only | web_admin |
| Traefik Dashboard | Authelia 2FA | No — LAN only | web_admin |
| Pi-hole | Authelia 2FA | No — LAN only | web_admin |
| n8n | Authelia 2FA | CF Access only | web_admin |
| Grafana | Authelia 2FA | No | web_admin |
| Home Assistant | Authelia 2FA | CF Access | web_admin |
| Omada | Authelia 2FA | No — LAN only | web_admin |
| MCP Proxy | Authelia 2FA | No — LAN only | web_admin |
| Filebrowser | Authelia 2FA | No — LAN only | web_admin |
| Service | Auth Level | Public? | Network |
|---|---|---|---|
| Nextcloud | Own auth | Yes (via CF) | web_public |
| Forgejo | Own auth | Yes (via CF) | web_public |
| Frigate | Authelia 1FA | No | web_admin |
| Kavita | Authelia 1FA | Yes | web_public |
| Audiobookshelf | Own auth | Yes | web_public |
| Service | Auth Level | Public? | Network |
|---|---|---|---|
| nginx (landing page) | None | Yes | web_public |
| Plex | Own auth | Yes | web_public |
| Ollama API | Authelia 2FA | No | web_admin |
| Health endpoints | None | API only | varies |
| # | Decision | Alternative | Rationale |
|---|---|---|---|
| 1 | Authelia | Authentik | Lighter (~40MB vs ~500MB), sufficient for 1 user, lower complexity |
| 2 | TOTP | WebAuthn | Works on any phone, no hardware purchase. WebAuthn addable later |
| 3 | Cloudflare Tunnel | Port forwarding | Zero inbound ports, free WAF, DDoS protection, IP hiding |
| 4 | WireGuard | Tailscale | Simpler for 2-host P2P, no external dependency |
| 5 | TinyDesk DMZ | Same-host isolation | Physical separation = strongest isolation |
| 6 | LUKS volumes | Full-disk encryption | Non-disruptive, targets highest-value data |
| 7 | CrowdSec | Fail2ban | Native Traefik integration, crowdsourced intelligence |
| 8 | File-based Authelia | DB-backed | 1 user doesn't need a database |
| 9 | ER605 VLANs (defense-in-depth) | Replace with OPNsense | Already deployed, host firewalls are primary |
| 10 | Separate Docker networks per tier | Single network + policies | Docker doesn't support network policies (that's Kubernetes). Separate bridge networks are Docker's isolation mechanism |
| 11 | Dedicated IoT VLAN (30) | IoT on Home VLAN (1) | IoT devices lack updates, are botnet targets (Mirai). Shared broadcast domain enables ARP spoofing. Home→IoT allowed, IoT→Home denied |
| 12 | Trivy scanning in Phase 1 | Defer to Phase 5 | Supply chain attacks in container images are a rising threat. Weekly scans catch newly disclosed CVEs before exploitation |
| 13 | Suricata NIDS (Phase 5) | CrowdSec alone | CrowdSec is log-based (reactive). Suricata analyzes packet traffic in real-time, detecting exfiltration patterns log parsers miss |
| 14 | Pi-hole aggressive blocklists | Default blocklists only | DNS sinkholing prevents compromised devices from contacting C2 servers. Covers insider threat / negligent user vector |
| 15 | Dual Docker socket proxy | Single proxy for all | Traefik only needs read; Portainer needs write. Separate proxies enforce least privilege and limit blast radius |
| 16 | CF Access + Authelia dual-layer | Single layer auth | Cloudflare protects edge (.sanchezfamily.ca) but not .home; Authelia protects both. Independent layers |
| 17 | Circuit Breaker middleware | No backend error detection | High 5xx ratios indicate exploitation. Circuit breaker halts traffic and triggers CrowdSec investigation |
| 18 | Grafana monitoring in Phase 1 | Defer to Phase 4 | Already deployed but unconfigured. Adding Prometheus/Loki in Phase 1 provides immediate visibility into security events |
| 19 | MQTT broker for IoT ↔ HA | Direct device-to-device | ER605 stateless ACLs make bidirectional IoT traffic unreliable. MQTT on Rod-Server bridges HA↔IoT without direct network paths |
| 20 | Database TLS in Phase 1 (not Phase 3) | Wait for WireGuard tunnel | Once WireGuard decapsulates, traffic hits Docker network unencrypted. Application-layer TLS provides defense-in-depth if VPN layer is bypassed or Docker network is compromised |
| 21 | IoT DNS rate-limiting (Option B) | Secondary Pi-hole (Option A) | Option B is lower complexity for initial deployment. VLAN 30→VLAN 20 ACL must be UDP 53 only with rate limiting. Option A deferred to Phase 5 |
| 22 | Suricata on WireGuard interface | Suricata on all interfaces | wg0 is the DMZ↔Internal choke point — captures all cross-zone traffic with minimal performance impact. CrowdSec handles HTTP (L7); Suricata handles packets (L3/4) |
| 23 | Image digest pinning | Tag-based pulls | :latest is mutable — a compromised registry can substitute a malicious image. Pinning by @sha256:... ensures immutability. Added to Phase 2 |
| 24 | Local-first, Cloudflare-complementary | Full Cloudflare dependency | Pi-hole, WireGuard, Authelia work offline. Cloudflare Tunnel/Access/WARP are edge enhancements, not replacements. Preserves functionality during internet outages |
Aligned with NIST SP 800-61 (Computer Security Incident Handling Guide) adapted for single-admin home infrastructure.
.env credentials and Authelia TOTP seedstcpdump, strace, lsof installed on hostIndicators of Compromise (IoCs) to check:
| Category | What to Look For | Command |
|---|---|---|
| Unauthorized containers | Unknown images running | docker ps --format "{{.Names}} {{.Image}}" |
| Unusual processes | Unexpected listeners, miners | ss -tlnp, top -bn1 | head -20 |
| SSH compromise | Unknown authorized_keys | cat ~/.ssh/authorized_keys |
| Persistence mechanisms | Unknown cron jobs, services | crontab -l, systemctl list-units --type=service |
| Modified system files | Changed binaries | debsums -c 2>/dev/null | head -20 |
| Network anomalies | Outbound to unknown IPs | ss -tnp | grep ESTABLISHED |
| CrowdSec alerts | Recent bans/alerts | cscli alerts list |
| Authelia logs | Failed/unusual logins | docker logs authelia --since 24h |
Immediate (< 5 minutes):
sudo ufw default deny incoming && sudo ufw reloaddocker logs --since 48h [container] > /tmp/incident_$(date +%s).logdocker ps -a > /tmp/containers_$(date +%s).txt && ss -tlnp > /tmp/listeners_$(date +%s).txtShort-term (< 1 hour):
docker stop [container] (do NOT docker rm — preserve for forensics)sudo ufw deny from [IP] and cscli decisions add --ip [IP] --duration 720h --reason "Manual incident response"trivy image --severity HIGH,CRITICAL [image] for every running container/home/rod/_rod/.env, update all servicesdocker compose pull && docker compose up -dsudo iptables -L DOCKER-USER -n --line-numbers_rod/INCIDENT_LOG.md: what happened, when detected, actions taken, root causedocker/authelia/config/users_database.yml to reset passwordsudo cryptsetup luksOpen /dev/mapper/encrypted-data recovery --key-file /path/to/recovery.key