Introduction
Spring Boot Actuator exposes operational endpoints (/actuator/health, /actuator/env, /actuator/beans, /actuator/metrics) that reveal sensitive application internals. By default in Spring Boot 3, only health and info are exposed over HTTP, but misconfiguration can expose env (showing environment variables and passwords), beans (showing application architecture), or threaddump to unauthenticated users. The most common production incidents involve accidentally exposed actuator endpoints that leak credentials, database connection strings, or internal configuration to the public internet.
Symptoms
Sensitive data exposed:
curl http://localhost:8080/actuator/env
# Shows all environment variables including passwords and API keysOr endpoints not accessible:
{
"timestamp": "2026-04-09T10:00:00.000+00:00",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/actuator/health"
}Common Causes
- expose=* exposes all endpoints: Including sensitive ones like env and beans
- No authentication on actuator port: Actuator accessible without login
- Actuator on same port as application: Public users can reach actuator endpoints
- Custom management port not configured: No separation between app and management
- Health endpoint shows full details: Database version, disk space exposed
- Actuator CORS allows all origins: Cross-origin requests to actuator endpoints
Step-by-Step Fix
Step 1: Configure safe endpoint exposure
```yaml # application.yml management: endpoints: web: exposure: include: health,info,metrics,prometheus # Only safe endpoints exclude: env,beans,threaddump,httptrace # Explicitly exclude sensitive
endpoint: health: show-details: when-authorized # Only show details to authenticated users show-components: always env: enabled: false # Disable entirely if not needed ```
Step 2: Secure actuator endpoints with Spring Security
```java @Configuration public class ActuatorSecurity extends WebSecurityConfigurerAdapter {
@Override protected void configure(HttpSecurity http) throws Exception { http.requestMatcher(EndpointRequest.toAnyEndpoint()) .authorizeRequests() .requestMatchers(EndpointRequest.to("health", "info")) .permitAll() // Health checks accessible to load balancer .anyRequest() .hasRole("ACTUATOR_ADMIN") // Other endpoints require admin role .and() .httpBasic(); } } ```
Step 3: Use separate management port
```yaml # application.yml management: server: port: 8081 # Separate port for actuator endpoints address: 127.0.0.1 # Bind to localhost only
endpoints: web: exposure: include: "*" # Safe because port is not publicly accessible ```
```nginx # nginx config - do NOT proxy /actuator to public server { listen 80; server_name app.example.com;
location / { proxy_pass http://localhost:8080; }
# Actuator on port 8081 is NOT proxied -- internal only } ```
Prevention
- Never use
management.endpoints.web.exposure.include=*in production - Use a separate management port bound to localhost only
- Restrict health endpoint details to authenticated users with
show-details: when-authorized - Add Spring Security rules specifically for actuator endpoints
- Disable endpoints you do not need with
management.endpoint.<name>.enabled: false - Configure CORS to block cross-origin requests to actuator endpoints
- Add security scanning to CI that checks actuator endpoint exposure