Why this matters
The previous lesson was about reading what a server sends. This one is the other side of the job: checking certificate files before you deploy them, building the chain file a server needs, catching expiry before users do, and the classic "curl works, the Java service does not" puzzle.
What you need to know already: leaf, intermediate, root, chain and trust store (9.15), openssl s_client (9.15), pipes and 2>/dev/null (1.7), the JVM (5.13), and restarting a systemd service (Chapter 2).
Reading a certificate file
Certificates are usually stored as PEM files: base64 text between -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- lines, often named .crt or .pem. openssl x509 reads one: -in FILE the input, -noout do not print the certificate itself back, then the fields you want.
$ openssl x509 -in /etc/ssl/lab/shop.lab.crt -noout -subject -issuer -dates
subject=CN=shop.lab
issuer=C=RO, O=LabCorp, CN=LabCorp Issuing CA G2
notBefore=Aug 24 17:43:01 2026 GMT
notAfter=Nov 22 17:43:01 2026 GMT
$ openssl x509 -in /etc/ssl/lab/shop.lab.crt -noout -ext subjectAltName
X509v3 Subject Alternative Name:
DNS:shop.lab
The four facts that matter: who (subject and SAN), signed by whom (issuer), when (dates). Options print in the order you give them. In a name like C=RO, O=LabCorp, CN=...: C = country, O = organisation, CN = common name. -ext subjectAltName prints one extension (an extra field; X509v3 is the certificate format version).
Straight from a server, without saving anything:
$ echo | openssl s_client -connect vault.lab:443 2>/dev/null | openssl x509 -noout -subject -enddate -ext subjectAltName
subject=CN=vault.lab
notAfter=Sep 11 17:43:01 2026 GMT
X509v3 Subject Alternative Name:
DNS:vault.internal.lab
echo | closes s_client's stdin so it exits; 2>/dev/null drops the depth lines; openssl x509 reads the first PEM block from the pipe.
The full decode
$ mkdir -p ~/certs && cp /etc/ssl/lab/*.crt ~/certs/ && cd ~/certs
$ openssl x509 -in shop.lab.crt -noout -text
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
7e:be:de:17:07:0c:b6:d0:d1:eb:ec:36:36:71:06:5e:77:cc:5d:78
Signature Algorithm: ecdsa-with-SHA256
Issuer: C=RO, O=LabCorp, CN=LabCorp Issuing CA G2
Validity
Not Before: Aug 24 17:43:01 2026 GMT
Not After : Nov 22 17:43:01 2026 GMT
Subject: CN=shop.lab
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
...
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:shop.lab
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Authority Key Identifier:
...
-text decodes everything. The serial number and signature algorithm identify the certificate and how it was signed; skip them for now. Beyond the four facts, look at Basic Constraints (CA:TRUE for an intermediate or root, CA:FALSE for a leaf), Extended Key Usage (what the key may be used for: a server cert needs TLS Web Server Authentication; client certificates, see mTLS below, need TLS Web Client Authentication), and Authority Information Access, the URL browsers use to fetch a missing intermediate.
Expiry monitoring in one line
$ openssl x509 -in shop.lab.crt -noout -checkend 604800; echo "exit $?"
Certificate will not expire
exit 0
$ echo | openssl s_client -connect vault.lab:443 2>/dev/null | openssl x509 -noout -checkend 0; echo "exit $?"
Certificate will expire
exit 1
-checkend N exits 1 if the certificate expires within N seconds (604800 = one week). That is a complete health check for a systemd timer (2.16) or cron job. An expired internal certificate is a top-five enterprise outage cause and the most preventable one on the list; every certificate you deploy should have an owner and an alert at 30, 14 and 7 days.
Verify a chain offline
Before you deploy files, prove they form a chain:
$ openssl verify shop.lab.crt
CN=shop.lab
error 20 at 0 depth lookup: unable to get local issuer certificate
error shop.lab.crt: verification failed
$ openssl verify -untrusted labcorp-issuing-g2.crt shop.lab.crt
shop.lab.crt: OK
openssl verify FILE checks that a certificate chains to a trusted root. -untrusted supplies intermediates (to use but not trust); -CAfile replaces the trust store with specific roots. By default verify uses the system store, which here contains the LabCorp root.
Building a fullchain file
A fullchain is simply the leaf followed by its intermediates, concatenated:
$ cat shop.lab.crt labcorp-issuing-g2.crt > shop.lab.fullchain.crt
Order matters: leaf first, then the certificate that signed it, then the one that signed that. Not the root. Check the result - but note that openssl x509 reads only the first certificate in a file, so it cannot show you a bundle:
$ openssl crl2pkcs7 -nocrl -certfile shop.lab.fullchain.crt | openssl pkcs7 -print_certs -noout
subject=CN=shop.lab
issuer=C=RO, O=LabCorp, CN=LabCorp Issuing CA G2
subject=C=RO, O=LabCorp, CN=LabCorp Issuing CA G2
issuer=C=RO, O=LabCorp, CN=LabCorp Root CA
(The recipe converts the file to PKCS#7, another certificate file format, whose -print_certs lists every certificate - copy it as is.) Each issuer= equals the next subject=: a correct chain. grep -c 'BEGIN CERTIFICATE' file counts them quickly.
Does this key belong to this certificate?
nginx refuses to start if they do not match:
nginx: [emerg] SSL_CTX_use_PrivateKey("/etc/ssl/private/shop.lab.key") failed (SSL: error:05800074:x509 certificate routines::key values mismatch)
Check before deploying - compare the public keys:
$ openssl x509 -in shop.lab.crt -noout -pubkey | sha256sum
9f2c... -
$ sudo openssl pkey -in /etc/ssl/private/shop.lab.key -pubout | sha256sum
9f2c... -
-pubkey prints the public key inside the certificate; openssl pkey -pubout derives the public key from the private key; sha256sum turns each into a short fingerprint (a hash). Same hash, same key pair. (Older guides compare -modulus, which only works for RSA keys; the public-key comparison works for every key type - RSA and EC are the two common kinds.)
The JVM has its own truststore
A JDK (Java Development Kit) is an installed copy of Java: the java program that starts the JVM (5.13), plus tools like keytool. $JAVA_HOME is the directory it lives in. Java does not read /etc/ssl/certs; it has its own trust store (Java calls it a truststore), a file called cacerts:
$JAVA_HOME/lib/security/cacerts PKCS12, default password "changeit"
(PKCS12 is the file format; "changeit" is the famous default password, which only protects the file against being changed.) This is the most common "curl works, Java fails" cause in a company with its own CA. The nuance you need, because it confuses people:
- Ubuntu's own OpenJDK packages (installed with apt) link
cacertsto/etc/ssl/certs/java/cacerts, which theca-certificates-javahook regenerates when you runupdate-ca-certificates. On those, the system store and Java agree. - A JDK from a download or a vendor build - Temurin (a popular free JDK build) unpacked in
/opt, anything installed by the SDKMAN tool - has its owncacerts, untouched byupdate-ca-certificates. The corporate CA is simply not there.
The Java error is always this one:
javax.net.ssl.SSLHandshakeException: PKIX path building failed:
sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid
certification path to requested target
Look, then import:
$ keytool -list -cacerts -storepass changeit | grep -i labcorp
$ sudo keytool -importcert -noprompt -alias labcorp-root -file /usr/local/share/ca-certificates/labcorp-root.crt -cacerts -storepass changeit
Certificate was added to keystore
keytool manages Java key files: -list shows entries, -importcert adds one, -noprompt skips the "Trust this certificate?" question, -alias is the entry's name, -file the certificate, -storepass the password. -cacerts means "this JDK's cacerts" (use keytool from the same JDK). Two things to remember: the JVM reads the truststore at startup - restart the service - and the JDK's cacerts is replaced on every JDK upgrade, so a hand import is a future outage. Durable options: add the CA wherever the JDK gets installed (the same script or package that installs it), point the JVM at a store you manage with the option -Djavax.net.ssl.trustStore=..., or (newer Temurin builds) enable their "use system CA certificates" option.
mTLS
In mutual TLS (mTLS) the client presents a certificate too, and the server verifies it against its own trusted CA list - so both sides prove who they are. Banks use it for calls between internal services. With curl:
curl --cert client.crt --key client.key --cacert server-ca.crt https://payments.internal/
(--cert our certificate, --key its private key, --cacert the CA to trust for the server's certificate.)
Everything above applies twice: two chains, two sets of expiry dates, two trust stores. The classic mTLS failure is the server's error, not the client's: the client cert expired, or was issued by a CA the server does not list. openssl s_client -connect host:443 -cert client.crt -key client.key shows the "Acceptable client certificate CA names" the server asks for.
What you can now do
- Read a certificate file's subject, issuer, dates and SAN with
openssl x509. - Prove a chain offline with
openssl verify -untrusted, build a fullchain in the right order, and check a key matches its certificate. - Explain "curl works, Java fails" as two trust stores, and fix it with keytool.