docs: document the Zoraxy deployment (README + config example)
- config.example.yaml: listen 0.0.0.0:8080, base_url https://myday.sechpoint.app, trusted_proxies = Zoraxy; a comment stating :8080 must be firewalled to Zoraxy only and that the bootstrap is tunnel-only (D-025/D-043). - README: a Deployment section (TLS at the proxy, the app on 0.0.0.0:8080, the firewall rule) and the tunnel-only bootstrap (ssh -L 8080:127.0.0.1:8080 → http://127.0.0.1:8080/setup, the one path a proxy request cannot take).
This commit is contained in:
parent
f0927fb0f0
commit
9a9888fa7b
2 changed files with 50 additions and 17 deletions
51
README.md
51
README.md
|
|
@ -41,29 +41,56 @@ go build -o myday ./cmd/myday
|
||||||
## ⚠️ First run — do NOT expose it publicly
|
## ⚠️ First run — do NOT expose it publicly
|
||||||
|
|
||||||
The **first user becomes the top admin** (the root of the chain). So the instance bootstraps
|
The **first user becomes the top admin** (the root of the chain). So the instance bootstraps
|
||||||
**loopback-only**, running a **`/setup` wizard** until that admin exists.
|
through a **`/setup` wizard** that is reachable **only over a direct SSH tunnel**, until that admin
|
||||||
|
exists.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# on the server it listens on 127.0.0.1:8080 only (config.example.yaml)
|
# from your workstation — tunnel to the app's port on the server
|
||||||
ssh -L 8080:127.0.0.1:8080 user@server # from your workstation
|
ssh -L 8080:127.0.0.1:8080 user@server
|
||||||
# then open http://127.0.0.1:8080/setup and complete the wizard
|
# then open http://127.0.0.1:8080/setup and complete the wizard
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Loopback is enforced per request, not just at bind.** While in setup mode the app answers **404**
|
> **The bootstrap is tunnel-only, enforced per request** (D-025 revised). While in setup mode the app
|
||||||
> to any request whose client is not loopback — so a reverse proxy must **not** sit in front of a
|
> answers **404** to any request that is not the direct tunnel — i.e. a request whose **TCP peer is
|
||||||
> fresh instance, even one bound to `127.0.0.1`. Bootstrap over the SSH tunnel (above) first.
|
> not loopback**, **or which carries an `X-Forwarded-For`** header. So a reverse proxy (even one on
|
||||||
|
> the same host) **cannot** reach a fresh instance: a same-host proxy presents a loopback peer but
|
||||||
|
> always adds `X-Forwarded-For`, which gives it away. Bootstrap over the SSH tunnel (above) **first**
|
||||||
|
> — then expose the instance behind the proxy.
|
||||||
|
|
||||||
> **Reach the app over `localhost` / `127.0.0.1` — not a hostname.** With an `https://` `base_url`
|
> **Reach the app over `localhost` / `127.0.0.1` — not a hostname.** Only a **loopback** origin
|
||||||
> the CSRF and session cookies are marked **`Secure`**, so they are **not** sent over plain
|
> counts as a trustworthy/secure context for the browser's **WebAuthn** dialogs. So for first run and
|
||||||
> `http://`; and only a **loopback** origin counts as a trustworthy/secure context for the
|
> any human testing, open the app at **`http://localhost:8080`** or **`http://127.0.0.1:8080`** (via
|
||||||
> browser's **WebAuthn** dialogs. So for first run and any human testing, open the app at
|
> the tunnel above) — never at a bare server hostname. Over plain http the cookies are sent without
|
||||||
> **`http://localhost:8080`** or **`http://127.0.0.1:8080`** (via the tunnel above) — never at a
|
> `Secure` (needed by the bootstrap); behind the proxy (https) they are hardened — see *Deployment*.
|
||||||
> bare server hostname.
|
|
||||||
|
|
||||||
Only **after** the top admin exists should you put MyDay behind a reverse proxy and expose it.
|
Only **after** the top admin exists should you put MyDay behind a reverse proxy and expose it.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Deployment — behind Zoraxy (TLS at the proxy)
|
||||||
|
|
||||||
|
The reference deployment terminates **TLS at [Zoraxy](https://github.com/tobychui/zoraxy)**; the app
|
||||||
|
itself speaks **plain HTTP**. Config (`config.example.yaml` → `config.yaml`, D-043):
|
||||||
|
|
||||||
|
- **App** — `server.listen: "0.0.0.0:8080"`; the app binds any address (there is **no** setup-mode
|
||||||
|
bind refusal, D-025 revised).
|
||||||
|
- **Proxy** — Zoraxy holds the certificate and forwards to `127.0.0.1:8080`, setting
|
||||||
|
`X-Forwarded-For` and `X-Forwarded-Proto`.
|
||||||
|
- **`base_url`** — `https://myday.sechpoint.app`: the public origin, used in invite/recovery links.
|
||||||
|
The app **honours `X-Forwarded-Proto`** from a trusted proxy (`server.trusted_proxies` = Zoraxy) to
|
||||||
|
decide a request is `https`, and then sets the session cookie **`Secure` + `__Host-`** and sends
|
||||||
|
**HSTS**. Over plain http (the tunnel) it keeps the bare, non-`Secure` cookie so the bootstrap works.
|
||||||
|
- **Firewall** — **`:8080` MUST be reachable only from Zoraxy.** Anything else that can reach the
|
||||||
|
port bypasses the proxy **and its TLS**; the app would then serve plain HTTP to that client.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# the tunnel-only bootstrap — the one path a proxy request cannot take
|
||||||
|
ssh -L 8080:127.0.0.1:8080 user@server
|
||||||
|
# open http://127.0.0.1:8080/setup (before the first admin exists)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## What `/setup` writes — and the restart
|
## What `/setup` writes — and the restart
|
||||||
|
|
||||||
The wizard **persists `config.yaml`** (`setup: false`, the base URL, mail/locale, and the secrets)
|
The wizard **persists `config.yaml`** (`setup: false`, the base URL, mail/locale, and the secrets)
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,21 @@
|
||||||
# Settings live HERE, not in the database (D-035).
|
# Settings live HERE, not in the database (D-035).
|
||||||
|
|
||||||
# ── setup & recovery ─────────────────────────────────────────────────────────
|
# ── setup & recovery ─────────────────────────────────────────────────────────
|
||||||
setup: true # D-015 / D-026 — while true the app serves ONLY /setup (loopback-only)
|
setup: true # D-015 / D-026 — while true the app serves ONLY /setup.
|
||||||
|
# D-025 (revised): the wizard is reachable ONLY over the
|
||||||
|
# direct SSH tunnel — a request relayed by a proxy (a
|
||||||
|
# loopback peer carrying X-Forwarded-For) is 404'd.
|
||||||
admin_recovery: false # D-033 — set true + restart to email user 1 an enrolment link
|
admin_recovery: false # D-033 — set true + restart to email user 1 an enrolment link
|
||||||
|
|
||||||
# ── server ───────────────────────────────────────────────────────────────────
|
# ── server ───────────────────────────────────────────────────────────────────
|
||||||
|
# D-043 (deployment behind Zoraxy): TLS terminates at the PROXY, so the app
|
||||||
|
# speaks plain HTTP and binds 0.0.0.0:8080. FIREWALL :8080 TO ZORAXY ONLY —
|
||||||
|
# anything else that can reach the port bypasses the proxy and its TLS.
|
||||||
server:
|
server:
|
||||||
listen: "127.0.0.1:8080" # D-025 — loopback during setup; bind the public iface only after
|
listen: "0.0.0.0:8080" # D-043 — any bind is allowed (no setup-mode refusal)
|
||||||
base_url: "https://myday.example.com" # used in invite / recovery links
|
base_url: "https://myday.sechpoint.app" # the PUBLIC origin: invite / recovery links
|
||||||
trusted_proxies: # XFF is resolved from these (A6)
|
trusted_proxies: # XFF + X-Forwarded-Proto are honoured from these (A6/D-043)
|
||||||
- "127.0.0.1"
|
- "127.0.0.1" # Zoraxy's address (127.0.0.1 if it runs on this host)
|
||||||
|
|
||||||
# ── app ──────────────────────────────────────────────────────────────────────
|
# ── app ──────────────────────────────────────────────────────────────────────
|
||||||
app:
|
app:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue