Merge pull request #24715 from hatefpalizgar
* pr/24715: Polish " Change info endpoint to be secure and unexposed by default" Change info endpoint to be secure and unexposed by default Closes gh-24715
This commit is contained in:
commit
587f96d4e0
|
@ -178,7 +178,7 @@ public class IncludeExcludeEndpointFilter<E extends ExposableEndpoint<?>> implem
|
||||||
/**
|
/**
|
||||||
* The default set of include patterns used for web.
|
* The default set of include patterns used for web.
|
||||||
*/
|
*/
|
||||||
WEB("info", "health");
|
WEB("health");
|
||||||
|
|
||||||
private final EndpointPatterns patterns;
|
private final EndpointPatterns patterns;
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,6 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAu
|
||||||
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
|
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
|
||||||
import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration;
|
import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration;
|
||||||
import org.springframework.boot.actuate.health.HealthEndpoint;
|
import org.springframework.boot.actuate.health.HealthEndpoint;
|
||||||
import org.springframework.boot.actuate.info.InfoEndpoint;
|
|
||||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
|
@ -40,8 +39,8 @@ import org.springframework.security.web.server.WebFilterChainProxy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link EnableAutoConfiguration Auto-configuration} for Reactive Spring Security when
|
* {@link EnableAutoConfiguration Auto-configuration} for Reactive Spring Security when
|
||||||
* actuator is on the classpath. Specifically, it permits access to the health and info
|
* actuator is on the classpath. Specifically, it permits access to the health endpoint
|
||||||
* endpoints while securing everything else.
|
* while securing everything else.
|
||||||
*
|
*
|
||||||
* @author Madhura Bhave
|
* @author Madhura Bhave
|
||||||
* @since 2.1.0
|
* @since 2.1.0
|
||||||
|
@ -59,7 +58,7 @@ public class ReactiveManagementWebSecurityAutoConfiguration {
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
|
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
|
||||||
http.authorizeExchange((exchanges) -> {
|
http.authorizeExchange((exchanges) -> {
|
||||||
exchanges.matchers(EndpointRequest.to(HealthEndpoint.class, InfoEndpoint.class)).permitAll();
|
exchanges.matchers(EndpointRequest.to(HealthEndpoint.class)).permitAll();
|
||||||
exchanges.anyExchange().authenticated();
|
exchanges.anyExchange().authenticated();
|
||||||
});
|
});
|
||||||
http.httpBasic(Customizer.withDefaults());
|
http.httpBasic(Customizer.withDefaults());
|
||||||
|
|
|
@ -20,7 +20,6 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAu
|
||||||
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
|
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
|
||||||
import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration;
|
import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration;
|
||||||
import org.springframework.boot.actuate.health.HealthEndpoint;
|
import org.springframework.boot.actuate.health.HealthEndpoint;
|
||||||
import org.springframework.boot.actuate.info.InfoEndpoint;
|
|
||||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
|
@ -38,10 +37,8 @@ import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link EnableAutoConfiguration Auto-configuration} for Spring Security when actuator is
|
* {@link EnableAutoConfiguration Auto-configuration} for Spring Security when actuator is
|
||||||
* on the classpath. It allows unauthenticated access to the {@link HealthEndpoint} and
|
* on the classpath. It allows unauthenticated access to the {@link HealthEndpoint}. If
|
||||||
* {@link InfoEndpoint}. If the user specifies their own
|
* the user specifies their own{@link SecurityFilterChain} bean, this will back-off
|
||||||
* {@link org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
|
|
||||||
* WebSecurityConfigurerAdapter} or {@link SecurityFilterChain} bean, this will back-off
|
|
||||||
* completely and the user should specify all the bits that they want to configure as part
|
* completely and the user should specify all the bits that they want to configure as part
|
||||||
* of the custom security configuration.
|
* of the custom security configuration.
|
||||||
*
|
*
|
||||||
|
@ -60,7 +57,7 @@ public class ManagementWebSecurityAutoConfiguration {
|
||||||
@Bean
|
@Bean
|
||||||
SecurityFilterChain managementSecurityFilterChain(HttpSecurity http) throws Exception {
|
SecurityFilterChain managementSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||||
http.authorizeRequests((requests) -> {
|
http.authorizeRequests((requests) -> {
|
||||||
requests.requestMatchers(EndpointRequest.to(HealthEndpoint.class, InfoEndpoint.class)).permitAll();
|
requests.requestMatchers(EndpointRequest.to(HealthEndpoint.class)).permitAll();
|
||||||
requests.anyRequest().authenticated();
|
requests.anyRequest().authenticated();
|
||||||
});
|
});
|
||||||
http.formLogin(Customizer.withDefaults());
|
http.formLogin(Customizer.withDefaults());
|
||||||
|
|
|
@ -91,8 +91,7 @@
|
||||||
{
|
{
|
||||||
"name": "management.endpoints.web.exposure.include",
|
"name": "management.endpoints.web.exposure.include",
|
||||||
"defaultValue": [
|
"defaultValue": [
|
||||||
"health",
|
"health"
|
||||||
"info"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -40,8 +40,8 @@ class ConditionalOnAvailableEndpointTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void outcomeShouldMatchDefaults() {
|
void outcomeShouldMatchDefaults() {
|
||||||
this.contextRunner.run((context) -> assertThat(context).hasBean("info").hasBean("health")
|
this.contextRunner.run((context) -> assertThat(context).hasBean("health").doesNotHaveBean("spring")
|
||||||
.doesNotHaveBean("spring").doesNotHaveBean("test").doesNotHaveBean("shutdown"));
|
.doesNotHaveBean("test").doesNotHaveBean("shutdown"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -79,7 +79,7 @@ class ConditionalOnAvailableEndpointTests {
|
||||||
@Test
|
@Test
|
||||||
void outcomeWhenIncludeAllJmxButJmxDisabledShouldMatchDefaults() {
|
void outcomeWhenIncludeAllJmxButJmxDisabledShouldMatchDefaults() {
|
||||||
this.contextRunner.withPropertyValues("management.endpoints.jmx.exposure.include=*")
|
this.contextRunner.withPropertyValues("management.endpoints.jmx.exposure.include=*")
|
||||||
.run((context) -> assertThat(context).hasBean("info").hasBean("health").doesNotHaveBean("spring")
|
.run((context) -> assertThat(context).hasBean("health").doesNotHaveBean("spring")
|
||||||
.doesNotHaveBean("test").doesNotHaveBean("shutdown"));
|
.doesNotHaveBean("test").doesNotHaveBean("shutdown"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,8 +95,8 @@ class ConditionalOnAvailableEndpointTests {
|
||||||
this.contextRunner
|
this.contextRunner
|
||||||
.withPropertyValues("management.endpoints.jmx.exposure.include=*", "spring.jmx.enabled=true",
|
.withPropertyValues("management.endpoints.jmx.exposure.include=*", "spring.jmx.enabled=true",
|
||||||
"management.endpoint.shutdown.enabled=true")
|
"management.endpoint.shutdown.enabled=true")
|
||||||
.run((context) -> assertThat(context).hasBean("info").hasBean("health").hasBean("test")
|
.run((context) -> assertThat(context).hasBean("health").hasBean("test").hasBean("spring")
|
||||||
.hasBean("spring").hasBean("shutdown"));
|
.hasBean("shutdown"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -36,14 +36,13 @@ class InfoEndpointAutoConfigurationTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void runShouldHaveEndpointBean() {
|
void runShouldHaveEndpointBean() {
|
||||||
this.contextRunner.withPropertyValues("management.endpoint.shutdown.enabled:true")
|
this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=info")
|
||||||
.run((context) -> assertThat(context).hasSingleBean(InfoEndpoint.class));
|
.run((context) -> assertThat(context).hasSingleBean(InfoEndpoint.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void runShouldHaveEndpointBeanEvenIfDefaultIsDisabled() {
|
void runWhenNotExposedShouldNotHaveEndpointBean() {
|
||||||
this.contextRunner.withPropertyValues("management.endpoint.default.enabled:false")
|
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(InfoEndpoint.class));
|
||||||
.run((context) -> assertThat(context).hasSingleBean(InfoEndpoint.class));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -91,7 +91,7 @@ class WebMvcEndpointExposureIntegrationTests {
|
||||||
assertThat(isExposed(client, HttpMethod.GET, "customservlet")).isFalse();
|
assertThat(isExposed(client, HttpMethod.GET, "customservlet")).isFalse();
|
||||||
assertThat(isExposed(client, HttpMethod.GET, "env")).isFalse();
|
assertThat(isExposed(client, HttpMethod.GET, "env")).isFalse();
|
||||||
assertThat(isExposed(client, HttpMethod.GET, "health")).isTrue();
|
assertThat(isExposed(client, HttpMethod.GET, "health")).isTrue();
|
||||||
assertThat(isExposed(client, HttpMethod.GET, "info")).isTrue();
|
assertThat(isExposed(client, HttpMethod.GET, "info")).isFalse();
|
||||||
assertThat(isExposed(client, HttpMethod.GET, "mappings")).isFalse();
|
assertThat(isExposed(client, HttpMethod.GET, "mappings")).isFalse();
|
||||||
assertThat(isExposed(client, HttpMethod.POST, "shutdown")).isFalse();
|
assertThat(isExposed(client, HttpMethod.POST, "shutdown")).isFalse();
|
||||||
assertThat(isExposed(client, HttpMethod.GET, "threaddump")).isFalse();
|
assertThat(isExposed(client, HttpMethod.GET, "threaddump")).isFalse();
|
||||||
|
|
|
@ -78,11 +78,6 @@ class ReactiveManagementWebSecurityAutoConfigurationTests {
|
||||||
this.contextRunner.run((context) -> assertThat(getAuthenticateHeader(context, "/actuator/health")).isNull());
|
this.contextRunner.run((context) -> assertThat(getAuthenticateHeader(context, "/actuator/health")).isNull());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void permitAllForInfo() {
|
|
||||||
this.contextRunner.run((context) -> assertThat(getAuthenticateHeader(context, "/actuator/info")).isNull());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void securesEverythingElse() {
|
void securesEverythingElse() {
|
||||||
this.contextRunner.run((context) -> {
|
this.contextRunner.run((context) -> {
|
||||||
|
|
|
@ -73,14 +73,6 @@ class ManagementWebSecurityAutoConfigurationTests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void permitAllForInfo() {
|
|
||||||
this.contextRunner.run((context) -> {
|
|
||||||
HttpStatus status = getResponseStatus(context, "/actuator/info");
|
|
||||||
assertThat(status).isEqualTo(HttpStatus.OK);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void securesEverythingElse() {
|
void securesEverythingElse() {
|
||||||
this.contextRunner.run((context) -> {
|
this.contextRunner.run((context) -> {
|
||||||
|
|
|
@ -3960,10 +3960,10 @@ You can register multiple relying parties under the `spring.security.saml2.relyi
|
||||||
|
|
||||||
[[boot-features-security-actuator]]
|
[[boot-features-security-actuator]]
|
||||||
=== Actuator Security
|
=== Actuator Security
|
||||||
For security purposes, all actuators other than `/health` and `/info` are disabled by default.
|
For security purposes, all actuators other than `/health` are disabled by default.
|
||||||
The configprop:management.endpoints.web.exposure.include[] property can be used to enable the actuators.
|
The configprop:management.endpoints.web.exposure.include[] property can be used to enable the actuators.
|
||||||
|
|
||||||
If Spring Security is on the classpath and no other `WebSecurityConfigurerAdapter` or `SecurityFilterChain` bean is present, all actuators other than `/health` and `/info` are secured by Spring Boot auto-configuration.
|
If Spring Security is on the classpath and no other `WebSecurityConfigurerAdapter` or `SecurityFilterChain` bean is present, all actuators other than `/health` are secured by Spring Boot auto-configuration.
|
||||||
If you define a custom `WebSecurityConfigurerAdapter` or `SecurityFilterChain` bean, Spring Boot auto-configuration will back off and you will be in full control of actuator access rules.
|
If you define a custom `WebSecurityConfigurerAdapter` or `SecurityFilterChain` bean, Spring Boot auto-configuration will back off and you will be in full control of actuator access rules.
|
||||||
|
|
||||||
NOTE: Before setting the `management.endpoints.web.exposure.include`, ensure that the exposed actuators do not contain sensitive information and/or are secured by placing them behind a firewall or by something like Spring Security.
|
NOTE: Before setting the `management.endpoints.web.exposure.include`, ensure that the exposed actuators do not contain sensitive information and/or are secured by placing them behind a firewall or by something like Spring Security.
|
||||||
|
|
|
@ -57,7 +57,7 @@ public class SecurityConfiguration {
|
||||||
SecurityFilterChain configure(HttpSecurity http) throws Exception {
|
SecurityFilterChain configure(HttpSecurity http) throws Exception {
|
||||||
http.authorizeRequests((requests) -> {
|
http.authorizeRequests((requests) -> {
|
||||||
requests.mvcMatchers("/actuator/beans").hasRole("BEANS");
|
requests.mvcMatchers("/actuator/beans").hasRole("BEANS");
|
||||||
requests.requestMatchers(EndpointRequest.to("health", "info")).permitAll();
|
requests.requestMatchers(EndpointRequest.to("health")).permitAll();
|
||||||
requests.requestMatchers(EndpointRequest.toAnyEndpoint().excluding(MappingsEndpoint.class))
|
requests.requestMatchers(EndpointRequest.toAnyEndpoint().excluding(MappingsEndpoint.class))
|
||||||
.hasRole("ACTUATOR");
|
.hasRole("ACTUATOR");
|
||||||
requests.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll();
|
requests.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll();
|
||||||
|
|
|
@ -110,16 +110,6 @@ class SampleActuatorApplicationTests {
|
||||||
assertThat(entity.getBody()).doesNotContain("\"hello\":\"1\"");
|
assertThat(entity.getBody()).doesNotContain("\"hello\":\"1\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void infoInsecureByDefault() {
|
|
||||||
ResponseEntity<String> entity = this.restTemplate.getForEntity("/actuator/info", String.class);
|
|
||||||
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
|
|
||||||
assertThat(entity.getBody()).contains("\"artifact\":\"spring-boot-smoke-test-actuator\"");
|
|
||||||
assertThat(entity.getBody()).contains("\"someKey\":\"someValue\"");
|
|
||||||
assertThat(entity.getBody()).contains("\"java\":{", "\"source\":\"1.8\"", "\"target\":\"1.8\"");
|
|
||||||
assertThat(entity.getBody()).contains("\"encoding\":{", "\"source\":\"UTF-8\"", "\"reporting\":\"UTF-8\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testErrorPage() {
|
void testErrorPage() {
|
||||||
ResponseEntity<String> entity = this.restTemplate.withBasicAuth("user", "password").getForEntity("/foo",
|
ResponseEntity<String> entity = this.restTemplate.withBasicAuth("user", "password").getForEntity("/foo",
|
||||||
|
|
|
@ -42,7 +42,7 @@ public class SecurityConfiguration {
|
||||||
SecurityFilterChain configure(HttpSecurity http) throws Exception {
|
SecurityFilterChain configure(HttpSecurity http) throws Exception {
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
http.authorizeRequests()
|
http.authorizeRequests()
|
||||||
.requestMatchers(EndpointRequest.to("health", "info")).permitAll()
|
.requestMatchers(EndpointRequest.to("health")).permitAll()
|
||||||
.requestMatchers(EndpointRequest.toAnyEndpoint().excluding(MappingsEndpoint.class)).hasRole("ACTUATOR")
|
.requestMatchers(EndpointRequest.toAnyEndpoint().excluding(MappingsEndpoint.class)).hasRole("ACTUATOR")
|
||||||
.antMatchers("/**").hasRole("USER")
|
.antMatchers("/**").hasRole("USER")
|
||||||
.and()
|
.and()
|
||||||
|
|
|
@ -92,7 +92,7 @@ class ManagementPortSampleSecureWebFluxTests {
|
||||||
@Bean
|
@Bean
|
||||||
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
|
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
|
||||||
http.authorizeExchange((exchanges) -> {
|
http.authorizeExchange((exchanges) -> {
|
||||||
exchanges.matchers(EndpointRequest.to("health", "info")).permitAll();
|
exchanges.matchers(EndpointRequest.to("health")).permitAll();
|
||||||
exchanges.matchers(EndpointRequest.toAnyEndpoint().excluding(MappingsEndpoint.class))
|
exchanges.matchers(EndpointRequest.toAnyEndpoint().excluding(MappingsEndpoint.class))
|
||||||
.hasRole("ACTUATOR");
|
.hasRole("ACTUATOR");
|
||||||
exchanges.matchers(PathRequest.toStaticResources().atCommonLocations()).permitAll();
|
exchanges.matchers(PathRequest.toStaticResources().atCommonLocations()).permitAll();
|
||||||
|
|
|
@ -50,11 +50,6 @@ class SampleSecureWebFluxApplicationTests {
|
||||||
.isOk();
|
.isOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void infoInsecureByDefault() {
|
|
||||||
this.webClient.get().uri("/actuator/info").accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void otherActuatorsSecureByDefault() {
|
void otherActuatorsSecureByDefault() {
|
||||||
this.webClient.get().uri("/actuator/env").accept(MediaType.APPLICATION_JSON).exchange().expectStatus()
|
this.webClient.get().uri("/actuator/env").accept(MediaType.APPLICATION_JSON).exchange().expectStatus()
|
||||||
|
|
|
@ -55,10 +55,9 @@ class SampleSecureWebFluxCustomSecurityTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void healthAndInfoDoNotRequireAuthentication() {
|
void healthDoesNotRequireAuthentication() {
|
||||||
this.webClient.get().uri("/actuator/health").accept(MediaType.APPLICATION_JSON).exchange().expectStatus()
|
this.webClient.get().uri("/actuator/health").accept(MediaType.APPLICATION_JSON).exchange().expectStatus()
|
||||||
.isOk();
|
.isOk();
|
||||||
this.webClient.get().uri("/actuator/info").accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -117,7 +116,7 @@ class SampleSecureWebFluxCustomSecurityTests {
|
||||||
@Bean
|
@Bean
|
||||||
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
|
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
|
||||||
http.authorizeExchange((exchanges) -> {
|
http.authorizeExchange((exchanges) -> {
|
||||||
exchanges.matchers(EndpointRequest.to("health", "info")).permitAll();
|
exchanges.matchers(EndpointRequest.to("health")).permitAll();
|
||||||
exchanges.matchers(EndpointRequest.toAnyEndpoint().excluding(MappingsEndpoint.class))
|
exchanges.matchers(EndpointRequest.toAnyEndpoint().excluding(MappingsEndpoint.class))
|
||||||
.hasRole("ACTUATOR");
|
.hasRole("ACTUATOR");
|
||||||
exchanges.matchers(PathRequest.toStaticResources().atCommonLocations()).permitAll();
|
exchanges.matchers(PathRequest.toStaticResources().atCommonLocations()).permitAll();
|
||||||
|
|
Loading…
Reference in New Issue