This example demonstrates mutual TLS (mTLS) on Apache TomEE and Apache Tomcat, where both the server and the client present certificates during the TLS handshake. A servlet displays every available SSL/TLS property, along with the Java code needed to obtain each value.
In standard HTTPS the server proves its identity to the client. With mutual TLS
the server also requires the client to present a certificate. Tomcat
validates the client certificate against a truststore and, if successful, makes
the full X509Certificate chain available to the application through a servlet
request attribute.
All certificates are generated automatically during the Maven build — there are
no manual keytool steps.
mvn clean install tomee:run| File | Format | Purpose |
|---|---|---|
target/conf/keystore.jks |
JKS | Server key pair (CN=localhost) |
target/conf/client.p12 |
PKCS12 | Client key pair (CN=Test Client) |
target/conf/client.cer |
PEM | Exported client public certificate |
target/conf/truststore.jks |
JKS | Truststore containing the client cert |
With client certificate (should return SSL property output):
curl -k --cert-type P12 --cert target/conf/client.p12:changeit \
https://localhost:8443/tomcat-ssl/Without client certificate (should fail — handshake error or HTTP 400):
curl -k https://localhost:8443/tomcat-ssl/Two things are required beyond a standard HTTPS connector:
certificateVerification="required"on<SSLHostConfig>— tells Tomcat to demand a client certificate.- A truststore containing the CA (or individual client certificates) that Tomcat will accept.
<Connector port="8443" protocol="HTTP/1.1"
SSLEnabled="true"
maxThreads="150"
scheme="https"
secure="true">
<SSLHostConfig certificateVerification="required"
truststoreFile="${catalina.base}/conf/truststore.jks"
truststorePassword="changeit">
<Certificate certificateKeystoreFile="${catalina.base}/conf/keystore.jks"
certificateKeystorePassword="changeit"
certificateKeyAlias="server"
type="RSA"/>
</SSLHostConfig>
</Connector>| Attribute | Value | Description |
|---|---|---|
certificateVerification |
required |
Client must present a valid certificate. Other options: optional, optionalNoCA, none. |
truststoreFile |
path | Truststore containing accepted client/CA certificates. |
truststorePassword |
string | Password for the truststore. |
Tomcat exposes SSL/TLS information through request.getAttribute(). These are
the key attributes:
| Attribute Name | Type | Description |
|---|---|---|
jakarta.servlet.request.X509Certificate |
X509Certificate[] |
Client certificate chain. Index [0] is the client cert. |
jakarta.servlet.request.cipher_suite |
String |
Negotiated SSL/TLS cipher suite. |
jakarta.servlet.request.key_size |
Integer |
Effective key size of the cipher. |
jakarta.servlet.request.ssl_session_id |
String |
SSL session identifier. |
org.apache.tomcat.util.net.secure_protocol_version |
String |
TLS protocol version (Tomcat-specific). |
X509Certificate[] certs = (X509Certificate[])
request.getAttribute("jakarta.servlet.request.X509Certificate");
if (certs != null && certs.length > 0) {
X509Certificate clientCert = certs[0];
String subjectDN = clientCert.getSubjectX500Principal().getName();
// ...
}Individual RDN values (CN, OU, O, etc.) can be extracted from the
Distinguished Name using javax.naming.ldap.LdapName:
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
LdapName ldapName = new LdapName(clientCert.getSubjectX500Principal().getName());
for (Rdn rdn : ldapName.getRdns()) {
System.out.println(rdn.getType() + "=" + rdn.getValue());
}The table below maps Apache HTTPD mod_ssl environment variables to their
Java/Tomcat equivalents. In every case, the certificate object is obtained
first:
X509Certificate[] certs = (X509Certificate[])
request.getAttribute("jakarta.servlet.request.X509Certificate");
X509Certificate cert = certs[0];| mod_ssl Variable | Java / Tomcat Equivalent |
|---|---|
SSL_PROTOCOL |
(String) request.getAttribute("org.apache.tomcat.util.net.secure_protocol_version") |
SSL_CIPHER |
(String) request.getAttribute("jakarta.servlet.request.cipher_suite") |
SSL_CIPHER_USEKEYSIZE |
(Integer) request.getAttribute("jakarta.servlet.request.key_size") |
SSL_SESSION_ID |
(String) request.getAttribute("jakarta.servlet.request.ssl_session_id") |
| mod_ssl Variable | Java / Tomcat Equivalent |
|---|---|
SSL_CLIENT_S_DN |
cert.getSubjectX500Principal().getName() |
SSL_CLIENT_S_DN_CN |
new LdapName(cert.getSubjectX500Principal().getName()).getRdns() — filter type="CN" |
SSL_CLIENT_S_DN_OU |
Same as above — filter type="OU" |
SSL_CLIENT_S_DN_O |
Same as above — filter type="O" |
SSL_CLIENT_S_DN_L |
Same as above — filter type="L" |
SSL_CLIENT_S_DN_ST |
Same as above — filter type="ST" |
SSL_CLIENT_S_DN_C |
Same as above — filter type="C" |
SSL_CLIENT_S_DN_Email |
Same as above — filter type="EMAILADDRESS" |
| mod_ssl Variable | Java / Tomcat Equivalent |
|---|---|
SSL_CLIENT_I_DN |
cert.getIssuerX500Principal().getName() |
SSL_CLIENT_I_DN_CN |
new LdapName(cert.getIssuerX500Principal().getName()).getRdns() — filter type="CN" |
SSL_CLIENT_I_DN_OU |
Same as above — filter type="OU" |
SSL_CLIENT_I_DN_O |
Same as above — filter type="O" |
SSL_CLIENT_I_DN_L |
Same as above — filter type="L" |
SSL_CLIENT_I_DN_ST |
Same as above — filter type="ST" |
SSL_CLIENT_I_DN_C |
Same as above — filter type="C" |
SSL_CLIENT_I_DN_Email |
Same as above — filter type="EMAILADDRESS" |
| mod_ssl Variable | Java / Tomcat Equivalent |
|---|---|
SSL_CLIENT_V_START |
cert.getNotBefore() |
SSL_CLIENT_V_END |
cert.getNotAfter() |
SSL_CLIENT_V_REMAIN |
(cert.getNotAfter().getTime() - System.currentTimeMillis()) / 86400000 |
| mod_ssl Variable | Java / Tomcat Equivalent |
|---|---|
SSL_CLIENT_M_VERSION |
cert.getVersion() |
SSL_CLIENT_M_SERIAL |
cert.getSerialNumber().toString(16).toUpperCase() |
SSL_CLIENT_A_SIG |
cert.getSigAlgName() |
SSL_CLIENT_A_KEY |
cert.getPublicKey().getAlgorithm() |
| — | ((RSAPublicKey) cert.getPublicKey()).getModulus().bitLength() |
SSL_CLIENT_CERT |
Base64.getMimeEncoder(64, "\n".getBytes()).encodeToString(cert.getEncoded()) |
SSL_CLIENT_VERIFY |
certs != null && certs.length > 0 ? "SUCCESS" : "NONE" |
These do not have direct mod_ssl equivalents but are commonly needed:
| Variable | Java / Tomcat Equivalent |
|---|---|
| SHA-1 fingerprint | MessageDigest.getInstance("SHA-1").digest(cert.getEncoded()) |
| SHA-256 fingerprint | MessageDigest.getInstance("SHA-256").digest(cert.getEncoded()) |
| MD5 fingerprint | MessageDigest.getInstance("MD5").digest(cert.getEncoded()) |
| mod_ssl Variable | Java / Tomcat Equivalent |
|---|---|
SSL_CLIENT_SAN_DNS_n |
cert.getSubjectAlternativeNames() — filter general name type 2 (dNSName) |
SSL_CLIENT_SAN_Email_n |
Same as above — filter type 1 (rfc822Name) |
| mod_ssl Variable | Java / Tomcat Equivalent |
|---|---|
SSL_CLIENT_CERT_CHAIN_n |
certs[n] (index 1+ in the X509Certificate[] array) |
tomcat-ssl/
├── pom.xml # Maven build with keytool + TomEE plugins
├── README.md
├── src/
│ ├── main/
│ │ ├── java/org/superbiz/tomcatssl/
│ │ │ └── CertInfoServlet.java # Servlet displaying all SSL properties
│ └── test/
│ └── conf/
│ └── server.xml # Tomcat config with mTLS enabled
└── target/conf/ # Generated at build time
├── keystore.jks
├── client.p12
├── client.cer
└── truststore.jks