LinuxIntermediate 13 min Lesson 19 of 24

SSL/TLS Certificates

Obtain and renew certificates with Let’s Encrypt, understand the chain, and diagnose the errors browsers show.

Linux · Lesson 19 of 24
0/24 done(0%)

What is it? #

A TLS certificate proves that a public key belongs to a domain. Browsers trust it because a certificate authority they already trust has signed it.

Let's Encrypt made certificates free and automatable, which removed the last excuse for serving anything over plain HTTP.

Certificates expire — 90 days for Let's Encrypt — so renewal must be automated. Expiry is one of the most common and most avoidable outages.

Most certificate errors come from three things: an expired certificate, a missing intermediate in the chain, or a name that does not match the requested domain.

Think of it like this #

A passport issued by an authority that border control already trusts.

It states who you are, it has an expiry date, and it is only valid for the name printed on it. An expired one, or one in a different name, is refused regardless of how genuine it looks.

Simple example #

You need HTTPS on a new domain, including the www subdomain, with automatic renewal and a check that nothing is misconfigured.

Code #

BASH
# Obtain and install a certificate with automatic Nginx configuration
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

# Certificate-only, for manual configuration
sudo certbot certonly --nginx -d example.com

# Files land here
# /etc/letsencrypt/live/example.com/fullchain.pem   certificate + intermediates
# /etc/letsencrypt/live/example.com/privkey.pem     private key (keep 600)
BASH
# Renewal — certbot installs a timer, but verify it
sudo certbot renew --dry-run          # test without issuing
systemctl list-timers | grep certbot  # confirm the timer exists and is active

# Manual renewal hook, if you configure it yourself
# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/bin/bash
systemctl reload nginx
BASH
# Inspecting a certificate
openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem \
  -noout -subject -issuer -dates

# What is actually being served?
openssl s_client -connect example.com:443 -servername example.com < /dev/null \
  | openssl x509 -noout -subject -dates

# Verify the chain is complete
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | grep -E "Verify return code|depth"
# "Verify return code: 0 (ok)" is what you want
TEXT
The three common errors

NET::ERR_CERT_DATE_INVALID
    expired. Renewal failed or was never automated.

NET::ERR_CERT_COMMON_NAME_INVALID
    the certificate does not cover the requested name. A certificate for
    example.com does not cover www.example.com unless both were requested.

incomplete chain
    the server sent only the leaf certificate. Browsers may still work
    (they cache intermediates) while other clients fail. Always serve
    fullchain.pem, not cert.pem.
BASH
# Monitor expiry so it never surprises you
DAYS=<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false">(</mo><mo stretchy="false">(</mo><mo stretchy="false">(</mo></mrow><annotation encoding="application/x-tex">(( (</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="katex-base"><span class="katex-strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(((</span></span></span></span>(date -d "$(openssl x509 -enddate -noout \
  -in /etc/letsencrypt/live/example.com/fullchain.pem | cut -d= -f2)" +%s) \
  - $(date +%s) ) / 86400 ))
echo "Certificate expires in $DAYS days"
[ "$DAYS" -lt 14 ] && echo "WARNING: renewal has not run"

How it works #

certbot --nginx proves you control the domain by serving a challenge file over HTTP, then obtains the certificate and edits the Nginx configuration to use it. The domain must already point at the server for that validation to succeed.

Requesting both example.com and www.example.com produces one certificate covering both names. Omitting one is the cause of the name-mismatch error on whichever was left out.

fullchain.pem contains the leaf certificate plus the intermediates. Serving cert.pem alone produces the incomplete-chain problem, which is particularly deceptive because browsers often succeed while API clients and mobile apps fail.

certbot renew only acts when the certificate is close to expiry, so running it twice daily is safe and is what the installed timer does. --dry-run exercises the whole process without issuing, which is how you verify renewal works before it matters.

The deploy hook reloads Nginx after renewal. Without it, the new certificate is on disk but the running server keeps serving the old one until it happens to restart.

openssl s_client shows what is actually being served, which can differ from what is on disk if a reload was missed. Verify return code: 0 (ok) confirms the chain validates.

The expiry check script is the safety net. Even with automation, verifying that renewal actually happened is worth an alert.

Real-world use #

Certificate expiry has taken down major services repeatedly. It is entirely preventable, and the prevention is an alert on days-to-expiry rather than trust in the automation.

Wildcard certificates cover *.example.com and require DNS-based validation, which means an API token for your DNS provider. They are convenient when subdomains are created frequently.

Behind a load balancer or CDN, TLS often terminates at the edge and the origin may use its own certificate or plain HTTP on a private network. Knowing which applies determines where renewal must happen.

Rate limits apply to certificate issuance, so repeatedly re-running certbot while debugging can lock you out for a week. The staging environment exists for exactly that.

The private key must stay at 600 and never be committed. A leaked key means the certificate must be revoked and reissued.

Common mistakes #

  • Not automating renewal, or never verifying that the automation works.
  • Serving cert.pem instead of fullchain.pem, breaking non-browser clients.
  • Requesting a certificate for the apex domain but not www.
  • Forgetting to reload the web server after renewal.
  • Hitting issuance rate limits by re-running certbot repeatedly during debugging.

Practice #

On a server with a real domain, obtain a certificate covering both the apex and www, confirm the renewal timer is active, and run a dry-run renewal. Then inspect the served certificate with openssl and confirm the chain verifies and the expiry date is what you expect.

Quick quiz

  1. 1. How long are Let’s Encrypt certificates valid?

  2. 2. What should Nginx be configured to serve?

  3. 3. Why might a site work in a browser but fail from a mobile app?

  4. 4. What does a deploy hook do after renewal?

  5. 5. Why use the staging environment while debugging certbot?

Summary

  • Certificates prove a key belongs to a domain and expire after 90 days.
  • Automate renewal and verify it with a dry run and an expiry alert.
  • Serve fullchain.pem so non-browser clients validate correctly.
  • Include every name you serve, including www.
  • Reload the server after renewal, and keep the private key at 600.