NextMyDay/README.md
Claus Lohmar 9a9888fa7b 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).
2026-10-15 05:40:38 +00:00

129 lines
5.3 KiB
Markdown

# MyDay
> A self-hosted employee **time, absence & reporting** app by **Sechpoint**.
> Delivered as a **PWA** for mobile and desktop. **Single-tenant** — one company per instance.
**Status:** 🚧 **M0 + M0.1 implemented** — the auth & routing foundation and the web/PWA layer
(the SPEC-002…009 slices) are built: routing · identity/sessions · the door · `/setup` ·
enroll/login (+ the passkey nudge) · roles/chain/recovery · the module registry · the web shell.
Next milestone: **M1 — org structure**. The design of record is
[`PROJECT_PLAN.md`](PROJECT_PLAN.md).
---
## What it will do
| Area | |
|---|---|
| **Org structure** | users, the manager hierarchy, departments, positions |
| **Time** | presence and timesheets (a per-user mode) |
| **Absence** | leave requests, approvals, balances |
| **Reporting** | rollups and aggregations |
Built as **one Go binary**: a small core (`MyDay.auth` — identity, sessions, the door) plus
drop-in **modules**.
---
## Running it
```bash
# 1. configure
cp config.example.yaml config.yaml # then edit; config.yaml is gitignored
# 2. build
go build -o myday ./cmd/myday
# 3. run
./myday
```
---
## ⚠️ First run — do NOT expose it publicly
The **first user becomes the top admin** (the root of the chain). So the instance bootstraps
through a **`/setup` wizard** that is reachable **only over a direct SSH tunnel**, until that admin
exists.
```bash
# 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
```
> **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.** 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)
next to the loaded file — never over the committed `config.example.yaml`. If the file is not
writable it **prints** the values for you to save by hand, and setup still completes.
The **secrets are generated at startup** when a value is still `CHANGE_ME…`, so the wizard's
ceremony is never signed with a known key. The persisted file then holds **those same values** —
so **a restart adopts them** and the admin's TOTP secret (sealed with the generated key) keeps
working. Boot a productive instance from the **written `config.yaml`**, not from the example:
`config.example.yaml`'s placeholders fail closed in productive mode by design.
---
## Documentation
| | |
|---|---|
| [`PROJECT_PLAN.md`](PROJECT_PLAN.md) | the plan & decision log of record |
| [`AGENT.md`](AGENT.md) | instructions for AI agents working here |
| [`docs/specs/`](docs/specs/) | implementation specs |
| [`config.example.yaml`](config.example.yaml) | configuration template |
---
## Security
- Secrets live in **`config.yaml`** (gitignored) — **never commit them**.
- Auth is **Passkey (primary) + TOTP (fallback)**.
- See `PROJECT_PLAN.md` §10 for the deferred-security register and its **beta gate**.
---
## License
TBD.