Identity
Operations

Identity

Passkey sign-in, proxy delegation, and gating reads on named users.

Runbooks can require named users, so that reads are gated and writes are attributed to a person. It is off by default: with no identity database configured the app is public and the sync API is gated by GITSYNC_API_TOKEN.

Sign-in is by passkey (WebAuthn): no password, no shared secret, no email. Registration is invite-only, and the first admin is created with a bootstrap token.

Enabling

VarMeaningDefault
IDENTITY_DB_DRIVERsqlite, mysql or postgres(unset, identity off)
IDENTITY_DB_DSNDSN / SQLite file pathfile:./data/runbooks.db (sqlite only)
IDENTITY_PUBLIC_URLScheme + host; derives the WebAuthn RP ID and originrequired when on
IDENTITY_BOOTSTRAP_TOKENGuards /setup for the first admingenerated and logged once
IDENTITY_RECOVERY_TOKENBreak-glass re-enrolment for the last admin(unset, /recovery disabled)
IDENTITY_SESSION_TTL / IDENTITY_SESSION_IDLEAbsolute / idle session lifetime720h / 168h
IDENTITY_SECURE_COOKIESCookie Secure flagtrue

IDENTITY_PUBLIC_URL is effectively immutable: changing the host after passkeys exist invalidates every credential.

Open /setup and present the bootstrap token to enrol the first admin; from there, /admin issues single-use invite links (/invite/<token>). With identity on, every page (including each runbook) redirects to /login when unauthenticated.

Delegating to an upstream proxy

Instead of passkeys, the app can trust an identity asserted by a reverse proxy or SSO gateway in front of it:

VarMeaningDefault
IDENTITY_TRUST_PROXY_AUTHTrust the upstream identity headerfalse
IDENTITY_PROXY_USER_HEADERHeader carrying the login/emailAuth-Request-Email
IDENTITY_PROXY_NAME_HEADERHeader carrying the display name(unset)

On first sight the asserted identity is provisioned as a member; admin is never granted from a header. A request is resolved from the session cookie first, then the header, so a proxied request is authenticated without a login.

Only enable this when the instance is unreachable except through the proxy, which must set and strip the header. A directly reachable instance is an impersonation hole: anyone who can reach it can set the header themselves.

Tailscale Serve

tailscale serve injects Tailscale-User-Login and Tailscale-User-Name on requests it proxies, so a Tailnet-fronted instance maps directly onto proxy delegation:

IDENTITY_DB_DRIVER=sqlite \
IDENTITY_DB_DSN=file:./data/runbooks.db \
IDENTITY_PUBLIC_URL=https://runbooks.example.ts.net \
IDENTITY_TRUST_PROXY_AUTH=true \
IDENTITY_PROXY_USER_HEADER=Tailscale-User-Login \
IDENTITY_PROXY_NAME_HEADER=Tailscale-User-Name \
  ./runbooks

with the app listening on loopback and tailscale serve in front of it. Caveats:

  • Serve omits the identity headers for tagged devices and for Funnel traffic, so those requests are anonymous.
  • Non-ASCII values may be RFC 2047 "Q"-encoded (=?utf-8?q?...?=).
  • Behind a Kubernetes L7 operator Ingress, the proxy forwards to a cluster-reachable Service rather than localhost, so the equivalent of "loopback only" is a NetworkPolicy admitting only the ingress proxy pods to the app port.

Attribution

With identity on, /api/git-sync/v1 requires an authenticated session (or proxy assertion), and the commit author is the signed-in user: name and email from the user record. GITSYNC_API_TOKEN remains the non-human automation fallback for CI and scripts. Runbook acknowledgements are recorded against the authenticated user.