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-Optionsor CSPframe-ancestors) X-Content-Type-Optionsmissing- Content-Security-Policy not set
- Permissions-Policy not set
- Cross-Origin-Resource-Policy missing or invalid
X-Powered-Byleaks implementation details- Minor cache-control warnings
- Minor
Content-Typeissue forfavicon.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 conservativelyX-Content-Type-Options: nosniff- Anti-clickjacking protection, preferably
Content-Security-Policy: frame-ancestors 'self'if supported cleanly, otherwiseX-Frame-Options: SAMEORIGIN - Remove or overwrite
X-Powered-Bywhere 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-PolicyReferrer-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:
- Pangolin-supported middleware or advanced config, if Pangolin exposes a stable way to attach Traefik headers middleware to a specific route
- Traefik dynamic configuration file/provider mounted into the Traefik container and referenced by the Immich router
- 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:
- Apply one small change or one tightly related bundle
- Reload/redeploy Traefik/Pangolin config
- Validate headers manually:
curl -I https://immich-test-2.frusetik.com- browser devtools network tab
- 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
- Re-run OWASP ZAP baseline scan
- Check browser console for CSP, CORP, mixed-content, frame, or MIME warnings
- 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, removeX-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-Onlyfirst 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.commapped 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:
- Inspect Pangolin + Traefik config locations and how route-specific middleware is attached
- Add a route-specific middleware for:
X-Content-Type-Options: nosniff- clickjacking protection (
frame-ancestors 'self'orX-Frame-Options: SAMEORIGIN) - suppress
X-Powered-By
- Test manually
- Re-run ZAP baseline
- Add conservative HSTS
- Test again
- 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.