NextMyDay/docs/deploy-zoraxy.md
Claus Lohmar cec7340c55 docs: document the setup-code first run for the LXC/Zoraxy topology
- PROJECT_PLAN: register D-044, re-revise D-025 to "tunnel OR setup code",
  update A13, O-8, §5.1, §12 and the change log.
- SPEC-003 §4/§7/§9/§12: the gate now serves the wizard to the tunnel OR an
  unlocked request; AC-7/AC-10 reworded ("refused the wizard" — the boilerplate
  may still answer `/` and `/setup`), AC-14 added.
- SPEC-005 §3/§4/§6/§9: the boilerplate, the unlock action, the code rules and
  the ACs; AC-7 added for single-use/rate-limit/redaction.
- deploy-zoraxy §1/§3/§5/§7 + README: the LXC/Proxmox topology, how to read the
  code (journalctl · pct exec 11020), the code gate replacing the tunnel-only
  bootstrap (the tunnel stays as gate a); firewall and Zoraxy sections kept.
2026-10-15 06:33:16 +00:00

11 KiB

Deploying MyDay behind Zoraxy

Status: the reference deployment for D-043 / D-044. The app is never the TLS endpoint; Zoraxy terminates TLS and relays plain HTTP to the app. This document is operator-facing — it changes no code. The decision of record is PROJECT_PLAN.md D-043 / D-025 (re-revised 2026-10-15b); the gate's behaviour is docs/specs/spec-003-core-door.md §7 and docs/specs/spec-005-setup-wizard.md §3/§4.


1. Topology

browser ──https──▶ Zoraxy  10.1.0.10  ──http──▶ app  0.0.0.0:8080
        (TLS public)   terminates TLS              (plain HTTP, trusted proxy)
                       sets X-Forwarded-For                │
                       + X-Forwarded-Proto                  │  runs inside LXC
                                                            ▼  container 11020 (Proxmox)
  • Zoraxy — 10.1.0.10, holds the certificate for myday.sechpoint.app, terminates TLS, and forwards to the app over plain HTTP.
  • App — listens on 0.0.0.0:8080 over HTTP (server.listen). Because the app is not the TLS endpoint it must honour the proxy's X-Forwarded-Proto to know a request arrived over https (that is what turns on the Secure + __Host- session cookie and HSTS).
  • Container — MyDay runs in an LXC container (id 11020) under Proxmox. This is why the first-run gate is a setup code and not just an SSH tunnel: SSH/pct enter lands on the container gateway, not loopback, so a tunnel to 127.0.0.1:8080 cannot be formed (§5).
  • Firewall — :8080 is reachable only from 10.1.0.10 (see §4). The port is the app's whole attack surface: anything that can reach it bypasses both Zoraxy and its TLS.

2. The app config (config.yaml)

Copy config.example.yaml → config.yaml (gitignored — never commit it, D-010) and set the three server keys:

server:
  listen: "0.0.0.0:8080"
  base_url: "https://myday.sechpoint.app"   # the PUBLIC origin (invite / recovery links)
  trusted_proxies:
    - "10.1.0.10"                            # Zoraxy
  • listen — any bind is allowed (there is no setup-mode bind refusal, D-025 revised). Bind 0.0.0.0:8080; the firewall narrows who can reach it.
  • base_url — the public origin. It is the host the browser actually visits, so it must be https:// (WebAuthn derives its Relying Party ID from it).
  • trusted_proxies — the peers whose X-Forwarded-For / X-Forwarded-Proto are honoured (A6/D-043).

It must be the address Zoraxy connects from, not the public hostname. trusted_proxies matches the TCP peer of the app's connection. With Zoraxy on 10.1.0.10, that peer is 10.1.0.10. (If Zoraxy ran on the same host you would use 127.0.0.1 instead — that is the only case where loopback is right.)


3. The Zoraxy ProxyHost

Create a Proxy Host (reverse proxy rule) in Zoraxy:

