logo

Go back to Blogs

Developing Zero Trust Security Model with TLS and Advanced Encryption in Spring Boot

August 23, 2024 0 Comments

What is Zero Trust Security?

Zero Trust security is an IT security model that enforces strict identity verification for every person and device attempting to access resources on a private network, regardless of their location relative to the network perimeter. Zero Trust Network Access (ZTNA) is the primary technology associated with Zero Trust architecture, but the Zero Trust model itself is a comprehensive approach to network security, incorporating multiple principles and technologies to ensure robust protection.

Traditional IT network security operates on the assumption that anyone and anything within the network can be trusted. In contrast, a Zero Trust architecture operates on the principle that no one and nothing can be trusted by default, regardless of whether they are inside or outside the network. This approach assumes that threats can originate both inside and outside the network, and thus every access request must be authenticated and authorized. 

Core Principles of Zero Trust security

  • Verify Explicitly: Always authenticate and authorize based on all available data points, including user identity, location, device health, and more. Continuous verification is necessary, not just at the point of entry.
  • Least Privilege Access: Limit user and device access to only what is necessary for their roles. Implement just-in-time (JIT) and just-enough-access (JEA) principles, risk-based adaptive policies, and data protection to minimize exposure.
  • Assume Breach: Operate with the expectation that a breach has either already occurred or is imminent. Minimize blast radius and segment access to prevent attackers from moving laterally within the network. Continuous monitoring and automated threat detection and response are crucial.
  • Micro-Segmentation: Break the network into smaller, isolated segments to control and manage traffic flow, thereby limiting the potential impact of a breach. This approach ensures that even if one segment is compromised, the rest remain secure.
  • Multi-Factor Authentication (MFA): Use multiple methods to verify a user’s identity. This adds an additional layer of security by requiring more than just a password to gain access.

The diagram below illustrates zero trust model message exchange between the client and the server to establish end-to-end secured communication.

In this article, we’ll walk through the implementation of the server side of Zero Trust principles using TLS and advanced encryption techniques in a Spring Boot application.

Implementing Side Zero Trust Server Security Model with Spring Boot

The following softwares, tools and frameworks are required for this article:

  • Java Development Kit (JDK) 11 or higher.
  • Maven
  • Spring Boot 3.x.x.
  • Spring Security
  • OpenSSL for generating certificates.
  • IDE or Text editor (We are using IntelliJ Idea)
  • HTTP Client, such as, Postman, Insomnia, cURL, etc

Zero Trust Server Application

The zero-trust-server application is the backend application that performs the registration, authentication and authorization of users. We’ll be developing the following APIs where some could be accessed from the client side while others are not.

  • [POST] /api/register – API to perform registration of new users in the system.
  • [POST] /api/authenticate – API that generates authentication tokens for the registered users to access the resources.
  • [GET] /api/protected – API that validates the user with the authentication token and allows access to the resources.
  • [GET] /api/getUser/{id} – API on the server side to retrieve an authenticated user by id.
  • [GET] /api/getUsers – API on the server side to retrieve all authenticated users.

Server Application Dependency Configuration – The dependencies needed for the server application are listed in the pom.xml file are the following:

  • Spring Web – Used to build RESTful web applications using spring MVC.
  • Spring Security – Authentication and authorization framework for spring application
  • Spring data JPA – Java persistence API using Spring data and hibernate.
  • H2 Database – In memory database for test purpose.
  • JSON Web Token Dependencies (jjwt-api, jjwt-imp and jjwt-jackson) – used to encode, decode and validate JWT token in the server application.
  • Test Dependencies (Spring boot starter test and Spring Security test).
  • Jasypt library – Adds basic encryption capabilities in our server.
<?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-server</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <name>zero-trust-server</name>
   <description>zero-trust-server</description>
   <properties>
       <java.version>17</java.version>
   </properties>
   <dependencies>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-data-jpa</artifactId>
       </dependency>
       <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>com.h2database</groupId>
           <artifactId>h2</artifactId>
           <scope>runtime</scope>
       </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>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>

JWT Token Service – A utility class to generate, decode or validate a JSON web token. All the methods used to perform JWT operations are grouped in this class.

package com.nextgenerationconsultancy.zerotrustserver;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Date;
import java.util.function.Function;

