Where to start First steps track · 15 stages
Linux Labs

TLS and certificates — openssl, the chain, verification

11 min Ubuntu 24.04 · OpenSSL 3.0 · curl 8.5

A TLS certificate binds a server key to a site name; openssl reads its fields, checks the chain and issues a new one.

Cheat sheet

9 commands Printable version
  • openssl x509 -in site.pem -noout -subject -issuer -dates
  • openssl x509 -in site.pem -noout -ext subjectAltName names
  • openssl x509 -in site.pem -noout -checkend 2592000 30 days?
  • openssl s_client -connect site.lab.test:443 -servername site.lab.test \
  • -showcerts </dev/null 2>/dev/null | grep -E '^ *[0-9]+ s:|^ +i:'
  • openssl verify -CAfile ca.pem -untrusted int.pem site.pem
  • curl --cacert ca.pem https://site.lab.test/ your own CA
  • sudo cp ca.pem /usr/local/share/ca-certificates/lab.crt
  • sudo update-ca-certificates trust the CA

Overview

TLS encrypts a connection and, before the first byte of http, lets the client check that the server is who it claims to be. For that the server presents a certificate: a public key, site names and a validity period, vouched for by the signature of a certificate authority. The matching private key stays on the server only.

text
$ openssl x509 -in site.pem -noout -subject -issuer -dates \
    -ext subjectAltName
subject=CN = secure.lab.test
issuer=CN = Lab Intermediate CA
notBefore=Oct 20 10:04:50 2026 GMT
notAfter=Nov 19 10:04:50 2026 GMT
X509v3 Subject Alternative Name:
    DNS:secure.lab.test

subject is who it was issued to, issuer who signed it, notBefore/notAfter the validity period, SAN the names the certificate is valid for.

The client's three checks

Check What it compares curl error
trust the signature chain reaches a CA in the store unable to get local issuer certificate
name the name from the URL is in SAN no alternative certificate subject name matches
date now is between notBefore and notAfter certificate has expired

If any fails, curl exits with code 60 and a browser shows an error page. curl -k turns all three off: the connection is encrypted, but nobody knows with whom.

The chain and trust

A site certificate is usually signed not by a root but by an intermediate CA, which the root signed. The root is signed by itself (subject = issuer) and is trusted because it sits in the client's trust store. On Ubuntu that is /etc/ssl/certs; update-ca-certificates adds your own root, and curl --cacert ca.pem does it for one request.

The client knows only roots. The server sends the intermediate along with the site certificate — that is the chain. If the server sends only the site certificate, the client cannot reach the root and reports unable to get local issuer certificate, even though it has the root.

Key, request, certificate

PEM files are text between -----BEGIN …----- and -----END …----- lines, base64 inside. The first line tells what the file holds:

First line What it is
-----BEGIN PRIVATE KEY----- a private key
-----BEGIN CERTIFICATE REQUEST----- a CSR
-----BEGIN CERTIFICATE----- a certificate

A certificate is issued from a signing request (CSR): it holds the public key and the requested names. The CA signs the request and returns a certificate. A new request on the old key gives a new certificate without changing the key.

Recipes

8

What the server sends

bash
openssl s_client -connect secure.lab.test:443 \
  -servername secure.lab.test -showcerts </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -enddate

The first certificate in the output is the site certificate. For the whole chain, look at the s: (subject) and i: (issuer) lines:

text
 0 s:CN = secure.lab.test
   i:CN = Lab Intermediate CA
 1 s:CN = Lab Intermediate CA
   i:CN = Lab Root CA

Only line 0 — the server does not send the intermediate. -servername passes the site name at the start of TLS (SNI): nginx with several sites on 443 picks the certificate by it. Without a name, for example when connecting to an IP, you get the default server's certificate.

Check a chain locally

bash
openssl verify -CAfile ca.pem site.pem
openssl verify -CAfile ca.pem -untrusted int.pem site.pem

The first command, without the intermediate, says error 20 at 0 depth lookup: unable to get local issuer certificate. The second says site.pem: OK: the chain goes from the site through the intermediate to the root.

Build a full chain file

bash
cat site.pem int.pem > fullchain.pem
grep -c BEGIN fullchain.pem      # 2

The order is the site certificate first, the intermediate after it. The root does not go in the file: the client has its own. Using the file in nginx — nginx.

Do the key and certificate match?

bash
openssl x509 -in site.pem -noout -pubkey | sha256sum
openssl pkey -in site.key -pubout | sha256sum

Equal sums mean a pair. If they differ, nginx rejects the config with key values mismatch.

Issue a certificate with the old key

bash
openssl req -new -key site.key -out site.csr -subj '/CN=shop.lab.test' \
  -addext 'subjectAltName=DNS:shop.lab.test,DNS:www.shop.lab.test'
openssl x509 -req -in site.csr -CA ca.pem -CAkey ca.key \
  -CAcreateserial -days 90 -copy_extensions copy -out site.pem
openssl x509 -in site.pem -noout -ext subjectAltName -enddate

-copy_extensions copy carries SAN from the request into the certificate. Without it the certificate has no names: -ext subjectAltName prints No extensions in certificate, and curl rejects it by name. Put the new certificate in the file the server uses, then nginx -t and reload.