Field Value
Domain / hostname myday.sechpoint.app
TLS on (Zoraxy's certificate for that host)
Upstream / target http://<app-host>:8080 (plain HTTP)
X-Forwarded-For set / forwarded
X-Forwarded-Proto set / forwarded (https)

Zoraxy sets X-Forwarded-For on a normal HTTP proxy rule; confirm it and X-Forwarded-Proto in the rule's header settings, then verify end-to-end (§7). The app deliberately requires both:

  • X-Forwarded-Proto: https is what makes the session cookie Secure (__Host-myday_session) and adds HSTS (AC-13).
  • X-Forwarded-For is what keeps the setup gate's tunnel test (gate a, §5) honest: a request with no X-Forwarded-For from a loopback peer is the direct tunnel, not a proxy relay.

4. Firewall — lock :8080 to Zoraxy

Allow :8080/tcp only from 10.1.0.10.

ufw:

ufw allow from 10.1.0.10 to any port 8080 proto tcp
# the default incoming policy (deny) drops every other source

nftables:

table inet myday_guard {
  chain input {
    type filter hook input priority filter; policy accept;
    tcp dport 8080 ip saddr != 10.1.0.10 drop
  }
}

If you run a host firewall, this rule is the load-bearing one: without it the app's plain-HTTP port is directly reachable and its TLS hardening (cookie Secure, HSTS, the tunnel gate) is bypassed.


5. First-run bootstrap — the setup code (no tunnel needed)

The first user becomes the top admin, so the initial /setup wizard is gated per request — never first-visitor-wins (D-025 · D-044). There are two ways in:

Gate Who Needs a code?
(a) the direct SSH tunnel — a loopback TCP peer with no X-Forwarded-For a workstation that can tunnel to the app's loopback port no
(b) the printed SETUP CODE anyone who can read the server log, over the public domain yes

The setup code is the general gate for this topology. MyDay generates a 128-bit code at first start and prints it in an unmistakable startup banner:

======================================================================
  MyDay is NOT set up yet — no admin exists.
======================================================================
  Open the setup wizard at:
      https://myday.sechpoint.app/setup
      SETUP CODE:  9f3a-1b2c-4d5e-6f70-8192-a3b4-c5d6-e7f8
  Read the code again:
      journalctl -u myday                             (in the container)
      pct exec 11020 -- journalctl -u myday    (from the Proxmox host)

Read the code (in the LXC container, or from Proxmox):

# inside the container
journalctl -u myday | grep -i 'setup code'

# from the Proxmox host (container 11020)
pct exec 11020 -- journalctl -u myday | grep -i 'setup code'

Then open https://myday.sechpoint.app/setup and enter the code. Until the code is entered:

  • GET / and GET /setup show a friendly first-run page (with the code form) — never a bare 404, so the domain does not look "down";
  • every other route answers 404;
  • the code is single-use and the entry form is rate-limited (wrong code → a generic 403).

No tunnel required. The old guidance ("bootstrap over an SSH tunnel first") no longer applies: in LXC a tunnel to 127.0.0.1:8080 cannot be formed (it lands on the container gateway), which is exactly why D-044 replaced it with the code. The tunnel gate (a) still works and needs no code, if you have a route to the app's loopback port.

The wizard creates the top admin (TOTP; the passkey is added at first sign-in on the real origin — D-041) and writes config.yaml (setup: false, the base URL, mail/locale, and the secrets).

The restart — adopting the persisted secrets

The secrets are generated at startup while any value is still CHANGE_ME… (so the wizard's ceremony is never signed with a shipped, public key). The wizard then persists those same values into config.yaml. So once setup is complete:

sudo systemctl restart myday

The restart adopts the written config.yaml — the generated keys are now on disk, so the admin's TOTP secret (sealed with that key) keeps working, and the setup code is gone (GET /setup → 404). Boot the productive instance from the written config.yaml, never from config.example.yaml (its placeholders fail closed in productive mode by design).

Pinning your own code. Set setup_code: in config.yaml to override the generated one (then protect the file). Leave it blank to have MyDay generate and print a fresh code each boot.


6. systemd

Build, install the unit, and start it. The unit is deploy/myday.service — replace every <PLACEHOLDER>:

# 1. build the static, CGO-free binary
go build -trimpath -o /usr/local/bin/myday ./cmd/myday

# 2. the app dir holds config.yaml and data/ (SQLite)
install -d -o <myday-user> -g <myday-group> /opt/myday
install -m 0640 -o <myday-user> -g <myday-group> config.yaml /opt/myday/config.yaml

# 3. the unit
install -m 0644 deploy/myday.service /etc/systemd/system/myday.service
systemctl daemon-reload
systemctl enable --now myday
journalctl -u myday -f

The unit sets WorkingDirectory= to the app dir (where config.yaml and data/ live), User=, ExecStart=<binary>, and After=network-online.target; see the file's header comment for the placeholders.


7. Verify, and the misconfig warnings

# from anywhere — the public origin
curl -sI https://myday.sechpoint.app/ | grep -i '^strict-transport-security'
# expect: Strict-Transport-Security: max-age=…   (HSTS — proves X-Forwarded-Proto was honoured)
Symptom Cause Fix
Session cookie has no Secure, no __Host- prefix, and no HSTS trusted_proxies is empty or wrong, so X-Forwarded-Proto is ignored (fails silently) Set server.trusted_proxies: ["10.1.0.10"] and restart
Client IP in the audit log is the proxy, not the user same — X-Forwarded-For is not trusted as above
GET / and GET /setup show a first-run page with a code box by design — MyDay is not set up yet (D-044) read the setup code (§5) and enter it; complete the wizard
Any other route returns 404 while bootstrapping by design — only / and /setup are served in setup mode (SPEC-005 §3) complete the wizard; the app serves normally once productive
The setup code is rejected (403) it is single-use (already spent) or mistyped restart MyDay to print a fresh code, or pin setup_code: in config.yaml (§5)
/setup returns 404 after the wizard by design — setup is complete re-enter only via setup: true + restart

The load-bearing deployment requirement: Zoraxy must set X-Forwarded-For. It is what distinguishes a proxy relay from the direct tunnel (gate a): a proxied request with no X-Forwarded-For would look like the tunnel and open the wizard without a code. (The setup code still closes first-visitor-wins for anything the proxy does mark.) X-Forwarded-Proto is what keeps the cookie Secure over the proxy.


See also

PROJECT_PLAN.md D-043 · D-025 (re-revised) · D-044
docs/specs/spec-003-core-door.md §7 the setup gate · §9 AC-7 / AC-10 / AC-13 / AC-14
docs/specs/spec-005-setup-wizard.md §3/§4 the bootstrap + the setup code
config.example.yaml the commented config template
deploy/myday.service the systemd unit