# 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. ## Recommended implementation plan ### 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.