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

Updated 26 August 2026

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

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

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

Prerequisite: Modules 01–09. You need CSRs from Module 04, the deploy-and-reload lesson from Module 08 (B3), and the short-lifetime argument from Module 09 (C3).


The picture to hold in your head for this whole module — the passport kiosk.

The old way of getting a certificate: fill in a form, email it to a company, wait, pay, receive a file, install it by hand. Repeat every year. That is a human process, and humans forget.

ACME replaces the counter clerk with a self-service kiosk. You walk up, the machine asks you to prove you live at the address, you do, and it prints the document. No appointment, no human, thirty seconds.

Three things follow immediately, and they are the shape of this whole module:

  1. The machine must be able to check your claim without a human — that is the challenge.
  2. It needs to know who is asking — that is the account key.
  3. It must stop one person printing a million passports — those are the rate limits.
About the exercises in this module. Several of them talk to Let's Encrypt's live API, which is read-only and safe — fetching the directory costs nothing and is not rate-limited.

Anything that would actually issue a certificate uses the staging environment, which issues untrusted certificates and has far looser limits. Never test against production. That is the single most common way people get themselves rate-limited for a week.

Nothing in this module needs root, and nothing touches your system trust store.

Part A · What ACME actually is

A1 · The directory — the menu board at the kiosk

The analogy — the menu board above the kiosk.

You do not walk up and start pressing buttons. You read the board first: new applications — slot 3. Renewals — slot 5. Cancellations — slot 7.

And crucially: the board can change. The kiosk operator can move things around, and a customer who memorised "slot 3" last year will press the wrong button.

That board is the ACME directory — a single JSON document listing every URL the client needs. A well-written client fetches it every time and never hard-codes a URL.

🧪 Exercise A1.1 — Read the live directory from the world's largest CA
bash
curl -sS https://acme-v02.api.letsencrypt.org/directory | python3 -m json.tool
Expected result — click to reveal
plain text
{
    "PeiDSJ6MPvc": "https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417",
    "keyChange": "https://acme-v02.api.letsencrypt.org/acme/key-change",
    "meta": {
        "caaIdentities": ["letsencrypt.org"],
        "profiles": {
            "classic": "https://letsencrypt.org/docs/profiles#classic",
            "shortlived": "https://letsencrypt.org/docs/profiles#shortlived",
            "tlsserver": "https://letsencrypt.org/docs/profiles#tlsserver"
        },
        "termsOfService": "https://letsencrypt.org/documents/LE-SA-v1.8-July-06-2026.pdf",
        "website": "https://letsencrypt.org"
    },
    "newAccount": "https://acme-v02.api.letsencrypt.org/acme/new-acct",
    "newNonce": "https://acme-v02.api.letsencrypt.org/acme/new-nonce",
    "newOrder": "https://acme-v02.api.letsencrypt.org/acme/new-order",
    "renewalInfo": "https://acme-v02.api.letsencrypt.org/acme/renewal-info",
    "revokeCert": "https://acme-v02.api.letsencrypt.org/acme/revoke-cert"
}

What to read out of this — there is a lot in eight lines.

  • PeiDSJ6MPvc — that random-looking key is deliberate, and it is my favourite detail in the whole protocol. Let's Encrypt inserts a random entry into the directory specifically to break clients that hard-code URLs or that choke on unexpected fields. If your client crashes on this, it was going to crash the next time a real field was added. Follow the link in its value and they explain exactly that.
  • newNonce exists because every ACME request must carry a single-use number (a nonce). That is what stops an attacker recording a valid request and replaying it later.
  • newAccount, newOrder, revokeCert, keyChange — the four things you can do: register, ask for a certificate, revoke one, and rotate your account key.
  • renewalInfo is ARI, standardised as RFC 9773 in September 2025. It lets the CA tell your client when to renew. Section C3.
  • profiles — three certificate profiles: classic, tlsserver and shortlived. Part D.
  • caaIdentities: ["letsencrypt.org"] is the name this CA looks for in your CAA DNS records — Module 11's subject.

🔑 The rule this teaches: fetch the directory, never hard-code an endpoint. Every URL above can change, and the CA has gone out of its way to punish clients that assume otherwise.

🧪 Exercise A1.2 — Compare production with staging
bash
echo "=== PRODUCTION ==="
curl -sS https://acme-v02.api.letsencrypt.org/directory | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['newOrder'])"

echo "=== STAGING - use this for ALL testing ==="
curl -sS https://acme-staging-v02.api.letsencrypt.org/directory | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['newOrder'])"
Expected result — click to reveal
plain text
=== PRODUCTION ===
https://acme-v02.api.letsencrypt.org/acme/new-order

=== STAGING - use this for ALL testing ===
https://acme-staging-v02.api.letsencrypt.org/acme/new-order

What to read out of this.

  • Two completely separate environments with separate accounts, separate rate limits, and separate CA hierarchies. A staging account does not exist in production and vice versa.
  • Staging certificates are signed by an untrusted root ("Fake LE Root"), so browsers reject them. That is the point — you can issue as many as you like without polluting Certificate Transparency logs or burning your production quota.
  • Staging limits are far higher. Production allows 50 certificates per registered domain per week (Part C2); staging is generous enough that you will not hit it while testing.

⚠️ This is the mistake to avoid above all others in this module. Debugging a broken ACME setup against production will exhaust your weekly quota in an afternoon, and then you cannot issue a real certificate for seven days — including the renewal that was already due. Test in staging, then switch one flag.

In certbot that flag is --test-cert (or --server with the staging directory URL). In acme.sh it is --staging. In lego it is --server.

🎯 Interview questions — The ACME directory

Q. What is ACME and what problem does it solve?

ACME — Automatic Certificate Management Environment, RFC 8555 — is a protocol for getting certificates without a human being involved. The client proves control of a domain by completing a challenge the CA can verify automatically, and the CA issues a certificate in seconds.

Before it, obtaining a certificate meant filling in a form, emailing a CSR, waiting, paying, and installing the result by hand. That is fine annually and impossible at scale — and it is the process that produces expired-certificate outages, because it depends on somebody remembering.

Why it matters more every year: the CA/Browser Forum is cutting maximum lifetimes to 200 days in March 2026, 100 days in 2027 and 47 days in 2029. At 47 days, manual renewal is roughly eight times a year per certificate. ACME is not a convenience any more; it is the only viable process.

The design detail worth mentioning: everything starts from a directory — one JSON document listing every endpoint. Clients are expected to fetch it each time rather than hard-code URLs, and Let's Encrypt enforces that culturally by inserting a random entry into the directory to break clients that make assumptions about its shape.


A2 · The account key — and what it does NOT authorise

The analogy — your membership card at the kiosk.

The kiosk gives you a card so it can recognise you between visits: this is the same person who asked yesterday. It tracks your quota against it, and it can suspend it if you misbehave.

But here is the important part: the card does not prove you own any address. It says who is asking, not what you are entitled to. Every single time you request a passport for an address, you must prove you control that address again.

So an account key is an identity, never an authorisation.

The consequence people get wrong. Stealing an ACME account key does not let an attacker get certificates for your domains. They would still have to pass a challenge — put a file on your web server, or create a DNS record you control.

What it does let them do is revoke your certificates, see your order history, and consume your rate limits.

So: protect the account key like any other key (0600, back it up, do not commit it) — but understand that the domain-control proof is the real security boundary, not the account.

KeyWhat it is for
Account keySigns every ACME request. Identifies you to the CA. One per client installation, reused forever
Certificate keyThe server's private key. Goes in the CSR, ends up in the certificate. Different from the account key
These two are constantly confused, and the confusion causes real incidents.

Your account key lives in /etc/letsencrypt/accounts/... and never changes. Your certificate key lives in /etc/letsencrypt/live/<domain>/privkey.pem and is what your web server loads.

Backing up /etc/letsencrypt captures both. Backing up only live/ captures the certificates but loses the account — so on a rebuild your client registers a brand-new account, which is usually harmless but resets your rate-limit history and orphans the old one.

🧪 Exercise A2.1 — Find both keys on a machine that already runs certbot

