Skip to content
DevOps AI ToolKit
Newsletter

Kali Linux for DevOps Engineers · Part 12 of 15

TLS and Certificate Troubleshooting With Kali Linux

Difficulty: Intermediate ~16 min Part 12/15
Series progress12 / 15
Series curriculum (15 lessons)

TLS is the layer that turns plain HTTP into HTTPS, and certificate problems are among the most common — and most frustrating — outages a DevOps engineer will diagnose. This lesson shows you how to inspect certificates, verify chains, check expiry, and confirm a certificate matches its private key using the openssl and curl tools that ship with Kali Linux, all against infrastructure you manage.

What TLS Certificates Actually Prove

When a client connects to https://example.com, the server presents a TLS certificate. That certificate does two jobs: it carries the server’s public key (used to negotiate encryption), and it lets the client verify it’s really talking to example.com and not an impostor.

A certificate is only trusted if a chain of signatures leads back to a root the client already trusts. A few terms you’ll meet constantly:

TermWhat it means
Leaf / server certificateThe certificate issued for your specific hostname (example.com)
Intermediate CAA certificate that signed your leaf; issued in turn by a root
Root CAThe trust anchor, pre-installed in OS/browser trust stores
CN (Common Name)The legacy “subject” name field; largely superseded by SAN
SAN (Subject Alternative Name)The list of hostnames the certificate is actually valid for
Chain of trustLeaf → intermediate(s) → root; each link signed by the next
Private keyThe secret half of the key pair; must match the certificate’s public key

💡 Note — Modern clients (all current browsers, most libraries) validate the hostname against the SAN list, not the CN. A certificate with the right CN but a missing SAN entry will still be rejected. When you troubleshoot “wrong host” errors, look at the SAN.

The TLS Handshake in One Paragraph

Before any HTTPS request body flows, the client and server perform a TLS handshake: the server sends its certificate (and usually the intermediates), the client validates the chain against its trust store, checks the certificate is currently valid (not expired, not yet-valid), confirms the hostname matches a SAN entry, and then both sides agree on keys to encrypt the session. Almost every “certificate error” you’ll debug is a failure in one of those specific steps — so your job is to find which step failed.

Inspecting a Live Certificate With openssl s_client

openssl s_client opens a raw TLS connection and prints everything the server sent, including the full certificate chain. It’s the single most useful command for TLS debugging.

openssl s_client -connect example.com:443 -servername example.com
  • -connect example.com:443 — the host and port to open a TLS connection to.
  • -servername example.com — sends the SNI (Server Name Indication) hostname. Modern servers host many sites on one IP and choose which certificate to present based on SNI. Omit this and you may get the wrong (default) certificate, which is a classic source of confusing results.

The command stays connected waiting for input; press Ctrl+C, or feed it an empty input so it exits cleanly:

echo | openssl s_client -connect example.com:443 -servername example.com

Near the top of the output you’ll see the chain the server presented:

Certificate chain
 0 s:CN=example.com
   i:C=US, O=DigiCert Inc, CN=DigiCert Global G3 TLS ECC SHA384 2020 CA1
 1 s:C=US, O=DigiCert Inc, CN=DigiCert Global G3 TLS ECC SHA384 2020 CA1
   i:C=US, O=DigiCert Inc, CN=DigiCert Global Root G3

Each entry shows s: (subject — who this certificate is for) and i: (issuer — who signed it). Read it as a chain: entry 0 is your leaf, signed by entry 1’s subject, and so on up toward the root. At the bottom, look for the verification result:

Verify return code: 0 (ok)

Any non-zero code tells you validation failed. Common ones:

Verify codeMeaning
0 (ok)Chain validated successfully
10Certificate has expired
20Unable to get local issuer certificate (missing intermediate)
21Unable to verify the first certificate (broken/incomplete chain)

🔎 Troubleshooting Tip — A 20 or 21 verify code almost always means the server is not sending its intermediate certificate. Your browser may still work (it caches intermediates from previous sites), while curl, mobile apps, and other servers fail. The fix is on the server: configure it to serve the full chain (leaf + intermediates), not just the leaf.

Decoding the Certificate With openssl x509

openssl s_client shows the connection; openssl x509 decodes a single certificate in detail. You can pipe one straight from s_client:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -text
  • -noout — don’t re-print the raw encoded certificate, only the decoded details.
  • -text — print the full human-readable breakdown: version, serial, issuer, validity dates, subject, public key, and all extensions (including the SAN list).

-text is verbose. Most of the time you want one specific field, and openssl x509 has flags for exactly that:

