Module 09 — Revocation: CRL, OCSP & Why It's Broken

Updated 24 August 2026

Module 09 · Revocation: CRL, OCSP & Why It's Broken

Every module so far has had a note saying "revocation is covered in Module 09". This is it — and the honest headline is that revocation on the public internet does not work, and the industry has stopped pretending otherwise. Understanding why explains the single biggest change happening in TLS right now: certificate lifetimes collapsing from years to weeks.

🧠 concept → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (answers hidden)

Prerequisite: Modules 01–08. You need the CRL and AIA extensions from Module 03 (C5), openssl ca and index.txt from Module 05 (B2), and OCSP stapling from Module 08 (D2).


The picture to hold in your head for this whole module — a cancelled credit card.

Your card is stolen. You ring the bank and cancel it. The card itself is unchanged — same numbers, same hologram, same expiry date printed on the front. Nothing about the card tells a shopkeeper it is dead.

So how does the shop find out? Two ways, historically:

  1. A printed book of cancelled numbers under the counter, delivered weekly. That is a CRL.
  2. Phoning the bank to ask about this one card. That is OCSP.

Both work in principle. Both have a problem that turns out to be fatal in practice — and this module is about what that problem is and what replaced them.

Build the revocation lab. Everything here runs offline — no network, no CA, nothing outside ~/tls-lab/.
bash
mkdir -p ~/tls-lab/m09 && cd ~/tls-lab/m09
umask 077
mkdir -p certs db && touch db/index.txt
openssl rand -hex 8 > db/serial
echo 1000 > db/crlnumber          # NEW: CRLs need their own counter

cat > ca.cnf <<'EOF'
[ca]
default_ca=CA
[CA]
dir=.
database=$dir/db/index.txt
serial=$dir/db/serial
crlnumber=$dir/db/crlnumber
new_certs_dir=$dir/certs
certificate=$dir/ca.crt
private_key=$dir/ca.key
default_md=sha256
default_days=90
default_crl_days=7
policy=pol
rand_serial=yes
unique_subject=no
copy_extensions=none
email_in_dn=no
[pol]
commonName=supplied
[srv]
basicConstraints=critical,CA:FALSE
keyUsage=critical,digitalSignature
extendedKeyUsage=serverAuth
subjectAltName=DNS:a.test,DNS:b.test,DNS:c.test
crlDistributionPoints=URI:http://crl.example.test/ca.crl
EOF

openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out ca.key
openssl req -x509 -key ca.key -out ca.crt -days 3650 -subj "/CN=Revocation Lab CA" \
  -addext "basicConstraints=critical,CA:TRUE" -addext "keyUsage=critical,keyCertSign,cRLSign"

openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out srv.key
for n in a b c; do
  openssl req -new -key srv.key -out $n.csr -subj "/CN=$n.test"
  openssl ca -config ca.cnf -extensions srv -batch -notext -in $n.csr -out $n.crt
done

cat db/index.txt

Two new things in this config, both required before openssl ca will generate a CRL: a crlnumber file (a counter, like serial, but for CRLs) and default_crl_days (how long each CRL is valid).

All output in this module was produced on OpenSSL 3.0.13.

Part A · What revocation actually is

A1 · Revoking a certificate — two fields in a text file

The analogy — crossing a name off the ledger.

In Module 05 the CA's index.txt was described as the office ledger: every document issued, with its number and its status.

Revocation is nothing more than changing one letter in that ledgerV for valid becomes R for revoked — and writing today's date beside it.

That is genuinely all it is. Everything else in this module is about the much harder problem of telling the world what the ledger now says.

🧪 Exercise A1.1 — Revoke a certificate and watch the ledger change
bash
cd ~/tls-lab/m09

echo "=== the ledger before ==="
cat db/index.txt

echo
echo "=== revoke b.test, and say why ==="
openssl ca -config ca.cnf -revoke b.crt -crl_reason keyCompromise

echo
echo "=== the ledger after ==="
cat db/index.txt
Expected result — click to reveal
plain text
=== the ledger before ===
V	261122052744Z		0A15C224EBD2EAB834BB6E87BE21A71E1DF7E26A	unknown	/CN=a.test
V	261122052744Z		63777A2531F69CF6C85A6C5B5DC374A653968A40	unknown	/CN=b.test
V	261122052744Z		5A89C90E462B572355643F3AD716B69378A4C51E	unknown	/CN=c.test

=== revoke b.test ===
Revoking Certificate 63777A2531F69CF6C85A6C5B5DC374A653968A40.
Database updated

=== the ledger after ===
V	261122052744Z		0A15C224EBD2EAB834BB6E87BE21A71E1DF7E26A	unknown	/CN=a.test
R	261122052744Z	260824052817Z,keyCompromise	63777A2531F69CF6C85A6C5B5DC374A653968A40	unknown	/CN=b.test
V	261122052744Z		5A89C90E462B572355643F3AD716B69378A4C51E	unknown	/CN=c.test

What to read out of this — compare the middle line before and after.

  • Field 1 changed from V to R. That is the revocation.
  • Field 3 went from empty to 260824052817Z,keyCompromise — the revocation timestamp and the reason. Module 05 (B2.2) predicted exactly this, and here it is.
  • Nothing else changed. The certificate file b.crt is byte-for-byte identical. You cannot revoke a certificate by editing it — the signature would break. Revocation is a statement the CA makes about the certificate, held entirely on the CA's side.
  • This is why revocation is hard. The certificate in the attacker's hands is unchanged and still perfectly valid-looking. The only way anyone learns otherwise is by asking the CA.

🔑 The reason codes matter more than people think. -crl_reason accepts: unspecified, keyCompromise, CACompromise, affiliationChanged, superseded, cessationOfOperation, certificateHold, removeFromCRL.

keyCompromise is the serious one. It means the private key leaked. Some clients and audit processes treat it differently from superseded — which just means "we replaced this one" and is what a routine reissue should use. Marking a routine replacement as keyCompromise triggers alarm that is not warranted; marking a real leak as superseded hides one that is.

🎯 Interview questions — What revocation is

Q. How do you revoke a certificate, and what actually changes?

You tell the CA, and the CA records it. With OpenSSL that is openssl ca -revoke cert.crt -crl_reason <reason>, which changes that certificate's status in index.txt from V to R and writes the revocation date and reason.

The certificate itself does not change and cannot change — editing it would break the signature. Revocation is a statement the CA makes about the certificate, held on the CA's side. The copy in an attacker's hands is byte-identical and still looks perfectly valid.

That asymmetry is the whole difficulty: the only way a client learns about revocation is by asking the CA, either by downloading a CRL or by querying OCSP.

The detail worth adding: choose the reason code deliberately. keyCompromise means the private key leaked and is treated seriously by audit processes; superseded means a routine replacement. Using the wrong one either raises a false alarm or hides a real incident.


A2 · CRL — the printed book under the counter

The analogy — the book of stolen card numbers.

Before card machines were online, shops kept a printed booklet of cancelled card numbers under the counter. The bank posted a new one every week. For a large purchase the assistant would look the number up.

It works, and you can see the problems immediately:

  • It is out of date the moment it is printed. A card cancelled on Tuesday is not in Monday's book.
  • It gets thick. Every cancellation ever, until the cards expire. Some banks' booklets became unusable.
  • Nobody actually checked it when the shop was busy.

A CRL is that booklet: a signed list of revoked serial numbers, published at a URL, with a "next update" date.

🧪 Exercise A2.1 — Generate a real CRL and read it
bash
cd ~/tls-lab/m09

openssl ca -config ca.cnf -gencrl -out ca.crl

openssl crl -in ca.crl -noout -text | head -20

echo "=== how big is it? ==="
ls -l ca.crl
Expected result — click to reveal
plain text
Certificate Revocation List (CRL):
        Version 2 (0x1)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN = Revocation Lab CA
        Last Update: Aug 24 05:28:17 2026 GMT
        Next Update: Aug 31 05:28:17 2026 GMT
        CRL extensions:
            X509v3 CRL Number:
                4096
Revoked Certificates:
    Serial Number: 63777A2531F69CF6C85A6C5B5DC374A653968A40
        Revocation Date: Aug 24 05:28:17 2026 GMT
        CRL entry extensions:
            X509v3 CRL Reason Code:
                Key Compromise

-rw------- 1 zaeem zaeem 638 Aug 24 05:28 ca.crl

What to read out of this.

  • Last Update and Next Update — seven days apart, from default_crl_days=7. This is the freshness problem, printed in the file. A client that fetched this CRL on Monday will not learn about a Tuesday revocation until it refetches. Public CAs typically publish every few hours to a few days, so there is always a window in which a revoked certificate still works.
  • It lists only serial numbers, not names. Revocation is identified by (issuer, serial), exactly as Module 03 (A2) said. That also means a CRL leaks very little — you cannot tell from it which sites were revoked without cross-referencing.
  • CRL Number: 4096 — the counter from db/crlnumber, which started at 1000 in hex. It stops an attacker replaying an older CRL that does not yet contain a revocation.
  • 638 bytes for one revoked certificate. Small. Now multiply: a large public CA has millions of unexpired certificates, and every revocation stays on the list until the certificate would have expired anyway. Real CRLs have reached tens of megabytes.
  • The CRL is signed by the CA. It has to be, or an attacker could publish an empty one. That is also why the CA needs cRLSign in its Key Usage (Module 03, C3).
