Skip to main content

Self-Hosting Security Checklist: 20 Things to Lock Down

·OSSAlt Team
securityself-hostinghardeningdockerguide

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

Tool2FA Support
VaultwardenTOTP, WebAuthn, YubiKey
Uptime KumaTOTP
GiteaTOTP, WebAuthn
NextcloudTOTP, WebAuthn
KeycloakTOTP, WebAuthn
OutlineVia 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

MistakeRiskFix
Exposing database ports publiclyDirect database accessUse Docker networks, bind to 127.0.0.1
Default passwordsUnauthorized accessGenerate unique passwords for everything
No firewallAll ports openEnable UFW with deny-by-default
Running as rootFull server compromise if app is exploitedCreate non-root users
No backupsTotal data lossImplement 3-2-1 backup strategy
No monitoringAttacks go unnoticedSet up Uptime Kuma + Fail2Ban alerts
Outdated softwareKnown vulnerabilitiesEnable automatic updates
HTTP without redirectData interceptionForce HTTPS via reverse proxy

Find secure, self-hostable tools on OSSAlt — security features, licensing, and deployment guides side by side.