Skip to content

TLS/SSL & mTLS - Secure Transport Implementation

Transport layer security is crucial for protecting data in transit. This chapter covers HTTPS configuration, SSL certificate management, mutual TLS (mTLS) implementation, and certificate rotation strategies in Spring Boot applications.

HTTPS Configuration in Spring Boot

SSL Certificate Management and KeyStore Setup

Setting up HTTPS with SSL certificates in Spring Boot:

yaml
server:
  port: 8443
  ssl:
    enabled: true
    key-store: classpath:keystore/server-keystore.p12
    key-store-password: ${KEYSTORE_PASSWORD}
    key-store-type: PKCS12
    key-alias: server
    key-password: ${KEY_PASSWORD}
    trust-store: classpath:keystore/server-truststore.p12
    trust-store-password: ${TRUSTSTORE_PASSWORD}
    trust-store-type: PKCS12
    client-auth: want
    protocol: TLS
    enabled-protocols: TLSv1.3,TLSv1.2
    ciphers: 
      - TLS_AES_256_GCM_SHA384
      - TLS_CHACHA20_POLY1305_SHA256
      - TLS_AES_128_GCM_SHA256
      - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
      - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256

Advanced SSL Configuration

java
@Configuration
@EnableWebSecurity
public class SSLConfiguration {
    
    @Value("${server.ssl.key-store}")
    private String keyStorePath;
    
    @Value("${server.ssl.key-store-password}")
    private String keyStorePassword;
    
    @Value("${server.ssl.trust-store}")
    private String trustStorePath;
    
    @Value("${server.ssl.trust-store-password}")
    private String trustStorePassword;
    
    @Bean
    public TomcatServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
            @Override
            protected void postProcessContext(Context context) {
                SecurityConstraint securityConstraint = new SecurityConstraint();
                securityConstraint.setUserConstraint("CONFIDENTIAL");
                SecurityCollection collection = new SecurityCollection();
                collection.addPattern("/*");
                securityConstraint.addCollection(collection);
                context.addConstraint(securityConstraint);
            }
        };
        
        tomcat.addAdditionalTomcatConnectors(redirectConnector());
        return tomcat;
    }
    
    private Connector redirectConnector() {
        Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
        connector.setScheme("http");
        connector.setPort(8080);
        connector.setSecure(false);
        connector.setRedirectPort(8443);
        return connector;
    }
    
    @Bean
    public SSLContext sslContext() throws Exception {
        // Load KeyStore
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        try (InputStream keyStoreInputStream = getClass().getClassLoader()
                .getResourceAsStream(keyStorePath.replace("classpath:", ""))) {
            keyStore.load(keyStoreInputStream, keyStorePassword.toCharArray());
        }
        
        // Initialize KeyManagerFactory
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
            KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore, keyStorePassword.toCharArray());
        
        // Load TrustStore
        KeyStore trustStore = KeyStore.getInstance("PKCS12");
        try (InputStream trustStoreInputStream = getClass().getClassLoader()
                .getResourceAsStream(trustStorePath.replace("classpath:", ""))) {
            trustStore.load(trustStoreInputStream, trustStorePassword.toCharArray());
        }
        
        // Initialize TrustManagerFactory
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
            TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(trustStore);
        
        // Create SSL Context
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(
            keyManagerFactory.getKeyManagers(),
            trustManagerFactory.getTrustManagers(),
            new SecureRandom()
        );
        
        return sslContext;
    }
}

Self-Signed Certificates for Development

Creating self-signed certificates using OpenSSL:

bash
#!/bin/bash

# Generate private key
openssl genrsa -out server.key 2048

# Generate certificate signing request
openssl req -new -key server.key -out server.csr -config <(
cat <<EOF
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn

[dn]
C=US
ST=California
L=San Francisco
O=MyCompany
OU=Development
CN=localhost

[req_ext]
subjectAltName = @alt_names

[alt_names]
DNS.1 = localhost
DNS.2 = *.localhost
IP.1 = 127.0.0.1
IP.2 = ::1
EOF
)

# Generate self-signed certificate
openssl x509 -req -in server.csr -signkey server.key -out server.crt -days 365 \
    -extensions req_ext -extfile <(
cat <<EOF
[req_ext]
subjectAltName = @alt_names

[alt_names]
DNS.1 = localhost
DNS.2 = *.localhost
IP.1 = 127.0.0.1
IP.2 = ::1
EOF
)

# Convert to PKCS12 format
openssl pkcs12 -export -in server.crt -inkey server.key -out server-keystore.p12 \
    -name server -passout pass:changeit

