SSL with Spring WebFlux and Vault PKI
In this article, you will learn how to configure the Vault PKI engine and integrate it with Spring WebFlux. With Vault PKI you can easily generate X.509 certificates signed by the CA. Then your application may get a certificate through a REST API. Its TTL is relatively short. It is unique per each application instance. Also, we can use Spring VaultTemplate
to simplify integration with Vault API.
Let’s say a little bit more about Vault. It allows you to secure, store, and control access to tokens, passwords, certificates, and encryption keys using UI, CLI, or HTTP API. It is a really powerful tool. With Vault, instead of a traditional approach, you can manage your security in a more dynamic, cloud-native way. For example, you can integrate Vault with a database backend, and then generate user login and password on the fly. Moreover, for Spring Boot applications you can take an advantage of the Spring Cloud Vault project. If you are interested in more information about it read my article Testing Spring Boot Integration with Vault and Postgres using Testcontainers.
That’s not all. You can integrate Vault with other tools from Hashicorp like Consul or Nomad. In other words, it allows us to build a cloud-native platform in a secure way. For more details please refer to the article Secure Spring Cloud Microservices with Vault and Nomad.
Source Code
If you would like to try it by yourself, you may always take a look at my source code. In order to do that you need to clone my repository sample-spring-cloud-security. Then you should go to the gateway-service
directory, and just follow my instructions in the next sections. The sample application acts as an API gateway for microservices. We use Spring Cloud Gateway. Since it is built on top of Spring WebFlux, that example is perfectly right for our current article.
1. Running Vault
We will run Vault inside the Docker container in development mode. The server running in that mode does not require any further setup, it is ready to use just after startup. After startup, our instance of Vault is available on port 8200. The version of Vault used in this article is 1.7.1
.
$ docker run --cap-add=IPC_LOCK -d --name vault -p 8200:8200 vault
It is possible to login using different methods, but the most suitable way for us is through a token. To do that we have to display container logs using command docker logs vault
, and then copy Root Token as shown below.
Finally, we are able to login to the Vault web console.
2. Enable and configure Vault PKI
There are two ways to enable and configure Vault PKI: with CLI or via UI. Most articles describe a list of CLI commands required to configure the PKI engine. One of them is available on the Hashicorp site. However, I’m going to use Vault UI for that. So first, let’s enable a new engine on the main site.
Then, we need to choose a type of engine to enable. In our case it is the option PKI Certificates.
During creation let’s leave a default name pki
. Then, we need to navigate into the newly enabled engine and create a new role. A role is used for generating certificates. The name of my role is default
. This name is important because we would have to call it from the code using VaultTemplate
.
The type of the key used in our role is rsa
.
Before creating it, we should set some important parameters. One of them is TTL, which is set to 3 days. Also, don’t forget to check fields Allow any name and Require Common Name. Both of them are related to the CN field inside the certificate. Because we will store a username inside the CN field, we need to allow any name for it.
Once a role is created, we need to configure CA. To do that, we should first switch to the Configuration tab and then click Configure button.
After that, let’s choose Configure CA.
Finally, we can create a new CA certificate. We should leave the root
value as CA Type and internal
as Type. The default key format is pem
. We can also set a Common Name for the CA certificate. For both role and CA it worth filling additional fields like e.g. the name of an organization or organization unit.
3. Integrating Spring WebFlux with Vault PKI
Let’s begin with dependencies. We need to include a Spring WebFlux starter for reactive API and a Spring Security starter to secure API. Integration with Vault API can be provided by spring-vault-core
. I also had to include Jackson libraries in order to be able to start the application with Spring Vault.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-cloud-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.vault</groupId>
<artifactId>spring-vault-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Then, let’s configure a VaultTemplate
bean. It should use the http
scheme and an authentication token injected from the configuration.
@Value("vault.token")
private String vaultToken;
@Bean
VaultTemplate vaultTemplate() {
VaultEndpoint e = new VaultEndpoint();
e.setScheme("http");
VaultTemplate template = new VaultTemplate(e, new TokenAuthentication(vaultToken));
return template;
}
The VaultTemplate
provides dedicated support for interaction with the PKI engine. We just need to call the method opsForPki
passing the PKI engine name to obtain the VaultPkiOperations
instance (1). Then we need to build a certificate request with VaultCertificateRequest
. We may set several parameters, but the most important is CN and certificate TTL (2). Finally, we should invoke the issueCertificate
method passing the request and the name of the role configured on Vault PKI (3). Our certificate has been successfully generated. Now, we just need to obtain it from the response. The generated certificate, CA certificate, and a private key are available inside the CertificateBundle
object, which is returned by the method.
private CertificateBundle issueCertificate() throws Exception {
VaultPkiOperations pkiOperations = vaultTemplate.opsForPki("pki"); // (1)
VaultCertificateRequest request = VaultCertificateRequest.builder()
.ttl(Duration.ofHours(12))
.commonName("localhost")
.build(); // (2)
VaultCertificateResponse response = pkiOperations.issueCertificate("default", request); // (3)
CertificateBundle certificateBundle = response.getRequiredData(); // (4)
log.info("Cert-SerialNumber: {}", certificateBundle.getSerialNumber());
return certificateBundle;
}
4. Enable Spring WebFlux security
We have already integrated Spring WebFlux with Vault PKI in the previous section. Finally, we can proceed to the last step in our implementation – enable security based on X.509 certificates. To do that we need to create a @Configuration
class. It should be annotated with @EnableWebFluxSecurity
(1). We also need to obtain a username from the certificate by implementing the principal extractor. We are going to use SubjectDnX509PrincipalExtractor
(2) with the right regex that reads data from the CN field. The final configuration disables CSRF, basic auth, and enables SSL with X509 certificates (3). We also need to provide an implementation of the UserDetails
interface (4) with a single username piotrm
.
@Configuration
@EnableWebFluxSecurity // (1)
public class SecurityConfig {
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
SubjectDnX509PrincipalExtractor principalExtractor =
new SubjectDnX509PrincipalExtractor(); // (2)
principalExtractor.setSubjectDnRegex("CN=(.*?)(?:,|$)");
return http.csrf().disable()
.authorizeExchange(exchanges ->
exchanges.anyExchange().authenticated())
.x509()
.principalExtractor(principalExtractor)
.and()
.httpBasic().disable().build(); // (3)
}
@Bean
public MapReactiveUserDetailsService users() { // (4)
UserDetails user1 = User.builder()
.username("piotrm")
.password("{noop}1234")
.roles("USER")
.build();
return new MapReactiveUserDetailsService(user1);
}
}
In the last step, we need to override the Netty server SSL configuration on runtime. Our customizer should implement the WebServerFactoryCustomizer
interface, and use NettyReactiveWebServerFactory
. Inside customize method we first invoke the method issueCertificate
responsible for generating a certificate in Vault (you can refer to the previous section to see the implementation of that method) (1). The CertificateBundle
contains all required data. We can invoke the method createKeyStore
on it to create a keystore (2) and then save it in the file (3).
To override Netty SSL settings we should use the Ssl
object. The client authentication needs to be enabled (4). We will also set the location of the currently created KeyStore
(5). After that, we may proceed to the truststore creation. The issuer certificate may be obtained from CertificateBundle
(6). Then we should create a new keystore, and set the CA certificate as an entry there (7). Finally, we will save the truststore to the file and set its location in the Ssl
object.
@Component
@Slf4j
public class GatewayServerCustomizer implements
WebServerFactoryCustomizer<NettyReactiveWebServerFactory> {
@SneakyThrows
@Override
public void customize(NettyReactiveWebServerFactory factory) {
String keyAlias = "vault";
CertificateBundle bundle = issueCertificate(); // (1)
KeyStore keyStore = bundle.createKeyStore(keyAlias); // (2)
String keyStorePath = saveKeyStoreToFile("server-key.pkcs12", keyStore); // (3)
Ssl ssl = new Ssl();
ssl.setEnabled(true);
ssl.setClientAuth(Ssl.ClientAuth.NEED); // (4)
ssl.setKeyStore(keyStorePath); // (5)
ssl.setKeyAlias(keyAlias);
ssl.setKeyStoreType(keyStore.getType());
ssl.setKeyPassword("");
ssl.setKeyStorePassword("123456");
X509Certificate caCert = bundle.getX509IssuerCertificate(); // (6)
log.info("CA-SerialNumber: {}", caCert.getSerialNumber());
KeyStore trustStore = KeyStore.getInstance("pkcs12");
trustStore.load(null, null);
trustStore.setCertificateEntry("ca", caCert); // (7)
String trustStorePath = saveKeyStoreToFile("server-trust.pkcs12", trustStore); // (8)
ssl.setTrustStore(trustStorePath); // (9)
ssl.setTrustStorePassword("123456");
ssl.setTrustStoreType(trustStore.getType());
factory.setSsl(ssl);
factory.setPort(8443);
}
}
5. Testing Spring WebFlux with Vault PKI
Let’s run our sample application. It is available under the 8443 port. We will test it using the curl tool. Before doing it we need to generate a client certificate with a private key. Let’s go to the Vault UI once again. If you click a default Vault UI redirects to form responsible for certificate generation as shown below. In the Common Name field, we should provide the test username configured inside the UserDetails
implementation. For me, it is piotrm
. Also, don’t forget to set the right TTL.
After generating a certificate you will be redirected to the site with the results. First, you should copy the string with your certificate, and save it to the file. For me it is piotrm.crt
. You can also display the content of a generated private key. Then, do the same as with the certificate. My filename is piotrm.key
.
Finally, we can send a test request to our sample application passing the names of key and certificate files.
$ curl https://localhost:8443/hello -v --key piotrm.key --cert piotrm.crt
Leave a Reply