Create Java mTLS Keystore and Truststore with keytool

Create Java mTLS keystore and truststore files with keytool and OpenSSL, then run a mutual TLS HttpsServer lab that fails without a client cert and succeeds.

Published

Updated

Read time 11 min read

Reviewed byDeepak Prasad

Java mTLS keystore and truststore banner with two-way TLS handshake and keytool PKCS12 files

Mutual TLS (mTLS) means both sides present a certificate during the handshake. The server still needs a keystore with its private key, and the client still needs a truststore to validate the server — but the server also needs a truststore to validate client certificates, and the client needs its own keystore to prove who it is.

Most Spring Boot mTLS tutorials jump straight to application.properties. This guide stays on the files: create a lab CA, sign server and client certificates with the right extended key usage, build four PKCS12 stores with keytool, and run a minimal Java HttpsServer that requires a client cert. You will see certificate_required when the client has trust but no identity, then HTTP 200 once both sides load the right files. For the one-way TLS baseline, see Java keystore vs truststore. For JVM -D flags after the files exist, see Configure javax.net.ssl.trustStore and keyStore.

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


Four PKCS12 files in an mTLS deployment

File Role Entry types Loaded by
server.p12 Server identity PrivateKeyEntry (+ optional CA chain) Server KeyManagerFactory
server-trust.p12 Who the server trusts (client CAs) trustedCertEntry Server TrustManagerFactory
client.p12 Client identity PrivateKeyEntry Client KeyManagerFactory
client-trust.p12 Who the client trusts (server CA) trustedCertEntry Client TrustManagerFactory

One-way HTTPS uses the first and fourth rows only. mTLS adds the client identity keystore and the server truststore so each peer can validate the other.


Prerequisites

Password changeit everywhere below.


Step 1 — Create a private root CA

keytool can generate key pairs but does not replace a full CA workflow for issuing many leaf certificates. Create the root with OpenSSL:

bash
mkdir -p ~/java-mtls-lab/certs && cd ~/java-mtls-lab
STOREPASS=changeit

openssl genrsa -out certs/root-ca.key 4096
openssl req -new -x509 -days 3650 -key certs/root-ca.key -out certs/root-ca.crt \
  -subj "/CN=Lab mTLS Root CA/O=GoLinuxCloud Lab/C=US"

Confirm the self-signed root:

bash
openssl x509 -in certs/root-ca.crt -noout -subject -issuer
text
subject=CN=Lab mTLS Root CA, O=GoLinuxCloud Lab, C=US
issuer=CN=Lab mTLS Root CA, O=GoLinuxCloud Lab, C=US

Keep root-ca.key offline in production. Clients and servers only need root-ca.crt inside truststores.


Step 2 — Server identity keystore (server.p12)

Generate the server key inside PKCS12, export a CSR, sign it with the lab CA, then import the CA and signed certificate back — the same pattern as keystore vs truststore, with serverAuth extended key usage and an IP SAN for 127.0.0.1:

bash
keytool -genkeypair -alias server -keyalg RSA -keysize 2048 -validity 365 \
  -keystore server.p12 -storetype PKCS12 -storepass "$STOREPASS" \
  -dname "CN=mtls-server.lab.local, O=Lab, C=US" \
  -ext "san=dns:mtls-server.lab.local,ip:127.0.0.1"

keytool -certreq -alias server -keystore server.p12 -storetype PKCS12 \
  -storepass "$STOREPASS" -file certs/server.csr

Sign with OpenSSL (SAN + server EKU):

bash
cat > certs/server.ext << 'EOF'
subjectAltName = IP:127.0.0.1,DNS:mtls-server.lab.local
extendedKeyUsage = serverAuth
EOF
openssl x509 -req -in certs/server.csr -CA certs/root-ca.crt -CAkey certs/root-ca.key \
  -CAcreateserial -out certs/server.crt -days 365 -extfile certs/server.ext

Import CA then signed server cert:

bash
keytool -importcert -alias lab-root-ca -keystore server.p12 -storetype PKCS12 \
  -storepass "$STOREPASS" -file certs/root-ca.crt -noprompt
keytool -importcert -alias server -keystore server.p12 -storetype PKCS12 \
  -storepass "$STOREPASS" -file certs/server.crt -noprompt

List the server keystore:

bash
keytool -list -keystore server.p12 -storepass "$STOREPASS"
text
Keystore type: PKCS12

Your keystore contains 2 entries

lab-root-ca, Jul 3, 2026, trustedCertEntry,
server, Jul 3, 2026, PrivateKeyEntry,

The server alias is what KeyManagerFactory presents during TLS. The lab-root-ca entry helps Java build the chain toward your private CA.

Verify extensions on the signed file:

bash
openssl x509 -in certs/server.crt -noout -ext subjectAltName -ext extendedKeyUsage
text
X509v3 Subject Alternative Name:
    IP Address:127.0.0.1, DNS:mtls-server.lab.local
X509v3 Extended Key Usage:
    TLS Web Server Authentication

Use 127.0.0.1 in client URLs so the IP SAN matches. Do not disable hostname verification in production.


Step 3 — Client identity keystore (client.p12)

Client certificates need clientAuth in extended key usage. Generate the key pair with keytool, sign the CSR with the same root CA, import the chain:

bash
keytool -genkeypair -alias client -keyalg RSA -keysize 2048 -validity 365 \
  -keystore client.p12 -storetype PKCS12 -storepass "$STOREPASS" \
  -dname "CN=mtls-client.lab.local, O=Lab, C=US" \
  -ext "san=dns:mtls-client.lab.local" -ext "extendedKeyUsage=clientAuth"

keytool -certreq -alias client -keystore client.p12 -storetype PKCS12 \
  -storepass "$STOREPASS" -file certs/client.csr

Sign the client CSR:

bash
cat > certs/client.ext << 'EOF'
subjectAltName = DNS:mtls-client.lab.local
extendedKeyUsage = clientAuth
EOF
openssl x509 -req -in certs/client.csr -CA certs/root-ca.crt -CAkey certs/root-ca.key \
  -CAcreateserial -out certs/client.crt -days 365 -extfile certs/client.ext

Import CA and signed client certificate:

bash
keytool -importcert -alias lab-root-ca -keystore client.p12 -storetype PKCS12 \
  -storepass "$STOREPASS" -file certs/root-ca.crt -noprompt
keytool -importcert -alias client -keystore client.p12 -storetype PKCS12 \
  -storepass "$STOREPASS" -file certs/client.crt -noprompt
bash
keytool -list -keystore client.p12 -storepass "$STOREPASS"
text
Keystore type: PKCS12

Your keystore contains 2 entries

lab-root-ca, Jul 3, 2026, trustedCertEntry,
client, Jul 3, 2026, PrivateKeyEntry,

Check the client EKU:

bash
openssl x509 -in certs/client.crt -noout -ext extendedKeyUsage
text
X509v3 Extended Key Usage:
    TLS Web Client Authentication

Without clientAuth, some servers reject the certificate even when the chain is valid.


Step 4 — Server and client truststores

Truststores hold public certificates only — no private keys. Import the root CA into both sides so each peer can validate chains signed by that CA:

bash
keytool -importcert -alias lab-root-ca -file certs/root-ca.crt \
  -keystore server-trust.p12 -storetype PKCS12 -storepass "$STOREPASS" -noprompt

keytool -importcert -alias lab-root-ca -file certs/root-ca.crt \
  -keystore client-trust.p12 -storetype PKCS12 -storepass "$STOREPASS" -noprompt
bash
keytool -list -keystore server-trust.p12 -storepass "$STOREPASS"
text
Keystore type: PKCS12

Your keystore contains 1 entry

lab-root-ca, Jul 3, 2026, trustedCertEntry,

client-trust.p12 has the same single trustedCertEntry. The server uses server-trust.p12 to validate client certs issued by lab-root-ca. The client uses client-trust.p12 to validate the server cert.

NOTE
You can import individual client .crt files into server-trust.p12 instead of the root CA when you want to allow only named clients. Trusting the root CA is simpler when every legitimate client chains to the same issuer. For production, prefer an intermediate CA dedicated to client identities instead of trusting a broad corporate root CA in server-trust.p12.

Step 5 — Java mTLS server (setNeedClientAuth)

Save as MtlsServer.java. The server loads server.p12 for identity and server-trust.p12 for client validation, then requires a client certificate on every connection:

java
import com.sun.net.httpserver.*;
import javax.net.ssl.*;
import java.io.*;
import java.net.InetSocketAddress;
import java.security.KeyStore;
import java.util.concurrent.Executors;

public class MtlsServer {
    private static final char[] PASS = "changeit".toCharArray();

    public static void main(String[] args) throws Exception {
        KeyStore ks = KeyStore.getInstance("PKCS12");
        try (FileInputStream in = new FileInputStream("server.p12")) {
            ks.load(in, PASS);
        }
        KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        kmf.init(ks, PASS);

        KeyStore ts = KeyStore.getInstance("PKCS12");
        try (FileInputStream in = new FileInputStream("server-trust.p12")) {
            ts.load(in, PASS);
        }
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(ts);

        SSLContext ctx = SSLContext.getInstance("TLS");
        ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);

