DEV Community

Zem Dev
Zem Dev

Posted on • Originally published at Medium

Dynamic SSL Configuration for Spring Boot Applications in AWS

In today's cloud-driven world, securing communication channels between services is crucial. For Spring Boot applications running on AWS, leveraging SSL with AWS Secrets Manager is an effective solution for managing sensitive keystore and truststore data. Below, we outline a method for dynamically configuring SSL in a Spring Boot application using AWS Secrets Manager.

Step 1: Generating SSL Certificates

Initially, you need to generate the required SSL certificates. The process involves creating a Certificate Authority (CA), a server private key, and a Certificate Signing Request (CSR). The server CSR is then signed with the CA to create a server certificate. These certificates are packaged into PKCS12 keystores and truststores, encoded in Base64, and stored in AWS Secrets Manager.

#!/bin/bash

set -e

# CONFIGURATION
KEYSTORE_PASSWORD="changeit"
TRUSTSTORE_PASSWORD="changeit"
SECRET_NAME="secret-name-keystore"
CA_ALIAS="my-internal-ca"
SERVER_ALIAS="server"
DNAME="/C=BR/ST=State/L=City/O=YourOrg/OU=YourUnit/CN=localhost"
CA_DNAME="/C=BR/ST=State/L=City/O=YourOrg/OU=YourUnit/CN=YourCA"
HOSTNAME="localhost"
JAVA_CACERTS="$JAVA_HOME/lib/security/cacerts"
JAVA_CACERTS_PASS="changeit"

# Generate CA
openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt \
  -subj "$CA_DNAME"

# Generate server private key and CSR
openssl req -new -newkey rsa:2048 -nodes -keyout server.key -out server.csr \
  -subj "$DNAME" \
  -addext "subjectAltName=DNS:$HOSTNAME"

# Sign server CSR with CA
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out server.crt -days 3650 -sha256 -extfile <(printf "subjectAltName=DNS:$HOSTNAME")

# Create PKCS12 keystore
openssl pkcs12 -export -in server.crt -inkey server.key -certfile ca.crt \
  -out keystore.p12 -name $SERVER_ALIAS -passout pass:$KEYSTORE_PASSWORD

# Create combined truststore (default JVM CAs + your CA)
cp $JAVA_CACERTS combined-truststore.p12
keytool -importcert -file ca.crt -keystore combined-truststore.p12 \
  -storetype PKCS12 -alias $CA_ALIAS -storepass $JAVA_CACERTS_PASS -noprompt

if [ "$TRUSTSTORE_PASSWORD" != "$JAVA_CACERTS_PASS" ]; then
  keytool -storepasswd -new $TRUSTSTORE_PASSWORD -keystore combined-truststore.p12 \
    -storetype PKCS12 -storepass $JAVA_CACERTS_PASS
fi

# Base64 encode keystore and truststore
base64 -w 0 keystore.p12 > keystore.p12.b64
base64 -w 0 combined-truststore.p12 > truststore.p12.b64

# Generate secret.json
cat > secret.json <<EOF
{
  "keystore": "$(cat keystore.p12.b64)",
  "keystorePassword": "$KEYSTORE_PASSWORD",
  "keystoreType": "PKCS12",
  "truststore": "$(cat truststore.p12.b64)",
  "truststorePassword": "$TRUSTSTORE_PASSWORD",
  "truststoreType": "PKCS12"
}
EOF

# Set secret in AWS
aws secretsmanager create-secret --name "$SECRET_NAME" --secret-string file://secret.json || \
aws secretsmanager update-secret --secret-id "$SECRET_NAME" --secret-string file://secret.json
Enter fullscreen mode Exit fullscreen mode

Step 2: Spring Boot Configuration

Once the certificates are securely stored, the next step is to configure the Spring Boot application to fetch and apply these SSL settings dynamically. This is achieved through a custom EnvironmentPostProcessor that retrieves the keystore and truststore from AWS Secrets Manager.

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder;
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest;

import java.io.FileOutputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;
import java.util.Map;