Skip this if you have no certbot installation — read the expected output instead.

bash
# the ACCOUNT key - identity, one per client
sudo find /etc/letsencrypt/accounts -name 'private_key.json' 2>/dev/null | head -2

# the CERTIFICATE keys - one per certificate
sudo ls -l /etc/letsencrypt/live/*/ 2>/dev/null | head -12
Expected result — click to reveal
plain text
/etc/letsencrypt/accounts/acme-v02.api.letsencrypt.org/directory/8f3a.../private_key.json

/etc/letsencrypt/live/shop.example.com/:
lrwxrwxrwx 1 root root   43 Aug 20 09:12 cert.pem -> ../../archive/shop.example.com/cert1.pem
lrwxrwxrwx 1 root root   44 Aug 20 09:12 chain.pem -> ../../archive/shop.example.com/chain1.pem
lrwxrwxrwx 1 root root   48 Aug 20 09:12 fullchain.pem -> ../../archive/shop.example.com/fullchain1.pem
lrwxrwxrwx 1 root root   46 Aug 20 09:12 privkey.pem -> ../../archive/shop.example.com/privkey1.pem

What to read out of this.

  • The account key is JSON, not PEM. ACME uses JWS — JSON Web Signature — so certbot stores the account key as a JWK. That is why you cannot read it with openssl pkey.
  • The account key path contains the directory URL. acme-v02.api.letsencrypt.org versus acme-staging-v02... — separate accounts for separate environments, exactly as Exercise A1.2 said.
  • Everything in live/ is a symlink into archive/. That is deliberate: your nginx config points at the stable live/ path, and renewal writes cert2.pem, cert3.pem into archive/ and re-points the symlink. Your config never changes.
  • The four files are exactly Module 08's set: fullchain.pem and privkey.pem are what you configure; cert.pem and chain.pem are the split versions, with chain.pem being what ssl_trusted_certificate wants for OCSP stapling.

⚠️ Never point nginx at archive/. Those filenames change on every renewal, and your config will silently keep loading cert1.pem forever. Always use live/.

🎯 Interview questions — Account keys

Q. What does an ACME account key authorise?

Identity, not authorisation. The account key signs every request so the CA knows who is asking, tracks rate limits against it, and can suspend it. It does not grant any right to a domain.

Every order still requires passing a challenge that proves current control of each name. So stealing an account key does not let an attacker obtain certificates for your domains — they would still have to place a file on your web server or create a DNS record.

What a stolen account key does allow: revoking your existing certificates, reading your order history, and burning your rate limits. Worth protecting, but the domain-control proof is the real security boundary.

The practical detail: the account key and the certificate key are different things and are constantly confused. The account key lives in /etc/letsencrypt/accounts/ as a JWK and never changes; the certificate key is per-certificate in live/<domain>/privkey.pem. Backing up only the certificates loses the account.


A3 · The order flow, end to end

Diagram source
sequenceDiagram
    participant C as 🤖 ACME client
    participant CA as 🏢 CA
    participant W as 🌐 your server / DNS
    C->>CA: GET /directory
    CA-->>C: every endpoint URL
    C->>CA: newAccount — here is my account key
    CA-->>C: account URL
    C->>CA: newOrder — I want shop.example.com
    CA-->>C: order + one authorization per name
    CA-->>C: challenge: "prove it — here is a token"
    C->>W: place the token<br>file on port 80, or TXT record
    C->>CA: ready — go and check
    CA->>W: fetch the file / query DNS
    W-->>CA: the token
    CA-->>C: authorization valid ✅
    C->>CA: finalize — here is my CSR
    CA-->>C: certificate URL
    C->>CA: download certificate
    Note over C,W: 🔄 deploy + RELOAD (Module 08, B3)
Three things in that diagram that are worth pausing on.

The CSR arrives at the very end, at finalize. Everything before it is about proving control. This is Module 04's lesson restated: the CSR carries the key, the challenge carries the authority, and they are separate concerns.

The CA connects back to you. In HTTP-01 and TLS-ALPN-01, the CA makes an inbound connection to your server. That has firewall consequences — and it is why these challenges cannot work for a host that is not reachable from the internet.

The last step is not in the protocol. ACME hands you a certificate file. Deploying and reloading is your problem, and skipping it is the outage from Module 08 (B3).

🧪 Exercise A3.1 — Watch a real order, without issuing anything

certbot --dry-run runs the entire flow against staging and throws the certificate away. It is the safest way to see the mechanism.

bash
# only if certbot is installed - otherwise read the expected output
certbot certonly --dry-run --standalone -d test.example.com -v 2>&1 | head -30
Expected result — click to reveal
plain text
Simulating a certificate request for test.example.com
Performing the following challenges:
http-01 challenge for test.example.com
Waiting for verification...
Challenge failed for domain test.example.com
http-01 challenge for test.example.com

Certbot failed to authenticate some domains (authenticator: standalone).
The Certificate Authority reported these problems:
  Domain: test.example.com
  Type:   dns
  Detail: DNS problem: NXDOMAIN looking up A for test.example.com

Hint: The Certificate Authority failed to verify the temporary standalone
server certbot used for the challenge. Ensure the listed domains point to
this machine and that it can accept inbound connections from the internet.

What to read out of this — a failure is the right result here, and it is informative.

  • Simulating a certificate request--dry-run used staging and will discard whatever it gets. Nothing was issued and no production quota was touched.
  • It failed at NXDOMAIN, which is correct: test.example.com does not point at your machine. The CA has to reach you, and this is the message you get when it cannot.
  • Type: dns on an http-01 challenge surprises people. Before the CA can fetch http://your-domain/.well-known/... it must resolve the name. DNS failures therefore surface as challenge failures with a dns type even when you chose the HTTP challenge.
  • The hint is unusually good. "Ensure the listed domains point to this machine and that it can accept inbound connections from the internet" is, in practice, the cause about 90% of the time.

🔑 Make --dry-run a habit. Run it after any change to your ACME setup, before the renewal is due. It exercises the entire path — DNS, firewall, web server, challenge, hooks — against staging, and costs nothing. The worst time to discover your renewal is broken is the day it needs to work.

🎯 Interview questions — The order flow

Q. Walk me through how ACME issues a certificate.
  1. Fetch the directory — one JSON document with every endpoint URL. Never hard-code these.
  2. Register or look up an account, identified by an account key that signs every request.
  3. Create an order listing the identifiers (domain names) you want.
  4. The CA returns an authorization per name, each with challenges you may complete.
  5. Complete a challenge — put a token file on port 80, or a TXT record in DNS — and tell the CA to check.
  6. The CA validates, connecting back to your server or querying DNS.
  7. Finalize the order by submitting a CSR; download the resulting certificate.
  8. Deploy it and reload the service — which is not part of ACME and is where the outages come from.

The detail worth drawing out: the CSR arrives at the very end. Everything before it is about proving control. The CSR carries the key; the challenge carries the authority. They are deliberately separate.

And the practical habit: certbot --dry-run runs the whole flow against staging and discards the result, so you can verify DNS, firewall, web server and hooks without spending quota. Run it after every change — the worst time to find out renewal is broken is the day it must work.


Part B · Proving you control the domain

Three challenge types, and choosing between them is the main design decision in any ACME deployment. The short version:
  • HTTP-01 — the CA fetches a file from your web server on port 80. Simple, and the default.
  • DNS-01 — you create a TXT record. The only one that does wildcards, and the only one that works for servers not reachable from the internet.
  • TLS-ALPN-01 — validated inside a TLS handshake on port 443. For reverse proxies that cannot spare port 80.

B1 · HTTP-01 — post a note through the letterbox

The analogy — posting a note through the letterbox.

"Prove you live at 42 Oak Street. Here is a random word. Write it on a card and put it inside the front door of number 42. I will come and look through the letterbox."

Anyone can claim to live there. Only someone with access to the house can put the card inside it. The randomness matters — you cannot prepare in advance, because the word is chosen after you ask.

That is HTTP-01: the CA gives you a token, you serve it at a known path, the CA fetches it.

DetailValue
Pathhttp://<domain>/.well-known/acme-challenge/<TOKEN>
Port80 only — and this is not negotiable
ContentThe token, a dot, and a thumbprint of your account key
WildcardsNo
Best forA normal public web server. The default, and the right default
Port 80 is mandatory, and redirects are followed — with one catch.

Let's Encrypt validates on port 80 only. You cannot move it. If your firewall blocks port 80, HTTP-01 cannot work, full stop.

It will follow a redirect to HTTPS, which is why the usual "redirect everything to HTTPS" setup normally works. But if your HTTPS certificate is broken or expired, the redirect leads somewhere the CA cannot validate — and renewal fails at exactly the moment you need it.

That is why Module 08 (B2) insisted on serving /.well-known/acme-challenge/ directly over HTTP rather than redirecting it. It is the difference between a bad afternoon and an unrecoverable one.

🧪 Exercise B1.1 — Serve a challenge by hand and see the shape of it
bash
mkdir -p ~/tls-lab/m10/webroot/.well-known/acme-challenge
cd ~/tls-lab/m10

# a token looks like this - random, URL-safe base64
TOKEN="LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0"
THUMB="9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI"
printf '%s.%s' "$TOKEN" "$THUMB" > "webroot/.well-known/acme-challenge/$TOKEN"

python3 -m http.server 8080 --directory webroot >/dev/null 2>&1 &
sleep 1

echo "=== what the CA would fetch ==="
curl -sS "http://127.0.0.1:8080/.well-known/acme-challenge/$TOKEN"; echo
curl -sS -o /dev/null -w 'HTTP %{http_code}  content-type: %{content_type}\n' \
  "http://127.0.0.1:8080/.well-known/acme-challenge/$TOKEN"

pkill -f 'http.server 8080'
Expected result — click to reveal
plain text
=== what the CA would fetch ===
LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0.9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI

HTTP 200  content-type: application/octet-stream

What to read out of this.

  • The content is <token>.<thumbprint> — the CA's random token, then a SHA-256 thumbprint of your account key. That second half is what stops one person completing a challenge on another person's behalf: the CA knows which account key the response must be bound to.
  • The filename is the token itself, so the CA fetches a path it chose. You cannot pre-create it.
  • Content-type does not matter. Let's Encrypt ignores it, which is why a plain static file works and no special web-server configuration is needed.
  • It is plain HTTP with no certificate involved at all. That is deliberate — you are trying to get a certificate, so requiring one would be circular. It is safe because the token is single-use, random, and bound to your account key.

💡 The whole mechanism is a static file. Any web server that can serve a file from a directory can complete HTTP-01, which is why --webroot is the most robust certbot mode: it writes into a directory your existing server already serves, and never touches your config.


B2 · DNS-01 — and the only way to get a wildcard

The analogy — changing the nameplate at the street registry.

Instead of putting a card inside one house, you go to the council's street registry and change the entry for Oak Street. Only someone with authority over the whole street can do that.

And that is exactly why it is the only challenge that grants a wildcard: proving you control one house proves nothing about the street. Proving you control the registry proves you control every house on it.

It also means the CA never has to visit your house at all — which is why DNS-01 works for servers with no public address.

DetailValue
Record_acme-challenge.<domain> TXT
PortNone — nothing connects to your server
WildcardsYes — and it is the only challenge that can
Best forWildcards, internal servers, load-balanced fleets, anything not reachable on port 80
NeedsAPI access to your DNS provider
The security problem with DNS-01, and it is a real one.

To automate DNS-01, something must hold credentials that can change your DNS. Put those on a web server and a compromise of that web server becomes a compromise of your entire domain — the attacker can point your mail, your website and your identity provider wherever they like. That is far worse than losing one certificate.

Let's Encrypt's own documentation says it plainly: avoid storing full DNS API credentials on your web server.

The mitigations, in order of preference:

  1. CNAME delegation. Point _acme-challenge.example.com at a record in a separate throwaway zone, and give the credentials only for that zone. A compromise then affects nothing but challenge records. This is the best answer and it is under-used.
  2. Narrowly scoped API tokens — many providers can issue a token limited to TXT records on one name.
  3. Validate from a separate host that has the credentials, and distribute the resulting certificate.
🧪 Exercise B2.1 — Look at a real _acme-challenge record
bash
echo "=== does anything have one right now? ==="
dig +short TXT _acme-challenge.letsencrypt.org
echo "(usually empty - records are removed after validation)"

echo
echo "=== the CNAME delegation pattern, if a domain uses it ==="
dig +short CNAME _acme-challenge.example.com

echo
echo "=== what a wildcard certificate looks like ==="
openssl s_client -connect www.wikipedia.org:443 -servername www.wikipedia.org </dev/null 2>/dev/null \
  | openssl x509 -noout -ext subjectAltName | tr ',' '\n' | grep -m4 '\*'
Expected result — click to reveal
plain text
=== does anything have one right now? ===
(usually empty - records are removed after validation)

=== the CNAME delegation pattern ===
(empty unless that domain uses delegation)

=== what a wildcard certificate looks like ===
DNS:*.wikipedia.org
DNS:*.m.wikipedia.org
DNS:*.wikimedia.org
DNS:*.m.wikimedia.org

What to read out of this.

  • _acme-challenge records are almost never present. They exist for seconds to minutes during validation and are then removed. Finding an empty result is the normal, healthy state — a stale record left behind usually means a failed run that never cleaned up.
  • Those wildcards could only have come from DNS-01. Every *. entry in a publicly trusted certificate implies the operator proved control of the DNS zone. HTTP-01 and TLS-ALPN-01 physically cannot produce them.
  • *.wikipedia.org and *.m.wikipedia.org both appear, which is Module 03's wildcard-depth rule (B3) again: one star covers one label, so a second level needs its own entry.

🔑 The connection worth making explicit: "wildcard" and "DNS-01" are the same requirement. If someone asks for a wildcard certificate, they are asking for DNS API automation, whether they realise it or not — and that brings the credential-scoping problem with it. The right follow-up question is almost always "do you actually need a wildcard, or would per-host certificates do?"


B3 · TLS-ALPN-01, and choosing between the three

The analogy — proving it during the handshake itself.

Instead of leaving a card inside the door, you answer the door yourself and say the password while shaking hands.

TLS-ALPN-01 uses the ALPN extension from Module 06 (D2): the CA connects on port 443, asks for the protocol acme-tls/1, and your server responds with a special self-signed certificate containing the token. The validation happens inside the TLS handshake, before any HTTP.

It exists for one specific situation: a TLS-terminating reverse proxy that must not give up port 80, and cannot easily serve a static file.

Diagram source
flowchart TD
    Q1{"Do you need a<br>WILDCARD certificate?"}
    Q1 -->|"yes"| DNS["🪧 DNS-01<br>the ONLY option"]
    Q1 -->|"no"| Q2{"Is the server reachable<br>from the internet<br>on port 80?"}
    Q2 -->|"no"| DNS2["🪧 DNS-01<br>nothing connects to you"]
    Q2 -->|"yes"| Q3{"Can you spare<br>port 80?"}
    Q3 -->|"yes"| HTTP["📬 HTTP-01<br>the default, and the right one"]
    Q3 -->|"no - TLS proxy<br>owns 443 only"| ALPN["🤝 TLS-ALPN-01"]
    DNS --> W["⚠️ needs DNS API credentials<br>→ use CNAME delegation"]
    DNS2 --> W
    style HTTP fill:#d5e8d4,stroke:#82b366,stroke-width:2px
    style DNS fill:#ffe6cc,stroke:#d79b00,stroke-width:2px
    style DNS2 fill:#ffe6cc,stroke:#d79b00,stroke-width:2px
    style W fill:#ffcccc,stroke:#cc0000,stroke-width:2px
ChallengePortWildcardsChoose it when
HTTP-0180Normal public web server. Default — start here
DNS-01noneWildcards, internal hosts, or port 80 unavailable
TLS-ALPN-01443TLS-terminating proxy that cannot use port 80
A note on acme-tls/1. That ALPN protocol string is on the same IANA list as h2 and http/1.1 from Module 06 (D2) — which is a nice illustration that ALPN is a general negotiation mechanism, not an HTTP feature. TLS-ALPN-01 borrowed it to carve out a validation-only handshake that can coexist with normal traffic on port 443.

🎯 Interview questions — Challenges

Q. What are the ACME challenge types and when would you use each?

HTTP-01 — the CA fetches http://<domain>/.well-known/acme-challenge/<token> on port 80 and expects the token plus a thumbprint of your account key. Simple, needs no credentials, and is the right default for any public web server. Cannot issue wildcards, and port 80 is not negotiable.

DNS-01 — you publish a TXT record at _acme-challenge.<domain>. Nothing connects to your server, so it works for internal hosts, and it is the only challenge that can issue wildcards.

TLS-ALPN-01 — validated inside a TLS handshake on port 443 using the acme-tls/1 ALPN protocol. For TLS-terminating proxies that cannot spare port 80. No wildcards.

The decision in one line: need a wildcard, or unreachable on port 80 → DNS-01. Otherwise HTTP-01. TLS-ALPN-01 only for the proxy case.

The security point to raise unprompted: DNS-01 requires credentials that can change your DNS. On a compromised web server that is far worse than losing a certificate — an attacker could repoint your mail and your identity provider. The mitigation is CNAME delegation: point _acme-challenge at a separate throwaway zone and scope the credentials to that zone alone.

Q. Why can't HTTP-01 issue a wildcard certificate?

Because it only proves control of one host. Serving a file at http://shop.example.com/.well-known/... demonstrates control of that one name and says nothing about api.example.com or any other subdomain.

A wildcard covers an entire namespace, so the CA needs proof of control over that namespace — which means the DNS zone. Only DNS-01 provides that.

The framing I like: proving you can get inside one house does not prove you control the street. Changing the street registry does.

And the follow-up worth volunteering: since a wildcard forces DNS-01, and DNS-01 forces DNS API credentials, asking for a wildcard is really asking for a much broader credential. With ACME automation, per-host certificates are usually the better answer — and with lifetimes heading to 47 days, the operational argument for wildcards is weakening rather than strengthening.


Part C · Running it in production

C1 · Clients — certbot and the alternatives

The analogy — different people operating the kiosk for you.

The kiosk is the same for everyone. What differs is who walks up and presses the buttons: a general-purpose assistant who also rearranges your filing (certbot), a tiny helper that does nothing but fetch documents (acme.sh), or a system that watches your whole office and keeps every document current without being asked (cert-manager).

Choosing a client is really choosing how much you want it to touch.

ClientWhat it is good at
certbotThe reference client. Python, official EFF tool, plugins for nginx and Apache that edit your config for you. Start here
acme.shPure shell, no dependencies, enormous DNS provider support. Ideal on minimal or embedded systems
legoSingle Go binary, also a library. Good when you want one static file and no runtime
cert-managerThe standard for Kubernetes. Certificates become resources the cluster reconciles. Module 12
Caddy / TraefikACME built into the web server. Zero configuration, and genuinely the least work if they suit you
Cloud managedAWS ACM, Google-managed certificates, Azure. No ACME at all — the provider handles everything, but the key is theirs
The certbot mode people choose wrongly.

--nginx and --apache edit your web server configuration. That is convenient the first time and unwelcome afterwards — it rewrites files that may be under configuration management, and it can conflict with your own changes.

--webroot writes only the challenge file into a directory your server already serves, and touches nothing else. --standalone runs its own temporary listener on port 80, which means stopping your web server first.

For anything managed by Ansible, Puppet or a container image, use --webroot. Let your config management own the config, and let certbot own only the certificate.

🧪 Exercise C1.1 — Inspect what certbot is managing, without changing anything
bash
# read-only - safe on any machine that runs certbot
sudo certbot certificates 2>/dev/null || echo "(certbot not installed)"

echo "=== how is renewal actually triggered? ==="
systemctl list-timers 2>/dev/null | grep -i certbot
cat /etc/cron.d/certbot 2>/dev/null
Expected result — click to reveal
plain text
Found the following certs:
  Certificate Name: shop.example.com
    Serial Number: 3f9a2c8e5b1d47f0a6c39e82b74d105f
    Key Type: ECDSA
    Domains: shop.example.com www.shop.example.com
    Expiry Date: 2026-11-18 09:12:44+00:00 (VALID: 62 days)
    Certificate Path: /etc/letsencrypt/live/shop.example.com/fullchain.pem
    Private Key Path: /etc/letsencrypt/live/shop.example.com/privkey.pem

=== how is renewal actually triggered? ===
NEXT                        LEFT      UNIT                 ACTIVATES
Mon 2026-08-24 11:42:19 UTC 5h 3min   certbot.timer        certbot.service

What to read out of this.

  • certbot certificates is the inventory command. It reads local state only — no network, no quota — and tells you exactly what this machine believes it manages, with expiry dates. First thing to run on an unfamiliar server.
  • The timer runs twice a day, not monthly. That is deliberate: certbot only renews certificates within 30 days of expiry, so most runs do nothing. Frequent checking means a transient failure has dozens of chances to recover before it becomes urgent.
  • Key Type: ECDSA — certbot has defaulted to ECDSA for new certificates for a while now, which is the Module 02 (A1) recommendation applied by default.
  • The paths are the live/ symlinks from Exercise A2.1, which is what your nginx config should point at.

🔑 "Twice a day, renew at 30 days" is worth being able to justify. With a 90-day certificate you get 60 days and roughly 120 attempts before expiry. A single failed run is a non-event. This is why automated renewal is more reliable than a calendar reminder — not because software is cleverer, but because it retries.


C2 · Rate limits — and how people burn them

The analogy — the kiosk only prints so many per household per week.

The machine is free and unattended, so someone will inevitably try to print a hundred thousand passports. The operator sets a quota: so many per household, per week, refilling gradually.

The quota is generous for normal use and unforgiving if you are stuck in a loop. And the people who hit it are almost never abusers — they are engineers debugging a broken setup by retrying against the real machine.

LimitValueRefill
Certificates per registered domain50 / 7 days1 every 202 minutes
Duplicate certificates (identical name set)5 / 7 days1 every 34 hours
Failed validations per identifier5 / hour1 every 12 minutes
Consecutive failures before pause1,152
New orders per account300 / 3 hours1 every 36 seconds
New accounts per IP10 / 3 hours1 every 18 minutes
The limit that actually catches people is "duplicate certificates": 5 per week for the same exact set of names.

Picture the situation. Renewal is failing. You run certbot. It fails. You change something, run it again. Fail. Again. On the sixth attempt you are locked out for a week — and the certificate you were trying to renew expires in the meantime.

"Registered domain" means the registrable part, so example.com — every subdomain shares the same 50-per-week pool. A large estate can exhaust it during a migration without anything being wrong.

The fix is always the same: debug in staging. --dry-run and --test-cert cost nothing and have no meaningful limits. Switch to production only when it works.

🧪 Exercise C2.1 — Read the current limits from the source
bash
curl -sS https://letsencrypt.org/docs/rate-limits/ 2>/dev/null \
  | sed -e 's/<[^>]*>//g' | grep -iE '50 certificates|5 certificates|5 authorization|300 new orders|10 accounts' \
  | sed 's/^[[:space:]]*//' | head -8
