The real topology is an LXC container (11020) behind Proxmox + Zoraxy, so the SSH tunnel is impractical: it lands on the container gateway, not loopback. D-025 is re-revised and D-044 adds the general gate. - core/setup: a Gate (gate.go) holding the 128-bit setup code (generated at first run, or pinned by config `setup_code`) plus a signed unlock cookie (`myday_setup`) that carries a keyed one-way FINGERPRINT of the code, never the code itself. The code is single-use (retired on the first success) and the comparison ignores case/separators. - The unlock action (`POST /setup?do=setup&action=unlock`) is CSRF-protected (A15), rate-limited (the /setup A3 class) and audited; a wrong or absent code gets the same generic 403 page. - The door (server.Deps.SetupGate): in setup mode the wizard is served to EITHER the direct tunnel (gate a) OR an unlocked request (gate b); `GET /` and an un-unlocked `GET /setup` render a friendly first-run page with the code form — never a bare 404 — and every other route stays 404. - cmd/myday prints an unmistakable startup banner: setup mode, the wizard URL, the code, where to read it (journalctl / pct exec 11020) and the restart that adopts the persisted secrets. - httpx.Signer.Fingerprint backs the unlock cookie; config gains `setup_code`. Tests cover the boilerplate at `/` and `/setup` (200, not 404), the unlock (code → wizard; wrong/absent → generic, 429 under the limit), single-use, the CSRF requirement, access-log redaction, and that a non-/setup route is still 404. Gate green (build · vet · test -race); live-smoked against the binary. |
||
|---|---|---|
| .opencode | ||
| cmd/myday | ||
| deploy | ||
| docs | ||
| internal | ||
| .gitignore | ||
| AGENT.md | ||
| config.example.yaml | ||
| go.mod | ||
| go.sum | ||
| PROJECT_PLAN.md | ||
| README.md | ||
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.
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
# 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.
# 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-Forheader. 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 addsX-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 athttp://localhost:8080orhttp://127.0.0.1:8080(via the tunnel above) — never at a bare server hostname. Over plain http the cookies are sent withoutSecure(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)
Full walkthrough:
docs/deploy-zoraxy.md— topology · config · the Zoraxy ProxyHost · the firewall rule · the tunnel-only bootstrap · systemd · the misconfig warnings. This section is the summary.
The reference deployment terminates TLS at Zoraxy at
10.1.0.10; 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 terminates TLS and forwards to
http://<app-host>:8080, settingX-Forwarded-ForandX-Forwarded-Proto. trusted_proxies—["10.1.0.10"](Zoraxy). The app honoursX-Forwarded-Protofrom a trusted proxy to decide a request ishttps, and then sets the session cookieSecure+__Host-and sends HSTS. Over plain http (the tunnel) it keeps the bare, non-Securecookie so the bootstrap works. A wrong value fails silently — noSecurecookie, no HSTS.base_url—https://myday.sechpoint.app: the public origin, used in invite/recovery links.- Firewall —
:8080MUST be reachable only from10.1.0.10. Anything else that can reach the port bypasses the proxy and its TLS; the app would then serve plain HTTP to that client.
# 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 |
the plan & decision log of record |
AGENT.md |
instructions for AI agents working here |
docs/specs/ |
implementation specs |
docs/deploy-zoraxy.md |
the Zoraxy deployment guide (D-043) |
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.