Files
second-brain/05_Resources/Immich Access Revisit Plan.md

8.3 KiB

Immich Access Revisit Plan

Context

We migrated several apps from Nginx to Pangolin on the VPS. Pangolin uses Traefik underneath as the reverse proxy. At least one test route, immich-test-2.frusetik.com, is publicly reachable over HTTPS through Pangolin/Traefik and works functionally.

An OWASP ZAP baseline scan against https://immich-test-2.frusetik.com showed mostly passive header and hardening findings, not critical exploit findings.

Main findings:

  • Strict-Transport-Security header not set
  • Missing anti-clickjacking header (X-Frame-Options or CSP frame-ancestors)
  • X-Content-Type-Options missing
  • Content-Security-Policy not set
  • Permissions-Policy not set
  • Cross-Origin-Resource-Policy missing or invalid
  • X-Powered-By leaks implementation details
  • Minor cache-control warnings
  • Minor Content-Type issue for favicon.ico

Important decision context:

  • Do not start by changing Immich app behavior.
  • Prefer fixing this at the reverse proxy edge, meaning Traefik under Pangolin.
  • Pangolin may not expose first-class UI support for response security headers, so implementation may require Traefik middleware directly or a Pangolin-compatible middleware layer.
  • Goal for the next pass is a practical rollout plan first, not immediate implementation.

1. Prioritize these headers first

Tier 1, high value, low risk

Implement these first at the reverse proxy layer:

  • Strict-Transport-Security (HSTS), starting conservatively
  • X-Content-Type-Options: nosniff
  • Anti-clickjacking protection, preferably Content-Security-Policy: frame-ancestors 'self' if supported cleanly, otherwise X-Frame-Options: SAMEORIGIN
  • Remove or overwrite X-Powered-By where possible

Reason:

  • These usually deliver real hardening value
  • They are unlikely to break a normal Immich deployment when rolled out carefully
  • They address the clearest ZAP findings first

Tier 2, useful but needs care

  • Permissions-Policy
  • Referrer-Policy (even if ZAP did not call it out, it is often worth setting while touching edge headers)
  • Cross-Origin-Resource-Policy

Reason:

  • Useful cleanup and defense-in-depth
  • More likely to have edge-case behavior depending on how the app serves media, embeds resources, or uses browser APIs

Tier 3, highest risk / most annoying

  • Full Content-Security-Policy

Reason:

  • CSP is valuable, but also the most likely to break Immich UI, API calls, previews, maps, workers, WebSockets, or third-party assets if applied too aggressively
  • Do not enable a strict CSP early without observing actual resource usage first

2. Where to implement in Pangolin + Traefik

Target layer: Traefik response middleware at the edge, attached only to the Immich router/service first.

Preferred order of implementation options:

  1. Pangolin-supported middleware or advanced config, if Pangolin exposes a stable way to attach Traefik headers middleware to a specific route
  2. Traefik dynamic configuration file/provider mounted into the Traefik container and referenced by the Immich router
  3. Docker labels on the relevant service/router, if that is how Pangolin composes Traefik config internally and if custom labels are supported safely

Practical principle:

  • Start route-specific, not global
  • Prove the header set on immich-test-2.frusetik.com
  • Only later consider promoting a safe subset to a shared middleware for other apps

3. Safe testing sequence after each change

For every header change:

  1. Apply one small change or one tightly related bundle
  2. Reload/redeploy Traefik/Pangolin config
  3. Validate headers manually:
    • curl -I https://immich-test-2.frusetik.com
    • browser devtools network tab
  4. Validate Immich behavior manually:
    • login page loads
    • login works
    • photo thumbnails load
    • full image/video views load
    • search/basic navigation works
    • mobile app or external clients still connect, if relevant
  5. Re-run OWASP ZAP baseline scan
  6. Check browser console for CSP, CORP, mixed-content, frame, or MIME warnings
  7. Keep rollback simple, ideally by removing a single middleware reference or reverting one dynamic-config block

