Apache Tomcat terminates HTTPS with a Java keystore — the same PKCS12 files you build with keytool for Spring Boot or standalone Java services. Tomcat 10 expects the keystore path and passwords on the <Connector> element in conf/server.xml, not in a Spring-style application.properties file.
This guide builds a PKCS12 keystore for Tomcat 10, imports the CA chain before the signed server certificate (order matters), configures port 8443, and verifies TLS with the same truststore pattern used in the Spring Boot lab.
Tested on: Ubuntu 26.04 LTS; OpenJDK 25.0.3; Apache Tomcat 10.1.40; kernel 7.0.0-27-generic.
Prerequisites
keytoolfrom OpenJDK — Install keytool on Ubuntu covers Ubuntu packages andJAVA_HOME.- Apache Tomcat 10.x extracted or installed under
/opt/tomcat(adjust paths to your layout). - A free TCP port — this lab uses
8443on127.0.0.1. - Basic familiarity with keystore vs truststore — Tomcat loads a keystore for server identity; browsers and Java clients use their own truststores.
Build a PKCS12 keystore for Tomcat
Tomcat needs a PrivateKeyEntry alias (this lab uses https) plus any CA certificates required to present a full chain to clients. The steps mirror the Spring Boot HTTPS guide but write tomcat-keystore.p12.
Step 1 — Create a lab CA and key pair
Generate the signing CA and the server key pair in separate PKCS12 files:
mkdir -p ~/tomcat-ssl-lab
cd ~/tomcat-ssl-lab
keytool -genkeypair -alias demo-ca -keyalg RSA -keysize 2048 -validity 3650 \
-dname "CN=Demo Lab CA,O=Lab,C=US" -ext "bc=ca:true" \
-keystore ca.p12 -storetype PKCS12 -storepass changeit -noprompt
keytool -genkeypair -alias https -keyalg RSA -keysize 2048 -validity 365 \
-dname "CN=localhost,O=Lab,C=US" \
-ext "SAN=DNS:localhost,IP:127.0.0.1" \
-keystore tomcat-keystore.p12 -storetype PKCS12 -storepass changeit -noprompt
keytool -list -keystore tomcat-keystore.p12 -storetype PKCS12 -storepass changeitKeystore type: PKCS12
Keystore provider: SUN
Your keystore contains 1 entry
https, Jul 2, 2026, PrivateKeyEntry,
Certificate fingerprint (SHA-256): 9F:61:99:E4:26:B4:D7:FC:9C:E0:BB:A5:23:67:8A:71:FF:CC:18:83:D4:2A:70:88:35:42:CD:7E:90:53:01:36At this stage the entry is self-signed. The next steps replace it with a CA-signed certificate.
Step 2 — Sign the server certificate
Export a CSR from the https alias, sign it with the lab CA, and keep the result as tomcat.crt:
keytool -certreq -alias https \
-keystore tomcat-keystore.p12 \
-storetype PKCS12 \
-storepass changeit \
-file tomcat.csr \
-ext "SAN=DNS:localhost,IP:127.0.0.1"
keytool -gencert -alias demo-ca -infile tomcat.csr -outfile tomcat.crt \
-keystore ca.p12 -storetype PKCS12 -storepass changeit -validity 365 \
-ext "SAN=DNS:localhost,IP:127.0.0.1" \
-ext "EKU=serverAuth"
keytool -printcert -file tomcat.crt | grep -E 'Owner:|Valid from:'Owner: CN=localhost, O=Lab, C=US
Valid from: Thu Jul 02 22:10:10 IST 2026 until: Fri Jul 02 22:10:10 IST 2027For the local keytool -gencert lab, SAN is added again during signing so the final certificate contains the extension. For a real PKI workflow, include SAN in the CSR and confirm your CA copies or applies the requested SAN values.
Use DNS: for hostnames and IP: for numeric addresses — do not put 127.0.0.1 under DNS:. The SAN extension must include the hostname clients use (localhost or your DNS name) or Java clients fail hostname verification even when the chain is trusted.
Step 3 — Import CA chain before server certificate
tomcat-keystore.p12 before you install the signed server reply. Reversing the order often triggers keytool error: java.lang.Exception: Failed to establish chain from reply.
Export the CA once, then import from the file:
keytool -exportcert -alias demo-ca \
-keystore ca.p12 -storetype PKCS12 -storepass changeit \
-rfc -file demo-ca.crt
keytool -importcert -alias demo-ca -file demo-ca.crt \
-keystore tomcat-keystore.p12 -storetype PKCS12 -storepass changeit -noprompt
keytool -importcert -alias https -file tomcat.crt \
-keystore tomcat-keystore.p12 -storetype PKCS12 -storepass changeit -nopromptVerify entries:
keytool -list -keystore tomcat-keystore.p12 -storetype PKCS12 -storepass changeitKeystore type: PKCS12
Keystore provider: SUN
Your keystore contains 2 entries
demo-ca, Jul 2, 2026, trustedCertEntry,
Certificate fingerprint (SHA-256): 82:55:84:43:0B:3A:73:DC:63:DC:E8:C7:EF:A7:98:39:FE:AA:AE:E1:84:82:95:56:2B:48:87:A1:0A:C5:1B:02
https, Jul 2, 2026, PrivateKeyEntry,
Certificate fingerprint (SHA-256): 92:E0:58:15:04:44:05:4B:7E:52:04:A1:F3:69:8A:4F:C4:81:4B:8E:87:ED:E5:3E:C4:14:74:D1:36:D0:96:F8demo-ca as trustedCertEntry gives keytool the CA material needed to build the chain during import. The https alias then holds the PrivateKeyEntry and signed certificate chain Tomcat presents to clients.
For production CAs, follow CSR and import CA-signed certificate with keytool instead of gencert.
Configure the Tomcat HTTPS Connector
Tomcat reads the keystore path from server.xml, not from Spring-style properties. Copy the PKCS12 file into catalina.base and restrict permissions so only the tomcat user can read the private key:
sudo cp tomcat-keystore.p12 /opt/tomcat/conf/tomcat-keystore.p12
sudo chown tomcat:tomcat /opt/tomcat/conf/tomcat-keystore.p12
sudo chmod 600 /opt/tomcat/conf/tomcat-keystore.p12Add or uncomment an HTTPS connector in conf/server.xml. Tomcat 10.1 uses NIO connector attributes under SSLHostConfig:
<Connector port="8443"
protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="150"
SSLEnabled="true"
scheme="https"
secure="true">
<SSLHostConfig>
<Certificate certificateKeystoreFile="${catalina.base}/conf/tomcat-keystore.p12"
certificateKeystorePassword="changeit"
certificateKeystoreType="PKCS12"
certificateKeyAlias="https" />
</SSLHostConfig>
</Connector>| Attribute | Value in this lab |
|---|---|
certificateKeystoreFile |
Path to PKCS12 under catalina.base |
certificateKeystorePassword |
changeit (externalize in production) |
certificateKeystoreType |
PKCS12 |
certificateKeyAlias |
https — must match PrivateKeyEntry from keytool -list |
server.xml are acceptable for a local lab only. For production, use protected configuration, environment-specific property substitution, filesystem permissions, or a secrets manager. Do not commit real keystore passwords to Git.
Disable the plain HTTP connector on port 8080 if your policy requires HTTPS-only access.
Start Tomcat and verify TLS
Start Tomcat and watch catalina.out for connector initialization errors:
/opt/tomcat/bin/catalina.sh start
tail -20 /opt/tomcat/logs/catalina.outInitializing ProtocolHandler ["https-openssl-nio-8443"]
Starting ProtocolHandler ["https-...-8443"]
Server startup in [1939] millisecondsDepending on Tomcat Native/OpenSSL availability, the log may say https-jsse-nio-8443 instead of https-openssl-nio-8443. A line like Starting ProtocolHandler ["https-...-8443"] without keystore password was incorrect means Tomcat loaded the PKCS12 file.
Build a client truststore with only the CA public certificate. Reuse demo-ca.crt from the export step above:
keytool -importcert -alias demo-ca -file demo-ca.crt \
-keystore truststore.p12 -storetype PKCS12 -storepass changeit -nopromptTest with a Java HttpClient while Tomcat listens on 8443. Save this as ClientTest.java in the lab directory:
import javax.net.ssl.*;
import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
import java.security.*;
public class ClientTest {
public static void main(String[] args) throws Exception {
KeyStore ts = KeyStore.getInstance("PKCS12");
try (var in = Files.newInputStream(Path.of("truststore.p12"))) {
ts.load(in, "changeit".toCharArray());
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ts);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, tmf.getTrustManagers(), null);
HttpResponse<String> r = HttpClient.newBuilder().sslContext(ctx).build()
.send(HttpRequest.newBuilder(URI.create("https://127.0.0.1:8443/")).GET().build(),
HttpResponse.BodyHandlers.ofString());
System.out.println("HTTP status: " + r.statusCode());
}
}javac ClientTest.java && java ClientTestHTTP status: 200curl without --cacert fails against a private CA (expected). Trust the exported CA explicitly for a quick command-line check:
curl --cacert demo-ca.crt -s -o /dev/null -w '%{http_code}\n' https://127.0.0.1:8443/200An HTTP 200 from curl confirms the TLS handshake and Tomcat default page both succeed when the client trusts the lab CA.
Troubleshooting
These errors map directly to keystore contents or server.xml attribute typos:
| Error | Fix |
|---|---|
Failed to establish chain from reply |
Import CA/intermediate certs before the signed server cert — see Import certificate chain with keytool |
keystore password was incorrect |
Match certificateKeystorePassword to the PKCS12 store password |
Alias <name> does not identify a key entry |
certificateKeyAlias must name a PrivateKeyEntry, not a trustedCertEntry |
PKIX path building failed on the client |
Client truststore missing the lab CA — import with keytool -importcert into a custom file |
| Hostname verification failure | Add DNS:localhost and IP:127.0.0.1 SAN at cert generation — localhost SAN guide |
Wrong password on the command line reproduces the same root cause Tomcat logs at startup:
keytool -list -keystore tomcat-keystore.p12 -storetype PKCS12 -storepass badpasskeytool error: java.io.IOException: keystore password was incorrectReferences
- Apache Tomcat 10 — SSL/TLS Configuration HOW-TO (official)
- Oracle keytool documentation (official)
- Spring Boot HTTPS with keytool (embedded Tomcat uses the same keystore format)
- keytool cheat sheet
Summary
Tomcat HTTPS on port 8443 comes down to a PKCS12 keystore from keytool, correct import order (CA chain before the signed server certificate), and a server.xml connector with certificateKeystoreFile, certificateKeystorePassword, certificateKeystoreType=PKCS12, and certificateKeyAlias set to your PrivateKeyEntry. I confirmed the keystore lists https as PrivateKeyEntry, curl --cacert demo-ca.crt returns HTTP 200, and a Java client with a separate truststore reaches HTTP 200 on https://127.0.0.1:8443/. For shipping trust to containers without touching cacerts, continue with Java truststore in Docker and Kubernetes.

