# Self-Hosted Service Security Audit A comprehensive methodology for assessing the security posture of any self-hosted web service (KasmVNC, Immich, Paperless, Mealie, etc.) behind a VPS reverse proxy. ## Audit Checklist (Run in Order) ### 1. Network Exposure ```bash # DNS resolution — where does the domain point? dig +short . @1.1.1.1 # Compare against server's public IP curl -s ifconfig.me # If they differ → VPS reverse proxy in front → good # If they match → service directly exposed → verify UFW ``` ### 2. TLS / Transport Security ```bash # Full handshake info + headers curl -sI -v https:/// 2>&1 | grep -E '(SSL|TLS|certificate|HTTP/|Server|Strict-Transport|X-Content|X-Frame|CSP|Referrer)' # Certificate details echo | openssl s_client -servername -connect :443 2>/dev/null | openssl x509 -noout -text | grep -E '(Subject:|Issuer:|Not Before|Not After|Subject Alternative)' ``` **Check for:** - TLS 1.2+ only (no TLS 1.0/1.1, no SSLv3) - Strong ciphers (AES-GCM, ChaCha20) — not RC4, 3DES, or MD5 - Let's Encrypt or similar trusted CA (not self-signed) - HSTS header present (`Strict-Transport-Security`) - Certificate covers the domain (Subject Alternative Name) ### 3. HTTP Security Headers ```bash curl -sI https:/// | grep -i -E '(strict-transport-security|x-content-type-options|x-frame-options|content-security-policy|referrer-policy|permissions-policy)' ``` **Check for:** - `Strict-Transport-Security` — HSTS, ideally `max-age>=31536000` - `X-Content-Type-Options: nosniff` — prevents MIME sniffing - `X-Frame-Options: DENY` or `SAMEORIGIN` — clickjacking protection - `Content-Security-Policy` — XSS mitigation (can be tricky with SPAs/WebSockets) - `Referrer-Policy` — controls referrer leakage - Missing headers are a hardening opportunity, not always critical for internal services ### 4. Nginx / Reverse Proxy Config ```bash # Inspect the nginx site config cat /etc/nginx/sites-available/ # Check for server_name — does it match the subdomain? # Check for auth_basic — any nginx-level auth? grep -rn "auth_basic" /etc/nginx/ 2>/dev/null # Check for rate limiting grep -rn "limit_req" /etc/nginx/ 2>/dev/null # Check for proxy_hide_header — which headers are stripped? grep "proxy_hide_header" /etc/nginx/sites-available/ ``` **Check for:** - `server_name` includes ALL valid domains (DuckDNS + custom domain) - `auth_basic` for defense-in-depth (recommended for single-user services) - `limit_req` to prevent brute force - `proxy_hide_header` — strips unwanted backend headers (critical for KasmVNC COEP/COOP) - `proxy_ssl_verify off` — acceptable for localhost backends, note it's not verifying ### 5. Docker Container Security ```bash # List containers with status docker ps --format "table {{.Names}}\t{{.Status}}" # Deep inspect docker inspect --format '{{.HostConfig.NetworkMode}}' # network mode docker inspect --format '{{.HostConfig.CapDrop}}' # dropped capabilities docker inspect --format '{{.HostConfig.CapAdd}}' # added capabilities docker inspect --format '{{.HostConfig.SecurityOpt}}' # security opts docker inspect --format '{{.Config.User}}' # running user ``` **Check for:** - **Network mode**: `bridge` (isolated, preferred) vs `host` (full host network access — riskier) - **Capabilities**: ideally `--cap-drop=ALL` with explicit `--cap-add` for what's needed - **Running user**: non-root inside container (e.g., `kasm-user` is good) - **Security opts**: `no-new-privileges:true` is a hardening bonus - **Restart policy**: `unless-stopped` (good) vs `always` (also fine) ### 6. Container Internals ```bash # Inside the container docker exec whoami docker exec cat /etc/os-release 2>/dev/null docker exec cat /etc/debian_version 2>/dev/null docker exec uname -r 2>/dev/null # Application version docker exec --version 2>/dev/null ``` **Check for:** - Base OS age (Ubuntu 20.04 is past standard EOL in April 2025) - Application version recency - Image build date (`docker inspect --format '{{.Created}}'`) - Whether the image is regularly updated ### 7. Firewall and Host Defense ```bash # UFW rules sudo ufw status verbose # iptables rules for the service port sudo iptables -L INPUT -n --line-numbers | grep # Security monitoring dpkg -l fail2ban crowdsec rkhunter chkrootkit 2>/dev/null | grep '^ii' ``` **Check for:** - Default inbound policy: DROP (not ACCEPT) - Service port allowed only from needed ranges (LAN, Tailscale VPN) - Public internet blocked at the firewall level - fail2ban or similar running for brute-force protection ### 8. VPS Reverse Proxy Architecture ```bash # Trace the architecture dig +short . # → VPS IP # VPS proxies to home server via Tailscale ssh ubuntu@ 'grep proxy_pass /etc/nginx/sites-enabled/' ``` **Check for:** - DNS points to VPS, not home server IP - VPS nginx proxies via Tailscale IP (100.x.x.x), not public IP - UFW on home server allows port only from LAN + Tailscale ranges - Home router has NO port forward for this service (zero open ports) ### 9. Risk Summary Matrix | Layer | Weakness | Severity | Mitigation | |-------|----------|----------|------------| | Network | Public exposure via DNS | Low if behind VPS | UFW, VPS proxy | | Transport | Missing HSTS | Low | `add_header` in nginx | | Auth | Single auth layer | Medium | Add nginx `auth_basic` | | Container | Host networking | High | Switch to bridge + iptables | | Container | Default capabilities | Medium | `--cap-drop=ALL` + explicit adds | | Container | Old base OS | Medium | Regular image pulls | | App | No rate limiting | Low | `limit_req` in nginx | ## Interpretation Guide - **Low severity** = hardening opportunity, not urgent. Benefits of fixing may not justify risk of breaking the service. - **Medium severity** = address when convenient. Adds defense-in-depth. - **High severity** = actively exploitable if attacker gains any toehold. Prioritize. The firewall + VPS architecture is almost always the strongest defense. Even a poorly-configured container behind a proper firewall is hard to reach from the internet. Focus hardening effort where it reduces blast radius once an attacker is inside the network.