🧪 Exercise A2.2 — The exercise that matters most in this module

This is where you find out what "revocation is broken" actually means.

bash
cd ~/tls-lab/m09

echo "=== 1. verify the REVOKED certificate, the normal way ==="
openssl verify -CAfile ca.crt b.crt

echo
echo "=== 2. the same certificate, this time ASKING about revocation ==="
cat ca.crt ca.crl > ca-with-crl.pem
openssl verify -CAfile ca-with-crl.pem -crl_check b.crt

echo
echo "=== 3. a certificate that was NOT revoked ==="
openssl verify -CAfile ca-with-crl.pem -crl_check a.crt

echo
echo "=== 4. asking about revocation with NO CRL available ==="
openssl verify -CAfile ca.crt -crl_check a.crt
Expected result — click to reveal
plain text
=== 1. verify the REVOKED certificate, the normal way ===
b.crt: OK

=== 2. the same certificate, this time ASKING about revocation ===
CN = b.test
error 23 at 0 depth lookup: certificate revoked
error b.crt: verification failed

=== 3. a certificate that was NOT revoked ===
a.crt: OK

=== 4. asking about revocation with NO CRL available ===
CN = a.test
error 3 at 0 depth lookup: unable to get certificate CRL
error a.crt: verification failed

What to read out of this — blocks 1 and 4 are the two halves of the problem.

Block 1: b.crt: OK. A revoked certificate passed verification, cleanly, with no warning. Because revocation checking is not on by default — you have to ask for it with -crl_check. Every chain check you have run since Module 05 has silently skipped this gate.

Block 4: error 3: unable to get certificate CRL. A perfectly good certificate failed, purely because the CRL was not available. That is hard-fail behaviour: no revocation information means refuse.

Put those two together and you have the entire dilemma:

  • Do not check → revoked certificates work forever. (Block 1)
  • Check and hard-fail → the CA's availability becomes your site's availability. Every CRL server outage becomes a global internet outage. (Block 4)
  • Check and soft-fail → an attacker simply blocks the CRL fetch, and you are back to block 1.

🔑 Browsers chose the third option, and Part B1 explains why that choice made revocation ornamental rather than protective.

Two new error codes for your table (Modules 05 and 07): 23 certificate revoked — revocation confirmed. 3 unable to get certificate CRL — could not check, and this configuration refuses rather than guesses.

🎯 Interview questions — CRLs

Q. What is a CRL and how is it used?

A Certificate Revocation List — a signed list of the serial numbers a CA has revoked, published at the URL in the certificate's crlDistributionPoints extension. It carries Last Update and Next Update timestamps, and a CRL Number that increments so an old CRL cannot be replayed.

A client fetches it, caches it until Next Update, and checks whether the certificate's serial appears.

The three problems, and they are all structural:

  1. Freshness. A CRL is a snapshot. Between publications there is a window in which a revoked certificate still works — hours to days for a public CA.
  2. Size. Every revocation stays listed until the certificate would have expired anyway, so CRLs for large CAs have reached tens of megabytes. Downloading that on a mobile connection to load one page is not viable.
  3. Availability. If the CRL cannot be fetched, the client must choose between refusing a good certificate or ignoring revocation entirely.

What is worth knowing about 2026: CRLs have made a comeback. Let's Encrypt shut down its OCSP responders in August 2025 and moved to CRLs, and browsers now consume CRLs centrally rather than per-client — Firefox's CRLite being the clearest example. The mechanism did not change; who downloads it did.


A3 · OCSP — phoning up to ask about one card

The analogy — phoning the bank about this one card.

Instead of a thick booklet, the shop simply rings the bank: "card ending 4471 — still good?" The bank answers good, cancelled, or never heard of it.

Much better on the face of it. Always current, and no enormous booklet to distribute.

But notice what you have just built. The bank now knows every shop you visit, and when. Every purchase generates a phone call that identifies the customer's location and the merchant. And the till cannot complete a sale while it waits on hold.

That is OCSP, and those two problems — privacy and latency — are why it is being switched off.

CRLOCSP
ShapeDownload the whole listAsk about one certificate
FreshnessStale between publicationsCurrent — in principle
Size on the wireKilobytes to tens of megabytesA few hundred bytes
PrivacyGood — the CA learns nothing about which siteBad — the CA learns every site you visit
LatencyNone if cachedA blocking round trip mid-handshake
Found incrlDistributionPointsauthorityInfoAccess → OCSP
Status in 2026ResurgentBeing retired
🧪 Exercise A3.1 — Look for OCSP URLs on real certificates
bash
echo "=== a certificate from a CA that still runs OCSP ==="
openssl s_client -connect www.microsoft.com:443 -servername www.microsoft.com </dev/null 2>/dev/null \
  | openssl x509 -noout -ext authorityInfoAccess,crlDistributionPoints

echo
echo "=== a Let's Encrypt certificate ==="
openssl s_client -connect letsencrypt.org:443 -servername letsencrypt.org </dev/null 2>/dev/null \
  | openssl x509 -noout -ext authorityInfoAccess,crlDistributionPoints
Expected result — click to reveal

A CA that still runs OCSP:

plain text
Authority Information Access:
    OCSP - URI:http://oneocsp.microsoft.com/ocsp
    CA Issuers - URI:http://www.microsoft.com/pkiops/certs/...crt
X509v3 CRL Distribution Points:
    Full Name:
      URI:http://www.microsoft.com/pkiops/crl/...crl

A Let's Encrypt certificate issued after May 2025:

plain text
Authority Information Access:
    CA Issuers - URI:http://e7.i.lencr.org/

What to read out of this — look at what is missing from the second one.

  • No OCSP - URI line at all. Let's Encrypt removed OCSP URLs from newly issued certificates on 7 May 2025 and shut down its OCSP responders on 6 August 2025.
  • And no crlDistributionPoints either. That surprises people. Let's Encrypt publishes CRLs, but does not point at them from the leaf — because the consumers are browser vendors building aggregated revocation data, not individual clients. The CRLs are fetched centrally by Mozilla and Google, not by your laptop.
  • CA Issuers is still there, because that is the AIA chasing URL from Module 03 (C5) and has nothing to do with revocation.
  • The first certificate has both — many enterprise and commercial CAs still publish OCSP and CRL URLs, and will for years.

🔑 This is the single clearest signal of where revocation has gone. The world's largest CA — by a wide margin — issues certificates with no revocation URL of any kind on them. It concluded that per-client revocation checking was not worth the privacy cost and the operational expense, and that browsers had already stopped relying on it.

⚠️ If you are behind a TLS-inspecting proxy (Module 03, B1.1) you will see the proxy's re-issued certificate instead, which may have quite different extensions. Check the issuer first.

🎯 Interview questions — OCSP

Q. How does OCSP differ from a CRL?

A CRL is a bulk download of every revoked serial from a CA; OCSP is a query about one certificate. The client finds the OCSP URL in the certificate's authorityInfoAccess extension and asks the responder, which replies good, revoked or unknown — signed, so it cannot be forged.

OCSP looks strictly better: a few hundred bytes instead of megabytes, and always current. Its two problems turned out to be worse than the CRL's:

  1. Privacy. The client tells the CA which site it is visiting, from which IP, every time. The CA becomes an unintentional log of everyone's browsing. This is the reason Let's Encrypt gave for shutting its responders down.
  2. Latency. It is a blocking round trip in the middle of connection setup, to a third party you do not control.

The 2026 answer to "which should I use": neither, per-client. OCSP is being retired — Let's Encrypt removed OCSP URLs from certificates in May 2025 and shut its responders down in August 2025. Browsers moved to aggregated, locally-stored revocation data (Firefox's CRLite, Chrome's CRLSets) built from CRLs fetched centrally. OCSP stapling remains useful for privacy and latency, but as Module 08 noted, it does not make revocation enforceable.


Part B · Why it does not work

B1 · Soft-fail — the compromise that made revocation ornamental

The analogy — the thief cuts the phone line.

The shop's rule is: ring the bank; if the line is down, accept the card anyway. That rule exists for a good reason — the shop cannot stop trading every time the bank's phone system has a wobble.

Now think like the thief. You do not need to defeat the bank. You just need to cut the phone line, and the shop's own rule waves you through.

That is soft-fail, and it is what essentially every browser does. It means revocation protects you against an attacker who is not paying attention, and does nothing at all against one who is.

