[← 03 Endpoint catalogue](03-endpoint-catalogue.md) · **04 · Securing Actuator** · [05 Custom health indicators →](05-custom-health-indicators.md) # 04 — Securing Actuator Two mechanisms, and you want both. Network isolation decides who can reach the port; authorisation decides who can use it. Neither is a substitute for the other. ## The separate management port [`SecuredActuatorConfig`](../src/main/java/com/ankurm/actuator/config/SecuredActuatorConfig.java) handles authorisation. `application-mgmtport.yaml` handles reachability: ```yaml management: server: port: 9001 address: 127.0.0.1 endpoints: web: base-path: /manage ``` From [`output/06-management-port.txt`](output/06-management-port.txt): ``` GET :8080/actuator/health 401 <- application chain, no Actuator here GET :8080/orders/count 200 GET :9001/manage/health 503 GET :9001/actuator/health 404 <- base-path moved GET :9001/orders/count 404 <- separate context, no app controllers ``` ``` LISTEN *:8080 LISTEN [::ffff:127.0.0.1]:9001 ``` The last two lines are the argument. The management port runs a **separate application context** with its own `DispatcherServlet`, so it cannot serve your controllers, and binding it to loopback (or simply not listing it as a Kubernetes Service port) makes Actuator unreachable from the internet by routing rather than by a rule somebody has to keep correct. This is defence in depth, not a replacement for authorisation. Anything running in the same pod or on the same host still reaches 9001. ## Match on `EndpointRequest`, not on a path ```java http.securityMatcher(EndpointRequest.toAnyEndpoint()) .authorizeHttpRequests((requests) -> requests .requestMatchers(EndpointRequest.to(HealthEndpoint.class, InfoEndpoint.class)).permitAll() .anyRequest().hasRole("ACTUATOR")) ``` A rule written as `requestMatchers("/actuator/**")` stops matching the moment somebody sets `management.endpoints.web.base-path` — as the `mgmtport` profile does. The endpoint moves, the rule does not follow, and the endpoint is now unprotected with no error anywhere. `EndpointRequest` asks the endpoint registry where things actually are. ## Order the chains `@Order(1)` on the Actuator chain, `@Order(2)` on the application chain. Without explicit ordering, whichever chain Spring registers first wins for a given request, and an application chain ending in `permitAll()` will happily swallow `/actuator/**`. ## The resulting matrix From [`output/05-secured-matrix.txt`](output/05-secured-matrix.txt), with exposure set to `"*"`: | Request | Anonymous | `ops` / ROLE_ACTUATOR | Wrong password | |---|---|---|---| | `/actuator/health` | 503 | 503 | — | | `/actuator/info` | 200 | — | — | | `/actuator/env` | 401 | 200 | 401 | | `/actuator/beans` | 401 | 200 | — | | `/actuator/threaddump` | 401 | 200 | — | | `/actuator` (links) | 401 | — | — | | `/orders/count` | 200 | — | — | Exposure is `"*"` and nothing leaks. That is the point: **exposure is not access control.** Use exposure to decide what exists, and the security chain to decide who may use it. ## `show-details: when-authorized` The third option, and the one to reach for. Anonymous: ```json {"groups":["liveness","readiness"],"status":"DOWN"} ``` Authenticated with `ROLE_ACTUATOR`: the full nine-component breakdown, naming `kafka` as the failing one. Same URL, same status code, different body. Your load balancer gets the signal it needs; an anonymous prober learns that something is wrong but not which of your dependencies to attack next. ```yaml management: endpoint: health: show-details: when-authorized roles: ACTUATOR ``` ## Make the Actuator chain stateless and CSRF-free Spring Boot's auto-configured security is browser-shaped: it enables CSRF protection. Actuator is a machine-to-machine API, and `POST /actuator/loggers/{name}` from a script will be rejected before it reaches the endpoint. While building this repository that showed up as a `401` on `POST /stub/upstream/mode` while `GET /orders/count` with the *same credentials* returned `200` — a genuinely confusing pair of results that has nothing to do with the credentials. See [`DemoSecurityConfig`](../src/main/java/com/ankurm/actuator/config/DemoSecurityConfig.java). ```java .csrf((csrf) -> csrf.disable()) .sessionManagement((s) -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) ``` --- [← 03](03-endpoint-catalogue.md) · **04** · [05 Custom health indicators →](05-custom-health-indicators.md)