# Who is this certificate for?
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -subject

# Who issued (signed) it?
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -issuer

# When is it valid?
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -dates
  • -subject — prints the subject line, e.g. subject=CN=example.com.
  • -issuer — prints who signed it; use this to see which CA (and which intermediate) is in play.
  • -dates — prints notBefore and notAfter, the validity window.

You can also decode a certificate file already on disk (a PEM .crt/.pem) without any network call:

openssl x509 -in example.com.crt -noout -text

Verifying Expiry

Expiry is the most common certificate outage, and usually the easiest to confirm.

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -dates
notBefore=Jan 15 00:00:00 2026 GMT
notAfter=Apr 15 23:59:59 2026 GMT

notAfter is your expiry date. openssl x509 can also answer expiry questions directly, which is perfect for monitoring:

# Exit status 0 if the cert is still valid for at least 7 more days, non-zero otherwise
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -checkend 604800
  • -checkend 604800 — “will this certificate expire within 604800 seconds (7 days)?” It prints Certificate will not expire / Certificate will expire and sets its exit code accordingly, so you can wire it into a cron check or CI gate.

🔎 Troubleshooting Tip — For an expired certificate, s_client reports Verify return code: 10 (certificate has expired) and -dates shows a notAfter in the past. Before assuming renewal failed, check the server’s own clock — a host with a badly skewed clock can report a valid certificate as expired (or vice versa). Confirm with timedatectl on the server.

🛠️ DevOps Perspective — Certificate expiry is a predictable, preventable outage. Bake a -checkend check into monitoring for every endpoint you own so you’re alerted weeks ahead, not paged at 2 a.m. when the leaf silently expires. Automated issuance (ACME/Let’s Encrypt) reduces the risk, but monitoring still catches renewal automation that has quietly broken.

Checking the Chain and Intermediates

A certificate can be perfectly valid on its own and still fail because the server didn’t send the intermediates needed to link it to a trusted root. To see the full chain the server offered, add -showcerts:

echo | openssl s_client -connect example.com:443 -servername example.com -showcerts 2>/dev/null
  • -showcerts — prints every certificate in the chain as PEM blocks, not just the leaf. Count them: you should see the leaf plus at least one intermediate. If you only see one certificate, the server is likely serving an incomplete chain.

You can also ask openssl to validate a certificate file against a specific chain or trust bundle:

# Verify a leaf against an untrusted intermediate bundle, using the system roots
openssl verify -untrusted intermediate.pem example.com.crt
  • -untrusted intermediate.pem — supply the intermediate(s) so openssl can build the path from your leaf up to a trusted root. A result of example.com.crt: OK means the chain is complete and valid.

curl -v is an excellent second opinion, because it uses a real trust store and validates the way clients actually do:

curl -v https://example.com 2>&1 | grep -Ei 'SSL|TLS|certificate|subject|issuer|expire'
  • -v (verbose) — prints the TLS handshake details, including the certificate subject, issuer, and validity, and whether verification succeeded (SSL certificate verify ok) or failed (with a specific reason). Filtering with grep keeps the output focused on the TLS lines.

🛠️ DevOps Perspective — When a service works in your browser but breaks in a container, from another server, or in a mobile app, an incomplete chain is the usual culprit. Browsers hide the problem by caching intermediates; curl and most SDKs do not. Reproduce with curl -v from a clean environment (a fresh Kali VM is ideal) to confirm before you touch server config.

Confirming the SAN Matches the Hostname

A certificate is only valid for the hostnames listed in its Subject Alternative Name extension. To pull just the SAN list:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -ext subjectAltName
  • -ext subjectAltName — prints only the SAN extension, e.g. DNS:example.com, DNS:www.example.com. Confirm the exact hostname a client uses appears in that list. Remember that example.com and www.example.com are different names — a certificate for one is not automatically valid for the other unless both are listed (or a wildcard like *.example.com covers it).

🔎 Troubleshooting Tip — The error “certificate is valid for X, not Y” (curl: SSL: no alternative certificate subject name matches target host name) means the SAN list doesn’t include the hostname you requested. Common causes: (1) the wrong virtual host / default certificate is served because SNI wasn’t sent — re-test with -servername; (2) you’re hitting the site by IP or an internal alias not in the SAN; (3) a wildcard *.example.com doesn’t cover the bare example.com or a two-level subdomain like a.b.example.com. Compare the SAN output above against the exact name the client used.

Verifying a Certificate Matches Its Private Key

