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
keytoolonPATH. 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-actuatorso/actuator/healthreturns HTTP200. - 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:
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:
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 -nopromptConfirm the signed entry is a PrivateKeyEntry with a two-certificate chain (server leaf + lab CA):
keytool -list -v \
-keystore server-keystore.p12 \
-storetype PKCS12 \
-storepass changeit \
-alias https | grep -E "Entry type|Certificate chain length|Owner:|Issuer:"Entry type: PrivateKeyEntry
Certificate chain length: 2
Owner: CN=localhost, O=Lab, C=US
Issuer: CN=Demo Lab CA, O=Lab, C=USSpring Boot refuses to start TLS if the alias points at a trustedCertEntry only. A quick alias summary:
keytool -list -keystore server-keystore.p12 -storetype PKCS12 -storepass changeitKeystore 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:A2demo-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:
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 changeitKeystore 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:F7If 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.
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) |
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):
# 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=PKCS12Do 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:
./mvnw spring-boot:run
# or: ./gradlew bootRunOn success, embedded Tomcat logs an HTTPS connector on port 8443:
Tomcat initialized with port 8443 (https)
Tomcat started on port 8443 (https) with context path '/'
Started SslDemoApplication in 7.656 secondsIf 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):
curl --cacert demo-ca.crt https://127.0.0.1:8443/actuator/health{"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:
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());
}
}javac ClientTest.java && java ClientTestHTTP 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:
keytool -list -keystore server-keystore.p12 -storetype PKCS12 -storepass wrongpasskeytool error: java.io.IOException: keystore password was incorrectSpring Boot 3.4.5 wraps that in an application startup failure similar to:
org.springframework.context.ApplicationContextException: Unable to start web server
...
Caused by: java.io.IOException: keystore password was incorrectMatch 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
- Spring Boot — SSL (official)
- Oracle keytool documentation (official)
- keytool cheat sheet
- Import a certificate chain with keytool
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.

