6.3 KiB
6.3 KiB
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
# DNS resolution — where does the domain point?
dig +short <subdomain>.<domain> @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
# Full handshake info + headers
curl -sI -v https://<domain>/ 2>&1 | grep -E '(SSL|TLS|certificate|HTTP/|Server|Strict-Transport|X-Content|X-Frame|CSP|Referrer)'
# Certificate details
echo | openssl s_client -servername <domain> -connect <domain>: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
curl -sI https://<domain>/ | 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, ideallymax-age>=31536000X-Content-Type-Options: nosniff— prevents MIME sniffingX-Frame-Options: DENYorSAMEORIGIN— clickjacking protectionContent-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
# Inspect the nginx site config
cat /etc/nginx/sites-available/<service>
# 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/<service>
Check for:
server_nameincludes ALL valid domains (DuckDNS + custom domain)auth_basicfor defense-in-depth (recommended for single-user services)limit_reqto prevent brute forceproxy_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
# List containers with status
docker ps --format "table {{.Names}}\t{{.Status}}"
# Deep inspect
docker inspect <container> --format '{{.HostConfig.NetworkMode}}' # network mode
docker inspect <container> --format '{{.HostConfig.CapDrop}}' # dropped capabilities
docker inspect <container> --format '{{.HostConfig.CapAdd}}' # added capabilities
docker inspect <container> --format '{{.HostConfig.SecurityOpt}}' # security opts
docker inspect <container> --format '{{.Config.User}}' # running user
Check for:
- Network mode:
bridge(isolated, preferred) vshost(full host network access — riskier) - Capabilities: ideally
--cap-drop=ALLwith explicit--cap-addfor what's needed - Running user: non-root inside container (e.g.,
kasm-useris good) - Security opts:
no-new-privileges:trueis a hardening bonus - Restart policy:
unless-stopped(good) vsalways(also fine)
6. Container Internals
# Inside the container
docker exec <container> whoami
docker exec <container> cat /etc/os-release 2>/dev/null
docker exec <container> cat /etc/debian_version 2>/dev/null
docker exec <container> uname -r 2>/dev/null
# Application version
docker exec <container> <app> --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 <image> --format '{{.Created}}') - Whether the image is regularly updated
7. Firewall and Host Defense
# UFW rules
sudo ufw status verbose
# iptables rules for the service port
sudo iptables -L INPUT -n --line-numbers | grep <port>
# 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
# Trace the architecture
dig +short <subdomain>.<domain> # → VPS IP
# VPS proxies to home server via Tailscale
ssh ubuntu@<vps-ip> 'grep proxy_pass /etc/nginx/sites-enabled/<service>'
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.