@Component
public class JwtTokenService {

   @Value("${security.jwt.token.secret-key}")
   private String secretKey;

   @Value("${security.jwt.token.expire-length}")
   private long tokenValidityInMillis; // 1h

   private SecretKey getKey() {
       return Keys.hmacShaKeyFor(Base64.getEncoder().encode(secretKey.getBytes(StandardCharsets.UTF_8)));
   }

   public String getUsernameFromToken(String token) {
       return getClaimFromToken(token, Claims::getSubject);
   }

   public Date getExpirationDateFromToken(String token) {
       return getClaimFromToken(token, Claims::getExpiration);
   }

   public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
       final Claims claims = Jwts.parser()
               .verifyWith(getKey())
               .build()
               .parseSignedClaims(token)
               .getPayload();
       return claimsResolver.apply(claims);
   }

   private Boolean isTokenExpired(String token) {
       final Date expiration = getExpirationDateFromToken(token);
       return expiration.before(new Date());
   }

   public String generateToken(String username) {
       Claims claims = Jwts.claims().subject(username).build();
       return Jwts.builder()
               .claims(claims)
               .subject(username)
               .issuedAt(new Date(System.currentTimeMillis()))
               .expiration(new Date(System.currentTimeMillis() + tokenValidityInMillis))
               .signWith(getKey())
               .compact();
   }

   public Boolean validateToken(String token, String username) {
       final String tokenUsername = getUsernameFromToken(token);
       return (tokenUsername.equals(username) && !isTokenExpired(token));
   }
}

JWT Request Filter – A component that retrieves the JWT bearer token from the “Authorization” header and handles the validation. If the JWT token is valid, it extracts the username from the DB and sets the authentication context so that the user can access it in the application layer.

package com.nextgenerationconsultancy.zerotrustserver;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Lazy;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

   private final JwtTokenService jwtTokenService;
   @Lazy
   private final CustomUserDetailsService customUserDetailsService;

   public JwtRequestFilter(JwtTokenService jwtTokenService, CustomUserDetailsService customUserDetailsService) {
       this.jwtTokenService = jwtTokenService;
       this.customUserDetailsService = customUserDetailsService;
   }

   @Override
   protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {

       String username = null;
       String jwtToken = null;

       // JWT Token is in the form "Bearer token"
       final String requestTokenHeader = request.getHeader("Authorization");
       if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
           jwtToken = requestTokenHeader.substring(7);
           try {
               username = jwtTokenService.getUsernameFromToken(jwtToken);
           } catch (Exception e) {
               System.out.println("Unable to get JWT Token");
           }
       }

       // Once we get the token, validate it.
       if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
           var userDetails = this.customUserDetailsService.loadUserByUsername(username);

           // If the token is valid, configure Spring Security to set authentication
           if (jwtTokenService.validateToken(jwtToken, userDetails.getUsername())) {
               var authToken = new UsernamePasswordAuthenticationToken(
                       userDetails,
                       null,
                       userDetails.getAuthorities()
               );
               authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
               // After setting the Authentication in the context, we specify that the current user is authenticated
               SecurityContextHolder.getContext().setAuthentication(authToken);
           }
       }

       filterChain.doFilter(request, response);
   }
}

Security Configuration – Create a security configuration criteria to handle JWT and security settings for the incoming request before it is forwarded to the application middleware. We defined the following criteria: 

  • CSRF is disabled – we don’t use it in our case.
  • Request URL matching /api/register and /api/authenticate doesn’t require authentication.
  • Any other request path must be authenticated
  • We use a custom authentication provider which must be used before the authentication service.
  • We add JWT request filters in the UsernamePasswordAuthenticationFilter class.
