Skip to content
BoringStack
Star

Firewall & TLS

3 min read

Secure your single VPS with UFW firewall rules that allow only Cloudflare IPs on ports 80/443, plus SSH access. Traefik on the VPS requests TLS certificates from Let’s Encrypt via ACME HTTP-01 and redirects plain HTTP to HTTPS.

If you provisioned via OpenTofu, the Hetzner cloud firewall already enforces the Cloudflare-only allowlist at the provider level. This UFW setup is for manually-provisioned hosts, or as defense-in-depth on top of the cloud firewall.

  • Inbound port 22 (SSH): open from anywhere (adjust SSH_PORT if you’ve moved it).
  • Inbound ports 80, 443: only from Cloudflare IP ranges (IPv4 + IPv6).
  • All other inbound: dropped.
  • Outbound: unrestricted.
  • Traefik: HTTPS via Let’s Encrypt, auto-renewing, HTTP redirects to HTTPS.
  • Your domain’s DNS is on Cloudflare in proxied mode (orange cloud).
  • The server has a static IPv4 address.
  • An ACME contact email (real address; Let’s Encrypt rejects example.com).
Terminal window
echo 'PUBLIC_UI_HOST=example.com' >> compose/.env
echo 'ACME_EMAIL=ops@example.com' >> compose/.env

The domain must resolve to this server via a Cloudflare proxied A/AAAA record on the apex. BoringStack routes /api/* and /* to the same Traefik instance on one cert.

Terminal window
STACK=prod ./scripts/compose-up.sh

Traefik will request ACME certificates on first boot. Watch the logs:

Terminal window
docker compose logs traefik | grep -i acme

Wait for obtain certificate to complete before applying UFW rules.

Terminal window
CONFIRM=yes ./scripts/ufw.example.sh

The script fetches current Cloudflare IP ranges and applies UFW rules:

UFW reset and rules applied
Port 22: ALLOW from anywhere
Ports 80,443: ALLOW from Cloudflare ranges only

Three checks:

Terminal window
# Local (self-signed OK)
curl -sI -H 'Host: example.com' https://localhost/health -k
# Via Cloudflare (the normal path)
curl -sI https://example.com/health
# Direct to server IP (should timeout - UFW is working)
curl -sI --resolve example.com:443:<your-vps-ip> https://example.com/health --max-time 5

Expected results: first two return 200, third times out. The timeout proves the firewall is blocking direct access.

Cloudflare publishes new IP ranges here and here semi-annually. Re-run the script when they change:

Terminal window
CONFIRM=yes ./scripts/ufw.example.sh

Idempotent. Safe to run anytime.

  • DDoS aimed at your domain: Use Cloudflare’s WAF and rate-limiting rules.
  • App-layer attacks: Traefik rate-limit middleware (already configured for the API) and per-account rate limits in the API.
  • SSH compromise: Use a non-standard SSH port, disable password auth, use a hardware key.

See Security for a full checklist.