Skip to content

Security Hardening

Dreadnought is designed with security in mind, but a self-hosted application is only as secure as the environment it runs in. This guide covers the steps to harden your deployment.


Secrets Management

SECRET_KEY

This is the most critical security value. It signs JWT session cookies. If an attacker knows your SECRET_KEY, they can forge valid session tokens without knowing the password.

Requirements: - Must be cryptographically random - At least 64 hex characters (256 bits of entropy) - Must be different for every deployment

Generate one:

openssl rand -hex 32

Never: - Use the default value (your-secret-key-change-in-production) - Reuse the same key across multiple deployments - Commit it to version control

ADMIN_PASSWORD

  • Use a password manager to generate a strong, unique password
  • Minimum 8 characters; use 20+ for production
  • The password is stored as a bcrypt hash — it's never stored in plaintext

CF_API_TOKEN

  • Scope it to only the zones Dreadnought needs to manage
  • Do not use the Global API Key — always use a scoped token
  • Rotate it periodically (every 90 days is a reasonable cadence)
  • If it's compromised, delete it from Cloudflare immediately and create a new one
  • Never commit .env to version control (check with git status before committing)

Network Security

Firewall Rules

On a bare-metal Linux server, configure UFW to allow only necessary traffic:

# Default: deny all incoming, allow all outgoing
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow SSH (adjust port if you've changed it)
sudo ufw allow 22/tcp

# Allow HTTP/HTTPS (for reverse proxy + Let's Encrypt)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# Block direct access to app ports (all traffic should go through reverse proxy)
sudo ufw deny 8081/tcp
sudo ufw deny 8082/tcp

# Enable the firewall
sudo ufw enable

# Verify
sudo ufw status verbose

LAN-Only Exposure

If you only need LAN access (no public HTTPS), expose the app only on your LAN interface instead of blocking publicly:

sudo ufw allow from 192.168.1.0/24 to any port 8082
sudo ufw allow from 192.168.1.0/24 to any port 8081

Adjust 192.168.1.0/24 to match your actual LAN subnet.

Docker and UFW

Warning: Docker bypasses UFW rules by default on Linux. If you publish a Docker port (-p 8081:8000), it's directly accessible on the Docker host even if UFW has a deny rule.

To prevent this, you can:

Option A — Bind to localhost in docker-compose.yml:

api:
  ports:
    - "127.0.0.1:8081:8000"   # Only accessible from localhost, not external IPs

web:
  ports:
    - "127.0.0.1:8082:3000"

This is the cleanest solution when using a reverse proxy on the same host.

Option B — Use Docker's --iptables=false with manual rules. More complex and generally not necessary for most deployments.


TLS / HTTPS

Always use HTTPS for any publicly accessible deployment. Without TLS: - Session cookies can be intercepted (session hijacking) - Credentials can be sniffed on the network - The Cloudflare API token in network requests could be exposed

Options: - Nginx + Let's Encrypt - Caddy — automatic HTTPS - Traefik — Docker-native, automatic TLS - Coolify — manages TLS automatically


Docker Security

Containers Already Run as Non-Root

All three Dreadnought containers run as non-root users:

Container User UID
api appuser 1000
worker appuser 1000
web nextjs 1001

This means a container breakout wouldn't immediately give root on the host.

Limit Exposed Ports

Only expose ports that need to be exposed. The worker container has no exposed ports. In production with a reverse proxy, consider changing exposed ports to bind on localhost only:

api:
  ports:
    - "127.0.0.1:8081:8000"

web:
  ports:
    - "127.0.0.1:8082:3000"

Keep Images Updated

Regularly rebuild images to pick up base image security patches:

# Pull latest base images and rebuild
docker compose build --pull
docker compose up -d
docker system prune -f

Cloudflare API Token Scoping

The narrower the token's scope, the less damage a compromised token can do.

Recommended minimum permissions:

Zone → Zone → Read     (to list and resolve zones)
Zone → DNS → Edit      (to create/update A and AAAA records)

Zone Resources: - Instead of "All zones", list only the specific zones Dreadnought manages

IP Restrictions: - If your server has a static IP, lock the token to that IP address in Cloudflare - Not practical for a home server with a dynamic IP


CORS Configuration

The API currently has permissive CORS settings (allows all origins). This is fine when: - The API is behind a firewall and not publicly accessible - The API is only accessible via the reverse proxy that also serves the frontend

If you need to restrict CORS, edit backend/main.py and modify the CORSMiddleware configuration:

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://ddns.yourdomain.com"],   # Restrict to your domain
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Then rebuild: docker compose up -d --build


Protecting the Dashboard

Restrict Access by IP (Nginx)

If you only want to access the dashboard from specific IPs:

location / {
    allow 203.0.113.0/24;    # Your office or home IP range
    allow 10.0.0.0/8;        # LAN
    deny all;

    proxy_pass http://127.0.0.1:8082;
    # ... rest of proxy config
}

Use VPN for Remote Access

For maximum security, don't expose the dashboard to the public internet at all. Access it via a VPN (WireGuard, Tailscale, OpenVPN) so only VPN clients can reach it.

Tailscale is particularly easy to set up and doesn't require port forwarding:

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up

Access Dreadnought via your Tailscale IP: http://100.x.x.x:8082


Database Security

The SQLite database (./data/ddns.db) contains: - The admin password hash (bcrypt — safe even if the file is stolen) - Your Cloudflare Zone IDs (not secret on their own) - DNS record configurations - Audit log history

It does not contain the Cloudflare API token — that's read from environment variables at runtime.

Secure the data directory:

# In production, 777 permissions on ./data are more permissive than necessary.
# If you know your container's UID (1000 for appuser):
sudo chown -R 1000:1000 ./data
chmod 755 ./data
chmod 644 ./data/ddns.db   # After the DB has been created

Note: chmod 777 ./data is used for simplicity in most guides because matching UIDs across host and container requires more setup. 777 is acceptable for home/LAN use where the host is trusted.


Security Checklist

Use this before going live:

  • [ ] SECRET_KEY is a 64-character random hex string (not the default)
  • [ ] ADMIN_PASSWORD is strong and unique
  • [ ] .env is not committed to version control (check git status)
  • [ ] CF_API_TOKEN is scoped to only the zones needed, not global
  • [ ] HTTPS is configured (reverse proxy + TLS certificate)
  • [ ] App ports (8081, 8082) are blocked from public internet access (firewall or localhost binding)
  • [ ] Docker is started as non-root, and containers run as non-root users ✓ (already handled)
  • [ ] Regular backups of ./data/ddns.db are set up
  • [ ] Base images are updated periodically