ActuatorAdminTokenSecurityConfig.java
package com.taxonomy.security.config;
import jakarta.servlet.DispatcherType;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.security.autoconfigure.web.servlet.SecurityFilterProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.RequestMatcher;
/**
* Lets the dedicated {@link ActuatorSecurityFilter} authenticate non-browser
* monitoring clients without weakening the ordinary form-login or Keycloak
* authorization rules.
*
* <p>When no admin token is configured this chain matches nothing, so sensitive
* Actuator endpoints retain the normal authenticated-user requirement. When a
* token is configured, the exact {@code /actuator} discovery root and all
* non-public {@code /actuator/**} paths enter this permit-all chain; the servlet
* filter remains the authoritative token validator.</p>
*
* <p>The servlet filter has one explicit registration immediately after Spring
* Security's delegating filter proxy. That makes authorization select the
* correct chain first and prevents component scanning from registering the
* machine-token filter at an implicit or duplicate order.</p>
*
* <p>CSRF protection remains enabled. The currently exposed token-authenticated
* Actuator endpoints are read-only; if a write-capable endpoint is exposed later,
* it must also satisfy Spring Security's CSRF protection.</p>
*/
@Configuration(proxyBeanMethods = false)
public class ActuatorAdminTokenSecurityConfig {
static final int ACTUATOR_FILTER_ORDER =
SecurityFilterProperties.DEFAULT_FILTER_ORDER + 1;
@Bean
ActuatorSecurityFilter actuatorSecurityFilter(
@Value("${admin.token:}") String adminToken) {
return new ActuatorSecurityFilter(adminToken);
}
@Bean(name = ActuatorSecurityFilter.REGISTRATION_NAME)
FilterRegistrationBean<ActuatorSecurityFilter>
actuatorSecurityFilterRegistration(ActuatorSecurityFilter filter) {
FilterRegistrationBean<ActuatorSecurityFilter> registration =
new FilterRegistrationBean<>(filter);
registration.setName(ActuatorSecurityFilter.REGISTRATION_NAME);
registration.addUrlPatterns("/*");
registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setOrder(ACTUATOR_FILTER_ORDER);
return registration;
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
SecurityFilterChain actuatorAdminTokenSecurityFilterChain(
HttpSecurity http,
@Value("${admin.token:}") String adminToken) throws Exception {
RequestMatcher tokenProtectedActuator = request ->
adminToken != null
&& !adminToken.isBlank()
&& ActuatorSecurityFilter.isSensitiveActuatorPath(request);
http.securityMatcher(tokenProtectedActuator)
.authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll());
return http.build();
}
}