# Create truststore
keytool -import -alias server -file server.crt -keystore server-truststore.p12 \
    -storetype PKCS12 -storepass changeit -noprompt

echo "SSL certificates generated successfully!"

Let's Encrypt Integration

java
@Component
public class LetsEncryptCertificateManager {
    
    private final AcmeClient acmeClient;
    private final DomainValidator domainValidator;
    
    public LetsEncryptCertificateManager(AcmeClient acmeClient, 
                                       DomainValidator domainValidator) {
        this.acmeClient = acmeClient;
        this.domainValidator = domainValidator;
    }
    
    public Certificate obtainCertificate(String domain) throws AcmeException {
        // Create account if not exists
        Account account = getOrCreateAccount();
        
        // Create order for domain
        Order order = account.newOrder()
            .domains(domain)
            .create();
        
        // Process authorizations
        for (Authorization auth : order.getAuthorizations()) {
            processAuthorization(auth);
        }
        
        // Generate key pair for certificate
        KeyPair domainKeyPair = KeyPairUtils.createKeyPair(2048);
        
        // Request certificate
        order.execute(domainKeyPair);
        
        // Wait for certificate to be ready
        waitForCertificate(order);
        
        // Download certificate
        Certificate certificate = order.getCertificate();
        
        // Store certificate
        storeCertificate(domain, certificate, domainKeyPair);
        
        return certificate;
    }
    
    private Account getOrCreateAccount() throws AcmeException {
        KeyPair accountKeyPair = loadOrCreateAccountKeyPair();
        
        return new AccountBuilder()
            .agreeToTermsOfService()
            .useKeyPair(accountKeyPair)
            .create(acmeClient.getSession());
    }
    
    private void processAuthorization(Authorization auth) throws AcmeException {
        String domain = auth.getIdentifier().getDomain();
        
        // Find HTTP challenge
        Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE);
        if (challenge == null) {
            throw new AcmeException("HTTP challenge not found for domain: " + domain);
        }
        
        // Setup challenge response
        domainValidator.setupChallengeResponse(
            challenge.getToken(), 
            challenge.getAuthorization()
        );
        
        // Trigger challenge
        challenge.trigger();
        
        // Wait for validation
        waitForValidation(challenge);
    }
    
    private void waitForValidation(Challenge challenge) throws AcmeException {
        int attempts = 10;
        while (challenge.getStatus() != Status.VALID && attempts-- > 0) {
            if (challenge.getStatus() == Status.INVALID) {
                throw new AcmeException("Challenge failed: " + challenge.getError());
            }
            
            try {
                Thread.sleep(3000L);
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
                throw new AcmeException("Validation interrupted", ex);
            }
            
            challenge.update();
        }
        
        if (challenge.getStatus() != Status.VALID) {
            throw new AcmeException("Challenge validation timeout");
        }
    }
    
    private void storeCertificate(String domain, Certificate certificate, KeyPair keyPair) {
        try {
            // Store private key
            Path keyPath = Paths.get("certificates", domain + ".key");
            Files.createDirectories(keyPath.getParent());
            
            try (FileWriter writer = new FileWriter(keyPath.toFile())) {
                writer.write(KeyPairUtils.getPrivateKeyPem(keyPair));
            }
            
            // Store certificate chain
            Path certPath = Paths.get("certificates", domain + ".crt");
            try (FileWriter writer = new FileWriter(certPath.toFile())) {
                certificate.writeTo(writer);
            }
            
            log.info("Certificate stored for domain: {}", domain);
        } catch (IOException e) {
            throw new RuntimeException("Failed to store certificate", e);
        }
    }
}

Mutual TLS (mTLS) Implementation

Certificate-Based Authentication

java
@Configuration
public class MutualTLSConfiguration {
    
    @Bean
    public X509AuthenticationProvider x509AuthenticationProvider() {
        X509AuthenticationProvider provider = new X509AuthenticationProvider();
        provider.setX509AuthoritiesPopulator(x509AuthoritiesPopulator());
        return provider;
    }
    
    @Bean
    public X509AuthoritiesPopulator x509AuthoritiesPopulator() {
        return new X509AuthoritiesPopulator() {
            @Override
            public Collection<? extends GrantedAuthority> getGrantedAuthorities(
                    X509Certificate userCert) {
                
                String clientCN = getCommonName(userCert);
                
                // Map certificate CN to authorities
                if (clientCN.startsWith("admin-")) {
                    return Arrays.asList(
                        new SimpleGrantedAuthority("ROLE_ADMIN"),
                        new SimpleGrantedAuthority("ROLE_USER")
                    );
                } else if (clientCN.startsWith("service-")) {
                    return Arrays.asList(
                        new SimpleGrantedAuthority("ROLE_SERVICE")
                    );
                } else {
                    return Arrays.asList(
                        new SimpleGrantedAuthority("ROLE_USER")
                    );
                }
            }
        };
    }
    
