Spring Boot Actuator Security Headers Missing - X-Content-Type-Options Not Set
I'm using Spring Boot Actuator endpoints in production, but security scanning tools are reporting missing security headers like X-Content-Type-Options
, X-Frame-Options
, and Strict-Transport-Security
. The actuator endpoints are returning 200 responses but without proper security headers, which is a security risk. How can I add security headers to Spring Boot Actuator endpoints?
Solution
Spring Boot Actuator endpoints don't automatically inherit security headers from your main application security configuration. You need to explicitly configure security headers for actuator endpoints.
Here's how to fix it:
- Configure security headers in your security configuration:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/actuator/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.headers(headers -> headers
.frameOptions().DENY
.contentTypeOptions().and()
.httpStrictTransportSecurity(hstsConfig -> hstsConfig
.maxAgeInSeconds(31536000)
.includeSubdomains(true)
)
.addHeaderWriter(new StaticHeadersWriter("X-Content-Type-Options", "nosniff"))
.addHeaderWriter(new StaticHeadersWriter("X-Frame-Options", "DENY"))
.addHeaderWriter(new StaticHeadersWriter("X-XSS-Protection", "1; mode=block"))
)
.csrf(csrf -> csrf.disable());
return http.build();
}
}
- Alternative: Use a dedicated security configuration for actuator:
@Configuration
@Order(1)
public class ActuatorSecurityConfig {
@Bean
public SecurityFilterChain actuatorFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/actuator/**")
.authorizeHttpRequests(authz -> authz
.requestMatchers("/actuator/health").permitAll()
.anyRequest().hasRole("ADMIN")
)
.headers(headers -> headers
.frameOptions().DENY
.contentTypeOptions().and()
.httpStrictTransportSecurity(hstsConfig -> hstsConfig
.maxAgeInSeconds(31536000)
.includeSubdomains(true)
)
);
return http.build();
}
}
Key points:
- Actuator endpoints need explicit security header configuration
- Use
securityMatcher
to target only actuator endpoints - Configure appropriate authorization for sensitive endpoints
- Apply security headers consistently across all actuator endpoints
This ensures your actuator endpoints have proper security headers while maintaining functionality.
Alternative #1
If you want to apply security headers globally to all endpoints including actuator, you can use a WebMvcConfigurer
:
@Configuration
public class SecurityHeadersConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new HandlerInterceptor() {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader("X-Frame-Options", "DENY");
response.setHeader("X-XSS-Protection", "1; mode=block");
response.setHeader("Strict-Transport-Security",
"max-age=31536000; includeSubDomains");
response.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
return true;
}
});
}
}
This approach:
- Applies headers to all endpoints automatically
- Works with both regular and actuator endpoints
- Easy to maintain and update
- Consistent security headers across the application
Alternative #2
For more granular control over different actuator endpoints, you can use endpoint-specific security configurations:
@Configuration
public class ActuatorSecurityConfig {
@Bean
public SecurityFilterChain actuatorFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/actuator/**")
.authorizeHttpRequests(authz -> authz
.requestMatchers("/actuator/health", "/actuator/info").permitAll()
.requestMatchers("/actuator/metrics/**").hasRole("MONITOR")
.requestMatchers("/actuator/env", "/actuator/configprops").hasRole("ADMIN")
.anyRequest().denyAll()
)
.headers(headers -> headers
.frameOptions().DENY
.contentTypeOptions().and()
.httpStrictTransportSecurity(hstsConfig -> hstsConfig
.maxAgeInSeconds(31536000)
.includeSubdomains(true)
)
.addHeaderWriter(new StaticHeadersWriter("Cache-Control", "no-cache, no-store, must-revalidate"))
.addHeaderWriter(new StaticHeadersWriter("Pragma", "no-cache"))
.addHeaderWriter(new StaticHeadersWriter("Expires", "0"))
);
return http.build();
}
}
This approach:
- Provides different access levels for different endpoints
- Adds cache control headers for sensitive endpoints
- Denies access to unknown actuator endpoints
- Maintains security while allowing necessary monitoring
Alternative #3
If you're using Spring Boot 3.x with the new security model, you can leverage the @ConditionalOnProperty
annotation for environment-specific configurations:
@Configuration
@ConditionalOnProperty(name = "management.endpoints.web.exposure.include")
public class ProductionActuatorSecurityConfig {
@Bean
@ConditionalOnProperty(name = "spring.profiles.active", havingValue = "prod")
public SecurityFilterChain productionActuatorFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/actuator/**")
.authorizeHttpRequests(authz -> authz
.requestMatchers("/actuator/health").permitAll()
.anyRequest().hasRole("ADMIN")
)
.headers(headers -> headers
.frameOptions().DENY
.contentTypeOptions().and()
.httpStrictTransportSecurity(hstsConfig -> hstsConfig
.maxAgeInSeconds(31536000)
.includeSubdomains(true)
)
.addHeaderWriter(new StaticHeadersWriter("X-Content-Type-Options", "nosniff"))
.addHeaderWriter(new StaticHeadersWriter("X-Frame-Options", "DENY"))
.addHeaderWriter(new StaticHeadersWriter("X-XSS-Protection", "1; mode=block"))
.addHeaderWriter(new StaticHeadersWriter("Referrer-Policy", "strict-origin-when-cross-origin"))
);
return http.build();
}
}
This approach:
- Only applies strict security in production
- Allows more relaxed security in development
- Uses Spring Boot's conditional configuration
- Maintains security best practices in production