package com.nextgenerationconsultancy.zerotrustserver;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

   private final JwtRequestFilter jwtRequestFilter;
   private final CustomUserDetailsService customUserDetailsService;

   public SecurityConfig(JwtRequestFilter jwtRequestFilter, CustomUserDetailsService customUserDetailsService) {
       this.jwtRequestFilter = jwtRequestFilter;
       this.customUserDetailsService = customUserDetailsService;
   }

   @Bean
   public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
       http.csrf(AbstractHttpConfigurer::disable)
               .authorizeHttpRequests(request -> request
                       .requestMatchers("/api/register", "/api/authenticate")
                       .permitAll()
                       .anyRequest()
                       .authenticated()
               )
               .httpBasic(Customizer.withDefaults())
               .exceptionHandling(Customizer.withDefaults())
               .sessionManagement(Customizer.withDefaults())
               .authenticationProvider(authenticationProvider())
               .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);

       return http.build();
   }

   @Bean
   public CorsConfigurationSource corsConfigurationSource() {
       CorsConfiguration configuration = new CorsConfiguration();

       configuration.setAllowedOrigins(List.of("https://localhost:8443"));
       configuration.setAllowedMethods(List.of("GET","POST"));
       configuration.setAllowedHeaders(List.of("Authorization","Content-Type"));
       UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

       source.registerCorsConfiguration("/**", configuration);

       return source;
   }

   @Bean
   public PasswordEncoder passwordEncoder() {
       return PasswordEncoderFactories.createDelegatingPasswordEncoder();
   }

   @Bean
   public AuthenticationProvider authenticationProvider() {
       DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
       authProvider.setUserDetailsService(customUserDetailsService);
       authProvider.setPasswordEncoder(passwordEncoder());
       return authProvider;
   }

   @Bean
   public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
       return authenticationConfiguration.getAuthenticationManager();
   }

   @Bean
   public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
       UserDetails user = User.builder()
                       .username("[email protected]")
                       .password(passwordEncoder.encode("password"))
                       .roles("USER")
                       .build();

       return new InMemoryUserDetailsManager(user);
   }
}

Controller for Authentication – Create a controller to handle authentication requests:

package com.nextgenerationconsultancy.zerotrustserver;

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;
import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/api")
public class AuthController {

   private final UserService userService;
   private final JwtTokenService jwtTokenService;
   private final CustomUserDetailsService userDetailsService;
   private final UserRepository userRepository;

   public AuthController(UserService userService, JwtTokenService jwtTokenService, CustomUserDetailsService userDetailsService, UserRepository userRepository) {
       this.userService = userService;
       this.jwtTokenService = jwtTokenService;
       this.userDetailsService = userDetailsService;
       this.userRepository = userRepository;
   }