public class SslEnvironmentPostProcessor implements EnvironmentPostProcessor {

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        try {
            if (!environment.getProperty("server.ssl.enabled", Boolean.class, false)) {
                return;
            }

            try (SecretsManagerClient client = buildClient(environment)) {
                GetSecretValueRequest request = GetSecretValueRequest.builder()
                    .secretId("secret-name")
                    .build();

                String secret = client.getSecretValue(request).secretString();

                SslCerts sslCerts = parseSecret(secret);

                setProperties(environment, sslCerts);
            }
        } catch (Exception e) {
            throw new RuntimeException("Failed to load SSL properties from AWS Secrets Manager", e);
        }
    }

    private SecretsManagerClient buildClient(ConfigurableEnvironment environment) {
        return SecretsManagerClient.builder()
            .build();
    }

    private SslSecrets parseSecret(String secret) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        JsonNode secretJson = mapper.readTree(secret);

        return new SslCerts(
                secretJson.get("keystore").asText(),
                secretJson.get("keystorePassword").asText(),
                secretJson.get("keystoreType").asText(),
                secretJson.get("truststore").asText(),
                secretJson.get("truststorePassword").asText(),
                secretJson.get("truststoreType").asText()
        );
    }

    private Path writeTempFile(String encodedContent, String fileName) throws Exception {
        byte[] bytes = Base64.getDecoder().decode(encodedContent);
        Path tempFile = Files.createTempFile(fileName, ".p12");

        try (FileOutputStream fos = new FileOutputStream(tempFile.toFile())) {
            fos.write(bytes);
        }

        tempFile.toFile().deleteOnExit();

        return tempFile;
    }

    private void setProperties(ConfigurableEnvironment environment, SslCerts sslCerts) {
        Path keystore = writeTempFile(sslCerts.keystore, "keystore");
        Path truststore = writeTempFile(sslCerts.truststore, "truststore");

        Map<String, Object> sslProps = Map.of(  
                "server.ssl.key-store", keystore.toAbsolutePath().toString(),  
                "server.ssl.key-store-password", sslCerts.keystorePassword(),  
                "server.ssl.key-store-type", sslCerts.keystoreType(),  
                "server.ssl.trust-store", truststore.toAbsolutePath().toString(),  
                "server.ssl.trust-store-password", sslCerts.truststorePassword(),  
                "server.ssl.trust-store-type", sslCerts.truststoreType()  
        );

        environment.getPropertySources().addFirst(new MapPropertySource("aws-ssl", sslProps));  

        System.setProperty("javax.net.ssl.trustStore", truststore.toAbsolutePath().toString());  
        System.setProperty("javax.net.ssl.trustStorePassword", sslCerts.truststorePassword());  
        System.setProperty("javax.net.ssl.trustStoreType", sslCerts.truststoreType());  
    }

    private record SslCerts(  
            String keystore,  
            String keystorePassword,  
            String keystoreType,  
            String truststore,  
            String truststorePassword,  
            String truststoreType) {  
    }

}
Enter fullscreen mode Exit fullscreen mode

Upon startup, if SSL is enabled, the Spring Boot application dynamically retrieves the necessary secret from AWS Secrets Manager. This secret contains the encoded keystore and truststore data, which are then decoded and saved to temporary files. These files are automatically configured with the appropriate SSL properties, enabling secure communication for the application. This seamless integration ensures that sensitive SSL configurations are managed securely and efficiently, leveraging the power of AWS Secrets Manager to dynamically adapt the application's environment. It is only required that the user set the property server.ssl.enabled to true, and the code will handle the configuration of the SSL certificates.

By following these steps, you can ensure secure communication for your Spring Boot applications running in an AWS environment, leveraging the power of AWS Secrets Manager for dynamic SSL configuration.

Benefits of this Approach

Enhanced Security: Sensitive SSL credentials (keystore and truststore) are not directly embedded in the application code or configuration files, but are securely managed by AWS Secrets Manager.
Dynamic Configuration: The application fetches and applies SSL settings at runtime, allowing for certificate updates without requiring application redeployment. This ensures that sensitive SSL configurations are managed securely and efficiently, leveraging the power of AWS Secrets Manager to dynamically adapt the application's environment.
Developer Simplification: Once the solution is implemented, the user only needs to set the server.ssl.enabled property to true, and the SSL certificate configuration is automatically handled by the custom EnvironmentPostProcessor.
Seamless Integration: The solution integrates fluidly with the AWS ecosystem, leveraging native cloud services.

Top comments (0)