Expected result — click to reveal
plain text
Up to 50 certificates can be issued per registered domain every 7 days.
Up to 5 certificates can be issued per exact same set of identifiers every 7 days.
Up to 5 authorization failures per identifier can be incurred by one account every hour.
Up to 300 new orders can be created by a single account every 3 hours.
Up to 10 accounts can be created from a single IP address every 3 hours.

What to read out of this.

  • These change. Let's Encrypt has revised its limits several times, and any blog post quoting them is a snapshot. Read the live page, which is what this command does.
  • The two seven-day limits are the dangerous ones, because a week is longer than most certificates have left when you notice a problem.
  • "Failed validations: 5 per hour" is the one that stops a broken cron job hammering the API. Note it is per identifier, not per account — so one broken domain does not block your others.
  • sed -e 's/<[^>]*>//g' strips HTML tags crudely. Fine for grepping a documentation page; not something to build on.

🔑 The single most valuable habit in this module: --dry-run first, every time. It uses staging, exercises the entire path, and cannot burn production quota. The people who get rate-limited are the people who skipped it.

🎯 Interview questions — Rate limits

Q. You are rate-limited by Let's Encrypt during an outage. What happened, and what do you do?

Almost certainly the duplicate certificate limit: 5 certificates per week for the same exact set of names. The pattern is always the same — renewal fails, someone retries against production while debugging, and after the fifth attempt they are locked out for seven days with the certificate still expiring.

The other one that bites at scale is 50 certificates per registered domain per week, where "registered domain" is the registrable part, so every subdomain of example.com shares one pool. A migration can exhaust that legitimately.

Immediate options: wait for the window to refill; change the set of names slightly, since the duplicate limit is keyed on the exact set, so adding one name creates a different set; or fall back to another CA if you have one configured.

The real answer is prevention: debug in staging. certbot --dry-run and --test-cert exercise the whole flow against an environment with no meaningful limits, and issue untrusted certificates that never touch your quota.

What this signals in an interview: knowing the numbers is fine, but knowing why people hit them — retry loops against production during an incident — is the operationally useful part.


C3 · Renewal, deploy hooks, and ARI

The analogy — the kiosk telling you when to come back.

Left to themselves, everyone turns up at 9am on the first of the month, and the queue is enormous. Worse: if the operator needs to reissue a batch of documents urgently, they have no way to tell anybody — everyone is following their own private schedule.

So the kiosk starts printing a line on your receipt: "come back between Tuesday 04:00 and Wednesday 04:00." Load is spread out, and if something goes wrong the operator can move everyone's window forward.

That is ARI — ACME Renewal Information — standardised as RFC 9773 in September 2025.

ARI solves a problem that only appears at scale, and it is a good one to understand.

