Files
preauth/readme.md
T
lyra 9ad54f8e2a
Sync GitHub / sync (push) Successful in 7s
Tests / test (pull_request) Successful in 58s
feat: configurable Remote-User header (design consideration 1.2)
Add REMOTE_USER env var with four modes:
- session (default): sends session id, backward-compatible
- static: sends a fixed string (REMOTE_USER_STATIC)
- mapped: looks up session id in REMOTE_USER_MAP
- none: omits the header entirely

New RemoteUserMode enum, ConfigBag parsing/validation, and
StringTrait::authSuccessResponse resolves the header value based
on the configured mode. AcceptListener now receives ConfigBag as
a constructor dependency.

Addresses design consideration 1.2 (Remote-User header value is
user-controlled) from DESIGN_CONSIDERATIONS.md.

241 tests pass, 0 cs-fixer violations.
2026-08-11 22:48:52 -04:00

236 lines
7.8 KiB
Markdown

# Preauth
A lightweight TOTP authentication gateway for self-hosted web services.
Preauth sits between your reverse proxy (Caddy) and your web service,
requiring a TOTP code before traffic ever reaches the protected application.
It is **not** a replacement for your service's own authentication — it's a
gate that prevents outsiders from even seeing what service is running.
For when you want a belt and suspenders.
## Features
- **TOTP authentication** — Time-based one-time passwords (compatible with
Google Authenticator, Authy, 1Password, etc.)
- **Backup codes** — Single-use backup codes for when TOTP devices are lost
- **Caddy native** — Designed for Caddy's `forward_auth` directive
- **Docker-first** — Single container, persistent volumes, no database
- **Rate limiting** — Per-IP burst and sustained limits (cannot be disabled)
- **Central auth** — Optional subdomain-based SSO across multiple services
- **IP-based bypass** — Optional, for services that don't handle cookies
- **Customizable** — Colors, labels, messages, and error text via env vars
- **Teapot mode** — Respond with `418 I'm a Teapot` when rate-limited
(because it's more fun than `429 Too Many Requests`)
- **Cookie security** — `__Host-` prefixed cookies with `SameSite=Strict`,
`Secure`, and `HttpOnly`
- **Nonce system** — Single-use nonces prevent replay and CSRF attacks
- **Dual-layer cache** — APCu for speed, file-based persistence for restarts
## Quick Start
### 1. Pull the Docker image
```bash
docker pull digitaladapt/preauth:latest
```
### 2. Create your environment file
```bash
# Generate a TOTP secret to get started
openssl rand -base64 30
```
Create a `.env` file (see `docs/example.env` for all options):
```env
APP_SECRET=your-random-secret-here
TOTP_URI=otpauth://totp/Preauth?secret=YOUR_SECRET
COOKIE_TTL=2592000
```
> If `TOTP_URI` is left blank, the app will generate one on first run
> and print it to the container logs. Copy it to your `.env` file.
### 3. Start the container
```bash
docker compose up -d
```
See `docs/compose.yaml` for an example Docker Compose file.
### 4. Configure Caddy
```caddyfile
service.example.com {
forward_auth preauth {
uri {uri}
copy_headers Remote-User
}
reverse_proxy your-service:80
}
```
See `docs/Caddyfile` for more examples, including path-specific protection
and central auth subdomain configuration.
### 5. Generate backup codes (optional)
```bash
docker exec -t preauth bin/console app:generate-backup-codes [count=10]
```
## Requirements
- **Docker** — Preauth runs as a Docker container
- **Caddy** — As your reverse proxy (uses `forward_auth` directive)
- **A web service** — The application you want to protect
Other reverse proxies with similar `forward_auth` / `auth_request`
capabilities may work, but only Caddy is officially supported.
## Configuration
All configuration is via environment variables. See `docs/example.env`
for the complete reference.
### Main Options
| Variable | Default | Description |
|----------|---------|-------------|
| `TOTP_URI` | _(empty)_ | TOTP provisioning URI. If blank, one is generated on first run. |
| `COOKIE_TTL` | `2592000` | Session duration in seconds (default: 30 days). |
| `SUBDOMAIN_REDIRECT` | `0` | Enable central auth across subdomains (boolean). |
| `AUTH_SUBDOMAIN` | _(empty)_ | Hostname for central auth (e.g., `auth.example.com`). |
### Extra Options
| Variable | Default | Description |
|----------|---------|-------------|
| `IP_TTL` | `0` | Seconds to allow all traffic from an IP after login (0 = disabled). |
| `TEAPOT` | `1` | Respond with 418 instead of 429 when rate-limited (boolean). |
### Remote-User Header
The `Remote-User` header sent to backends on successful auth is configurable:
| Variable | Default | Description |
|----------|---------|-------------|
| `REMOTE_USER` | `session` | Mode: `session`, `static`, `mapped`, or `none`. |
| `REMOTE_USER_STATIC` | `authenticated` | Value sent when mode is `static`. |
| `REMOTE_USER_MAP` | _(empty)_ | Comma-separated map for `mapped` mode (e.g. `alice:admin,bob:user`). |
- **`session`** (default): Sends the session id. Backward-compatible.
- **`static`**: Sends a fixed string for all authenticated requests.
- **`mapped`**: Looks up the session id in the map; falls back to session id if not found.
- **`none`**: Omits the header entirely (Caddy still accepts based on status code).
### Rate Limiting
Rate limiting **cannot be disabled**. It uses a compound sliding window:
| Variable | Default | Description |
|----------|---------|-------------|
| `BURST_COUNT` | `2` | Max attempts per burst window. |
| `BURST_TIME` | `30` | Burst window in seconds. |
| `UPPER_COUNT` | `10` | Max attempts per upper window. |
| `UPPER_TIME` | `3600` | Upper window in seconds (1 hour). |
### Styling
All UI text and colors are configurable:
| Variable | Default | Description |
|----------|---------|-------------|
| `TITLE` | `Pre-Authentication System` | Page title. |
| `BG_COLOR` | `#029386` | Background color. |
| `FG_COLOR` | `#ffffff` | Foreground (text) color. |
| `ERROR_COLOR` | `#ffb16d` | Error message color. |
| `ID_NAME` | `Session ID` | Label for the ID field. |
| `TOKEN_NAME` | `Authentication Token` | Label for the TOTP field. |
| `SUBMIT_NAME` | `Submit` | Submit button text. |
| `ERROR_MESSAGE` | `Unsuccessful login attempt` | Failed login message. |
| `TEAPOT_TITLE` | `I'm a teapot` | Title when rate-limited (teapot mode). |
| `TEAPOT_MESSAGE` | `I refuse to brew coffee` | Message when rate-limited (teapot mode). |
| `TOO_MANY_TITLE` | `Too many requests` | Title when rate-limited (non-teapot). |
| `TOO_MANY_MESSAGE` | `Try again later` | Message when rate-limited (non-teapot). |
## Architecture
```
Client → Caddy → forward_auth → Preauth listeners → 200/401/418
```
Preauth is entirely event-listener-driven (no controllers). Each request
passes through a priority-ordered chain of listeners:
1. **AcceptListener** (priority 99) — Checks for valid session cookie.
2. **AllowListener** (priority 88) — Checks for valid IP-based session.
3. **RejectListener** (priority 77) — Rate-limiting gate.
4. **LoginListener** (priority 66) — Processes login attempts.
5. **InterceptListener** (priority 55) — Renders login page or redirects.
6. **SecurityHeadersListener** (response) — Adds security headers.
### Security Model
- **Cookies**: `__Host-` prefixed, `SameSite=Strict`, `Secure`, `HttpOnly`
- **Nonces**: 15-byte random, single-use, 120-second TTL
- **TOTP**: ±1 period leeway (±30 seconds) for clock drift
- **Backup codes**: Case-insensitive, single-use, alphanumeric
- **Rate limiting**: Per-IP, compound sliding window, cannot be disabled
- **Security headers**: CSP, X-Frame-Options, X-Content-Type-Options,
Referrer-Policy, HSTS
### Cache
Preauth uses a dual-layer cache:
- **APCu** (in-memory) — Fast session and nonce lookups
- **Filesystem** — Persistent storage for container restarts
`MonitorCacheKeys` wraps the PSR-6 cache pool to track changes, so only
modified items are persisted to disk on shutdown.
## Development
### Code Style
This project follows [PSR-12](https://www.php-fig.org/psr/psr-12/) and
includes `php-cs-fixer` as a dev dependency.
```bash
# Check for style violations
vendor/bin/php-cs-fixer fix --dry-run --diff
# Auto-fix
vendor/bin/php-cs-fixer fix
```
### Running Tests
```bash
vendor/bin/phpunit
```
The test suite includes 222 tests with 100% code coverage (lines, methods,
and classes). Both unit tests and functional tests (full HTTP kernel flow)
are included.
### Requirements
- PHP 8.4+
- Composer
- Xdebug (for coverage reports)
## License
MIT — see `license.txt`.
## Project Status
Running in production since June 2024, protecting multiple self-hosted
services. The core authentication gate is complete and battle-tested.
See `ROADMAP.md` for planned features and `CHANGELOG.md` for version history.