Go back to Blogs
Zero Trust Architecture – Client Implementation and Secure Communication
Overview
In our previous blog article, Developing Zero Trust Security Model with TLS and Advanced Encryption in Spring Boot, we explored the foundation of Zero-trust security model, core principles and implementation of Zero-trust server architecture. We covered key concepts such as mutual TLS, JWT authentication, and enforcing strict security measures to protect sensitive data and API’s, ensuring that only authenticated and authorized entities can access the Zero-Trust Server.
However, a Zero Trust Architecture is incomplete without a secure client that adheres to the same principles. The client plays a crucial role in authenticating itself to the server, securely transmitting requests, and ensuring that data remains protected during communication.
The diagram below represents a simplified view of Zero Trust architecture with mutual TLS, secure communication, request/response validation and labeled endpoints like /register, /authenticate and /authorized. It illustrates how a client securely communicates with a server over HTTPS, with mutual authentication and token validation on the server side.
In this article, we’ll dive into the zero trust client implementation, focusing on:
- Configuring the client dependencies.
- Managing the client security configurations.
- Configuring the client for mutual TLS (mTLS) communication.
- Implementing robust authentication and authorization mechanisms.
- Developing Client middleware controllers to interact with the server.
- Managing keys generation and Configuration
- Testing and troubleshooting common issues when integrating the client with a Zero Trust server.
By the end of this guide, you’ll have a fully functioning client capable of securely interacting with your Zero Trust server, completing the architecture needed to establish trust boundaries in your applications.
Let’s get started!
Zero-trust Client Application
The zero-trust-client application is the client application that serves as a middleware frontend application to send registration, authentication and protection requests to the server. The client and the server must establish a secure end-to-end communication channel before sending and receiving requests. Once the user is authenticated, the server sends a JWT bearer token to the client to securely establish connection.
Client Application Dependency Configuration – maven dependencies on the client side are listed in the pom.xml file are listed below:
- Spring Web – Used to build RESTful web applications using spring MVC.
- JSON Web Token Dependencies (jjwt-api, jjwt-imp and jjwt-jackson) – used to encode, decode and validate JWT tokens in the client application.
- Spring reactive Web (Spring Web-Flux) – Used to build reactive Web with Spring Web-flux and netty.
- Client Test Dependencies (Spring boot starter test, Spring Security test and reactor-test).
- Jasypt library – Adds basic encryption capabilities in our Client
- Spring Security – Authentication and authorization framework for spring application
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.nextgenerationconsultancy</groupId>
<artifactId>zero-trust-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>zero-trust-client</name>
<description>zero-trust-client</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
https://mvnrepository.com/artifact/com.github.ulisesbocchio/jasypt-spring-boot-starter -->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Client Security Configuration – Creates security filter chain beans on the client to handle security settings for the incoming requests before forwarding it to the zero trust server. We configured the following three properties:
- CSRF is disabled – we don’t use it in our case.
- Request URL matching /client/register and /client/authenticate doesn’t require authentication.
- Any other request path must be authenticated
package com.nextgenerationconsultancy.zerotrustclient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web\.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web\.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class ClientSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(request -> request
.requestMatchers("/client/register", "/client/authenticate")
.permitAll()
.anyRequest()
.authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
}
WebClient Configuration – Configure WebClient to load the client keystore and truststore, to setup http client with SSL, to use mTLS and handle JWT:
package com.nextgenerationconsultancy.zerotrustclient;
import io.netty.handler.ssl.ClientAuth;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import java.io.FileInputStream;
import java.security.KeyStore;
@Configuration
public class WebClientConfig {
@Value("${server.ssl.key-store}")
private Resource keyStore;
@Value("${server.ssl.key-store-password}")
private String keyStorePassword;
@Value("${server.ssl.trust-store}")
private Resource trustStore;
@Value("${server.ssl.trust-store-password}")
private String trustStorePassword;
@Value("${server.url}")
private String serverUrl;
@Bean
public WebClient webClient() throws Exception {
// Load KeyStore
KeyStore keyStore = KeyStore.getInstance("PKCS12");
try (FileInputStream keyStoreInputStream = new FileInputStream(this.keyStore.getFile())) {
keyStore.load(keyStoreInputStream, keyStorePassword.toCharArray());
}
// Load TrustStore
KeyStore trustStore = KeyStore.getInstance("PKCS12");
try (FileInputStream trustStoreInputStream = new FileInputStream(this.trustStore.getFile())) {
trustStore.load(trustStoreInputStream, trustStorePassword.toCharArray());
}
// Initialize KeyManagerFactory
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, keyStorePassword.toCharArray());
// Initialize TrustManagerFactory
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(trustStore);
// Setup HttpClient with SSL
SslContext sslContext = SslContextBuilder.forClient()
.clientAuth(ClientAuth.REQUIRE)
//.protocols("TLS")
.keyManager(keyManagerFactory)
.trustManager(trustManagerFactory)
.build();
HttpClient httpClient = HttpClient.newConnection()
.secure(sslContextSpec -> sslContextSpec.sslContext(sslContext));
// Create WebClient
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.baseUrl(serverUrl)
.build();
}
}
Client Service – Create a service to interact with the zero trust server and forward the requests to the corresponding server endpoint.
package com.nextgenerationconsultancy.zerotrustclient;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
@Service
public class ClientService {
private final WebClient webClient;
public ClientService(WebClient webClient) {
this.webClient = webClient;
}
public Mono<String> registerUser(AuthRequest request) {
return webClient.post()
.uri("/api/register")
.body(BodyInserters.fromValue(request))
.retrieve()
.bodyToMono(String.class);
}
public Mono<String> authenticate(AuthRequest request) {
return webClient.post()
.uri("/api/authenticate")
.body(BodyInserters.fromValue(request))
.retrieve()
.bodyToMono(String.class);
}
public Mono<String> getProtectedResource(String token) {
return webClient.get()
.uri("/api/protected")
.header(HttpHeaders.AUTHORIZATION, token)
.retrieve()
.bodyToMono(String.class);
}
}
Client Controller – To expose the client functionality, we will create the following endpoints on the client side:
- [POST] /client/register – API to send request for registration of new users to the server.
- [POST] /client/authenticate – API that sends authentication to the server.
- [GET] /client/protected – API that sends validation requests to the server.
package com.nextgenerationconsultancy.zerotrustclient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/client")
public class ClientController {
private final ClientService clientService;
public ClientController(ClientService clientService) {
this.clientService = clientService;
}
@PostMapping("/register")
public ResponseEntity<String> registerUser(@RequestBody AuthRequest request) {
return clientService.registerUser(request).map(ResponseEntity::ok).block();
}
@PostMapping("/authenticate")
public ResponseEntity<String> authenticate(@RequestBody AuthRequest request) {
return clientService.authenticate(request).map(ResponseEntity::ok).block();
}
@GetMapping("/protected")
public ResponseEntity<String> getProtectedResource(@RequestHeader("Authorization") String token) {
return clientService.getProtectedResource(token).map(ResponseEntity::ok).block();
}
}
AuthRequest – POJO container for client request:
package com.nextgenerationconsultancy.zerotrustclient;
import java.util.List;
public class AuthRequest {
private String username;
private String password;
private List<String> roles;
public AuthRequest(String username, String password, List<String> roles) {
this.username = username;
this.password = password;
this.roles = roles;
}
// Getters and Settters
}
Zero Trust Client Main Application – Entry point to run the zero trust client Spring Boot application:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
package com.nextgenerationconsultancy.zerotrustclient;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ZeroTrustClientApplication {
public static void main(String[] args) {
SpringApplication.run(ZeroTrustClientApplication.class, args);
}
}
Key generation and Configuration
Key Generation – The key generation involves the creation of client keystore, client certificate and server truststore is crucial for setting up TLS (Transport Layer Security) and mutual TLS (mTLS). Key generation for the server keystore, server certificate and client trust store is provided in Developing Zero Trust Security Model with TLS and Advanced Encryption in Spring Boot. The code below illustrates how to generate self-signed certificates using OpenSSL key generation methods:
# Generate client private key and certificate
openssl req -x509 -newkey rsa:4096 -keyout client-key.pem -out client-cert.pem -days 365
# Convert the certificate and key to PKCS12 format
openssl pkcs12 -export -in client-cert.pem -inkey client-key.pem -out client-keystore.p12 -name clientalias
# Generate server truststore and import client certificate
keytool -import -alias clientcert -file client-cert.pem -keystore server-truststore.p12 -storetype PKCS12
Client Application properties Configuration – The required client configuration, such as, SSL, client-keystore, client-truststore, client-alias and Jasypt are configured inside src/main/resources/application.properties file:
spring.application.name=zero-trust-client
server.port=8444
server.ssl.key-store=classpath:client-keystore.p12
server.ssl.key-store-password=[client-key-store-password]
server.ssl.key-store-type=PKCS12
server.ssl.key-alias=clientalias
server.ssl.trust-store=classpath:client-truststore.p12
server.ssl.trust-store-password=[client-trust-store-password]
server.ssl.enabled-protocols=TLSv1.2
jasypt.encryptor.password=secretKey
# Base URL for WebClient to connect to the server
server.url=https://localhost:8443
Testing the Zero trust architecture
Building and running the applications
We can compile and execute the server and client application application in terminal with following commands:
# compile, package and execute the zero-trust server
mvn clean
mvn compile
mvn package
java -jar zeroTrustServer.jar
# compile, package and execute the zero-trust client
mvn clean
mvn compile
mvn package
java -jar zeroTrustClient.jar
The client will start on port 8444 and accept registration, authentication authorization requests from the application layer and forward the request to the server. The server will start on port 8443 and performs registration, authentication and authorization tasks and sends responses back to the client. The communication between the server and the client happens via secure SSL communication.
Test
In this section we will test our zero trust model by performing the following operations:
- Register a new user in the system.
- Authenticate and obtain the JWT token from the server.
- Use that token in the Authorization header to access the /protected
Registration requests – Use the following cURL POST command to invoke /register endpoint. The client application is configured to run on port 8444 and is exposed via the /client/register endpoint. It will forward this request to the zero trust server running on port 8443.
curl --key src/resources/client-key.pem \
--cert src/resources/client-cert.pem \
--cacert src/resources/server-cert.pem \
-H "Content-Type: application/json" \
-d '{"username":"[email protected]", "password":"my-secret-password-placeholder", "roles":["USER"]}' \
-X POST https://localhost:8444/client/register
Expected workFlow
- The client receives the registration request via https://localhost:8444/client/register.
- The client service forwards the request to the server’s https://localhost:8443/api/register endpoint.
- The server processes the registration and responds accordingly.
Expected Responses
If everything is configured correctly and the request is valid, we get the following response:
HTTP/1.1 200 OK
Potential Errors
- Invalid SSL Configuration – The client may return an SSL error if the certificates are not set up correctly.
- Connection Refused – If either the client or server isn’t running or configured to use mutual TLS.
Authenticate Request – For the /authenticate request, here’s how the flow would look when the client communicates with the server. Below is the cURL command for authentication request:
curl --key src/resources/client-key.pem \
--cert src/resources/client-cert.pem \
--cacert src/resources/server-cert.pem \
-H "Content-Type: application/json" \
-d '{"username":"[email protected]", "password":"my-secret-password-placeholder"}' \
-X POST https://localhost:8444/client/authenticate
The client-key.pem and client-cert.pem files are the client’s SSL key and certificate, while server-cert.pem is the server’s certificate used for mutual TLS.
- This request is sent to the client application running on port 8444, which will forward it to the server’s authentication endpoint, i.e, https://localhost:8443/api/authenticate.
- The client-key.pem and client-cert.pem files are the client’s SSL key and certificate, while server-cert.pem is the server’s certificate used for mutual TLS.
Expected Responses:
- Success:
HTTP/1.1 200 OK
Content-Type: application/json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
- Failure
HTTP/1.1 401 UNAUTHORIZED
Content-Type: application/json
{
"status": "error",
"message": "Invalid or improperly formatted keys or certificates."
}
Key Points
- The client handles receiving the JWT token from the server and relays it back to the user.
- The JWT token from this step is typically used for subsequent requests, such as accessing protected resources.
Access the Authorized /protected Resource – Now that we have the token, we can use it to access the protected resource.
curl --key src/resources/client-key.pem \
--cert src/resources/client-cert.pem \
--cacert src/resources/server-cert.pem \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" \
-v https://localhost:8444/client/protected
Expected Response:
- Success:
HTTP/1.1 200 OK
Content-Type: application/json
{
"message": "Protected User response"
}
- Invalid or expired token:
HTTP/1.1 403 FORBIDDEN
Content-Type: application/json
{
"message": "Invalid or expired token"
}
Key Points
- Make sure to replace “eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…” with the actual token received from the authentication step.
- Ensure that the server and client certificates are correctly configured to avoid SSL/TLS errors.
Summary
Zero Trust Architecture represents a fundamental shift in cybersecurity strategy, prioritizing the assumption that threats can exist both outside and inside a network. The core principle is to “never trust, always verify.” This approach ensures that every request, whether it originates from within or outside the network, is authenticated, authorized, and encrypted before access is granted.
The implementation of a Zero Trust Client, as detailed in this article, is a critical component of this architecture. By integrating Mutual TLS (mTLS) and JWT-based authentication, the Zero Trust Client ensures that all communications with the server are secure, authenticated, and authorized at every step. While the server plays a critical role in enforcing security policies and handling requests, the client’s implementation ensures that security is maintained from the very first connection, through every interaction with the server.
The successful deployment of Zero Trust Client and Server components forms a robust defense mechanism in today’s threat landscape, aligning with modern security demands where traditional perimeter-based defenses are no longer sufficient. As organizations continue to evolve their IT infrastructure, adopting Zero Trust principles will be essential for maintaining security and trust in their digital interactions.