docs(spec): external reverse-proxy TLS mode (Zoraxy in front)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>feat/v1-foundation
parent
cc8e60af5a
commit
19fc6e1f34
|
|
@ -0,0 +1,82 @@
|
|||
# External reverse-proxy TLS mode (Zoraxy in front) — Design
|
||||
|
||||
**Date:** 2026-06-14 · **Branch:** `feat/v1-foundation` · **Status:** approved (build as 0.8.0)
|
||||
|
||||
Today Clusev's Caddy obtains a Let's Encrypt certificate **on demand** for the configured panel
|
||||
domain (`/_caddy/ask` approves only that domain) and `PanelScheme` forces HTTPS for it. That fails or
|
||||
is unwanted when the operator runs an **upstream reverse proxy (e.g. Zoraxy)** that already terminates
|
||||
TLS — or for a purely local/internal domain. This feature adds a **TLS mode** where Caddy does **not**
|
||||
issue certs and the panel is served over HTTP for the upstream proxy to wrap.
|
||||
|
||||
## Operator decisions (locked)
|
||||
- Mode: **external reverse-proxy** (not self-signed).
|
||||
- Topology: **Zoraxy terminates TLS → forwards to Clusev's existing Caddy HTTP port** (keeps the
|
||||
Reverb/WebSocket routing `/app`, `/apps`). Caddy is configured to **honor the upstream's
|
||||
`X-Forwarded-Proto`** so the app sees the real scheme.
|
||||
|
||||
## 1. Setting + DeploymentService
|
||||
- New Setting key `tls_mode` ∈ {`caddy` (default), `external`}, via the existing `Setting` model.
|
||||
- `DeploymentService::externalTls(): bool` = `Setting::get('tls_mode', 'caddy') === 'external'`.
|
||||
- `DeploymentService::setTlsMode(string $mode): void` (validates the enum, persists, busts cache).
|
||||
- `hasTls()` stays as-is for the `caddy` path; in `external` mode TLS is asserted by the upstream, so
|
||||
HSTS/secure-cookie decisions follow the **real request scheme** (`$request->isSecure()` via the
|
||||
honored `X-Forwarded-Proto`), not a static flag.
|
||||
|
||||
## 2. Caddy `/_caddy/ask` — no ACME in external mode
|
||||
The `caddy.ask` route currently approves cert issuance for the configured domain. In `external` mode
|
||||
it must **deny every host** (HTTP 403 / non-`ok` body) so Caddy never attempts ACME — the upstream
|
||||
proxy owns the certificate. In `caddy` mode the behaviour is unchanged (approve the active domain only).
|
||||
|
||||
## 3. PanelScheme — defer HTTPS to the upstream in external mode
|
||||
With a domain set and `external` mode:
|
||||
- Keep the **host enforcement** (only the configured domain by hostname; the literal server IP stays
|
||||
the plain-HTTP recovery path; `_caddy/*` + health exempt).
|
||||
- **Skip the app-level `redirect → https://domain:443`** — the upstream proxy performs HTTP→HTTPS, and
|
||||
Caddy here is HTTP-only, so a 443 redirect would be wrong/unreachable.
|
||||
- Cookie `Secure` + HSTS still follow `$request->isSecure()`, which now reflects the upstream's
|
||||
`X-Forwarded-Proto` (honored by Caddy — see §4) and Laravel's `trustProxies(at: '*')`.
|
||||
In `caddy` mode `PanelScheme` is unchanged (still forces HTTPS for the active domain).
|
||||
|
||||
## 4. Caddyfile — honor the upstream X-Forwarded-Proto (env-gated)
|
||||
Caddy's `reverse_proxy` overwrites `X-Forwarded-Proto` with the scheme **it** received (HTTP from
|
||||
Zoraxy), losing the original `https`. To recover it, add a global
|
||||
`servers { trusted_proxies static {$TRUSTED_PROXY_CIDR} }` block so Caddy trusts the upstream's
|
||||
`X-Forwarded-*` (incl. Proto) from that CIDR. New env `TRUSTED_PROXY_CIDR` (default **empty** → no
|
||||
trust, current behaviour). The operator sets it to Zoraxy's address/range and restarts the stack when
|
||||
using external mode. (The feature still *functions* without it — the panel works over HTTP behind
|
||||
Zoraxy — but the `Secure` cookie flag + HSTS are only correct once it's set.)
|
||||
|
||||
## 5. UI — System → Domain & TLS
|
||||
In `App\Livewire\System\Index` (the domain editor), add a **TLS-Modus** control beside the domain:
|
||||
`Caddy (Let's Encrypt, automatisch)` vs `Externer Reverse-Proxy (TLS davor)`. Saving persists
|
||||
`tls_mode` and — like a domain change — surfaces the **"restart required"** notice (and, for external
|
||||
mode, a hint to set `TRUSTED_PROXY_CIDR` to the proxy's address). Audited via the shared confirm modal,
|
||||
mirroring `confirmDomain`.
|
||||
|
||||
## Files
|
||||
- `app/Models/Setting.php` — n/a (generic key/value).
|
||||
- `app/Services/DeploymentService.php` — `externalTls()`, `setTlsMode()`.
|
||||
- the `caddy.ask` closure in `routes/web.php` — deny in external mode.
|
||||
- `app/Http/Middleware/PanelScheme.php` — skip the forced redirect in external mode.
|
||||
- `app/Livewire/System/Index.php` + its view — the TLS-mode control + restart/CIDR hint.
|
||||
- `docker/caddy/Caddyfile` — `trusted_proxies static {$TRUSTED_PROXY_CIDR}` global option.
|
||||
- `.env.example` / docker-compose — document `TRUSTED_PROXY_CIDR`.
|
||||
- `lang/{de,en}/system.php` — the mode labels, restart + CIDR hints, audit copy.
|
||||
|
||||
## Testing
|
||||
- `externalTls()` reflects the setting; `setTlsMode()` validates the enum.
|
||||
- `caddy.ask`: external mode → deny (non-ok) for the configured domain; caddy mode → approve it.
|
||||
- `PanelScheme`: external mode + domain + HTTP request → **no** redirect (200/pass), host check still
|
||||
refuses a foreign host, bare-IP still served; caddy mode unchanged (still 301→https).
|
||||
- System UI: switching mode persists `tls_mode`, shows the restart notice; DE+EN; R12 (200, no console
|
||||
errors) at the 3 breakpoints; Codex clean.
|
||||
|
||||
## Safety invariants
|
||||
- Never lock the operator out: the bare-IP HTTP recovery path stays reachable in **both** modes.
|
||||
- External mode must not silently leave ACME on (cert-issuance spam) — `/_caddy/ask` denies.
|
||||
- Honoring `X-Forwarded-Proto` is gated by `TRUSTED_PROXY_CIDR`; with it empty, no header from an
|
||||
untrusted client is ever trusted (no scheme spoofing).
|
||||
|
||||
## Out of scope
|
||||
- Self-signed certificates for bare-IP (operator chose external-proxy mode only).
|
||||
- Auto-detecting the proxy IP (operator sets `TRUSTED_PROXY_CIDR`).
|
||||
Loading…
Reference in New Issue