仕組み: 1 枚の証明書が
更新されるまで

このページでは,1 つのホスト名を登録してから証明書が実際に更新されるまでに, Conductor と Runner の間で何がやり取りされるかを順を追って説明します. まず登場する概念を整理し,次に全体の流れ,Run の状態遷移,更新要否の判断, Runner の 1 回の実行,JobSpec/Result の実例,障害時の振る舞いの順に見ていきます.

登場する概念

管理者が API・GUI で操作するのは「名前」だけです.コマンドや資格情報そのものを 直接指定することはできません.次の概念を押さえておくと,以降の図が読みやすくなります.

Target

管理対象の証明書 1 枚を表すレコードです.主たる FQDN(ホスト名)と,必要なら追加の名前(SAN)を持ち, どの CertificatePolicy に従うか,どのバインディング名を使うかを持ちます.1 つの名前は 1 つの Target にしか属しません.

CertificatePolicy

許可する DNS サフィックス,ワイルドカードの可否,鍵の種類(keyType), 更新を始める日数(renewBeforeDays)などの規則です.複数の Target が 1 つの ポリシーを共有できます.

4 種類のバインディング

ExecutionBinding・DnsBinding・StoreBinding・AcmeBinding はいずれも管理者が登録した 論理名です.Target やポリシーはこの名前だけを運びます. ExecutionBinding は Conductor 自身の設定で launcher に解決されますが, DnsBinding・StoreBinding・AcmeBinding の名前が実際に何を指すか (ディレクトリ URL,DNS プロバイダの資格情報,Key Vault の URL など)は Runner 自身の設定ファイルだけで解決されます.Conductor はその実体を知りません.

Run と AuditEvent

Run は 1 回の reconcile 試行の記録で, queued/starting/running/succeeded/failed/cancelled のいずれかの状態を持ちます. AuditEvent は target の作成・更新,run の開始・失敗,ポリシー拒否などを記録する 追記専用のログです.

名前とその解決の分離 Conductor 側の Target は CertificatePolicy を参照し,4 種類のバインディングは論理名だけを持つ.executionBinding は Conductor 自身の設定で launcher に解決されるが,acme/dns/store の 3 つの名前は,Conductor には見えない Runner 側の設定ファイルの中だけで実体に解決される. Conductor 側(名前だけを保持する) executionBinding だけは Conductor 自身の設定で launcher に解決される Target fqdn: wiki.example.ac.jp policyRef: 01JPOLICY… executionBinding: "local" dnsBinding: "azure-dns-…" storeBinding: "filesystem-dev" — どれも文字列の名前 — CertificatePolicy allowedDnsSuffixes allowWildcard renewBeforeDays keyType acmeBinding: "letsencrypt-…" Run / AuditEvent Run: queued / starting / running / succeeded / failed / cancelled AuditEvent: target・run・policy の イベントを記録(追記専用) 証明書の実体はここに含まれない acme/dns/store の名前だけを渡す Runner 側(acme/dns/store の名前を実体に解決する唯一の場所) acmeBindings letsencrypt-… → directoryURL・EAB dnsBindings azure-dns-… → provider・env storeBindings filesystem-dev → vaultURL 等
Conductor が持つ情報(名前のみ) Runner 側だけが持つ実体
Conductor が API で受け付けるのはあらかじめ登録済みのバインディング名だけであり,コマンドや 資格情報を直接指定することはできません.名前が何に解決されるかは Runner の設定であり, Conductor がそれを見ることは決してありません.詳しくは architecture.md: バインディングモデル を参照してください.

全体の流れ

以下は,証明書の期限が近づいてから新しい証明書が Certificate Store に格納されるまでの 典型的な流れです.横方向が登場人物,縦方向が時間の経過を表します.

証明書が更新されるまでのシーケンス 管理者が target を登録すると,Conductor のスケジューラが期限到来を判断して run を待機状態にし,署名付き JobSpec を Runner に渡す.Runner は検証と認可を行い,Certificate Store に現在の証明書を問い合わせ,更新が不要なら noop で終了し,必要なら lego を起動して ACME CA・DNS とやり取りし,証明書を Store に格納してから Result を Conductor に返す. 管理者 Conductor Runner Certificate Store ACME CA / DNS ① target 登録 POST /targets(fqdn・policyRef・binding 名) ②スケジューラが 期限到来を判断 run が queued になる ③ JobSpec(署名付き) ポリシーの値はスナップショットとして同梱 ④検証・認可 (自身の設定で) ⑤現在の証明書を問い合わせ 格納済みの証明書(公開部分) ⑥まだ有効なら noop で終了(lego は起動しない) 更新が必要な場合はここから続く ↓ ⑦ lego を起動 ⑧ DNS-01 チャレンジの TXT を書き込み 伝播確認 ⑨証明書の発行を要求 証明書とチェーン ⑩発行結果を 検証 ⑪証明書+秘密鍵を格納 ⑫ Result(署名付き) action・fingerprint・expiresAt/error ⑬ Run を succeeded/failed に記録
Conductor が関わるやり取り Runner が関わるやり取り 秘密鍵が動く経路
Conductor が直接話すのは管理者(API/GUI)だけです.ACME CA・DNS・Certificate Store と話すのは すべて Runner です.詳しくは runner.md: 実行フロー と conductor.md: Run のライフサイクルとスケジューリング を参照してください.

Run の状態遷移

1 回の reconcile 試行は Run というレコードで表され,次のいずれかの状態を持ちます. ある target について,同時に queued/starting/running の run が存在できるのは 1 つだけです.2 つ目を要求すると,アクティブな run を示す 409 run_active が返ります.

Run の状態遷移 中央の列が進行中の状態で,queued からスケジューラのクレームで starting,Runner の起動で running になり,Result が成功なら succeeded になる.左の cancelled へは,queued と running からは API のキャンセルで,starting からは target やポリシーの無効化または revision の変更で遷移する.右の failed へは,starting からは JobSpec の拒否または起動の失敗で,running からは Result の失敗やタイムアウトなどで遷移する. 終端 進行中(target ごとに最大 1 つ) 終端 queued スケジューラがクレーム starting Runner を起動 running Result が成功 succeeded cancelled API でキャンセル target/ポリシー無効化 revision の変更 API でキャンセル failed JobSpec の拒否 起動の失敗 Result が失敗 タイムアウトなど
成功で終わる遷移 失敗・キャンセルにつながる遷移
失敗またはキャンセルされた run の後は,retryBackoffSeconds(既定 300 秒)待ってから 同じ target を再試行します.連続して失敗するたびに待ち時間は 2 倍になり, maxRetryBackoffSeconds(既定 21600 秒)で頭打ちになります.詳しくは conductor.md: Run のライフサイクルとスケジューリング を参照してください.

更新が必要かの判断

「そろそろ確認する時期か」を決めるのは Conductor のスケジューラですが, 「実際に発行・更新が必要か」を最終的に決めるのは常に Runner です. この 2 つの判断は分かれています.

Conductor 側: run を queued にするかどうか

有効なポリシーを持つ有効な target について,一度も成功していない,target のリビジョンが 最後に成功した run と異なる,最後の成功が有効期限を記録していない,または expiresAt − renewBeforeDays を過ぎていれば run を待機状態にします. これは「確認しに行くべきか」という判断であり,「発行すべきか」ではありません.

Runner 側: 実際に lego を起動するかどうか

Runner は Certificate Store に現在の証明書を問い合わせ,格納済みの証明書が target の名前(FQDN と追加名)と SAN が過不足なく一致し,すでに有効で,鍵の種別がポリシー通りで, renewBeforeDays より先まで有効なら,その場で noop として終了します.lego は一度も起動されません. そうでなければ発行・更新に進みます.

Result の action は 3 種類です.issued はそのオブジェクトについて store に証明書が存在しなかった場合, renewed は証明書が存在し新しいフィンガープリントが以前と異なる場合, noop は上記の早期終了の場合です.Conductor が「期限到来」と判断した run でも,Runner が証明書はまだ最新だと確認すれば ACME CA には一切接触せず noop で終わります.

Runner の 1 回の実行

Runner は起動されるたびに,ちょうど 1 つの JobSpec を処理してから終了する使い捨てのプロセスです. 次の手順は runner.md: 実行フロー を要約したものです.

  1. ジョブを読む — 署名付きエンベロープなら署名を検証し,リプレイ台帳と照合する.
  2. 厳密に検証する — 未知のフィールドや重複キーを拒否し,文書が自己整合していることを確かめる(これは認可ではない).
  3. 自身の設定で認可する — RunnerAuthorizationPolicy に照らし,DNS サフィックスとバインディング名を既定拒否で検査する.
  4. バインディング名を解決する — acme/dns/store の 3 つの名前を自身の設定の実体(URL・資格情報の変数名など)に変換する.
  5. Certificate Store に現在の証明書を問い合わせる — 更新が不要なら noop でここまでにする.
  6. 作業ディレクトリを作る — <workDir>/run-<runId>-<rand>(モード 0700).ACME アカウントの状態だけを状態ディレクトリからコピーする.
  7. lego の argv と環境をゼロから組み立てる — Runner プロセス自身の環境は継承しない.JobSpec の値がシェル文字列に補間されることはない.
  8. lego をちょうど 1 回起動する — 専用のプロセスグループで,タイムアウト付きで実行する.
  9. 生成された証明書を検証する — SAN が target の名前と一致すること,鍵が一致すること,まだ失効していないことなどを確認する.
  10. Certificate Store に格納する — ここでのみ証明書と秘密鍵が永続化される.
  11. 作業ディレクトリを破棄する — defer により,成功・失敗を問わずすべての経路で実行される.これが秘密鍵のローカルコピーを消す唯一の処理である.
  12. Result を出力して終了する — stdout に 1 行の JSON,指定があればファイルにも.