        HttpsServer server = HttpsServer.create(new InetSocketAddress("127.0.0.1", 8443), 0);
        server.setHttpsConfigurator(new HttpsConfigurator(ctx) {
            public void configure(HttpsParameters p) {
                p.setNeedClientAuth(true);
            }
        });
        server.createContext("/", ex -> {
            ex.sendResponseHeaders(200, 2);
            try (OutputStream os = ex.getResponseBody()) {
                os.write("OK".getBytes());
            }
        });
        server.setExecutor(Executors.newSingleThreadExecutor());
        server.start();
        System.out.println("mTLS server listening on https://127.0.0.1:8443/");
    }
}

Compile and start in one terminal:

bash
javac MtlsServer.java
java MtlsServer
text
mTLS server listening on https://127.0.0.1:8443/

setNeedClientAuth(true) is the line that turns one-way TLS into mutual TLS. Without a loaded server truststore, the server cannot validate whatever certificate the client might present.


Step 6 — Java mTLS client (fail then succeed)

Save as MtlsClient.java. Pass with-client-key to load client.p12 through KeyManagerFactory; always load client-trust.p12 for server validation:

java
import javax.net.ssl.*;
import java.io.*;
import java.net.URL;
import java.security.KeyStore;

public class MtlsClient {
    public static void main(String[] args) throws Exception {
        boolean withKey = args.length > 0 && "with-client-key".equals(args[0]);
        char[] pass = "changeit".toCharArray();

        KeyStore ts = KeyStore.getInstance("PKCS12");
        try (FileInputStream in = new FileInputStream("client-trust.p12")) {
            ts.load(in, pass);
        }
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(ts);

        KeyManager[] kms = null;
        if (withKey) {
            KeyStore ks = KeyStore.getInstance("PKCS12");
            try (FileInputStream in = new FileInputStream("client.p12")) {
                ks.load(in, pass);
            }
            KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
            kmf.init(ks, pass);
            kms = kmf.getKeyManagers();
        }

        SSLContext ctx = SSLContext.getInstance("TLS");
        ctx.init(kms, tmf.getTrustManagers(), null);
        HttpsURLConnection.setDefaultSSLSocketFactory(ctx.getSocketFactory());

        try {
            HttpsURLConnection c = (HttpsURLConnection) new URL("https://127.0.0.1:8443/").openConnection();
            c.setConnectTimeout(5000);
            c.setReadTimeout(5000);
            c.connect();
            System.out.println("HTTP status: " + c.getResponseCode());
        } catch (Exception e) {
            String msg = e.getMessage();
            if (msg != null && msg.contains("\n")) {
                msg = msg.split("\n")[0];
            }
            System.out.println("FAILED: " + e.getClass().getSimpleName() + ": " + msg);
        }
    }
}

With MtlsServer running, compile and run twice.

Client with truststore only (no identity)

bash
javac MtlsClient.java
java MtlsClient

The server requires a client certificate. The client trusts the server but never sends its own cert:

text
FAILED: SSLHandshakeException: (certificate_required) Received fatal alert: certificate_required

Depending on JDK version and TLS protocol, the client may show certificate_required, bad_certificate, or Remote host terminated the handshake. The key point is that the server requested a client certificate and the client did not present one.

That alert is expected mTLS behavior — not a PKIX trust failure. The fix is loading client.p12 as a keystore, not adding more CAs to client-trust.p12.

Client with identity + truststore

bash
java MtlsClient with-client-key
text
HTTP status: 200

Two-way TLS completed: the client validated the server chain against client-trust.p12, presented the client PrivateKeyEntry, and the server validated that chain against server-trust.p12.


Step 7 — Same client with JVM system properties

To prove the javax.net.ssl.* properties work, use a client that does not manually load client.p12 or client-trust.p12. Save as MtlsClientSystemProps.java:

java
import javax.net.ssl.HttpsURLConnection;
import java.net.URL;

public class MtlsClientSystemProps {
    public static void main(String[] args) throws Exception {
        try {
            HttpsURLConnection c = (HttpsURLConnection) new URL("https://127.0.0.1:8443/").openConnection();
            c.setConnectTimeout(5000);
            c.setReadTimeout(5000);
            c.connect();
            System.out.println("HTTP status: " + c.getResponseCode());
        } catch (Exception e) {
            String msg = e.getMessage();
            if (msg != null && msg.contains("\n")) {
                msg = msg.split("\n")[0];
            }
            System.out.println("FAILED: " + e.getClass().getSimpleName() + ": " + msg);
        }
    }
}

Compile MtlsClientSystemProps while MtlsServer is running in another terminal:

bash
javac MtlsClientSystemProps.java

With MtlsServer still running, run without JVM properties:

bash
java MtlsClientSystemProps

The client falls back to JSSE default truststore lookup and has no identity keystore, so the handshake fails before HTTP:

text
FAILED: SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

Now pass both truststore and keystore properties documented in Oracle's JSSE reference guide. mTLS clients need all six flags — trust alone still leaves the server waiting for a client certificate:

