diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java index 08b95444e50..dae4058e9eb 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java @@ -160,10 +160,19 @@ public class EndpointWebMvcAutoConfiguration + "through JMX)"); } } - if (managementPort == ManagementServerPort.SAME && this.applicationContext - .getEnvironment() instanceof ConfigurableEnvironment) { - addLocalManagementPortPropertyAlias( - (ConfigurableEnvironment) this.applicationContext.getEnvironment()); + if (managementPort == ManagementServerPort.SAME) { + if (new RelaxedPropertyResolver(this.applicationContext.getEnvironment(), + "management.ssl.").getProperty("enabled") != null) { + throw new IllegalStateException( + "Management-specific SSL cannot be configured as the management " + + "server is not listening on a separate port"); + } + if (this.applicationContext + .getEnvironment() instanceof ConfigurableEnvironment) { + addLocalManagementPortPropertyAlias( + (ConfigurableEnvironment) this.applicationContext + .getEnvironment()); + } } } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcChildContextConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcChildContextConfiguration.java index d765209410c..a61a32dceb4 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcChildContextConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcChildContextConfiguration.java @@ -188,6 +188,9 @@ public class EndpointWebMvcChildContextConfiguration { container.setContextPath(""); // and add the management-specific bits container.setPort(this.managementServerProperties.getPort()); + if (this.managementServerProperties.getSsl() != null) { + container.setSsl(this.managementServerProperties.getSsl()); + } container.setServerHeader(this.server.getServerHeader()); container.setAddress(this.managementServerProperties.getAddress()); container.addErrorPages(new ErrorPage(this.server.getError().getPath())); diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementServerProperties.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementServerProperties.java index 7821ea8afd8..0c7447bc71b 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementServerProperties.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementServerProperties.java @@ -25,7 +25,9 @@ import javax.validation.constraints.NotNull; import org.springframework.boot.autoconfigure.security.SecurityPrerequisite; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.context.embedded.Ssl; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -68,6 +70,9 @@ public class ManagementServerProperties implements SecurityPrerequisite { */ private Integer port; + @NestedConfigurationProperty + private Ssl ssl; + /** * Network address that the management endpoints should bind to. */ @@ -112,6 +117,14 @@ public class ManagementServerProperties implements SecurityPrerequisite { this.port = port; } + public Ssl getSsl() { + return this.ssl; + } + + public void setSsl(Ssl ssl) { + this.ssl = ssl; + } + public InetAddress getAddress() { return this.address; } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java index 34982b826f8..f44386c11eb 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java @@ -29,6 +29,11 @@ import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.TrustSelfSignedStrategy; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.ssl.SSLContextBuilder; import org.hamcrest.Matcher; import org.junit.After; import org.junit.Before; @@ -75,6 +80,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.stereotype.Controller; import org.springframework.test.util.ReflectionTestUtils; @@ -113,6 +119,12 @@ public class EndpointWebMvcAutoConfigurationTests { private static ManagementServerProperties management = new ManagementServerProperties(); + @Before + public void defaultContextPath() { + management.setContextPath(""); + server.setContextPath(""); + } + @Before public void grabPorts() { Ports values = new Ports(); @@ -200,8 +212,6 @@ public class EndpointWebMvcAutoConfigurationTests { assertThat(managementContainerFactory) .isInstanceOf(SpecificEmbeddedServletContainerFactory.class); assertThat(managementContainerFactory).isNotSameAs(parentContainerFactory); - this.applicationContext.close(); - assertAllClosed(); } @Test @@ -485,6 +495,73 @@ public class EndpointWebMvcAutoConfigurationTests { .hasSize(1); } + @Test + public void managementSpecificSslUsingDifferentPort() throws Exception { + EnvironmentTestUtils.addEnvironment(this.applicationContext, + "management.ssl.enabled=true", + "management.ssl.key-store=classpath:test.jks", + "management.ssl.key-password=password"); + this.applicationContext.register(RootConfig.class, EndpointConfig.class, + DifferentPortConfig.class, BaseConfiguration.class, + EndpointWebMvcAutoConfiguration.class, ErrorMvcAutoConfiguration.class); + this.applicationContext.refresh(); + assertContent("/controller", ports.get().server, "controlleroutput"); + assertContent("/endpoint", ports.get().server, null); + assertHttpsContent("/controller", ports.get().management, null); + assertHttpsContent("/endpoint", ports.get().management, "endpointoutput"); + assertHttpsContent("/error", ports.get().management, startsWith("{")); + ApplicationContext managementContext = this.applicationContext + .getBean(ManagementContextResolver.class).getApplicationContext(); + List interceptors = (List) ReflectionTestUtils.getField( + managementContext.getBean(EndpointHandlerMapping.class), "interceptors"); + assertThat(interceptors).hasSize(1); + ManagementServerProperties managementServerProperties = this.applicationContext + .getBean(ManagementServerProperties.class); + assertThat(managementServerProperties.getSsl()).isNotNull(); + assertThat(managementServerProperties.getSsl().isEnabled()).isTrue(); + } + + @Test + public void managementSpecificSslUsingSamePortFails() throws Exception { + EnvironmentTestUtils.addEnvironment(this.applicationContext, + "management.ssl.enabled=true", + "management.ssl.key-store=classpath:test.jks", + "management.ssl.key-password=password"); + this.applicationContext.register(RootConfig.class, EndpointConfig.class, + BaseConfiguration.class, EndpointWebMvcAutoConfiguration.class, + ErrorMvcAutoConfiguration.class, ServerPortConfig.class); + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("Management-specific SSL cannot be configured as the " + + "management server is not listening on a separate port"); + this.applicationContext.refresh(); + } + + @Test + public void managementServerCanDisableSslWhenUsingADifferentPort() throws Exception { + EnvironmentTestUtils.addEnvironment(this.applicationContext, + "server.ssl.enabled=true", "server.ssl.key-store=classpath:test.jks", + "server.ssl.key-password=password", "management.ssl.enabled=false"); + + this.applicationContext.register(RootConfig.class, EndpointConfig.class, + DifferentPortConfig.class, BaseConfiguration.class, + EndpointWebMvcAutoConfiguration.class, ErrorMvcAutoConfiguration.class); + this.applicationContext.refresh(); + assertHttpsContent("/controller", ports.get().server, "controlleroutput"); + assertHttpsContent("/endpoint", ports.get().server, null); + assertContent("/controller", ports.get().management, null); + assertContent("/endpoint", ports.get().management, "endpointoutput"); + assertContent("/error", ports.get().management, startsWith("{")); + ApplicationContext managementContext = this.applicationContext + .getBean(ManagementContextResolver.class).getApplicationContext(); + List interceptors = (List) ReflectionTestUtils.getField( + managementContext.getBean(EndpointHandlerMapping.class), "interceptors"); + assertThat(interceptors).hasSize(1); + ManagementServerProperties managementServerProperties = this.applicationContext + .getBean(ManagementServerProperties.class); + assertThat(managementServerProperties.getSsl()).isNotNull(); + assertThat(managementServerProperties.getSsl().isEnabled()).isFalse(); + } + private void endpointDisabled(String name, Class type) { this.applicationContext.register(RootConfig.class, BaseConfiguration.class, ServerPortConfig.class, EndpointWebMvcAutoConfiguration.class); @@ -512,10 +589,27 @@ public class EndpointWebMvcAutoConfigurationTests { assertContent("/endpoint", ports.get().management, null); } - public void assertContent(String url, int port, Object expected) throws Exception { - SimpleClientHttpRequestFactory clientHttpRequestFactory = new SimpleClientHttpRequestFactory(); - ClientHttpRequest request = clientHttpRequestFactory - .createRequest(new URI("http://localhost:" + port + url), HttpMethod.GET); + private void assertHttpsContent(String url, int port, Object expected) + throws Exception { + assertContent("https", url, port, expected); + } + + private void assertContent(String url, int port, Object expected) throws Exception { + assertContent("http", url, port, expected); + } + + private void assertContent(String scheme, String url, int port, Object expected) + throws Exception { + + SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( + new SSLContextBuilder() + .loadTrustMaterial(null, new TrustSelfSignedStrategy()).build()); + HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory) + .build(); + HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory( + httpClient); + ClientHttpRequest request = requestFactory.createRequest( + new URI(scheme + "://localhost:" + port + url), HttpMethod.GET); try { ClientHttpResponse response = request.execute(); if (HttpStatus.NOT_FOUND.equals(response.getStatusCode())) { diff --git a/spring-boot-actuator/src/test/resources/test.jks b/spring-boot-actuator/src/test/resources/test.jks new file mode 100644 index 00000000000..cc0d7081c2e Binary files /dev/null and b/spring-boot-actuator/src/test/resources/test.jks differ diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index c1bd3acf551..d2940df769e 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -1006,10 +1006,25 @@ content into your application; rather pick only the properties that you need. management.add-application-context-header=true # Add the "X-Application-Context" HTTP header in each response. management.address= # Network address that the management endpoints should bind to. management.context-path= # Management endpoint context-path. For instance `/actuator` - management.port= # Management endpoint HTTP port. Use the same port as the application by default. + management.port= # Management endpoint HTTP port. Uses the same port as the application by default. Configure a different port to use management-specific SSL. management.security.enabled=true # Enable security. management.security.roles=ADMIN # Comma-separated list of roles that can access the management endpoint. management.security.sessions=stateless # Session creating policy to use (always, never, if_required, stateless). + management.ssl.ciphers= # Supported SSL ciphers. Requires a custom management.port. + management.ssl.client-auth= # Whether client authentication is wanted ("want") or needed ("need"). Requires a trust store. Requires a custom management.port. + management.ssl.enabled= # Enable SSL support. Requires a custom management.port. + management.ssl.enabled-protocols= # Enabled SSL protocols. Requires a custom management.port. + management.ssl.key-alias= # Alias that identifies the key in the key store. Requires a custom management.port. + management.ssl.key-password= # Password used to access the key in the key store. Requires a custom management.port. + management.ssl.key-store= # Path to the key store that holds the SSL certificate (typically a jks file). Requires a custom management.port. + management.ssl.key-store-password= # Password used to access the key store. Requires a custom management.port. + management.ssl.key-store-provider= # Provider for the key store. Requires a custom management.port. + management.ssl.key-store-type= # Type of the key store. Requires a custom management.port. + management.ssl.protocol=TLS # SSL protocol to use. Requires a custom management.port. + management.ssl.trust-store= # Trust store that holds SSL certificates. Requires a custom management.port. + management.ssl.trust-store-password= # Password used to access the trust store. Requires a custom management.port. + management.ssl.trust-store-provider= # Provider for the trust store. Requires a custom management.port. + management.ssl.trust-store-type= # Type of the trust store. Requires a custom management.port. # HEALTH INDICATORS (previously health.*) management.health.db.enabled=true # Enable database health check. diff --git a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc index 5315c622468..6c0d2d273ab 100644 --- a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc @@ -595,6 +595,39 @@ disable the management security in this way, and it might even break the applica +[[production-ready-management-specific-ssl]] +=== Configuring management-specific SSL +When configured to use a custom port, the management server can also be configured with +its own SSL using the various `management.ssl.*` properties. For example, this allows a +management server to be available via HTTP while the main application uses HTTPS: + +[source,properties,indent=0] +---- + server.port=8443 + server.ssl.enabled=true + server.ssl.key-store=classpath:store.jks + server.ssl.key-password=secret + management.port=8080 + management.ssl.enable=false +---- + +Alternatively, both the main server and the management server can use SSL but with +different key stores: + +[source,properties,indent=0] +---- + server.port=8443 + server.ssl.enabled=true + server.ssl.key-store=classpath:main.jks + server.ssl.key-password=secret + management.port=8080 + management.ssl.enable=true + management.ssl.key-store=classpath:management.jks + management.ssl.key-password=secret +---- + + + [[production-ready-customizing-management-server-address]] === Customizing the management server address You can customize the address that the management endpoints are available on by