{"id":31,"date":"2026-07-30T18:02:33","date_gmt":"2026-07-30T15:02:33","guid":{"rendered":"https:\/\/john-nessime.com\/blog\/?p=31"},"modified":"2026-07-30T18:11:35","modified_gmt":"2026-07-30T15:11:35","slug":"tls-certificate-errors","status":"publish","type":"post","link":"https:\/\/john-nessime.com\/blog\/linux\/tls-certificate-errors\/","title":{"rendered":"TLS Certificate Errors: Chain Issues, SANs, and Expiry Automation"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">The site loads fine in your browser. Then a mobile app starts throwing handshake failures, a webhook from a payment provider stops arriving, and someone on the team runs <code>curl<\/code> and gets &#8220;SSL certificate problem: unable to get local issuer certificate&#8221;. Nothing changed on the server. Except something did, and your browser has been quietly covering for it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Nearly every TLS certificate error I have had to chase down comes back to one of three things: the chain your server sends is incomplete, the certificate does not actually cover the hostname being requested, or it expired and no alert fired. This article walks through all three \u2014 how to identify which one you are looking at, the exact commands to prove it, and how to set up renewal automation so the third category stops being a category at all.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why TLS certificate errors are so confusing to debug<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Two things conspire to make this harder than it should be.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The first is that browsers are forgiving in ways other clients are not. When a server sends an incomplete certificate chain, most modern browsers will quietly fetch the missing intermediate using the Authority Information Access extension embedded in the certificate. They fix your misconfiguration on the fly and show you a padlock. Meanwhile <code>curl<\/code>, Python&#8217;s <code>requests<\/code>, Java clients, Go services, and older Android devices do no such thing. They fail. This is why &#8220;it works in Chrome&#8221; is worth almost nothing as a diagnostic signal.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The second is that error messages describe the symptom rather than the cause. &#8220;Unable to get local issuer certificate&#8221; sounds like a client problem. It usually is not \u2014 it usually means your server is not sending the full chain. Learning to translate the message into the actual failure is most of the work.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">One command that tells you almost everything<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Before touching config files, look at what the server is actually sending on the wire:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>openssl s_client -connect example.com:443 -servername example.com -showcerts &lt;\/dev\/null<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>-servername<\/code> flag matters more than people expect. It sets SNI, and without it you may be handed the server&#8217;s default certificate rather than the one for the domain you care about. Plenty of confusing &#8220;the certificate is for the wrong domain&#8221; reports come from testing without SNI.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Three parts of the output are worth reading carefully:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>The &#8220;Certificate chain&#8221; block<\/strong> near the top, listing each certificate the server sent with its subject and issuer.<\/li>\n\n<li><strong>The &#8220;Verify return code&#8221;<\/strong> at the bottom. <code>0 (ok)<\/code> is what you want. <code>20<\/code> and <code>21<\/code> both point at chain problems.<\/li>\n\n<li><strong>The subject line of the first certificate<\/strong>, which tells you whether you even got the certificate you expected.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">If you want the certificate details without the chain noise, pipe it into <code>x509<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>echo | openssl s_client -connect example.com:443 -servername example.com 2&gt;\/dev\/null \n  | openssl x509 -noout -subject -issuer -dates -ext subjectAltName<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That single line answers &#8220;who issued it&#8221;, &#8220;when does it expire&#8221;, and &#8220;what hostnames does it cover&#8221; in one shot. It is the first thing I run.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Family one: certificate chain issues<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A publicly trusted certificate is almost never signed directly by a root. There is at least one intermediate CA in between. The client already trusts the root \u2014 it ships in the operating system or browser trust store \u2014 but it has no idea about the intermediate unless you hand it over. Your server&#8217;s job is to send the leaf certificate plus every intermediate needed to walk up to a trusted root.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The classic mistake: serving the leaf without the intermediates<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If you use Let&#8217;s Encrypt, certbot writes several files into <code>\/etc\/letsencrypt\/live\/example.com\/<\/code>. Two of them look interchangeable and are not:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>cert.pem<\/code> \u2014 the leaf certificate on its own.<\/li>\n\n<li><code>fullchain.pem<\/code> \u2014 the leaf plus the intermediates.<\/li>\n\n<li><code>chain.pem<\/code> \u2014 the intermediates on their own.<\/li>\n\n<li><code>privkey.pem<\/code> \u2014 the private key.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Nginx wants <code>fullchain.pem<\/code>. Point it at <code>cert.pem<\/code> and browsers will still work, so the mistake survives testing and surfaces weeks later when an API client fails.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>server {\n    listen 443 ssl;\n    server_name example.com www.example.com;\n\n    ssl_certificate     \/etc\/letsencrypt\/live\/example.com\/fullchain.pem;\n    ssl_certificate_key \/etc\/letsencrypt\/live\/example.com\/privkey.pem;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">On Apache 2.4.8 and later, <code>SSLCertificateFile<\/code> can hold the leaf and the intermediates concatenated together, and <code>SSLCertificateChainFile<\/code> is deprecated. If you inherited a config that still uses the old directive, it will work, but it is worth cleaning up:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SSLCertificateFile    \/etc\/letsencrypt\/live\/example.com\/fullchain.pem\nSSLCertificateKeyFile \/etc\/letsencrypt\/live\/example.com\/privkey.pem<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Wrong order, or a root that should not be there<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Order is part of the specification, not a suggestion. The leaf comes first, then each issuer in sequence walking upward. Some clients tolerate a shuffled bundle; several do not, and the ones that do not tend to be the ones you find out about from an angry customer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Including the root certificate at the end is a different case: it is harmless from a trust perspective, since the client ignores it and uses its own copy, but it adds bytes to every single handshake for no benefit. Leave it out.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Verifying a chain offline<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">You can check a bundle before deploying it, which is far better than discovering the problem in production:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Does the leaf verify against the intermediates you plan to serve?\nopenssl verify -untrusted chain.pem cert.pem\n\n# Print the subject and issuer of every certificate in a bundle, in order\nopenssl crl2pkcs7 -nocrl -certfile fullchain.pem \n  | openssl pkcs7 -print_certs -noout<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That second command is the one I reach for most. Read the output top to bottom: the issuer of each certificate should be the subject of the next one down. The moment that pattern breaks, you have found your gap.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The key and the certificate do not match<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Less common, but it produces a spectacularly unhelpful error. After a renewal where files were copied around by hand, confirm the pair still belongs together \u2014 the two hashes must be identical:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>openssl x509 -noout -modulus -in cert.pem | openssl md5\nopenssl rsa  -noout -modulus -in privkey.pem | openssl md5<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Family two: SAN mismatches<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The Common Name field is dead. It has been deprecated for hostname matching for years, and Chrome stopped honouring it entirely back in 2017. What matters now is the Subject Alternative Name extension. If the hostname a client requested is not in the SAN list, the connection fails, regardless of what the CN says.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Check what a certificate actually covers:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># From a file\nopenssl x509 -noout -ext subjectAltName -in cert.pem\n\n# From a live server\necho | openssl s_client -connect example.com:443 -servername example.com 2&gt;\/dev\/null \n  | openssl x509 -noout -ext subjectAltName<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">The four SAN mistakes that keep recurring<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Apex without www, or www without apex.<\/strong> These are two distinct names. A certificate for <code>example.com<\/code> does not cover <code>www.example.com<\/code>. Both need to be in the SAN list, and both need to be passed to certbot with separate <code>-d<\/code> flags.<\/li>\n\n<li><strong>Assuming a wildcard covers everything.<\/strong> It does not. <code>*.example.com<\/code> matches exactly one label. It covers <code>api.example.com<\/code>. It does not cover <code>example.com<\/code> itself, and it does not cover <code>staging.api.example.com<\/code>. Multi-level subdomains need their own wildcard or their own entry.<\/li>\n\n<li><strong>A new subdomain that never made it into the renewal.<\/strong> You add <code>metrics.example.com<\/code> to nginx, point DNS at it, and forget that the certificate was issued for a fixed list of names. Adding a name means reissuing, not just reloading.<\/li>\n\n<li><strong>Connecting by IP address.<\/strong> Almost no public certificate includes an IP SAN, so hitting the server by IP will always fail hostname verification. That is expected behaviour, not a broken certificate.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">One more that catches people out: if you use Cloudflare in proxied mode, the certificate a visitor sees is Cloudflare&#8217;s edge certificate, not yours. Debugging the origin means bypassing the proxy and testing the origin IP directly with the <code>Host<\/code> header set correctly, or temporarily turning the orange cloud grey.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Family three: expiry, and why automation is no longer optional<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Expiry used to be a calendar problem. You bought a certificate for a year, set a reminder, and dealt with it once. That era is closing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In April 2025 the CA\/Browser Forum passed ballot SC-081v3, which phases the maximum validity period for publicly trusted TLS certificates down from 398 days to 47 days. The schedule runs in three steps: 200 days from 15 March 2026, 100 days from 15 March 2027, and 47 days from 15 March 2029. The first phase is already in force. Domain validation reuse windows shrink alongside it.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">The 47-day target was chosen deliberately: short enough that manual renewal stops being viable at any meaningful scale. Automation is the point of the change, not a side effect of it.<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">If you run a handful of domains, this is a nuisance. If you run fifty, manual renewal is already finished as a strategy.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Getting ACME renewal right<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Certbot installs a systemd timer or cron entry that attempts renewal twice a day and does nothing unless the certificate is close to expiry. Confirm it is actually there:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>systemctl list-timers | grep certbot\ncertbot renew --dry-run<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The dry run exercises the whole path against the staging environment without touching rate limits. Run it after any change to your web server config, not just after installing certbot. A renewal that worked last quarter can break because someone added a redirect that intercepts the ACME challenge path.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The trap: renewal succeeds, the site still serves the old certificate<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This is the failure I see most often, and it is genuinely maddening because every log says success. Certbot writes the new certificate to disk. Nginx has the old one loaded in memory and keeps serving it until it is told otherwise. The certificate on disk is valid, the certificate on the wire is expired, and everything looks fine from the server&#8217;s point of view.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The fix is a deploy hook, which runs only when a certificate was actually renewed:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>certbot renew --deploy-hook \"systemctl reload nginx\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Better still, make it permanent by adding it to the renewal configuration at <code>\/etc\/letsencrypt\/renewal\/example.com.conf<\/code>, under the <code>[renewalparams]<\/code> section, so it survives however the renewal happens to be triggered:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>renew_hook = systemctl reload nginx<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Use <code>reload<\/code> rather than <code>restart<\/code>. A reload picks up the new certificate without dropping connections.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Remember that anything else terminating TLS needs the same treatment. A mail server, a database proxy, a Docker container with the certificate baked into its image \u2014 each one needs its own reload step, and containers in particular will happily run for months with an expired certificate mounted from the host.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Wildcards need DNS-01<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Wildcard certificates cannot be issued through HTTP-01 validation. They require DNS-01, which means certbot needs to create a TXT record on your domain, which means API credentials for your DNS provider. Use a provider plugin rather than the manual mode \u2014 manual DNS-01 cannot be automated, and an unautomated wildcard on a 47-day cycle is an outage with a scheduled start time.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If issuance fails outright with a policy error, check your CAA records before assuming the ACME client is broken. A CAA record that names a different CA will block issuance entirely:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dig CAA example.com +short<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Monitor from outside, not from the server<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Checking the certificate file on disk tells you what should be served. Only an external check tells you what is being served. Given the reload trap above, that difference is the entire point.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A minimal check that works anywhere:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/usr\/bin\/env bash\n# Usage: .\/cert-days.sh example.com\n# Exits non-zero if fewer than 21 days remain.\nset -euo pipefail\n\nDOMAIN=\"$1\"\nTHRESHOLD=21\n\nEND=$(echo | openssl s_client -connect \"${DOMAIN}:443\" -servername \"${DOMAIN}\" 2&gt;\/dev\/null \n      | openssl x509 -noout -enddate | cut -d= -f2)\n\nDAYS=$(( ( $(date -d \"${END}\" +%s) - $(date +%s) ) \/ 86400 ))\necho \"${DOMAIN}: ${DAYS} days remaining (expires ${END})\"\n\n[ \"${DAYS}\" -lt \"${THRESHOLD}\" ] &amp;&amp; exit 1\nexit 0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If you already run Prometheus, the blackbox exporter gives you this for free through the <code>probe_ssl_earliest_cert_expiry<\/code> metric. An alert rule looks like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>groups:\n  - name: tls\n    rules:\n      - alert: TLSCertificateExpiringSoon\n        expr: (probe_ssl_earliest_cert_expiry - time()) \/ 86400 &lt; 21\n        for: 10m\n        labels:\n          severity: warning\n        annotations:\n          summary: \"TLS certificate expiring in under 21 days\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Set the threshold well above your renewal window. With 200-day certificates renewing at 30 days remaining, a 21-day alert gives you nine days of warning that something in the pipeline broke. Once 47-day certificates arrive, those numbers compress hard, and an alert that fires too late is the same as no alert.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">A workflow for diagnosing TLS certificate errors<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">When something breaks, work through it in this order. It takes about a minute and rules out whole families of causes as you go.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Reproduce outside the browser.<\/strong> Use <code>curl -v<\/code> or <code>openssl s_client<\/code>. If the browser works and curl does not, you are almost certainly looking at a chain problem.<\/li>\n\n<li><strong>Read the verify return code.<\/strong> Code 20 or 21 means a chain issue. A hostname mismatch message means a SAN issue. A date error means expiry \u2014 or a wrong system clock on the client.<\/li>\n\n<li><strong>Print the dates.<\/strong> Rules expiry in or out in one command, and tells you whether the certificate on the wire matches the one on disk.<\/li>\n\n<li><strong>Print the SANs.<\/strong> Confirm the exact hostname being requested appears in the list. Watch for apex versus www.<\/li>\n\n<li><strong>Walk the chain.<\/strong> Each certificate&#8217;s issuer should be the next one&#8217;s subject. Find where the sequence breaks.<\/li>\n\n<li><strong>Compare disk against wire.<\/strong> If the file is newer than what is being served, you have a reload problem, not a certificate problem.<\/li>\n\n<li><strong>Check the client&#8217;s trust store.<\/strong> If everything above is clean and one specific client still fails, the problem has moved to their end \u2014 an old Android device, a Java service with its own <code>cacerts<\/code>, or a container image built on a base with a stale CA bundle.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">Common mistakes worth avoiding<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Testing only in a browser, and mistaking its silent chain repair for a working configuration.<\/li>\n\n<li>Pointing the web server at <code>cert.pem<\/code> instead of <code>fullchain.pem<\/code>.<\/li>\n\n<li>Renewing without reloading the service that holds the certificate in memory.<\/li>\n\n<li>Running <code>certbot renew<\/code> on a schedule but never running <code>--dry-run<\/code> after config changes.<\/li>\n\n<li>Copying key and certificate files between servers by hand and losing track of which pair belongs together.<\/li>\n\n<li>Monitoring the file on disk rather than the certificate actually being served.<\/li>\n\n<li>Forgetting that mail servers, load balancers, and containers terminate TLS too, and need their own renewal path.<\/li>\n\n<li>Adding a subdomain to the web server config and assuming the existing certificate covers it.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Best practices that hold up over time<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Automate issuance and renewal from day one, even for a single domain. The habit matters more than the scale.<\/li>\n\n<li>Always use a deploy hook, and always reload rather than restart.<\/li>\n\n<li>Alert on days remaining, measured externally, with a threshold at least twice your renewal window.<\/li>\n\n<li>Keep an inventory of every hostname you hold a certificate for. You cannot monitor what you have forgotten about.<\/li>\n\n<li>Prefer DNS-01 with a provider plugin if you need wildcards, and never rely on manual DNS validation.<\/li>\n\n<li>Set CAA records deliberately so you know which CAs are permitted to issue for your domain.<\/li>\n\n<li>Test with a non-browser client before calling a certificate deployment finished.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Frequently asked questions<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Why does my site work in Chrome but fail in curl?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Almost always an incomplete chain. Browsers fetch missing intermediate certificates automatically using the Authority Information Access extension; curl does not. Point your web server at the full chain file rather than the leaf certificate and both will work.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What does &#8220;unable to get local issuer certificate&#8221; actually mean?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The client could not build a path from your certificate to a root it trusts. Nine times out of ten the server is not sending its intermediates. Occasionally the client&#8217;s own CA bundle is out of date, which is common in older container base images.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Does a wildcard certificate cover the root domain?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No. <code>*.example.com<\/code> matches a single label, so it covers <code>www.example.com<\/code> but not <code>example.com<\/code> and not <code>a.b.example.com<\/code>. Include the apex explicitly as an additional SAN entry when you request the certificate.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">My certificate renewed but the site still shows it as expired. Why?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The service holding the certificate has not reloaded. The new file is on disk; the old one is still in memory. Add a deploy hook that reloads the service on renewal, then verify from outside with <code>openssl s_client<\/code> rather than trusting the file timestamp.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How short will certificate lifetimes actually get?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Under the CA\/Browser Forum schedule, 200 days applies from March 2026, 100 days from March 2027, and 47 days from March 2029. Private and internal CAs are not bound by these rules, but public certificates are.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Is the Common Name field still used at all?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Not for hostname validation. Modern clients read only the Subject Alternative Name extension. A certificate whose CN matches but whose SAN list does not include the hostname will be rejected.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Wrapping up<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">TLS certificate errors feel opaque mostly because the error text points at the wrong layer. Once you sort them into chain, SAN, or expiry, each one has a short diagnostic path and an obvious fix. Get comfortable with <code>openssl s_client<\/code> and <code>openssl x509<\/code> and you will resolve most incidents faster than it takes to find the right dashboard.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The expiry category is the one worth engineering away permanently. With maximum lifetimes dropping toward 47 days, any process that depends on a person remembering something will fail eventually. Automate the renewal, add the deploy hook, and alert on what is actually being served rather than what is sitting on disk. Do that once and TLS certificate errors go back to being a rare curiosity instead of a recurring outage.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Need help with TLS, certificates, or infrastructure automation?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">I work with teams on exactly this kind of problem \u2014 broken certificate chains, ACME automation across multiple servers, expiry monitoring wired into Prometheus and Grafana, and the wider DevOps plumbing that keeps it all running quietly in the background.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you would rather hand this off than debug it at two in the morning, you can hire me on Upwork.<\/p>\n\n\n\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/www.upwork.com\/freelancers\/~01f15a912ad84a6620\" target=\"_blank\" rel=\"noreferrer noopener\">Work with me on Upwork<\/a><\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Almost every TLS certificate error traces back to one of three causes: a broken chain, a hostname the certificate does not cover, or an expiry nobody was watching. Here is how to tell them apart in under a minute, and how to stop the third one from ever happening again.<\/p>\n","protected":false},"author":1,"featured_media":35,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26,63,31,30],"tags":[20,86,71,3,87,6,13,18,85,57,72,84,4],"class_list":["post-31","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-linux","category-system-administration","category-troubleshooting","category-web-security","tag-apache","tag-certbot","tag-cloudflare","tag-devops","tag-lets-encrypt","tag-linux","tag-monitoring","tag-nginx","tag-openssl","tag-ssl-certificate","tag-sysadmin","tag-tls","tag-troubleshooting","entry","has-media"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>TLS Certificate Errors: Chain, SAN &amp; Expiry Fixes<\/title>\n<meta name=\"description\" content=\"TLS certificate errors almost always come down to a broken chain, a missing SAN, or expiry. Diagnose each in one command and automate renewals for good.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/john-nessime.com\/blog\/linux\/tls-certificate-errors\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"TLS Certificate Errors: Chain, SAN &amp; Expiry Fixes\" \/>\n<meta property=\"og:description\" content=\"TLS certificate errors almost always come down to a broken chain, a missing SAN, or expiry. Diagnose each in one command and automate renewals for good.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/john-nessime.com\/blog\/linux\/tls-certificate-errors\/\" \/>\n<meta property=\"og:site_name\" content=\"John Nessime\" \/>\n<meta property=\"article:published_time\" content=\"2026-07-30T15:02:33+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-07-30T15:11:35+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/tls-certificate-errors-featured-1.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"800\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"John Nessime\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"John Nessime\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"12 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/linux\\\/tls-certificate-errors\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/linux\\\/tls-certificate-errors\\\/\"},\"author\":{\"name\":\"John Nessime\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/#\\\/schema\\\/person\\\/ede0b56d0c808f123f57d5d796902105\"},\"headline\":\"TLS Certificate Errors: Chain Issues, SANs, and Expiry Automation\",\"datePublished\":\"2026-07-30T15:02:33+00:00\",\"dateModified\":\"2026-07-30T15:11:35+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/linux\\\/tls-certificate-errors\\\/\"},\"wordCount\":2655,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/#\\\/schema\\\/person\\\/ede0b56d0c808f123f57d5d796902105\"},\"image\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/linux\\\/tls-certificate-errors\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/tls-certificate-errors-featured-1.png\",\"keywords\":[\"Apache\",\"Certbot\",\"Cloudflare\",\"DevOps\",\"Let's Encrypt\",\"Linux\",\"Monitoring\",\"Nginx\",\"OpenSSL\",\"SSL Certificate\",\"Sysadmin\",\"TLS\",\"Troubleshooting\"],\"articleSection\":[\"Linux\",\"System Administration\",\"Troubleshooting\",\"Web Security\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/john-nessime.com\\\/blog\\\/linux\\\/tls-certificate-errors\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/linux\\\/tls-certificate-errors\\\/\",\"url\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/linux\\\/tls-certificate-errors\\\/\",\"name\":\"TLS Certificate Errors: Chain, SAN & Expiry Fixes\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/linux\\\/tls-certificate-errors\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/linux\\\/tls-certificate-errors\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/tls-certificate-errors-featured-1.png\",\"datePublished\":\"2026-07-30T15:02:33+00:00\",\"dateModified\":\"2026-07-30T15:11:35+00:00\",\"description\":\"TLS certificate errors almost always come down to a broken chain, a missing SAN, or expiry. Diagnose each in one command and automate renewals for good.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/linux\\\/tls-certificate-errors\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/john-nessime.com\\\/blog\\\/linux\\\/tls-certificate-errors\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/linux\\\/tls-certificate-errors\\\/#primaryimage\",\"url\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/tls-certificate-errors-featured-1.png\",\"contentUrl\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/tls-certificate-errors-featured-1.png\",\"width\":1200,\"height\":800,\"caption\":\"Diagram of a TLS certificate chain showing root, intermediate and leaf certificates alongside openssl s_client output with verify return code 0\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/linux\\\/tls-certificate-errors\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"TLS Certificate Errors: Chain Issues, SANs, and Expiry Automation\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/\",\"name\":\"John Nessime\",\"description\":\"Cloud, DevOps, Data &amp; AI \u2014 Built, Tested, Explained\",\"publisher\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/#\\\/schema\\\/person\\\/ede0b56d0c808f123f57d5d796902105\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/#\\\/schema\\\/person\\\/ede0b56d0c808f123f57d5d796902105\",\"name\":\"John Nessime\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/cropped-jn.png\",\"url\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/cropped-jn.png\",\"contentUrl\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/cropped-jn.png\",\"width\":512,\"height\":512,\"caption\":\"John Nessime\"},\"logo\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/cropped-jn.png\"},\"description\":\"AWS Certified Solutions Architect helping businesses build reliable cloud, data, reporting, and automation solutions. I help startups, agencies, and growing businesses replace manual processes and disconnected data with practical AWS architectures, clean data pipelines, useful dashboards, and maintainable automation.\",\"sameAs\":[\"https:\\\/\\\/john-nessime.com\\\/blog\",\"https:\\\/\\\/www.linkedin.com\\\/in\\\/john-m-nessime\"],\"url\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/author\\\/johnnessime\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"TLS Certificate Errors: Chain, SAN & Expiry Fixes","description":"TLS certificate errors almost always come down to a broken chain, a missing SAN, or expiry. Diagnose each in one command and automate renewals for good.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/john-nessime.com\/blog\/linux\/tls-certificate-errors\/","og_locale":"en_US","og_type":"article","og_title":"TLS Certificate Errors: Chain, SAN & Expiry Fixes","og_description":"TLS certificate errors almost always come down to a broken chain, a missing SAN, or expiry. Diagnose each in one command and automate renewals for good.","og_url":"https:\/\/john-nessime.com\/blog\/linux\/tls-certificate-errors\/","og_site_name":"John Nessime","article_published_time":"2026-07-30T15:02:33+00:00","article_modified_time":"2026-07-30T15:11:35+00:00","og_image":[{"width":1200,"height":800,"url":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/tls-certificate-errors-featured-1.png","type":"image\/png"}],"author":"John Nessime","twitter_card":"summary_large_image","twitter_misc":{"Written by":"John Nessime","Est. reading time":"12 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/john-nessime.com\/blog\/linux\/tls-certificate-errors\/#article","isPartOf":{"@id":"https:\/\/john-nessime.com\/blog\/linux\/tls-certificate-errors\/"},"author":{"name":"John Nessime","@id":"https:\/\/john-nessime.com\/blog\/#\/schema\/person\/ede0b56d0c808f123f57d5d796902105"},"headline":"TLS Certificate Errors: Chain Issues, SANs, and Expiry Automation","datePublished":"2026-07-30T15:02:33+00:00","dateModified":"2026-07-30T15:11:35+00:00","mainEntityOfPage":{"@id":"https:\/\/john-nessime.com\/blog\/linux\/tls-certificate-errors\/"},"wordCount":2655,"commentCount":0,"publisher":{"@id":"https:\/\/john-nessime.com\/blog\/#\/schema\/person\/ede0b56d0c808f123f57d5d796902105"},"image":{"@id":"https:\/\/john-nessime.com\/blog\/linux\/tls-certificate-errors\/#primaryimage"},"thumbnailUrl":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/tls-certificate-errors-featured-1.png","keywords":["Apache","Certbot","Cloudflare","DevOps","Let's Encrypt","Linux","Monitoring","Nginx","OpenSSL","SSL Certificate","Sysadmin","TLS","Troubleshooting"],"articleSection":["Linux","System Administration","Troubleshooting","Web Security"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/john-nessime.com\/blog\/linux\/tls-certificate-errors\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/john-nessime.com\/blog\/linux\/tls-certificate-errors\/","url":"https:\/\/john-nessime.com\/blog\/linux\/tls-certificate-errors\/","name":"TLS Certificate Errors: Chain, SAN & Expiry Fixes","isPartOf":{"@id":"https:\/\/john-nessime.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/john-nessime.com\/blog\/linux\/tls-certificate-errors\/#primaryimage"},"image":{"@id":"https:\/\/john-nessime.com\/blog\/linux\/tls-certificate-errors\/#primaryimage"},"thumbnailUrl":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/tls-certificate-errors-featured-1.png","datePublished":"2026-07-30T15:02:33+00:00","dateModified":"2026-07-30T15:11:35+00:00","description":"TLS certificate errors almost always come down to a broken chain, a missing SAN, or expiry. Diagnose each in one command and automate renewals for good.","breadcrumb":{"@id":"https:\/\/john-nessime.com\/blog\/linux\/tls-certificate-errors\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/john-nessime.com\/blog\/linux\/tls-certificate-errors\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/john-nessime.com\/blog\/linux\/tls-certificate-errors\/#primaryimage","url":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/tls-certificate-errors-featured-1.png","contentUrl":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/tls-certificate-errors-featured-1.png","width":1200,"height":800,"caption":"Diagram of a TLS certificate chain showing root, intermediate and leaf certificates alongside openssl s_client output with verify return code 0"},{"@type":"BreadcrumbList","@id":"https:\/\/john-nessime.com\/blog\/linux\/tls-certificate-errors\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/john-nessime.com\/blog\/"},{"@type":"ListItem","position":2,"name":"TLS Certificate Errors: Chain Issues, SANs, and Expiry Automation"}]},{"@type":"WebSite","@id":"https:\/\/john-nessime.com\/blog\/#website","url":"https:\/\/john-nessime.com\/blog\/","name":"John Nessime","description":"Cloud, DevOps, Data &amp; AI \u2014 Built, Tested, Explained","publisher":{"@id":"https:\/\/john-nessime.com\/blog\/#\/schema\/person\/ede0b56d0c808f123f57d5d796902105"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/john-nessime.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/john-nessime.com\/blog\/#\/schema\/person\/ede0b56d0c808f123f57d5d796902105","name":"John Nessime","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/cropped-jn.png","url":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/cropped-jn.png","contentUrl":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/cropped-jn.png","width":512,"height":512,"caption":"John Nessime"},"logo":{"@id":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/cropped-jn.png"},"description":"AWS Certified Solutions Architect helping businesses build reliable cloud, data, reporting, and automation solutions. I help startups, agencies, and growing businesses replace manual processes and disconnected data with practical AWS architectures, clean data pipelines, useful dashboards, and maintainable automation.","sameAs":["https:\/\/john-nessime.com\/blog","https:\/\/www.linkedin.com\/in\/john-m-nessime"],"url":"https:\/\/john-nessime.com\/blog\/author\/johnnessime\/"}]}},"_links":{"self":[{"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/posts\/31","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/comments?post=31"}],"version-history":[{"count":1,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/posts\/31\/revisions"}],"predecessor-version":[{"id":33,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/posts\/31\/revisions\/33"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/media\/35"}],"wp:attachment":[{"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/media?parent=31"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/categories?post=31"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/tags?post=31"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}