引数ベクトルは常に固定の順序で組み立てられ(--accept-tos --email … --server … --dns … --domains … --key-type … --path … の後に run),シェルは一度も起動されません.EAB のキー ID と HMAC は argv ではなく LEGO_EAB_KID/LEGO_EAB_HMAC という環境変数で渡されるため,プロセス一覧には現れません. 詳しくはrunner.md: lego の起動を参照してください.

JobSpec と Result の例

Conductor と Runner の間を行き来する文書はこの 2 種類だけです.どちらも厳密にデコードされ (未知のフィールドと重複キーは拒否),現れるのは不透明な識別子・正規化された FQDN・ ポリシーの値・論理バインディング名だけです.

JobSpec(CertificateReconcileJob.実例):

{
  "apiVersion": "acme-conductor.cits-nue.github.io/v1alpha1",
  "kind": "CertificateReconcileJob",
  "runId": "01JABCDEFGHJKMNPQRSTVWXYZ0",
  "target": { "id": "01JABCDEFGHJKMNPQRSTVWXYZ1", "fqdn": "wiki.example.ac.jp", "revision": 3 },
  "policy": {
    "allowedDnsSuffixes": ["example.ac.jp"],
    "allowWildcard": false,
    "renewBeforeDays": 30,
    "keyType": "ec256"
  },
  "acme": { "binding": "letsencrypt-staging" },
  "dns": { "binding": "azure-dns-staging" },
  "store": { "binding": "filesystem-dev" }
}

Result(CertificateReconcileResult)成功時:

{
  "kind": "CertificateReconcileResult",
  "runId": "01JABCDEFGHJKMNPQRSTVWXYZ0",
  "status": "succeeded",
  "action": "issued",
  "expiresAt": "2026-12-19T00:00:00Z",
  "fingerprintSha256": "a1b2c3d4…",
  "storeObjectRef": "www-example-ac-jp",
  "error": null
}

失敗時:

{
  "kind": "CertificateReconcileResult",
  "runId": "01JABCDEFGHJKMNPQRSTVWXYZ0",
  "status": "failed",
  "action": "failed",
  "error": { "code": "DnsFailure", "summary": "TXT record propagation timed out" }
}

どちらの文書にも 決して現れないもの があります: シェルコマンド,実行ファイルの パス,任意の環境変数,コンテナイメージの参照,クライアントシークレットやアクセスキーや秘密鍵, 任意のクラウドリソース ID,任意の出力パスです.そのためのフィールドがそもそも存在しません. error.summary も lego の生の出力や環境変数のダンプではなく,Runner が持つ安全な テンプレートからのみ生成されます.詳しくは architecture.md: JobSpec/Result コントラクト と runner.md: Result とエラーコード を参照してください.

障害時の振る舞い

Conductor が停止・再起動すると

SIGTERM/SIGINT を受けると,実行中の run は最大 server.shutdownGraceSeconds まで継続してからキャンセルされます. 再起動時,レジストリにまだ starting または running として残っている run は,Runner が実際に何をしたか分からないまま「outcome unknown(結果不明)」として failed と記録されます.Conductor が run を再開することは決してありません.

取り残された Runner はどうなるか

強制終了で取り残された Runner が Store への書き込みを完了させているかもしれません. その場合,Conductor は何も推測せず,単に「結果不明」として記録するだけです. 次に期限到来の検査が新しい run をスケジュールしたとき,Runner が Certificate Store に 問い合わせて証明書がすでに最新であることを見つければ,CA には接触せず noop で終わります.そうでなければ改めて更新します.

この設計により,Conductor 側の「結果不明」という保守的な記録と,Runner 側の 「Store に問い合わせてから判断する」という冪等な振る舞いの組み合わせだけで, 二重発行や証明書の取りこぼしを防いでいます.詳しくは conductor.md: シャットダウンと復旧 を参照してください.