Without ARI, every client decides for itself when to renew — usually "at two thirds of lifetime". Three consequences:

  1. Renewals cluster, because certificates issued together renew together.
  2. A mass revocation event is unmanageable. When a CA must revoke a large batch within five days (Module 03, A2.1 — the 2019 serial-entropy incident), it has no channel to tell operators to renew early.
  3. Changing certificate lifetimes breaks assumptions baked into client code.

With ARI, the client asks the renewalInfo endpoint and is told a suggestedWindow with a start and end. The CA can spread load, and can pull the window forward to trigger early renewal across its whole population without anyone changing any configuration.

That last point is the important one: ARI is how a CA recovers from a mass-revocation event without an internet-wide outage.

🧪 Exercise C3.1 — Look at the ARI endpoint
bash
curl -sS https://acme-v02.api.letsencrypt.org/directory \
  | python3 -c "import json,sys; print(json.load(sys.stdin)['renewalInfo'])"

echo
echo "=== a real ARI response looks like this ==="
cat <<'EOF'
{
  "suggestedWindow": {
    "start": "2026-02-03T04:00:00Z",
    "end":   "2026-02-04T04:00:00Z"
  }
}
EOF
Expected result — click to reveal
plain text
https://acme-v02.api.letsencrypt.org/acme/renewal-info

=== a real ARI response looks like this ===
{
  "suggestedWindow": {
    "start": "2026-02-03T04:00:00Z",
    "end":   "2026-02-04T04:00:00Z"
  }
}

What to read out of this.

  • The endpoint is in the directory, so a client that fetches the directory properly discovers ARI automatically — the design from Exercise A1.1 paying off.
  • A window, not a deadline. The client picks a random moment inside it, which is what spreads load. If everyone renewed exactly at start, ARI would have recreated the thundering herd it exists to prevent.
  • The client queries per certificate, identified by its issuer key hash and serial — the same (issuer, serial) identifier used for revocation in Module 09.
  • Support is real but not universal. Certbot supports ARI; adoption across the wider client ecosystem was still growing through 2026.

🔑 The renewal rules that matter regardless of ARI, and these are the exam answer:

  1. Renew early — at two thirds of lifetime, or when ARI says. Never at the last minute.
  2. Check often — twice a day. Most runs do nothing; the point is that failures get dozens of retries.
  3. Use --deploy-hook, not --post-hook — the former runs only when a certificate actually changed, the latter on every check.
  4. The hook must reload the service (Module 08, B3), or the renewal achieves nothing.
  5. Verify on the wire afterwards, not on disk.
bash
certbot renew --deploy-hook "systemctl reload nginx"

🎯 Interview questions — Renewal

Q. How should certificate renewal be automated?

Check frequently, renew early, reload on success, verify on the wire.

  • Twice a day, via a systemd timer or cron. Most runs do nothing because certbot only renews within 30 days of expiry — but with a 90-day certificate that gives roughly 120 attempts over 60 days, so a transient failure is a non-event.
  • Renew at about two thirds of lifetime, or when ARI tells you. Never at the last moment.
  • --deploy-hook, not --post-hook. Deploy hooks fire only when a certificate actually changed; post hooks fire on every check, reloading nginx twice a day for nothing.
  • The hook must reload the service. A renewal with no reload is the outage from Module 08 — new file on disk, old certificate still in memory, failure weeks later.
  • Monitor the live endpoint, comparing the fingerprint on the wire with the one on disk.

The modern addition worth mentioning: ARI (RFC 9773) lets the CA suggest a renewal window rather than the client guessing. It spreads load, adapts automatically when lifetimes change, and — most importantly — gives the CA a channel to pull renewal forward across its whole population during a mass-revocation event, without anyone changing configuration.


Part D · Profiles and short-lived certificates

D1 · The three profiles

The analogy — three grades of document from the same kiosk.

Standard — what everyone has always had. Valid three months, printed with all the traditional fields, works everywhere.

Slim — a modern, lighter document. Six weeks, fewer redundant fields, smaller to carry. For people whose systems are fully automatic.

Day pass — valid six days. So short-lived that there is no cancellation process at all, because nothing lives long enough to need one.

ProfileLifetimeWhat is different
classic90 daysThe default. Includes Common Name, keyEncipherment on RSA, Subject Key ID. Up to 100 names. Carries CRL info
tlsserver45 daysOmits Common Name, drops keyEncipherment, drops Subject Key ID. Up to 25 names. Pending authorizations expire in 1 hour
shortlived~6 days (160h)Identical to tlsserver but qualifies as a Short-Lived Subscriber Certificate — no revocation mechanism required at all
Every difference in the tlsserver profile is something this track already explained.
  • No Common Name — because RFC 9525 says it must not be used for identification, and the Baseline Requirements now mark it NOT RECOMMENDED (Module 03, B2).
  • No keyEncipherment — because TLS 1.3 removed RSA key transport, so the bit has no purpose (Module 06, C2).
  • No Subject Key Identifier — because on a leaf it is only an index, and the leaf has nothing beneath it (Module 03, C4).
  • 1-hour authorization lifetime instead of 7 days — deliberate pressure toward fully automatic issuance.

This is what "smaller certificates for people who fully embrace automation" means in practice: every field that exists only for compatibility with older assumptions has been removed.

🧪 Exercise D1.1 — Read the profiles from the live directory
bash
curl -sS https://acme-v02.api.letsencrypt.org/directory \
  | python3 -c "
import json,sys
d = json.load(sys.stdin)
for name, url in sorted(d['meta']['profiles'].items()):
    print(f'{name:12} {url}')
"
Expected result — click to reveal
plain text
classic      https://letsencrypt.org/docs/profiles#classic
shortlived   https://letsencrypt.org/docs/profiles#shortlived
tlsserver    https://letsencrypt.org/docs/profiles#tlsserver

What to read out of this.

  • The CA advertises its profiles in the directory, so a client can discover what is on offer rather than being told out of band. Same discovery principle as every other endpoint.
  • Requesting one is a field in the order. In certbot: --preferred-profile tlsserver. Omit it and you get classic.
  • classic remains the default, which is the right choice — it is the most compatible, and changing the default under everyone would break things.

