Production TLS on Java apps rarely stays self-signed. You generate a key pair in a keystore, export a certificate signing request (CSR), send it to a public CA or internal PKI team, and import the signed reply back into the same alias. keytool handles the keystore side; OpenSSL (or your CA portal) handles signing.
This guide runs the full loop on Ubuntu: keytool -genkeypair, keytool -certreq, sign the CSR with a local intermediate CA via OpenSSL, then keytool -importcert to replace the bootstrap self-signed certificate. The private key never leaves the PKCS12 file.
Tested on: Ubuntu 26.04 LTS; OpenJDK 25.0.3; kernel 7.0.0-27-generic.
Prerequisites
Confirm tooling and background before you generate keys — the CSR flow assumes a server identity keystore, not a client truststore.
- Install keytool on Ubuntu (OpenJDK 11+; 25 tested here).
- Install OpenSSL on Ubuntu for the lab CA signing step.
- Familiarity with keystore vs truststore — this guide builds a server identity keystore, not a client truststore.
Keep a keytool cheat sheet handy for -list, -exportcert, and password flags.
Lab layout
The lab keeps CA material separate from the server keystore so you can practice signing without touching production PKI. The server PKCS12 file holds the private key for the entire workflow.
~/keytool-lab/
├── ca/
│ ├── root-ca.crt # long-lived root (trust anchor)
│ ├── root-ca.key
│ ├── intermediate-ca.crt # issues server certs
│ └── intermediate-ca.key
└── server/
├── https-san.p12 # keytool keystore (private key lives here)
├── https.csr # CSR you submit to the CA
└── https-signed.crt # CA reply you importIf you do not have a CA yet, create a root and intermediate with OpenSSL (one-time setup). These commands produce four files under ~/keytool-lab/ca/:
mkdir -p ~/keytool-lab/ca ~/keytool-lab/server
cd ~/keytool-lab/ca
openssl genrsa -out root-ca.key 4096
openssl req -new -x509 -days 3650 -key root-ca.key -out root-ca.crt \
-subj "/C=US/O=GoLinuxCloud/CN=Lab Root CA"
openssl genrsa -out intermediate-ca.key 4096
openssl req -new -key intermediate-ca.key -out intermediate-ca.csr \
-subj "/C=US/O=GoLinuxCloud/CN=Lab Intermediate CA"
openssl x509 -req -in intermediate-ca.csr -CA root-ca.crt -CAkey root-ca.key \
-CAcreateserial -out intermediate-ca.crt -days 1825 -sha256 \
-extfile <(printf "basicConstraints=CA:TRUE,pathlen:0\nkeyUsage=critical,keyCertSign,cRLSign")<(...) syntax requires Bash. If your shell does not support process substitution, write the extension content to a temporary file and pass that file with -extfile.
When both CA commands finish without errors, root-ca.crt, root-ca.key, intermediate-ca.crt, and intermediate-ca.key are ready. You sign server CSRs with the intermediate key in the steps below.
Step 1 — Generate the key pair and self-signed bootstrap cert
keytool -genkeypair creates the PrivateKeyEntry and a temporary self-signed certificate. That self-signed cert is replaced after import; it exists only so the alias has a full entry while you build the CSR.
cd ~/keytool-lab/server
keytool -genkeypair \
-alias https \
-keyalg RSA \
-keysize 2048 \
-validity 365 \
-dname "CN=app.lab.local, OU=Lab, O=GoLinuxCloud, C=US" \
-ext "SAN=DNS:app.lab.local,DNS:localhost,IP:127.0.0.1" \
-keystore https-san.p12 \
-storetype PKCS12 \
-storepass changeit \
-nopromptConfirm the alias before requesting a signature. You should see one PrivateKeyEntry named https:
keytool -list -keystore https-san.p12 -storetype PKCS12 -storepass changeitKeystore type: PKCS12
Your keystore contains 1 entry
https, Jul 2, 2026, PrivateKeyEntry,If the alias is missing or shows trustedCertEntry, fix the keystore before exporting a CSR.
Step 2 — Export the CSR with keytool -certreq
The CSR contains the public key and distinguished name from the keystore entry. The private key never leaves https-san.p12 — only the public half is exported.
Pass -ext SAN=... on -certreq so Subject Alternative Names appear in the CSR. OpenSSL does not copy extensions from the bootstrap self-signed certificate by default when it signs the request — you must include SAN on the CSR and again at signing time.
Export the CSR to a PEM file your CA can read:
keytool -certreq \
-alias https \
-keystore https-san.p12 \
-storetype PKCS12 \
-storepass changeit \
-file https.csr \
-ext "SAN=DNS:app.lab.local,DNS:localhost,IP:127.0.0.1"Preview the CSR before you submit it:
keytool -printcertreq -file https.csrCertificate request self-signature:
Version: 1
Subject: CN=app.lab.local, OU=Lab, O=GoLinuxCloud, C=US
...Confirm the CSR carries SAN before signing:
keytool -printcertreq -file https.csr | grep -A6 "SubjectAlternativeName"SubjectAlternativeName [
DNSName: app.lab.local
DNSName: localhost
IPAddress: 127.0.0.1
]The subject and SAN extensions should match your -dname and -ext flags. Send https.csr to your CA. For this lab, sign it with the intermediate CA key in the next step.
Step 3 — Sign the CSR with OpenSSL (lab CA)
Use the intermediate CA certificate and key from the lab layout. OpenSSL x509 -req does not copy CSR extensions unless you pass -extfile or -copy_extensions — add SAN explicitly when signing.
Create an extensions file, then sign the CSR:
cd ~/keytool-lab/server
cat > server-ext.cnf <<'EOF'
basicConstraints=critical,CA:FALSE
keyUsage=critical,digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
subjectAltName=DNS:app.lab.local,DNS:localhost,IP:127.0.0.1
EOF
openssl x509 -req -in https.csr \
-CA ../ca/intermediate-ca.crt \
-CAkey ../ca/intermediate-ca.key \
-CAcreateserial \
-out https-signed.crt \
-days 365 -sha256 \
-extfile server-ext.cnfConfirm the signed leaf cert carries the expected subject and that the issuer is your intermediate CA, not the server itself:
openssl x509 -in https-signed.crt -noout -subject -issuersubject=C=US, O=GoLinuxCloud, OU=Lab, CN=app.lab.local
issuer=C=US, O=GoLinuxCloud, CN=Lab Intermediate CAConfirm SAN survived signing:
openssl x509 -in https-signed.crt -noout -text | grep -A2 "Subject Alternative"X509v3 Subject Alternative Name:
DNS:app.lab.local, DNS:localhost, IP Address:127.0.0.1A self-signed issuer matching the subject means you signed with the wrong key or skipped the intermediate. In production, the CA may return a .p7b bundle or multiple PEM files — see Import root, intermediate, and server certificate chain with keytool.
Step 4 — Import the CA-signed certificate
Import into the same alias (https) so the reply public key matches the private key already in the keystore.
https alias before importing the CA reply. Deleting the alias removes the private key that generated the CSR. Without that private key, the signed certificate cannot be installed as a PrivateKeyEntry.
.p7b file. The important rule is the same: install the CA/intermediate certificates first when keytool cannot build the chain, then import the server reply into the original key alias.
keytool validates the signed reply against trusted certificates already in the keystore. Import the CA chain before the server reply:
keytool -importcert \
-alias labroot \
-file ../ca/root-ca.crt \
-keystore https-san.p12 \
-storetype PKCS12 \
-storepass changeit \
-noprompt
keytool -importcert \
-alias labintermediate \
-file ../ca/intermediate-ca.crt \
-keystore https-san.p12 \
-storetype PKCS12 \
-storepass changeit \
-nopromptImport the signed reply into the original key alias:
keytool -importcert \
-alias https \
-file https-signed.crt \
-keystore https-san.p12 \
-storetype PKCS12 \
-storepass changeit \
-nopromptOn success:
Certificate reply was installed in keystoreThat line means the CA-signed cert replaced the bootstrap self-signed cert on alias https. If intermediate CA certificates are missing, keytool may print Failed to establish chain from reply instead — import CA certs first or use -trustcacerts when those CAs already exist in cacerts. See Fix keytool Failed to establish chain from reply.
Verify the entry type, chain length, and issuer:
keytool -list -v \
-keystore https-san.p12 \
-storetype PKCS12 \
-storepass changeit \
-alias https | grep -E "Entry type|Certificate chain length|Owner:|Issuer:"Entry type: PrivateKeyEntry
Certificate chain length: 3
Owner: CN=app.lab.local, OU=Lab, O=GoLinuxCloud, C=US
Issuer: CN=Lab Intermediate CA, O=GoLinuxCloud, C=US
Owner: CN=Lab Intermediate CA, O=GoLinuxCloud, C=US
Issuer: CN=Lab Root CA, O=GoLinuxCloud, C=US
Owner: CN=Lab Root CA, O=GoLinuxCloud, C=US
Issuer: CN=Lab Root CA, O=GoLinuxCloud, C=USYou should still see Entry type: PrivateKeyEntry with issuer Lab Intermediate CA instead of a self-signed issuer matching the subject. The private key is unchanged; only the certificate chain was updated.
Step 5 — Verify the keystore for Tomcat or Spring Boot
Before you deploy, confirm SAN survived CA signing and export the public cert for a quick OpenSSL check.
Export and inspect SAN on the imported entry:
keytool -exportcert -alias https -keystore https-san.p12 \
-storetype PKCS12 -storepass changeit -rfc | openssl x509 -noout -text | grep -A2 "Subject Alternative"X509v3 Subject Alternative Name:
DNS:app.lab.local, DNS:localhost, IP Address:127.0.0.1If SAN entries appear, wire the PKCS12 into your app:
| Runtime | Setting |
|---|---|
| JVM flags | -Djavax.net.ssl.keyStore=https-san.p12 -Djavax.net.ssl.keyStorePassword=changeit -Djavax.net.ssl.keyStoreType=PKCS12 |
| Spring Boot | server.ssl.key-store=file:/path/https-san.p12, server.ssl.key-store-password, server.ssl.key-store-type=PKCS12, server.ssl.key-alias=https |
| Tomcat | certificateKeystoreFile, certificateKeystorePassword, certificateKeystoreType="PKCS12", certificateKeyAlias="https" |
Clients must trust the signing CA (import root or intermediate into a truststore). For the client-side fix, see Fix Java PKIX path building failed. A CA-signed server cert alone does not fix client trust — that is a truststore problem, not a keystore problem.
Troubleshooting
These errors usually appear at -importcert or when the CSR does not match the keystore alias:
| Symptom | Likely cause | Fix |
|---|---|---|
Failed to establish chain from reply |
Intermediate CA missing in keystore | Import CA certs first; see chain import guide |
Public keys in reply and keystore don't match |
Wrong alias or regenerated keystore | Import only into the alias that created the CSR |
alias already exists on import |
Typo in alias name | Use exact alias from -certreq |
| CSR rejected by public CA | Weak key or missing SAN | Use RSA 2048+ and include SAN on -certreq |
| Browser shows wrong hostname | SAN omitted by CA | Reissue CSR with -ext SAN=... |
References
Summary
Create the private key with keytool -genkeypair, export a CSR with keytool -certreq (include -ext SAN=... on the CSR), have your CA sign it with SAN extensions preserved, import root and intermediate CA certificates into the keystore, then run keytool -importcert with the same alias to swap the self-signed cert for the CA reply. Keep the private key in the PKCS12 file throughout — never paste the key into the CSR.