PolicyWhat happens when revocation cannot be checkedThe cost
No checkRevoked certificates work until they expireRevocation is meaningless
Hard-failConnection refusedThe CA's uptime becomes your uptime. One responder outage = a global internet outage
Soft-failConnection proceedsAn attacker blocks the check and is waved through. This is what browsers do
Why browsers chose soft-fail, and why it was the right call given the alternatives.

Hard-fail was tried and it was untenable. If every browser refuses to connect whenever a CA's OCSP responder is slow or unreachable, then a single CA having a bad afternoon takes a large fraction of the web offline — and CAs did have bad afternoons. The internet cannot make its availability depend on every CA's infrastructure being perfect.

So soft-fail was chosen as the lesser evil. But an attacker who has already achieved the position needed to use a revoked certificate — on the network path, or holding a stolen key — can trivially also block the revocation check. Drop the packets, and the client's own policy accepts the certificate.

That is the sentence to be able to say in an interview: "Soft-fail means revocation only stops attackers who are not trying, because anyone in a position to exploit a revoked certificate is also in a position to block the check."

🧪 Exercise B1.1 — Prove your own tools are soft-fail by default
bash
cd ~/tls-lab/m09
export no_proxy="*"

nohup openssl s_server -cert b.crt -key srv.key -accept 4433 -www > /tmp/rev.log 2>&1 &
sleep 1

echo "=== curl against a REVOKED certificate, trusting the CA ==="
curl -sS --cacert ca.crt --resolve b.test:4433:127.0.0.1 https://b.test:4433/ \
  -o /dev/null -w 'HTTP %{http_code}\n' 2>&1 | head -2

echo "=== and with curl asked to check revocation ==="
curl -sS --cacert ca.crt --crlfile ca.crl --resolve b.test:4433:127.0.0.1 \
  https://b.test:4433/ -o /dev/null -w 'HTTP %{http_code}\n' 2>&1 | head -2

pgrep -f 'accept 4433' | xargs -r kill
Expected result — click to reveal
plain text
=== curl against a REVOKED certificate, trusting the CA ===
HTTP 200

=== and with curl asked to check revocation ===
curl: (60) SSL certificate problem: certificate revoked

What to read out of this.

  • HTTP 200 against a certificate you revoked five minutes ago. curl trusted the CA, validated the chain, checked the dates, matched the hostname — and never asked whether the certificate was still valid. Revocation checking is off by default in curl, as it is in most clients.
  • With --crlfile it fails correctly. The mechanism works perfectly. Nobody uses it.
  • Notice what --crlfile requires: you had to already have the CRL, locally, as a file. curl will not go and fetch it from crlDistributionPoints for you. So making this work in production means building your own CRL distribution — which is precisely the effort nobody wants to spend.

🔑 This exercise is the module in miniature. The cryptography is fine. The protocol is fine. The default is off, and turning it on is somebody's job that nobody is doing.

💡 Your browser behaves differently again: it will not consult this CRL at all, and will instead check its own aggregated revocation data (Part C2) — which knows nothing about your private lab CA. A private CA gets no revocation support from browsers whatsoever. That is Part D's problem.

🎯 Interview questions — Soft-fail

Q. What is soft-fail, and why is it a problem?

When a client cannot obtain revocation information — the responder is unreachable, times out, or returns an error — soft-fail means it proceeds with the connection anyway. Hard-fail means it refuses.

Browsers use soft-fail, and had to. Under hard-fail, any CA whose OCSP responder is slow or down takes a large fraction of the web offline with it, and the internet's availability cannot depend on every CA's infrastructure being perfect.

But soft-fail makes revocation ineffective against a real attacker. Anyone in a position to exploit a revoked certificate — on the network path, or holding a stolen key — is also in a position to block the revocation check. Drop those packets and the client's own policy accepts the certificate.

The framing that lands: soft-fail means revocation stops attackers who are not trying. It is a genuine dilemma rather than an oversight, and the industry's response was not to fix soft-fail but to stop depending on revocation — hence aggregated browser revocation data, and certificate lifetimes falling to 47 days.


B2 · The other two problems: privacy and size

The analogy — the bank's call log.

Every time a shop rings the bank about your card, the bank writes down: which card, which shop, what time, which phone line.

Nobody designed that as surveillance. It is an unavoidable side effect of asking. But after a few million calls the bank is holding a detailed record of where everybody shops — and it never wanted that data, cannot safely keep it, and may be compelled to hand it over.

That is OCSP's privacy problem, and it is the reason Let's Encrypt gave for shutting its responders down. In their words: the CA "immediately becomes aware of which website is being visited from that visitor's particular IP address."

DateWhat happened
30 Jan 2025Let's Encrypt stopped accepting new OCSP Must-Staple requests
7 May 2025OCSP URLs removed from newly issued certificates; all Must-Staple requests denied
6 Aug 2025OCSP responders shut down entirely
And the size problem, which pushed in the opposite direction.

A CRL lists every revoked certificate until it would have expired anyway. A CA issuing hundreds of millions of certificates therefore produces CRLs measured in tens of megabytes.

That is unusable as a per-client download — nobody will fetch 40 MB to load one web page, especially on mobile.

So the two mechanisms failed for opposite reasons: OCSP was small but leaked, and CRLs were private but enormous. Neither could be fixed without becoming the other.

The way out, as Part C2 shows, was to change who does the downloading.

🎯 Interview questions — Privacy and scale

Q. Why did Let's Encrypt shut down OCSP?

Two reasons, and they said so publicly.

Privacy. With OCSP, the CA learns which website is being visited from which IP address, on every connection. Let's Encrypt's own wording is that the CA "immediately becomes aware of which website is being visited from that visitor's particular IP address." That makes the CA an unintended log of a large share of the web's browsing, which it never wanted, cannot safely retain, and could be compelled to disclose.

Cost. Running responders at their scale consumed resources they judged better spent elsewhere — and browsers had largely stopped depending on OCSP anyway.

The timeline: Must-Staple requests rejected from 30 January 2025, OCSP URLs removed from certificates on 7 May 2025, responders shut down on 6 August 2025.

What replaced it: CRLs, but consumed centrally by browser vendors rather than by individual clients. Notably Let's Encrypt certificates now carry no revocation URL at all — neither OCSP nor CRL — because the intended consumer is Mozilla and Google's aggregation pipelines, not your laptop.

The general point worth making: CRLs and OCSP failed for opposite reasons — OCSP was small but leaked, CRLs were private but enormous. Neither could be fixed without turning into the other, so the fix was to change who downloads them.


Part C · What the industry actually did

C1 · Must-Staple — the fix that nobody could afford to use

The analogy — "always show me the bank's letter."

Soft-fail exists because the shop cannot always reach the bank. So someone proposes a fix: print on the card itself"this card is only valid when presented with a current letter from the bank."

Now the thief cannot benefit from cutting the phone line, because a card with no letter is simply refused. Hard-fail, but only for the cards that opted in.

It is a genuinely clever design. And it failed, for a reason worth understanding: if your letter-fetching breaks, your card stops working. You have taken your shop's availability and tied it to the bank's letter service — voluntarily, and with no way to change your mind quickly, because the instruction is printed on a card that is valid for months.

Must-Staple is a foot-gun with a 90-day fuse.

The extension is in the certificate. Once issued, you cannot remove it — you would have to reissue. So if your OCSP stapling breaks for any reason — a missing nginx resolver (Module 08, D2), a responder outage, a firewall change — your site goes hard-down for every visitor, and stays down until you fix stapling or replace the certificate.

Very few operators accepted that trade. Adoption stayed negligible, and Let's Encrypt stopped issuing Must-Staple certificates entirely on 7 May 2025.

It is worth knowing about precisely because it is the honest attempt to fix soft-fail, and its failure is what forced the industry down a completely different road.

🧪 Exercise C1.1 — Look for the Must-Staple extension
bash
cd ~/tls-lab/m09

# issue a certificate with Must-Staple, so you can recognise it
cat > ms.cnf <<'EOF'
basicConstraints=critical,CA:FALSE
keyUsage=critical,digitalSignature
extendedKeyUsage=serverAuth
subjectAltName=DNS:muststaple.test
1.3.6.1.5.5.7.1.24=DER:30:03:02:01:05
EOF
openssl req -new -key srv.key -out ms.csr -subj "/CN=muststaple.test"
openssl x509 -req -in ms.csr -CA ca.crt -CAkey ca.key -out ms.crt -days 90 -extfile ms.cnf

openssl x509 -in ms.crt -noout -text | grep -A2 '1.3.6.1.5.5.7.1.24'
Expected result — click to reveal
plain text
1.3.6.1.5.5.7.1.24:
    0...

What to read out of this.

  • 1.3.6.1.5.5.7.1.24 is the TLS Feature extension from RFC 7633. The value 30:03:02:01:05 is DER for a sequence containing the integer 5, which is the TLS extension number for status_request — OCSP stapling.
  • So the certificate is saying: "I will always be served with a stapled OCSP response. If one is missing, refuse me."
  • OpenSSL prints it as a raw OID because it does not have a friendly name for it — a fair indication of how rare it is.
  • You have just created a certificate that would take your own site down the moment stapling failed. That is not a criticism of the design; it is the design. It is why adoption never happened.

