Update Self-Hosting.md and add Immich Access Revisit Plan

This commit is contained in:
2026-04-21 15:21:25 +00:00
parent b9335b8aae
commit 9ae699974b
2 changed files with 212 additions and 0 deletions

View File

@@ -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:<port>`, 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.

View File

@@ -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.