Create a CSR with keytool and Import a CA-Signed Certificate

Generate a private key with keytool -genkeypair, build a CSR with -certreq, sign it with a local OpenSSL CA, and replace the self-signed entry using -importcert on the same alias.

Published

Updated

Read time 8 min read

Reviewed byDeepak Prasad

keytool certificate signing request and CA-signed import workflow banner

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.

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.

text
~/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 import

If 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/:

bash
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")
NOTE
The <(...) 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.

bash
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 \
  -noprompt

Confirm the alias before requesting a signature. You should see one PrivateKeyEntry named https:

bash
keytool -list -keystore https-san.p12 -storetype PKCS12 -storepass changeit
text
Keystore 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:

bash
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:

bash
keytool -printcertreq -file https.csr
text
Certificate request self-signature:
Version: 1

Subject: CN=app.lab.local, OU=Lab, O=GoLinuxCloud, C=US
...

Confirm the CSR carries SAN before signing:

bash
keytool -printcertreq -file https.csr | grep -A6 "SubjectAlternativeName"
text
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:

bash
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.cnf

Confirm the signed leaf cert carries the expected subject and that the issuer is your intermediate CA, not the server itself:

bash
openssl x509 -in https-signed.crt -noout -subject -issuer
text
subject=C=US, O=GoLinuxCloud, OU=Lab, CN=app.lab.local
issuer=C=US, O=GoLinuxCloud, CN=Lab Intermediate CA

Confirm SAN survived signing:

bash
openssl x509 -in https-signed.crt -noout -text | grep -A2 "Subject Alternative"
text
X509v3 Subject Alternative Name: 
                DNS:app.lab.local, DNS:localhost, IP Address:127.0.0.1

A 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.

WARNING
Do not delete the 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.
NOTE
Public CAs often return a ZIP, PEM bundle, or PKCS#7 .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:

bash
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 \
  -noprompt

Import the signed reply into the original key alias:

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

On success:

text
Certificate reply was installed in keystore

That 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:

bash
keytool -list -v \
  -keystore https-san.p12 \
  -storetype PKCS12 \
  -storepass changeit \
  -alias https | grep -E "Entry type|Certificate chain length|Owner:|Issuer:"
text
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=US

You 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:

bash
keytool -exportcert -alias https -keystore https-san.p12 \
  -storetype PKCS12 -storepass changeit -rfc | openssl x509 -noout -text | grep -A2 "Subject Alternative"
text
X509v3 Subject Alternative Name: 
                DNS:app.lab.local, DNS:localhost, IP Address:127.0.0.1

If 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.


Frequently Asked Questions

1. How do I create a CSR with keytool?

First run keytool -genkeypair to create the private key and a temporary self-signed cert in the keystore. Then run keytool -certreq -alias ALIAS -file host.csr -keystore store.p12 -storepass PASS to export the CSR PEM file you send to a CA.

2. Does keytool -importcert replace the self-signed certificate?

Yes, when you import the CA reply with -importcert using the same alias as the key entry, keytool replaces the self-signed certificate in the chain while keeping the original private key. The entry stays PrivateKeyEntry.

3. Can I import a CA-signed certificate into a different alias?

The signed certificate must match the private key that generated the CSR. Importing into a different alias causes public keys in reply and keystore do not match. Always use the alias you passed to -certreq.

4. Should I add SAN extensions on the CSR?

Yes for modern HTTPS. Pass -ext "SAN=DNS:app.example.com,IP:10.0.0.5" on -genkeypair or -certreq so the CA-signed certificate includes Subject Alternative Names browsers and Java require.

5. What file format does keytool -certreq produce?

keytool writes a PEM CSR (BEGIN NEW CERTIFICATE REQUEST). Most CAs and OpenSSL accept it directly. Inspect with keytool -printcertreq -file host.csr.

6. Do I need to delete the self-signed cert before importing the CA reply?

No. keytool -importcert on the existing PrivateKeyEntry alias updates the certificate chain automatically. Deleting the alias would remove the private key and force you to start over.
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 …