3.5 KiB
Security Policy
Supported Versions
| Version | Supported |
|---|---|
| unreleased (v1 development) | ✅ |
Reporting a Vulnerability
Report vulnerabilities privately to security@digitaladapt.com (or open a private security advisory on the repository). Please include reproduction steps and affected versions. You will receive an acknowledgement within 48 hours and a status update at least weekly until resolution.
Do not open a public issue for a suspected vulnerability. preauth is an authentication gateway — it sits in front of every protected service, so a weakness here is a weakness everywhere behind it.
Security model summary
preauth implements the auth half of the forward_auth pattern: a reverse proxy
calls it per request to decide whether a request may reach the upstream service.
- Two outcomes per request: allow or intercept.
AcceptListener/RejectListener/InterceptListenerdecide, and the decision is made on every request rather than cached — a cached auth session is an anti-pattern (GUIDING-LIGHT §3.3d), which is also why this project gets no service worker. - The login flow is never cached. The login page, failed logins, redirects
and rate-limit responses are sent with
Cache-Control: no-cache, no-store, must-revalidate, proxy-revalidate, max-age=0, s-maxage=0. An aggressive cache (notably older Safari) replaying a stale pre-auth response presents to the user as being logged back out after a refresh. - Headers are set by the app, not left to the proxy.
X-Content-Type-Options: nosniff,X-Frame-Options: DENY, a Content-Security-Policy, andStrict-Transport-Security: max-age=31536000. Docs recommend mirroring the caching headers at the edge as defence in depth, but the app does not depend on it. - TOTP is required. Secrets come from
TOTP_URI; if it is unset the app generates one and prints it for enrolment. Login state is carried in a signed payload (src/Data/Payload.php) bound to a nonce and a scope, not in a server-side session store. - Rate limiting is on by default, with the block response configurable
(
TEAPOT=falsereturns 429 rather than 418). REMOTE_USERis trusted input, not a secret. Inremote_usermodes the gateway accepts an upstream-asserted identity, so the upstream must be the only path to the app. Do not expose preauth directly to the internet for this mode..envis never committed; secrets are env vars injected at runtime. Real secrets belong in.env.localorbin/console secrets:set, read via%env(...)%..env.exampleand.env.testare the committed env files.
Scope
In scope: the application code in src/, the shipped Caddyfile, the
Dockerfile, and anything that affects the allow/intercept decision.
Out of scope: the forward_auth integration at the edge (a host-proxy
configuration concern, see docs/examples/Caddyfile) and the security of the
services preauth protects.
Deployment note
preauth runs as a container and drops privileges via USER (Guiding Light
§6.4): the image runs as the non-root app user (uid/gid 1000) and owns the
state paths it needs. Only /data is written at runtime — the cache pools
behind sessions, backup codes and rate limiting — and /config is declared
because the base image points Caddy's XDG config dir there. If you pin a
different user: in your compose file, that user must be able to write to
both paths — otherwise login state and backup codes cannot be persisted.