🔑 The lesson generalises well beyond TLS. Must-Staple is a security control that works exactly as intended and was rejected because the failure mode was worse than the threat. Operators would rather accept a small, hard-to-exploit risk than a mechanism that can take the site down. Being able to reason about that trade-off out loud is worth more in an interview than knowing the OID.


C2 · CRLite and CRLSets — the browsers gave up on asking

The analogy — the bank posts one tiny summary to every shop, every morning.

Stop having shops ring up. Stop posting thick booklets. Instead, the bank compresses every cancelled card in the country into a small slip of paper — cleverly encoded, a few hundred bytes — and posts it to every shop each morning.

Now the shop checks instantly, offline, with no phone call and no booklet. The bank learns nothing about where anyone shops, because nobody asks it anything.

That is CRLite, and the clever encoding is the whole trick.

Firefox — CRLiteChrome — CRLSets
CoverageAll revocations visible in Certificate Transparency logsRoughly 1% — a curated set of high-priority revocations
Size~4 MB snapshot every 45 days, plus ~300 kB/day of deltasSmall, but roughly double CRLite's bandwidth for 1% of the data
RefreshEvery 12 hoursPushed via the browser's component updater
PrivacyPerfect — entirely local, nothing is askedPerfect — same reason
Default sinceFirefox 137Long-standing
How CRLite gets all revocations into 4 MB. Mozilla fetches every CA's CRLs centrally, cross-references them against Certificate Transparency logs — which list every publicly trusted certificate ever issued (Module 03, C6) — and builds a compact set-membership filter.

The structure Mozilla calls Clubcard, a partitioned two-level cascade of Ribbon filters. The point is that you can answer "is this certificate revoked?" with certainty, using far less space than storing the serial numbers themselves.

Mozilla's own comparison: "one thousand times more bandwidth-efficient than daily CRL downloads", at half Chrome's bandwidth while covering all revocations rather than about 1%.

And it is CT that makes it possible. Without a public log of every issued certificate there would be no way to build a complete, verifiable picture. That connection between Module 11's subject and this one is worth holding onto.

The shift to notice, because it is the module's real conclusion.

Old model: the client asks, at connection time. Fragile, slow, leaky, soft-fail.

New model: the vendor aggregates, ahead of time, and ships the answer to the client. Fast, private, offline, and hard-fail-capable because there is nothing to block.

Revocation did not get fixed. It got moved — out of the connection path entirely, and out of your hands as an operator.

And that has a direct consequence for you: your private CA is not in Certificate Transparency logs and is not consumed by Mozilla or Google. Browsers will never know your internal revocations. Part D is about what you can actually do instead.

🎯 Interview questions — Modern browser revocation

Q. How do browsers actually handle revocation in 2026?

Not by asking at connection time. They ship aggregated revocation data to the client in advance and check it locally.

Firefox uses CRLite, default since Firefox 137: Mozilla fetches every CA's CRLs centrally, cross-references Certificate Transparency logs, and compresses all known revocations into a compact set-membership filter — about a 4 MB snapshot every 45 days plus roughly 300 kB of daily deltas, refreshed every 12 hours. Mozilla describes it as a thousand times more bandwidth-efficient than daily CRL downloads.

Chrome uses CRLSets, a much smaller curated list covering roughly 1% of revocations — high-priority ones — pushed through the component updater.

Why this is better: it is fast, entirely offline, perfectly private (nothing is asked, so nothing is logged), and it cannot be blocked by an attacker on the path, which finally escapes soft-fail.

The trade-off: it depends on Certificate Transparency to be complete, and it is controlled by the browser vendor. It also means a private CA gets no browser revocation support at all — internal CAs are not in CT logs and are not aggregated by anyone, so internal revocation has to be handled by other means.


C3 · Short-lived certificates — the answer that actually stuck

The analogy — cards that expire every six weeks.

Stop trying to make cancellation work. Make the cards expire so fast that cancellation barely matters.

If a card is only valid for six weeks, a stolen one is worth six weeks at most — and usually far less, because it was stolen partway through. You no longer need a booklet, or a phone call, or a rule about what to do when the line is down.

The cost is obvious: you are now reissuing cards constantly. That is only acceptable if reissuing is completely automatic — which is exactly why the certificate lifetime reductions and the mandatory move to ACME are the same story.

FromMaximum lifetimeWhat it means for revocation
Historically3–5 yearsRevocation genuinely mattered — and genuinely did not work
2020398 daysStill long enough to need revocation
15 March 2026200 daysExposure window roughly halved
March 2027100 daysManual renewal becomes impractical
March 202947 daysA compromised certificate expires faster than most CRLs refresh
Read the last row again, because it is the punchline of the whole module.

A public CRL typically refreshes every few hours to a few days. At a 47-day maximum lifetime, a compromised certificate's remaining life is measured in weeks — and the industry has decided that is a smaller, more predictable risk than depending on a revocation system that soft-fails.

Revocation was not fixed. It was made less necessary.

That is the sentence that ties Modules 09 and 10 together: everything about ACME, automation and renewal exists because this was the answer to the problem in this module.

🧪 Exercise C3.1 — Compare exposure windows yourself
bash
echo "=== how long do real certificates live? ==="
for h in letsencrypt.org www.google.com www.microsoft.com; do
  dates=$(openssl s_client -connect $h:443 -servername $h </dev/null 2>/dev/null \
          | openssl x509 -noout -dates 2>/dev/null)
  nb=$(printf '%s' "$dates" | grep notBefore | cut -d= -f2)
  na=$(printf '%s' "$dates" | grep notAfter  | cut -d= -f2)
  if [ -n "$nb" ]; then
    days=$(( ( $(date -d "$na" +%s) - $(date -d "$nb" +%s) ) / 86400 ))
    printf '  %-24s %3d days total lifetime\n' "$h" "$days"
  fi
done
Expected result — click to reveal
plain text
letsencrypt.org           90 days total lifetime
www.google.com            90 days total lifetime
www.microsoft.com         91 days total lifetime

What to read out of this.

  • Everyone is already at about 90 days, well under the 200-day cap that took effect in March 2026. The large operators went short voluntarily, years before they were required to — because they had automated renewal and there was no reason not to.
  • 90 days is the Let's Encrypt default and has become the industry norm. Google has been issuing 90-day certificates for years.
  • The date -d arithmetic is Linux-only — BSD and macOS need date -j -f. Same portability trap as Module 03 (A3.1).

🔑 The thing to notice: nobody at this scale is anywhere near the maximum. The regulatory cap is chasing behaviour that automated operators adopted willingly. The organisations that will be hurt by 47 days in 2029 are the ones still renewing by hand — and that is deliberate pressure, not an accident.

💡 Let's Encrypt now also offers 6-day certificates. At that lifetime revocation is genuinely irrelevant: a compromised certificate expires before most CRLs would even have listed it.

🎯 Interview questions — Short-lived certificates

Q. Why do certificate lifetimes keep getting shorter?

Because revocation does not work, and shortening lifetimes is the alternative.

Revocation fails for structural reasons: CRLs are stale and enormous, OCSP leaks browsing history and adds latency, and both soft-fail — so an attacker who can use a revoked certificate can also block the check. Must-Staple was the honest fix and nobody adopted it, because the failure mode took sites offline.

If you cannot reliably withdraw a certificate, the next best thing is to ensure it stops working on its own, soon. The CA/Browser Forum schedule is 200 days from March 2026, 100 days from March 2027, and 47 days from March 2029. At 47 days, a compromised certificate's remaining life is shorter than the refresh interval of many CRLs.

The second effect, which is arguably the point: short lifetimes force automation, and automation removes the manual steps where outages come from. The industry is deliberately making manual certificate management impossible.

The judgement to show: shorter lifetimes trade a rare catastrophic risk for a frequent routine operation. That is a good trade only if the routine operation is automated. For a team renewing by hand it makes things strictly worse — which is exactly the pressure intended.

---"

Part D · Revocation in your own PKI

D1 · What you can actually do internally

The analogy — a members' club instead of a national card network.

Everything so far has been about the public card network: millions of shops, no coordination, and a bank that must not learn where you shop.

Your internal PKI is a members' club. You know every member, you control every door, and nobody outside cares. So the constraints that killed public revocation mostly do not apply:

  • Size? Hundreds or thousands of certificates, not hundreds of millions. Your CRL is kilobytes.
  • Privacy? The CA is you. There is no third party to leak to.
  • Availability? Your CRL is on your own infrastructure, next to everything else that already has to be up.

So revocation can genuinely work inside your own estate — which makes it worth doing properly, and worth knowing that this is where the effort pays off.