How long until it expires

bash
openssl x509 -in site.pem -noout -enddate
openssl x509 -in site.pem -noout -checkend 2592000; echo $?

-checkend N asks whether the certificate expires within N seconds (2592000 is 30 days): Certificate will expire and code 1, or Certificate will not expire and code 0. Handy in checks and monitoring.

Trust your own CA system-wide

bash
sudo cp ca.pem /usr/local/share/ca-certificates/lab-root.crt
sudo update-ca-certificates

The file must be named *.crt. The command prints 1 added, 0 removed; done., and after that curl and other OpenSSL programs trust this CA's certificates without --cacert.

Let's Encrypt (certbot)

bash
sudo certbot --nginx -d example.com -d www.example.com
sudo certbot renew --dry-run
systemctl list-timers certbot.timer

A public CA for sites on the internet: certificates last 90 days, and a timer renews them in advance. It needs a DNS name pointing at this server and port 80 reachable from outside. The lab VMs have no outside network, so they use a lab CA.

Troubleshooting

6

certificate has expired

curl: (60) SSL certificate problem: certificate has expired; openssl s_client says Verify return code: 10. Check notAfter of the certificate the server sends (recipe above) and the machine's clock — timedatectl. Issue a new one, replace the file and reload: nginx keeps the old certificate in memory.

unable to get local issuer certificate

The client did not reach a trusted root. Two causes:

  • the client does not know the CA — --cacert or update-ca-certificates;
  • the server does not send the intermediate — s_client shows a single 0 s: line and Verify return code: 21 (unable to verify the first certificate). You need a full chain file.

no alternative certificate subject name matches

curl: (60) SSL: no alternative certificate subject name matches target host name 'other.lab.test' — the name from the URL is not in SAN. curl answers the same when you connect by IP. Look at the names: openssl x509 -noout -ext subjectAltName; if SAN is empty, the certificate was signed without -copy_extensions.

self-signed certificate

curl: (60) SSL certificate problem: self-signed certificate; s_client says code 18. The server sends a certificate it signed itself, and the trust store does not have it. For your own service, issue a certificate from your own CA and put the root in the trust store.

wrong version number

curl: (35) … SSL routines::wrong version number — https:// went to a port that speaks plain HTTP. Check the port and the server's listen … ssl.

The server sends the old certificate

The file is replaced but s_client shows the old notAfter: nginx has not re-read its config. sudo nginx -t && sudo systemctl reload nginx, then check a second later — nginx.

Danger zone

2

curl -k in scripts and checks

-k (--insecure) silently accepts a spoofed server, an expired certificate and a wrong name. A "site answers over HTTPS" check with -k passes even when users see an error. Use --cacert with the right CA instead.

The private key

The key lets anyone pose as the site. Mode 600, owner root, and it never goes into a repository, an email or a chat. If it leaks, issue a certificate on a new key.

Terms

11
TLS
The protocol for encrypted connections over TCP: the client checks the server certificate, then both sides encrypt the exchange. HTTPS is HTTP inside TLS. Also appears in: curl, HTTP, wget, nginx and 4 more
certificate (X.509)
A file in which a certificate authority binds a public key to site names for the period from notBefore to notAfter with its signature. The server presents it to the client at the start of TLS. Also appears in: curl, nginx, wget, GPG and 3 more
certificate authority (CA)
The party that signs certificates; its name is in the issuer field. A client trusts a certificate if the chain of signatures reaches a root CA in its trust store. Also appears in: wget, curl
certificate chain
The site certificate and the intermediate CA certificates up to the root, each signed by the next. The server sends the site and intermediate certificates; the client has its own root. Also appears in: wget, HTTP, nginx
private key
The secret half of a key pair; it stays on the server and proves the certificate belongs to it. Certificate and key must match, or nginx rejects the configuration (key values mismatch). Also appears in: GPG, SSH keys and ssh-agent, WireGuard, chmod / chown
certificate signing request (CSR)
A file with the public key and the requested names, signed by the private key. A CA issues a certificate from it; a new CSR on the old key gives a new certificate without changing the key.
SAN (subject alternative name)
The certificate extension listing the names it is valid for; the client compares the name from the URL against it. A name missing from SAN gives «no alternative certificate subject name matches».
trust store
The root CA certificates a client trusts. On Ubuntu it is /etc/ssl/certs, and update-ca-certificates adds your own CA; for a single request curl --cacert names the CA.
self-signed certificate
A certificate signed by its own key: subject and issuer are the same. Every root CA is like this — it is trusted because it is in the trust store; curl rejects one it does not know (self-signed certificate). Also appears in: curl
intermediate CA
A certificate authority between the root and the site — the root CA signed it, and it signs site certificates. Clients do not know it, so the server sends it along with the site certificate.
SNI
The site name the client sends at the very start of TLS, before any HTTP. A server with several sites on one address uses it to choose which certificate to present. Also appears in: curl

See also

  • nginx — ssl_certificate, the chain, the redirect to HTTPS.
  • http — what travels inside TLS, status codes.
  • curl — --cacert, -v with the handshake.
  • timedatectl — the machine's clock if "expired" comes too early.
  • sshd — another service built on keys and fingerprints.
Loading…