From 9ae699974b6ccee953e856d3dccdf38bfc8ad8b7 Mon Sep 17 00:00:00 2001 From: Orik Date: Tue, 21 Apr 2026 15:21:25 +0000 Subject: [PATCH] Update Self-Hosting.md and add Immich Access Revisit Plan --- 04_Topics/Self-Hosting.md | 9 + 05_Resources/Immich Access Revisit Plan.md | 203 +++++++++++++++++++++ 2 files changed, 212 insertions(+) create mode 100644 05_Resources/Immich Access Revisit Plan.md diff --git a/04_Topics/Self-Hosting.md b/04_Topics/Self-Hosting.md index a2578c3..9dce765 100644 --- a/04_Topics/Self-Hosting.md +++ b/04_Topics/Self-Hosting.md @@ -64,3 +64,12 @@ - Proxmox is for ephemeral/experimental services - Dell is for always-on base services (DNS, backups, HA) - Synology is bulk storage + media + +## Pangolin reverse proxy notes + +- Pangolin runs in Docker, so when exposing a service that is running directly on the VPS host, the backend must be reachable from the container network, not just from the host itself. +- For Pangolin public resources that forward to host-level services, use the Docker-to-host reachable IP (`172.17.0.1`) rather than `127.0.0.1`. +- `127.0.0.1` inside the Pangolin container refers to the container loopback, not the VPS host loopback. +- If a VPS service is bound only to `127.0.0.1:`, Pangolin cannot reach it from the isolated Docker network. +- For example, Gitea did not work when forwarded to `127.0.0.1:3000`; removing the `127.0.0.1` bind and exposing the service on a host-reachable interface made it work. +- Practical rule: when a reverse proxy lives in Docker but the upstream service lives on the host, confirm both the host IP and the bind address are reachable from the container namespace. diff --git a/05_Resources/Immich Access Revisit Plan.md b/05_Resources/Immich Access Revisit Plan.md new file mode 100644 index 0000000..9c2536b --- /dev/null +++ b/05_Resources/Immich Access Revisit Plan.md @@ -0,0 +1,203 @@ +# 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.