ActuatorSecurityFilter.java
package com.taxonomy.security.config;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
/**
* Protects the Actuator discovery root and sensitive Actuator endpoints with the
* dedicated machine-token mechanism.
*
* <ul>
* <li>{@code /actuator/health}, {@code /actuator/health/**}, and
* {@code /actuator/info} are public for platform probes and non-sensitive
* deployment metadata.</li>
* <li>The exact {@code /actuator} discovery root and every other
* {@code /actuator/**} path are machine-token protected when an
* {@code admin.token} value (typically {@code ADMIN_TOKEN}) is configured.</li>
* <li>Monitoring clients may use either {@code X-Admin-Token} or one strict
* {@code Authorization: Bearer <token>} value.</li>
* <li>When no machine token is configured, this filter defers to the ordinary
* local-user or Keycloak authenticated-user security chain.</li>
* </ul>
*
* <p>This filter is registered explicitly after Spring Security's delegating
* filter proxy by {@link ActuatorAdminTokenSecurityConfig}; it is not a
* component-scanned servlet filter.</p>
*/
public final class ActuatorSecurityFilter extends OncePerRequestFilter {
static final int MAX_TOKEN_CANDIDATE_LENGTH = 512;
static final String UNAUTHORIZED_CODE = "ACTUATOR_MACHINE_TOKEN_REQUIRED";
static final String REGISTRATION_NAME = "actuatorSecurityFilterRegistration";
private static final String LEGACY_TOKEN_HEADER = "X-Admin-Token";
private static final String BEARER_PREFIX = "Bearer ";
private static final String UNAUTHORIZED_MESSAGE =
"A valid Actuator machine token is required.";
private final String adminToken;
ActuatorSecurityFilter(String adminToken) {
this.adminToken = adminToken == null ? "" : adminToken;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String path = applicationPath(request);
if (!isActuatorPath(path) || isPublicActuatorPath(path)) {
filterChain.doFilter(request, response);
return;
}
// Without a machine token, defer to the ordinary authenticated-user rule.
if (adminToken.isBlank()) {
filterChain.doFilter(request, response);
return;
}
if (matchesToken(request.getHeader(LEGACY_TOKEN_HEADER))
|| matchesBearerToken(request.getHeader(HttpHeaders.AUTHORIZATION))) {
filterChain.doFilter(request, response);
return;
}
writeUnauthorized(response);
}
static boolean isSensitiveActuatorPath(HttpServletRequest request) {
String path = applicationPath(request);
return isActuatorPath(path) && !isPublicActuatorPath(path);
}
static String applicationPath(HttpServletRequest request) {
String path = request.getRequestURI();
String contextPath = request.getContextPath();
if (contextPath != null
&& !contextPath.isBlank()
&& path.startsWith(contextPath)) {
return path.substring(contextPath.length());
}
return path;
}
private static boolean isActuatorPath(String path) {
return "/actuator".equals(path) || path.startsWith("/actuator/");
}
private static boolean isPublicActuatorPath(String path) {
return path.equals("/actuator/health")
|| path.startsWith("/actuator/health/")
|| path.equals("/actuator/info");
}
private boolean matchesBearerToken(String authorization) {
if (authorization == null
|| authorization.length()
> BEARER_PREFIX.length() + MAX_TOKEN_CANDIDATE_LENGTH
|| !authorization.regionMatches(
true, 0, BEARER_PREFIX, 0, BEARER_PREFIX.length())) {
return false;
}
String candidate = authorization.substring(BEARER_PREFIX.length());
if (candidate.isEmpty()
|| candidate.chars().anyMatch(Character::isWhitespace)) {
return false;
}
return matchesToken(candidate);
}
private boolean matchesToken(String candidate) {
if (candidate == null
|| candidate.isEmpty()
|| candidate.length() > MAX_TOKEN_CANDIDATE_LENGTH) {
return false;
}
return MessageDigest.isEqual(
adminToken.getBytes(StandardCharsets.UTF_8),
candidate.getBytes(StandardCharsets.UTF_8));
}
private static void writeUnauthorized(HttpServletResponse response)
throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setHeader(HttpHeaders.CACHE_CONTROL, "no-store");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write("{\"status\":401,\"code\":\""
+ UNAUTHORIZED_CODE
+ "\",\"error\":\""
+ UNAUTHORIZED_MESSAGE
+ "\"}");
}
}