No description
Find a file
Claus Lohmar f0927fb0f0 feat(server): gate /setup by the direct tunnel and trust the proxy scheme
Implements D-025 (revised 2026-10-15) and D-043.

Setup gate (D-025):
- Remove the bind refusal (CheckSetupBind/ErrNonLoopbackSetup/server.IsLoopback);
  Serve binds any address, so the app can listen on 0.0.0.0:8080 behind Zoraxy.
- The gate is now per request and strict (httpx.SetupRequestAllowed): a loopback
  TCP peer AND no X-Forwarded-For — the direct SSH tunnel. Anything else is 404
  before dispatch; an unparseable peer fails closed (AC-7/AC-10). The raw XFF is
  read before ClientIP SETS it (A6).

Proxy scheme (D-043):
- httpx.RequestIsHTTPS resolves the scheme per request: direct TLS, or a trusted
  proxy's X-Forwarded-Proto. The door carries it on the request context
  (httpx.WithSecure) so the session cookie, its __Host- prefix, the pre-session
  and ceremony cookies, and HSTS all agree (AC-13).
- Over plain http (the tunnel) cookies stay bare and non-Secure, which fixes the
  bootstrap from an https:// config.example.yaml; Config.IsSecure() is removed
  as now dead.

Tests: rewrite the setup-gate tests for AC-7/AC-10, add AC-13, and unit-test
SetupRequestAllowed / RequestIsHTTPS.
2026-10-15 05:40:36 +00:00
.opencode chore: bootstrap MyDay repository 2026-10-13 11:52:30 +00:00
cmd/myday feat(server): gate /setup by the direct tunnel and trust the proxy scheme 2026-10-15 05:40:36 +00:00
docs docs(spec): revise D-025 to a tunnel-only setup gate and add D-043 2026-10-15 05:40:30 +00:00
internal feat(server): gate /setup by the direct tunnel and trust the proxy scheme 2026-10-15 05:40:36 +00:00
.gitignore chore(gitignore): ignore only the root build binary, not the cmd/myday source 2026-10-14 10:53:46 +00:00
AGENT.md docs: add the project plan, M0/M0.1 specs, agent guide and README 2026-10-14 10:53:44 +00:00
config.example.yaml refactor(config): drop the dead secrets.session_key 2026-10-14 11:30:06 +00:00
go.mod chore(go): add the Go module manifest 2026-10-14 10:53:46 +00:00
go.sum chore(go): add the Go module manifest 2026-10-14 10:53:46 +00:00
PROJECT_PLAN.md feat(server): gate /setup by the direct tunnel and trust the proxy scheme 2026-10-15 05:40:36 +00:00
README.md docs: record the M0 review fixes and correct the README 2026-10-14 11:30:17 +00:00

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 loopback-only, running a /setup wizard until that admin exists.

# 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
# 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.

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.

Only after the top admin exists should you put MyDay behind a reverse proxy and expose it.


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
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.