🧪 Exercise D1.1 — Publish and consume a CRL the way you would internally
bash
cd ~/tls-lab/m09

echo "=== 1. regenerate the CRL - do this on a schedule ==="
openssl ca -config ca.cnf -gencrl -out ca.crl
openssl crl -in ca.crl -noout -lastupdate -nextupdate

echo
echo "=== 2. publish it as DER too - many clients expect DER over HTTP ==="
openssl crl -in ca.crl -outform DER -out ca.crl.der
ls -l ca.crl ca.crl.der

echo
echo "=== 3. a client that actually enforces it ==="
cat ca.crt ca.crl > bundle.pem
printf '  %-12s ' "a.test:"; openssl verify -CAfile bundle.pem -crl_check a.crt 2>&1 | tail -1
printf '  %-12s ' "b.test:"; openssl verify -CAfile bundle.pem -crl_check b.crt 2>&1 | tail -1
printf '  %-12s ' "c.test:"; openssl verify -CAfile bundle.pem -crl_check c.crt 2>&1 | tail -1

echo
echo "=== 4. is the CRL stale? the check that matters operationally ==="
nu=$(openssl crl -in ca.crl -noout -nextupdate | cut -d= -f2)
if [ "$(date -d "$nu" +%s)" -lt "$(date +%s)" ]; then
  echo "  STALE - clients doing hard-fail will now REJECT everything"
else
  echo "  fresh until $nu"
fi
Expected result — click to reveal
plain text
=== 1. regenerate the CRL ===
lastUpdate=Aug 24 05:28:17 2026 GMT
nextUpdate=Aug 31 05:28:17 2026 GMT

=== 2. publish it as DER too ===
-rw------- 1 zaeem zaeem 638 Aug 24 05:28 ca.crl
-rw------- 1 zaeem zaeem 431 Aug 24 05:31 ca.crl.der

=== 3. a client that actually enforces it ===
  a.test:      a.crt: OK
  b.test:      error b.crt: verification failed
  c.test:      c.crt: OK

=== 4. is the CRL stale? ===
  fresh until Aug 31 05:28:17 2026 GMT

What to read out of this.

  • DER is 431 bytes against PEM's 638 — the Base64 overhead from Module 02 (B2), on a file that gets fetched constantly. CRLs are conventionally published as DER over HTTP, and crlDistributionPoints URLs usually end in .crl meaning DER. Publishing PEM there is a common mistake that some clients silently fail on.
  • Only b.test failed, and it failed because you revoked it. The mechanism works exactly as designed.
  • Check 4 is the one that will bite you. A CRL past its nextUpdate is treated as absent, and any client configured to hard-fail then rejects every certificate from that CA. Your entire internal estate goes down because a cron job did not run.

⚠️ This is the real operational risk of internal revocation, and it is worth stating plainly: a stale CRL is more likely to cause an outage than a revoked certificate is to cause a breach. So:

  • Regenerate well before nextUpdate — if the CRL is valid for 7 days, regenerate daily.
  • Monitor the published CRL's nextUpdate, not just the fact that the file exists.
  • Publish to somewhere at least as available as the services depending on it.

🔑 The full internal revocation flow, for reference:

bash
# revoke
openssl ca -config ca.cnf -revoke bad.crt -crl_reason keyCompromise
# regenerate and publish (cron, daily)
openssl ca -config ca.cnf -gencrl -out /var/www/crl/ca.crl.pem
openssl crl -in /var/www/crl/ca.crl.pem -outform DER -out /var/www/crl/ca.crl
# verify what you published
openssl crl -in /var/www/crl/ca.crl -inform DER -noout -nextupdate

D2 · When to revoke, and what to do instead

The analogy — cancelling the card is the last step, not the first.

Your card is stolen. What do you do first?

Not cancel it. You get a new card first, so you can still buy food. Then you cancel the old one.

Doing it the other way round leaves you with no working card and a stolen one still out there — the worst of both.

Certificate incidents work the same way, and people get the order wrong under pressure.

SituationRevoke?What actually matters
Private key leakedYeskeyCompromiseReplace first, revoke second. Revocation may not stop anyone; replacement definitely does
Key was in git, even brieflyYesGit history is permanent. Treat as leaked
Certificate issued for a wrong nameYes — supersededPublic CAs are required to revoke mis-issuance within 5 days
Employee left / server decommissionedYes — cessationOfOperationMostly hygiene. Low urgency
Routine replacementOptionalJust let the old one expire. Revoking adds CRL bulk for nothing
Renewed earlyNoRevoking the old one while it is still deployed somewhere causes the outage you were avoiding
The incident order, and it is the wrong way round in most people's heads:

1. Generate a new key. Not the same key — the old one is compromised.

2. Issue a new certificate for it.

3. Deploy it everywhere, and confirm on the wire (Module 08, B3).

4. Only now revoke the old certificate.

5. Fix the cause — git history, backups, log aggregation, image layers, and whether the same key was reused elsewhere.

Step 4 is last because revocation is the weakest control in the list and might protect nobody. Step 3 is what actually ends the exposure. Putting revocation first is the instinct, and it takes your service down while achieving little.

🧪 Exercise D2.1 — Rehearse a key-compromise response
bash
cd ~/tls-lab/m09
umask 077

echo "=== step 1-2: NEW key, NEW certificate. Never reuse the compromised key ==="
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out c-new.key
openssl req -new -key c-new.key -out c-new.csr -subj "/CN=c.test"
openssl ca -config ca.cnf -extensions srv -batch -notext -in c-new.csr -out c-new.crt

echo "=== confirm the new pair actually matches, BEFORE deploying ==="
diff <(openssl x509 -in c-new.crt -noout -pubkey) <(openssl pkey -in c-new.key -pubout) \
  && echo "  MATCH - safe to deploy"

echo
echo "=== step 3 would be: deploy + reload + verify on the wire ==="
echo
echo "=== step 4: ONLY NOW revoke the old one ==="
openssl ca -config ca.cnf -revoke c.crt -crl_reason keyCompromise 2>&1 | tail -1
openssl ca -config ca.cnf -gencrl -out ca.crl 2>/dev/null

echo
echo "=== the result ==="
cat ca.crt ca.crl > bundle.pem
printf '  old c.crt : '; openssl verify -CAfile bundle.pem -crl_check c.crt     2>&1 | tail -1
printf '  new c.crt : '; openssl verify -CAfile bundle.pem -crl_check c-new.crt 2>&1 | tail -1
echo
grep -c '^R' db/index.txt | xargs printf '  revoked certificates in the ledger: %s\n'
Expected result — click to reveal
plain text
=== step 1-2: NEW key, NEW certificate ===
Database updated
=== confirm the new pair actually matches ===
  MATCH - safe to deploy

=== step 4: ONLY NOW revoke the old one ===
Revoking Certificate 5A89C90E462B572355643F3AD716B69378A4C51E.

=== the result ===
  old c.crt : error c.crt: verification failed
  new c.crt : c-new.crt: OK
  revoked certificates in the ledger: 2

What to read out of this.

  • A new key, not the old one. This is the step people skip under pressure — reissuing with the same key means the attacker still holds a working key for the new certificate. Reissuing on a compromised key achieves nothing.
  • The key↔certificate check happened before deployment, not after. Module 04 (D2.1)'s gate, in an incident where you least want a second surprise.
  • The old certificate now fails and the new one passes, on a client that actually enforces revocation. On a browser against a public CA it might well not — which is the whole point of Part B.
  • Two revoked certificates in the ledger, b from earlier and c now. The ledger is the audit trail: what was revoked, when, and why.

🔑 The sentence worth having ready: "I would replace before revoking, with a fresh key, because revocation may not reach clients but replacement definitely ends the exposure. Then revoke, then fix the source of the leak."

Now imagine this at 500 hosts. Every step above needs to be automatable, because doing it by hand across 500 hosts during an incident is where mistakes happen. This is the strongest argument for ACME and short lifetimes that has nothing to do with expiry: when a key leaks, you want reissuing everything to be a routine operation you have already performed a thousand times — not an emergency procedure nobody has rehearsed.

🎯 Interview questions — Incident response

Q. A private key has leaked. Walk me through your response.

Replace first, revoke second — and the order matters more than people expect.

  1. Generate a brand-new key. Not the same one. Reissuing a certificate on a compromised key achieves nothing, and it is the step most often skipped under pressure.
  2. Issue a new certificate and verify the key↔certificate match before touching anything.
  3. Deploy it everywhere and reload, confirming on the wire rather than on disk.
  4. Then revoke the old certificate, with reason keyCompromise.
  5. Fix the source. Git history — permanent, so the key is burned regardless. Backups, container image layers, log aggregation, config-management repos. And check whether the same key was reused on other hosts.

Why revocation is last: it is the weakest control in that list. On the public internet it soft-fails, so it may protect nobody. Deploying the replacement is what actually ends the exposure. Revoking first takes your service down while achieving little.

