Files
preauth/SECURITY.md
T
lyra 538bd74100
PHP Test / test (pull_request) Successful in 52s
Tests / test (pull_request) Successful in 52s
docker to be more inline with other projects and best practices
2026-09-25 08:25:55 -04:00

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 / InterceptListener decide, 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, and Strict-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=false returns 429 rather than 418).
  • REMOTE_USER is trusted input, not a secret. In remote_user modes 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.
  • .env is never committed; secrets are env vars injected at runtime. Real secrets belong in .env.local or bin/console secrets:set, read via %env(...)%. .env.example and .env.test are 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.