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

EndpointExposesRisk
/actuator/healthApplication healthLow — often public
/actuator/infoApp metadataLow
/actuator/metricsJVM/HTTP metricsMedium — business data
/actuator/envAll configuration properties (including secrets)Critical
/actuator/configpropsAll @ConfigurationProperties valuesCritical
/actuator/loggersLog levels (writable)High
/actuator/heapdumpFull JVM heap as a fileCritical
/actuator/threaddumpThread stateMedium
/actuator/mappingsAll URL mappingsMedium — reveals API surface
/actuator/shutdownKills the JVMCritical
/actuator/auditeventsSecurity eventsHigh

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
  • DelegatingPasswordEncoder in 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, and SameSite=Strict

Authorization

  • Deny-by-default: security config ends with .anyRequest().denyAll()
  • Method security enabled and annotated on all service methods that touch sensitive data
  • @PreAuthorize tested with both allowed and denied users
  • ACLs or ownerId checks 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: DENY or frame-ancestors 'none' in CSP
  • Referrer-Policy: strict-origin-when-cross-origin
  • Permissions-Policy disabling 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 localhost or an internal network
  • Use EndpointRequest.toAnyEndpoint() in a separate high-priority SecurityFilterChain for 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.