The scale point worth adding: every one of those steps needs to be automatable, because doing it by hand across a fleet during an incident is where the real mistakes happen. That is an argument for ACME that has nothing to do with expiry — when a key leaks you want mass reissuance to be a routine operation you have already done a thousand times.

Q. Should you revoke a certificate you are replacing routinely?

Usually no. If the old certificate is simply being superseded and the key was never exposed, let it expire. Revoking it adds an entry to the CRL until its original expiry date, which grows the CRL for no security benefit.

Worse, revoking early is actively risky: if the old certificate is still deployed anywhere you have forgotten — a second load balancer, a health-check endpoint, a container image — you have just caused an outage on any client that does enforce revocation.

Revoke when the key may have been exposed, or when the certificate was mis-issued. For public certificates, mis-issuance is not optional: the CA/Browser Forum requires revocation within five days, which is what caused the mass revocations in the 2019 serial-number-entropy incident (Module 03, A2.1) — certificates that were perfectly safe but non-compliant.

The nuance worth voicing: with 47-day lifetimes arriving in 2029, "just let it expire" becomes a much stronger answer than it used to be. That is the whole design intent.


Part E · Putting it together

E1 · How this all fits — the complete picture

Diagram source
flowchart TD
    R["🚫 Certificate revoked<br>index.txt: V → R"] --> Q{"How does a client<br>find out?"}
    Q --> CRL["📕 CRL<br>download the whole list"]
    Q --> OCSP["☎️ OCSP<br>ask about one cert"]
    CRL --> P1["❌ stale between publications<br>❌ tens of MB at scale<br>✅ private"]
    OCSP --> P2["✅ small and current<br>❌ leaks every site you visit<br>❌ blocking round trip"]
    P1 --> SF{"What if the check<br>cannot be made?"}
    P2 --> SF
    SF -->|"hard-fail"| HF["💥 CA outage =<br>internet outage"]
    SF -->|"soft-fail"| SFA["🕳️ attacker blocks<br>the check and wins"]
    HF --> FIX["🔧 SO WHAT WORKED?"]
    SFA --> FIX
    FIX --> A1["📥 AGGREGATE IT<br>CRLite · CRLSets<br>vendor ships the answer"]
    FIX --> A2["⏳ MAKE IT MOOT<br>200d → 100d → 47d<br>expire before it matters"]
    style SFA fill:#ffcccc,stroke:#cc0000,stroke-width:2px
    style HF fill:#ffcccc,stroke:#cc0000,stroke-width:2px
    style A1 fill:#d5e8d4,stroke:#82b366,stroke-width:2px
    style A2 fill:#d5e8d4,stroke:#82b366,stroke-width:2px

Two mechanisms, each broken in the opposite way to the other. One impossible choice between hard-fail and soft-fail. And two things that actually worked — neither of which is revocation checking.

The green boxes are the conclusion: move the checking out of the connection path (aggregate it centrally and ship it), and make certificates expire so fast that revocation barely matters. Everything in Module 10 exists to make the second one survivable.


E2 · Production practice

HabitWhy
Do not rely on revocation to contain a public-certificate compromiseSoft-fail means an attacker who can use the certificate can also block the check
Replace before revoking, with a brand-new keyReplacement definitely ends the exposure; revocation may reach nobody. Reissuing on the leaked key achieves nothing
Choose reason codes deliberately — keyCompromise vs supersededThe wrong code either raises a false alarm or hides a real incident from audit
Do not revoke routine replacementsGrows the CRL for no benefit, and breaks anything still serving the old certificate
Regenerate internal CRLs daily if they are valid for 7 daysA CRL past nextUpdate is treated as absent — hard-fail clients then reject your whole estate
Monitor the published CRL's nextUpdate, not just that the file existsA stale CRL is more likely to cause an outage than a revoked certificate is to cause a breach
Publish internal CRLs as DER over HTTPcrlDistributionPoints URLs conventionally point at DER; some clients silently fail on PEM
Keep OCSP stapling on for speed and privacy — but do not count it as revocationStapling is optional from the client's side, so an attacker just does not staple
Avoid Must-StapleThe extension is baked into the certificate. If stapling breaks, the site is hard-down until reissue
Get to short lifetimes and full automationIt is the only measure in this table that reliably shrinks the exposure window
Remember browsers know nothing about your private CA's revocationsInternal CAs are not in CT logs and are not aggregated by Mozilla or Google

E3 · Capstone exercise

Build a working internal revocation setup and then prove its weaknesses — including the stale-CRL outage, which is the failure you are far more likely to cause than to suffer.

Brief.

  1. Build a CA that publishes a CRL with a short validity (2 days), and issue three certificates
  2. Revoke one with keyCompromise and one with superseded; leave one valid
  3. Show that all three pass a normal verification, and that only the revoked ones fail with -crl_check
  4. Generate a deliberately expired CRL and show that a hard-fail client now rejects the valid certificate too
  5. Write a crl-health check that reports: how many certificates are revoked, when the CRL was published, when it expires, and whether it is stale — exiting non-zero if stale
  6. Rehearse a key-compromise response in the correct order, and explain in one line why revocation comes last
Model answer — attempt it first, then click
bash
mkdir -p ~/tls-lab/m09/capstone && cd ~/tls-lab/m09/capstone
umask 077
mkdir -p certs db && touch db/index.txt
openssl rand -hex 8 > db/serial && echo 1000 > db/crlnumber

cat > ca.cnf <<'EOF'
[ca]
default_ca=CA
[CA]
dir=.
database=$dir/db/index.txt
serial=$dir/db/serial
crlnumber=$dir/db/crlnumber
new_certs_dir=$dir/certs
certificate=$dir/ca.crt
private_key=$dir/ca.key
default_md=sha256
default_days=90
default_crl_days=2
policy=pol
rand_serial=yes
unique_subject=no
copy_extensions=none
email_in_dn=no
[pol]
commonName=supplied
[srv]
basicConstraints=critical,CA:FALSE
keyUsage=critical,digitalSignature
extendedKeyUsage=serverAuth
subjectAltName=DNS:one.test,DNS:two.test,DNS:three.test
crlDistributionPoints=URI:http://crl.internal.test/ca.crl
EOF

# ---------- 1. CA + three certificates ----------
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out ca.key
openssl req -x509 -key ca.key -out ca.crt -days 3650 -subj "/CN=Capstone Internal CA" \
  -addext "basicConstraints=critical,CA:TRUE" -addext "keyUsage=critical,keyCertSign,cRLSign"
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out srv.key
for n in one two three; do
  openssl req -new -key srv.key -out $n.csr -subj "/CN=$n.test"
  openssl ca -config ca.cnf -extensions srv -batch -notext -in $n.csr -out $n.crt
done

# ---------- 2. revoke two, with different reasons ----------
openssl ca -config ca.cnf -revoke one.crt   -crl_reason keyCompromise
openssl ca -config ca.cnf -revoke two.crt   -crl_reason superseded
openssl ca -config ca.cnf -gencrl -out ca.crl
cat ca.crt ca.crl > bundle.pem

# ---------- 3. with and without revocation checking ----------
echo "== WITHOUT -crl_check =="
for n in one two three; do printf '  %-8s ' "$n"; openssl verify -CAfile ca.crt $n.crt 2>&1 | tail -1; done
echo "== WITH -crl_check =="
for n in one two three; do printf '  %-8s ' "$n"; openssl verify -CAfile bundle.pem -crl_check $n.crt 2>&1 | tail -1; done

# ---------- 4. the stale-CRL outage ----------
echo "== a CRL that expired yesterday =="
openssl ca -config ca.cnf -gencrl -crldays -1 -out stale.crl
cat ca.crt stale.crl > stale-bundle.pem
printf '  three.crt (VALID cert, stale CRL): '
openssl verify -CAfile stale-bundle.pem -crl_check three.crt 2>&1 | tail -1

# ---------- 5. the health check ----------
cat > crl-health <<'EOS'
#!/usr/bin/env bash
# crl-health <crlfile> [-inform DER]
set -uo pipefail
crl="${1:?usage: crl-health <crlfile>}"; shift || true
inf=(); [ "${1:-}" = "DER" ] && inf=(-inform DER)

lu=$(openssl crl -in "$crl" "${inf[@]}" -noout -lastupdate | cut -d= -f2)
nu=$(openssl crl -in "$crl" "${inf[@]}" -noout -nextupdate | cut -d= -f2)
n=$(openssl crl -in "$crl" "${inf[@]}" -noout -text | grep -c 'Serial Number:')
num=$(openssl crl -in "$crl" "${inf[@]}" -noout -crlnumber 2>/dev/null | cut -d= -f2)

printf '\n  CRL: %s\n' "$crl"
printf '  %-16s %s\n' "revoked certs"  "$n"
printf '  %-16s %s\n' "CRL number"     "${num:-n/a}"
printf '  %-16s %s\n' "published"      "$lu"
printf '  %-16s %s\n' "expires"        "$nu"