Recommended rollout bundles:

  • Bundle A: X-Content-Type-Options, clickjacking header, remove X-Powered-By
  • Bundle B: HSTS with conservative settings
  • Bundle C: Permissions-Policy + optional Referrer-Policy
  • Bundle D: CORP if still useful after observing actual asset behavior
  • Bundle E: CSP in report-first or minimal mode

4. Headers that may be risky or annoying to enable immediately

Content-Security-Policy

Highest risk. Possible breakage areas:

  • inline scripts/styles
  • API endpoints
  • media blobs
  • WebSockets
  • map tiles / third-party origins
  • workers / object URLs

Safer approach later:

  • start with very small policy pieces such as frame-ancestors 'self'
  • or use Content-Security-Policy-Report-Only first if feasible

Strict-Transport-Security

Useful, but do not jump immediately to a long max-age with includeSubDomains and preload. Safer start:

  • modest max-age, confirm no HTTP fallback dependencies remain, then increase later

Cross-Origin-Resource-Policy

Can interfere with media/resource loading depending on how Immich serves assets and whether anything cross-origin is intentional. Start only after confirming actual request patterns.

Permissions-Policy

Usually not dangerous, but easy to over-tighten if Immich or browser features rely on camera, microphone, geolocation, fullscreen, etc. Needs app-aware review.

5. What to inspect before implementation

Before touching config, inspect these exact areas:

A. Pangolin deployment and compose files

Need to locate:

  • Pangolin docker-compose.yml / compose stack
  • Traefik container definition
  • mounted config volumes
  • environment variables controlling Traefik/Pangolin integration
  • whether Pangolin regenerates or overwrites Traefik config automatically

Questions to answer:

  • Is Traefik running as its own container or embedded in the Pangolin stack?
  • Where does dynamic config live?
  • What survives container restarts or stack updates?

B. Traefik static and dynamic config

Need to inspect:

  • static Traefik config file (traefik.yml / traefik.toml / command args)
  • dynamic config directory or file provider
  • existing middlewares
  • existing routers/services for the Immich test route

Questions to answer:

  • How is immich-test-2.frusetik.com mapped today?
  • Can a custom headers middleware be attached cleanly to only that router?
  • Is there already a shared security middleware that should be extended instead of duplicated?

C. Pangolin route/resource definitions

Need to inspect:

  • how Pangolin stores public-resource definitions
  • whether there is a UI/API/config field for advanced middleware, headers, or Traefik labels
  • whether Pangolin regenerates routes from an internal database/config API

Questions to answer:

  • Is custom middleware supported directly?
  • If not, what is the least fragile escape hatch?

D. Immich upstream service definition

Need to inspect:

  • compose/service definition for Immich and related containers
  • whether responses already include any app-set headers
  • whether WebSockets or special upstream behavior need to be preserved

Questions to answer:

  • Are any headers already being set upstream and potentially overwritten?
  • Does the route use WebSockets, SSE, special caching, or asset paths that constrain proxy hardening?

E. Current public response behavior

Capture a baseline before changes:

  • curl -I https://immich-test-2.frusetik.com
  • full response headers for key pages and asset URLs
  • browser devtools export or notes for console/network behavior
  • current ZAP baseline output

This gives a clean before/after comparison and rollback confidence.

Suggested first implementation later

When revisiting, the safest first real pass is:

  1. Inspect Pangolin + Traefik config locations and how route-specific middleware is attached
  2. Add a route-specific middleware for:
    • X-Content-Type-Options: nosniff
    • clickjacking protection (frame-ancestors 'self' or X-Frame-Options: SAMEORIGIN)
    • suppress X-Powered-By
  3. Test manually
  4. Re-run ZAP baseline
  5. Add conservative HSTS
  6. Test again
  7. Leave CSP for a separate deliberate pass

Decision rule

If Pangolin does not provide a stable, update-safe way to express these headers, prefer a Traefik dynamic-config middleware over ad-hoc hacks inside the app container.