Actuator Security and Production Hardening
The Actuator Security Problem
Spring Boot Actuator exposes endpoints that reveal sensitive information about your application — environment variables, configuration properties, heap dumps, thread dumps, and the ability to shut down the application remotely.
An exposed /actuator/env endpoint can leak database passwords, API keys, and JWT signing secrets. An exposed /actuator/shutdown is a denial-of-service button. Actuator security is not optional in production.
Actuator Endpoints and Their Risk Level
| Endpoint | Exposes | Risk |
|---|---|---|
/actuator/health | Application health | Low — often public |
/actuator/info | App metadata | Low |
/actuator/metrics | JVM/HTTP metrics | Medium — business data |
/actuator/env | All configuration properties (including secrets) | Critical |
/actuator/configprops | All @ConfigurationProperties values | Critical |
/actuator/loggers | Log levels (writable) | High |
/actuator/heapdump | Full JVM heap as a file | Critical |
/actuator/threaddump | Thread state | Medium |
/actuator/mappings | All URL mappings | Medium — reveals API surface |
/actuator/shutdown | Kills the JVM | Critical |
/actuator/auditevents | Security events | High |
Step 1: Expose Only What You Need
By default, only health is exposed over HTTP. Explicitly list what to expose:
# Expose only health and info publicly
management.endpoints.web.exposure.include=health,info
# Or expose more for internal monitoring
management.endpoints.web.exposure.include=health,info,metrics,loggers
# Never expose these without extreme care:
# env, configprops, heapdump, shutdown, threaddump, mappings
Disable the shutdown endpoint explicitly:
management.endpoint.shutdown.enabled=false
Step 2: Restrict with Spring Security
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// Public health check (for load balancers)
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/actuator/info").permitAll()
// Sensitive actuator endpoints — admin only
.requestMatchers("/actuator/**").hasRole("ACTUATOR_ADMIN")
// Application endpoints
.requestMatchers("/api/**").authenticated()
.anyRequest().denyAll()
);
return http.build();
}
}
Grant ROLE_ACTUATOR_ADMIN only to operations/monitoring users, not application users.
Step 3: Separate Management Port
The most secure option: run Actuator on a completely separate port, inaccessible from the public network:
management.server.port=8081
management.server.address=127.0.0.1 # bind to localhost only
With the management port bound to 127.0.0.1, Actuator is only reachable from within the host. Your monitoring tools (Prometheus, Grafana, etc.) need to run on the same host or use an SSH tunnel.
In Kubernetes, use a separate internal service:
# Internal management service — no external ingress
apiVersion: v1
kind: Service
metadata:
name: app-management
spec:
selector:
app: your-app
ports:
- port: 8081
targetPort: 8081
type: ClusterIP # internal only, no LoadBalancer
Step 4: Health Endpoint Detail Control
The /actuator/health endpoint can expose database connectivity details and disk space. Configure what is shown to whom:
# Detailed health only for authenticated users
management.endpoint.health.show-details=when-authorized
management.endpoint.health.show-components=when-authorized
management.endpoint.health.roles=ACTUATOR_ADMIN
// Public view: {"status": "UP"}
// Authenticated admin view: full breakdown with database, disk, redis status
For load balancer health checks, create a custom lightweight endpoint:
@RestController
public class HealthCheckController {
@GetMapping("/health")
public ResponseEntity<Map<String, String>> health() {
return ResponseEntity.ok(Map.of("status", "UP"));
}
}
Configure the load balancer to use /health instead of /actuator/health.
Step 5: Securing Sensitive Endpoints with Basic Auth on Management Port
For the management port, add Basic Auth independently:
management.server.port=8081
spring.security.user.name=actuator
spring.security.user.password=${ACTUATOR_PASSWORD}
spring.security.user.roles=ACTUATOR_ADMIN
Or configure a separate SecurityFilterChain for the management port:
@Configuration
@Order(1) // higher priority than the main SecurityFilterChain
public class ManagementSecurityConfig {
@Bean
@ConditionalOnProperty(name = "management.server.port")
public SecurityFilterChain managementFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher(EndpointRequest.toAnyEndpoint())
.authorizeHttpRequests(auth -> auth
.requestMatchers(EndpointRequest.to("health", "info")).permitAll()
.anyRequest().hasRole("ACTUATOR_ADMIN")
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
}
EndpointRequest.toAnyEndpoint() matches all Actuator endpoints regardless of their URL path. EndpointRequest.to("health", "info") matches specific endpoints by ID.
Audit Logging
Spring Security publishes ApplicationEvent for authentication and authorization events. Log them for security monitoring:
@Component
public class SecurityAuditListener {
private static final Logger auditLog = LoggerFactory.getLogger("SECURITY_AUDIT");
@EventListener
public void onAuthenticationSuccess(AuthenticationSuccessEvent event) {
auditLog.info("LOGIN_SUCCESS user={} ip={}",
event.getAuthentication().getName(),
getClientIp());
}
@EventListener
public void onAuthenticationFailure(AbstractAuthenticationFailureEvent event) {
auditLog.warn("LOGIN_FAILURE user={} reason={}",
event.getAuthentication().getName(),
event.getException().getMessage());
}
@EventListener
public void onAuthorizationDenied(AuthorizationDeniedEvent<?> event) {
Authentication auth = (Authentication) event.getAuthentication().get();
auditLog.warn("ACCESS_DENIED user={} object={}",
auth != null ? auth.getName() : "anonymous",
event.getObject());
}
}
Spring Security Audit Events (via Actuator)
Enable Actuator’s audit event endpoint to query security events:
management.endpoints.web.exposure.include=auditevents
Spring Security populates AuditApplicationEvent automatically for form login, HTTP Basic, and remember-me authentication.
Secrets Management
Never store secrets in application.properties or environment variables checked into source control.
Spring Cloud Config + Vault
spring.config.import=vault://secret/myapp
spring.cloud.vault.uri=https://vault.internal
spring.cloud.vault.authentication=KUBERNETES
AWS Secrets Manager
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-secrets-manager-config</artifactId>
</dependency>
spring.config.import=aws-secretsmanager:/myapp/prod/secrets
Minimum: Environment Variables from a Secret Store
At minimum, inject secrets as environment variables from your container orchestration layer:
# Kubernetes — pull from Secret, not ConfigMap
env:
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: db-password
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: app-secrets
key: jwt-secret
Dependency Vulnerability Scanning
Outdated dependencies with known CVEs are a major source of Spring Security bypasses. Automate scanning:
<!-- Maven: OWASP Dependency-Check -->
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>9.0.9</version>
<executions>
<execution>
<goals><goal>check</goal></goals>
</execution>
</executions>
<configuration>
<failBuildOnCVSS>7</failBuildOnCVSS> <!-- fail on high/critical CVEs -->
</configuration>
</plugin>
Run in CI:
mvn dependency-check:check
Subscribe to the Spring Security advisories and keep Spring Security and Spring Boot up to date.
Production Security Checklist
Authentication
- Passwords hashed with BCrypt/Argon2, cost factor calibrated to 100–500 ms
-
DelegatingPasswordEncoderin use to support future algorithm upgrades - MFA available for privileged accounts
- Failed login rate limiting and account lockout implemented
- Remember-me uses persistent tokens (not hash-based)
- Session cookies are
HttpOnly,Secure, andSameSite=Strict
Authorization
- Deny-by-default: security config ends with
.anyRequest().denyAll() - Method security enabled and annotated on all service methods that touch sensitive data
-
@PreAuthorizetested with both allowed and denied users - ACLs or
ownerIdchecks on all data endpoints that return user-specific data
Headers and Transport
- HTTPS enforced everywhere; HTTP redirects to HTTPS
- HSTS enabled with
includeSubDomains - Content Security Policy deployed (at minimum in report-only mode)
-
X-Frame-Options: DENYorframe-ancestors 'none'in CSP -
Referrer-Policy: strict-origin-when-cross-origin -
Permissions-Policydisabling unused browser APIs
Infrastructure
- Actuator endpoints restricted to admin role or separate internal port
- Sensitive Actuator endpoints (
env,heapdump,shutdown) disabled - Secrets in a secret store (Vault, AWS Secrets Manager, K8s Secrets) — not in source control
- Dependency vulnerability scanning in CI pipeline
- Security audit events logged to a centralized log management system
- Alerting on repeated login failures, access denied spikes, and configuration changes
Key Takeaways
- Expose only the Actuator endpoints you actively use — default to a minimal set
- Bind Actuator to a separate port restricted to
localhostor an internal network - Use
EndpointRequest.toAnyEndpoint()in a separate high-prioritySecurityFilterChainfor management endpoints - Log all authentication and authorization events to a centralized audit log
- Store secrets in Vault or a cloud secret store — never in properties files or container environment variables from ConfigMaps
- Automate dependency CVE scanning in CI and subscribe to Spring Security advisories
Next: Reactive Security with Spring WebFlux — apply Spring Security to reactive applications built on Project Reactor.