From 9a9888fa7bb57a1aeb1244b7d162a1478b6ba230 Mon Sep 17 00:00:00 2001 From: Claus Lohmar Date: Thu, 15 Oct 2026 05:40:38 +0000 Subject: [PATCH] docs: document the Zoraxy deployment (README + config example) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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). --- README.md | 51 ++++++++++++++++++++++++++++++++++----------- config.example.yaml | 16 +++++++++----- 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 977e4d0..6f17547 100644 --- a/README.md +++ b/README.md @@ -41,29 +41,56 @@ go build -o myday ./cmd/myday ## ⚠️ First run — do NOT expose it publicly 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 -# on the server it listens on 127.0.0.1:8080 only (config.example.yaml) -ssh -L 8080:127.0.0.1:8080 user@server # from your workstation +# from your workstation — tunnel to the app's port on the server +ssh -L 8080:127.0.0.1:8080 user@server # 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** -> to any request whose client is not loopback — so a reverse proxy must **not** sit in front of a -> fresh instance, even one bound to `127.0.0.1`. Bootstrap over the SSH tunnel (above) first. +> **The bootstrap is tunnel-only, enforced per request** (D-025 revised). While in setup mode the app +> answers **404** to any request that is not the direct tunnel — i.e. a request whose **TCP peer is +> 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` -> the CSRF and session cookies are marked **`Secure`**, so they are **not** sent over plain -> `http://`; and only a **loopback** origin counts as a trustworthy/secure context for the -> browser's **WebAuthn** dialogs. So for first run and any human testing, open the app at -> **`http://localhost:8080`** or **`http://127.0.0.1:8080`** (via the tunnel above) — never at a -> bare server hostname. +> **Reach the app over `localhost` / `127.0.0.1` — not a hostname.** Only a **loopback** origin +> counts as a trustworthy/secure context for the browser's **WebAuthn** dialogs. So for first run and +> any human testing, open the app at **`http://localhost:8080`** or **`http://127.0.0.1:8080`** (via +> the tunnel above) — never at a bare server hostname. Over plain http the cookies are sent without +> `Secure` (needed by the bootstrap); behind the proxy (https) they are hardened — see *Deployment*. 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 The wizard **persists `config.yaml`** (`setup: false`, the base URL, mail/locale, and the secrets) diff --git a/config.example.yaml b/config.example.yaml index e47f59f..9872aca 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -4,15 +4,21 @@ # Settings live HERE, not in the database (D-035). # ── 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 # ── 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: - listen: "127.0.0.1:8080" # D-025 — loopback during setup; bind the public iface only after - base_url: "https://myday.example.com" # used in invite / recovery links - trusted_proxies: # XFF is resolved from these (A6) - - "127.0.0.1" + listen: "0.0.0.0:8080" # D-043 — any bind is allowed (no setup-mode refusal) + base_url: "https://myday.sechpoint.app" # the PUBLIC origin: invite / recovery links + trusted_proxies: # XFF + X-Forwarded-Proto are honoured from these (A6/D-043) + - "127.0.0.1" # Zoraxy's address (127.0.0.1 if it runs on this host) # ── app ────────────────────────────────────────────────────────────────────── app: