A self-signed certificate from keytool -genkeypair is the fastest way to stand up HTTPS on a local Java app, Tomcat, or Kafka broker. The catch is hostname verification: if you connect to https://127.0.0.1:8443 but the certificate only has CN=localhost and no IP in the Subject Alternative Name (SAN) extension, Java prints No subject alternative names present and the handshake fails.
This guide walks through generating a PKCS12 keystore with DNS and IP SAN entries for localhost, a custom hostname, and 127.0.0.1, verifying the extensions with keytool -list -v, and contrasting that with a cert that omits SAN. I ran every command on Ubuntu with OpenJDK 25 and kept the real terminal output below.
Tested on: Ubuntu 26.04 LTS; OpenJDK 25.0.3; kernel 7.0.0-27-generic.
Prerequisites
You need a working keytool binary, a place to write lab keystores, and optionally OpenSSL to double-check SAN in PEM form.
keytoolon your PATH. If the shell reportscommand not found, follow Install keytool on Ubuntu.- A writable directory for lab files (this guide uses
~/keytool-lab/server/). - Optional:
opensslto inspect PEM exports; see the keytool cheat sheet for related commands.
For background on what a keystore holds versus a truststore, see Keystore vs truststore in Java.
Why CN alone is not enough
Historically, TLS clients matched the server hostname against the certificate Common Name (CN). RFC 6125 and modern Java HTTPS check the Subject Alternative Name extension first. When you browse or call https://127.0.0.1, the client looks for an IPAddress SAN entry — not merely CN=localhost.
| Connect URL | Needs in certificate |
|---|---|
https://localhost:8443 |
DNS:localhost in SAN (CN alone may work on some stacks but is unreliable) |
https://127.0.0.1:8443 |
IP:127.0.0.1 in SAN |
https://app.lab.local:8443 |
DNS:app.lab.local in SAN |
keytool adds SAN at creation time with the -ext flag on -genkeypair (or on -certreq when you plan CA signing later).
Generate a self-signed certificate with SAN
The -genkeypair command creates a PKCS12 file that holds both the private key and a one-certificate chain. PKCS12 is the default store type on modern OpenJDK; you do not need -storetype JKS. Add DNS and IP names with -ext on the same line as key generation so they are baked into the self-signed cert.
Create a PKCS12 keystore:
mkdir -p ~/keytool-lab/server
cd ~/keytool-lab/server
keytool -genkeypair \
-alias localhost \
-keyalg RSA \
-keysize 2048 \
-validity 365 \
-dname "CN=localhost, OU=Lab, O=GoLinuxCloud, L=Lab, ST=Lab, C=US" \
-ext "SAN=DNS:localhost,DNS:lab.local,IP:127.0.0.1" \
-keystore localhost-san.p12 \
-storetype PKCS12 \
-storepass changeit \
-nopromptAfter the command completes, the keystore file appears in the current directory. Some JDK builds also print a short “Generating … key pair” message.
Run verbose list to confirm the alias, entry type, and SAN extension before wiring the file into a server:
keytool -list -v -keystore localhost-san.p12 -storetype PKCS12 -storepass changeitNear the bottom of the certificate section you should see SubjectAlternativeName with both DNS and IP entries:
Extensions:
#1: ObjectId: 2.5.29.17 Criticality=false
SubjectAlternativeName [
DNSName: localhost
DNSName: lab.local
IPAddress: 127.0.0.1
]The same command also shows Entry type: PrivateKeyEntry and a one-certificate chain (self-signed issuer equals owner). If any expected DNS or IP name is missing from the SAN block, regenerate the keystore with a corrected -ext string before continuing.
Verify SAN with keytool and OpenSSL
keytool -list -v is enough when you stay inside Java tooling. Export the certificate to PEM when you want a second opinion from OpenSSL or need to share the public cert with a teammate.
Export the certificate from the keystore:
keytool -exportcert -alias localhost -keystore localhost-san.p12 \
-storetype PKCS12 -storepass changeit -rfc -file localhost-san.crt
openssl x509 -in localhost-san.crt -noout -text | grep -A3 "Subject Alternative Name"Sample OpenSSL output:
X509v3 Subject Alternative Name:
DNS:localhost, DNS:lab.local, IP Address:127.0.0.1Both tools should agree on the three SAN values. A mismatch usually means you exported the wrong alias or edited the keystore after generation.
What breaks when SAN is missing
To see the failure in context, generate a second keystore that omits -ext entirely. The cert still has CN=localhost, but modern clients ignore CN when checking IP addresses.
Generate a no-SAN keystore and confirm the extension is absent:
keytool -genkeypair \
-alias localhost \
-keyalg RSA \
-keysize 2048 \
-validity 365 \
-dname "CN=localhost, OU=Lab, O=GoLinuxCloud, L=Lab, ST=Lab, C=US" \
-keystore no-san.p12 \
-storetype PKCS12 \
-storepass changeit \
-noprompt
keytool -list -v -keystore no-san.p12 -storetype PKCS12 -storepass changeit | grep -i "Subject Alternative"On the no-SAN keystore, grep returns nothing — there is no SubjectAlternativeName extension block. That is the root cause when clients connect by IP.
Start a minimal Java HTTPS server bound to 127.0.0.1 using no-san.p12 (see Keystore vs truststore for a HttpsServer example), then connect with a Java HttpClient to https://127.0.0.1:8443/. The client fails during hostname verification with one of these messages depending on the JDK and URL:
javax.net.ssl.SSLHandshakeException: No subject alternative names presentjavax.net.ssl.SSLHandshakeException: No subject alternative names matching IP address 127.0.0.1 foundConnecting to https://localhost:8443 may appear to work on some JDK builds because the CN matches, but relying on CN is fragile — always add explicit SAN entries for every hostname and IP clients use.
SAN fixes hostname verification. It does not make a self-signed certificate trusted. For Java clients, import the exported certificate into a truststore or use a local CA — see Fix Java PKIX path building failed.
127.0.0.1 while docs say localhost; list both in SAN.
Use the keystore with Java HTTPS
Once SAN is confirmed, point your server at localhost-san.p12. For plain Java JSSE configuration, the JVM needs the keystore path, password, and store type. Frameworks such as Spring Boot or Tomcat can also let you choose a specific alias.
For a plain Java process, pass system properties on the command line:
java -Djavax.net.ssl.keyStore=localhost-san.p12 \
-Djavax.net.ssl.keyStorePassword=changeit \
-Djavax.net.ssl.keyStoreType=PKCS12 \
YourServerMainSpring Boot reads the same values from application.properties or environment variables:
server.ssl.key-store=file:./localhost-san.p12
server.ssl.key-store-password=changeit
server.ssl.key-store-type=PKCS12
server.ssl.key-alias=localhostUse classpath:localhost-san.p12 only after copying the keystore into src/main/resources. For a file beside the running JAR, use file:./localhost-san.p12.
After restart, the server presents the cert with SAN when clients connect to localhost or 127.0.0.1. Clients still need to trust a self-signed cert — import the public certificate into a truststore or use -Djavax.net.ssl.trustStore with a PKCS12 file that contains a trustedCertEntry for your cert. Production systems should replace the self-signed cert with a CA-signed chain; see Create a CSR with keytool and import a CA-signed certificate.
Troubleshooting
Use this table when SAN or keystore wiring still fails after regeneration:
| Symptom | Likely cause | Fix |
|---|---|---|
No subject alternative names present or No subject alternative names matching IP address ... |
No IP/DNS SAN for the URL you use | Regenerate with -ext "SAN=DNS:...,IP:..." |
alias already exists |
Reused alias in same keystore | Pick a new alias or delete the old entry with keytool -delete |
java.io.IOException: keystore password was incorrect |
Wrong -storepass |
Match the password used at -genkeypair time |
| SAN missing after CA signing | CSR signed without SAN extensions | Add -ext SAN=... on -certreq or ensure the CA copies extensions |
| Browser warns “not secure” | Self-signed, not in OS trust store | Expected for dev; import CA or use a public CA for production |
References
Summary
Use keytool -genkeypair with -ext "SAN=DNS:localhost,DNS:lab.local,IP:127.0.0.1" so Java HTTPS accepts connections to both localhost and 127.0.0.1. Confirm SAN with keytool -list -v before wiring the PKCS12 file into Tomcat or Spring Boot. A certificate with only CN=localhost leaves out the IP SAN and triggers No subject alternative names present when clients target the numeric loopback address.

