// Security Context
org.springframework.boot.actuate.endpoint.SecurityContext
// Authentication Audit
org.springframework.boot.actuate.security.AbstractAuthenticationAuditListener
org.springframework.boot.actuate.security.AuthenticationAuditListener
// Authorization Audit
org.springframework.boot.actuate.security.AbstractAuthorizationAuditListener
org.springframework.boot.actuate.security.AuthorizationAuditListener
// Audit Events
org.springframework.boot.actuate.audit.AuditEvent
org.springframework.boot.actuate.audit.AuditEventRepositoryActuator endpoint security needs?
│
├─ Public endpoints (health, info)?
│ └─ .requestMatchers("/actuator/health", "/actuator/info").permitAll()
│
├─ Admin-only endpoints (env, loggers, shutdown)?
│ └─ .requestMatchers("/actuator/**").hasRole("ADMIN")
│
├─ Monitoring role (metrics, httpexchanges)?
│ └─ .requestMatchers("/actuator/metrics").hasAnyRole("ADMIN", "MONITOR")
│
└─ Custom endpoint-specific security?
└─ Use SecurityContext in operation methods| Endpoint | Suggested Role | Risk Level | Reason |
|---|---|---|---|
| health | PUBLIC | Low | Non-sensitive status |
| info | PUBLIC | Low | Build metadata |
| metrics | MONITOR/ADMIN | Medium | Performance data |
| env | ADMIN | High | Contains credentials |
| configprops | ADMIN | High | Contains credentials |
| loggers | ADMIN | High | Can modify log levels |
| heapdump | ADMIN | Critical | Contains all memory |
| threaddump | ADMIN | Medium | Shows internal state |
| shutdown | ADMIN | Critical | Stops application |
| auditevents | ADMIN | High | Security events |
// Authentication events
AuthenticationAuditListener.AUTHENTICATION_SUCCESS
AuthenticationAuditListener.AUTHENTICATION_FAILURE
AuthenticationAuditListener.AUTHENTICATION_SWITCH
AuthenticationAuditListener.LOGOUT_SUCCESS
// Authorization events
AuthorizationAuditListener.AUTHORIZATION_FAILUREAlways secure:
Consider public access for:
Never expose publicly:
PATTERN: Layered security
// ✓ Multiple security layers
@Configuration
@EnableWebSecurity
public class ActuatorSecurityConfig {
@Bean
public SecurityFilterChain actuatorSecurity(HttpSecurity http) throws Exception {
http
.securityMatcher("/actuator/**")
.authorizeHttpRequests(auth -> auth
// Public
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/actuator/info").permitAll()
// Monitoring role
.requestMatchers("/actuator/metrics").hasAnyRole("MONITOR", "ADMIN")
.requestMatchers("/actuator/prometheus").hasAnyRole("MONITOR", "ADMIN")
// Admin only
.requestMatchers("/actuator/env").hasRole("ADMIN")
.requestMatchers("/actuator/loggers").hasRole("ADMIN")
.requestMatchers("/actuator/heapdump").hasRole("ADMIN")
// Require authentication for all others
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
}ANTI-PATTERN: All or nothing
// ❌ Either everything is public or everything is locked down
@Bean
public SecurityFilterChain actuatorSecurity(HttpSecurity http) {
http.securityMatcher("/actuator/**")
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll() // ❌ All endpoints public!
);
return http.build();
}PATTERN: Role-based endpoint operations
// ✓ Check roles within operation
@ReadOperation
public EnvironmentDescriptor environment(
SecurityContext securityContext,
@Nullable String pattern) {
// Show sanitized values unless user has ADMIN role
boolean showUnsanitized = securityContext.isUserInRole("ADMIN");
return delegate.environment(pattern, showUnsanitized);
}ANTI-PATTERN: No role checking
// ❌ Always shows sensitive data
@ReadOperation
public EnvironmentDescriptor environment(@Nullable String pattern) {
return delegate.environment(pattern, true); // Always unsanitized!
}PATTERN: Audit security events
// ✓ Automatic security event auditing
@Bean
public AuthenticationAuditListener authenticationAuditListener() {
return new AuthenticationAuditListener();
}
@Bean
public AuthorizationAuditListener authorizationAuditListener() {
return new AuthorizationAuditListener();
}Security integration provides security context abstraction and audit listeners for authentication and authorization events with role-based access control and Spring Security integration.
/**
* Security context for endpoint invocation.
*
* Thread-safe: Immutable
* Nullability: Principal can be null (anonymous)
* Since: Spring Boot 2.0+
*/
public interface SecurityContext {
/**
* Empty security context (no authentication).
*/
SecurityContext NONE = /* empty implementation */;
/**
* Get authenticated principal.
*
* @return Principal or null if not authenticated
*/
@Nullable Principal getPrincipal();
/**
* Check if principal has role.
*
* @param role Role name (without ROLE_ prefix)
* @return true if principal has role
*/
boolean isUserInRole(String role);
}/**
* Base class for authentication audit listeners.
*
* Thread-safe: Yes
* Event publishing: Asynchronous
* Since: Spring Boot 1.0+
*/
public abstract class AbstractAuthenticationAuditListener
implements ApplicationListener<AbstractAuthenticationEvent>,
ApplicationEventPublisherAware {
void setApplicationEventPublisher(ApplicationEventPublisher publisher);
protected ApplicationEventPublisher getPublisher();
protected void publish(AuditEvent event);
}
/**
* Audit listener for authentication events.
*
* Captures:
* - Login success/failure
* - Logout
* - User switching
*
* Thread-safe: Yes
* Auto-configured: No (manual registration)
* Since: Spring Boot 1.0+
*/
public class AuthenticationAuditListener
extends AbstractAuthenticationAuditListener {
/** Successful authentication */
public static final String AUTHENTICATION_SUCCESS = "AUTHENTICATION_SUCCESS";
/** Failed authentication */
public static final String AUTHENTICATION_FAILURE = "AUTHENTICATION_FAILURE";
/** User switch (switch user) */
public static final String AUTHENTICATION_SWITCH = "AUTHENTICATION_SWITCH";
/** Logout */
public static final String LOGOUT_SUCCESS = "LOGOUT_SUCCESS";
@Override
public void onApplicationEvent(AbstractAuthenticationEvent event);
}/**
* Base class for authorization audit listeners.
*
* Thread-safe: Yes
* Event publishing: Asynchronous
* Since: Spring Boot 2.1+
*/
public abstract class AbstractAuthorizationAuditListener
implements ApplicationListener<AuthorizationEvent>,
ApplicationEventPublisherAware {
void setApplicationEventPublisher(ApplicationEventPublisher publisher);
protected ApplicationEventPublisher getPublisher();
protected void publish(AuditEvent event);
}
/**
* Audit listener for authorization events.
*
* Captures:
* - Access denied events
* - Authorization failures
*
* Thread-safe: Yes
* Auto-configured: No (manual registration)
* Since: Spring Boot 2.1+
*/
public class AuthorizationAuditListener
extends AbstractAuthorizationAuditListener {
/** Authorization failure (access denied) */
public static final String AUTHORIZATION_FAILURE = "AUTHORIZATION_FAILURE";
@Override
public void onApplicationEvent(AuthorizationEvent event);
}package com.example.actuator;
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.boot.actuate.security.AuthenticationAuditListener;
import org.springframework.boot.actuate.security.AuthorizationAuditListener;
import org.springframework.boot.actuate.audit.AuditEventRepository;
import org.springframework.boot.actuate.audit.InMemoryAuditEventRepository;
import org.springframework.boot.actuate.audit.listener.AuditListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.Customizer;
import org.springframework.security.web.SecurityFilterChain;
/**
* Security configuration for actuator endpoints.
*
* Thread-safe: Yes
* Security model: Role-based access control
* Since: Application 1.0
*/
@Configuration
public class ActuatorSecurityConfiguration {
/**
* Configure actuator endpoint security.
*
* Roles:
* - PUBLIC: health, info
* - MONITOR: metrics, httpexchanges
* - ADMIN: env, loggers, heapdump, threaddump, shutdown
*
* @param http HTTP security
* @return Security filter chain
*/
@Bean
public SecurityFilterChain actuatorSecurity(HttpSecurity http) throws Exception {
http
.securityMatcher("/actuator/**")
.authorizeHttpRequests(authorize -> authorize
// Public endpoints
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/actuator/health/**").permitAll()
.requestMatchers("/actuator/info").permitAll()
// Monitoring endpoints
.requestMatchers("/actuator/metrics").hasAnyRole("MONITOR", "ADMIN")
.requestMatchers("/actuator/metrics/**").hasAnyRole("MONITOR", "ADMIN")
.requestMatchers("/actuator/httpexchanges").hasAnyRole("MONITOR", "ADMIN")
.requestMatchers("/actuator/prometheus").hasAnyRole("MONITOR", "ADMIN")
// Admin-only configuration endpoints
.requestMatchers("/actuator/env").hasRole("ADMIN")
.requestMatchers("/actuator/env/**").hasRole("ADMIN")
.requestMatchers("/actuator/configprops").hasRole("ADMIN")
.requestMatchers("/actuator/configprops/**").hasRole("ADMIN")
// Admin-only operational endpoints
.requestMatchers("/actuator/loggers").hasRole("ADMIN")
.requestMatchers("/actuator/loggers/**").hasRole("ADMIN")
.requestMatchers("/actuator/heapdump").hasRole("ADMIN")
.requestMatchers("/actuator/threaddump").hasRole("ADMIN")
.requestMatchers("/actuator/shutdown").hasRole("ADMIN")
// All other actuator endpoints require authentication
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
/**
* Configure authentication audit listener.
* Captures login, logout, and authentication failures.
*
* @return Authentication audit listener
*/
@Bean
public AuthenticationAuditListener authenticationAuditListener() {
return new AuthenticationAuditListener();
}
/**
* Configure authorization audit listener.
* Captures access denied events.
*
* @return Authorization audit listener
*/
@Bean
public AuthorizationAuditListener authorizationAuditListener() {
return new AuthorizationAuditListener();
}
/**
* Configure audit event repository.
*
* @return In-memory audit repository
*/
@Bean
public AuditEventRepository auditEventRepository() {
return new InMemoryAuditEventRepository(10000);
}
/**
* Configure audit listener.
*
* @param repository Audit repository
* @return Audit listener
*/
@Bean
public AuditListener auditListener(AuditEventRepository repository) {
return new AuditListener(repository);
}
}
// Configuration in application.yml
/*
management:
endpoints:
web:
exposure:
include: health,info,metrics,env,loggers,auditevents
endpoint:
health:
show-details: when-authorized
roles: ADMIN,MONITOR
env:
show-values: when-authorized
roles: ADMIN
configprops:
show-values: when-authorized
roles: ADMIN
spring:
security:
user:
name: admin
password: ${ADMIN_PASSWORD:changeme}
roles: ADMIN
*/package com.example.actuator;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.stereotype.Component;
import org.jspecify.annotations.Nullable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Custom endpoint with role-based operations.
*
* Thread-safe: Yes
* Security: Role-based access control
* Since: Application 1.0
*/
@Endpoint(id = "appcache")
@Component
public class ApplicationCacheEndpoint {
private final Map<String, Object> cache = new ConcurrentHashMap<>();
/**
* Get cache statistics (available to all authenticated users).
*
* @param securityContext Security context
* @return Cache statistics
*/
@ReadOperation
public Map<String, Object> getStats(SecurityContext securityContext) {
// Verify authentication
if (securityContext.getPrincipal() == null) {
throw new SecurityException("Authentication required");
}
return Map.of(
"size", cache.size(),
"user", securityContext.getPrincipal().getName()
);
}
/**
* Get cache entry (admins see values, others see keys only).
*
* @param key Cache key
* @param securityContext Security context
* @return Cache entry
*/
@ReadOperation
public Map<String, Object> getEntry(
@Selector String key,
SecurityContext securityContext) {
Object value = cache.get(key);
if (value == null) {
return Map.of("exists", false);
}
// Show value only to admins
if (securityContext.isUserInRole("ADMIN")) {
return Map.of(
"key", key,
"value", value,
"exists", true
);
} else {
return Map.of(
"key", key,
"exists", true,
"message", "Value hidden (admin role required)"
);
}
}
/**
* Update cache entry (admin only).
*
* @param key Cache key
* @param value New value
* @param securityContext Security context
*/
@WriteOperation
public void updateEntry(
@Selector String key,
String value,
SecurityContext securityContext) {
// Require ADMIN role
if (!securityContext.isUserInRole("ADMIN")) {
throw new SecurityException("ADMIN role required");
}
cache.put(key, value);
}
}package com.example.actuator;
import org.springframework.boot.actuate.audit.AuditEvent;
import org.springframework.boot.actuate.audit.AuditEventRepository;
import org.springframework.boot.actuate.security.AuthenticationAuditListener;
import org.springframework.boot.actuate.security.AuthorizationAuditListener;
import org.springframework.stereotype.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Service for monitoring security events.
*
* Thread-safe: Yes
* Performance: Query-based (caching recommended)
* Since: Application 1.0
*/
@Service
public class SecurityMonitoringService {
private static final Logger logger = LoggerFactory.getLogger(SecurityMonitoringService.class);
private final AuditEventRepository auditRepository;
public SecurityMonitoringService(AuditEventRepository auditRepository) {
this.auditRepository = auditRepository;
}
/**
* Get recent failed login attempts.
*
* @param minutes Time window in minutes
* @return List of failed login events
*/
public List<AuditEvent> getRecentFailedLogins(int minutes) {
Instant cutoff = Instant.now().minus(Duration.ofMinutes(minutes));
return auditRepository.find(
null, // all users
cutoff,
AuthenticationAuditListener.AUTHENTICATION_FAILURE
);
}
/**
* Detect brute force attack attempts.
*
* @param threshold Number of failures to trigger alert
* @param minutes Time window
* @return Map of users to failure counts
*/
public Map<String, Long> detectBruteForce(int threshold, int minutes) {
List<AuditEvent> failures = getRecentFailedLogins(minutes);
Map<String, Long> failuresByUser = failures.stream()
.collect(Collectors.groupingBy(
AuditEvent::getPrincipal,
Collectors.counting()
));
// Log and alert on suspicious activity
failuresByUser.forEach((user, count) -> {
if (count >= threshold) {
logger.warn("SECURITY ALERT: {} failed login attempts for user: {}",
count, user);
alertSecurityTeam(user, count);
}
});
return failuresByUser;
}
/**
* Get authorization failures (access denied).
*
* @param minutes Time window
* @return List of authorization failures
*/
public List<AuditEvent> getAuthorizationFailures(int minutes) {
Instant cutoff = Instant.now().minus(Duration.ofMinutes(minutes));
return auditRepository.find(
null,
cutoff,
AuthorizationAuditListener.AUTHORIZATION_FAILURE
);
}
/**
* Get successful logins by user.
*
* @param minutes Time window
* @return Map of users to login counts
*/
public Map<String, Long> getSuccessfulLoginsByUser(int minutes) {
Instant cutoff = Instant.now().minus(Duration.ofMinutes(minutes));
List<AuditEvent> successes = auditRepository.find(
null,
cutoff,
AuthenticationAuditListener.AUTHENTICATION_SUCCESS
);
return successes.stream()
.collect(Collectors.groupingBy(
AuditEvent::getPrincipal,
Collectors.counting()
));
}
/**
* Get security event summary.
*
* @param minutes Time window
* @return Summary of security events
*/
public SecuritySummary getSecuritySummary(int minutes) {
Instant cutoff = Instant.now().minus(Duration.ofMinutes(minutes));
long authSuccesses = auditRepository.find(
null, cutoff,
AuthenticationAuditListener.AUTHENTICATION_SUCCESS
).size();
long authFailures = auditRepository.find(
null, cutoff,
AuthenticationAuditListener.AUTHENTICATION_FAILURE
).size();
long authzFailures = auditRepository.find(
null, cutoff,
AuthorizationAuditListener.AUTHORIZATION_FAILURE
).size();
return new SecuritySummary(
authSuccesses,
authFailures,
authzFailures,
minutes
);
}
private void alertSecurityTeam(String user, long failureCount) {
// Implementation: send email, Slack notification, etc.
logger.error("SECURITY ALERT sent for user {} ({} failures)", user, failureCount);
}
public record SecuritySummary(
long successfulLogins,
long failedLogins,
long authorizationFailures,
int timeWindowMinutes
) {}
}package com.example.actuator;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import java.security.Principal;
import java.util.Map;
import static org.assertj.core.api.Assertions.*;
class ApplicationCacheEndpointTest {
private ApplicationCacheEndpoint endpoint;
@BeforeEach
void setUp() {
endpoint = new ApplicationCacheEndpoint();
}
@Test
void getStats_WithAuthentication_ReturnsStats() {
SecurityContext ctx = createSecurityContext("user", false);
Map<String, Object> stats = endpoint.getStats(ctx);
assertThat(stats).containsKeys("size", "user");
}
@Test
void getStats_WithoutAuthentication_ThrowsException() {
SecurityContext ctx = SecurityContext.NONE;
assertThatThrownBy(() -> endpoint.getStats(ctx))
.isInstanceOf(SecurityException.class)
.hasMessageContaining("Authentication required");
}
@Test
void updateEntry_WithAdminRole_Succeeds() {
SecurityContext ctx = createSecurityContext("admin", true);
assertThatNoException().isThrownBy(() ->
endpoint.updateEntry("key1", "value1", ctx)
);
}
@Test
void updateEntry_WithoutAdminRole_ThrowsException() {
SecurityContext ctx = createSecurityContext("user", false);
assertThatThrownBy(() ->
endpoint.updateEntry("key1", "value1", ctx)
)
.isInstanceOf(SecurityException.class)
.hasMessageContaining("ADMIN role required");
}
private SecurityContext createSecurityContext(String username, boolean isAdmin) {
return new SecurityContext() {
@Override
public Principal getPrincipal() {
return () -> username;
}
@Override
public boolean isUserInRole(String role) {
return isAdmin && "ADMIN".equals(role);
}
};
}
}Problem: All actuator endpoints return 401
Causes:
Solutions:
# Solution 1: Configure credentials
spring:
security:
user:
name: admin
password: admin123
roles: ADMIN
# Solution 2: Disable security (dev only!)
spring:
security:
enabled: false
# Solution 3: Check authentication
# Use Basic Auth: admin:admin123Problem: Authenticated but access denied
Causes:
Solutions:
// Check role in security config matches user role
.requestMatchers("/actuator/env").hasRole("ADMIN")
// User must have ROLE_ADMIN (or role: ADMIN in config)
// Check role-based endpoint config
management:
endpoint:
env:
roles: ADMIN // Must match user's roleProblem: Security events not appearing
Causes:
Solutions:
// Register audit listeners
@Bean
public AuthenticationAuditListener authenticationAuditListener() {
return new AuthenticationAuditListener();
}
@Bean
public AuthorizationAuditListener authorizationAuditListener() {
return new AuthorizationAuditListener();
}
// Configure repository
@Bean
public AuditEventRepository auditEventRepository() {
return new InMemoryAuditEventRepository();
}
// Register AuditListener
@Bean
public AuditListener auditListener(AuditEventRepository repository) {
return new AuditListener(repository);
}// Audit events are published asynchronously
// No performance impact on authentication/authorization
// For high-traffic applications:
// 1. Use persistent repository (avoid memory)
// 2. Add indexes for queries
// 3. Archive old events periodically
@Scheduled(cron = "0 0 0 * * *") // Daily at midnight
public void archiveOldAuditEvents() {
Instant cutoff = Instant.now().minus(Duration.ofDays(90));
// Archive events older than 90 days
}// SecurityContext resolution is lightweight (~0.1ms)
// Safe to use in endpoint operations
@ReadOperation
public Data getData(SecurityContext ctx) {
// Context resolution is fast
if (ctx.isUserInRole("ADMIN")) {
return getAdminData();
}
return getUserData();
}