When you deploy or rotate a certificate, a frequent failure is pairing the wrong private key with the certificate — the server then refuses to start, or presents a certificate it can’t complete the handshake for. A certificate and its private key match only if their public modulus is identical. You can compare them without ever exposing the key by hashing each modulus:

# Modulus of the certificate
openssl x509 -noout -modulus -in example.com.crt | openssl md5

# Modulus of the private key
openssl rsa -noout -modulus -in example.com.key | openssl md5
  • openssl x509 -noout -modulus — extracts the public modulus from the certificate.
  • openssl rsa -noout -modulus — extracts the modulus from the private key (for an ECDSA key, use openssl ec -noout -pubout and compare public keys instead).
  • Piping each into openssl md5 produces a short hash. If the two hashes are identical, the certificate and key are a matching pair. If they differ, you have a mismatch — you’re using the wrong key (or the wrong certificate).

Optionally confirm the CSR that requested the certificate matches too:

openssl req -noout -modulus -in example.com.csr | openssl md5

All three hashes matching means the CSR, certificate, and key belong to the same key pair.

🔐 Security Note — Private keys are secrets. Keep them chmod 600, never commit them to git, never paste them into a chat or ticket, and never copy them onto a shared or disposable Kali VM unless that VM is truly under your control. Comparing the modulus hash (as above) lets you verify a match without ever printing or transmitting the key material itself.

🔐 Security Note — When a chain or hostname check fails, it is tempting to “fix” it by disabling verification — curl -k, --insecure, verify=False, or NODE_TLS_REJECT_UNAUTHORIZED=0. Never disable certificate verification in production to work around a certificate problem. It doesn’t fix the certificate; it silently turns off the protection TLS exists to provide and exposes traffic to interception. curl -k is acceptable only as a diagnostic in a lab, to isolate whether a fault is chain/hostname validation versus connectivity. Then go and fix the certificate.

Try It in Your Lab

Only test systems you own or are explicitly authorized to test. The public example.com used here is safe for read-only inspection; anything involving keys should use certificates you generated.

🧪 Try It — Generate a self-signed certificate and prove the key-matching check works:

# 1. Create a key + self-signed cert with a SAN, valid 1 day
openssl req -x509 -newkey rsa:2048 -nodes \
  -keyout lab.key -out lab.crt -days 1 \
  -subj "/CN=lab.example.com" \
  -addext "subjectAltName=DNS:lab.example.com"

# 2. Inspect what you made
openssl x509 -in lab.crt -noout -subject -dates -ext subjectAltName

# 3. Confirm the cert and key are a matching pair (hashes should be identical)
openssl x509 -noout -modulus -in lab.crt | openssl md5
openssl rsa  -noout -modulus -in lab.key | openssl md5

Now try it with a mismatch: generate a second key (openssl genrsa -out other.key 2048), hash its modulus, and confirm it does not match lab.crt. That’s exactly the check you’ll run when a production deploy rejects a certificate/key pair.

Where This Fits

TLS troubleshooting sits directly on top of the HTTP layer and the network layer beneath it:

  • HTTP and API troubleshooting — once TLS validates, the request/response debugging continues at the HTTP layer with curl.
  • Networking fundamentals — if the TLS handshake never even starts, the problem is connectivity (routing, firewall, port 443 reachability), not the certificate.

If you also work with configuration files that define TLS settings, the site’s validators can sanity-check your YAML before it ever reaches a server.

What You Learned

  • A TLS certificate is only trusted when a complete chain — leaf → intermediate(s) → root — validates, and openssl s_client -connect host:443 -servername host shows you exactly what the server presents plus the Verify return code.
  • openssl x509 -noout with -text, -subject, -issuer, and -dates decodes any certificate, and -checkend turns expiry into a scriptable monitoring check.
  • Hostname failures (“valid for X, not Y”) come from the SAN list, not the CN — inspect it with -ext subjectAltName and remember SNI (-servername) decides which certificate is served.
  • Incomplete chains are the classic “works in my browser, fails everywhere else” bug — confirm with -showcerts and curl -v, and fix it on the server by serving the full chain.
  • Verify a certificate matches its private key by comparing modulus hashes (openssl ... -modulus | openssl md5), which proves the pair without exposing the key.
  • Never disable certificate verification in production to paper over a chain or hostname error — diagnose and fix the certificate instead.

Affiliate Disclosure: Some links on this page are affiliate links. If you purchase through one of these links, DevOps AI Toolkit may earn a commission at no additional cost to you. See our affiliate disclosure.

← Back to Kali Linux for DevOps Engineers

Related on DevOps AI Toolkit