Commit 587f96d4 authored by Madhura Bhave's avatar Madhura Bhave

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
parents 0fc33b02 d07e351e
...@@ -178,7 +178,7 @@ public class IncludeExcludeEndpointFilter<E extends ExposableEndpoint<?>> implem ...@@ -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 ...@@ -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; ...@@ -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 { ...@@ -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 ...@@ -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; ...@@ -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 { ...@@ -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 @@ ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment