Configure Spring Boot HTTPS with keytool Keystore and Truststore

Enable Spring Boot HTTPS on port 8443 with a PKCS12 keystore from keytool, set server.ssl.key-store in application.properties, add an optional truststore for private CAs, and verify with Java HttpClient.

Published

Updated

Read time 8 min read

Reviewed byDeepak Prasad

Spring Boot HTTPS with keytool keystore and truststore banner

Spring Boot can terminate TLS directly when you point server.ssl.* at a Java keystore built with keytool. You do not need Nginx or Apache in front for local development or small services — PKCS12 on disk plus a few application.properties lines is enough.

This guide creates a lab keystore with a PrivateKeyEntry alias https, wires Spring Boot 3.4.x application.properties, adds an optional truststore for clients that call your app over a private CA, and shows how a Java HttpClient with that truststore returns HTTP 200 and {"status":"UP"} from https://127.0.0.1:8443/actuator/health. I also document the startup failure you get when the keystore password is wrong.

Tested on: Ubuntu 26.04 LTS; OpenJDK 25.0.3; Spring Boot 3.4.5; kernel 7.0.0-27-generic.


Prerequisites

  • OpenJDK 11+ with keytool on PATH. See Install keytool on Ubuntu if the command is missing.
  • Spring Boot 3.x project (3.4.5 tested here) with spring-boot-starter-web.
  • Optional but used in this lab: spring-boot-starter-actuator so /actuator/health returns HTTP 200.
  • Understanding of keystore vs truststore in Java — the server loads a keystore for its identity; clients use a truststore to validate your certificate chain.

For a quick localhost certificate with SAN, you can start from Generate a self-signed certificate with keytool instead of the signed-chain lab below.


Create a PKCS12 keystore with keytool

Spring Boot expects a keystore file that contains a PrivateKeyEntry under the alias you configure. The lab below builds a small private CA, signs a server certificate with localhost and 127.0.0.1 SAN entries, imports the CA chain before the signed reply, and stores the result in server-keystore.p12.

Run the full sequence in one directory — order matters for importcert:

bash
mkdir -p ~/spring-boot-ssl-lab
cd ~/spring-boot-ssl-lab

keytool -genkeypair -alias demo-ca -keyalg RSA -keysize 2048 -validity 3650 \
  -dname "CN=Demo Lab CA,O=Lab,C=US" -ext "bc=ca:true" \
  -keystore ca.p12 -storetype PKCS12 -storepass changeit -noprompt

keytool -genkeypair -alias https -keyalg RSA -keysize 2048 -validity 365 \
  -dname "CN=localhost,O=Lab,C=US" \
  -ext "SAN=DNS:localhost,IP:127.0.0.1" \
  -keystore server-keystore.p12 -storetype PKCS12 -storepass changeit -noprompt

keytool -certreq -alias https \
  -keystore server-keystore.p12 \
  -storetype PKCS12 \
  -storepass changeit \
  -file server.csr \
  -ext "SAN=DNS:localhost,IP:127.0.0.1"

keytool -gencert -alias demo-ca -infile server.csr -outfile server.crt \
  -keystore ca.p12 -storetype PKCS12 -storepass changeit -validity 365 \
  -ext "SAN=DNS:localhost,IP:127.0.0.1" \
  -ext "EKU=serverAuth"

For the local keytool -gencert lab, SAN is added again during signing so the final certificate contains the extension. For a real PKI workflow, include SAN in the CSR and confirm your CA copies or applies the requested SAN values. In production you would send server.csr to your PKI team and import the returned .crt instead.

Use DNS: for hostnames and IP: for numeric addresses — do not put 127.0.0.1 under DNS:. Modern HTTPS hostname verification depends on SAN; many CAs will not infer it from CN alone.

Continue importing the signed certificate:

bash
keytool -exportcert -alias demo-ca \
  -keystore ca.p12 -storetype PKCS12 -storepass changeit \
  -rfc -file demo-ca.crt

keytool -importcert -alias demo-ca -file demo-ca.crt \
  -keystore server-keystore.p12 -storetype PKCS12 -storepass changeit -noprompt

keytool -importcert -alias https -file server.crt \
  -keystore server-keystore.p12 -storetype PKCS12 -storepass changeit -noprompt

Confirm the signed entry is a PrivateKeyEntry with a two-certificate chain (server leaf + lab CA):

bash
keytool -list -v \
  -keystore server-keystore.p12 \
  -storetype PKCS12 \
  -storepass changeit \
  -alias https | grep -E "Entry type|Certificate chain length|Owner:|Issuer:"
text
Entry type: PrivateKeyEntry
Certificate chain length: 2
Owner: CN=localhost, O=Lab, C=US
Issuer: CN=Demo Lab CA, O=Lab, C=US

Spring Boot refuses to start TLS if the alias points at a trustedCertEntry only. A quick alias summary:

bash
keytool -list -keystore server-keystore.p12 -storetype PKCS12 -storepass changeit
text
Keystore type: PKCS12
Keystore provider: SUN

Your keystore contains 2 entries

demo-ca, Jul 2, 2026, trustedCertEntry,
Certificate fingerprint (SHA-256): A8:6A:55:06:E2:06:33:1F:5E:B9:50:39:1D:83:E8:A2:10:87:6B:47:07:7F:8B:E6:FA:EF:5B:F8:E1:73:B0:F7
https, Jul 2, 2026, PrivateKeyEntry,
Certificate fingerprint (SHA-256): 05:DF:3D:97:2E:6D:1C:0F:B8:52:16:4C:99:2B:D3:23:A6:D5:24:67:D7:48:1B:F9:F8:00:70:C7:8C:EA:80:A2

demo-ca as trustedCertEntry gives the JVM enough anchor material to validate the signed server cert inside the same store.

Clients that call your app over HTTPS need a separate truststore with only the CA public certificate — not the server private key. Reuse demo-ca.crt from the export step above:

bash
keytool -importcert -alias demo-ca -file demo-ca.crt \
  -keystore truststore.p12 -storetype PKCS12 -storepass changeit -noprompt

keytool -list -keystore truststore.p12 -storetype PKCS12 -storepass changeit
text
Keystore type: PKCS12
Keystore provider: SUN

Your keystore contains 1 entry

demo-ca, Jul 2, 2026, trustedCertEntry,
Certificate fingerprint (SHA-256): A8:6A:55:06:E2:06:33:1F:5E:B9:50:39:1D:83:E8:A2:10:87:6B:47:07:7F:8B:E6:FA:EF:5B:F8:E1:73:B0:F7

If keytool -importcert replies with a chain error, see Fix keytool failed to establish chain from reply.


Configure Spring Boot application.properties

Point server.ssl.* at the PKCS12 file and the PrivateKeyEntry alias from keytool -list. Copy server-keystore.p12 into your Spring Boot project (for example src/main/resources/keystore.p12) or reference an absolute path on disk.

server.port=8443 is a lab choice — Spring Boot defaults to 8080 even when SSL is enabled.

properties
server.port=8443
server.ssl.enabled=true
server.ssl.key-store=file:/home/you/spring-boot-ssl-lab/server-keystore.p12
server.ssl.key-store-password=changeit
server.ssl.key-store-type=PKCS12
server.ssl.key-alias=https
Property Purpose
server.ssl.key-store Path to PKCS12/JKS file (classpath: or file: URI)
server.ssl.key-store-password Store password from keytool
server.ssl.key-store-type PKCS12 (recommended) or JKS
server.ssl.key-alias Alias of the PrivateKeyEntry (https in this lab)
NOTE
Use file: with an absolute path when the keystore lives outside the JAR. Inside the JAR, classpath:keystore.p12 works but remember the file is read-only at runtime — prefer a mounted volume in production.

Optional truststore for mutual TLS (client certificate authentication):

properties
# Only needed for mutual TLS / client certificate authentication
server.ssl.client-auth=need
server.ssl.trust-store=file:/home/you/spring-boot-ssl-lab/truststore.p12
server.ssl.trust-store-password=changeit
server.ssl.trust-store-type=PKCS12

Do not add server.ssl.trust-store for normal one-way HTTPS. The server keystore is enough for the server identity. Add server.ssl.trust-store plus server.ssl.client-auth=need only when Spring Boot must verify client certificates. For mTLS, the truststore must contain the CA that issued client certificates, which may or may not be the same CA that signed the server certificate.

For outbound HTTP from the same JVM to another internal HTTPS endpoint, prefer a custom truststore instead of editing cacerts and point javax.net.ssl.trustStore or a Spring Boot SSL bundle at that file.


Start Spring Boot and verify HTTPS

Start the application from the project root where application.properties lives:

bash
./mvnw spring-boot:run
# or: ./gradlew bootRun

On success, embedded Tomcat logs an HTTPS connector on port 8443:

text
Tomcat initialized with port 8443 (https)
Tomcat started on port 8443 (https) with context path '/'
Started SslDemoApplication in 7.656 seconds

If the keystore path or password is wrong, startup stops before this line — see the troubleshooting section below.

Quick check with curl (use the exported demo-ca.crt):

bash
curl --cacert demo-ca.crt https://127.0.0.1:8443/actuator/health
text
{"status":"UP"}

Verify with a Java client that loads truststore.p12 (same pattern as the keystore vs truststore lab). Save this as ClientTest.java in the lab directory and run it while the app is listening:

java
import javax.net.ssl.*;
import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
import java.security.*;

public class ClientTest {
    public static void main(String[] args) throws Exception {
        KeyStore ts = KeyStore.getInstance("PKCS12");
        try (var in = Files.newInputStream(Path.of("truststore.p12"))) {
            ts.load(in, "changeit".toCharArray());
        }
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(ts);
        SSLContext ctx = SSLContext.getInstance("TLS");
        ctx.init(null, tmf.getTrustManagers(), null);
        HttpResponse<String> r = HttpClient.newBuilder().sslContext(ctx).build()
            .send(HttpRequest.newBuilder(URI.create("https://127.0.0.1:8443/actuator/health")).GET().build(),
                HttpResponse.BodyHandlers.ofString());
        System.out.println("HTTP status: " + r.statusCode());
        System.out.println("Body: " + r.body());
    }
}
bash
javac ClientTest.java && java ClientTest
text
HTTP status: 200
Body: {"status":"UP"}

A 200 status means the TLS handshake succeeded and Spring Boot returned a response body. Without the custom truststore, the same URL fails with PKIX path building failed because the lab CA is not in cacerts.


Wrong keystore password at startup

Spring Boot reads the keystore during embedded Tomcat SSL initialization. A password mismatch surfaces before the web server accepts connections — the same keytool error you see on the command line:

bash
keytool -list -keystore server-keystore.p12 -storetype PKCS12 -storepass wrongpass
text
keytool error: java.io.IOException: keystore password was incorrect

Spring Boot 3.4.5 wraps that in an application startup failure similar to:

text
org.springframework.context.ApplicationContextException: Unable to start web server
...
Caused by: java.io.IOException: keystore password was incorrect

Match server.ssl.key-store-password to the value you used with keytool, or rotate the store password with keytool change alias, password, and delete (keytool -storepasswd).

Other common misconfigurations:

Symptom Likely cause
Alias not found server.ssl.key-alias does not match a PrivateKeyEntry name from keytool -list
File not found Wrong file: path or missing mount in Docker/Kubernetes
Handshake OK but browser warning Self-signed or private CA — expected until you import the CA
404 on /actuator/health Missing spring-boot-starter-actuator dependency

Package the keystore for deployment

Production deployments should keep passwords out of Git and mount keystores at runtime rather than baking them into image layers.

  • Mount the PKCS12 file as a Secret volume in Kubernetes rather than copying it into the JAR.
  • Externalize passwords with environment variables: server.ssl.key-store-password=${SSL_KEYSTORE_PASSWORD}.
  • Rotate certificates by replacing the mounted file and rolling the deployment; alias and property names can stay the same.

To convert legacy JKS to PKCS12 before migration, see Convert JKS to PKCS12 with keytool.


References


Summary

You enable Spring Boot HTTPS by pointing server.ssl.key-store at a PKCS12 file from keytool, setting the store password and PrivateKeyEntry alias, and setting server.port=8443 explicitly when you want HTTPS on that port (Spring Boot defaults to 8080). Clients that use a private CA need their own truststore — the server keystore does not replace that. I verified a Java HttpClient with truststore.p12 returns HTTP 200 and {"status":"UP"} from https://127.0.0.1:8443/actuator/health, and a wrong password produces keystore password was incorrect before the app finishes starting. For container fleets, pair this server keystore guide with Add Java truststore in Docker and Kubernetes.


Frequently Asked Questions

1. How do I enable HTTPS in Spring Boot with keytool?

Create a PKCS12 keystore with keytool -genkeypair (include SAN for localhost and 127.0.0.1), then set server.ssl.key-store, server.ssl.key-store-password, server.ssl.key-store-type=PKCS12, and server.ssl.key-alias in application.properties. Spring Boot serves HTTPS on whatever value you set in server.port. The default port is 8080, so set server.port=8443 explicitly if you want HTTPS on 8443.

2. What is the Spring Boot property for a Java truststore?

Use server.ssl.trust-store, server.ssl.trust-store-password, and server.ssl.trust-store-type for the embedded server trust material. For outbound RestClient or WebClient calls to a private CA, set javax.net.ssl.trustStore on the JVM or configure an SSL bundle in Spring Boot 3.1+.

3. Why does Spring Boot fail with keystore password was incorrect?

The password in application.properties does not match the keystore file. PKCS12 uses one store password for both entries in most keytool flows. Fix the typo or re-export the keystore with keytool -storepasswd.

4. Can Spring Boot use JKS instead of PKCS12?

Yes, set server.ssl.key-store-type=JKS and point server.ssl.key-store at a .jks file. PKCS12 is preferred on modern OpenJDK because JKS is legacy; keytool defaults to PKCS12 on OpenJDK 9+.

5. Do I need a truststore for Spring Boot HTTPS server mode?

The server keystore alone is enough for inbound HTTPS clients to connect (they validate your cert with their truststore). Add server.ssl.trust-store plus server.ssl.client-auth=need only when the embedded server must verify client certificates (mutual TLS).

6. How do I test Spring Boot HTTPS locally?

Use curl --cacert demo-ca.crt https://127.0.0.1:8443/actuator/health after exporting your lab CA, or run a Java HttpClient with a custom truststore that contains the signing CA. Browsers reject self-signed certs until you import the CA.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …