TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

How cert-manager creates and rotates certificates

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 via openssl x509 -noout -dates , and probing the public address with openssl 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

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

Certificate 하나가 Secret 이 되기까지 왼쪽은 객체(원하는 상태), 오른쪽은 그 객체를 움직이는 컨트롤러 Gateway · Ingress cert-manager.io/cluster-issuer gateway-shim · ingress-shim Secret 이름마다 Certificate 생성 Certificate 이름 목록 · 이슈어 · Secret 이름 trigger · keymanager 갱신 시점·Secret 불일치 판단, 새 키 CertificateRequest CSR 한 장 · 리비전 번호 approver · 이슈어별 서명기 승인 → 서명 → Ready (실패 시 끝) Order (ACME 만) ACME 주문 1건 · finalize URL orders 주문 → 권한 → CSR → 인증서 Challenge (이름마다) token · 방식(http-01/dns-01) challenges 풀이 → 자체 점검 → 준비 알림 Secret (kubernetes.io/tls) tls.crt · tls.key · ca.crt issuing 컨트롤러가 쓴다 → 게이트웨이·Ingress·파드가 읽는다 CR 이 Ready 가 되면 주문과 챌린지는 ACME 이슈어일 때만 생긴다. 자체 서명·CA 이슈어는 CR 에서 바로 서명한다.
Figure: The object chain and controllers at each step from Gateway annotation through Certificate, CertificateRequest, Order, and Challenge to Secret

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:

  1. 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, and SecretMismatch when 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.
  2. keymanager: First creates the private key to be used for the next certificate in a temporary Secret . The Certificate's status.nextPrivateKeySecretName is that name, and in empirical measurements, a name with a random suffix like cert-manager.io/private-key-secret-name remained 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 from Never to Always , generating a new key on every renewal (1.18 Release Notes).
  3. requestmanager: Creates a CSR using that key to generate a CertificateRequest. The number at the end of the name is the revision (<인증서>-1, -2, …).
  4. approver: Attaches Approved to the CertificateRequest. The default approver is cert-manager itself, which can be replaced with policy tools (approver-policy). No issuer signs without approval.
  5. 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).
  6. issuing: Pairs the certificate from the Ready CertificateRequest with the key from the temporary Secret, writes them to the real Secret, and removes Issuing .
  7. revisionmanager: Deletes old CertificateRequests. Since v1.18, the revisionHistoryLimit default 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).

HTTP-01 한 번: 누가 누구에게 무엇을 묻나 cert-manager 컨트롤러 ACME 서버 Let's Encrypt 게이트웨이 + 풀이 파드 ① 새 주문(이름 목록) order: pending ② 권한(authz) 조회 챌린지 3종 + token ③ 풀이 파드 · HTTPRoute 생성 /.well-known/acme-challenge/<token> ④ 자체 점검: 공인 주소로 GET 통과할 때까지 반복 ⑤ 챌린지에 「준비됐다」 POST ⑥ 여러 곳에서 포트 80 GET key authorization 이 맞나 ⑦ authz valid → order ready ⑧ finalize(CSR) processing → valid ⑨ 인증서 내려받기 → Secret, 풀이 객체 삭제 실측(2026-10-09, 새 이름 하나): ①→⑨ 6분 5초. ③은 2초 뒤, 나머지는 ④에서 기다렸다. 같은 이름을 30일 안에 다시 받을 때는 authz 가 이미 valid 라 ③–⑥이 없었다.
Figure: Nine steps of a single HTTP-01 exchange between cert-manager, the ACME server, and the gateway

Accounts, orders, authorizations, and challenges

  1. 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 privateKeySecretRef is that key, and the issuer's Ready condition means "account registered" (empirical: The ACME account was registered with the ACME server).
  2. Order. A request stating "I want a certificate for these names." According to RFC 8555 §7.1.6, an order is born as pending , becomes valid when all authorizations become ready, becomes processingwhen a CSR is submitted, and becomes valid when issued. If any step fails, it becomes invalid .
  3. 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-01 were 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.
  4. Finalize and download. Once all authorizations are valid, the CSR is sent to the finalize URL (§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.

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.

90일 인증서는 60일째에 갱신한다 (기본: 수명의 2/3) 9월 10월 11월 12월 1월 공개 인증서 A 갱신 11/8 만료 12/8 공개 인증서 B 갱신 12/7 만료 1/6 자체 서명 (renewBefore 15일) 갱신 11/6 만료 11/21 읽은 날 10/26 공개 인증서는 Let's Encrypt, 진한 칸은 갱신 구간(만료 전 1/3). 공개 CA 는 기본 수명을 2027-02 에 64일, 2028-02 에 45일로 줄인다. 날짜 대신 비율(2/3)이나 ARI 로 갱신한다.
Figure: Validity period, renewal time, and read date for two public certificates and one self-signed certificate
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.

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.

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.

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

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

Sign in to like

Comments

No comments yet.

Sign in to leave a comment