TT Lab
ブログ

한국어 English 日本語 中文

はじめる
ブログ

ブログ

cert-managerはどのように証明書を作成し、更新しているのか

状態:ドラフト(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. 人間が証明書を更新するときに起こること

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つのオブジェクトとそれを動かすコントローラー

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 에서 바로 서명한다.
図:Gatewayの注釈(アノテーション)からCertificate、CertificateRequest、Order、Challengeを経てSecretになるまでのオブジェクトの連鎖と、各段階のコントローラー

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ドキュメント)。

コントローラーは単体の巨大なものではなく、処理が分割された複数のものから構成されています。順番に見ると次のようになります。

  1. trigger:CertificateとSecretを比較し、「再取得が必要である」と判断した場合に Issuing=True を付与します。実測ログに残っていた理由は2つありました。Secretが完全に存在しない場合の DoesNotExist、Secretの秘密鍵が現在のリクエストと異なる場合の SecretMismatch です(第8節)。更新時刻を過ぎた場合も、同じ場所で発行が開始されます。
  2. keymanager:次の証明書で使用する秘密鍵を 一時Secret にまず作成します。Certificateの status.nextPrivateKeySecretName がその名前であり、実測ではCertificateRequestの注釈 cert-manager.io/private-key-secret-name に <인증서>-xxxxx のような乱数のサフィックスが付いた名前が残っていました。鍵を直接Secretに書き込まない理由は、署名が失敗した場合に使用中の鍵と証明書のペアを破損させないためです。v1.18からは鍵ローテーションポリシーのデフォルト値が Never から Always に変更され、更新のたびに新しい鍵が作成されます(1.18リリースノート)。
  3. requestmanager:その鍵でCSRを作成し、1つのCertificateRequestを作成します。名前の末尾の数字がリビジョンです(<인증서>-1, -2, …).
  4. approver:CertificateRequestに Approved を付与します。デフォルトの承認者はcert-manager自身であり、ポリシーツール(approver-policy)で変更できます。承認なしでは、いかなるイシュアも署名を行いません。
  5. イシュア別の署名器:SelfSignedおよびCAはその場で署名します。ACMEはOrderを作成し、結果を待ちます。CertificateRequestは一度失敗しても再試行されず、再試行はCertificate側が新しいリビジョンで行います(公式ドキュメント)。
  6. issuing:ReadyになったCertificateRequestの証明書と一時Secretの鍵をペアにして本番のSecretに書き込み、 Issuing を削除します。
  7. revisionmanager:古いCertificateRequestを削除します。v1.18以降は revisionHistoryLimit のデフォルト値が1であるため、最後の1つだけが残ります(実測でも証明書ごとにCRは1つだけ存在していました)。

すべてのコントローラーが同じオブジェクトを同時に修正しようとするため、「楽観的ロックによりキューに戻す」というログが頻繁に出力されます。実測での1度の発行の中でもこのようなログが6回出力されましたが、すべて直ちに再試行されて問題ありませんでした。Kubernetesコントローラーの正常な動作であるため、驚く必要はありません。

3. ACME:「この名前が本当にあなたのものか」を証明する規約

Let's Encryptのような公開CAは人間を確認しません。確認するのは 名前を実際に制御できるか の1点のみです。その手順を定めたものが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 라 ③–⑥이 없었다.
図:cert-manager、ACMEサーバー、ゲートウェイの間でHTTP-01が1回往復する9つのステップ

アカウント、オーダー、権限、チャレンジ

  1. アカウント。 クライアントはアカウント鍵(非対称鍵)を1つ作成し、ACMEサーバーに登録します。以降すべてのリクエストはこの鍵で署名されたJWSとなります。cert-managerではClusterIssuerの privateKeySecretRef がその鍵であり、イシュアの Ready 条件は「アカウントが登録された」ことを意味します(実測: The ACME account was registered with the ACME server).
  2. オーダー(order)。 「これらの名前で証明書が欲しい」というリクエストです。RFC 8555 §7.1.6に従い、オーダーは pending として生成され、すべての権限が valid になると readyになり、CSRを提出すると processingになり、発行されると valid になります。どの段階であれ失敗すると invalid になります。
  3. 権限(authorization)とチャレンジ。 名前ごとに権限オブジェクトが1つあり、サーバーはその権限を取得する方法(チャレンジ)を複数提示します。実測のオーダーには http-01·dns-01·tls-alpn-01 の3つが同じtokenとして提示され、cert-managerはイシュアに指定された検証方式(ここではHTTP-01)のみを選択しました。cert-managerが使用する方式はHTTP-01とDNS-01の2つです。
  4. 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で十分です。

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 で計算します。実測値がそのまま示してくれます。

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 로 갱신한다.
図:公開証明書2つと自己署名証明書1つの有効期間、更新タイミング、読み取り日
証明書(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つずつ稼働していました。

cainjectorがないと、ウェブホックサーバーの証明書が更新された日にAPIサーバーが新しい証明書を信頼できなくなり、 該当する種類のオブジェクト作成がすべて失敗します。ウェブホック証明書の更新がサイレント障害になりやすい理由はここにあります。

7. ゲートウェイ・Ingressと連携する方法:shim

Certificateを個別に記述せず、ゲートウェイ側にアノテーションを1つ付与することも可能です。

TT Labクラスターのcontroller設定ファイルには旧名称である enableGatewayAPI: true が含まれており、Gatewayには cert-manager.io/cluster-issuer: <클러스터 이슈어> アノテーションが付いていました。その結果を読み解くと、shimの性質がよく見えてきます。

証明書が変更されたとき、ゲートウェイはどうやって気づくのか

ゲートウェイ(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. まとめ

Titi の一言。 証明書はラリーみたいなものです。ボールを打ち返す人が 2 人いたら終わりませんよ。1 つの Secret にオーナーも 1 人だけ!それに、「3 か月ごと」ではなく「有効期間の 3 分の 2」の時点でバトンタッチしなければ、ボールが短くなっても(45 日!)落とさずに済みますよ。

参考

ログインするといいねできます

コメント

まだコメントはありません。

ログインするとコメントできます