Walkthrough: Cloud Foundry mTLS using the X-Forwarded-Client-Cert (XFCC) header and Java Buildpack Client Certificate Mapper
Prior to the introduction of mTLS using XFCC and the Java Buildpack Client Certificate Mapper, the only available option for mTLS was through TCP Routing to pass through the TLS handshake to the application, this approach bypasses all the layer-7 HTTP features of GoRouter, including context-path routing, transparent retries, and sticky sessions.
This walkthrough aims to show a basic configuration of the new features using Cloud Foundry on bosh-lite with a simple Spring Boot application. In the walkthrough, we are using Cloud Foundry with SSL/TLS terminated at the GoRouter
- git installed
- Java keytool on your path (keytool is in Java bin)
- Maven installed and on your path
- Cloud Foundry command line client installed
$ cd [GITHUB HOME]
$ git clone https://github.com/ob-sc/cf-xfcc-demo
For windows environments see: https://github.com/goettw/bosh-lite-windows-bosh-client2
For Linux environments see: http://www.starkandwayne.com/blog/bosh-lite-on-virtualbox-with-bosh2/
-
Login
$ cf login -a api.bosh-lite.com -u admin --skip-ssl-validation
-
Create Org
$ cf create-org demos
-
Target Org
$ cf t -o demos
-
Create Space
$ cf create-space mtls
-
Target Space
$ cf t -s mtls
Check that the cf-deployment.yml is correctly configured based on the instructions in the Cloud Foundry Admin Guide:
- name: gorouter
release: routing
properties:
router:
enable_ssl: true
tls_pem:
- cert_chain: "((router_ssl.certificate))"
private_key: "((router_ssl.private_key))"
Applications that require mutual TLS (mTLS) need metadata from Client Certificates to authorize requests. Cloud Foundry supports this use case without bypassing layer-7 load balancers and the GoRouter.
Modify your GoRouter configuration in the cf-deployment.yml as follows, this will ensure Client Certificates are mapped to the XFCC header when the GoRouter is configured to terminate SSL/TLS:
- name: gorouter
release: routing
properties:
router:
forwarded_client_cert: sanitize_set
A convenience script has been included in the github repo to create the appropriate keys/authorities/stores/certificates. To run the script and generate the artifacts:
$ cd [GITHUB HOME]/cf-mtls-demo/certs
$ chmod +x gen_certs.sh
$ ./gen_certs.sh
At a high level the included script performs the following actions using the Java keytool:
- Create Keystore with a Certificate Authority (keystore.jks)
- Export Certificate Authority (ca.crt)
- Create a Truststore (truststore.jks)
- Create Client Certificate for Joe Bloggs (joe.crt)
We will add our Client Certificate Authority (ca.crt) to the list of authorities used to validate certificates provided by remote systems during mTLS handshakes.
Modify the GoRouter configuration as follows:
- name: gorouter
release: routing
properties:
router:
ca_certs: "((router_ca_certs))"
Copy your generated Certificate Authority ca.crt to the same directory as your cf-deployment.yml
NOTE
Given the the GoRouter is initiating the mTLS handshake does this mean that all apps have to present a Client Certificate? The answer is no as by default the GoRouter is configured to only ask for a Client Certificate to be presented if it is available. We will see in a later step that we are still able to access non-X509 applications even if a Client Certificate is not available.
Redeploy with the new configuration and providing your ca_certs in a var file (note the additional --var-file router_ca_certs=ca.crt flag):
$ bosh -d cf deploy ~/workspace/cf-deployment/cf-deployment.yml --var-file router_ca_certs=ca.crt -o ~/workspace/cf-deployment/operations/bosh-lite.yml --vars-store ~/deployments/vbox/deployment-vars.yml -v system_domain=bosh-lite.com
The insecure server is a simple Spring Boot application that exposes a simple /header endpoint that when called (GET request) will return all available headers.
To package and deploy the Insecure Server app (assuming you have targeted your bosh-lite CF deployment):
$ cd [GITHUB HOME]/cf-mtls-demo/insecure-server
$ mvn clean package
$ cf push
Using a web browser (tested with Firefox), browse to https://insecure-server.bosh-lite.com/headers (assuming your cf deployment system domain is bosh-lite.com) and you should see all available headers.
NOTE
Even thought the GoRouter is configured with the additional Certificate Authority you will not have been prompted to present a Client Certificate (you will need to accept the Server Certificate though). This demonstrates that even though the GoRouter has been configured for mTLS it will not affect deployed applications that do not require a Client Certificate.
The secure server is a simple Spring Boot application that is configured to only authenticate the user joe.bloggs@acme.com from a Client Certificate (using X509 based pre-authenticate).
The app exposes a simple /user endpoint that when called (GET request) will return the user name as well as a simple /header endpoint that when called (GET request) will return the all available headers.
To package and deploy the Secure Server app (assuming you have targeted your bosh-lite CF deployment):
$ cd [GITHUB HOME]/cf-mtls-demo/secure-server
$ mvn clean package
$ cf push
The main differences between the insecure-server and secure-server apps are as follows:
- Additional Spring Security Dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- Security configuration to check for a valid user from the X509 Certificate (in our case joe.bloggs@acme.com):
@EnableWebSecurity
public class HttpSecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${valid-user}")
private String validUser;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and()
.x509()
.subjectPrincipalRegex("CN=(.*?)(?:,|$)")
.userDetailsService(userDetailsService());
}
@Bean
public UserDetailsService userDetailsService() {
return new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username) {
if (username.equals(validUser)) {
return new User(username, "", AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
} else {
throw new UsernameNotFoundException("Invalid user: " + username);
}
}
};
}
}
NOTE
The Java Buildpack Client Certificate mapper is automatically adding a Servlet Filter that maps the X-Forwarded-Client-Cert to the javax.servlet.request.X509Certificate Servlet attribute that we are using to authenticate
- Additional /user end point that prints the username:
@RequestMapping(value = "/user")
public String user(@RequestHeader HttpHeaders headers, Principal principal) {
UserDetails currentUser = (UserDetails) ((Authentication) principal).getPrincipal();
return "Hello " + currentUser.getUsername();
}
If we call either the /user or /headers end point in the secure-server app, we will get a 403 Forbidden exception as no Client Certificate is present.
To authenticate we can add our joe.p12 private key in Firefox security settings:
- Settings => Privacy & Security => Certificates => Ask you every time = true
- Settings => Privacy & Security => Certificates => View Certificates => Import
- Clear cache/history and restart Firefox
Now when we call either the /user or /headers end point we will be prompted for our Client Certificate, once selected the app will authenticate joe.bloggs@acme.com and allow access.
NOTE
When calling the /headers end point we can see the additional X-Forwarded-Client-Cert header that has been added by the GoRouter from the Client Certificate. This header is picked up by the Java Buildpack Client Certificate Mapper Servlet Filter and maps it to the javax.servlet.request.X509Certificate Servlet attribute so it can be used for authentication.