12 KiB
12 KiB
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_PATHSenv 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_TIMEenv vars for burst rate limiting (default: 100 requests per 60 seconds). - New
PUBLIC_UPPER_COUNT/PUBLIC_UPPER_TIMEenv 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-Afterheader. - New
PublicPathMatcherservice for path pattern matching. - New
PublicAccessListener(priority 84) in the request pipeline.
- New
Changed
- Upgraded Symfony 7.4 → 8.1 — All
symfony/*components bumped to8.1.*(resolved to 8.1.2–8.1.6). The 7.4 deprecation sweep was clean (test suite runs withfailOnDeprecation), so the major-version jump required no application code changes. Seedocs/symfony-8.1-upgrade-plan.md.
Removed
runtime/frankenphp-symfony— No longer needed:symfony/runtime8.1 handles FrankenPHP worker mode natively via its built-inFrankenPhpWorkerRunner. Theextra.runtimeoverride incomposer.jsonwas removed so the runtime auto-detects FrankenPHP. The old package'sFRANKENPHP_LOOP_MAXenv var is no longer read; an equivalent recycle limit is restored via the newMAX_REQUESTSsetting below.
Added
MAX_REQUESTSworker-thread recycle limit — TheCaddyfilenow sets FrankenPHP's nativemax_requestsfrom theMAX_REQUESTSenvironment 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 previousruntime/frankenphp-symfonydefault), baked in as a Docker build arg and overridable at runtime (MAX_REQUESTS=0disables restarts). Arbitraryfrankenphp-block configuration is still possible via the stockFRANKENPHP_CONFIGenv 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=0plusPragma,Expires,Surrogate-Control, andVary: *), the login form'sfetch()bypasses the HTTP cache, and the example Caddyfile guards everyforward_authblock with matchingheader_downrules. 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'sforward_authcheck 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.dockerignoredecide what reaches the context, instead of maintaining a hand-writtenCOPY ./x /app/xallowlist 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
appuser (uid/gid 1000, the same convention as task-loom/context-shuttle)./data(cache pools) and/configare created and owned by it. This resolves the last failing conformance check (§6.4dockerfile-nonroot). .dockerignorerebuilt on the Guiding Light §6.2 baseline — in particular.envis now excluded explicitly (§6.1), so a developer's local environment file can never be baked into a layer.docker/php.inianddocker/Caddyfileadded. 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-trixieand APCu installed via the base image'sinstall-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. /appis now the whole project. The old image only shippedbin/console,config,public,src,templatesand the composer manifests;config/reference.phpand other loose files are now present. No application path changes:public/index.phpandbin/consoleresolve through the same relative paths.bin/franken.shmounts the share dir at its new default (/app/var/share) instead of the old/app/var/sharebind that no longer matched the image.
Fixed
composer dump-env prod --emptyremoved. preauth does not depend onsymfony/dotenv(it is not incomposer.lock), so nothing reads a.envfile in the container — the command only produced a dead.env.local.phpin the build stage. The Dockerfile comment that claimed otherwise is gone with it.composer installno longer ships a classmap missingApp\. The old build raninstall --optimize-autoloaderbeforesrc/was copied, and the final--classmap-authoritativedump happened before anyCOPY . .; 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 a401— 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_phpis now genuinely off in the runtime image. The base image ships thephp.ini-productiontemplate but no activephp.ini, so the previouscpof the template was the only thing setting it — and thedocker/php.inioverrides are loaded after it, so stating it here makes the intent explicit; verified against a real boot that noX-Powered-Byheader is emitted.bin/franken.shno longer passesDEFAULT_URI, which the application does not read (config/packages/routing.yamlsets the router'sdefault_uri).
[1.0.0] — v1.0 Release
Security
- Made
Remote-Userheader value configurable viaREMOTE_USERenvironment variable with four modes:session(default),static,mapped, andnone. This allows deployments to prevent user-controlled header values from reaching backend services. - Added
SecurityHeadersListenerto setX-Content-Type-Options,X-Frame-Options,Content-Security-Policy,Referrer-Policy, andStrict-Transport-Securityheaders on all responses. - Replaced
document.write()withdocument.documentElement.innerHTMLin 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_SECRETfrombin/franken.sh(now uses environment variable or generates a random secret). - Removed backup code values from debug log output.
- Added
.envto.gitignore. - Expanded TLD list in
DomainManagerwith 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->jsonaccess on possibly-null$payloadinLoginListenerusing null-safe operator (?->). - Fixed
validReturn()not checkingfalsereturn fromparse_url(), which could cause aTypeErroron malformed URLs. - Added
isHit()race condition check inAcceptListenerandAllowListenerbetweenhasItem()andgetItem()calls. - Added
try/finallyinKernel::terminate()soparent::terminate()always runs even ifpersist()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
vprefix (v1.0.0instead of1.0.0). - Updated CI workflows to use
v*.*.*tag pattern and stripvprefix for Docker image tags. - Removed stale
developbranch from CI triggers. - Fixed
publish.yamlto usegit remote set-urlon re-runs instead of failing when the remote already exists. - Explicitly install
curlin the Docker final image (needed for healthcheck). - Added
declare(strict_types=1)to all interface files. - Added
#[AsCommand]attribute toGenerateBackupCodesCommand. - Fixed
BackupCodeInterfacedefault count to match implementation (10). - Used
Response::HTTP_INTERNAL_SERVER_ERRORconstant inGetTotpTraitinstead of literal500.
[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-codesconsole 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-Preauthheader 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.