Self-Hosting Security Checklist: 20 Things to Lock Down
Self-Hosting Security Checklist: 20 Things to Lock Down
Self-hosting gives you control, but you're also responsible for security. Here are the 20 things you need to lock down before exposing any self-hosted service to the internet.
Server Access (Items 1-5)
1. ✅ Disable Root Login
# /etc/ssh/sshd_config
PermitRootLogin no
sudo systemctl restart sshd
Create a regular user and use sudo:
adduser deployer
usermod -aG sudo deployer
2. ✅ SSH Key-Only Authentication
# On your local machine
ssh-keygen -t ed25519 -C "your@email.com"
ssh-copy-id deployer@your-server
# Then disable password auth
# /etc/ssh/sshd_config
PasswordAuthentication no
PubkeyAuthentication yes
3. ✅ Change SSH Port
# /etc/ssh/sshd_config
Port 2222 # Pick any non-standard port
Reduces automated brute force attempts by 99%.
4. ✅ Set Up Fail2Ban
sudo apt install -y fail2ban
# /etc/fail2ban/jail.local
[sshd]
enabled = true
port = 2222
filter = sshd
maxretry = 3
bantime = 3600
findtime = 600
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
5. ✅ Configure UFW Firewall
sudo ufw default deny incoming
sudo ufw default allow outgoing
# SSH (your custom port)
sudo ufw allow 2222/tcp
# HTTP/HTTPS (for Caddy)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Enable
sudo ufw enable
sudo ufw status
Never expose database ports (5432, 3306, 6379) to the internet.
Docker Security (Items 6-10)
6. ✅ Don't Run Containers as Root
# docker-compose.yml
services:
myapp:
user: "1000:1000" # Non-root user
Or in Dockerfile:
RUN adduser -D appuser
USER appuser
7. ✅ Limit Container Resources
services:
myapp:
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
memory: 512M
Prevents a single container from consuming all server resources.
8. ✅ Use Read-Only File Systems Where Possible
services:
myapp:
read_only: true
tmpfs:
- /tmp
- /var/tmp
volumes:
- app_data:/data # Only mount what's needed
9. ✅ Don't Bind to 0.0.0.0 for Internal Services
# BAD — exposed to internet
ports:
- "5432:5432"
# GOOD — only accessible from localhost (for reverse proxy)
ports:
- "127.0.0.1:5432:5432"
# BEST — no port binding, use Docker networks
# (services communicate via Docker internal DNS)
10. ✅ Use Docker Networks for Service Isolation
services:
app:
networks:
- frontend
- backend
db:
networks:
- backend # Not accessible from frontend network
networks:
frontend:
backend:
internal: true # No external access
Web Security (Items 11-14)
11. ✅ Force HTTPS Everywhere
Caddy does this automatically. For other reverse proxies:
# Nginx
server {
listen 80;
return 301 https://$host$request_uri;
}
12. ✅ Security Headers
Caddy adds basic headers. For stricter security:
# /etc/caddy/Caddyfile
(security_headers) {
header {
X-Content-Type-Options nosniff
X-Frame-Options SAMEORIGIN
X-XSS-Protection "1; mode=block"
Referrer-Policy strict-origin-when-cross-origin
Strict-Transport-Security "max-age=31536000; includeSubDomains"
}
}
myapp.yourdomain.com {
import security_headers
reverse_proxy localhost:3000
}
13. ✅ Rate Limiting
# Caddy rate limiting
myapp.yourdomain.com {
rate_limit {
zone dynamic_zone {
key {remote_host}
events 100
window 1m
}
}
reverse_proxy localhost:3000
}
14. ✅ Restrict Admin Panels by IP
# Only allow admin access from specific IPs
admin.yourdomain.com {
@blocked not remote_ip 1.2.3.4 5.6.7.8
respond @blocked "Forbidden" 403
reverse_proxy localhost:3000
}
Application Security (Items 15-17)
15. ✅ Use Strong, Unique Passwords for Every Service
Generate strong passwords:
openssl rand -hex 32 # Database passwords
openssl rand -hex 64 # Secret keys, encryption keys
Never reuse passwords between services. Store them in Vaultwarden.
16. ✅ Enable 2FA on All Admin Accounts
| Tool | 2FA Support |
|---|---|
| Vaultwarden | TOTP, WebAuthn, YubiKey |
| Uptime Kuma | TOTP |
| Gitea | TOTP, WebAuthn |
| Nextcloud | TOTP, WebAuthn |
| Keycloak | TOTP, WebAuthn |
| Outline | Via Keycloak SSO |
17. ✅ Disable Sign-Ups After Setup
Most tools let you disable public registration:
# Vaultwarden
SIGNUPS_ALLOWED=false
# n8n
N8N_USER_MANAGEMENT_DISABLED=true
# Gitea
DISABLE_REGISTRATION=true
# Grafana
GF_USERS_ALLOW_SIGN_UP=false
Invite users through admin panel instead.
Monitoring & Maintenance (Items 18-20)
18. ✅ Automatic Security Updates
# Ubuntu/Debian — enable unattended upgrades
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
# Verify it's enabled
cat /etc/apt/apt.conf.d/20auto-upgrades
19. ✅ Monitor for Intrusions
Check auth logs regularly:
# Failed SSH attempts
journalctl -u sshd --since "24 hours ago" | grep "Failed"
# Fail2ban status
sudo fail2ban-client status sshd
Set up alerts:
- Monitor with Uptime Kuma for service availability
- Set up Fail2Ban email notifications
- Monitor disk usage (prevent denial of service)
20. ✅ Keep Docker Images Updated
# Check for updates
docker compose pull
# Update all services
docker compose up -d
# Clean up old images
docker image prune -f
Schedule monthly updates:
# First Sunday of each month at 4 AM
0 4 1-7 * 0 cd /opt/mystack && docker compose pull && docker compose up -d
Security Audit Script
Run this to check your server's security posture:
#!/bin/bash
# security-check.sh
echo "=== Security Audit ==="
# SSH config
echo -n "Root login disabled: "
grep -q "^PermitRootLogin no" /etc/ssh/sshd_config && echo "✅" || echo "❌"
echo -n "Password auth disabled: "
grep -q "^PasswordAuthentication no" /etc/ssh/sshd_config && echo "✅" || echo "❌"
# Firewall
echo -n "UFW active: "
sudo ufw status | grep -q "active" && echo "✅" || echo "❌"
# Fail2Ban
echo -n "Fail2Ban running: "
systemctl is-active fail2ban > /dev/null && echo "✅" || echo "❌"
# Docker
echo -n "Exposed ports (should be minimal): "
docker ps --format '{{.Ports}}' | grep "0.0.0.0" | wc -l
# Updates
echo -n "Unattended upgrades: "
dpkg -l | grep -q unattended-upgrades && echo "✅" || echo "❌"
# SSL
echo -n "SSL certificates valid: "
echo | openssl s_client -connect yourdomain.com:443 2>/dev/null | openssl x509 -noout -dates 2>/dev/null && echo "✅" || echo "❌"
echo "=== Done ==="
Common Mistakes
| Mistake | Risk | Fix |
|---|---|---|
| Exposing database ports publicly | Direct database access | Use Docker networks, bind to 127.0.0.1 |
| Default passwords | Unauthorized access | Generate unique passwords for everything |
| No firewall | All ports open | Enable UFW with deny-by-default |
| Running as root | Full server compromise if app is exploited | Create non-root users |
| No backups | Total data loss | Implement 3-2-1 backup strategy |
| No monitoring | Attacks go unnoticed | Set up Uptime Kuma + Fail2Ban alerts |
| Outdated software | Known vulnerabilities | Enable automatic updates |
| HTTP without redirect | Data interception | Force HTTPS via reverse proxy |
Find secure, self-hostable tools on OSSAlt — security features, licensing, and deployment guides side by side.