   @GetMapping("/getUser/{id}")
   public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
       Optional<UserDTO> userRecord = userRepository.findById(id);
       return userRecord.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
   }

   @GetMapping("/getUsers")
   public ResponseEntity<List<UserDTO>> getUsers() {
       List<UserDTO> userRecords = userRepository.findAll();
       return ResponseEntity.ok(userRecords);
   }

   @PostMapping(value = "/register", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
   public ResponseEntity<Void> registerUser(@RequestBody UserDTO userDTO, UriComponentsBuilder uriComponentsBuilder) {
       try {
           Optional<UserDTO> userOptional = userRepository.findByUsername(userDTO.getUsername());
           if(userOptional.isPresent()) {
               return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
           }
           UserDTO registeredUser = userService.registerUser(userDTO);
           URI recordPath = uriComponentsBuilder.path("/api/getUser/{id}").buildAndExpand(registeredUser.getId()).toUri();
           return ResponseEntity.created(recordPath).build();
       } catch (Exception e) {
           return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
       }
   }


   @PostMapping(value = "/authenticate", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
   public ResponseEntity<?> createAuthenticationToken(@RequestBody UserDTO userDTO) {
       try {
           var authenticate = userService.authenticate(userDTO);
           var principal = (UserDetails) authenticate.getPrincipal();
           final UserDetails userDetails = userDetailsService.loadUserByUsername(principal.getUsername());
           final String token = jwtTokenService.generateToken(userDetails.getUsername());
           AuthResponse authResponse = new AuthResponse(token);

           return ResponseEntity.ok(authResponse);
       } catch (AuthenticationException e) {
           return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Error: " + e.getMessage());
       }
   }

   @GetMapping("/protected")
   @PreAuthorize("hasRole('USER')")
   public ResponseEntity<String> protectedUser() {
       return ResponseEntity.ok("Protected User response");
   }
}

UserRepository – An interface that extends and inherits the JpaRepository interface. Implementation is provided by spring boot.

package com.nextgenerationconsultancy.zerotrustserver;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<UserDTO, Long> {
   Optional<UserDTO> findByUsername(String username);
}

UserDTO – This is a class that implements UserDetails. In our implementation, this is also a POJO that contains user information (username, password, roles, etc). This class overrides the methods in the UserDetails interface. Make sure the booleans methods return true to avoid authentication failure. The getAuthorities() method returns the list of Roles assigned to the user.

package com.nextgenerationconsultancy.zerotrustserver;


import com.ulisesbocchio.jasyptspringboot.annotation.EnableEncryptableProperties;
import jakarta.persistence.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;

@EnableEncryptableProperties
@Entity
public class UserDTO implements UserDetails {

   @Id
   @GeneratedValue(strategy=GenerationType.IDENTITY)
   private Long id;
   private String username;
   private String password;

   @ElementCollection(fetch = FetchType.EAGER)
   private Set<String> roles;
   public UserDTO() {
   }
   public void setId(Long id) {
       this.id = id;
   }

   public Long getId() {
       return id;
   }

   public String getUsername() {
       return username;
   }

   public void setUsername(String username) {
       this.username = username;
   }

   public String getPassword() {
       return password;
   }

   public void setPassword(String password) {
       this.password = password;
   }

   public Set<String> getRoles() {
       return roles;
   }

   public void setRoles(Set<String> roles) {
       this.roles = roles;
   }

   @Override
   public boolean isAccountNonExpired() {
       return true;
   }

   @Override
   public boolean isAccountNonLocked() {
       return true;
   }

   @Override
   public boolean isCredentialsNonExpired() {
       return true;
   }

   @Override
   public boolean isEnabled() {
       return true;
   }

   @Override
   public Collection<? extends GrantedAuthority> getAuthorities() {
       return roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
   }
}

AuthResponse – when the request is authenticated, the token response is contained in the AuthResponse class.

package com.nextgenerationconsultancy.zerotrustserver;

public class AuthResponse {
   private String token;
   public AuthResponse(String token) {
       this.token = token;
   }
   // Getter and Setter
}

CustomerUserDetailsService – Create a service that implements UserDetailsService interface to load UserDetails. The UserDTO class above implements UserDetails, hence we can directly return what is stored in the repository with the implemented method loadUserByUsername(username).

package com.nextgenerationconsultancy.zerotrustserver;

import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class CustomUserDetailsService implements UserDetailsService {

   @Lazy
   private final UserRepository repository;

   public CustomUserDetailsService(UserRepository repository) {
       this.repository = repository;
   }

   @Override
   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return repository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));
   }
}

UserService – A utility service that registers users in the DB and performs authenticates.

package com.nextgenerationconsultancy.zerotrustserver;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class UserService {

   private final UserRepository userRepository;
   private final PasswordEncoder passwordEncoder;
   private final AuthenticationManager authenticationManager;

   public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, AuthenticationManager authenticationManager) {
       this.userRepository = userRepository;
       this.passwordEncoder = passwordEncoder;
       this.authenticationManager = authenticationManager;
   }

   public UserDTO registerUser(UserDTO userDTO) {
       UserDTO newUser = new UserDTO();
       newUser.setUsername(userDTO.getUsername());
       newUser.setPassword(passwordEncoder.encode(userDTO.getPassword()));
       newUser.setRoles(userDTO.getRoles());
       return userRepository.save(newUser);
   }

   public Authentication authenticate(UserDetails user) {
       UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
               user.getUsername(),
               user.getPassword(),
               user.getAuthorities()
       );
       return authenticationManager.authenticate(authenticationToken);
   }
}

Main Application – ZeroTrustServerApplication is the main entry point to our backed server application.

package com.nextgenerationconsultancy.zerotrustserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ZeroTrustServerApplication {

   public static void main(String[] args) {
       SpringApplication.run(ZeroTrustServerApplication.class, args);
   }
}

Testing the server application

Key generation and Configuration

Key Generation – The key generation, specifically the creation of keystore and server certificates, is crucial for setting up TLS (Transport Layer Security) and mutual TLS (mTLS). Below is a more detailed explanation on how to generate keys with self-signed certificates. In production environment, the keys typically are provided by certification provider but for our test purpose, we will generate self-signed certificate using openssl key generation methods.

  • Generate server private key and certificate