now=$(date +%s); exp=$(date -d "$nu" +%s 2>/dev/null || echo 0)
left=$(( (exp - now) / 3600 ))
if   [ "$exp" -eq 0 ];  then printf '  ⚠️  could not parse nextUpdate (BSD date? use gdate)\n\n'; exit 2
elif [ "$left" -lt 0 ]; then printf '  ❌ STALE by %d hours - hard-fail clients will reject EVERYTHING\n\n' "$(( -left ))"; exit 1
elif [ "$left" -lt 24 ];then printf '  ⚠️  expires in %d hours - regenerate now\n\n' "$left"; exit 1
else                         printf '  ✅ fresh for another %d hours\n\n' "$left"; exit 0
fi
EOS
chmod +x crl-health
./crl-health ca.crl
./crl-health stale.crl; echo "  exit code: $?"

# ---------- 6. incident order ----------
echo "== key compromise on three.test: replace FIRST =="
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out three-new.key
openssl req -new -key three-new.key -out three-new.csr -subj "/CN=three.test"
openssl ca -config ca.cnf -extensions srv -batch -notext -in three-new.csr -out three-new.crt
diff <(openssl x509 -in three-new.crt -noout -pubkey) <(openssl pkey -in three-new.key -pubout) \
  && echo "  new pair matches - deploy and reload, verify on the wire"
echo "  ...only now:"
openssl ca -config ca.cnf -revoke three.crt -crl_reason keyCompromise
openssl ca -config ca.cnf -gencrl -out ca.crl
./crl-health ca.crl

Expected output at the key points:

plain text
== WITHOUT -crl_check ==
  one      one.crt: OK          <- REVOKED, and it passes
  two      two.crt: OK          <- REVOKED, and it passes
  three    three.crt: OK

== WITH -crl_check ==
  one      error one.crt: verification failed
  two      error two.crt: verification failed
  three    three.crt: OK

== a CRL that expired yesterday ==
  three.crt (VALID cert, stale CRL): error three.crt: verification failed

  CRL: ca.crl
  revoked certs    2
  CRL number       1001
  published        Aug 24 06:02:11 2026 GMT
  expires          Aug 26 06:02:11 2026 GMT
  ✅ fresh for another 47 hours

  CRL: stale.crl
  ❌ STALE by 25 hours - hard-fail clients will reject EVERYTHING
  exit code: 1

The five things this capstone is really testing.

1. Requirement 3 is the module's core finding. Two revoked certificates pass a normal verification cleanly. Revocation checking is off by default, so every chain check you have run since Module 05 has skipped this gate silently.

2. Requirement 4 is the failure you are more likely to cause. A valid certificate rejected because the CRL expired. -crldays -1 generates a CRL that expired yesterday — a one-flag way to test that your monitoring catches it. In production this is a cron job that stopped running, and it takes down everything at once rather than one service.

3. crl-health exits non-zero on stale, so it can be a monitoring check rather than something a human reads. It also warns at under 24 hours rather than only on failure — because by the time a CRL is stale, the outage has already started.

4. The two reason codes are visible in the ledger. openssl crl -text shows Key Compromise against one and Superseded against the other. That distinction is what an auditor reads, and it costs nothing to get right at revocation time.

5. Requirement 6's one-line answer: revocation comes last because replacement definitely ends the exposure while revocation might reach nobody — and because revoking before the replacement is live guarantees an outage while achieving nothing.

What is still missing, honestly: no OCSP responder, no automated publication, no distribution to clients, and — the big one — no browser will ever consult this CRL, because your CA is not in Certificate Transparency and nobody aggregates it. Internal revocation only works for clients you explicitly configure. That is the honest limit of what Part D can achieve.


E4 · Official documentation — what to bookmark and how to read it

The single most useful document for this module: Let's Encrypt — Ending OCSP Support. It is short, it is written by the people running the largest CA in the world, and it states the privacy argument plainly.

Make it a reflex: when someone proposes relying on revocation, read this first. It is the clearest available statement that the industry's largest issuer concluded per-client revocation checking was not worth its cost.

Core reference pages

LinkWhat it is for
RFC 5280 §5 — CRL ProfileEvery CRL field and extension, including reason codes in §5.3.1
RFC 6960 — OCSPThe protocol, the three response states, and the security considerations in §5
RFC 7633 — TLS Feature ExtensionMust-Staple. Worth reading as a well-designed control that failed on operability
Let's Encrypt — Ending OCSP SupportThe privacy argument, and the exact retirement timeline
Mozilla — CRLite in FirefoxHow aggregated revocation works, with the size and coverage figures
CA/B Forum Ballot SC081v3The 200 → 100 → 47 day schedule, in the original ballot text
openssl ca · openssl crl · openssl ocspRevoking, generating CRLs, and running a test OCSP responder
Feisty Duck — The Slow Death of OCSPGood narrative history of how the industry arrived here
crt.shCertificate Transparency search — which also shows revocation status. Module 11's tool

How to read a CRL

plain text
Last Update: Aug 24 05:28:17 2026 GMT     <- when this snapshot was taken
Next Update: Aug 31 05:28:17 2026 GMT     <- READ THIS FIRST. Past it = treated as ABSENT
X509v3 CRL Number: 4096                   <- increments; stops replay of an older CRL
Revoked Certificates:
    Serial Number: 63777A25...             <- identified by SERIAL, never by name
        Revocation Date: ...
        CRL Reason Code: Key Compromise    <- keyCompromise is the serious one
  1. Next Update first. A stale CRL is the outage you will actually cause.
  2. CRL Number second — if it has not incremented, nothing has been republished.
  3. Serial numbers, not names. Revocation is keyed on (issuer, serial), so you need the certificate to look anything up.

The offline alternative

bash
openssl crl -help                          # every flag
openssl crl -in ca.crl -noout -nextupdate  # ⭐ the one that matters
openssl ca -help 2>&1 | grep -iE 'revoke|crl'
man openssl-crl
🧪 Exercise E4.1 — Find the revocation flags from the CLI
bash
openssl verify -help 2>&1 | grep -iE 'crl|revok'
Expected result — click to reveal
plain text
-crl_check                  Check leaf certificate revocation
-crl_check_all              Check full chain revocation
-crl_download               Download CRL from distribution point of checked certificates
-no_check_time              Do not check certificate/CRL validity period
-extended_crl               Enable extended CRL features

What to read out of this — three of these are worth knowing.

  • -crl_check versus -crl_check_all. The first checks only the leaf. The second checks every certificate in the chain, including intermediates. A revoked intermediate is far more serious than a revoked leaf, and -crl_check alone will not notice it — the same leaf-only blind spot as expiry monitoring in Module 05 (D3).
  • -crl_download will fetch the CRL from crlDistributionPoints automatically. Very useful for a one-off check, and it explains why --crlfile in curl felt so manual: most tools make you supply the CRL yourself, and OpenSSL's ability to fetch it is opt-in.
  • -no_check_time disables validity-period checking for both certificates and CRLs — which is how you would examine an expired CRL's contents without the staleness getting in the way. Never in production.

💡 The most useful one-liner this gives you, for checking a public certificate's revocation status end to end:

bash
openssl s_client -connect host:443 -servername host -showcerts </dev/null 2>/dev/null \
  | sed -n '/BEGIN CERT/,/END CERT/p' > c.pem
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt -crl_download -crl_check c.pem

Expect it to fail on many modern certificates — Let's Encrypt leaves out crlDistributionPoints entirely, so there is nothing to download. That failure is the module's conclusion, demonstrated.


E5 · Self-assessment

Answer each out loud before opening it. If your answer is materially thinner than the one behind the toggle, that topic is worth a second pass.

1. What actually changes when you revoke a certificate?

One field in the CA's database — V becomes R in index.txt, plus a timestamp and reason. The certificate itself does not change and cannot, because editing it would break the signature.

So revocation is a statement the CA holds, and the only way a client learns it is by asking the CA. That asymmetry is the entire difficulty.

2. CRL versus OCSP — how do they fail differently?

A CRL is a bulk download: private but stale and enormous — tens of megabytes at CA scale, and always a window between publications.

OCSP is a per-certificate query: small and current but leaks which site you are visiting to the CA, and adds a blocking round trip mid-handshake.

They fail in opposite directions, and neither could be fixed without becoming the other.

3. What is soft-fail and why did browsers choose it?

If revocation information cannot be obtained, the connection proceeds anyway. Browsers chose it because hard-fail makes every CA's uptime into the web's uptime — one responder outage takes a large share of the internet down.

But it makes revocation ineffective: an attacker able to use a revoked certificate can also block the check, and the client's own policy waves them through.

4. Why is revocation checking off by default in most tools?

Because the alternatives are both bad. Hard-fail creates availability risk; soft-fail creates a false sense of security while adding latency and, for OCSP, a privacy leak.

