“How would you configure Spring Boot Actuator for production?” sounds like a configuration question, but interviewers use it to test security judgement. Actuator can tell an attacker your environment variables, dump your heap (with session tokens and passwords inside), or let them change log levels at runtime. A candidate who answers management.endpoints.web.exposure.include=* without a second sentence has told the interviewer something important.
The strong answer separates three questions: which endpoints exist, which ones are reachable over HTTP, and who is allowed to call them.
Before you start
You should know how a Spring Boot application is configured through application.yml, how Spring Security’s SecurityFilterChain matches requests, and what a Kubernetes liveness or readiness probe does. The examples use Spring Boot 3.x with spring-boot-starter-actuator and spring-boot-starter-security.
The short answer
Actuator endpoints are enabled and exposed independently. In Spring Boot 3 only health is exposed over HTTP by default. Expose the minimum your monitoring needs (usually health, info and prometheus), protect everything except the probe endpoints with Spring Security, keep env, configprops, heapdump and threaddump off the public network, and verify reachability from the real network boundary rather than trusting property names.
How it works
Every Actuator endpoint goes through two gates before it handles a web request. The first is availability: an endpoint must be enabled (most are by default) and exposed over a technology, either JMX or the web. The second is ordinary request authorization, which Actuator does not do on its own once Spring Security is on the classpath; your filter chain decides.
management:
endpoints:
web:
exposure:
include: health,info,prometheus # reachable under /actuator/*
endpoint:
health:
show-details: when-authorized # never, when-authorized, always
roles: OPS
probes:
enabled: true # /actuator/health/liveness and /readinessinclude and exclude are evaluated together and exclude wins, so include: "*" with exclude: env,heapdump still exposes beans, mappings, loggers and more. That is why allow-listing is the safer habit.
Authorization is a normal SecurityFilterChain. Spring Boot provides EndpointRequest, a request matcher that understands the actuator base path and endpoint IDs, so your rules keep working if someone changes management.endpoints.web.base-path.
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.info.InfoEndpoint;
@Configuration
class ActuatorSecurity {
@Bean
@Order(1)
SecurityFilterChain actuatorChain(HttpSecurity http) throws Exception {
http.securityMatcher(EndpointRequest.toAnyEndpoint())
.authorizeHttpRequests(auth -> auth
.requestMatchers(EndpointRequest.to(HealthEndpoint.class, InfoEndpoint.class)).permitAll()
.anyRequest().hasRole("OPS"))
.httpBasic(Customizer.withDefaults());
return http.build();
}
}The @Order(1) chain claims only actuator URLs, and your application chain handles the rest.
Step-by-step walkthrough
Step 1: List the operations you actually need
Start from consumers, not from endpoints. A Kubernetes kubelet needs liveness and readiness. Prometheus needs /actuator/prometheus. A support engineer may occasionally need loggers to raise a log level during an incident. Nobody needs heapdump over the public internet.
management:
endpoints:
web:
exposure:
include: health,info,prometheus,loggersWrite the consumer next to each endpoint in a comment or runbook. If you cannot name one, do not expose it.
Step 2: Separate exposure from access
Exposure makes a URL respond. It says nothing about who may call it. Once exposed, loggers accepts a POST that changes a logger level, so it must sit behind a role:
.authorizeHttpRequests(auth -> auth
.requestMatchers(EndpointRequest.to("health", "info")).permitAll()
.requestMatchers(EndpointRequest.to("prometheus")).hasRole("METRICS")
.requestMatchers(EndpointRequest.to("loggers")).hasRole("OPS")
.anyRequest().denyAll())Ending with denyAll() means a newly exposed endpoint is closed until someone writes a rule for it.
Step 3: Trim what health reveals
/actuator/health aggregates indicators for the database, disk, Redis and so on. With show-details: always an anonymous caller learns your database vendor and disk capacity. Use when-authorized so the public sees only {"status":"UP"} and operators with the role see components.
management:
endpoint:
health:
show-details: when-authorized
show-components: when-authorized
roles: OPSStep 4: Decide on a management port deliberately
management.server.port=8081 moves Actuator to a separate listener. This helps only if the network prevents outside traffic from reaching 8081, for example a Kubernetes Service that publishes 8080 only, or an ingress that never routes to it. The port alone is not a security control.
management:
server:
port: 8081Worked scenario
A team ships a service with this configuration because “the dashboards needed metrics”:
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: alwaysNo Spring Security is on the classpath. A penetration tester requests /actuator/env and sees property names with sanitized values, then requests /actuator/heapdump and receives a 200 MB HPROF file. Opening it in Eclipse MAT reveals a cached JDBC password and several bearer tokens in live request objects. /actuator/mappings gives them a map of every internal route.
The fix has three parts. First, allow-list exposure to health,info,prometheus. Second, add the actuator security chain from the “How it works” section so prometheus requires a role and everything not named is denied. Third, switch health details to when-authorized. After the change, /actuator/heapdump returns 404 because it is not exposed at all, which is better than a 401 because it does not even confirm the endpoint exists.
management:
endpoints:
web:
exposure:
include: health,info,prometheus
endpoint:
health:
show-details: when-authorized
roles: OPS
probes:
enabled: trueGotcha
Spring Boot 3 sanitizes values in
envandconfigpropsby default (show-values: never), but key names, active profiles and property sources still leak useful reconnaissance. Treat both endpoints as privileged.
Common mistake
- “Actuator is safe because values are masked.” Masking covers
envandconfigprops. It does nothing forheapdump, which contains raw memory. - “I excluded the dangerous ones.” Deny-listing fails open: every new endpoint added by a dependency upgrade becomes public.
- “It’s on port 8081, so it’s internal.” If the load balancer or a misconfigured Service routes to it, it is public.
- “Health should show everything so on-call can debug.” Give on-call a role; do not give the internet your dependency topology.
- Confusing enabled with exposed.
management.endpoint.shutdown.enabled(oraccessin newer versions) controls whether the endpoint exists; exposure controls whether it is reachable.
Verify the behavior
Write a test that asserts both directions: public probes work and privileged endpoints do not.
@SpringBootTest
@AutoConfigureMockMvc
class ActuatorExposureTest {
@Autowired MockMvc mvc;
@Test
void healthIsPublicButMinimal() throws Exception {
mvc.perform(get("/actuator/health"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("UP"))
.andExpect(jsonPath("$.components").doesNotExist());
}
@Test
void heapdumpIsNotExposed() throws Exception {
mvc.perform(get("/actuator/heapdump").with(user("ops").roles("OPS")))
.andExpect(status().isNotFound());
}
@Test
void prometheusNeedsARole() throws Exception {
mvc.perform(get("/actuator/prometheus")).andExpect(status().isUnauthorized());
}
}Then check the deployed system from outside the cluster with curl -i https://api.example.com/actuator/env and from inside with the management port. A test proves configuration; only the curl from the real boundary proves the network.
Follow-up questions
What is the default exposure in Spring Boot 3? Over HTTP only health is exposed. Spring Boot 3.0 also reduced the default JMX exposure to just health; earlier versions exposed everything over JMX.
How do Kubernetes probes map to Actuator? With probes enabled, /actuator/health/liveness reports the LivenessState and /actuator/health/readiness reports ReadinessState. Keep external dependencies out of liveness so a database outage does not restart every pod.
Why prefer EndpointRequest over "/actuator/**"? It follows the configured base path and management port, so renaming the path cannot silently bypass your rules.
How would you expose a custom endpoint? Annotate a bean with @Endpoint(id = "features") and @ReadOperation methods, then add features to the include list and to the security rules like any other endpoint.
Interview exercise
Your service runs on Kubernetes, is scraped by Prometheus inside the cluster, and support needs to change log levels during incidents. Write the exposure and authorization policy, and explain what an anonymous internet user can see.
Answer and reasoning
Expose health,info,prometheus,loggers and enable probes. Permit health (including the liveness and readiness groups) and info anonymously with show-details: when-authorized. Require a METRICS role (or rely on a network policy allowing only the Prometheus pod) for prometheus, and an OPS role for loggers because it accepts writes. End the actuator chain with denyAll(). Optionally move management to port 8081 and publish only 8080 through the ingress. An anonymous internet user then sees {"status":"UP"} and the info payload, nothing else; everything else is either unexposed (404) or unauthorized (401). The reasoning to state aloud: allow-list exposure, deny by default in security, and verify from outside.