-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #148 from xenit-eu/actuator-security-autoconfig
Actuator security autoconfig
- Loading branch information
Showing
7 changed files
with
367 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
96 changes: 96 additions & 0 deletions
96
...rid/spring/boot/autoconfigure/security/ActuatorEndpointsWebSecurityAutoConfiguration.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
package com.contentgrid.spring.boot.autoconfigure.security; | ||
|
||
import jakarta.servlet.http.HttpServletRequest; | ||
import java.net.InetAddress; | ||
import java.net.UnknownHostException; | ||
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; | ||
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration; | ||
import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration; | ||
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest; | ||
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest.EndpointRequestMatcher; | ||
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType; | ||
import org.springframework.boot.actuate.health.HealthEndpoint; | ||
import org.springframework.boot.actuate.info.InfoEndpoint; | ||
import org.springframework.boot.actuate.metrics.MetricsEndpoint; | ||
import org.springframework.boot.actuate.metrics.export.prometheus.PrometheusScrapeEndpoint; | ||
import org.springframework.boot.autoconfigure.AutoConfiguration; | ||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; | ||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; | ||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; | ||
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.core.Ordered; | ||
import org.springframework.core.annotation.Order; | ||
import org.springframework.core.env.Environment; | ||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; | ||
import org.springframework.security.web.SecurityFilterChain; | ||
import org.springframework.security.web.util.matcher.AndRequestMatcher; | ||
import org.springframework.security.web.util.matcher.RequestMatcher; | ||
|
||
|
||
@AutoConfiguration( | ||
after = { | ||
HealthEndpointAutoConfiguration.class, | ||
InfoEndpointAutoConfiguration.class, | ||
WebEndpointAutoConfiguration.class, | ||
SecurityAutoConfiguration.class | ||
}) | ||
@ConditionalOnClass(value = {HealthEndpoint.class, SecurityFilterChain.class, HttpSecurity.class}) | ||
@ConditionalOnWebApplication(type = Type.SERVLET) | ||
public class ActuatorEndpointsWebSecurityAutoConfiguration { | ||
|
||
/** | ||
* List of publicly accessible management endpoints | ||
*/ | ||
private static final EndpointRequestMatcher PUBLIC_ENDPOINTS = EndpointRequest.to( | ||
InfoEndpoint.class, | ||
HealthEndpoint.class | ||
); | ||
|
||
/** | ||
* List of management metrics endpoints, allowed when the management port and server port are different. | ||
*/ | ||
private static final EndpointRequestMatcher METRICS_ENDPOINTS = EndpointRequest.to( | ||
MetricsEndpoint.class, | ||
PrometheusScrapeEndpoint.class | ||
); | ||
|
||
@Bean | ||
@Order(Ordered.HIGHEST_PRECEDENCE) | ||
SecurityFilterChain actuatorEndpointsSecurityFilterChain(HttpSecurity http, Environment environment) | ||
throws Exception { | ||
|
||
http.authorizeHttpRequests((requests) -> requests.requestMatchers( | ||
PUBLIC_ENDPOINTS, | ||
new AndRequestMatcher( | ||
METRICS_ENDPOINTS, | ||
request -> ManagementPortType.get(environment) == ManagementPortType.DIFFERENT | ||
), | ||
new AndRequestMatcher( | ||
EndpointRequest.toAnyEndpoint(), | ||
new LoopbackInetAddressMatcher() | ||
) | ||
) | ||
.permitAll()); | ||
|
||
// all the other /actuator endpoints fall through | ||
return http.build(); | ||
} | ||
|
||
private static class LoopbackInetAddressMatcher implements RequestMatcher { | ||
|
||
@Override | ||
public boolean matches(HttpServletRequest request) { | ||
return isLoopbackAddress(request.getRemoteAddr()); | ||
} | ||
|
||
boolean isLoopbackAddress(String address) { | ||
try { | ||
var remoteAddress = InetAddress.getByName(address); | ||
return remoteAddress.isLoopbackAddress(); | ||
} catch (UnknownHostException ex) { | ||
return false; | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
213 changes: 213 additions & 0 deletions
213
...spring/boot/autoconfigure/security/ActuatorEndpointsWebSecurityAutoConfigurationTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
package com.contentgrid.spring.boot.autoconfigure.security; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; | ||
|
||
import com.contentgrid.spring.boot.autoconfigure.security.ManagementContextSupplierConfiguration.ManagementContextSupplier; | ||
import java.net.InetAddress; | ||
import java.util.Objects; | ||
import java.util.function.Consumer; | ||
import lombok.NonNull; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.SneakyThrows; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.junit.jupiter.api.Test; | ||
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; | ||
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; | ||
import org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointAutoConfiguration; | ||
import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration; | ||
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration; | ||
import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration; | ||
import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; | ||
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; | ||
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsEndpointAutoConfiguration; | ||
import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; | ||
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; | ||
import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration; | ||
import org.springframework.boot.autoconfigure.AutoConfigurations; | ||
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; | ||
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; | ||
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; | ||
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; | ||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner; | ||
import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer; | ||
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; | ||
import org.springframework.context.ApplicationContext; | ||
import org.springframework.http.MediaType; | ||
import org.springframework.lang.Nullable; | ||
import org.springframework.test.web.reactive.server.StatusAssertions; | ||
import org.springframework.test.web.reactive.server.WebTestClient; | ||
import org.springframework.test.web.servlet.client.MockMvcWebTestClient; | ||
import org.springframework.test.web.servlet.request.RequestPostProcessor; | ||
import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; | ||
import org.springframework.test.web.servlet.setup.MockMvcConfigurer; | ||
import org.springframework.test.web.servlet.setup.MockMvcConfigurerAdapter; | ||
import org.springframework.web.context.WebApplicationContext; | ||
|
||
@Slf4j | ||
class ActuatorEndpointsWebSecurityAutoConfigurationTest { | ||
|
||
static final String ACTUATOR_ROOT = "/actuator"; | ||
static final String ACTUATOR_HEALTH = "/actuator/health"; | ||
static final String ACTUATOR_INFO = "/actuator/info"; | ||
static final String ACTUATOR_METRICS = "/actuator/metrics"; | ||
static final String ACTUATOR_ENV = "/actuator/env"; | ||
|
||
static class AutoConfigs { | ||
|
||
static final AutoConfigurations ACTUATORS = AutoConfigurations.of( | ||
HealthContributorAutoConfiguration.class, | ||
HealthEndpointAutoConfiguration.class, | ||
|
||
InfoEndpointAutoConfiguration.class, | ||
|
||
MetricsAutoConfiguration.class, | ||
MetricsEndpointAutoConfiguration.class, | ||
CompositeMeterRegistryAutoConfiguration.class, | ||
|
||
EnvironmentEndpointAutoConfiguration.class, | ||
|
||
EndpointAutoConfiguration.class, | ||
WebMvcAutoConfiguration.class, | ||
WebEndpointAutoConfiguration.class | ||
); | ||
|
||
static final AutoConfigurations MANAGEMENT = AutoConfigurations.of( | ||
ManagementContextAutoConfiguration.class, | ||
ServletManagementContextAutoConfiguration.class, | ||
ServletWebServerFactoryAutoConfiguration.class, | ||
DispatcherServletAutoConfiguration.class | ||
); | ||
} | ||
|
||
WebApplicationContextRunner runner = new WebApplicationContextRunner( | ||
AnnotationConfigServletWebServerApplicationContext::new) | ||
.withPropertyValues( | ||
"management.endpoints.web.exposure.include=*" | ||
) | ||
.withInitializer(new ServerPortInfoApplicationContextInitializer()) | ||
.withConfiguration(AutoConfigs.ACTUATORS) | ||
.withConfiguration(AutoConfigs.MANAGEMENT) | ||
.withConfiguration(AutoConfigurations.of( | ||
SecurityAutoConfiguration.class, | ||
ManagementWebSecurityAutoConfiguration.class, | ||
ActuatorEndpointsWebSecurityAutoConfiguration.class) | ||
); | ||
|
||
@Test | ||
void whenAccessFromRemoteAddress() { | ||
|
||
runner.run(context -> { | ||
assertThat(context) | ||
.hasNotFailed() | ||
.hasBean("actuatorEndpointsSecurityFilterChain"); | ||
|
||
withWebTestClient(context, remoteAddress("192.0.2.1"), assertHttp -> { | ||
// public endpoints | ||
assertHttp.get(ACTUATOR_HEALTH).isOk(); | ||
assertHttp.get(ACTUATOR_INFO).isOk(); | ||
|
||
// management on primary server port | ||
assertHttp.get(ACTUATOR_METRICS).isForbidden(); | ||
|
||
// other endpoints fall through if not from loopback-address | ||
assertHttp.get(ACTUATOR_ENV).isForbidden(); | ||
|
||
// root forbidden | ||
assertHttp.get(ACTUATOR_ROOT).isForbidden(); | ||
}); | ||
}); | ||
} | ||
|
||
@Test | ||
void whenManagementOnDifferentPort() { | ||
|
||
runner.withPropertyValues("management.server.port=0") | ||
|
||
.run(context -> { | ||
|
||
assertThat(context) | ||
.hasNotFailed() | ||
.hasBean("actuatorEndpointsSecurityFilterChain"); | ||
|
||
withWebTestClient(context, remoteAddress("192.0.2.1"), assertHttp -> { | ||
// public endpoints | ||
assertHttp.get(ACTUATOR_HEALTH).isOk(); | ||
assertHttp.get(ACTUATOR_INFO).isOk(); | ||
|
||
// management on different port | ||
assertHttp.get(ACTUATOR_METRICS).isOk(); | ||
|
||
// other endpoints fall through if not from loopback-address | ||
assertHttp.get(ACTUATOR_ENV).isForbidden(); | ||
|
||
// root forbidden | ||
assertHttp.get(ACTUATOR_ROOT).isForbidden(); | ||
}); | ||
}); | ||
} | ||
|
||
@Test | ||
void whenAccessFromLocalAddress() { | ||
|
||
runner.run(context -> { | ||
assertThat(context) | ||
.hasNotFailed() | ||
.hasBean("actuatorEndpointsSecurityFilterChain"); | ||
|
||
withWebTestClient(context, remoteAddress("localhost"), assertHttp -> { | ||
// all /actuator endpoitns allowed when from a loopback address | ||
assertHttp.get(ACTUATOR_HEALTH).isOk(); | ||
assertHttp.get(ACTUATOR_INFO).isOk(); | ||
assertHttp.get(ACTUATOR_METRICS).isOk(); | ||
assertHttp.get(ACTUATOR_ENV).isOk(); | ||
|
||
assertHttp.get(ACTUATOR_ROOT).isOk(); | ||
}); | ||
}); | ||
} | ||
|
||
private void withWebTestClient(ApplicationContext context, MockMvcConfigurer remoteAddress, | ||
Consumer<HttpAssertClient> callback) { | ||
WebTestClient client; | ||
|
||
var managementContext = context.getBean(ManagementContextSupplier.class).get(); | ||
client = MockMvcWebTestClient.bindToApplicationContext(Objects.requireNonNull(managementContext)) | ||
.apply(remoteAddress) | ||
.apply(springSecurity()) | ||
.build(); | ||
|
||
callback.accept((@NonNull String endpoint) -> | ||
client.get().uri(endpoint).accept(MediaType.APPLICATION_JSON).exchange().expectStatus()); | ||
} | ||
|
||
@FunctionalInterface | ||
interface HttpAssertClient { | ||
|
||
StatusAssertions get(String endpoint); | ||
} | ||
|
||
@SneakyThrows | ||
private MockMvcConfigurer remoteAddress(String remoteAddress) { | ||
var address = InetAddress.getByName(remoteAddress); | ||
return new RemoteAddressMockMvcConfigurer(address); | ||
} | ||
|
||
@RequiredArgsConstructor | ||
static class RemoteAddressMockMvcConfigurer extends MockMvcConfigurerAdapter { | ||
|
||
@NonNull | ||
final InetAddress remoteAddress; | ||
|
||
@Override | ||
@Nullable | ||
public RequestPostProcessor beforeMockMvcCreated(ConfigurableMockMvcBuilder<?> builder, | ||
WebApplicationContext cxt) { | ||
return request -> { | ||
request.setRemoteAddr(remoteAddress.getHostAddress()); | ||
request.setRemoteHost(remoteAddress.getHostName()); | ||
return request; | ||
}; | ||
} | ||
} | ||
} |
Oops, something went wrong.