状態:ドラフト(2026-10-26)、実測値を反映。 実測は2026-10-26にTT Labクラスター(cert-manager 1.21系)から 読み取りのみ で行いました。
kubectl get·logs、Secretの証明書をopenssl x509 -noout -datesで日付だけ読み取ること、公開アドレスにopenssl s_clientでアクセスしてみることまでが含まれます。公開名や内部オブジェクト名はマスクしています(<공개 이름 A>,<클러스터 이슈어>)。公式ドキュメントとRFCは同日に原文を開いて確認しました。 作成ルール:実測データを基本とし、測定できなかったものは「未実測」、確認できなかったものは「確認できず」、推測は「推定」と記載します。
目次
- 1. 人間が証明書を更新するときに起こること
- 2. 5つのオブジェクトとそれを動かすコントローラー
- 3. ACME:「この名前が本当にあなたのものか」を証明する規約
- 4. 誰が署名するのか:SelfSigned、CA、ACME
- 5. いつ更新するのか
- 6. Webhookとcainjector:なぜコンポーネントが3つなのか
- 7. ゲートウェイやIngressと連携する方法:shim
- 8. 実測:TT Labクラスターでの読み取り結果
- 9. よくある障害と確認手順
- 10. まとめ
- 参考
1. 人間が証明書を更新するときに起こること
Let's Encryptの証明書は90日間の有効期限があります。これは、3ヶ月に1回誰かがカレンダーを見て更新コマンドを実行する必要があり、その誰かはいつか忘れてしまうということです。証明書の有効期限が切れるとブラウザは警告を表示し、サービス間でTLS通信を行っていた箇所は丸ごと切断されます。人間の記憶に頼る方法は、必ず一度は失敗します。
cert-managerはこの問題をKubernetesらしい方法で解決します。人間は「このような証明書がこのSecretに常になければならない」という 望ましい状態(desired state)だけをオブジェクトとして記述します。残りの作業、すなわち鍵の作成、認証局(CA)へのリクエスト、ドメイン所有権の証明、Secretへの書き込み、有効期限前の再取得は、コントローラーが常時稼働して維持します。Deploymentを記述すればPodが作成され、終了すれば再作成されるのと同じ原理です。
この記事では、その「維持するプロセス」を1層ずつ紐解いていきます。どのようなオブジェクトがどのような順序で作成されるか(第2節)、ACMEサーバーと何をやり取りするか(第3節)、誰が署名するか(第4節)、いつ更新するか(第5節)、Webhookとcainjectorはなぜ別々にあるのか(第6節)、ゲートウェイとはどのように連携するか(第7節)を確認した後、TT Labクラスターで実際に読み取った状態と1つの障害(第8節)、よくある障害(第9節)で締めくくります。
2. 5つのオブジェクトとそれを動かすコントローラー
cert-managerのCRDは6種類あります。このうち人間が直接使用するのは2つ(イシュアとCertificate)で、残りはコントローラーが処理を分担するために作成する中間オブジェクトです。
| オブジェクト | スコープ | 作成者 | 内容 |
|---|---|---|---|
Issuer / ClusterIssuer |
ネームスペース / クラスター | 人間 | 誰が署名するか:ACME(Let's Encryptなど)、CA、SelfSigned、Vault、外部イシュア |
Certificate |
ネームスペース | 人間またはshim | 望ましい証明書:名前のリスト、イシュア、Secret名、寿命・更新タイミング、鍵の種類 |
CertificateRequest |
ネームスペース | コントローラー | CSR 1通。リビジョン番号が付与され、承認(Approved)後に署名される |
Order |
ネームスペース | コントローラー(ACMEのみ) | ACMEサーバーの1つのオーダーとそのURL |
Challenge |
ネームスペース | コントローラー(ACMEのみ) | 1つの名前に対する1つの所有権証明 |
最も短い使用例は次の通りです。Let's EncryptでHTTP-01により証明を行い、証明用パスをGateway APIのHTTPRouteで公開するClusterIssuerと、そのイシュアで1つの証明書を求めるCertificateです。
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: <클러스터 이슈어> }
状態機械:「今すぐ発行すべきか」を誰が決めるのか
Certificateの状態は2つの条件(condition)で読み取ります。 Ready は、Secretが存在し、有効期限が切れておらず、鍵のペアが一致し、名前が仕様と一致していることを意味します。 Issuing は「現在新しく取得中である」という目印であり、必要なときに付与され、発行が完了するとissuingコントローラーによって削除されます(公式APIドキュメント)。
コントローラーは単体の巨大なものではなく、処理が分割された複数のものから構成されています。順番に見ると次のようになります。
- trigger:CertificateとSecretを比較し、「再取得が必要である」と判断した場合に
Issuing=Trueを付与します。実測ログに残っていた理由は2つありました。Secretが完全に存在しない場合のDoesNotExist、Secretの秘密鍵が現在のリクエストと異なる場合のSecretMismatchです(第8節)。更新時刻を過ぎた場合も、同じ場所で発行が開始されます。 - keymanager:次の証明書で使用する秘密鍵を 一時Secret にまず作成します。Certificateの
status.nextPrivateKeySecretNameがその名前であり、実測ではCertificateRequestの注釈cert-manager.io/private-key-secret-nameに<인증서>-xxxxxのような乱数のサフィックスが付いた名前が残っていました。鍵を直接Secretに書き込まない理由は、署名が失敗した場合に使用中の鍵と証明書のペアを破損させないためです。v1.18からは鍵ローテーションポリシーのデフォルト値がNeverからAlwaysに変更され、更新のたびに新しい鍵が作成されます(1.18リリースノート)。 - requestmanager:その鍵でCSRを作成し、1つのCertificateRequestを作成します。名前の末尾の数字がリビジョンです(
<인증서>-1,-2, …). - approver:CertificateRequestに
Approvedを付与します。デフォルトの承認者はcert-manager自身であり、ポリシーツール(approver-policy)で変更できます。承認なしでは、いかなるイシュアも署名を行いません。 - イシュア別の署名器:SelfSignedおよびCAはその場で署名します。ACMEはOrderを作成し、結果を待ちます。CertificateRequestは一度失敗しても再試行されず、再試行はCertificate側が新しいリビジョンで行います(公式ドキュメント)。
- issuing:ReadyになったCertificateRequestの証明書と一時Secretの鍵をペアにして本番のSecretに書き込み、
Issuingを削除します。 - revisionmanager:古いCertificateRequestを削除します。v1.18以降は
revisionHistoryLimitのデフォルト値が1であるため、最後の1つだけが残ります(実測でも証明書ごとにCRは1つだけ存在していました)。
すべてのコントローラーが同じオブジェクトを同時に修正しようとするため、「楽観的ロックによりキューに戻す」というログが頻繁に出力されます。実測での1度の発行の中でもこのようなログが6回出力されましたが、すべて直ちに再試行されて問題ありませんでした。Kubernetesコントローラーの正常な動作であるため、驚く必要はありません。
3. ACME:「この名前が本当にあなたのものか」を証明する規約
Let's Encryptのような公開CAは人間を確認しません。確認するのは 名前を実際に制御できるか の1点のみです。その手順を定めたものがACME(RFC 8555)です。
アカウント、オーダー、権限、チャレンジ
- アカウント。 クライアントはアカウント鍵(非対称鍵)を1つ作成し、ACMEサーバーに登録します。以降すべてのリクエストはこの鍵で署名されたJWSとなります。cert-managerではClusterIssuerの
privateKeySecretRefがその鍵であり、イシュアのReady条件は「アカウントが登録された」ことを意味します(実測:The ACME account was registered with the ACME server). - オーダー(order)。 「これらの名前で証明書が欲しい」というリクエストです。RFC 8555 §7.1.6に従い、オーダーは
pendingとして生成され、すべての権限がvalidになるとreadyになり、CSRを提出するとprocessingになり、発行されるとvalidになります。どの段階であれ失敗するとinvalidになります。 - 権限(authorization)とチャレンジ。 名前ごとに権限オブジェクトが1つあり、サーバーはその権限を取得する方法(チャレンジ)を複数提示します。実測のオーダーには
http-01·dns-01·tls-alpn-01の3つが同じtokenとして提示され、cert-managerはイシュアに指定された検証方式(ここではHTTP-01)のみを選択しました。cert-managerが使用する方式はHTTP-01とDNS-01の2つです。 - finalizeとダウンロード。 すべての権限がvalidになると、CSRを
finalizeのURLに送信し(§7.4)、発行された証明書チェーンを取得します(§7.4.2)。
key authorization:1つのトークンで何を証明するのか
サーバーから提供されたtokenをウェブ上に配置するだけでは不十分です。それではtokenを盗み見た第三者も同じことができてしまいます。RFC 8555 §8.1では、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)
実際にそのように計算されるかをPythonの標準ライブラリで確認しました。フィンガープリントの入力はRFC 7638 §3.1のサンプルRSA公開鍵であり、tokenは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()))
結果(2026-10-26、ローカルのpython3):フィンガープリントは NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs であり、 RFC 7638が記載している期待値と一致し、そのトークンのDNS-01 TXT値は ZTRx1Ckl1-tM05o5zaizTTA0yUy5AGereMgSNWC6Ll8 でした。フィンガープリントがアカウント鍵から生成されるため、アカウント鍵を持つ側のみが正しい応答を作成できます。
HTTP-01とDNS-01、どちらを選ぶべきか
| HTTP-01 | DNS-01 | |
|---|---|---|
| 証明する場所 | ポート80の /.well-known/acme-challenge/ |
_acme-challenge.<이름> TXTレコード |
| 外部からアクセス可能である必要があるか | はい。インターネットからポート80で | いいえ。DNSのみが公開されていればよい |
ワイルドカード(*.example.com) |
不可 | 可 |
| クラスターが持つべき権限 | パスを1つ開く権限(ソルバポッド・ルート) | DNSプロバイダAPIキー(そのゾーン全体を変更できることが多い) |
| ハマりやすいポイント | ファイアウォール・リダイレクト・クラスター内のヘアピン | 伝播遅延、委任(CNAME)の設定 |
ワイルドカードとポート80の条件はLet's Encryptの公式ドキュメント(チャレンジの種類)で確認しました。DNS-01のAPIキーは「DNSゾーンをごっそり書き換えられる鍵」がクラスター内に入ること意味するため、セキュリティコストが高くなります。公開名がいくつかだけでポート80が開いているなら、HTTP-01のほうがシンプルです。
cert-managerがHTTP-01を解く方法
Challengeコントローラーは名前ごとに ソルバポッド(acmesolverイメージ)、Service、そしてその経路を開くルート(IngressまたはGateway APIのHTTPRoute)を作成します。その後すぐにACMEサーバーに「準備できた」と通知するのではなく、 セルフチェック(self check) を行います。クラスター内からその公開名で直接 GET を試行し、正しいkey authorizationが返ってきたときのみ通知します。ACMEサーバーに失敗を蓄積しないための仕組みです。検証の失敗は名前・アカウントごとに1時間あたり5回に制限されているためです(Let's Encrypt rate limit)。代わりに、クラスター内から自身のパブリックアドレスに出て戻る経路(ヘアピン)がブロックされていると、セルフチェックが永遠にパスしません。第9節で改めて見ていきます。
4. 誰が署名するのか:SelfSigned、CA、ACME
公開CAだけがissuerではありません。クラスター内のウェブホックサーバーのように 自側のクライアントだけを信用すればよい 証明書には、プライベートCAで十分です。
- SelfSigned:自分の鍵で自分の証明書に署名します。受け取る側が信じる根拠が証明書自身しかないため、通常はCAを最初に作成する用途に使われます。
- CA:Secretに含まれるCA鍵で署名します。公式ドキュメントが推奨するブートストラップの形は、「SelfSigned ClusterIssuerで
isCA: trueであるルートCertificateを作成し、そのSecretをCA Issuerが使う」というものです。
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 } }
プライベートCAの本当の難しさは署名ではなく 信頼の配布です。ルート証明書をすべてのクライアントの信頼リストに入れ、ルートを置き換えるときは古いものと新しいものを同時に信頼させなければなりません。公式ドキュメントはその用途にtrust-managerを紹介しており、運用のPKIにはローテーション・配布・復旧の計画が必要だと警告しています。ポッドにSecretなしで証明書を直接マウントするcsi-driverもあります。
5. いつ更新するのか
spec.duration のデフォルト値は90日、 spec.renewBefore のデフォルト値は 寿命の1/3 です。つまり何も記述しないと、 寿命の2/3の時点での更新になります。v1.16からは割合で指定する renewBeforePercentage もあります(公式ドキュメント、1.16リリースノート)。ACMEのようにCAが寿命を決める場合は、要求したdurationではなく 実際に受け取った証明書のnotBefore・notAfter で計算します。実測値がそのまま示してくれます。
| 証明書(2026-10-26読み取り) | notBefore → notAfter | 寿命 | renewalTime | 有効期限までの残り期間 |
|---|---|---|---|---|
| 公開証明書A(Let's Encrypt、設定なし) | 09-09 12:09:29 → 12-08 12:09:28 | 90日 − 1秒 | 11-08 12:09:28 | 30日(1/3) |
| 公開証明書B(Let's Encrypt、設定なし) | 10-08 23:15:09 → 01-06 23:15:08 | 90日 − 1秒 | 12-07 23:15:08 | 30日(1/3) |
自己署名(duration: 2160h·renewBefore: 360h を記述したもの) |
08-23 08:45:34 → 11-21 08:45:34 | 90日 | 11-06 08:45:34 | 15日 |
| 自己署名(ウェブホック用、設定なし) | 10-18 → 01-16 | 90日 | 12-17 | 30日(1/3) |
時刻はすべてUTCであり、 kubectl get certificate -o custom-columns=… の status.notBefore·notAfter·renewalTime と、Secretの tls.crt を openssl x509 -noout -startdate -enddate として読み取った日付が一致していました。計算すると、公開証明書のrenewalTimeは正確に notAfter − 30일 になります。
公開CA側の変化
Let's Encryptは2025-12-02に寿命短縮のスケジュールを発表しました。デフォルトプロファイルが2027-02-10に64日、2028-02-16に45日となり、権限の再利用期間も30日から10日、さらに7時間に短縮されます。発表文では「60日ごと」のような固定間隔での更新方式ではもはや不十分であると断言しています。割合(2/3)で更新するcert-managerのデフォルト値はこの変化にそのまま追従しますが、 renewBefore: 720h のような 固定値を記述している箇所では、45日証明書での更新の余裕が15日に減少します。 CAが「いつ更新すべきか」を通知してくれるARI(RFC 9773)もあります。cert-managerはv1.21で ACMEUseARI 機能フラグの下で実験的に導入しており、デフォルトは無効です。
6. ウェブホックとcainjector:なぜコンポーネントが3つなのか
cert-managerをインストールするとDeploymentが3つ作成されます。TT Labクラスターでもcontroller・webhook・cainjectorが1つずつ稼働していました。
- controller:第2節のコントローラーはすべてこのプロセスの中にあります。
- webhook:APIサーバーがcert-managerオブジェクトを保存する前に呼び出すサーバーです。公式ドキュメントが挙げる役割は3つです。仕様の検証(不正なCertificateを一切保存させない)、デフォルト値の補完、APIバージョン間の変換です。ウェブホックはAPIサーバーとTLSで通信するため、 ウェブホック自体にも証明書が必要になります。鶏と卵の問題を回避するため、webhookはcert-managerの発行パスを使用せず、自らCAを作成してSecretに格納します(実測:そのCAは2026-08-19から1年間)。
- cainjector:ウェブホック設定(Validating/MutatingWebhookConfiguration)、CRDの変換ウェブホック、APIServiceの
caBundleフィールドを埋めます。対象オブジェクトにcert-manager.io/inject-ca-from: <네임스페이스>/<Certificate>アノテーションを付与すると、そのCertificateのCAを読み込んで挿入し、証明書が変更された場合は再度挿入します。TT Labクラスターでも、複数のコンポーネントの検証・変異ウェブホック設定やCRDがこのアノテーションによってCAを受け取っていました。
cainjectorがないと、ウェブホックサーバーの証明書が更新された日にAPIサーバーが新しい証明書を信頼できなくなり、 該当する種類のオブジェクト作成がすべて失敗します。ウェブホック証明書の更新がサイレント障害になりやすい理由はここにあります。
7. ゲートウェイ・Ingressと連携する方法:shim
Certificateを個別に記述せず、ゲートウェイ側にアノテーションを1つ付与することも可能です。
- ingress-shim:
cert-manager.io/cluster-issuer(またはissuer)アノテーションが付与されたIngressを監視し、tls.secretNameごとにCertificateを作成します。 - gateway-shim:同様のアノテーションが付与されたGatewayを監視し、HTTPSリスナーの
hostnameとcertificateRefsからCertificateを作成します。この機能はデフォルトで無効です。v1.15でベータとなり--enable-gateway-apiで有効化し、v1.21では設定ファイルのgatewayAPI.enabledで有効化します(旧名称enableGatewayAPIはまだ動作しますが非推奨です)。GAであるという公式文書は見つかりませんでした。
TT Labクラスターのcontroller設定ファイルには旧名称である enableGatewayAPI: true が含まれており、Gatewayには cert-manager.io/cluster-issuer: <클러스터 이슈어> アノテーションが付いていました。その結果を読み解くと、shimの性質がよく見えてきます。
- 公開証明書AのCertificateは
ownerReferencesが Gateway でした。shimが作成したオブジェクトであることを意味し、Gatewayを削除すると一緒に削除されます。 - 公開証明書Bは、人間が
kubectl applyで個別に作成したCertificateでした。同じGatewayにBのリスナーがあるためshimもそのSecret名でCertificateを作成しようとしましたが、発行中に「所有者のないCertificateは修正しない」というログを残して手出ししませんでした(実測、2026-10-09のログ10行)。人間が作成したものをshimが上書きしないためのセーフティネットです。ただし、ひとつの名前を2つの経路で管理しているというシグナルであるため、どちらかに整理するほうが望ましいです。
証明書が変更されたとき、ゲートウェイはどうやって気づくのか
ゲートウェイ(Envoyベース)はSecretを監視し、変更があれば再読み込みします。プロキシを再起動することはありません。実測で確認した経路は次のとおりです。TT LabクラスターのCiliumゲートウェイは、参照するTLS Secretを自己専用のネームスペースへハッシュ名のコピーとして複製します。公開証明書Aのコピーは 2026-08-19(初回発行)に作成されましたが、中身は2026-09-09に再取得した証明書が含まれていました。コピーを新しく作成するのではなく、その場で更新されたのです。外部から openssl s_client で接続してみると、公開アドレスが返す証明書のnotAfterも Dec 8 12:09:28 2026 と一致していました。
一方、Secretをファイルとしてマウントし、 起動時に1度だけ読み込む アプリケーションは異なります。公式ドキュメントでは、そうしたアプリの場合は新しい証明書がすぐに適用されないと記載されており、 kubectl rollout restart やReloaderなどのツールを推奨しています。第9節の罠の中で最も一般的なものです。
8. 実測:TT Labクラスターで読み取ったもの
環境:cert-manager 1.21系(Helmチャート、controller・webhook・cainjector各1つ)。読み取り日2026-10-26、時刻はすべてUTCです。
| 項目 | 値 |
|---|---|
| Issuer | 公開証明書用ClusterIssuer(ACME、HTTP-01、Gateway APIソルバ)とコンポーネントごとのSelfSigned Issuer |
| Certificate | 公開証明書いくつか、およびウェブホック・内部TLS用の自己署名証明書複数 |
| Order・Challenge | Orderは公開証明書ごとに最後の1つが valid として残り、Challengeはない ―― 成功すると削除される |
| 公開証明書の鍵 | RSA 2048、署名sha256WithRSA。発行者はLet's Encryptの中間CA |
新しい名前を初めて取得する場合:6分5秒
公開証明書Bを初めて作成した日(2026-10-09)のcontrollerログが、以前のコンテナログ(kubectl logs --previous)に残っていました。行を時系列順に並べると次のようになります。
| 時刻 | 内容 |
|---|---|
| 00:07:36.141 | trigger: Certificate must be re-issued、理由 DoesNotExist(Secretがない)→ Issuing |
| 00:07:36.332 | CertificateRequestリビジョン1作成・Approved |
| 00:07:38.209 | ソルバポッド作成 |
| 00:07:38.274 | ソルバ用HTTPRoute作成(Gatewayを親に指定) |
| 00:07:48 – 00:13:28 | ソルバポッド・Service・HTTPRouteが存在するか確認を繰り返す(最初10秒、後20秒間隔、23回) |
| 00:13:41.037 | certificate issued、CR Ready |
| 00:13:41.075 | Certificate Ready=True |
準備は2秒で完了し、残りの6分間は待機していました。何を待っていたのか、つまりセルフチェックがなぜ遅れて通過したのかはこのログの段階(--v=2)には残っていなかったため 確認できずです。新しく作成した公開名だったため、DNSの伝播を待っていた可能性があります(推測)。オーダーオブジェクトにはサーバーが提示した3つのチャレンジがそのまま残っており、権限の初期状態は pending でした。
同じ名前を再度取得する場合:4秒
公開証明書 A のリビジョン 2 は、2026-09-09 13:07:57 に CR が作成され、13:08:01 に Ready になりました。 4秒です。Order オブジェクトを見るとその理由がわかります。権限の初期状態が initialState: valid であり、チャレンジ一覧には http-01 が 1 つだけありました。その名前の権限を 21 日前(初回発行時)にすでに取得しており、Let's Encrypt が権限を 30 日間再利用するため、プルの Pod も自己チェックも不要だったのです。この再利用期間が上記の日程のように 10 日 7 時間に短縮されると、このような近道はほとんどなくなります。リビジョン 2 がなぜ作成されたのかは残っているログがないため 確認できずであり、Certificate の generation が 3 であることから、仕様が変更されたことがあるということがわかるだけです。
notBefore は発行時刻より前になる
両方の公開証明書で、notBefore が CR の Ready 時刻より正確に 58分32秒前になっていました(A: 13:08:01 に対し 12:09:29、B: 00:13:41 に対し 23:15:09)。時計の遅いクライアントでも新しい証明書を「まだ有効ではない」として拒否しないよう、Let's Encrypt が notBefore を約 1 時間早めているという説明は、Let's Encrypt コミュニティフォーラムの回答でのみ見つかり、公式ドキュメントでは確認できませんでした。
インシデント:1つの Secret にオーナーが2人いると、何千回も再発行される
クラスター内のコンポーネントの 1 つが使用する Webhook 証明書(自己署名)のリビジョン番号が 1 万を超えていました。2026-10-18 に Certificate が作成されてループが停止するまでに、 73分間に約1万回の再発行が行われました(平均して 1 秒あたり約 2.7 回)。残っていたログは最後の 1 分余りのみでしたが、その中だけでも 300 回を超えて同じ行が繰り返されていました。
"Certificate must be re-issued" … reason="SecretMismatch"
message="Secret contains a private key that does not match the current CertificateRequest"
発行 → Secret に書き込む → 直後に「Secret のキーが現在のリクエストと異なる」 → 再発行のループです。 誰かが cert-manager が書き込んだ Secret を上書きしていたことを意味します。状況証拠は以下の通りです。その日、そのコンポーネントは「自身のコントローラーが Webhook 証明書を自分で作成する」というインストール方式から、「cert-manager が作成する」という方式に移行しており、 両方の方式で同じ Secret 名が使用されていました。 古い設定で起動したコントローラーが、自身の証明書を同じ Secret に書き込み続けたと考えればループの説明がつきます(推測)。ループが停止した時刻と、そのコントローラーが再起動した時刻が数分ずれていたため、正確に何がループを断ち切ったのかは確認できませんでした。自己署名証明書であったため外部の制限には引っかかりませんでしたが、 ACME イシュアであれば最初の数分以内に Let's Encrypt の週あたりの制限(同一名セットで 5 枚)に引っかかり、1 週間近く発行がブロックされていたはずです。
教訓はシンプルです。 1つの Secret にオーナーは 1 つ。 そして revision 数値や CertificateRequest の生成速度のような「普段はほとんど動かない値」を監視していれば、このようなループを早期に検知できます。
9. よくある障害と確認手順
① チャレンジが完了しない。 上から下へと順に辿って describe 確認します。どのオブジェクトで停止したかが、そのまま原因のある場所です。
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> # 바깥에서 직접
HTTP-01 でよくある原因は 3 つあります。ポート 80 が外部からブロックされている、すべての http を https にリダイレクトする設定がプルのパスよりも先に適用されている、そして ヘアピン(ループバック)です。最後の項目は、クラスター内から自分のパブリックアドレスへの通信がルーターやロードバランサーでブロックされ、外部からはアクセスできるものの cert-manager の自己チェックだけが失敗するケースです。ルーター配下の小規模なクラスターで特に頻発します。DNS-01 の場合は、TXT レコードの伝播遅延と委任(CNAME)の設定を最初に確認します。
② レートリミット(制限)に引っかかった。 Let's Encrypt の現在の制限は、登録ドメインあたり 7 日間で 50 枚、 同一名セットで 7 日間で 5 枚、名前およびアカウントあたりの検証失敗が 1 時間に 5 回、アカウントあたりの新規 Order が 3 時間に 300 個です(公式のレートリミットドキュメントより)。同一名セットの制限は 34 時間に 1 枚のペースでしか回復しません。設定をテストする際は、必ず最初にステージングサーバーを使用することが、Let's Encrypt と cert-manager の両方のチュートリアルで推奨されています。セクション 8 のループが ACME 環境で発生していれば、まさにこの制限に引っかかっていたことでしょう。
③ 証明書は更新されたが、アプリが古いものを使用している。 プロキシは自動的に再読み込みを行いますが、起動時にキーストアを一度だけ作成するアプリケーション(起動スクリプト内で Java の PKCS12 キーストアを作成する場合など)は再起動する必要があります。Secret を subPath としてマウントした場合、kubelet がファイルを更新しない点にも注意してください。解決策としては、再起動を自動化する(Reloader などを利用する)か、アプリがファイルの変更を検知して再読み込みするようにすることです。
④ 1つの Secret に 2 人のオーナーがいる。 セクション 8 のインシデントです。他のツール(Chart 独自の証明書ジェネレーター、手動で作成した Secret、別のコントローラー)が同じ名前を使用していないか確認します。 SecretMismatch が繰り返し発生し、リビジョンが増加し続ける場合は、ほぼこれが原因です。
⑤ Webhook 証明書の更新日の障害。 cainjector が停止している場合や、対象オブジェクトのアノテーションが間違っている場合、caBundle が古い CA のままになり、その Webhook を経由するすべてのリクエストが TLS エラーで失敗します。Webhook 設定の caBundle のフィンガープリントと Secret の ca.crt を比較すればすぐにわかります。
⑥ shim と手動で作成した Certificate の競合。 セクション 7 で述べたように、shim はオーナーのいない Certificate には干渉しません。「アノテーションを追加したのに証明書が更新されない」という場合は、同じ名前の手動作成された Certificate が存在するか確認します。
10. まとめ
- cert-manager は、「必要な証明書」が記述された Certificate を複数のコントローラーが手分けして処理し、Secret の形で同期するシステムです。中間オブジェクト(CertificateRequest・Order・Challenge)は作業の各ステップであり、障害発生時にどこで停止したかを示す目印になります。
- ACME では、key authorization(トークン + アカウントキーのフィンガープリント)を使用して「ドメイン名を制御できること」を証明します。HTTP-01 はシンプルですがポート 80 とヘアピンに依存し、DNS-01 はワイルドカード証明書に対応できる一方で DNS API キーの設定が必要になります。
- 証明書の更新は、デフォルトでは有効期間の 2/3 の時点で実行されます。公開 CA の有効期間が 64 日、45 日へと短縮されているため、固定期間がハードコードされている設定がないか探して修正します。
- 実測では、新しい名前の発行には 6 分 5 秒、権限を再利用した再発行には 4 秒かかりました。また、1 つの Secret を 2 つのコントローラーがそれぞれオーナーとして使用した結果、73 分間に約 1 万回の再発行ループが発生しました。
Titi の一言。 証明書はラリーみたいなものです。ボールを打ち返す人が 2 人いたら終わりませんよ。1 つの Secret にオーナーも 1 人だけ!それに、「3 か月ごと」ではなく「有効期間の 3 分の 2」の時点でバトンタッチしなければ、ボールが短くなっても(45 日!)落とさずに済みますよ。
参考
- RFC 8555, Automatic Certificate Management Environment (ACME). §7.1.3·§7.1.6 (Order states), §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 Examples. https://www.rfc-editor.org/rfc/rfc7638.html
- RFC 9773, ACME Renewal Information (ARI). https://www.rfc-editor.org/rfc/rfc9773.html
- cert-manager ドキュメント: Certificate(更新のデフォルト値・再起動の案内) https://cert-manager.io/docs/usage/certificate/ · API リファレンス 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 リリースノート 1.15 (Gateway API ベータ)・1.16 (renewBeforePercentage)・1.18 (キーローテーションデフォルト Always, revisionHistoryLimit 1)・1.21 (ARI 実験的機能, 設定名). 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
- notBefore を早めることに関する説明(公式ドキュメントではありません): Let's Encrypt コミュニティフォーラム https://community.letsencrypt.org/t/certificate-not-valid-before-dated-1-hour-prior-to-generation/47246
- すべての原文は 2026-10-26 にアクセスして確認しました。