    @Bean
    public PreAuthenticatedAuthenticationProvider preAuthenticatedProvider() {
        PreAuthenticatedAuthenticationProvider provider = 
            new PreAuthenticatedAuthenticationProvider();
        provider.setPreAuthenticatedUserDetailsService(
            new X509UserDetailsService(x509AuthoritiesPopulator())
        );
        return provider;
    }
    
    private String getCommonName(X509Certificate certificate) {
        String dn = certificate.getSubjectX500Principal().getName();
        for (String part : dn.split(",")) {
            if (part.trim().startsWith("CN=")) {
                return part.trim().substring(3);
            }
        }
        return "unknown";
    }
}

X.509 Certificate Filter

java
@Component
public class X509CertificateFilter extends AbstractPreAuthenticatedProcessingFilter {
    
    private final CertificateValidator certificateValidator;
    
    public X509CertificateFilter(CertificateValidator certificateValidator) {
        this.certificateValidator = certificateValidator;
        setAuthenticationManager(new NoOpAuthenticationManager());
    }
    
    @Override
    protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
        X509Certificate[] certificates = extractCertificates(request);
        
        if (certificates == null || certificates.length == 0) {
            return null;
        }
        
        X509Certificate clientCert = certificates[0];
        
        // Validate certificate
        if (!certificateValidator.validate(clientCert)) {
            throw new BadCredentialsException("Invalid client certificate");
        }
        
        return clientCert.getSubjectX500Principal().getName();
    }
    
    @Override
    protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
        X509Certificate[] certificates = extractCertificates(request);
        return certificates != null ? certificates[0] : null;
    }
    
    private X509Certificate[] extractCertificates(HttpServletRequest request) {
        return (X509Certificate[]) request.getAttribute(
            "jakarta.servlet.request.X509Certificate"
        );
    }
}

Certificate Validation Service

java
@Service
public class CertificateValidator {
    
    private final CertificateRepository certificateRepository;
    private final CRLService crlService;
    private final OCSPService ocspService;
    
    public CertificateValidator(CertificateRepository certificateRepository,
                              CRLService crlService,
                              OCSPService ocspService) {
        this.certificateRepository = certificateRepository;
        this.crlService = crlService;
        this.ocspService = ocspService;
    }
    
    public boolean validate(X509Certificate certificate) {
        try {
            // Check certificate validity period
            certificate.checkValidity();
            
            // Check if certificate is in our trusted store
            if (!isTrustedCertificate(certificate)) {
                log.warn("Certificate not in trusted store: {}", 
                    certificate.getSubjectX500Principal().getName());
                return false;
            }
            
            // Check certificate revocation status
            if (isRevoked(certificate)) {
                log.warn("Certificate is revoked: {}", 
                    certificate.getSubjectX500Principal().getName());
                return false;
            }
            
            // Additional custom validation
            return performCustomValidation(certificate);
            
        } catch (CertificateExpiredException | CertificateNotYetValidException e) {
            log.error("Certificate validity check failed", e);
            return false;
        }
    }
    
    private boolean isTrustedCertificate(X509Certificate certificate) {
        String fingerprint = calculateFingerprint(certificate);
        return certificateRepository.existsByFingerprint(fingerprint);
    }
    
    private boolean isRevoked(X509Certificate certificate) {
        // Check CRL
        if (crlService.isRevoked(certificate)) {
            return true;
        }
        
        // Check OCSP
        return ocspService.isRevoked(certificate);
    }
    
    private boolean performCustomValidation(X509Certificate certificate) {
        // Check key usage
        boolean[] keyUsage = certificate.getKeyUsage();
        if (keyUsage != null && !keyUsage[0]) { // Digital signature
            log.warn("Certificate does not allow digital signature");
            return false;
        }
        
        // Check extended key usage
        try {
            List<String> extendedKeyUsage = certificate.getExtendedKeyUsage();
            if (extendedKeyUsage != null && 
                !extendedKeyUsage.contains("1.3.6.1.5.5.7.3.2")) { // Client authentication
                log.warn("Certificate not valid for client authentication");
                return false;
            }
        } catch (CertificateParsingException e) {
            log.error("Failed to parse extended key usage", e);
            return false;
        }
        
        return true;
    }
    
