Java TLS servers need more than a single .crt file in a PKCS12 keystore. When your certificate is signed by an intermediate CA, keytool must find that intermediate (and sometimes the root) inside the keystore to build a valid chain. Import order matters: anchors first, server reply last on the key alias.
This guide builds a three-tier lab chain (root → intermediate → server), imports each layer with keytool -importcert, shows what a healthy keystore looks like in keytool -list -v, and demonstrates what happens when you skip the intermediate. I ran the workflow on Ubuntu with OpenJDK 25 and OpenSSL for CA signing.
Tested on: Ubuntu 26.04 LTS; OpenJDK 25.0.3; kernel 7.0.0-27-generic.
Prerequisites
You need a working keytool install, lab CA files, and a clear picture of which file is identity vs trust:
- Install keytool on Ubuntu.
- Lab CA files (
root-ca.crt,intermediate-ca.crt,intermediate-ca.key) — create them in Create a CSR with keytool and import a CA-signed certificate if needed. - Understanding of keystore vs truststore entry types (
PrivateKeyEntryvstrustedCertEntry).
Chain roles and aliases
Each file in a three-tier PKI maps to a distinct keystore role. Keep CA aliases separate from the server key alias:
| File | Keystore role | Suggested alias | Entry type |
|---|---|---|---|
root-ca.crt |
Lab trust anchor; often omitted from the served TLS chain in production | rootca |
trustedCertEntry |
intermediate-ca.crt |
Issuer of server cert | intermediateca |
trustedCertEntry |
server-signed.crt |
Leaf TLS certificate | server (same as key alias) |
Updates PrivateKeyEntry chain |
In production, servers usually send the leaf and intermediate certificates; clients already have the root. In this lab, importing the root makes keytool chain validation and verification easier.
The server private key is created first with keytool -genkeypair. CA certificates are public — import them with -importcert and no private key.
Step 1 — Create the server key pair
Generate the TLS identity first. The alias you choose here (server) must receive the signed reply later:
mkdir -p ~/keytool-lab/server
cd ~/keytool-lab/server
keytool -genkeypair \
-alias server \
-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 server-chain.p12 \
-storetype PKCS12 \
-storepass changeit \
-nopromptserver-chain.p12 now holds a self-signed PrivateKeyEntry on alias server — a placeholder until the CA signs your CSR.
Step 2 — Import root CA
Import the root as a trustedCertEntry before any leaf reply. This gives keytool an anchor when it validates the chain:
keytool -importcert \
-alias rootca \
-file ../ca/root-ca.crt \
-keystore server-chain.p12 \
-storetype PKCS12 \
-storepass changeit \
-nopromptCertificate was added to keystoreThe root is stored as a public trust anchor — no private key is involved.
Step 3 — Import intermediate CA
Add the intermediate that will sign the server certificate:
keytool -importcert \
-alias intermediateca \
-file ../ca/intermediate-ca.crt \
-keystore server-chain.p12 \
-storetype PKCS12 \
-storepass changeit \
-nopromptkeytool should print Certificate was added to keystore for the intermediate as well.
List all entries — you should see two trustedCertEntry rows and one PrivateKeyEntry:
keytool -list -keystore server-chain.p12 -storetype PKCS12 -storepass changeitKeystore type: PKCS12
Your keystore contains 3 entries
intermediateca, Jul 2, 2026, trustedCertEntry,
rootca, Jul 2, 2026, trustedCertEntry,
server, Jul 2, 2026, PrivateKeyEntry,Three entries means the keystore is ready for the CSR and signed reply.
Verbose view of the intermediate confirms it chains to the root you just imported:
keytool -list -v -keystore server-chain.p12 -storetype PKCS12 -storepass changeit -alias intermediatecaAlias name: intermediateca
Entry type: trustedCertEntry
Owner: C=US, O=GoLinuxCloud, CN=Lab Intermediate CA
Issuer: C=US, O=GoLinuxCloud, CN=Lab Root CAThe intermediate's issuer matches the root subject — the first two tiers are linked inside the keystore.
Step 4 — CSR, sign, and import the server certificate
Export the CSR from alias server with SAN extensions, sign it with the intermediate CA, then import the reply on the same alias:
keytool -certreq \
-alias server \
-keystore server-chain.p12 \
-storetype PKCS12 \
-storepass changeit \
-file server.csr \
-ext "SAN=DNS:app.lab.local,DNS:localhost,IP:127.0.0.1"
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 server.csr \
-CA ../ca/intermediate-ca.crt \
-CAkey ../ca/intermediate-ca.key \
-CAcreateserial \
-out server-signed.crt \
-days 365 -sha256 \
-extfile server-ext.cnfConfirm SAN survived signing:
openssl x509 -in server-signed.crt -noout -text | grep -A2 "Subject Alternative"X509v3 Subject Alternative Name:
DNS:app.lab.local, DNS:localhost, IP Address:127.0.0.1Import the signed reply:
keytool -importcert -alias server -file server-signed.crt \
-keystore server-chain.p12 -storetype PKCS12 -storepass changeit -nopromptSuccess means keytool replaced the self-signed placeholder with the CA-signed certificate:
Certificate reply was installed in keystoreCheck chain metadata on the server entry:
keytool -list -v \
-keystore server-chain.p12 \
-storetype PKCS12 \
-storepass changeit \
-alias server | 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=USThe server alias should now contain the CA-signed certificate chain (leaf → intermediate → root), not only the self-signed placeholder.
Using -trustcacerts
keytool already checks trusted certificates in the target keystore when importing a certificate reply. Add -trustcacerts when you also want keytool to consult the JDK cacerts file. This helps for public CAs already trusted by the JDK, but it does not replace missing private CA certificates.
When CA certificates already live in cacerts (public CAs) or were imported into your keystore, retry the server import with -trustcacerts:
keytool -importcert -trustcacerts \
-alias server -file server-signed.crt \
-keystore server-chain.p12 -storetype PKCS12 -storepass changeit -nopromptFor private lab CAs, cacerts does not contain your root — import root and intermediate into the keystore explicitly instead of relying on -trustcacerts alone.
What happens when the intermediate is missing
Skipping the intermediate reproduces the most common chain import failure. Create a fresh keystore with only the key pair and try importing the signed server cert:
keytool -genkeypair -alias server -keyalg RSA -keysize 2048 -validity 365 \
-dname "CN=app.lab.local, OU=Lab, O=GoLinuxCloud, C=US" \
-keystore omit-inter.p12 -storetype PKCS12 -storepass changeit -noprompt
keytool -certreq -alias server -keystore omit-inter.p12 -storetype PKCS12 \
-storepass changeit -file omit.csr
openssl x509 -req -in omit.csr -CA ../ca/intermediate-ca.crt \
-CAkey ../ca/intermediate-ca.key -CAcreateserial \
-out omit-signed.crt -days 365 -sha256
keytool -importcert -alias server -file omit-signed.crt \
-keystore omit-inter.p12 -storetype PKCS12 -storepass changeit -nopromptkeytool cannot link the reply to a known issuer:
keytool error: java.lang.Exception: Failed to establish chain from replyFix by importing intermediate-ca.crt (and root-ca.crt if required) before the server reply. Full troubleshooting is in Fix keytool Failed to establish chain from reply.
Client truststore vs server keystore
The server keystore contains the private key alias and the certificate chain that the TLS server presents to clients. Client truststores are separate and should contain only public CA certificates.
Export the intermediate for client trust bundles:
keytool -exportcert -alias intermediateca -keystore server-chain.p12 \
-storetype PKCS12 -storepass changeit -rfc -file intermediate-for-clients.crtImport that PEM into a client-only PKCS12 with keytool -importcert — never copy the server keystore to clients.
Troubleshooting
Use this table when chain import or browser verification fails after the steps above:
| Symptom | Likely cause | Fix |
|---|---|---|
Failed to establish chain from reply |
Missing intermediate or root in keystore | Import CA certs before leaf; see fix guide |
| Duplicate alias on CA import | Re-importing same alias | Use keytool -delete -alias rootca or pick new names |
Chain length remains 1 or issuer is self-signed |
Server reply was not installed | Re-run -importcert on the original PrivateKeyEntry alias and check stderr |
| Browser shows incomplete chain | Server not sending intermediate | Include intermediate in keystore; configure connector to send chain |
| Wrong entry type for CA | Imported server cert as new alias | CA files must be trustedCertEntry, not new PrivateKeyEntry |
References
Summary
Import root and intermediate CA certificates as separate trustedCertEntry aliases, then import the CA-signed server reply on the PrivateKeyEntry alias that owns the private key. Use keytool -list to confirm three entries and keytool -list -v -alias server to verify Certificate chain length: 3 with issuer Lab Intermediate CA. Skipping the intermediate produces Failed to establish chain from reply — add the missing CA certificates and import the leaf again.

