TT Lab
Get started
Learn Learning paths Courses

The certificate was renewed, but the browser still showed the old one

A certificate error is one of four things

Continue in TT Lab

In one line

A certificate is a document in which someone signs "this public key belongs to this name." Verification means checking four things separately: the signature chain, the validity period, the name (SAN), and the key match. The failure message points exactly to which of them broke.

Why you need this

A report of a "certificate error" has one of four causes, yet the wording on screen usually lumps them into one. The certificate expired, or the name is different, or an intermediate CA is missing and the chain is broken, or the deployment paired the certificate with a different private key. Each has a completely different fix, so if you start with "just reissue it," you do the work twice. Once you know the structure, a single openssl x509 -text lets you read which field is the problem.

How it works

The format is X.509 v3 from RFC 5280. The body (TBSCertificate) contains the serialNumber (a number unique within the issuer), the issuer (the name of the CA that signed), the validity (notBefore and notAfter), the subject, the subjectPublicKeyInfo (the public key), and extensions; a value signed over all of that with the issuer's private key is attached. When you renew, the serial and validity change even if you reuse the same key, so the serial acts as the version number of the certificate. That is why you look at the serial or the SHA-256 fingerprint when you check which certificate is being served.

Two of the extensions are decisive in operations. subjectAltName (SAN) is the list of names the certificate represents. RFC 6125 says to check names against the dNSName entries in the SAN rather than the subject's CN, and modern clients really do look only at the SAN — putting the name in the CN is useless. Only a certificate whose basicConstraints cA is TRUE can sign other certificates, and pathLenConstraint limits how many levels of CAs may sit below it.

The chain runs leaf (server certificate) → intermediate CA → root. The client has only the root in its trust store, and the server must send the leaf together with the intermediate CA. If you leave out the intermediate CA, the client cannot find the leaf's issuer. OpenSSL's verify takes the trust anchor with -CAfile and the intermediate certificates that are "not trusted, but used to link the chain" with -untrusted.

$ openssl verify -CAfile ca.crt api.crt
error 20 at 0 depth lookup: unable to get local issuer certificate     ← 중간 CA 가 없다
$ openssl verify -CAfile ca.crt -untrusted inter.crt api.crt
api.crt: OK

One trap when you create certificates. Even if you put the SAN in the CSR with -addext "subjectAltName=DNS:...", when you sign with openssl x509 -req the default behavior is to discard the CSR's extensions. In 3.0 you have to pass -copy_extensions copy for the SAN to be carried into the certificate. If you forget it, issuance succeeds without a problem but the browser reports a name mismatch — and because the CN is there, it looks fine to a human eye.

The key match can be checked by comparing the public key in the certificate with the public key derived from the private key. Both come out in the same PEM format with -pubkey and -pubout, so comparing hashes is enough. Expiry is read with -enddate, and the judgment of remaining time is done by -checkend <초> (replace the placeholder with a number of seconds): if the certificate expires within that many seconds, it exits with a nonzero code, which makes it easy to use in scripts.

$ openssl x509 -in server.crt -noout -checkend 604800 ; echo $?      # 7일 안에 만료되는가
Certificate will expire
1

What it looks like in the field

"It worked until yesterday but fails today" is expiry. A certificate dies by the clock, so it blows up regardless of any deployment. Remember that this is judged by the client's clock, not the server's — incidents where only one machine with a wrong clock fails really do happen. "It works with curl but only the Java app fails" is the chain. curl may have the intermediate CA cached in the system store, or follow the AIA to link the chain, but Java and Go clients build the chain only from what the server sent. Configure the server with the fullchain and you are done. "Since we changed the reverse proxy" makes you suspect a key mismatch. It happens when the certificate and key were fetched from different paths and no longer pair up; nginx refuses it at startup, but some servers only fail at the handshake.

What you will do in the next lab

You create a root CA, make a CSR with two SANs, and issue a three-day server certificate. You see for yourself why the grader flags "no SAN" if you omit -copy_extensions. You read the serial, fingerprint, and expiry date, judge the key match by hash, and use -checkend to decide whether renewal is needed for a 7-day window. Finally, you set up an intermediate CA and issue a leaf from it, build a chain that fails with the root alone but passes with -untrusted, assemble fullchain.pem, and record the name checks in a table with -verify_hostname.