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.
|
||||
*/
|
||||
WEB("info", "health");
|
||||
WEB("health");
|
||||
|
||||
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.info.InfoEndpointAutoConfiguration;
|
||||
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.AutoConfigureBefore;
|
||||
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
|
||||
* actuator is on the classpath. Specifically, it permits access to the health and info
|
||||
* endpoints while securing everything else.
|
||||
* actuator is on the classpath. Specifically, it permits access to the health endpoint
|
||||
* while securing everything else.
|
||||
*
|
||||
* @author Madhura Bhave
|
||||
* @since 2.1.0
|
||||
|
@ -59,7 +58,7 @@ public class ReactiveManagementWebSecurityAutoConfiguration {
|
|||
@Bean
|
||||
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
|
||||
http.authorizeExchange((exchanges) -> {
|
||||
exchanges.matchers(EndpointRequest.to(HealthEndpoint.class, InfoEndpoint.class)).permitAll();
|
||||
exchanges.matchers(EndpointRequest.to(HealthEndpoint.class)).permitAll();
|
||||
exchanges.anyExchange().authenticated();
|
||||
});
|
||||
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.info.InfoEndpointAutoConfiguration;
|
||||
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.AutoConfigureBefore;
|
||||
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
|
||||
* on the classpath. It allows unauthenticated access to the {@link HealthEndpoint} and
|
||||
* {@link InfoEndpoint}. If the user specifies their own
|
||||
* {@link org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
|
||||
* WebSecurityConfigurerAdapter} or {@link SecurityFilterChain} bean, this will back-off
|
||||
* on the classpath. It allows unauthenticated access to the {@link HealthEndpoint}. If
|
||||
* the user specifies their own{@link SecurityFilterChain} bean, this will back-off
|
||||
* completely and the user should specify all the bits that they want to configure as part
|
||||
* of the custom security configuration.
|
||||
*
|
||||
|
@ -60,7 +57,7 @@ public class ManagementWebSecurityAutoConfiguration {
|
|||
@Bean
|
||||
SecurityFilterChain managementSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
http.authorizeRequests((requests) -> {
|
||||
requests.requestMatchers(EndpointRequest.to(HealthEndpoint.class, InfoEndpoint.class)).permitAll();
|
||||
requests.requestMatchers(EndpointRequest.to(HealthEndpoint.class)).permitAll();
|
||||
requests.anyRequest().authenticated();
|
||||
});
|
||||
http.formLogin(Customizer.withDefaults());
|
||||
|
|
|
@ -91,8 +91,7 @@
|
|||
{
|
||||
"name": "management.endpoints.web.exposure.include",
|
||||
"defaultValue": [
|
||||
"health",
|
||||
"info"
|
||||
"health"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
@ -40,8 +40,8 @@ class ConditionalOnAvailableEndpointTests {
|
|||
|
||||
@Test
|
||||
void outcomeShouldMatchDefaults() {
|
||||
this.contextRunner.run((context) -> assertThat(context).hasBean("info").hasBean("health")
|
||||
.doesNotHaveBean("spring").doesNotHaveBean("test").doesNotHaveBean("shutdown"));
|
||||
this.contextRunner.run((context) -> assertThat(context).hasBean("health").doesNotHaveBean("spring")
|
||||
.doesNotHaveBean("test").doesNotHaveBean("shutdown"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -79,7 +79,7 @@ class ConditionalOnAvailableEndpointTests {
|
|||
@Test
|
||||
void outcomeWhenIncludeAllJmxButJmxDisabledShouldMatchDefaults() {
|
||||
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"));
|
||||
}
|
||||
|
||||
|
@ -95,8 +95,8 @@ class ConditionalOnAvailableEndpointTests {
|
|||
this.contextRunner
|
||||
.withPropertyValues("management.endpoints.jmx.exposure.include=*", "spring.jmx.enabled=true",
|
||||
"management.endpoint.shutdown.enabled=true")
|
||||
.run((context) -> assertThat(context).hasBean("info").hasBean("health").hasBean("test")
|
||||
.hasBean("spring").hasBean("shutdown"));
|
||||
.run((context) -> assertThat(context).hasBean("health").hasBean("test").hasBean("spring")
|
||||
.hasBean("shutdown"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -36,14 +36,13 @@ class InfoEndpointAutoConfigurationTests {
|
|||
|
||||
@Test
|
||||
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));
|
||||
}
|
||||
|
||||
@Test
|
||||
void runShouldHaveEndpointBeanEvenIfDefaultIsDisabled() {
|
||||
this.contextRunner.withPropertyValues("management.endpoint.default.enabled:false")
|
||||
.run((context) -> assertThat(context).hasSingleBean(InfoEndpoint.class));
|
||||
void runWhenNotExposedShouldNotHaveEndpointBean() {
|
||||
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(InfoEndpoint.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -91,7 +91,7 @@ class WebMvcEndpointExposureIntegrationTests {
|
|||
assertThat(isExposed(client, HttpMethod.GET, "customservlet")).isFalse();
|
||||
assertThat(isExposed(client, HttpMethod.GET, "env")).isFalse();
|
||||
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.POST, "shutdown")).isFalse();
|
||||
assertThat(isExposed(client, HttpMethod.GET, "threaddump")).isFalse();
|
||||
|
|
|
@ -78,11 +78,6 @@ class ReactiveManagementWebSecurityAutoConfigurationTests {
|
|||
this.contextRunner.run((context) -> assertThat(getAuthenticateHeader(context, "/actuator/health")).isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
void permitAllForInfo() {
|
||||
this.contextRunner.run((context) -> assertThat(getAuthenticateHeader(context, "/actuator/info")).isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
void securesEverythingElse() {
|
||||
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
|
||||
void securesEverythingElse() {
|
||||
this.contextRunner.run((context) -> {
|
||||
|
|
|
@ -3960,10 +3960,10 @@ You can register multiple relying parties under the `spring.security.saml2.relyi
|
|||
|
||||
[[boot-features-security-actuator]]
|
||||
=== 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.
|
||||
|
||||
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.
|
||||
|
||||
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 {
|
||||
http.authorizeRequests((requests) -> {
|
||||
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))
|
||||
.hasRole("ACTUATOR");
|
||||
requests.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll();
|
||||
|
|
|
@ -110,16 +110,6 @@ class SampleActuatorApplicationTests {
|
|||
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
|
||||
void testErrorPage() {
|
||||
ResponseEntity<String> entity = this.restTemplate.withBasicAuth("user", "password").getForEntity("/foo",
|
||||
|
|
|
@ -42,7 +42,7 @@ public class SecurityConfiguration {
|
|||
SecurityFilterChain configure(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http.authorizeRequests()
|
||||
.requestMatchers(EndpointRequest.to("health", "info")).permitAll()
|
||||
.requestMatchers(EndpointRequest.to("health")).permitAll()
|
||||
.requestMatchers(EndpointRequest.toAnyEndpoint().excluding(MappingsEndpoint.class)).hasRole("ACTUATOR")
|
||||
.antMatchers("/**").hasRole("USER")
|
||||
.and()
|
||||
|
|
|
@ -92,7 +92,7 @@ class ManagementPortSampleSecureWebFluxTests {
|
|||
@Bean
|
||||
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
|
||||
http.authorizeExchange((exchanges) -> {
|
||||
exchanges.matchers(EndpointRequest.to("health", "info")).permitAll();
|
||||
exchanges.matchers(EndpointRequest.to("health")).permitAll();
|
||||
exchanges.matchers(EndpointRequest.toAnyEndpoint().excluding(MappingsEndpoint.class))
|
||||
.hasRole("ACTUATOR");
|
||||
exchanges.matchers(PathRequest.toStaticResources().atCommonLocations()).permitAll();
|
||||
|
|
|
@ -50,11 +50,6 @@ class SampleSecureWebFluxApplicationTests {
|
|||
.isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void infoInsecureByDefault() {
|
||||
this.webClient.get().uri("/actuator/info").accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
void otherActuatorsSecureByDefault() {
|
||||
this.webClient.get().uri("/actuator/env").accept(MediaType.APPLICATION_JSON).exchange().expectStatus()
|
||||
|
|
|
@ -55,10 +55,9 @@ class SampleSecureWebFluxCustomSecurityTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
void healthAndInfoDoNotRequireAuthentication() {
|
||||
void healthDoesNotRequireAuthentication() {
|
||||
this.webClient.get().uri("/actuator/health").accept(MediaType.APPLICATION_JSON).exchange().expectStatus()
|
||||
.isOk();
|
||||
this.webClient.get().uri("/actuator/info").accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -117,7 +116,7 @@ class SampleSecureWebFluxCustomSecurityTests {
|
|||
@Bean
|
||||
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) throws Exception {
|
||||
http.authorizeExchange((exchanges) -> {
|
||||
exchanges.matchers(EndpointRequest.to("health", "info")).permitAll();
|
||||
exchanges.matchers(EndpointRequest.to("health")).permitAll();
|
||||
exchanges.matchers(EndpointRequest.toAnyEndpoint().excluding(MappingsEndpoint.class))
|
||||
.hasRole("ACTUATOR");
|
||||
exchanges.matchers(PathRequest.toStaticResources().atCommonLocations()).permitAll();
|
||||
|
|
Loading…
Reference in New Issue