Actuator Endpoints
Add Spring Boot Actuator, expose only the endpoints you need, and keep the sensitive ones off the public internet.
management.endpoints.web.exposure.include=health,info,metrics
management.server.port=9090
management.endpoint.<id>.access=none|read-only|unrestricted
@Endpoint(id = "name") class X { @ReadOperation Object read() { ... } }Actuator gives a running application a set of operational endpoints: health, info, metrics, loggers, environment, thread dumps and more. Add spring-boot-starter-actuator and they appear under /actuator. Operations teams, load balancers and Kubernetes all talk to these endpoints, so they are part of your API surface even though no customer calls them.
Available versus exposed
Two separate questions decide whether an endpoint answers. Access says whether the endpoint exists at all and what it may do (none, read-only or unrestricted), set per endpoint with management.endpoint.<id>.access. Exposure says which transport it is published on. Over HTTP only health is exposed by default, which is a safe starting point.
management.endpoints.web.exposure.include=health,info,metrics,loggersList the endpoints by name. include=* is fine on a laptop and a serious leak in production: env and configprops describe your configuration, heapdump can contain passwords and tokens held in memory, and loggers accepts writes that change log levels. Values in env and configprops are masked by default (show-values=never), and heapdump and shutdown have access none unless you change it. Keep it that way unless you have a reason.
A separate port for operations
management.server.port moves every Actuator endpoint to its own port. The public load balancer only routes the application port, so the management port is reachable only from inside the cluster or network. This is simpler and harder to get wrong than path-based rules, and it keeps scrapes and probes out of your request logs.
Securing what stays on the main port
If Spring Security is on the classpath, Actuator endpoints are protected by your security configuration like any other URL. Use EndpointRequest to write rules in terms of endpoint IDs rather than paths, so a changed base path does not open a hole. A common split is health and info open to everyone, everything else behind an operations role.
Your own information
The info endpoint is empty until something contributes to it. Build information appears when the build generates META-INF/build-info.properties (the Maven plugin's build-info goal), git details when git.properties is present. An InfoContributor bean adds anything else. For operations that do not fit an existing endpoint, @Endpoint with @ReadOperation and @WriteOperation methods defines a new one. It is not tied to HTTP: the same endpoint is also published over JMX when JMX is enabled (spring.jmx.enabled, off by default).
A mistake that shows up in audits: teams expose env to debug one incident and never remove it. Treat the exposure list like a firewall rule and review it with the same care.
Example
import org.springframework.boot.security.autoconfigure.actuate.web.servlet.EndpointRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
public class ActuatorSecurityConfig {
// Checked before the application's own chain because of the lower order value.
(1)
SecurityFilterChain actuatorChain(HttpSecurity http) throws Exception {
http
.securityMatcher(EndpointRequest.toAnyEndpoint())
.authorizeHttpRequests(auth -> auth
.requestMatchers(EndpointRequest.to("health", "info")).permitAll()
.anyRequest().hasRole("OPS"))
.httpBasic(Customizer.withDefaults());
return http.build();
}
}
When to use it
- Give the platform team a /actuator/info response that shows the git commit and build time of every running instance.
- Move all Actuator endpoints to port 9090 so the public ingress can never route to them.
- Change a logger to DEBUG on one live instance through /actuator/loggers while investigating a fault, then set it back.
More examples
Production exposure settings
Setting <code>access=none</code> removes the endpoint completely, so a later change to the exposure list cannot publish it. The separate management port is the strongest single setting here: even a mistaken exposure list is then only visible inside your network.
# Only what operations actually use.
management.endpoints.web.exposure.include=health,info,metrics,loggers
# Serve Actuator on a port the public load balancer never routes.
management.server.port=9090
# Turn off endpoints you never want, even by accident.
management.endpoint.heapdump.access=none
management.endpoint.env.access=none
# Show health details only to authenticated callers.
management.endpoint.health.show-details=when-authorized
# Include Java and OS details in /actuator/info.
management.info.java.enabled=true
management.info.os.enabled=true
Adding to the info endpoint
Every <code>InfoContributor</code> bean adds keys to the JSON returned by <code>/actuator/info</code>. Region and start time help when you are comparing several instances during an incident. Never put secrets here: the info endpoint is usually left open.
import java.time.Instant;
import java.util.Map;
import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.stereotype.Component;
@Component
public class DeploymentInfoContributor implements InfoContributor {
private final Instant startedAt = Instant.now();
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("deployment", Map.of(
"region", System.getenv().getOrDefault("APP_REGION", "local"),
"startedAt", startedAt.toString()));
}
}
A custom endpoint for feature flags
This publishes <code>GET /actuator/features</code>, <code>GET /actuator/features/{name}</code> and <code>POST /actuator/features/{name}</code> with a JSON body such as <code>{"enabled": true}</code>. It still has to be added to the exposure list, and because it accepts writes it belongs behind the operations role or on the management port.
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.stereotype.Component;
@Component
@Endpoint(id = "features")
public class FeaturesEndpoint {
private final Map<String, Boolean> flags = new ConcurrentHashMap<>(Map.of("newCheckout", false));
@ReadOperation
public Map<String, Boolean> all() {
return Map.copyOf(flags);
}
@ReadOperation
public Boolean one(@Selector String name) {
return flags.get(name);
}
@WriteOperation
public void set(@Selector String name, boolean enabled) {
flags.put(name, enabled);
}
}
Discussion