Property File in this lab
javax.net.ssl.trustStore client-trust.p12 — validates the server chain
javax.net.ssl.keyStore client.p12 — presents the client PrivateKeyEntry
*Password / *Type changeit and PKCS12 for both files
bash
java \
  -Djavax.net.ssl.trustStore=client-trust.p12 \
  -Djavax.net.ssl.trustStorePassword=changeit \
  -Djavax.net.ssl.trustStoreType=PKCS12 \
  -Djavax.net.ssl.keyStore=client.p12 \
  -Djavax.net.ssl.keyStorePassword=changeit \
  -Djavax.net.ssl.keyStoreType=PKCS12 \
  MtlsClientSystemProps
text
HTTP status: 200

This version proves that JSSE picked up the system properties because the client code does not load any keystore or truststore manually — the same pattern you would use for a packaged JAR you cannot recompile.

For Spring Boot, map the same files to server.ssl.key-store, server.ssl.trust-store, and server.ssl.client-auth=need as described in Spring Boot HTTPS with keytool.


Troubleshooting

Symptom Likely cause Fix
certificate_required Server has needClientAuth but client has no keystore Load client.p12 via KeyManagerFactory or javax.net.ssl.keyStore
PKIX path building failed on client Server cert not signed by a CA in client-trust.p12 Import issuing CA with keytool -importcert
PKIX path building failed on server Client cert not chained to a CA in server-trust.p12 Import root CA or specific client .crt into server-trust.p12
Client certificate rejected even though CA is trusted Client certificate missing clientAuth EKU or wrong certificate purpose Reissue the client certificate with extendedKeyUsage = clientAuth
Client rejects server certificate purpose Server certificate missing serverAuth EKU Reissue the server certificate with extendedKeyUsage = serverAuth
Handshake OK but wrong hostname URL hostname does not match server SAN Use 127.0.0.1 when cert has IP SAN, or fix DNS SAN
Server accepts any client setWantClientAuth instead of setNeedClientAuth Use setNeedClientAuth(true) when client certs are mandatory

Enable JSSE debug when PKIX messages are unclear:

bash
java -Djavax.net.debug=ssl,handshake MtlsClient with-client-key

Trim the log to the certificate_required or PKIX block — full debug is verbose.


References

  • keytool man pagePrivateKeyEntry, trustedCertEntry, -genkeypair, -importcert
  • Java Secure Socket Extension (JSSE) reference guide — javax.net.ssl.* properties
  • RFC 8446 — TLS 1.3 — client certificate authentication

Summary

mTLS adds two files to the familiar TLS pair: a client identity keystore and a server truststore. Use keytool to generate PKCS12 key pairs, export CSRs, import signed certificates with the right extended key usage (serverAuth / clientAuth), and build truststores with keytool -importcert of your root CA. On the server, load both stores into SSLContext and call setNeedClientAuth(true). On the client, load client-trust.p12 for server validation and client.p12 for identity — trust alone triggers certificate_required, and identity plus trust returns HTTP 200.


Frequently Asked Questions

1. What files do I need for Java mTLS?

At minimum four PKCS12 files: server keystore (server identity), client keystore (client identity), server truststore (CAs or client certs the server trusts), and client truststore (CAs the client trusts to validate the server). One-way TLS needs only server keystore plus client truststore; mTLS adds client keystore and server truststore.

2. Does keytool create client certificates for mTLS?

Use keytool -genkeypair to create the client key pair inside PKCS12, keytool -certreq to export a CSR, sign the CSR with your CA (OpenSSL or corporate PKI), then keytool -importcert for the CA and signed client certificate. Set extendedKeyUsage=clientAuth on the signed cert so the server accepts it as a TLS client identity.

3. How does a Java server require client certificates?

On com.sun.net.httpserver.HttpsServer, call p.setNeedClientAuth(true) inside HttpsConfigurator.configure. On embedded Tomcat or Spring Boot, enable client authentication in the connector or server.ssl.client-auth property. The server must also load a truststore with TrustManagerFactory so it can validate presented client certificates.

4. Why do I get certificate_required during mTLS?

The server sent a TLS fatal alert because setNeedClientAuth(true) is enabled but the client did not present a certificate. Load the client identity keystore with KeyManagerFactory and pass those KeyManagers into SSLContext.init, or set javax.net.ssl.keyStore system properties before the client connects.

5. Can I put the root CA only in the server truststore?

Yes. If client certificates chain to that root CA, PKIX validation succeeds without importing each client leaf. Import individual client .crt files only when you want to trust specific clients without trusting every cert issued by the CA.

6. Is mTLS the same as Spring Boot SSL client-auth?

Same TLS mechanism. Spring Boot maps server.ssl.key-store to server identity, trust-store to server-side peer trust, and client SSL bundles or RestClient SSLContext for outbound mTLS. This guide builds the PKCS12 files those properties point at using keytool and OpenSSL signing.
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 …