Spring Boot Actuator: What to Expose, What to Hide, and What to Check Before Adding Endpoints
Actuator in Spring Boot is basically the dashboard of a car: oil level, engine temperature, speed, odometer. Information the mechanic needs — not the passenger in the back seat. If you install that dashboard in the rear seat with public access, you didn't break the car, but you created a problem you didn't have before.
That's exactly what happens when someone enables management.endpoints.web.exposure.include=* in a production application.properties without reading what they just turned on. It's not that Actuator is inherently dangerous — the mistake is exposing it without a clear policy.
My take: Actuator is a legitimate and powerful operational tool. The risk isn't in using it — it's in dropping it in like just another dependency without deciding which endpoints make sense to expose, for whom, and behind what access control.
What the Official Docs Say (and What They Don't)
The official Spring Boot Actuator documentation is more honest than it looks on first read. It defines two distinct things that a lot of people collapse into one:
- Enabling an endpoint: whether it exists and can execute.
- Exposing an endpoint: whether it's accessible via HTTP or JMX.
By default, Spring Boot enables most endpoints but only exposes health and info over HTTP. The rest exist, but they don't respond on the web unless you explicitly include them.
This is an intentional security contract. The docs say it plainly:
"For security purposes, all actuators other than
/healthare not exposed over HTTP by default."
What the docs don't do is tell you what to expose based on your application's context. That's an architecture decision, not a configuration one.
# application.yml — sensible base configuration
management:
endpoints:
web:
exposure:
# Only expose what operations actually consumes
include: health, info, metrics
# Never use '*' in production without authentication and internal network
endpoint:
health:
# Show details only to authenticated users, not everyone
show-details: when-authorizedWhere People Go Wrong: The Copypaste Recipe and Its Cost
The most common mistake I see in Stack Overflow answers, old tutorials, and internal projects without audits is this block:
# What NOT to do in production without authentication
management:
endpoints:
web:
exposure:
include: "*" # Exposes EVERYTHING: env, beans, heapdump, shutdown...What did you just expose with that?
/actuator/env: environment variables, system properties, credentials if they're inapplication.properties./actuator/beans: the complete Spring bean graph — internal architecture, fully visible./actuator/heapdump: an on-demand heap dump. Yes, everything that's in memory./actuator/shutdown: if enabled, shuts down the application via HTTP POST. Enabled by default: no. But if someone added it "for testing" and never removed it before production.../actuator/loggers: live log level changes. Useful in staging. Dangerous exposed without auth.
The official documentation lists every endpoint with its capabilities and default enablement state. No guesswork needed — it's all right there.
The hidden cost isn't just security: it's attack surface, log noise, endpoints that respond even when they serve no purpose in that context. Every exposed endpoint is a path a scanner will probe.
Decision Matrix: What to Expose, for Whom, and with What Control
This is the question worth asking before adding any Actuator endpoint:
| Endpoint | Enable | Expose via HTTP | With What Control |
|---|---|---|---|
health | ✅ Always | ✅ Yes, with show-details: when-authorized | Public for liveness/readiness, details only with auth |
info | ✅ Always | ✅ Yes | Public, no sensitive data |
metrics | ✅ In staging/prod | Internal network only or with auth | Spring Security or private network |
env | ⚠️ Only if needed | ❌ Never without auth + internal network | Spring Security mandatory |
loggers | ⚠️ Staging/debug | Internal network only | Spring Security mandatory |
heapdump | ❌ Not in standard prod | ❌ Never | Only on-demand in controlled debug |
shutdown | ❌ Disabled | ❌ Never | — |
threaddump | ⚠️ Only if needed | Internal network only | Spring Security mandatory |
# Sensible configuration for a production backend
management:
endpoints:
web:
exposure:
include: health, info
base-path: /internal/actuator # Move away from the default path
endpoint:
health:
show-details: when-authorized
shutdown:
enabled: false # Explicit: never in production
# If you need metrics, Spring Security covers them
# management.endpoints.web.exposure.include: health, info, metricsOne detail the documentation mentions that usually gets ignored: you can change Actuator's base-path. Moving it from /actuator to something like /internal/actuator is not security through obscurity if you also apply network control — it's an additional layer that cuts down the noise from automated scanners.
Checklist Before Adding an Actuator Endpoint
Before adding any endpoint to include, three questions:
1. Who actually consumes it?
If the answer is "Prometheus scrape," metrics with basic authentication or a private network is enough. If it's "the ops team for debugging," loggers behind Spring Security makes sense. If it's "not sure, I'll add it just in case" — don't add it.
2. Is it behind access control? Actuator integrates with Spring Security directly. If you already have Security configured, you can restrict Actuator paths like any other:
// SecurityConfig.java — example Actuator restriction
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// Only ACTUATOR_ADMIN role can access sensitive endpoints
.requestMatchers("/internal/actuator/env",
"/internal/actuator/heapdump",
"/internal/actuator/loggers")
.hasRole("ACTUATOR_ADMIN")
// Health and info are public for Kubernetes probes
.requestMatchers("/internal/actuator/health",
"/internal/actuator/info")
.permitAll()
.anyRequest().authenticated()
);
return http.build();
}3. Is it on the right network?
In a containerized environment (Docker, Kubernetes), the usual approach is to use a different port for management than for the app. Spring Boot lets you configure management.server.port to separate the traffic:
# Separate port for management — not exposed externally
management:
server:
port: 8081 # Only accessible from the cluster's internal networkThis is an infrastructure decision that complements Spring Security — it doesn't replace it.
What You Can't Conclude from This Evidence
There are clear limits to what this guide can claim:
- There are no reproducible impact metrics here. I'm not going to tell you "40% of Spring CVEs come from misconfigured Actuator" because I don't have a verifiable source for that. What does exist is official documentation explaining why the default is conservative.
- The real cost of exposed surface depends on context. An internal API inside a private VPC has a completely different risk profile than a service exposed on the public internet. This guide gives you principles; you apply judgment based on your network.
- There's no public evidence that
heapdumporenvcaused a production incident in any specific project I can cite. The recommendation not to expose them comes from reasoning about what they contain, not from a specific post-mortem.
What is verifiable: the official documentation describes exactly what each endpoint exposes. Before enabling any of them, read that table. It takes five minutes and it's the only source you need to make an informed decision.
Frequently Asked Questions About Spring Boot Actuator and Endpoint Security
Is it safe to have /actuator/health public in production?
Depends on what it shows. With show-details: always, health can expose information about databases, caches, and external services. With show-details: when-authorized, it returns only UP/DOWN status to unauthenticated users — which is all Kubernetes health checks or a load balancer need. Spring Boot's default is show-details: never, which is the most conservative starting point.
What's the difference between enabling and exposing an endpoint?
Enabling means the endpoint exists and can execute internally. Exposing means it's accessible via HTTP or JMX. An endpoint can be enabled but not exposed: it exists in the Spring context but doesn't respond to web requests. The official documentation separates these two dimensions with distinct properties: management.endpoint.<id>.enabled and management.endpoints.web.exposure.include.
Does management.endpoints.web.exposure.include=* make sense in any context?
In local development, it can be useful for exploring what information is available. In staging with Spring Security and an internal network, it's reasonable if the team is actively consuming that information. In production exposed without authentication: no. The * in production without access control is the pattern the documentation implicitly discourages by establishing conservative defaults.
How do I integrate Actuator with Prometheus without exposing metrics publicly?
Use management.server.port to separate the management port from the application port, and configure the Prometheus scrape to point to the internal port. In Kubernetes, this means the Prometheus Service points to the management port that isn't exposed by the app's Ingress. Spring Boot Actuator includes Prometheus format support via Micrometer if you add spring-boot-starter-actuator along with micrometer-registry-prometheus.
Does /actuator/env show passwords or secrets?
Spring Boot masks properties containing words like password, secret, key, or token in the /env output, replacing them with ******. But sanitization depends on the configured patterns and whether the property name matches those patterns. Variables with unconventional names might not get masked. The conservative recommendation is to not expose /env over HTTP in production, regardless of sanitization.
Is it worth changing Actuator's base-path?
Moving it from /actuator to another path reduces noise from automated scanners looking for that default path. It's not a security measure on its own, but combined with Spring Security and port separation it reduces visible attack surface. The configuration is management.endpoints.web.base-path.
Actuator as an Operational Contract, Not a Toggle
My position is this: Actuator is one of the best-designed parts of the Spring ecosystem. The enable vs. expose model is deliberate and sensible. The problem appears when someone treats it as a binary toggle — "I turn it on or off" — instead of as a contract between the application, the operations team, and the network it lives in.
What I don't buy is the generic recommendation of "don't use Actuator in production." That's throwing out the dashboard because you're afraid someone might look at it. The right answer is to decide what operational information you need, expose it only to whoever consumes it, and with the appropriate access control.
The concrete next step: open the application.properties or application.yml of a Spring Boot backend that's already running and look for what's configured under management.endpoints.web.exposure.include. If it's empty, the default is conservative and that's fine. If it has *, there's a conversation pending about who consumes those endpoints and from where.
If you're interested in applying this decision-making framework to other contexts — like the authentication token decision tree or authorization patterns in middleware — there are related posts on this blog that tackle the same question from a different angle: Next.js 16 Middleware: authorization patterns that scale, Rate limiting in web applications: what to protect first, and Authentication tokens: JWT, Paseto, and session tokens.
Primary source:
- Spring Boot Actuator — Endpoints: https://docs.spring.io/spring-boot/reference/actuator/endpoints.html
Related Articles
OpenTelemetry in Next.js: traces that survive the edge/server boundary without losing context
OpenTelemetry in Next.js works, but the default propagator silently breaks the trace at the edge/node boundary. Here's what you need to configure explicitly so context doesn't vanish between Middleware, Server Components, and Server Actions.
How Memory Safety CVEs Differ Between Rust and C/C++
Rust has fewer memory CVEs than C/C++ — but that's not the whole story. My analysis of what that number actually says, what it doesn't, and how to turn it into a real technical decision.
What Job Interviews Taught Me About Kubernetes
Kubernetes technical interviews have a problem nobody names: they ask about objects you'll never touch in production, while ignoring the mistakes that actually break real systems. Here's the map I was missing.
Comments (0)
What do you think of this?
Drop your comment in 10 seconds.
We only use your login to show your name and avatar. No spam.