🔑 The shortlived profile is Module 09's conclusion made real. At six days a certificate qualifies under the Baseline Requirements as a Short-Lived Subscriber Certificate, which means no revocation mechanism is required at all — no CRL, no OCSP, nothing.

That is the industry's answer to Module 09 in its purest form: not better revocation, but a lifetime short enough that revocation is unnecessary.

⚠️ Only use shortlived if your automation is genuinely trustworthy. At six days, roughly 60 renewals a year per certificate, and any renewal failure becomes an outage within days rather than months. Let's Encrypt's own guidance is that it is for those who fully trust their automation. Run classic until your renewal pipeline has proved itself for months.

🎯 Interview questions — Profiles and short-lived certificates

Q. What are ACME certificate profiles, and would you use a 6-day certificate?

Profiles let the CA offer different certificate shapes from the same API. Let's Encrypt advertises three in its directory: classic (90 days, the default, includes Common Name and legacy fields for maximum compatibility), tlsserver (45 days, drops Common Name, keyEncipherment and Subject Key ID, limited to 25 names), and shortlived (about 6 days).

Every field tlsserver removes is one that modern TLS made obsolete: Common Name because RFC 9525 forbids using it for identification, keyEncipherment because TLS 1.3 removed RSA key transport, Subject Key ID because it is only an index and a leaf has nothing below it.

Would I use shortlived? Not immediately. At six days it qualifies as a Short-Lived Subscriber Certificate under the Baseline Requirements, which means no revocation mechanism is required at all — the cleanest possible answer to the fact that revocation does not work. But it means roughly 60 renewals a year per certificate, and any pipeline failure becomes an outage in days rather than months.

The judgement I would show: run classic until the renewal pipeline has proved itself over months — with monitoring on the live endpoint, deploy hooks that reload, and alerting on renewal failure rather than just expiry. Then move to shorter profiles. Adopting six-day certificates before the automation is trustworthy converts a rare risk into a frequent outage.


Part E · Putting it together

E1 · How this all fits — the complete picture

Diagram source
flowchart TD
    D["📋 DIRECTORY<br>fetch every time,<br>hard-code nothing"] --> A["🎫 ACCOUNT KEY<br>identity, NOT authority"]
    A --> O["📝 ORDER<br>these are the names I want"]
    O --> C{"🔐 CHALLENGE<br>prove control"}
    C -->|"port 80 file"| H["📬 HTTP-01<br>default · no wildcards"]
    C -->|"TXT record"| DN["🪧 DNS-01<br>wildcards · internal hosts<br>⚠️ DNS API credentials"]
    C -->|"port 443 ALPN"| T["🤝 TLS-ALPN-01<br>TLS proxies"]
    H --> F["📄 FINALIZE<br>submit the CSR"]
    DN --> F
    T --> F
    F --> CERT["✅ CERTIFICATE"]
    CERT --> DEP["🔄 DEPLOY + RELOAD<br>NOT part of ACME<br>this is where outages live"]
    DEP --> R["⏰ RENEW<br>twice daily · at 2/3 life<br>or when ARI says"]
    R --> O
    style H fill:#d5e8d4,stroke:#82b366,stroke-width:2px
    style DEP fill:#ffcccc,stroke:#cc0000,stroke-width:2px
    style CERT fill:#d5e8d4,stroke:#82b366,stroke-width:2px

A loop, not a line. The certificate is not the finish — it is one turn of a cycle that runs forever.

The red box is the only step ACME does not do for you, and it is where the failures are. The protocol hands you a file; deploying it and reloading the service is your problem, and skipping it produces the outage from Module 08 (B3) — a renewal that logged success while the running process kept serving the old certificate.


E2 · Production practice

HabitWhy
Always --dry-run first, after every changeUses staging, exercises the whole path, cannot burn production quota. The people who get rate-limited skipped it
Debug against staging, never production5 duplicate certificates per week. The sixth retry locks you out for 7 days with the certificate still expiring
Prefer HTTP-01 unless you need a wildcard or the host is unreachableNo credentials to store, nothing to leak, and the simplest failure modes
If you must use DNS-01, use CNAME delegation to a throwaway zoneDNS API credentials on a web server turn one compromise into control of your whole domain
Never redirect /.well-known/acme-challenge/ to HTTPSIf HTTPS breaks, renewal depends on the broken thing and you cannot renew your way out
Use --webroot, not --nginx/--apache, under configuration managementLet config management own the config; let certbot own only the certificate
Point your server at live/, never archive/archive/ filenames change every renewal; your config would silently keep loading cert1.pem
--deploy-hook, not --post-hook, and it must reloadDeploy hooks fire only on actual renewal. Without a reload the renewal achieves nothing
Check twice daily, renew at two thirds of lifetime~120 retry opportunities over 60 days on a 90-day certificate. Retries are why automation beats reminders
Alert on renewal failure, not only on expiryBy the time an expiry alert fires you have already lost most of your retry window
Back up /etc/letsencrypt whole, not just live/live/ alone loses the account key and your rate-limit history
Stay on classic until the pipeline has proved itself for monthsshortlived means ~60 renewals a year; any failure becomes an outage in days rather than months

E3 · Capstone exercise

Write the health check for an ACME deployment — the one that tells you renewal is broken while you still have time to fix it, rather than when the certificate expires.

Brief. Write acme-health that, for a given certificate directory and hostname:

  1. Reports days remaining and, from the certificate's own dates, what fraction of its lifetime has elapsed — flagging anything past 80%
  2. Confirms the fingerprint on disk matches the fingerprint on the wire (renewed but not reloaded)
  3. Checks that the ACME challenge path is reachable over plain HTTP and is not redirected to HTTPS
  4. Reports which profile the certificate looks like it came from, by inspecting its fields
  5. Confirms the CA's directory is reachable and reports the ARI endpoint
  6. Exits non-zero if anything is actually broken
Model answer — attempt it first, then click
bash
#!/usr/bin/env bash
# acme-health <certdir> <hostname>
set -uo pipefail
dir="${1:?usage: acme-health <certdir> <hostname>}"; host="${2:?}"
FC="$dir/fullchain.pem"
fail=0
ok()   { printf '  ✅ %-24s %s\n' "$1" "$2"; }
bad()  { printf '  ❌ %-24s %s\n' "$1" "$2"; fail=1; }
warn() { printf '  ⚠️  %-24s %s\n' "$1" "$2"; }

printf '\n  ACME health: %s\n  %s\n\n' "$host" "$(printf '=%.0s' {1..58})"

[ -r "$FC" ] || { bad "certificate" "cannot read $FC"; exit 1; }

# --- 1. lifetime elapsed ---
nb=$(openssl x509 -in "$FC" -noout -startdate | cut -d= -f2)
na=$(openssl x509 -in "$FC" -noout -enddate   | cut -d= -f2)
nbs=$(date -d "$nb" +%s 2>/dev/null) || { warn "dates" "BSD date - use gdate"; nbs=0; }
nas=$(date -d "$na" +%s 2>/dev/null || echo 0); now=$(date +%s)
if [ "$nbs" -gt 0 ] && [ "$nas" -gt "$nbs" ]; then
  total=$(( nas - nbs )); used=$(( now - nbs ))
  pct=$(( used * 100 / total )); left=$(( (nas - now) / 86400 ))
  life=$(( total / 86400 ))
  if   [ "$left" -lt 0 ];  then bad  "lifetime" "EXPIRED ${left#-} days ago"
  elif [ "$pct" -ge 95 ];  then bad  "lifetime" "${pct}% elapsed, ${left}d left of ${life}d - renewal is FAILING"
  elif [ "$pct" -ge 80 ];  then warn "lifetime" "${pct}% elapsed, ${left}d left of ${life}d - should have renewed by now"
  else                          ok   "lifetime" "${pct}% elapsed, ${left}d left of ${life}d"
  fi
fi

# --- 2. disk vs wire ---
disk=$(openssl x509 -in "$FC" -noout -fingerprint -sha256)
wire=$(openssl s_client -connect "$host:443" -servername "$host" </dev/null 2>/dev/null \
       | openssl x509 -noout -fingerprint -sha256 2>/dev/null)
