Status: Draft (2026-10-26), empirical data applied. Empirical measurements were obtained on 2026-10-26 from the TT Lab cluster (cert-manager 1.21 series) via read-only operations .
kubectl get·logs, reading the certificate's dates from the Secret viaopenssl x509 -noout -dates, and probing the public address withopenssl s_client. Public names and internal object names have been masked (<공개 이름 A>,<클러스터 이슈어>). Official documentation and RFCs were verified by opening the original texts on the same day. Writing rules: Empirical data comes first. Unmeasured items are marked as "Unmeasured", unverified items as "Unverified", and guesses as "Estimated".
Table of Contents
- 1. What happens when a human renews a certificate
- 2. Five objects and the controllers driving them
- 3. ACME: The protocol for proving "this name is truly yours"
- 4. Who signs: SelfSigned, CA, ACME
- 5. When to renew
- 6. Webhook and cainjector: Why are there three components?
- 7. How to integrate with Gateways and Ingresses: shim
- 8. Empirical measurements: Read from the TT Lab cluster
- 9. Common failures and troubleshooting order
- 10. Conclusion
- References
1. What happens when a human renews a certificate
Let's Encrypt certificates are valid for 90 days. This means someone has to check the calendar and run a renewal command every three months, and that someone will eventually forget. When a certificate expires, browsers show warnings, and services communicating via TLS break entirely. Relying on human memory is guaranteed to fail at least once.
cert-manager solves this problem in a Kubernetes-native way. Humans only write down the desired state—that "such a certificate must always exist in this Secret"—as an object. The rest—generating keys, requesting from a certificate authority (CA), proving domain ownership, writing to the Secret, and fetching a new one before expiration—is continuously handled by controllers. This is the exact same principle as writing a Deployment, which creates pods and restarts them if they die.
This post unwraps that "matching process" layer by layer. After looking at which objects are created in what order (Section 2), what is exchanged with the ACME server (Section 3), who signs (Section 4), when renewals happen (Section 5), why webhooks and cainjectors exist separately (Section 6), and how it integrates with gateways (Section 7), we conclude with the actual state read from the TT Lab cluster, an incident (Section 8), and common failures (Section 9).
2. Five objects and the controllers driving them
cert-manager has six CRDs. Among them, humans directly use two (Issuers and Certificates), while the rest are intermediate objects created by controllers to divide the work.
| Object | Scope | Creator | Contents |
|---|---|---|---|
Issuer / ClusterIssuer |
Namespace / Cluster | Human | Who signs: ACME (Let's Encrypt, etc.), CA, SelfSigned, Vault, external issuers |
Certificate |
Namespace | Human or shim | Desired certificate: list of names, issuer, Secret name, lifetime/renewal timing, key type |
CertificateRequest |
Namespace | Controller | A single CSR. Assigned a revision number and signed after approval |
Order |
Namespace | Controller (ACME only) | An order from the ACME server and its URL |
Challenge |
Namespace | Controller (ACME only) | A single ownership proof for a single name |
The shortest usage example looks like this: a ClusterIssuer that proves via HTTP-01 to Let's Encrypt and opens the proof path using Gateway API's HTTPRoute, and a Certificate that requests a certificate using that issuer.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata: { name: <클러스터 이슈어> }
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef: { name: <ACME 계정 키 Secret> } # 계정 키는 여기 한 번 만들어 계속 쓴다
solvers:
- http01:
gatewayHTTPRoute:
parentRefs: [{ kind: Gateway, name: <게이트웨이>, namespace: <네임스페이스> }]
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata: { name: <인증서> }
spec:
secretName: <인증서 Secret> # 결과가 쓰일 자리
dnsNames: [<공개 이름 A>]
issuerRef: { kind: ClusterIssuer, name: <클러스터 이슈어> }
State machine: Who decides "should we issue now"
A Certificate's state is read through two conditions. Ready means the Secret exists, is not expired, its key matches, and its names match the spec. Issuing is an indicator meaning "currently being re-acquired", which is attached when needed and removed by the issuing controller once issuance completes (official API documentation).
Controllers are not a single monolithic block, but multiple components divided by task. Looking at them in order:
- trigger: Compares the Certificate and Secret, and if it determines that "it must be re-acquired", attaches
Issuing=True. There were two reasons recorded in empirical logs:DoesNotExistwhen the Secret doesn't exist at all, andSecretMismatchwhen the Secret's private key differs from the current request (Section 8). Issuance also triggers from this exact place when the renewal time has passed. - keymanager: First creates the private key to be used for the next certificate in a temporary Secret . The Certificate's
status.nextPrivateKeySecretNameis that name, and in empirical measurements, a name with a random suffix likecert-manager.io/private-key-secret-nameremained in the CertificateRequest annotation<인증서>-xxxxx. The reason the key is not written directly to the Secret is to avoid breaking the currently used key-certificate pair if signing fails. Starting from v1.18, the key rotation policy default changed fromNevertoAlways, generating a new key on every renewal (1.18 Release Notes). - requestmanager: Creates a CSR using that key to generate a CertificateRequest. The number at the end of the name is the revision (
<인증서>-1,-2, …). - approver: Attaches
Approvedto the CertificateRequest. The default approver is cert-manager itself, which can be replaced with policy tools (approver-policy). No issuer signs without approval. - Issuer-specific signers: SelfSigned and CA sign on the spot. ACME creates an Order and waits for the result. A CertificateRequest does not retry once it fails; retries are handled by the Certificate side with a new revision (official documentation).
- issuing: Pairs the certificate from the Ready CertificateRequest with the key from the temporary Secret, writes them to the real Secret, and removes
Issuing. - revisionmanager: Deletes old CertificateRequests. Since v1.18, the
revisionHistoryLimitdefault is 1, leaving only the last one (in empirical measurements, there was also only one CR per certificate).
Because all controllers attempt to modify the same object simultaneously, logs reading "Retrying with optimistic lock" are commonly printed. Even during a single empirical issuance, such lines appeared six times and all were retried immediately without issues. This is normal behavior for Kubernetes controllers, so there is no need to worry.
3. ACME: The protocol for proving "this name is truly yours"
Public CAs like Let's Encrypt do not verify humans. What they verify is just one thing: whether you can actually control the name . The protocol defining this procedure is ACME (RFC 8555).
Accounts, orders, authorizations, and challenges
- Account. The client creates an account key (asymmetric key) and registers it with the ACME server. All subsequent requests are JWS signed with this key. In cert-manager, the ClusterIssuer's
privateKeySecretRefis that key, and the issuer'sReadycondition means "account registered" (empirical:The ACME account was registered with the ACME server). - Order. A request stating "I want a certificate for these names." According to RFC 8555 §7.1.6, an order is born as
pending, becomesvalidwhen all authorizations becomeready, becomesprocessingwhen a CSR is submitted, and becomesvalidwhen issued. If any step fails, it becomesinvalid. - Authorization and challenge. There is one authorization object per name, and the server presents multiple ways (challenges) to acquire that authorization. In the empirical order, three types of
http-01·dns-01·tls-alpn-01were presented with the same token, and cert-manager picked only the solving method specified in the issuer (HTTP-01 here). The methods used by cert-manager are HTTP-01 and DNS-01. - Finalize and download. Once all authorizations are valid, the CSR is sent to the
finalizeURL (§7.4), and the issued certificate chain is fetched (§7.4.2).
Key authorization: What does a single token prove?
It is not enough to simply host the server-provided token on the web. If that were the case, a third party who snooped on the token could do the same thing. RFC 8555 §8.1 requires appending the account key thumbprintto the token.
keyAuthorization = token + "." + base64url(SHA-256(계정 공개키 JWK 의 정규형)) # 지문은 RFC 7638
HTTP-01: http://<이름>/.well-known/acme-challenge/<token> 이 keyAuthorization 을 돌려준다 (§8.3)
DNS-01 : _acme-challenge.<이름> 의 TXT = base64url(SHA-256(keyAuthorization)) (§8.4)
We verified whether it is calculated this way using Python's standard library. The input for the thumbprint is the example RSA public key from RFC 7638 §3.1, and the token is a value shaped like the example in RFC 8555.
import base64, hashlib, json
b64u = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=").decode()
jwk = {"e": "AQAB", "kty": "RSA", "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWh…"} # RFC 7638 예제(줄임)
thumb = b64u(hashlib.sha256(json.dumps(jwk, separators=(",", ":"), sort_keys=True).encode()).digest())
ka = "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA" + "." + thumb
print(thumb, b64u(hashlib.sha256(ka.encode()).digest()))
Result (2026-10-26, local python3): The thumbprint was NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs , which matched the expected value documented in RFC 7638, and the DNS-01 TXT value for that token was ZTRx1Ckl1-tM05o5zaizTTA0yUy5AGereMgSNWC6Ll8 . Because the thumbprint originates from the account key, only the party holding the account key can generate a valid response.
HTTP-01 vs DNS-01, what to choose
| HTTP-01 | DNS-01 | |
|---|---|---|
| Proof location | Port 80 /.well-known/acme-challenge/ |
_acme-challenge.<이름> TXT record |
| Must be reachable from outside | Yes. Via port 80 from the internet | No. Only DNS needs to be public |
Wildcard (*.example.com) |
Not supported | Supported |
| Permissions the cluster must have | Permission to open a path (solver pod, route) | DNS provider API key (often capable of modifying the entire zone) |
| Common pitfalls | Firewalls, redirects, cluster-internal hairpins | Propagation delay, CNAME delegation settings |
Wildcards and port 80 conditions were verified in the official Let's Encrypt documentation (challenge types). Because a DNS-01 API key means a key that can modify the entire DNS zone is brought inside the cluster, the security cost is high. If there are only a few public names and port 80 is open, HTTP-01 is simpler.
How cert-manager solves HTTP-01
The Challenge controller creates a solver pod(acmesolver image), a Service, and a route to open that path (an Ingress or Gateway API HTTPRoute). Then, instead of immediately notifying the ACME server that it is "ready," it performs a self check . It attempts the GET directly from within the cluster using that public name, and only notifies the server when the correct key authorization is returned. This is a mechanism to avoid accumulating failures on the ACME server, as validation failures are limited to 5 per name/account per hour (Let's Encrypt rate limit). Conversely, if the path exiting and re-entering the cluster via its own public address (hairpinning) is blocked, the self check will never pass. We will revisit this in Section 9.
4. Who signs: SelfSigned, CA, ACME
Public CAs are not the only issuers. For certificates where only our own clients need to trust them , such as a webhook server inside the cluster, a private CA is sufficient.
- SelfSigned: Signs its own certificate with its own key. Since the recipient has only the certificate itself as a basis for trust, this is usually used for initially creating a CA.
- CA: Signs using the CA key contained in a Secret. The bootstrap shape recommended by the official documentation is "create a root Certificate marked
isCA: truewith a SelfSigned ClusterIssuer, and have the CA Issuer use that Secret."
kind: ClusterIssuer
metadata: { name: bootstrap }
spec: { selfSigned: {} }
---
kind: Certificate
metadata: { name: root-ca }
spec:
isCA: true
commonName: example-root-ca # 기본은 빈 subject 이니 이름을 준다(공식 문서의 경고)
secretName: root-ca
issuerRef: { kind: ClusterIssuer, name: bootstrap }
---
kind: ClusterIssuer
metadata: { name: internal-ca }
spec: { ca: { secretName: root-ca } }
The real difficulty of a private CA is not signing, but trust distribution. You must add the root certificate to the trust list of every client and ensure that when the root is changed, both the old and new roots are trusted simultaneously. The official documentation introduces trust-manager for this purpose, and warns that an operational PKI requires rotation, distribution, and recovery plans. There is also the csi-driver, which mounts certificates directly into pods without Secrets.
5. When to renew
spec.duration default is 90 days, and the default spec.renewBefore is 1/3 of the lifespan . In other words, if nothing is specified, renewal occurs at the 2/3 mark of the lifespan. Since v1.16, there is also renewBeforePercentage specified as a ratio (official documentation, 1.16 release notes). When the CA determines the lifespan, such as with ACME, it is calculated using the notBefore and notAfter of the actually received certificate , rather than the requested duration. The measured values show this directly.
| Certificate (read on 2026-10-26) | notBefore → notAfter | Lifespan | renewalTime | Time remaining until expiration |
|---|---|---|---|---|
| Public Certificate A (Let's Encrypt, no configuration) | 09-09 12:09:29 → 12-08 12:09:28 | 90 days − 1 second | 11-08 12:09:28 | 30 days (1/3) |
| Public Certificate B (Let's Encrypt, no configuration) | 10-08 23:15:09 → 01-06 23:15:08 | 90 days − 1 second | 12-07 23:15:08 | 30 days (1/3) |
Self-signed (duration: 2160h·renewBefore: 360h with the value written) |
08-23 08:45:34 → 11-21 08:45:34 | 90 days | 11-06 08:45:34 | 15 days |
| Self-signed (for webhook, no configuration) | 10-18 → 01-16 | 90 days | 12-17 | 30 days (1/3) |
All times are in UTC, and the dates when kubectl get certificate -o custom-columns=… 's status.notBefore·notAfter·renewalTime and Secret's tls.crt were read as openssl x509 -noout -startdate -enddate matched. Calculating this, the renewalTime of the public certificate is precisely notAfter − 30일 .
Changes on the Public CA Side
Let's Encrypt announced its lifespan reduction schedule on 2025-12-02. The default profile will become 64 days on 2027-02-10 and 45 days on 2028-02-16, and the authorization reuse period will be reduced from 30 days to 10 days, and then down to 7 hours. The announcement asserts that fixed-interval renewal methods like "every 60 days" are no longer sufficient. While cert-manager's default value of renewing by ratio (2/3) follows this change naturally, places where a fixed value like renewBefore: 720h is written will see their renewal margin reduced to 15 days for a 45-day certificate. fixed value is written will see their renewal margin reduced to 15 days for a 45-day certificate. There is also ARI (RFC 9773) where the CA tells you "when to renew." cert-manager introduced this experimentally behind the ACMEUseARI feature gate in v1.21, and it is disabled by default.
6. Webhook and cainjector: Why Three Components
Installing cert-manager creates three Deployments. In the TT Lab cluster as well, the controller, webhook, and cainjector were running one by one.
- controller: All the controllers from Section 2 reside within this process.
- webhook: A server called by the API server before it saves cert-manager objects. The official documentation lists three jobs: spec validation (preventing invalid Certificates from being saved at all), filling in default values, and conversion between API versions. Since the webhook talks to the API server via TLS, The webhook itself requires a certificate.To avoid the chicken-and-egg problem, the webhook does not use cert-manager's issuance path; instead, it generates its own CA and places it in a Secret (measured: that CA is valid for one year starting from 2026-08-19).
- cainjector: Fills in the
caBundlefield of webhook configurations (Validating/MutatingWebhookConfiguration), CRD conversion webhooks, and APIService objects. When a target object is annotated withcert-manager.io/inject-ca-from: <네임스페이스>/<Certificate>, it reads and injects the CA from that Certificate, and reinjects it whenever the certificate changes. In the TT Lab cluster as well, the validation/mutation webhook configurations and CRDs of several components were receiving their CAs via this annotation.
Without cainjector, on the day the webhook server's certificate is renewed, the API server fails to trust the new certificate, causing all object creations of that kind to fail. This is why webhook certificate renewal easily becomes a silent failure.
7. Integrating with Gateways and Ingresses: shim
Instead of writing Certificates manually, you can simply add a single annotation to the gateway side.
- ingress-shim:
cert-manager.io/cluster-issuer(orissuer) watches Ingresses with the annotation and creates a Certificate for eachtls.secretName. - gateway-smh: Watches Gateways with the same annotation and creates a Certificate using the HTTPS listener's
hostnameandcertificateRefs. This feature is disabled by default. It became beta in v1.15 and was enabled via--enable-gateway-api, and in v1.21 it is enabled viagatewayAPI.enabledin the configuration file (the old nameenableGatewayAPIstill works but is deprecated). I could not find official documentation stating it has reached GA.
The controller configuration file for the TT Lab cluster contained the old name enableGatewayAPI: true , and the Gateway had the cert-manager.io/cluster-issuer: <클러스터 이슈어> annotation. Reading the results clearly reveals the nature of the shim.
- The Certificate for public certificate A had
ownerReferencesas its Gateway . This means it is an object created by the shim, and it will be deleted along with the Gateway if the Gateway is removed. - Public certificate B was a Certificate manually created by a person using
kubectl apply. Since the same Gateway had a listener for B, the shim attempted to create a Certificate with that Secret name as well, but during issuance, it logged "Certificates without an owner are not modified" and left it untouched (observed in practice, 10 lines of logs from 2026-10-09). This is a safety measure to prevent the shim from overwriting something created by a human. However, since this is a signal that a single name is being managed through two paths, it is better to consolidate it to one side.
When do gateways notice certificate changes?
Gateways (Envoy-based) watch Secrets and reload them when they change. They do not restart the proxy. The observed path is as follows: The Cilium gateway in the TT Lab cluster copies the referenced TLS Secret into its dedicated namespace as a hash-named copy. The copy of public certificate A was created on 2026-08-19 (first issuance), but inside it was the certificate re-issued on 2026-09-09. It was renewed in place without creating a new copy. When connecting from the outside via openssl s_client , the notAfter of the certificate served by the public address was also identical to Dec 8 12:09:28 2026 .
On the other hand, applications that mount a Secret as a file and read it only once at startup are different. The official documentation notes that such apps will not immediately pick up new certificates, and kubectl rollout restart recommends tools like Reloader. This is the most common pitfall in Section 9.
8. Measurement: Observations from the TT Lab Cluster
Environment: cert-manager v1.21 series (Helm chart, 1 controller, 1 webhook, 1 cainjector). Observed on 2026-10-26, all times in UTC.
| Item | Value |
|---|---|
| Issuer | ClusterIssuer for public certificates (ACME, HTTP-01, Gateway API solver) and SelfSigned Issuers for each component |
| Certificate | A few public certificates and several self-signed certificates for webhooks and internal TLS |
| Order and Challenge | Orders leave the last one for each public certificate as valid , and there are no Challenges — they are deleted upon success |
| Public certificate key | RSA 2048, signature sha256WithRSA. Issuer is Let's Encrypt intermediate CA |
When initially requesting a new name: 6 minutes and 5 seconds
Controller logs from the day public certificate B was first created (2026-10-09) remained in the previous container log (kubectl logs --previous). Sorting the lines chronologically yields the following:
| Time | Event |
|---|---|
| 00:07:36.141 | trigger: Certificate must be re-issued, reason DoesNotExist(Secret does not exist) → Issuing |
| 00:07:36.332 | CertificateRequest revision 1 created,Approved |
| 00:07:38.209 | solver pod created |
| 00:07:38.274 | HTTPRoute created for solver (with Gateway as parent) |
| 00:07:48 – 00:13:28 | Repeated checks for solver pod, Service, and HTTPRoute (initial 10-second interval, later 20-second interval, 23 times) |
| 00:13:41.037 | certificate issued, CR Ready |
| 00:13:41.075 | Certificate Ready=True |
Preparation finished in 2 seconds, and the remaining 6 minutes were spent waiting. What was being waited for—meaning why the self-check passed late—was not recorded in this log stage (--v=2), making it unverifiable. Since it was a newly created public name, it likely waited for DNS propagation (estimated). The Order object still contained the three challenges presented by the server, and the initial status of authorization was pending .
When requesting the same name again: 4 seconds
Revision 2 of public certificate A had its CR created at 2026-09-09 13:07:57 and became Ready at 13:08:01, taking 4 seconds. Looking at the Order object reveals why. The initial state of the authorization was initialState: valid , and the challenge list contained only http-01. Because the authorization for that name was already acquired 21 days prior (on the first issuance) and Let's Encrypt reuses authorizations for 30 days, neither a solver pod nor a self-check was necessary. If this reuse period is reduced to 10 days and 7 hours as scheduled above, such shortcuts will almost entirely disappear. Why revision 2 was created is unconfirmeddue to missing logs, and we only know that the generation of the Certificate was 3, meaning the spec had changed.
notBefore is earlier than the issuance time
For both public certificates, notBefore was precisely 58 minutes and 32 seconds aheadof the time the CR became Ready (A: 13:08:01 vs 12:09:29, B: 00:13:41 vs 23:15:09). The explanation that Let's Encrypt moves notBefore forward by about 1 hour so that clients with slow clocks do not reject new certificates as "not yet valid" was found only in a reply on the Let's Encrypt community forum and could not be verified in the official documentation.
Incident: When a Secret Has Two Owners, It Re-issues Thousands of Times
The revision number of a webhook certificate (self-signed) used by a component within the cluster exceeded 10,000. From when the Certificate was created on 2026-10-18 until the loop stopped, it was re-issued approximately 10,000 times over 73 minutes(an average of about 2.7 times per second). The remaining logs only covered the final minute or so, but within that short span alone, the same line was repeated over 300 times.
"Certificate must be re-issued" … reason="SecretMismatch"
message="Secret contains a private key that does not match the current CertificateRequest"
Issuance → written to Secret → immediately "the key in the Secret differs from the current request" → re-issuance loop. This means someone was overwriting the Secret written by cert-manager. The situation unfolded as follows: On that day, that component transitioned from an installation method where "its controller creates the webhook certificate itself" to one where "cert-manager creates it," and both methods used the same Secret name. (Presumably) a controller running with the old configuration kept writing its certificate to the same Secret, which explains the loop. Because the time the loop stopped and the time that controller restarted differed by a few minutes, we could not confirm exactly what broke the loop. Since it was self-signed, it did not hit external limits, but had it been an ACME issuer, it would have hit Let's Encrypt's weekly limit (5 certificates per identical name set) within the first few minutes, blocking issuance for nearly a week.
The lesson is simple: One Secret, one owner. And revision by keeping an eye on values that "hardly ever change during normal operation," such as revision numbers or CertificateRequest generation speed, you can catch such loops early.
9. Common Failures and Troubleshooting Order
① Challenges do not finish. Follow the steps from top to bottom describe . The object where it stops points directly to the location of the cause.
kubectl -n <ns> describe certificate <이름> # Issuing 이 붙어 있나, 메시지
kubectl -n <ns> get certificaterequest,order,challenge # 어디까지 생겼나
kubectl -n <ns> describe challenge <이름> # Reason: 자체 점검 실패 문구가 여기 있다
curl -v http://<이름>/.well-known/acme-challenge/<token> # 바깥에서 직접
Three common causes for HTTP-01 are: port 80 blocked externally, a redirect forcing all HTTP to HTTPS applied before the solver path, and hairpinning. The last one occurs when traffic originating within the cluster heading to its own public address is blocked by the router or load balancer, causing external access to work while cert-manager's self-check fails. This is especially common in small clusters behind home routers or standard gateways. For DNS-01, check TXT propagation delays and delegation (CNAME) settings first.
② Hit a rate limit. Let's Encrypt's current limits are 50 certificates per registered domain every 7 days, 5 certificates per identical name set every 7 days, 5 validation failures per hour per name/account, and 300 new orders per 3 hours per account (official rate limit documentation). The identical name set limit recovers at a rate of only one certificate every 34 hours. Both Let's Encrypt and cert-manager tutorials strongly recommend testing configurations on the staging server first. If the loop in Section 8 had happened with ACME, it would have immediately hit this limit.
③ The certificate changed, but the app uses the old one. While proxies reload automatically, apps that build a keystore only once upon startup (such as Java applications creating a PKCS12 keystore in their startup script) must be restarted. Also remember that if a Secret is mounted as subPath , kubelet will not update the files. The solution is to automate restarts (using tools like Reloader) or make the application watch for file changes and reload.
④ Two owners for a single Secret. This is the incident from Section 8. Check if another tool (a chart's built-in certificate generator, a manually created Secret, or another controller) is using the same name. SecretMismatch repeating and the revision continually increasing is almost always a sign of this.
⑤ Failures on webhook certificate renewal days. If cainjector is stopped or the target object's annotations are incorrect, the caBundle remains set to the old CA, causing all requests passing through that webhook to fail with TLS errors. You can find this immediately by comparing the caBundle fingerprint in the webhook configuration with the ca.crt of the Secret.
⑥ Conflicts between a shim and a manually created Certificate. As seen in Section 7, a shim will not touch an ownerless Certificate. If "you added annotations but the certificate did not change," check if a manually created Certificate with the same name exists.
10. Conclusion
- cert-manager is a system where multiple controllers take responsibility for Certificates specifying "desired certificates" and reconcile them into Secrets. Intermediate objects (CertificateRequest, Order, Challenge) represent the stages of work and serve as markers indicating where things stopped if a failure occurs.
- ACME proves "ability to control a name" using key authorization (token + account key thumbprint). HTTP-01 is simple but relies on port 80 and hairpinning, whereas DNS-01 supports wildcards but incurs costs in terms of DNS API keys.
- Renewal defaults to 2/3 of the certificate's lifespan. Since public CA lifespans are shrinking to 64 and 45 days, you should locate and update any configurations with hardcoded periods.
- Measurements showed that issuing a new name took 6 minutes and 5 seconds, re-issuance reusing an authorization took 4 seconds, and when two controllers claimed ownership of a single Secret, it triggered a loop re-issuing the certificate around 10,000 times in 73 minutes.
A Word from Titi. Certificates are just like a rally game. If two people are passing the ball, the game never ends. Every Secret should have only one owner! And instead of renewing every "three months," hand over the ball at "two-thirds of its lifespan" so you never drop it, even if the ball gets shorter (45 days!).
References
- RFC 8555, Automatic Certificate Management Environment (ACME). §7.1.3, §7.1.6 (Order Status), §7.4 (finalize), §8.1 (key authorization), §8.3 (HTTP-01), §8.4 (DNS-01). https://www.rfc-editor.org/rfc/rfc8555.html
- RFC 7638, JSON Web Key (JWK) Thumbprint. §3.1 Example. https://www.rfc-editor.org/rfc/rfc7638.html
- RFC 9773, ACME Renewal Information (ARI). https://www.rfc-editor.org/rfc/rfc9773.html
- cert-manager Documentation: Certificate (Renewal Defaults & Restart Guide) https://cert-manager.io/docs/usage/certificate/ · API Reference https://cert-manager.io/docs/reference/api-docs/ · CertificateRequest https://cert-manager.io/docs/concepts/certificaterequest/ · ACME Orders and Challenges https://cert-manager.io/docs/concepts/acme-orders-challenges/ · Webhook https://cert-manager.io/docs/concepts/webhook/ · CA Injector https://cert-manager.io/docs/concepts/ca-injector/ · Ingress https://cert-manager.io/docs/usage/ingress/ · Gateway https://cert-manager.io/docs/usage/gateway/ · SelfSigned https://cert-manager.io/docs/configuration/selfsigned/ · trust-manager https://cert-manager.io/docs/trust/trust-manager/
- cert-manager Release Notes: v1.15 (Gateway API Beta), v1.16 (renewBeforePercentage), v1.18 (Key Rotation Default Always, revisionHistoryLimit 1), v1.21 (ARI Experimental, Configuration Names). https://cert-manager.io/docs/releases/
- Let's Encrypt: Challenge Types https://letsencrypt.org/docs/challenge-types/ · Rate Limits https://letsencrypt.org/docs/rate-limits/ · Staging https://letsencrypt.org/docs/staging-environment/ · "From 90 to 45" (2025-12-02) https://letsencrypt.org/2025/12/02/from-90-to-45
- Explanation regarding notBefore adjustment (unofficial documentation): Let's Encrypt Community Forum https://community.letsencrypt.org/t/certificate-not-valid-before-dated-1-hour-prior-to-generation/47246
- All original texts were accessed and verified on 2026-10-26.