clusev/bootstrap/app.php

88 lines
4.8 KiB
PHP

<?php
use App\Http\Middleware\BlockBannedIp;
use App\Http\Middleware\DetectHoneytoken;
use App\Http\Middleware\PanelScheme;
use App\Http\Middleware\SecurityHeaders;
use App\Http\Middleware\SetLocale;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
use Illuminate\Session\Middleware\AuthenticateSession;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
// Registers the `/broadcasting/auth` endpoint (under the `web` group, so PanelScheme
// guards it) and the private-channel authorization callbacks in routes/channels.php.
// Without this, private/presence channels can never be subscribed to. See R-note in
// routes/channels.php: every broadcast channel is PRIVATE (authenticated) — metrics are
// fleet infrastructure data and must not be reachable on a stale Caddy host via Reverb.
->withBroadcasting(__DIR__.'/../routes/channels.php')
->withMiddleware(function (Middleware $middleware): void {
// Trust Caddy's X-Forwarded-* ONLY in production, where the app has no host port
// (docker-compose.prod.yml exposes app:80 on the internal net only) so Caddy is
// the sole peer. In dev the app port is published on the host, so trusting forged
// proxy headers would let a LAN peer fake HTTPS (forcing a Secure cookie over
// plain HTTP and breaking the bare-IP recovery login).
//
// Trust only PRIVATE ranges, not '*': Caddy reaches app:80 from the docker bridge (a private
// IP), so it stays trusted, but anything arriving from a public source (a stray published
// port, an off-network relay) can no longer forge X-Forwarded-For to spoof request()->ip()
// and defeat the IP-keyed login/2FA/forgot throttles + the brute-force ban.
if (($_SERVER['APP_ENV'] ?? getenv('APP_ENV') ?: 'local') === 'production') {
$middleware->trustProxies(at: ['127.0.0.1/32', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']);
}
// PanelScheme first (scheme/host enforcement before auth), so an HTTP request to
// the active domain is sent straight to HTTPS and a wrong host is refused before
// any auth redirect. AuthenticateSession ties each request to the password hash that
// was current when the session started, so a password change (or a remember-token
// rotation in the session-management actions) invalidates every OTHER session — the
// database session driver makes those sessions enumerable/revocable in Settings.
$middleware->web(
prepend: [PanelScheme::class],
append: [SetLocale::class, SecurityHeaders::class, AuthenticateSession::class, BlockBannedIp::class, DetectHoneytoken::class],
);
// The terminal sidecar POSTs to the internal resolve endpoint from the private network with
// no browser session/CSRF token — it is guarded by the shared-secret header instead. The
// honeypot decoys are also exempt: a probe POSTing the fake login must be trapped + logged,
// not bounced with a 419 that unmasks the framework (these paths reach only HoneypotController).
$middleware->validateCsrfTokens(except: [
'_internal/terminal/*',
'.env', '.git/config',
'wp-login.php', 'wp-admin', 'wp-admin/*', 'xmlrpc.php',
'phpmyadmin', 'phpmyadmin/*', 'administrator', 'admin.php', 'adminer.php',
'config.json', 'vendor/*', 'cgi-bin/*', 'solr/*', 'actuator/*',
]);
})
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->shouldRenderJsonWhen(
fn (Request $request) => $request->is('api/*'),
);
// Errors thrown BEFORE the web-group SetLocale middleware (CSRF 419, an
// unmatched-route 404, maintenance 503) would render the custom error page in
// the default locale. Resolve the user's language first so those pages honour
// it too (R16); returning null falls through to the normal renderer (the
// resources/views/errors/* views).
$exceptions->render(function (Throwable $e, Request $request) {
// Best-effort: locale resolution touches the session/user, which can be the
// very thing that failed (e.g. a DB outage). Swallow any error so this hook
// can never throw a second exception and break the custom error page — the
// page just renders in the default locale instead.
try {
SetLocale::apply($request);
} catch (Throwable) {
// keep the default locale
}
return null;
});
})->create();