Since the WebSecurityConfigurerAdapter
was deprecated and removed in Spring Boot 3, I'm having to update some of our applications running on Spring Boot.
In order to Monitor these, we have a Spring Boot Admin server and expose the actuator and the Prometheus scraper Endpoints on most Clients.
To ensure a consistent Configuration and avoid duplication we've had a custom library facilitating http security for the Actuator endpoints and optionally auto-configuring security for the Prometheus Endpoint. This was also to make sure, developers could focus on the security of their own Application.
Following code is from the Library.
@Configuration
@Order(ACTUATOR_FILTER_ORDER)
@EnableConfigurationProperties(SpringBootAdminActuatorWebSecurityProperties::class)
public class SpringBootAdminActuatorWebSecurityConfigurerAdapter(
private val properties: SpringBootAdminActuatorWebSecurityProperties,
private val environment: ConfigurableEnvironment
) : WebSecurityConfigurerAdapter() {
internal val actuatorEndpointAccessPassword: String = Base64StringKeyGenerator(KEY_LENGTH).generateKey()
override fun configure(auth: AuthenticationManagerBuilder) {
val encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder()
auth.inMemoryAuthentication()
.withUser(properties.actuatorEndpointAccessUser)
.password(encoder.encode(actuatorEndpointAccessPassword))
.roles(properties.actuatorEndpointAccessRole)
// Note: unencrypted pw in memory was deemed acceptable here
environment.propertySources.addLast(
MapPropertySource(
"spring-boot-admin-metadata",
mapOf(
"spring.boot.admin.client.instance.metadata.user.name" to properties.actuatorEndpointAccessUser,
"spring.boot.admin.client.instance.metadata.user.password" to actuatorEndpointAccessPassword
)
)
)
}
override fun configure(http: HttpSecurity) {
http
.requestMatcher(EndpointRequest.toAnyEndpoint())
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic()
.and()
.authorizeRequests()
.requestMatchers(EndpointRequest.to(HealthEndpoint::class.java)).permitAll()
.requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole(properties.actuatorEndpointAccessRole)
}
public companion object {
private const val KEY_LENGTH = 64
public const val ACTUATOR_FILTER_ORDER: Int = SecurityProperties.BASIC_AUTH_ORDER - 10
public const val DEFAULT_ACTUATOR_ACCESS_ROLE: String = "ACTUATOR_ENDPOINT_ADMIN"
}
}
@Validated
@ConstructorBinding
@ConfigurationProperties("custom.spring.utilities.security.actuator")
public data class SpringBootAdminActuatorWebSecurityProperties(
@NotBlank
val actuatorEndpointAccessUser: String = "actuator-endpoint-user",
@NotBlank
val actuatorEndpointAccessRole: String = DEFAULT_ACTUATOR_ACCESS_ROLE
)
@Configuration
@Order(PROMETHEUS_FILTER_ORDER)
@EnableConfigurationProperties(PrometheusActuatorWebSecurityProperties::class)
public class PrometheusActuatorWebSecurityConfigurerAdapter(
private val properties: PrometheusActuatorWebSecurityProperties
) : WebSecurityConfigurerAdapter() {
override fun configure(auth: AuthenticationManagerBuilder) {
auth.inMemoryAuthentication()
.withUser(properties.actuatorEndpointAccessUser)
.password(properties.actuatorEndpointAccessPassword)
.roles(properties.actuatorEndpointAccessRole)
}
override fun configure(http: HttpSecurity) {
http
.requestMatcher(EndpointRequest.to((PrometheusScrapeEndpoint::class.java)))
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic()
.and()
.authorizeRequests()
.requestMatchers(EndpointRequest.to(PrometheusScrapeEndpoint::class.java))
.hasAnyRole(properties.actuatorEndpointAccessRole)
}
public companion object {
public const val PROMETHEUS_FILTER_ORDER: Int = ACTUATOR_FILTER_ORDER - 1
public const val DEFAULT_PROMETHEUS_ACCESS_ROLE: String = "PROMETHEUS_ACTUATOR_ENDPOINT_SCRAPER"
}
}
@Validated
@ConstructorBinding
@ConfigurationProperties("custom.spring.utilities.security.prometheus")
public data class PrometheusActuatorWebSecurityProperties(
@NotBlank
val actuatorEndpointAccessUser: String,
@NotEmpty
val actuatorEndpointAccessPassword: String,
@NotBlank
val actuatorEndpointAccessRole: String = DEFAULT_PROMETHEUS_ACCESS_ROLE
)
@Configuration
@ConditionalOnProperty(prefix = "custom.spring.utilities.security.prometheus", name = ["actuatorEndpointAccessUser"])
@ConditionalOnAvailableEndpoint(endpoint = PrometheusScrapeEndpoint::class)
@ConditionalOnBean(SpringBootAdminActuatorWebSecurityConfigurerAdapter::class)
@AutoConfigureAfter(SecurityAutoConfiguration::class)
@EnableConfigurationProperties(PrometheusActuatorWebSecurityProperties::class)
@Import(
PrometheusActuatorWebSecurityConfigurerAdapter::class
)
public class PrometheusActuatorWebSecurityAutoConfiguration
Following this, an application could implement it own WebSecurityConfigurerAdapter
in much the same way and Spring did its Magic.
This setup worked fine for the most part, and I wasn't made aware of any major flaws with this.
Personal attempts
So far I've tried to initially follow the migration guide. That however turned out to be inadequate for this setup.
Exposing a UserDetailsService
Bean won't work, as only one such Bean may be exposed. This would prevent independent configuration.
After too many attempts I have come up with this:
@Configuration
@Order(ACTUATOR_FILTER_ORDER)
@EnableConfigurationProperties(SpringBootAdminActuatorWebSecurityProperties::class)
@EnableWebSecurity
public class SpringBootAdminActuatorWebSecurityConfigurerAdapter(
private val properties: SpringBootAdminActuatorWebSecurityProperties,
private val environment: ConfigurableEnvironment
) {
internal val actuatorEndpointAccessPassword: String = Base64StringKeyGenerator(KEY_LENGTH).generateKey()
@Bean(ACTUATOR_AUTHENTICATION_MANAGER_BEAN_NAME)
public fun actuatorAuthenticationManager(): AuthenticationManager {
val encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder()
val user = User
.withUsername(properties.actuatorEndpointAccessUser)
.password(encoder.encode(actuatorEndpointAccessPassword))
.roles(properties.actuatorEndpointAccessRole)
.build()
val authenticationProvider = DaoAuthenticationProvider()
authenticationProvider.setUserDetailsService(InMemoryUserDetailsManager(user))
authenticationProvider.setPasswordEncoder(encoder)
// add pw to properties
return ProviderManager(authenticationProvider)
}
@Bean("actuatorFilterChain")
@Order(1)
public fun actuatorFilterChain(
http: HttpSecurity,
@Qualifier(ACTUATOR_AUTHENTICATION_MANAGER_BEAN_NAME) authenticationManager: AuthenticationManager,
): SecurityFilterChain {
http
.securityMatcher(EndpointRequest.toAnyEndpoint())
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic()
.and()
.authenticationManager(authenticationManager)
.authorizeHttpRequests()
.requestMatchers(EndpointRequest.to(HealthEndpoint::class.java)).permitAll()
.requestMatchers(EndpointRequest.toAnyEndpoint()).hasRole(properties.actuatorEndpointAccessRole)
return http.build()
}
public companion object {
private const val KEY_LENGTH = 64
public const val ACTUATOR_FILTER_ORDER: Int = SecurityProperties.BASIC_AUTH_ORDER - 10
public const val DEFAULT_ACTUATOR_ACCESS_ROLE: String = "ACTUATOR_ENDPOINT_ADMIN"
}
}
@Configuration
@Order(PROMETHEUS_FILTER_ORDER)
@EnableConfigurationProperties(PrometheusActuatorWebSecurityProperties::class)
public class PrometheusActuatorWebSecurityConfigurerAdapter(
private val properties: PrometheusActuatorWebSecurityProperties
) {
@Bean(PROMETHEUS_AUTHENTICATION_MANAGER_BEAN_NAME)
public fun prometheusActuatorAuthenticationManager(): AuthenticationManager {
val user = User
.withUsername(properties.actuatorEndpointAccessUser)
.password(properties.actuatorEndpointAccessPassword)
.roles(properties.actuatorEndpointAccessRole)
.build()
val authenticationProvider = DaoAuthenticationProvider()
authenticationProvider.setUserDetailsService(InMemoryUserDetailsManager(user))
return ProviderManager(authenticationProvider)
}
@Bean
@Order(0)
public fun prometheusActuatorFilterChain(
http: HttpSecurity,
@Qualifier(PROMETHEUS_AUTHENTICATION_MANAGER_BEAN_NAME) authenticationManager: AuthenticationManager,
): SecurityFilterChain {
http
.securityMatcher(EndpointRequest.to((PrometheusScrapeEndpoint::class.java)))
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic()
.and()
.authenticationManager(authenticationManager)
.authorizeHttpRequests()
.requestMatchers(EndpointRequest.to(PrometheusScrapeEndpoint::class.java))
.hasAnyRole(properties.actuatorEndpointAccessRole)
return http.build()
}
public companion object {
public const val PROMETHEUS_FILTER_ORDER: Int = ACTUATOR_FILTER_ORDER - 1
public const val DEFAULT_PROMETHEUS_ACCESS_ROLE: String = "PROMETHEUS_ACTUATOR_ENDPOINT_SCRAPER"
}
}
@Configuration
@ConditionalOnProperty(prefix = "custom.spring.utilities.security.prometheus", name = ["actuatorEndpointAccessUser"])
@ConditionalOnAvailableEndpoint(endpoint = PrometheusScrapeEndpoint::class)
@ConditionalOnBean(SpringBootAdminActuatorWebSecurityConfigurerAdapter::class)
@AutoConfigureAfter(SecurityAutoConfiguration::class)
@EnableWebSecurity
@EnableConfigurationProperties(PrometheusActuatorWebSecurityProperties::class)
@Import(
PrometheusActuatorWebSecurityConfigurerAdapter::class
)
public class PrometheusActuatorWebSecurityAutoConfiguration
This setup requires another authenticationManager
Bean annotated with @Primary
.
It also leads to the securityFilterChain
instances from the library to be ignored.
Additionally, only the authenticationManager Bean marked as Primary was properly configured. I previously tried to configure is using HttpSecurity
like this:
@Bean(PROMETHEUS_AUTHENTICATION_MANAGER_BEAN_NAME)
public fun prometheusActuatorAuthenticationManager(http: HttpSecurity): AuthenticationManager {
val auth: AuthenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder::class.java)
auth.inMemoryAuthentication()
.withUser(properties.actuatorEndpointAccessUser)
.password(properties.actuatorEndpointAccessPassword)
.roles(properties.actuatorEndpointAccessRole)
return auth.build()
}
This worked in registering the users, but resulted in a Stackoverflow due to repeated calls to the DaoAuthenticationProvider
(or similar) when a provided User cannot be found.
I have read, that this should be done using a custom DSL in this Spring Blog Post.
The lack of a comprehensive example and seeming lack of this being explored anywhere else has made it difficult for me to grasp this approach.
My main questions therefore are:
- Can I configure Authentication in a similar fashion?
- Is it possible to configure
HttpSecurity
using Beans, or similar to the pre-update method? - Or is this approach flawed from the start?
The closest example I have seen so far is this: spring-webflux-multiple-auth-mechanisms
It is however for webflux and kept simple.
Any further reading especially examples in this direction would be greatly appreciated.
Excluding the Spring documentation, I must have read the relevant sections a dozen times. (Unless I missed something).
Thanks in advance.