or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

audit.mdbuiltin-endpoints.mdendpoint-framework.mdhttp-exchanges.mdindex.mdinfo-contributors.mdjmx-integration.mdmanagement-operations.mdoperation-invocation.mdsanitization.mdsecurity.mdweb-integration.md
tile.json

security.mddocs/

Security Integration

QUICK REFERENCE

Key Classes and Packages

// 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.AuditEventRepository

Security Configuration Patterns

Actuator 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

Role Setup Guide

EndpointSuggested RoleRisk LevelReason
healthPUBLICLowNon-sensitive status
infoPUBLICLowBuild metadata
metricsMONITOR/ADMINMediumPerformance data
envADMINHighContains credentials
configpropsADMINHighContains credentials
loggersADMINHighCan modify log levels
heapdumpADMINCriticalContains all memory
threaddumpADMINMediumShows internal state
shutdownADMINCriticalStops application
auditeventsADMINHighSecurity events

Event Type Constants

// Authentication events
AuthenticationAuditListener.AUTHENTICATION_SUCCESS
AuthenticationAuditListener.AUTHENTICATION_FAILURE
AuthenticationAuditListener.AUTHENTICATION_SWITCH
AuthenticationAuditListener.LOGOUT_SUCCESS

// Authorization events
AuthorizationAuditListener.AUTHORIZATION_FAILURE

AGENT GUIDANCE

Security Best Practices

Always secure:

  • Production actuator endpoints
  • Endpoints exposing configuration
  • Endpoints allowing modifications
  • Endpoints downloading files
  • Shutdown endpoint

Consider public access for:

  • Health checks (for load balancers)
  • Info endpoint (build version)
  • Prometheus metrics (in private network)

Never expose publicly:

  • Environment variables
  • Configuration properties
  • Heap/thread dumps
  • Shutdown endpoint
  • Logger configuration

Pattern vs Anti-Pattern

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.

Capabilities

Security Context

/**
 * 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);
}

Authentication Audit Listeners

/**
 * 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);
}

Authorization Audit Listeners

/**
 * 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);
}

COMPLETE WORKING EXAMPLES

Example 1: Complete Actuator Security Configuration

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
*/

Example 2: Custom Security Context Usage

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);
    }
}

Example 3: Security Monitoring Service

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
    ) {}
}

TESTING EXAMPLES

Test 1: Security Context Usage

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);
            }
        };
    }
}

TROUBLESHOOTING

Common Error: 401 Unauthorized

Problem: All actuator endpoints return 401

Causes:

  1. Spring Security auto-configured without credentials
  2. Missing authentication
  3. Wrong credentials

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:admin123

Common Error: 403 Forbidden

Problem: Authenticated but access denied

Causes:

  1. Missing required role
  2. Wrong role name
  3. Security configuration mismatch

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 role

Common Error: Audit Events Not Recorded

Problem: Security events not appearing

Causes:

  1. Audit listeners not registered
  2. Repository not configured
  3. Events not published by Spring Security

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);
}

PERFORMANCE NOTES

Audit Event Performance

// 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
}

Security Context Overhead

// 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();
}

Cross-References

  • For audit framework: Audit Framework
  • For endpoint basics: Endpoint Framework
  • For data sanitization: Data Sanitization
  • For built-in endpoints: Built-in Endpoints