    private String calculateFingerprint(X509Certificate certificate) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] digest = md.digest(certificate.getEncoded());
            return Base64.getEncoder().encodeToString(digest);
        } catch (Exception e) {
            throw new RuntimeException("Failed to calculate certificate fingerprint", e);
        }
    }
}

Advanced Certificate Management

Certificate Rotation Strategy

java
@Service
public class CertificateRotationService {
    
    private final CertificateStore certificateStore;
    private final NotificationService notificationService;
    private final ApplicationEventPublisher eventPublisher;
    
    @Value("${certificate.rotation.days-before-expiry:30}")
    private int daysBeforeExpiry;
    
    public CertificateRotationService(CertificateStore certificateStore,
                                    NotificationService notificationService,
                                    ApplicationEventPublisher eventPublisher) {
        this.certificateStore = certificateStore;
        this.notificationService = notificationService;
        this.eventPublisher = eventPublisher;
    }
    
    @Scheduled(cron = "0 0 1 * * ?") // Daily at 1 AM
    public void checkCertificateExpiry() {
        List<CertificateInfo> certificates = certificateStore.getAllCertificates();
        
        for (CertificateInfo cert : certificates) {
            if (isNearExpiry(cert)) {
                handleNearExpiryCertificate(cert);
            }
        }
    }
    
    private boolean isNearExpiry(CertificateInfo certificate) {
        LocalDate expiryDate = certificate.getExpiryDate();
        LocalDate checkDate = LocalDate.now().plusDays(daysBeforeExpiry);
        return expiryDate.isBefore(checkDate);
    }
    
    private void handleNearExpiryCertificate(CertificateInfo certificate) {
        log.info("Certificate nearing expiry: {}", certificate.getCommonName());
        
        try {
            // Attempt automatic renewal
            if (certificate.isAutoRenewable()) {
                renewCertificate(certificate);
            } else {
                // Send notification for manual renewal
                notificationService.sendCertificateExpiryNotification(certificate);
            }
        } catch (Exception e) {
            log.error("Failed to handle certificate rotation for: {}", 
                certificate.getCommonName(), e);
            notificationService.sendCertificateRotationFailureNotification(certificate, e);
        }
    }
    
    @Async
    public void renewCertificate(CertificateInfo certificateInfo) {
        try {
            log.info("Starting certificate renewal for: {}", certificateInfo.getCommonName());
            
            // Generate new certificate
            X509Certificate newCertificate = generateNewCertificate(certificateInfo);
            
            // Validate new certificate
            if (!validateNewCertificate(newCertificate)) {
                throw new CertificateException("New certificate validation failed");
            }
            
            // Store new certificate
            certificateStore.storeCertificate(certificateInfo.getCommonName(), newCertificate);
            
            // Schedule old certificate removal
            scheduleOldCertificateRemoval(certificateInfo);
            
            // Publish rotation event
            eventPublisher.publishEvent(new CertificateRotatedEvent(
                certificateInfo.getCommonName(), 
                certificateInfo.getExpiryDate(),
                newCertificate.getNotAfter().toInstant().atZone(ZoneId.systemDefault()).toLocalDate()
            ));
            
            log.info("Certificate renewal completed for: {}", certificateInfo.getCommonName());
            
        } catch (Exception e) {
            log.error("Certificate renewal failed for: {}", certificateInfo.getCommonName(), e);
            throw new CertificateRotationException("Certificate renewal failed", e);
        }
    }
    
    private X509Certificate generateNewCertificate(CertificateInfo certificateInfo) 
            throws Exception {
        // Implementation depends on certificate type (Let's Encrypt, internal CA, etc.)
        if (certificateInfo.getIssuer().equals("Let's Encrypt")) {
            return renewLetsEncryptCertificate(certificateInfo);
        } else {
            return renewInternalCertificate(certificateInfo);
        }
    }
    
    private boolean validateNewCertificate(X509Certificate certificate) {
        try {
            // Check validity
            certificate.checkValidity();
            
            // Check key usage
            return certificate.getKeyUsage() != null && certificate.getKeyUsage()[0];
            
        } catch (CertificateException e) {
            log.error("Certificate validation failed", e);
            return false;
        }
    }
    
    @Async
    @EventListener
    public void handleCertificateRotated(CertificateRotatedEvent event) {
        // Update application configuration
        updateSSLConfiguration(event.getCommonName());
        
        // Notify monitoring systems
        notificationService.sendCertificateRenewalSuccessNotification(event);
        
        // Update load balancer configuration if applicable
        updateLoadBalancerCertificate(event.getCommonName());
    }
}

