How do you configure Spring Security to restrict Actuator endpoints, and what does EndpointRequest.toAnyEndpoint() do?
answer
- EndpointRequest not antMatchers
- toAnyEndpoint() = all endpoints, base-path aware
- to("health","info") / toLinks() / .excluding()
- dedicated @Order'd filter chain, HTTP Basic
- hasRole adds ROLE_ prefix
basics
~10 sDefine a SecurityFilterChain bean and use the EndpointRequest matcher instead of hardcoding /actuator paths. EndpointRequest.toAnyEndpoint() matches every actuator endpoint; require a role like hasRole('ACTUATOR_ADMIN'). You can exclude health/info so they stay public.
solid answer
~40 sYou register a SecurityFilterChain and match actuator requests with the type-safe EndpointRequest matcher rather than a literal path. EndpointRequest.toAnyEndpoint() resolves to whatever the actuator base path and registered endpoints are (respecting management.endpoints.web.base-path and separate ports), so it survives config changes. Typical config: permit EndpointRequest.to("health", "info") for liveness/readiness probes, then require a role for EndpointRequest.toAnyEndpoint(). You can also use .excluding("health") or EndpointRequest.toLinks() for the discovery page. Because it's a dedicated filter chain you usually give it @Order so it runs before the app's chain and uses its own authentication (often HTTP Basic for machine callers). This keeps path knowledge in one type-safe place and avoids brittle antMatchers("/actuator/**").
code
java · 16 lines@Configuration
public class ActuatorSecurityConfig {
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER - 1)
SecurityFilterChain actuatorChain(HttpSecurity http) throws Exception {
http
.securityMatcher(EndpointRequest.toAnyEndpoint()) // scope this chain to /actuator
.authorizeHttpRequests(auth -> auth
.requestMatchers(EndpointRequest.to("health", "info")).permitAll() // probes
.anyRequest().hasRole("ACTUATOR_ADMIN"))
.httpBasic(Customizer.withDefaults())
.csrf(csrf -> csrf.disable()); // stateless machine callers
return http.build();
}
}go deeper
Should know a SecurityFilterChain plus a role requirement locks actuator down.
Should use EndpointRequest.toAnyEndpoint()/to()/excluding() and explain why it beats path literals.
Should design a dedicated ordered chain, permit probes, and handle CSRF/Basic for machine callers.
Should reason about chain ordering, separate-port context, and probe vs admin auth boundaries holistically.
**The problem with path literals.** Hardcoding `requestMatchers("/actuator/**")` is brittle: the base path is configurable (`management.endpoints.web.base-path`), individual endpoint paths can be remapped (`management.endpoints.web.path-mapping.*`), and a separate management port changes routing. Spring Boot provides `org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest` — a `RequestMatcher` factory that knows the actual runtime layout. **The matcher factories:** - `EndpointRequest.toAnyEndpoint()` — matches **all** actuator endpoints (respecting base path/port). Supports `.excluding("health", "info")` to carve out public ones. - `EndpointRequest.to("health", "info")` or `EndpointRequest.to(HealthEndpoint.class)` — matches only the named/typed endpoints. - `EndpointRequest.toLinks()` — matches the discovery/links endpoint served at the base path (`/actuator`). **Wiring a filter chain (Spring Security 6 / Boot 3, lambda DSL):** create a `@Bean SecurityFilterChain` that scopes itself to actuator via `securityMatcher(EndpointRequest.toAnyEndpoint())`, then authorizes inside. Or keep one chain and add rules. Common pattern is a **dedicated, higher-priority chain** annotated with `@Order(SecurityProperties.BASIC_AUTH_ORDER - 1)` (or any order lower than the app chain) so probes/monitoring use their own auth mechanism (HTTP Basic) independent of the app's form login / JWT. **Role semantics.** `hasRole("ACTUATOR_ADMIN")` requires authority `ROLE_ACTUATOR_ADMIN` (the `ROLE_` prefix is added automatically). Use `hasAuthority` if you don't use the prefix convention. For probes you typically `permitAll()` `health` and `info` so Kubernetes liveness/readiness work anonymously — but then rely on `show-details: when-authorized` so anonymous callers see only UP/DOWN, not component detail. **Edge cases / gotchas:** - The matcher only matches when the actuator is on the **same** port as the chain. With a separate management port, the management context has its own auto-configured security; be deliberate about which chain applies. - If you `permitAll()` `toLinks()` you expose the discovery listing of endpoints — usually fine but it advertises what's available. - Ordering matters: if the app's catch-all chain (with `anyRequest().authenticated()` and form login) is evaluated first, actuator calls may get redirected to a login page instead of a 401. A dedicated, higher-priority chain avoids that. - CSRF: state-changing actuator endpoints (POST /loggers, /shutdown) are subject to CSRF protection; machine callers using stateless Basic auth typically disable CSRF on the actuator chain. **When to use what:** use `toAnyEndpoint().excluding("health","info")` + role for the standard 'lock everything, expose probes' pattern; use `to(...)` when you want a fine-grained allowlist.
- Why prefer EndpointRequest.toAnyEndpoint() over requestMatchers("/actuator/**")?Because it's type-safe and resolves the real runtime layout — it honors management.endpoints.web.base-path, per-endpoint path mappings, and a separate management port. A hardcoded path silently stops matching if any of those change.
- How do you let Kubernetes probes hit /health anonymously while locking everything else?permitAll() EndpointRequest.to("health", "info") and require a role for anyRequest(), and set management.endpoint.health.show-details=when-authorized so anonymous probes only see UP/DOWN, not component internals.
- Why give the actuator chain its own @Order and HTTP Basic?So monitoring/probe traffic authenticates with a mechanism suited to machines and returns 401 rather than being redirected to the app's form-login page by a lower-priority catch-all chain.
saying these in an interview costs you the question
- Using antMatchers/requestMatchers('/actuator/**') as the primary approach
- Forgetting that hasRole('ACTUATOR_ADMIN') expects authority ROLE_ACTUATOR_ADMIN
- Putting the actuator rules in the app's form-login chain and getting redirects instead of 401s
- permitAll() on toAnyEndpoint() thinking it only opens health