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), with records for the apex and
app. - The server has a static IPv4 address.
- An ACME contact email (real address; Let’s Encrypt rejects
example.com). - An
apprecord for the app host, proxied, alongside the apex record.
1. Configure the domain and ACME email
Section titled “1. Configure the domain and ACME email”echo 'PUBLIC_SITE_HOST=example.com' >> compose/.envecho 'PUBLIC_UI_HOST=app.example.com' >> compose/.envecho 'SITE_CONTACT_EMAIL=hello@example.com' >> compose/.envecho 'SITE_IMAGE_TAG=v0.1.0' >> compose/.envecho 'ACME_EMAIL=ops@example.com' >> compose/.envThe apex (example.com) serves the public site. The app lives on app.example.com, and Traefik routes /api/* and /health on that host to the API and everything else on it to the UI. Both hosts need a Cloudflare proxied A/AAAA record pointing at this server. Traefik issues one Let’s Encrypt certificate per host.
To keep the single-domain layout instead, set WITH_SITE=0 and PUBLIC_UI_HOST=example.com, and add only the apex record.
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 against the app host, plus one for the apex:
# Local (self-signed OK)curl -sI -H 'Host: app.example.com' https://localhost/health -k
# Via Cloudflare (the normal path)curl -sI https://app.example.com/health
# Direct to server IP (should timeout - UFW is working)curl -sI --resolve app.example.com:443:<your-vps-ip> https://app.example.com/health --max-time 5
# Public site on the apexcurl -sI https://example.com/Expected results: the first two and the apex return 200, the 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.