Zero-Downtime Certificate Updates

java
@Component
public class GracefulCertificateUpdater {
    
    private final TomcatWebServer tomcatWebServer;
    private final CertificateStore certificateStore;
    
    public GracefulCertificateUpdater(TomcatWebServer tomcatWebServer,
                                    CertificateStore certificateStore) {
        this.tomcatWebServer = tomcatWebServer;
        this.certificateStore = certificateStore;
    }
    
    public void updateCertificateWithoutDowntime(String commonName, 
                                               X509Certificate newCertificate,
                                               PrivateKey privateKey) throws Exception {
        
        // Get Tomcat connector
        Connector connector = tomcatWebServer.getTomcat()
            .getService()
            .findConnectors()[0]; // HTTPS connector
        
        ProtocolHandler protocolHandler = connector.getProtocolHandler();
        
        if (protocolHandler instanceof AbstractHttp11JsseProtocol) {
            AbstractHttp11JsseProtocol<?> jsseProtocol = 
                (AbstractHttp11JsseProtocol<?>) protocolHandler;
            
            // Create new SSL context with updated certificate
            SSLContext newSSLContext = createSSLContext(newCertificate, privateKey);
            
            // Update SSL context on the connector
            jsseProtocol.setSslContext(newSSLContext);
            
            // Store new certificate
            certificateStore.storeCertificate(commonName, newCertificate);
            
            log.info("Certificate updated without downtime for: {}", commonName);
        } else {
            throw new UnsupportedOperationException(
                "Protocol handler does not support runtime SSL context updates");
        }
    }
    
    private SSLContext createSSLContext(X509Certificate certificate, PrivateKey privateKey) 
            throws Exception {
        
        // Create keystore with new certificate
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        keyStore.load(null, null);
        
        Certificate[] certificateChain = {certificate};
        keyStore.setKeyEntry("server", privateKey, "changeit".toCharArray(), certificateChain);
        
        // Initialize key manager
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
            KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore, "changeit".toCharArray());
        
        // Create SSL context
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(keyManagerFactory.getKeyManagers(), null, new SecureRandom());
        
        return sslContext;
    }
}

OpenSSL CA Setup

Setting up a Certificate Authority with OpenSSL:

bash
#!/bin/bash

# Create CA directory structure
mkdir -p ca/{certs,crl,newcerts,private}
cd ca

# Initialize CA database
touch index.txt
echo 1000 > serial
echo 1000 > crlnumber

# Create CA configuration
cat > openssl.cnf << EOF
[ ca ]
default_ca = CA_default

[ CA_default ]
dir               = .
certs             = \$dir/certs
crl_dir           = \$dir/crl
new_certs_dir     = \$dir/newcerts
database          = \$dir/index.txt
serial            = \$dir/serial
RANDFILE          = \$dir/private/.rand

private_key       = \$dir/private/ca.key.pem
certificate       = \$dir/certs/ca.cert.pem

crlnumber         = \$dir/crlnumber
crl               = \$dir/crl/ca.crl.pem
crl_extensions    = crl_ext
default_crl_days  = 30

default_md        = sha256
name_opt          = ca_default
cert_opt          = ca_default
default_days      = 375
preserve          = no
policy            = policy_strict

[ policy_strict ]
countryName             = match
stateOrProvinceName     = match
organizationName        = match
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

[ req ]
default_bits        = 2048
distinguished_name  = req_distinguished_name
string_mask         = utf8only
default_md          = sha256
x509_extensions     = v3_ca

[ req_distinguished_name ]
countryName                     = Country Name (2 letter code)
stateOrProvinceName             = State or Province Name
localityName                    = Locality Name
0.organizationName              = Organization Name
organizationalUnitName          = Organizational Unit Name
commonName                      = Common Name
emailAddress                    = Email Address

[ v3_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign

[ server_cert ]
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth

[ crl_ext ]
authorityKeyIdentifier=keyid:always
EOF

# Generate CA private key
openssl genrsa -aes256 -out private/ca.key.pem 4096
chmod 400 private/ca.key.pem

# Generate CA certificate
openssl req -config openssl.cnf -key private/ca.key.pem \
    -new -x509 -days 7300 -sha256 -extensions v3_ca \
    -out certs/ca.cert.pem

chmod 444 certs/ca.cert.pem

echo "Certificate Authority setup completed!"

This comprehensive TLS/SSL and mTLS implementation provides secure transport layer protection for Spring Boot applications with proper certificate management and rotation capabilities.

Created by Eren Demir.