Documents the architectural decision behind the dedicated management SecurityFilterChain, the discovery that SB4+Jetty removed the isolated management child-context security, and the consequences for actuator endpoint exposure. Addresses @markus blocker from PR #606 review. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4.5 KiB
ADR-017: Spring Boot 4.0 management port shares the main security filter chain
Status
Accepted
Context
The Familienarchiv backend runs Spring Boot Actuator on a dedicated management port (8081) so that Caddy never proxies /actuator/* requests and Prometheus can reach the scrape endpoint directly inside archiv-net.
In earlier Spring Boot versions (< 4.0), the management server ran in an isolated child application context whose security was governed independently by ManagementWebSecurityAutoConfiguration. The main app's SecurityConfig filter chain (port 8080) never intercepted requests arriving on port 8081.
In Spring Boot 4.0 with Jetty, this isolation was removed. The management server now traverses the same Spring Security FilterChainProxy as the main application. Concretely:
- Any
SecurityFilterChainbean in the application context is evaluated for requests arriving on the management port. - There is no longer a separate "management security" child context.
This was discovered when Prometheus began receiving HTTP 401 responses from /actuator/prometheus despite the endpoint being exposed and the micrometer-registry-prometheus dependency being present. Prometheus rejected these responses with received unsupported Content-Type "text/html" because the main filter chain's form-login DelegatingAuthenticationEntryPoint was redirecting unauthenticated requests to /login (302 → HTML).
A secondary issue: Spring Boot 4.0 no longer auto-enables Prometheus metrics export — management.prometheus.metrics.export.enabled must be set explicitly, and the Prometheus scrape endpoint requires spring-boot-starter-micrometer-metrics (a new starter that was split out in Spring Boot 4.0).
Decision
-
Dedicated management
SecurityFilterChainscoped to/actuator/**at@Order(1)(highest precedence). This chain:permitAll()for/actuator/healthand/actuator/prometheus— required for Docker health checks and unauthenticated Prometheus scraping.authenticated()for all other actuator endpoints — blocks/actuator/metrics,/actuator/info, etc. without credentials.- Uses an explicit
401entry point (not form-login redirect) so that API clients — including Prometheus — receive a machine-readable status code rather than an HTML redirect. - No CSRF, no form login.
-
Belt-and-suspenders
permitAll()in the mainSecurityFilterChainfor/actuator/healthand/actuator/prometheus, in case a future configuration change causes these paths to escape the management chain'ssecurityMatcher. -
Network isolation as the outer defense boundary. Port 8081 is not published in
docker-compose.ymland is not routed through Caddy. Only services insidearchiv-net(primarily Prometheus and the Docker health checker) can reach the management port.
Alternatives rejected
- Exclude
ManagementWebSecurityAutoConfiguration: This auto-configuration no longer exists in Spring Boot 4.0. Exclusion is not applicable. - Keep
SecurityConfigas the sole filter chain without@Order(1)management chain: The main chain's form-loginDelegatingAuthenticationEntryPointredirects browser-like clients to/login(302). Prometheus and automated health check clients cannot follow this redirect, so the endpoint would be unreachable without a dedicated chain that returns plain 401 or 200. - Per-endpoint
@Order(1)filter chain usingEndpointRequest.toAnyEndpoint(): Thespring-boot-securityartifact that providesEndpointRequestis not a transitive dependency ofspring-boot-starter-actuatorin Spring Boot 4.0. Using a path-basedsecurityMatcher("/actuator/**")achieves the same scoping without an extra dependency.
Consequences
- All actuator endpoints on port 8081 that are not explicitly
permitAll()-ed require HTTP Basic credentials. Without valid credentials, the response is 401 (not a redirect). - Adding a new actuator endpoint to
management.endpoints.web.exposure.includeimplicitly protects it viaanyRequest().authenticated()in the management chain — no additionalpermitAll()needed unless intentional. - A regression test (
ActuatorPrometheusIT) verifies:/actuator/prometheusreturns 200 without credentials./actuator/metricsreturns 401 without credentials.- Prometheus metric names are present in the response body.
- If port 8081 is ever accidentally published in
docker-compose.yml, actuator endpoints other than health and prometheus are still protected by HTTP Basic. This reduces (but does not eliminate) the risk of inadvertent exposure.