Documentation embedded in this build.
Revocation, and how to need it less
Two things have to be true before a revocation means anything. Somebody has to be able to publish it, and every verifier has to actually consult it. Almost all of the difficulty lives in the second one, and it is the half that is easiest to ship without noticing you have not: a deployment where nothing is revoked and a deployment where revocation is never checked look identical from the outside.
This page covers both halves, and then the part that matters more than either — the certificate profile whose purpose is to make revocation something you reach for rarely. The framing is deliberate: revocation you rarely need beats revocation you cannot check. Every claim below names how you would check it, because a claim you cannot test is worth what you paid for reading it.
Checking is required, and an unreadable source denies
Revocation lists are checked at the handshake, on by default, and they fail closed. A revocation source that cannot be read is not evidence that there are no revocations; it is a reason to stop admitting certificates until it can be read again.
- Required unless you turn it off, and turning it off is a recorded weakening.
pki.crl.requiredefaults to true, and it defaults to true when it is unset rather than only when it is written down — the deployment that never thought about revocation gets the checking anyway. Switching it off is a registered tier-2 departure, so it appears in the deployment's own record of what it has weakened. Check it by starting a deployment with an emptypki.crl.dir: the daemon warns that "no CRLs loaded but revocation checking is required; every client certificate will be denied until a CRL is imported", and then denies them. - Every certificate in the presented chain, not just the leaf. A revoked intermediate invalidates everything beneath it, so each link is looked up against the list held for the specific issuing key that signed it — the physical certificate, not merely a name that matches. Check it by revoking an intermediate and watching leaves under it stop working.
- A missing list denies. With no list held for an issuer, the handshake is refused with "no CRL held for issuer ...; cannot establish that the certificate is not revoked". Check it by moving the list out of the directory and reconnecting; the connection must fail, not succeed.
- A stale list denies too. Past its own next-update time the refusal says it is "refusing to treat an unreadable revocation source as no revocations"; past
pki.crl.max_age(24h by default) it says the list "is ... old, past the freshness budget". That budget is the honest name for the window in which a revocation may not yet be visible to this verifier — shortening it shortens the window and shortens how long you may go between imports. Check it with a list you deliberately let age. - You are told before it starts denying. At
pki.crl.warn_fractionof the budget (0.75 by default) the authenticated health surface raisescrl_aging, naming the issuer, well beforecrl_stalereaches readiness and handshakes begin failing;crl_absentcovers the case where checking is required and nothing is held at all. Both numbers are exported asauthbox_crl_age_secondsandauthbox_crl_budget_seconds, so the alert you want is a ratio between two gauges you already scrape, andauthbox_handshake_totalis where the denials surface.
The same picture is available from the command line, without waiting for a handshake to fail:
authboxctl crl status -dir /var/lib/authbox/crl -anchors /etc/authbox/ca.pem
The online answer, fed by the issuance ledger
Lists are the airgap-friendly baseline. Where a network exists, this deployment also answers OCSP, and it answers it as the CA rather than as a bystander: it knows what it signed and what it revoked.
- Four answers, not three. good, revoked, never-issued, and unknown are distinguished on purpose: a responder holding its own issuance history that still says "unknown" about a serial it never issued is telling a smaller truth than it knows, and clients widely soft-fail unknown into acceptance.
- It answers where the asking happens. The responder is mounted at
/ocspon the unauthenticated enrolment listener, because a caller asking whether a certificate is revoked frequently does not hold a valid one — that is often exactly why they are asking. Check it by querying with no client certificate at all. - The signing key is a delegated one, and the CA key stays cold.
ocsp.signer_idnames a dedicated key whose certificate carries the OCSPSigning extended key usage and id-pkix-ocsp-nocheck. A key asked to sign on every status request is a hot key, and the point of an offline CA key is that it is not one, so naming the issuing key here is refused rather than discouraged. Mint the signer with:
authboxctl ocsp issue-signer -anchors ca.pem -out ocsp-signer.pem \
-keystore /var/lib/authbox/keys -passphrase-file pass -signer-key-id ocsp-signer
- The answers come from the issuance record, live. At boot the responder's ledger is seeded from the revocations on the CRLs it already holds plus every row of the issuance ledger, and the issuance hook feeds it afterwards — a certificate signed a second ago already answers good, with no restart and no reload step to forget. Check it by enrolling and immediately querying the new serial.
ocsp.issuance_completeis the honest switch, and it is off by default. Turn it on and a serial this CA never issued answers revoked instead of unknown, which is what stops a forged certificate carrying an invented serial from being soft-failed into acceptance. It is off by default because a deployment whose ledger does not cover certificates it issued before the ledger existed would answer revoked for perfectly good ones. It is therefore a claim you make about the completeness of your own record, not a preference — and the deployment says so at startup while it is off.- Neither source can un-revoke. If either the list or the responder says revoked, the certificate is revoked; otherwise it is not, provided at least one of them is fresh enough to have an opinion. There is no precedence table, because a good OCSP answer overturning a CRL entry is the only direction in which being wrong lets somebody in. Clients on the other side get
ocsp_client.responders[].urlandocsp_client.freshness, and a stale responder shows up asocsp_stale.
Requesting a revocation, when nothing online may sign one
The signing key is offline, and it stays offline. What is online is the queue: a place for "please revoke serial N" to live that is not somebody's memory or a message thread.
- The request is an intent, and it says so. Filing one signs nothing and revokes nothing; the certificate keeps working until a signed list covering it reaches the deployment. Check it by filing a request and watching the certificate go on authenticating.
- Two states, and the second is reached by evidence. A request is pending until a loaded list covers its serial, at which point it is fulfilled — the CRL is the fulfilment. There is deliberately no import verb to forget, because a queue closed by a separate manual step is a queue that stays open after the work is done. Check it by importing a list covering a pending serial and reading the queue back.
- The same row however it arrives.
authboxctl revoke requestfiles one and gets 202; the console's "Revocation requests" page shows the queue; and an ACME client callingPOST /acme/revoke-certfiles the same row with its account as the requester. Filing one takes the administration mission plus an audit-scoped grant on top of it; reading the queue takes only read. - The queue feeds the offline signing directly. Save it, carry it to the CA host, and every pending serial joins the next list.
authboxctl revoke request 284837219194 -reason "laptop stolen"
authboxctl get revocations > revocations.json
Then, on the host where the key lives:
authboxctl crl generate -anchors ca.pem -dir crl-out \
-keystore /var/lib/authbox/keys -passphrase-file pass \
-from-requests revocations.json
Copy the result back with authboxctl crl import, and the pending rows it covers close themselves. The whole loop is written out in CRL distribution.
Proving it to somebody who does not trust you
A revocation story that only you can see is an operational tool. This one is also evidence.
- Every request is an audited decision, attributed to the authenticated requester. The DN recorded is the one that authenticated, not one the request body claimed.
- The audit chain leaves the deployment signed.
authboxctl audit exportships sealed segments to a SIEM, andauthboxctl audit verify-exportchecks them on a machine that has never spoken to this deployment, against a public key. See Signed audit export. - Issuance itself is provable. With the issuance log on (
log.enabled,log.path,log.origin,log.signer_id), every certificate this deployment signed is a leaf of a Merkle tree whose checkpoint is signed by a dedicated key.authboxctl log proveproduces an inclusion proof for a serial andauthboxctl log checkverifies it offline, from the tiles alone, with no network. - The two records are stitched together.
log.audit_pathis a second tree over the audit chain'ssegment_sealrecords, so an exported segment carries its own inclusion proof rather than asking a reviewer to trust that nothing was removed.
The point for revocation specifically: "this certificate was issued, then" and "this revocation was requested, by whom, and when" are both demonstrable to somebody with no reason to believe you.
Needing it less: the short-lived profile
Everything above is the machinery for a credential that outlives its usefulness. The better answer for a service is a credential that does not.
The short-lived issuance profile gives leaves hours of validity — 12h by default, with a floor of 1h, below which ordinary clock skew starts denying honest peers. It is mintable only through an invitation whose subject the deployment already permits to renew itself, carrying the autorenew attribute described in Living with your certificate. The renewal window opens at the leaf's half-life, and each renewal is a possession-authenticated call to POST /enroll/renew — ledgered, logged, and OCSP-answerable like any other issuance. Every leaf is still writer-issued; nothing signs locally, so "who issued this" stays a one-hop question.
What that buys is a different withdrawal mechanism. Taking away self-renewal is deleting one attribute; it takes effect at the next request, there is no list to sign, carry, and distribute, and the exposure left behind is the hours-long tail of the current leaf rather than the months-long tail of a long-lived one. If you need that tail closed too, the CRL is still there and still works. And withdrawing possession — a stolen key, a compromised host — still means revocation, because a holder who kept a copy of the key does not need your permission to go on using it. Nothing about short validity is a substitute for that.
The cost is real, and it is a chosen trade rather than an oversight: a writer outage longer than the leaf validity takes the fleet down with it. A fleet renewing at a six-hour half-life has that much tolerance for the writer being unreachable. Inside an enclave, where the writer is always reachable and shares a failure domain with the services renewing against it, that is an acceptable bargain; across a link you do not control it is a bad one. Choose it with that sentence in front of you, and set the validity against how long you would be willing to be down.
Not the same question: suspending a person
Most of what an organization actually does on a Tuesday is not "this key is compromised". It is "this person left", "this person is on leave", "this account is paused pending an investigation" — and reaching for revocation there answers a question nobody asked.
Suspension is the state for that, and it is the opposite of revocation on every axis that matters:
| Revocation | Suspension | |
|---|---|---|
| What it is about | one certificate | one entity — the person or service |
| Reversible | no, and never was | yes, by a signed reinstatement |
| Takes effect | when a signed list reaches each relying party | at the next request, everywhere |
| Needs an offline signature | yes, the CRL | no |
| Carries a justification | a free-text request field | required, structurally |
| Preserves standing | irrelevant — it is about the key | yes: memberships, attributes, grants |
authboxctl entity suspend "CN=…" -reason "separated 2026-08-30, HR case 4411" pauses the account. Everything it held stays exactly where it was: memberships, attributes, access-list standing, and brokerage grants, which are frozen rather than voided — no approver's record is rewritten, and each one authorizes again the moment the pause lifts. That preservation is the whole reason the state exists instead of deleting the entity, which destroys the record of who had access at the moment it removes the access.
Certificates are untouched by default, and the short-lived argument above is why. A suspended subject cannot renew — the refusal sits where every identity is resolved, and /enroll/renew is one of the doors it covers — so under the short-lived profile the suspension is self-cleaning within one validity period: the current leaf expires on its own schedule and no successor is ever issued. No list to sign, carry and distribute, for a withdrawal that was never about the key.
Where you do want the certificates gone as well, -request-revocations opts in. It files one revocation request per still-live serial onto the queue an operator signs offline — filed, not fulfilled, exactly like every other revocation intent in this system. Nothing is revoked by the suspension itself, and the certificates keep working until a signed list covering them arrives.
Two things suspension is not. It is not the deny list: identity.deny_list is the local, immediate, file-authored, survives-every-bundle break-glass override, and it stays that. Suspension lives with the record's owner and syncs with the record, so an upstream's suspension arrives at every downstream in the ordinary authority bundle, and a local suspension of an upstream-owned record is refused rather than silently reverted by the next one. And it is not a remedy for possession compromise: a holder who kept a copy of the key does not need an account to be active to use it against somebody who is not asking this deployment. A stolen key is still a revocation.
One thing to know before you write the reason: the suspended person can read it. It is shown to them, and only to them, on the self-disclosure page they get when their own credential is refused — see Troubleshooting: forbidden. That is deliberate: one honestly-labelled reason field, rather than a public/private pair inviting a second and unaccountable channel. Write it knowing who reads it.
Two tiers, and which one a credential belongs in
- Long-lived credentials for people, and for anything that cannot renew unattended. A laptop that is closed for a week cannot renew on a half-life schedule, and a person losing a key is exactly the event revocation exists for. Here revocation is the tool, and the pages above are the ones that matter.
- Short-lived credentials for unattended services. A process that is always running and always able to reach the writer should hold a certificate that expires faster than anyone could get around to revoking it. Here expiry is the tool, and revocation is the backstop you keep because possession compromise does not respect your renewal schedule.
Most deployments run both, and that is the intended shape rather than a migration state. The question to ask about an identity is not "how long should this certificate last" but "if I needed this credential to stop working within the hour, what would I actually do" — then pick the tier whose answer you can live with.
Where to look next
- Living with your certificate — renewal, expiry, and what to do the moment you think a key is lost, from the holder's side.
- Leaving the public CA — why the client half of your PKI is moving in-house, and what a bare private CA leaves you to solve.
- CRL distribution — the runbook: generating, carrying, importing, and scheduling lists so they never go stale.
- Signed audit export — shipping the decision record somewhere it can be verified without trusting the deployment that wrote it.