diff --git a/docs/reference/https/caddy.md b/docs/reference/https/caddy.md index b747e978f..33b99047d 100644 --- a/docs/reference/https/caddy.md +++ b/docs/reference/https/caddy.md @@ -107,6 +107,59 @@ docker compose up -d You should now be able to access Open WebUI at `https://your-domain.com`. +## Optional: Security Headers via Reverse Proxy + +Open WebUI supports setting security headers directly at the application layer via environment variables +(recommended path — see [Security Headers](/getting-started/advanced-topics/hardening#security-headers)). +If you manage multiple applications behind a single Caddy instance, or if your deployment does not allow +direct environment variable access, you can set these headers at the proxy layer instead. + +**Choose one layer only.** Setting the same header in both places produces duplicate directives: +duplicate `Content-Security-Policy` headers are combined by browsers (intersection), which can +unexpectedly tighten or break your policy, while duplicate `Strict-Transport-Security` or +`X-Frame-Options` headers lead to ambiguous, browser-dependent behavior. If you already set any of +`HSTS`, `XFRAME_OPTIONS`, `XCONTENT_TYPE`, `REFERRER_POLICY`, `PERMISSIONS_POLICY`, or +`CONTENT_SECURITY_POLICY` via environment variables, skip this section. + +Add the following `header` block inside your existing site block: + +```caddyfile +your.domain.example { + header { + Strict-Transport-Security "max-age=31536000; includeSubDomains" + X-Frame-Options "DENY" + X-Content-Type-Options "nosniff" + Referrer-Policy "strict-origin-when-cross-origin" + Permissions-Policy "camera=(), microphone=(), geolocation=()" + -Server + } + + reverse_proxy localhost:8080 +} +``` + +**HSTS is sticky and hard to undo.** `max-age=31536000` (1 year) with `includeSubDomains` instructs every browser that has visited this domain to refuse plain HTTP for the domain **and all its subdomains** for a full year — this cannot be reverted server-side quickly (you must serve `max-age=0` and wait for each client to revisit). Only keep `includeSubDomains` if every current and future subdomain serves valid HTTPS. If unsure, start with a short value such as `max-age=300` and increase it after confirming HTTPS works everywhere. Do **not** add `preload` unless you understand it is near-permanent and requires separate submission to the browser preload list. + +The `-Server` directive removes the `Server` response header that Caddy adds by default. +Caddy handles WebSocket upgrades and SSE connections automatically — no additional configuration +is required for Open WebUI's streaming responses. + +The `Permissions-Policy` example above is a deny-list of common sensitive features, not an exhaustive +list. If your deployment uses voice input or webcam capture, set the relevant directive to `(self)` +instead of removing it. + +**Content-Security-Policy.** CSP is highly deployment-specific (custom plugins, embedded iframes, +external model providers, Artifacts/code rendering, voice input), so this section intentionally does +**not** ship a ready-made proxy-layer CSP. Set CSP via the `CONTENT_SECURITY_POLICY` environment +variable and use `CONTENT_SECURITY_POLICY_REPORT_ONLY` to derive a working policy before enforcing it — +see [Security Headers](/getting-started/advanced-topics/hardening#security-headers). + +Verify headers are applied after reloading Caddy: + +```bash +curl -sI https://your.domain.example | grep -E "Strict-Transport|X-Frame|X-Content|Referrer|Permissions" +``` + ## Updating Open WebUI I wanted to include a quick note on how to update Open WebUI without losing your data. Since we're using a volume to store the data, you can simply pull the latest image and restart the container.