Firewall & TLS
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.
What you’ll have
Section titled “What you’ll have”- Inbound port 22 (SSH): open from anywhere (adjust
SSH_PORTif 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.
Prerequisites
Section titled “Prerequisites”- 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).
1. Configure the domain and ACME email
Section titled “1. Configure the domain and ACME email”echo 'PUBLIC_UI_HOST=example.com' >> compose/.envecho 'ACME_EMAIL=ops@example.com' >> compose/.envThe 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.
2. Boot the production stack
Section titled “2. Boot the production stack”STACK=prod ./scripts/compose-up.shTraefik will request ACME certificates on first boot. Watch the logs:
docker compose logs traefik | grep -i acmeWait for obtain certificate to complete before applying UFW rules.
3. Apply the firewall script
Section titled “3. Apply the firewall script”CONFIRM=yes ./scripts/ufw.example.shThe script fetches current Cloudflare IP ranges and applies UFW rules:
UFW reset and rules appliedPort 22: ALLOW from anywherePorts 80,443: ALLOW from Cloudflare ranges only4. Verify
Section titled “4. Verify”Three checks:
# 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 5Expected results: first two return 200, third times out. The timeout proves the firewall is blocking direct access.
Updating Cloudflare IP ranges
Section titled “Updating Cloudflare IP ranges”Cloudflare publishes new IP ranges here and here semi-annually. Re-run the script when they change:
CONFIRM=yes ./scripts/ufw.example.shIdempotent. Safe to run anytime.
What this doesn’t protect against
Section titled “What this doesn’t protect against”- 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.
Related
Section titled “Related”- Deployment - the production sequence this sits in.
- Provisioning with OpenTofu - cloud firewall as an alternative.
- Backups and Image updates - the other production essentials.