Demonstrably: openssl verify accepts a revoked certificate with no warning unless you pass -crl_check, and curl accepts one unless you pass --crlfile and supply the CRL yourself.

5. What is Must-Staple and why did it fail?

An extension (RFC 7633) placed in the certificate, saying it must always be served with a stapled OCSP response — turning soft-fail into hard-fail for that certificate only. A genuinely clever fix.

It failed on operability: the instruction is baked into a certificate valid for months, so if stapling breaks for any reason the site is hard-down until stapling is fixed or the certificate reissued. Adoption stayed negligible, and Let's Encrypt stopped issuing Must-Staple certificates in May 2025.

6. Why did Let's Encrypt shut down OCSP, and when?

Privacy first: the CA learns which site is being visited from which IP on every connection, making it an unwanted log of the web's browsing. Cost second.

Must-Staple rejected from 30 January 2025, OCSP URLs removed from certificates 7 May 2025, responders shut down 6 August 2025. Their certificates now carry no revocation URL at all.

7. How do Firefox and Chrome handle revocation now?

Firefox: CRLite, default since Firefox 137 — Mozilla fetches all CAs' CRLs centrally, cross-references Certificate Transparency, and ships a compact filter covering all revocations: ~4 MB every 45 days plus ~300 kB daily, refreshed every 12 hours.

Chrome: CRLSets — a curated list covering roughly 1% of revocations, at about double CRLite's bandwidth.

Both are local, offline and perfectly private, and cannot be blocked by an attacker — which finally escapes soft-fail.

8. What is the industry's actual answer to revocation?

Short lifetimes. 200 days from March 2026, 100 from March 2027, 47 from March 2029. At 47 days a compromised certificate's remaining life is shorter than many CRLs' refresh interval.

Revocation was not fixed; it was made less necessary. The second effect is deliberate: short lifetimes force automation, and automation removes the manual steps where outages come from.

9. A private key leaked. What order do you do things in?

New key → new certificate → verify the pair → deploy and reload everywhere → confirm on the wire → then revoke → then fix the source.

Revocation is last because it is the weakest control and may reach nobody, while deploying the replacement definitely ends the exposure. And never reissue on the compromised key.

10. What is the biggest operational risk of running internal revocation?

A stale CRL. Past nextUpdate it is treated as absent, and any client configured to hard-fail then rejects every certificate from that CA — the whole estate at once, because a cron job stopped.

A stale CRL is far more likely to cause an outage than a revoked certificate is to cause a breach. Regenerate daily on a 7-day CRL, and monitor nextUpdate rather than the file's existence.

11. -crl_check versus -crl_check_all?

-crl_check checks the leaf only. -crl_check_all checks every certificate in the chain, including intermediates.

A revoked intermediate is far more serious than a revoked leaf, and leaf-only checking will not see it — the same blind spot as leaf-only expiry monitoring.

12. Does revocation work for your private CA?

For clients you explicitly configure, yes, and better than on the public internet — your CRL is kilobytes, there is no third-party privacy problem, and it is on infrastructure you control.

For browsers, no, not at all. Your CA is not in Certificate Transparency logs and is not aggregated by Mozilla or Google, so CRLite and CRLSets know nothing about it. Internal revocation reaches only the clients you have set up to check.


E6 · Command reference — everything from this module

The commands introduced in Module 09, grouped by what you are trying to achieve. The ones marked ⭐ are genuinely daily-use.

Revoke and publish

bash
openssl ca -config ca.cnf -revoke bad.crt -crl_reason keyCompromise    # ⭐ revoke, with a reason
openssl ca -config ca.cnf -gencrl -out ca.crl                          # ⭐ generate the CRL
openssl ca -config ca.cnf -gencrl -crldays 2 -out ca.crl                # override validity
openssl ca -config ca.cnf -gencrl -crldays -1 -out stale.crl            # ⭐ a DELIBERATELY stale CRL, for testing
openssl crl -in ca.crl -outform DER -out ca.crl.der                     # ⭐ publish as DER over HTTP
grep -c '^R' db/index.txt                                               # how many revoked?

Read a CRL

bash
openssl crl -in ca.crl -noout -text                    # ⭐ everything
openssl crl -in ca.crl -noout -nextupdate              # ⭐ THE check that matters
openssl crl -in ca.crl -noout -lastupdate -crlnumber   # ⭐ published when, which revision
openssl crl -in ca.crl -inform DER -noout -text        # if it came from an HTTP endpoint
openssl crl -in ca.crl -noout -text | grep -c 'Serial Number:'   # how many entries

Verify with revocation checking

bash
cat ca.crt ca.crl > bundle.pem                                   # ⭐ CRL must be in the trust bundle
openssl verify -CAfile bundle.pem -crl_check leaf.crt            # ⭐ check the LEAF
openssl verify -CAfile bundle.pem -crl_check_all leaf.crt        # ⭐ check the WHOLE chain
openssl verify -CAfile ca.crt -crl_download -crl_check leaf.crt  # ⭐ fetch the CRL automatically
curl -sS --cacert ca.crt --crlfile ca.crl https://host/           # curl, with a local CRL

Check public certificates

bash
openssl s_client -connect h:443 -servername h </dev/null 2>/dev/null \
  | openssl x509 -noout -ext crlDistributionPoints,authorityInfoAccess   # ⭐ does it even have URLs?
openssl s_client -connect h:443 -servername h -status </dev/null 2>/dev/null \
  | grep -A5 'OCSP response'                                             # ⭐ is it stapling?
openssl ocsp -issuer chain.pem -cert leaf.pem -url http://ocsp.example/ -text   # query a responder

Incident response, in order

bash
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out new.key   # ⭐ 1. NEW key
openssl req -new -key new.key -out new.csr -subj "/CN=host"                 #   2. new CSR
openssl ca -config ca.cnf -extensions srv -batch -notext -in new.csr -out new.crt
diff <(openssl x509 -in new.crt -noout -pubkey) <(openssl pkey -in new.key -pubout)  # ⭐ 3. verify pair
#   4. deploy + reload + confirm on the wire  (Module 08, B3)
openssl ca -config ca.cnf -revoke old.crt -crl_reason keyCompromise         # ⭐ 5. ONLY NOW revoke
openssl ca -config ca.cnf -gencrl -out ca.crl                               #   6. republish
The CRL health check worth running on a schedule. A stale CRL is the outage you are most likely to cause:
bash
nu=$(openssl crl -in /var/www/crl/ca.crl -inform DER -noout -nextupdate | cut -d= -f2)
left=$(( ( $(date -d "$nu" +%s) - $(date +%s) ) / 3600 ))
if   [ "$left" -lt 0 ];  then echo "CRITICAL: CRL stale by $(( -left ))h - hard-fail clients reject EVERYTHING"; exit 2
elif [ "$left" -lt 24 ]; then echo "WARNING: CRL expires in ${left}h"; exit 1
else                          echo "OK: CRL fresh for ${left}h"; exit 0; fi

Monitor nextUpdate, not whether the file exists. A file that is present and expired is worse than no file at all.


Next — Module 10 · ACME & Let's Encrypt: Automation Is Now Mandatory.

This module ended with the industry's real answer to revocation: make certificates expire before it matters. Module 10 is how you survive that — because 47-day certificates are only viable if issuance is completely automatic.

It covers the ACME protocol itself (RFC 8555), the three challenge types and when each is the right one, account keys and what they actually authorise, certbot and its alternatives, wildcard certificates via DNS-01, rate limits and how to avoid hitting them, renewal automation with deploy hooks, and the failure modes that only appear at scale.

Official reading ahead of it: RFC 8555 — Automatic Certificate Management Environment and the certbot documentation.

📚 Sources for the interview questions

Question selection was cross-referenced against publicly published 2026 SSL/TLS and PKI interview question sets, then rewritten and deepened:

Every command and expected output in this module was executed on OpenSSL 3.0.13, including the full revoke → CRL → verify cycle, the index.txt before-and-after showing V becoming R with a reason code, and the four verification outcomes that make up Exercise A2.2 — that a revoked certificate passes a normal openssl verify (b.crt: OK), that -crl_check catches it (error 23), and that -crl_check with no CRL available rejects a valid certificate (error 3). That last pair is the hard-fail/soft-fail dilemma, demonstrated rather than described.

Current-state claims were verified against primary sources: Let's Encrypt's OCSP retirement announcement for the January/May/August 2025 timeline and the privacy rationale; Mozilla's CRLite post for Firefox 137 default enablement, the ~4 MB/45-day plus ~300 kB/day figures, and the comparison with Chrome's CRLSets covering roughly 1% of revocations; CA/B Forum Ballot SC081v3 for the 200/100/47-day schedule; and RFC 6960, RFC 7633 and RFC 5280 §5 for the protocols themselves.

Answers were rewritten and deepened rather than reproduced — published versions are usually correct but shallow, and the added operational detail is what actually differentiates a candidate in the room.

Spotted a mistake or want something added? Send me a note.