kmail.at
← learning

networking · difficulty ◆◆

openssl — inspect any TLS certificate from the terminal

One pipeline, `echo | openssl s_client -connect host:443 -servername host 2>/dev/null | openssl x509 -noout -dates`, answers the certificate questions that page you at 03:00.

Every browser tab you have open says the padlock is fine. None of them will tell you the certificate dies in nine days — one pipeline will.

fully AI-generatedThis text is fully AI-generated and was published without human editorial review.

2026-10-15 · 6 min read

$ openssl

What it does

openssl is the command line for the library that browsers, load balancers and half of your containers link against. Two subcommands carry almost all of the everyday certificate work. s_client is a bare TLS client: you hand it host:port, it completes the handshake and prints what happened. x509 is a certificate reader: you hand it a PEM file or, more usefully, the stdout of s_client, and it pulls out one field per flag. Neither of them knows what HTTPS is — there is no URL, no path, no redirect, no cookie jar. That narrowness is the point: you are talking to the TLS layer with nothing in between. The box I am typing on runs OpenSSL 3.0.13, and every output below was captured from it.

The three questions it answers

When does it expire? notAfter in the certificate, in GMT, nothing else. Which names does it cover? The subjectAltName extension — and only that; the CN has been ignored by browsers for years. Is this key the one inside this certificate? Compare the SHA-256 of the public key exported from both sides. Those three questions account for essentially every certificate incident I have been paged for, and all three are one pipeline each. The fourth question — is the chain trusted — is `-brief`'s 'Verification: OK', which is the part s_client checks against your local trust store rather than merely printing.

stdout, stderr, and the stdin nobody closes

s_client writes the peer's PEM certificate to stdout and everything else to stderr: the verify chain, the negotiated cipher, the session byte counters. So `2>/dev/null` before the pipe is not decoration, it is what makes the pipeline stable — and the certificate text on stdout is the only thing x509 should ever see. Two consequences follow. First, s_client stays connected and forwards stdin to the server, because it is a client and not a fetcher, so without `echo |` or `</dev/null` it prints the certificate and then sits there. Second, even the tidy `-brief` summary is written to stderr, which is why `-brief 2>/dev/null` returns nothing at all.

Example

$ echo | openssl s_client -connect kmail.at:443 -servername kmail.at 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates
subject=CN = kmail.at
issuer=C = US, O = Let's Encrypt, CN = YR2
notBefore=Sep 24 14:16:58 2026 GMT
notAfter=Dec 23 14:16:57 2026 GMT

The whole check, run from any machine with openssl installed. `2>/dev/null` throws away roughly 5 KB of handshake detail that s_client writes to stderr; the PEM on stdout is what x509 eats. Both timestamps are GMT, so compare them against GMT and not against your laptop's clock. Add `-dateopt iso_8601` if you want sortable dates instead of the OpenSSL 1.0 spacing.

$ echo | openssl s_client -connect kmail.at:443 -servername kmail.at 2>/dev/null \
  | openssl x509 -noout -ext subjectAltName
X509v3 Subject Alternative Name: 
    DNS:kmail.at

The names the certificate is actually valid for. `-subject` printing CN = kmail.at is decoration: Chrome, Firefox and Go's crypto/tls match the hostname only against SAN entries. A renewal that yields CN=www.example.com with no SAN for example.com is a broken certificate that looks perfect in `-subject`. Yes, there is a stray space after that colon — it is generated by OpenSSL, not a typo in this page.

$ openssl req -x509 -newkey rsa:2048 -keyout lab.key -out lab.crt -days 5 -nodes \
  -subj "/CN=lab.internal" 2>/dev/null
openssl x509 -in lab.crt -noout -checkend 604800; echo "exit=$?"
openssl x509 -in lab.crt -noout -ext subjectAltName
Certificate will expire
exit=1
No extensions in certificate

Three answers, one block. `-checkend 604800` asks a single yes/no question — is this certificate good for the next seven days — prints one line and sets the exit code: 1 for 'will expire', 0 for 'will not expire'. That exit code is the entire reason it belongs in cron. The third line is the trap I walked into while writing this: `openssl req -x509` on 3.0.13 adds no Subject Alternative Name unless you pass `-addext`, so a self-signed certificate minted like this is rejected by every modern client no matter how correct its dates are.

$ echo | openssl s_client -connect kmail.at:443 -servername kmail.at -brief 2>&1
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_128_GCM_SHA256
Peer certificate: CN = kmail.at
Hash used: SHA256
Signature type: RSA-PSS
Verification: OK
Server Temp Key: X25519, 253 bits
DONE

The one to read by eye: protocol, cipher suite, key-exchange group and, above all, Verification: OK — that is the chain checked against your local trust store, not just parsed. Nine lines instead of a 5 KB dump. Keep `2>&1` here; the summary is written to stderr, so `-brief 2>/dev/null` prints nothing whatsoever.

