Files
preauth/CHANGELOG.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

12 KiB
Raw Blame History

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased] — v1.1

Added

  • Public rate-limited access — Select paths can now be made publicly accessible without TOTP authentication, with separate per-IP rate limiting. This is useful for exposing public content (e.g., public Gitea repositories) while protecting server resources from bot traffic.
    • New PUBLIC_PATHS env var: comma-separated path patterns with * (single segment) and ** (cross-segment) wildcard support. Optional host prefix (e.g., code.example.com/public/**). When empty (default), the feature is fully disabled.
    • New PUBLIC_BURST_COUNT / PUBLIC_BURST_TIME env vars for burst rate limiting (default: 100 requests per 60 seconds).
    • New PUBLIC_UPPER_COUNT / PUBLIC_UPPER_TIME env vars for sustained rate limiting (default: 500 requests per 3600 seconds).
    • Authenticated users bypass the public rate limiter entirely.
    • Over-limit responses include a Retry-After header.
    • New PublicPathMatcher service for path pattern matching.
    • New PublicAccessListener (priority 84) in the request pipeline.

Changed

  • Upgraded Symfony 7.4 → 8.1 — All symfony/* components bumped to 8.1.* (resolved to 8.1.2–8.1.6). The 7.4 deprecation sweep was clean (test suite runs with failOnDeprecation), so the major-version jump required no application code changes. See docs/symfony-8.1-upgrade-plan.md.

Removed

  • runtime/frankenphp-symfony — No longer needed: symfony/runtime 8.1 handles FrankenPHP worker mode natively via its built-in FrankenPhpWorkerRunner. The extra.runtime override in composer.json was removed so the runtime auto-detects FrankenPHP. The old package's FRANKENPHP_LOOP_MAX env var is no longer read; an equivalent recycle limit is restored via the new MAX_REQUESTS setting below.

Added

  • MAX_REQUESTS worker-thread recycle limit — The Caddyfile now sets FrankenPHP's native max_requests from the MAX_REQUESTS environment variable: each PHP worker thread is gracefully restarted after N requests while others keep serving, containing slow memory growth across long uptime. The image default is 500 (matching the previous runtime/frankenphp-symfony default), baked in as a Docker build arg and overridable at runtime (MAX_REQUESTS=0 disables restarts). Arbitrary frankenphp-block configuration is still possible via the stock FRANKENPHP_CONFIG env var.

Fixed

  • Login flow responses are no longer cacheable — the login page, failed logins, redirects, and rate-limit/error pages now send strict anti-caching headers (Cache-Control: no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0, s-maxage=0 plus Pragma, Expires, Surrogate-Control, and Vary: *), the login form's fetch() bypasses the HTTP cache, and the example Caddyfile guards every forward_auth block with matching header_down rules. This prevents browsers — notably older Safari — from replaying a stale pre-auth response on refresh (previously: log in successfully, refresh, and land back on the login page). Successful (2xx) responses are deliberately excluded: they are consumed by the proxy's forward_auth check and never reach the browser.

Changed

  • Dockerfile rebuild — same layout as the rest of the portfolio (Guiding Light §6.4). The build now copies the tree (COPY . .) and lets .dockerignore decide what reaches the context, instead of maintaining a hand-written COPY ./x /app/x allowlist that had to be kept in step with the project layout. var/ — which the old file list never copied — now simply stays out via the ignore file.
  • The image runs as a non-root app user (uid/gid 1000, the same convention as task-loom/context-shuttle). /data (cache pools) and /config are created and owned by it. This resolves the last failing conformance check (§6.4 dockerfile-nonroot).
  • .dockerignore rebuilt on the Guiding Light §6.2 baseline — in particular .env is now excluded explicitly (§6.1), so a developer's local environment file can never be baked into a layer.
  • docker/php.ini and docker/Caddyfile added. The PHP overrides (expose_php=Off, error/log settings, OPcache timestamps off, APCu for CLI) and the FrankenPHP app config now live in the repository instead of being three heredocs inside the Dockerfile, so what the image runs is reviewable in a diff.
  • Runtime base image pinned to dunglas/frankenphp:1-php8.5-trixie and APCu installed via the base image's install-php-extensions — the versioned tag replaces the floating one, and the build no longer drags a compiler toolchain into the runtime layer to build one extension.
  • /app is now the whole project. The old image only shipped bin/console, config, public, src, templates and the composer manifests; config/reference.php and other loose files are now present. No application path changes: public/index.php and bin/console resolve through the same relative paths.
  • bin/franken.sh mounts the share dir at its new default (/app/var/share) instead of the old /app/var/share bind that no longer matched the image.

Fixed

  • composer dump-env prod --empty removed. preauth does not depend on symfony/dotenv (it is not in composer.lock), so nothing reads a .env file in the container — the command only produced a dead .env.local.php in the build stage. The Dockerfile comment that claimed otherwise is gone with it.
  • composer install no longer ships a classmap missing App\. The old build ran install --optimize-autoloader before src/ was copied, and the final --classmap-authoritative dump happened before any COPY . .; the classmap is now rebuilt after the application is in place.
  • The HEALTHCHECK can actually pass. It probed curl -f http://localhost/, and preauth answers every unauthenticated request to / with the login page and a 401 — so the probe failed 100% of the time and the container was permanently marked unhealthy. It now probes Caddy's loopback admin endpoint (the base image's own default probe, restated explicitly), which is why the Caddyfile deliberately does not disable the admin API.
  • expose_php is now genuinely off in the runtime image. The base image ships the php.ini-production template but no active php.ini, so the previous cp of the template was the only thing setting it — and the docker/php.ini overrides are loaded after it, so stating it here makes the intent explicit; verified against a real boot that no X-Powered-By header is emitted.
  • bin/franken.sh no longer passes DEFAULT_URI, which the application does not read (config/packages/routing.yaml sets the router's default_uri).

[1.0.0] — v1.0 Release

Security

  • Made Remote-User header value configurable via REMOTE_USER environment variable with four modes: session (default), static, mapped, and none. This allows deployments to prevent user-controlled header values from reaching backend services.
  • Added SecurityHeadersListener to set X-Content-Type-Options, X-Frame-Options, Content-Security-Policy, Referrer-Policy, and Strict-Transport-Security headers on all responses.
  • Replaced document.write() with document.documentElement.innerHTML in login page JavaScript to avoid CSP violations.
  • Added CSS escaping (|e('css')) to environment-configured color values in the login page template to prevent CSS injection.
  • Documented CSRF protection model: the nonce system provides CSRF protection for POST form logins (server-generated, single-use, 120s TTL).
  • Reduced TOTP verification window from 10 periods (±5 minutes) to 1 period (±30 seconds) to reduce brute-force attack surface.
  • Removed hardcoded APP_SECRET from bin/franken.sh (now uses environment variable or generates a random secret).
  • Removed backup code values from debug log output.
  • Added .env to .gitignore.
  • Expanded TLD list in DomainManager with many missing multi-part TLDs (.com.au, .co.jp, .com.br, .co.kr, .com.tw, .co.za, etc.) to prevent open redirect vulnerabilities from incorrect domain matching.
  • Lowercased host before TLD lookup to fix case-sensitivity issue.

Fixed

  • Fixed $payload->json access on possibly-null $payload in LoginListener using null-safe operator (?->).
  • Fixed validReturn() not checking false return from parse_url(), which could cause a TypeError on malformed URLs.
  • Added isHit() race condition check in AcceptListener and AllowListener between hasItem() and getItem() calls.
  • Added try/finally in Kernel::terminate() so parent::terminate() always runs even if persist() throws an exception.
  • Added input validation to GenerateBackupCodesCommand — rejects count < 1.

Changed

  • Disabled unused Symfony sessions in framework.yaml (preauth implements its own cookie/cache-based session management).
  • Standardized git tag format to use v prefix (v1.0.0 instead of 1.0.0).
  • Updated CI workflows to use v*.*.* tag pattern and strip v prefix for Docker image tags.
  • Removed stale develop branch from CI triggers.
  • Fixed publish.yaml to use git remote set-url on re-runs instead of failing when the remote already exists.
  • Explicitly install curl in the Docker final image (needed for healthcheck).
  • Added declare(strict_types=1) to all interface files.
  • Added #[AsCommand] attribute to GenerateBackupCodesCommand.
  • Fixed BackupCodeInterface default count to match implementation (10).
  • Used Response::HTTP_INTERNAL_SERVER_ERROR constant in GetTotpTrait instead of literal 500.

[0.10.0] - 2026-08-11

Added

  • PHP-CS-Fixer with PSR-12 configuration and CI check.

[0.9.0] - 2026-07-15

Added

  • PHPUnit test suite — 222 tests, 100% code coverage (lines, methods, classes).

[0.8.1] - 2026-05-30

Fixed

  • Bug fixes and cleanup from develop branch merge.

[0.8.0] - 2026-05-29

Changed

  • Renamed form fields for clarity.
  • Fixed invalid login bug.

[0.7.0] - 2026-05-29

Added

  • Single-use backup codes via app:generate-backup-codes console command.
  • Cache persistence improvement — only write changed keys to file storage.

Removed

  • Static password and lookup token (security risks).

Changed

  • Updated to PHP 8.5, updated dependencies.

[0.6.0] - 2026-02-10

Added

  • Optional (disabled by default) ability to lookup token by static password.

[0.5.0] - 2026-01-17

Added

  • Optional (disabled by default) ability to use a static password as backup auth.

Changed

  • Nonce-related cleanup.

[0.4.1] - 2025-12-26

Fixed

  • Bug which can occur if cache files are deleted.

[0.4.0] - 2025-12-26

Changed

  • Massive rewrite to listener-based architecture instead of controllers.
  • Login payload sent via X-Preauth header instead of GET request parameters.
  • Enhanced cookie security.
  • Removed icon system and asset system.

[0.3.0] - 2025-12-15

Changed

  • Breaking: Default port and transport changed to HTTP on port 80.
  • Breaking: Environment variable names have changed.
  • Refactored to Symfony 7.4 with FrankenPHP.

[0.2.0] - 2025-12-03

Added

  • Login rate limiting (burst + upper window).
  • Error page for rate-limited clients ("too many requests").
  • Example Docker Compose file.

[0.1.0] - 2025-11-14

Added

  • Docker image published to Docker Hub.
  • PHP-FPM based, code in src/, templates in separate files.

[0.0.1] - 2024-06-26

Notes

  • Started as a single-file script in Caddy config. Hardcoded TOTP secret, zero flexibility, but functional. Ran quietly in production for about a year before any real development began.