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
.envto version control (check withgit statusbefore 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_KEYis a 64-character random hex string (not the default) - [ ]
ADMIN_PASSWORDis strong and unique - [ ]
.envis not committed to version control (checkgit status) - [ ]
CF_API_TOKENis 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.dbare set up - [ ] Base images are updated periodically