if   [ -z "$wire" ];            then bad "disk vs wire" "could not reach $host:443"
elif [ "$disk" = "$wire" ];     then ok  "disk vs wire" "in sync"
else                                 bad "disk vs wire" "MISMATCH - renewed but NOT RELOADED"
fi

# --- 3. challenge path reachable over plain HTTP, not redirected ---
code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 \
       "http://$host/.well-known/acme-challenge/healthcheck-probe" 2>/dev/null)
case "$code" in
  404|403) ok   "challenge path" "reachable over HTTP (got $code - expected, no such token)" ;;
  30[0-9]) bad  "challenge path" "REDIRECTED ($code) - renewal breaks if HTTPS breaks" ;;
  000)     bad  "challenge path" "port 80 unreachable - HTTP-01 cannot work" ;;
  *)       warn "challenge path" "unexpected status $code" ;;
esac

# --- 4. which profile does this look like? ---
cn=$(openssl x509 -in "$FC" -noout -subject | grep -c 'CN *=' || true)
ku=$(openssl x509 -in "$FC" -noout -ext keyUsage 2>/dev/null | grep -ci 'Key Encipherment' || true)
ski=$(openssl x509 -in "$FC" -noout -ext subjectKeyIdentifier 2>/dev/null | grep -c ':' || true)
if [ "${life:-0}" -le 10 ]; then                       ok "profile" "looks like shortlived (~6d)"
elif [ "$cn" -eq 0 ] && [ "$ski" -eq 0 ]; then         ok "profile" "looks like tlsserver (no CN, no SKI)"
else                                                    ok "profile" "looks like classic (CN present)"
fi

# --- 5. CA reachable, ARI advertised ---
ari=$(curl -sS --max-time 10 https://acme-v02.api.letsencrypt.org/directory 2>/dev/null \
      | python3 -c "import json,sys; print(json.load(sys.stdin).get('renewalInfo','none'))" 2>/dev/null)
[ -n "$ari" ] && [ "$ari" != "none" ] && ok "CA directory" "ARI: $ari" \
                                      || warn "CA directory" "unreachable or no ARI"

printf '\n  %s\n\n' "$([ $fail -eq 0 ] && echo 'HEALTHY' || echo 'PROBLEMS FOUND')"
exit $fail

Try it:

bash
chmod +x acme-health
./acme-health /etc/letsencrypt/live/shop.example.com shop.example.com

The five design decisions worth understanding.

1. It reports lifetime as a percentage, not days remaining. This is the key idea. "30 days left" means something completely different on a 90-day certificate (fine) versus a 6-day one (impossible). Percentage elapsed works across every profile, and 80% is the point at which renewal should already have happened — so it flags a broken renewal rather than an imminent expiry. Those are different alerts, and the first one is the useful one.

2. The disk-versus-wire check is Module 08's lesson, and it is the highest-value line here. Renewal succeeding while the service keeps the old certificate is the commonest ACME-adjacent outage, and nothing else detects it.

3. Getting a 404 on the challenge path is the success case. You are probing a token that does not exist, so 404 means "the path is reachable and served over plain HTTP". A 3xx is a failure, because a redirect means renewal depends on HTTPS working — and if HTTPS is broken, you cannot renew your way out of it.

4. Profile detection reads the certificate rather than trusting configuration. No Common Name plus no Subject Key Identifier means tlsserver; a very short lifetime means shortlived. That is the Module 03 field knowledge doing something useful.

5. It distinguishes ❌ from ⚠️. Expired, mismatched, redirected challenge path and unreachable port 80 are failures. Nearing renewal and an unreachable CA are warnings. A check that fails the build for a warning gets disabled by whoever is trying to ship.

Where this goes next: combine with certinfo (Module 03), tlsinfo (Module 06), tlsdiag (Module 07) and tlsdeploy-check (Module 08). Module 13 assembles them into one auditing tool.


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

The single most useful page for this module: Let's Encrypt — Rate Limits. Not because the limits are interesting, but because reading them before you start is what prevents the week-long lockout that ends most ACME debugging sessions badly.

Make it a reflex: before debugging anything against production, switch to staging. --dry-run costs nothing, tests everything, and is the difference between a fixable afternoon and a seven-day wait.

Core reference pages

LinkWhat it is for
RFC 8555 — ACMEThe protocol. §7 is the resource model, §8 is the challenge types
Let's Encrypt — Challenge TypesThe clearest explanation of HTTP-01, DNS-01 and TLS-ALPN-01, with the security caveats
Let's Encrypt — Rate LimitsCurrent numbers. They change — always read the live page rather than a blog post
Let's Encrypt — Certificate Profilesclassic, tlsserver, shortlived — lifetimes and exact field differences
Let's Encrypt — Staging EnvironmentHow to point any client at staging. Read before your first real issuance
certbot documentationEvery flag, the plugin modes, and the hook semantics
RFC 9773 — ACME Renewal Information · Let's Encrypt on ARIHow the CA tells clients when to renew, and why it matters for mass revocation
Let's Encrypt — ACME client optionsThe full list of clients, by language and platform
Let's Encrypt — Integration GuideWritten for people building on ACME at scale. The best single document on doing this properly
cert-manager · acme.sh · legoThe main alternatives to certbot

How to read the ACME directory

plain text
newNonce      <- get a single-use number; every request needs one (anti-replay)
newAccount    <- register, or look up an existing account by key
newOrder      <- "I want a certificate for these names"
renewalInfo   <- ARI: "when should I renew?"          (RFC 9773)
revokeCert    <- revoke
keyChange     <- rotate the account key
meta.profiles <- which certificate shapes are on offer
meta.caaIdentities <- the name this CA looks for in your CAA records (Module 11)
<random key>  <- deliberately there to break clients that hard-code
  1. Fetch it every time. Every URL can change, and the random entry exists to punish assumptions.
  2. meta is where the policy lives — terms of service, profiles, CAA identity.
  3. The absence of a key is meaningful. No renewalInfo means that CA does not support ARI.

The offline alternative

bash
certbot --help all | less                    # every flag, grouped
certbot certificates                         # ⭐ local inventory, no network
certbot show_account                         # which account, which server
man certbot
🧪 Exercise E4.1 — Find the hook flags from the CLI
bash
certbot --help all 2>/dev/null | grep -A3 -E '^\s+--(pre|post|deploy)-hook' | head -20
Expected result — click to reveal
plain text
--pre-hook PRE_HOOK   Command to be run in a shell before obtaining any
                      certificates. Intended primarily for renewal, where it
                      can be used to temporarily shut down a webserver...
--post-hook POST_HOOK Command to be run in a shell after attempting to
                      obtain/renew certificates. Can be used to deploy
                      renewed certificates, or to restart any servers that
                      were stopped by --pre-hook...
--deploy-hook DEPLOY_HOOK
                      Command to be run in a shell once for each
                      successfully issued certificate...

What to read out of this — the difference is in the last four words.

  • --post-hook: "after attempting". It runs whether or not anything was renewed. With a twice-daily timer that is 730 nginx reloads a year, 728 of them pointless.
  • --deploy-hook: "once for each successfully issued certificate". Fires only when a certificate actually changed. This is the one you want, and the wording is the only place the distinction is made clear.
  • --pre-hook is for stopping a service so --standalone can bind port 80. If you use --webroot you do not need it, which is one more reason --webroot is the better default.

💡 --deploy-hook also runs once *per certificate*, so on a host with several certificates it fires several times. If your hook is expensive, make it idempotent — or use --post-hook guarded by a check that something actually changed.


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 is ACME and why does it matter more every year?

A protocol (RFC 8555) for obtaining certificates with no human involved: the client proves domain control via an automated challenge and the CA issues in seconds.

It matters more each year because maximum lifetimes are falling — 200 days in March 2026, 100 in 2027, 47 in 2029. At 47 days manual renewal is eight times a year per certificate. Automation is no longer a convenience.

2. What does the ACME directory do, and why the random entry?

It is one JSON document listing every endpoint URL, so clients discover them rather than hard-coding them. Let's Encrypt inserts a random key deliberately, to break clients that hard-code URLs or choke on unexpected fields — a client that fails on it would fail on the next real addition.

