I spent part of today getting Nextcloud properly hardened behind Traefik + Cloudflare tunnel. Seemed simple. Two gotchas nearly wasted an hour.
The Stack
- Nextcloud 29 in Docker on a Proxmox LXC
- Traefik v3 as reverse proxy (separate LXC)
- Cloudflare Tunnel for public access — no open inbound ports
- Domain:
cloud.example.com
Problem 1: Nextcloud Doesn't Know the Real Client IP
Out of the box, Nextcloud sees Traefik's IP as every client's IP. This breaks:
- Brute force protection (bans your proxy instead of attackers)
- Rate limiting
- Admin logs (every request looks like it's from the same IP)
Fix — set trusted proxies via occ:
docker exec -u 33 nextcloud php occ config:system:set trusted_proxies 0 \
--value=192.168.1.x # your Traefik IP
# If you use Cloudflare Tunnel, add all Cloudflare CIDRs too:
docker exec -u 33 nextcloud php occ config:system:set trusted_proxies 1 \
--value=173.245.48.0/20
# ... (repeat for all Cloudflare CIDRs from cloudflare.com/ips)
docker exec -u 33 nextcloud php occ config:system:set forwarded_for_headers 0 \
--value=HTTP_X_FORWARDED_FOR
Note the -u 33 — run as www-data, not root, or occ refuses.
Problem 2: HSTS Headers Disappear Through Cloudflare Tunnel
Traefik has a built-in stsSeconds middleware:
middlewares:
default-headers:
headers:
browserXssFilter: true
contentTypeNosniff: true
stsSeconds: 31536000
This should send Strict-Transport-Security. It does — when traffic hits Traefik directly. But through Cloudflare Tunnel, Cloudflare terminates TLS before your Traefik sees it. Traefik receives HTTP from the tunnel daemon, so it treats the connection as non-HTTPS and suppresses STS.
Fix: Force it as an explicit custom response header instead of relying on stsSeconds:
middlewares:
default-headers:
headers:
browserXssFilter: true
contentTypeNosniff: true
stsSeconds: 31536000
stsIncludeSubdomains: true
customResponseHeaders:
Strict-Transport-Security: "max-age=31536000; includeSubDomains; preload"
Then apply the middleware to your Nextcloud router:
routers:
nextcloud-https:
entryPoints:
- websecure
middlewares:
- default-headers
rule: Host(`cloud.example.com`)
service: nextcloud
tls:
certResolver: cloudflare
Traefik hot-reloads dynamic config — no restart needed. Verify:
curl -I https://cloud.example.com/login | grep -i strict
# strict-transport-security: max-age=31536000; includeSubDomains; preload ✓
Other Warnings to Clear
Run occ setupchecks to audit everything. Common ones:
Maintenance window not set — without this, heavy background jobs run during your peak hours:
docker exec -u 33 nextcloud php occ config:system:set \
maintenance_window_start --value=1 --type=integer
No default phone region — affects phone number validation in profiles:
docker exec -u 33 nextcloud php occ config:system:set \
default_phone_region --value=CA
Installing Apps via CLI
Skip the web UI for bulk installs. occ app:install pulls from the Nextcloud app store directly:
docker exec -u 33 nextcloud php occ app:install calendar
docker exec -u 33 nextcloud php occ app:install contacts
docker exec -u 33 nextcloud php occ app:install notes
docker exec -u 33 nextcloud php occ app:install tasks
docker exec -u 33 nextcloud php occ app:install deck # Kanban boards
docker exec -u 33 nextcloud php occ app:install spreed # Talk/video
docker exec -u 33 nextcloud php occ app:enable bruteforcesettings
Each installs and enables in one step. Much faster than clicking through Settings → Apps.
Final Security Header Check
After all of the above:
curl -I https://cloud.example.com/login 2>&1 | grep -iE "strict|x-content|x-frame|x-xss"
Expected output:
strict-transport-security: max-age=31536000; includeSubDomains; preload
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
TL;DR
- Set
trusted_proxies+forwarded_for_headersvia occ — critical if you're behind any proxy -
stsSecondsalone won't work through Cloudflare Tunnel — usecustomResponseHeadersto force HSTS -
occ app:installbeats the web UI for batch app installs - Run
occ setupchecks— it tells you exactly what's wrong
Nextcloud's occ CLI does most heavy lifting without ever touching the admin UI. Once you're comfortable with it, setup goes fast.
Top comments (0)