DEV Community

Cover image for Homelab Day: Nextcloud + Traefik + HSTS Headers That Actually Work
PyHackSecGP
PyHackSecGP

Posted on

Homelab Day: Nextcloud + Traefik + HSTS Headers That Actually Work

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 ✓
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

TL;DR

  1. Set trusted_proxies + forwarded_for_headers via occ — critical if you're behind any proxy
  2. stsSeconds alone won't work through Cloudflare Tunnel — use customResponseHeaders to force HSTS
  3. occ app:install beats the web UI for batch app installs
  4. 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)