3. What does an ACME account key authorise?

Identity, not authority. It identifies you to the CA and carries your rate limits, but grants no right to any domain — every order still needs a fresh challenge.

A stolen account key cannot get certificates for your domains, but can revoke existing ones and burn your quota. The domain-control proof is the real security boundary.

4. Which challenge would you pick, and why?

HTTP-01 by default — no credentials, simplest failure modes. DNS-01 if you need a wildcard (the only option) or the host is unreachable on port 80. TLS-ALPN-01 only for a TLS-terminating proxy that cannot spare port 80.

DNS-01's cost is DNS API credentials; mitigate with CNAME delegation to a throwaway zone.

5. Why can only DNS-01 issue wildcards?

Because a wildcard covers a whole namespace, and only control of the DNS zone proves control of a namespace. Serving a file on one host proves control of that host and nothing else.

Practical consequence: asking for a wildcard is really asking for DNS API automation, with the broader credential exposure that brings.

6. Why must /.well-known/acme-challenge/ not be redirected to HTTPS?

The CA fetches it over plain HTTP on port 80. If it is redirected and your HTTPS is broken or expired, renewal depends on the very thing that is broken — and you cannot renew your way out of it.

Serve that one path directly over HTTP; redirect everything else.

7. Which Let's Encrypt rate limit catches people, and how do you avoid it?

Duplicate certificates: 5 per week for the same exact set of names. Renewal fails, someone retries against production while debugging, and the sixth attempt locks them out for seven days with the certificate still expiring.

Avoid it by debugging in staging--dry-run / --test-cert. Also relevant at scale: 50 certificates per registered domain per week, shared across all subdomains.

8. --deploy-hook versus --post-hook?

--deploy-hook runs only when a certificate was actually issued. --post-hook runs after every attempt, so a twice-daily timer reloads nginx 730 times a year for nothing.

Use --deploy-hook, and make sure it reloads — a renewal without a reload is the Module 08 outage.

9. Why check for renewal twice a day when certificates last 90 days?

Because certbot only renews within 30 days of expiry, so most runs do nothing — the point is retries. Twice daily over 60 days gives roughly 120 chances, so a transient DNS or network failure is a non-event.

Automation beats a calendar reminder not because it is cleverer but because it retries.

10. What is ARI and what problem does it solve?

ACME Renewal Information, RFC 9773 (September 2025). The CA returns a suggestedWindow telling the client when to renew, instead of the client guessing.

It spreads renewal load, adapts automatically when lifetimes change, and — most importantly — gives the CA a channel to pull renewal forward across its entire population during a mass-revocation event, with no configuration change anywhere.

11. What are the three Let's Encrypt profiles?

classic (90 days, default, includes Common Name and legacy fields, up to 100 names), tlsserver (45 days, no Common Name, no keyEncipherment, no Subject Key ID, 25 names), and shortlived (~6 days).

Every field tlsserver drops is one modern TLS made obsolete. shortlived qualifies as a Short-Lived Subscriber Certificate, requiring no revocation mechanism at all.

12. Would you adopt 6-day certificates?

Not until the pipeline has proved itself over months. It means roughly 60 renewals a year per certificate, and any failure becomes an outage in days rather than months.

Prerequisites first: monitoring on the live endpoint, deploy hooks that reload, and alerting on renewal failure rather than expiry. Then shorten. Adopting it early converts a rare risk into a frequent outage.


E6 · Command reference — everything from this module

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

Explore the CA, safely

bash
curl -sS https://acme-v02.api.letsencrypt.org/directory | python3 -m json.tool     # ⭐ production
curl -sS https://acme-staging-v02.api.letsencrypt.org/directory | python3 -m json.tool  # ⭐ staging
curl -sS https://acme-v02.api.letsencrypt.org/directory \
  | python3 -c "import json,sys; print(json.load(sys.stdin)['renewalInfo'])"       # ARI endpoint

Test without spending quota

bash
certbot certonly --dry-run --webroot -w /var/www/html -d example.com     # ⭐ ALWAYS do this first
certbot renew --dry-run                                                  # ⭐ test the whole renewal path
certbot certonly --test-cert --webroot -w /var/www/html -d example.com    # staging, keeps the cert

Issue for real

bash
certbot certonly --webroot -w /var/www/html -d example.com -d www.example.com   # ⭐ safest mode
certbot certonly --standalone -d example.com                       # its own listener; stop nginx first
certbot certonly --dns-cloudflare --dns-cloudflare-credentials ~/.secrets/cf.ini \
  -d '*.example.com' -d example.com                                # ⭐ wildcard needs DNS-01
certbot certonly --webroot -w /var/www/html -d example.com --preferred-profile tlsserver   # 45-day profile

Inspect what is managed

bash
certbot certificates                    # ⭐ local inventory: names, expiry, paths
certbot show_account                    # which account and which ACME server
sudo ls -l /etc/letsencrypt/live/*/     # ⭐ the symlinks your config should point at
sudo find /etc/letsencrypt/accounts -name private_key.json    # the ACCOUNT key
systemctl list-timers | grep certbot    # ⭐ is renewal actually scheduled?

Renew properly

bash
certbot renew --deploy-hook "systemctl reload nginx"        # ⭐ fires ONLY on real renewal
certbot renew --force-renewal                               # ⚠️ ignores the 30-day rule; burns quota
certbot renew --cert-name shop.example.com                  # just one certificate

Verify the result

bash
diff <(openssl s_client -connect h:443 -servername h </dev/null 2>/dev/null \
       | openssl x509 -noout -fingerprint -sha256) \
     <(openssl x509 -in /etc/letsencrypt/live/h/fullchain.pem -noout -fingerprint -sha256)   # ⭐ reloaded?
curl -sS -o /dev/null -w '%{http_code}\n' http://h/.well-known/acme-challenge/probe          # ⭐ 404 = good, 3xx = BAD
dig +short TXT _acme-challenge.example.com                                                    # DNS-01 record
The ACME pre-flight, before you ever touch production:
bash
certbot certonly --dry-run --webroot -w /var/www/html -d example.com    # 1. does the whole flow work?
curl -sS -o /dev/null -w '%{http_code}\n' http://example.com/.well-known/acme-challenge/probe   # 2. 404, not 3xx
systemctl list-timers | grep certbot                                    # 3. is renewal scheduled?
grep -r deploy-hook /etc/letsencrypt/renewal/                           # 4. will it RELOAD?

Does it work · is the challenge path clear · will it run again · will the service pick it up. Four checks, no quota spent, and between them they prevent nearly every ACME failure that reaches production.


Next — Module 11 · Certificate Transparency, CAA & the Public Trust Ecosystem.

Two things in this module pointed forward. The directory advertised caaIdentities, and Module 09's CRLite depended on Certificate Transparency to be complete. Module 11 covers both properly: how CT logs work and what SCTs actually prove, how to monitor the logs for certificates issued for your domains by CAs you have never used, how CAA records tell CAs who is allowed to issue for you, what DV, OV and EV really verify, and the mis-issuance incidents that got real CAs distrusted.

Official reading ahead of it: RFC 6962 — Certificate Transparency and RFC 8659 — DNS CAA Resource Record.

📚 Sources for the interview questions

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

The ACME directory output in Exercise A1.1 is the live production directory, fetched from acme-v02.api.letsencrypt.org while writing this module — including the deliberate random entry (PeiDSJ6MPvc), the renewalInfo ARI endpoint, and the three advertised profiles. The rate limits in Part C2 were read from Let's Encrypt's live rate-limits page rather than recalled, and the profile lifetimes and field differences from the profiles documentation.

Standards and current status were verified against primary sources: RFC 8555 for the protocol, Let's Encrypt's challenge-types documentation for the port and wildcard constraints of each challenge, RFC 9773 and Let's Encrypt's ARI announcement for renewal information, and the certbot documentation for hook semantics.

certbot itself was not run for this module — the sandbox has no public DNS name and could not complete a challenge — so the certbot outputs are documentation-verified rather than execution-verified. The curl-based directory and rate-limit exercises are live and reproducible.

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.