openssl req -x509 -newkey rsa:4096 -keyout server-key.pem -out server-cert.pem -days 365 -nodes -subj '/CN=localhost'
  • Convert the certificate and key to PKCS12 format
openssl pkcs12 -export -in server-cert.pem -inkey server-key.pem -out server-keystore.p12 -name serveralias
  • Generate client truststore and import server certificate
keytool -import -alias serveralias -file server-cert.pem -keystore client-truststore.p12 -storepass password -storetype PKCS12 -noprompt

Server Application Properties Configuration – We will configure the required server application configuration, such as SSL, key-store, trust-store, passwords, etc inside the src/main/resource/application.properties file.

spring.application.name=zero-trust-server

server.port=8443

server.ssl.key-store=classpath:server-keystore.p12
server.ssl.key-store-password=[your_keystore_password]
server.ssl.key-store-type=PKCS12
server.ssl.key-alias=serveralias

server.ssl.trust-store=classpath:server-truststore.p12
server.ssl.trust-store-password=[your_trustore_password]

server.ssl.enabled-protocols=TLSv1.2

security.jwt.token.secret-key=[secret_key_for_testing_client_server_communication_20240818]
security.jwt.token.expire-length=3600000

Building, running and sending requests

Building and running our server application – the zero trust server application is meant to receive and process registration, authentication and authorization requests from the client side / frontend application. The server and client need to establish end-to-end security prior to exchanging information. We can start the server application in terminal with following commands:

mvn clean
mvn compile
mvn package
java -jar zeroTrustServer.jar

Sending request in the application middleware – now the server can process registration, authentication and authorization request from the middleware application layer. We can use the following cURL request from the terminal to test our endpoints:

curl --key src/resources/server-key.pem \
     --cert src/resources/server-key.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:8443/api/register

The output of below demonstrates what a successful registration request looks like:

Successful request
HTTP/1.1 201 Created
Location: http://localhost:8443/api/getUser/{id}
Content-Length: 0

If the request is bad, for instance if the username is already registered would look the following:

HTTP/1.1 400 Bad Request
Content-Type: application/json
{
    "status": "error",
    "message": "Username already exists. Please choose a different one."
}

For /api/authenticate endpoint the request and the corresponding successful and unsuccessful request, for example, if the authentication request is sent with invalid certificate is demonstrated bellow:

# Authenticate request
curl --key src/resources/server-key.pem \
     --cert src/resources/server-key.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:8443/api/authenticate

# Response with valid certificates
HTTP/1.1 200 OK
Content-Type: application/json
{
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

# Response with invalide certificates

{
    "status": "error",
    "message": "Invalid or improperly formatted keys or certificates."
}

The output below demonstrates request and response for /api/getUsers endpoint:

# Request to list all registered/authenticated user
curl --key src/resources/server-key.pem \
     --cert src/resources/server-key.pem \
     --cacert src/resources/server-cert.pem \
     -H "Content-Type: application/json" \
     https://localhost:8443/api/getUsers

# The response for for get users request

[
    {
        "id": 1,
        "username": "[email protected]",
        "password": "*******",
        "owner": "USER",
    },
   {
        "id": 2,
        "username": "[email protected]",
        "password": "*******",
        "owner": "USER",
    }
]

Summary

In this article, we provided an overview of the Zero Trust cybersecurity model and detailed its application in a Spring Boot environment by implementing the server application. Zero Trust operates on the principle that no entity, inside or outside the network, should be trusted by default. We focused on ensuring secure communication throughout user registration, authentication, and authorization processes using JSON Web Tokens (JWTs).

The approach involves using JWTs to manage secure token exchange, which helps in validating user identities and controlling access to resources. We also implemented role-based access control (RBAC) to enforce permissions based on user roles, ensuring that users can only access resources they are authorized to use. The system is designed to be flexible according to your application’s security requirements. This ensures that the security measures can be tailored to meet unique needs, while maintaining robust protection against unauthorized access.

To complete the implementation of Zero Trust security model described in this article, we provide a detailed guide on how to integrate a Zero Trust Client application in the article:- Zero trust architecture – Client Implementation and secure communication. The article outlines the architecture and necessary steps for building secure communication between clients and servers, which aligns with Zero Trust principles.

References

Footer