Actuator exposed to the internet
An exposed Actuator is not a vulnerability. Every part of it is a documented feature
working exactly as designed, and Spring Boot’s defaults are deliberately conservative
— only /health over HTTP, values sanitized, show-details: never.
Which makes the interesting question not “what is the bug” but how does a well-defaulted system end up open. There are two routes, and neither looks like a security decision at the time.
Route one: the wildcard
Section titled “Route one: the wildcard”Someone needs Prometheus scraping, or a Kubernetes probe, or an APM agent. The endpoint they need is not exposed. The fastest thing that works is:
management.endpoints.web.exposure.include: "*"It ships, monitoring works, and nobody returns to narrow it. Boot’s documentation warns about exactly this property, and the warning is easy to skip past when you are debugging a scrape target at the time.
Route two: the migration — the one worth understanding
Section titled “Route two: the migration — the one worth understanding”This is the route that gets missed, because the change that causes it is a security change.
Spring Boot secures Actuator for you, but only conditionally. From its documentation:
If Spring Security is on the classpath and no other
SecurityFilterChainbean is present, all actuators other than/healthare secured by Spring Boot auto-configuration. If you define a customSecurityFilterChainbean, Spring Boot auto-configuration backs off and lets you fully control the actuator access rules.
Now recall that WebSecurityConfigurerAdapter was removed in Spring Security
6.0. Every application migrating to 6 or 7 must
define a SecurityFilterChain. The moment it does, Boot’s Actuator protection backs
off — and a chain written from a form-login tutorial says nothing about /actuator/**.
A mandatory security migration silently removes a protection somewhere else. No warning, no failing test, no compile error. If both routes happen in the same application — the wildcard for monitoring, then a migration that drops the protection — the endpoints are public.
curl -s -o /dev/null -w 'env=%{http_code} ' https://example.com/actuator/envcurl -s -o /dev/null -w 'heapdump=%{http_code}\n' https://example.com/actuator/heapdumpRun it unauthenticated, from outside your network. 401, 403 or 404 are all fine.
A 200 on /actuator/heapdump is an incident, not a finding.
What an attacker gets
Section titled “What an attacker gets”Roughly in order of severity:
/heapdump — a dump of the JVM heap, downloadable as a file. Every secret the
process has in memory is in it: datasource passwords, API tokens, session contents,
decrypted configuration. No sanitization setting applies, because sanitization
operates on configuration properties, not on memory. This alone justifies the
critical rating.
/env and /configprops — the shape of the configuration. Values are ****** by
default, but the keys are not, and they map the application: which database, which
message broker, which internal services, which cloud provider.
/loggers — writable. A POST changes log levels at runtime. That is a
denial-of-service lever, and a way to make the application start logging data it
normally would not.
/mappings and /beans — a complete route list and object graph, which turns
black-box probing into reading a map.
/threaddump — stack traces with arguments, which regularly contain tokens.
Note what is mostly not here: this is a disclosure and control-plane compromise
rather than direct code execution. There is no documented path from an exposed
Actuator to running arbitrary code on the host, the way there is with a writable data
store that can be made to drop a file on disk. It is rated critical on the heap dump
alone, and inflating it into RCE would be the easy mistake.
Closing it
Section titled “Closing it”Three layers, in order of durability:
- Do not expose what you do not need. Name endpoints; never
*. See exposure. - Say something explicit in your
SecurityFilterChain, usingEndpointRequest, because Boot has stopped saying it for you. - Move Actuator to its own port with
management.server.portand keep that port off the internet. This is the only layer that still holds when someone adds a wildcard in six months.
The third is the one to argue for. The first two depend on every future change being made carefully; the third does not.
Related
Section titled “Related”- SecurityFilterChain — the migration that opens this
- Actuator exposure — the control
- What /env actually shows — what sanitization does and does not cover