$ openssl x509 -in lab.crt -noout -pubkey | openssl sha256
openssl pkey -in lab.key -pubout | openssl sha256
openssl x509 -in lab.crt -noout -modulus | openssl sha256
SHA2-256(stdin)= 5256c5a57f54211526e436c73324e3deb23cb53783c4419d738560ce78c8c996
SHA2-256(stdin)= 5256c5a57f54211526e436c73324e3deb23cb53783c4419d738560ce78c8c996
SHA2-256(stdin)= b014f6661d1eee0404a40c4429187f62137f0d070a8f221569cdf77ef237d6ef

Two identical hashes mean this private key really belongs to this certificate — the check to run before you drop a .pem into a load balancer and learn otherwise during the deploy. Your hashes will differ because the key was generated at random; equality is the answer, not the value. Line three is the old RSA-only ritual: hashing -modulus also matches the key's modulus, but ask an EC or Ed25519 certificate for a modulus and you get `Modulus=No modulus for this public key type` with exit code 0 — so the pubkey hash is the version that works for every key type.

Common flags

-connect host:port
Where s_client dials. A socket address, not a URL — no scheme, no path. Pair it with -starttls for SMTP/IMAP/Postgres-style protocols.
-servername host
Sends the SNI extension. Without it you are shown whichever certificate that IP's default virtual host serves, and the mismatch error names a host you never typed.
-noout
Suppresses the base64 PEM body so you get only the field you asked for. Every x509 field flag is useless in a pipeline without it.
-dates / -enddate / -checkend N
Both validity timestamps, just the expiry, or a yes/no answer on the next N seconds — the last one exits 1 when the certificate falls inside the window.
-dateopt iso_8601
Reformats dates as 2026-09-24 14:16:58Z instead of the legacy 'Sep 24 14:16:58 2026 GMT', so you can sort and compare them as strings.
-ext subjectAltName
Prints one extension (3.0 and later). Replaces the old `x509 -text | grep -A1 'Alternative Name'` hack, which parsed a human-readable dump.
-brief
One-screen handshake summary: protocol, cipher suite, verification result, signature type, temp key. Newer than the rest of the CLI and written to stderr.

History

SSLeay's initials, renamed 23 December 1998

The code was written by Eric A. Young and named SSLeay — SSL plus 'eay', his own handle. In 1998 Young and Tim Hudson handed the library to a group of volunteers, Ralf S. Engelschall among them, and it was renamed OpenSSL; the first release under the new name, 0.9.1c, is dated 23 December 1998. The command line that shipped with it had one subcommand per verb, a pile of single-dash flags and quiet output flags for scripting, and that grammar is still what you type today.

After Heartbleed: two maintainers on payroll

Before April 2014 OpenSSL was maintained in spare time, on donations of a few thousand dollars a year. The heartbeat bug — CVE-2014-0160, a missing bounds check on a length field in an extension added for DTLS in 2012 — turned that into a funding problem headline, and the Linux Foundation's Core Infrastructure Initiative, followed by the OpenSSL Software Foundation, put the project's first full-time maintainers on payroll. The 3.0 provider architecture that s_client now sits on is the product of that suddenly-paid-for engineering attention.

Fun facts

Pros & cons

pros

  • + Already on the box — distro package, macOS, every container base image. No agent, no account, no dashboard, no certificate uploaded anywhere
  • + Covers the entire lifecycle in one binary: mint a CSR, self-sign something for the lab, read a live handshake, prove which key sits in a certificate
  • + Scriptable by design: -noout and single-field flags keep output one line per question, and -checkend turns an expiry check into a plain exit code

cons

  • − Flag soup — a few hundred subcommands, and the same idea is spelled differently across them (-noout on x509, -pubout on pkey, -text or -ext depending on what you want)
  • − s_client floods stderr with handshake detail, so the pipeline is unreadable until you silence or summarise it, and the idiomatic filtering is easy to misplace
  • − Sharp defaults with no warnings: no SAN from req -x509, no SNI without -servername, and -checkend tells you the bad news only through its exit code

Takeaways

  1. 1Ask expiry with `| openssl x509 -noout -dates`; both values are GMT, so never compare them to local time
  2. 2Always pass -servername alongside -connect, because SNI decides which certificate you are shown
  3. 3-checkend N is the whole monitoring check: one line of output and exit code 1 when the certificate expires within N seconds
  4. 4Prove a key, certificate or CSR match with `-pubkey | openssl sha256` on both sides — identical hash for RSA, ECDSA and Ed25519 alike
  5. 5Validate a renewal before it ships: `openssl req -in new.csr -noout -subject -verify` prints 'Certificate request self-signature verify OK', and `-addext 'subjectAltName=DNS:name'` is what puts a SAN into a self-signed cert

Related commands