仕組み: 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 の開始・失敗,ポリシー拒否などを記録する
追記専用のログです.
全体の流れ
以下は,証明書の期限が近づいてから新しい証明書が Certificate Store に格納されるまでの 典型的な流れです.横方向が登場人物,縦方向が時間の経過を表します.
Run の状態遷移
1 回の reconcile 試行は Run というレコードで表され,次のいずれかの状態を持ちます.
ある target について,同時に queued/starting/running
の run が存在できるのは 1 つだけです.2 つ目を要求すると,アクティブな run を示す
409 run_active が返ります.
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: 実行フロー を要約したものです.
- ジョブを読む — 署名付きエンベロープなら署名を検証し,リプレイ台帳と照合する.
- 厳密に検証する — 未知のフィールドや重複キーを拒否し,文書が自己整合していることを確かめる(これは認可ではない).
- 自身の設定で認可する —
RunnerAuthorizationPolicyに照らし,DNS サフィックスとバインディング名を既定拒否で検査する. - バインディング名を解決する — acme/dns/store の 3 つの名前を自身の設定の実体(URL・資格情報の変数名など)に変換する.
- Certificate Store に現在の証明書を問い合わせる — 更新が不要なら noop でここまでにする.
- 作業ディレクトリを作る —
<workDir>/run-<runId>-<rand>(モード 0700).ACME アカウントの状態だけを状態ディレクトリからコピーする. - lego の argv と環境をゼロから組み立てる — Runner プロセス自身の環境は継承しない.JobSpec の値がシェル文字列に補間されることはない.
- lego をちょうど 1 回起動する — 専用のプロセスグループで,タイムアウト付きで実行する.
- 生成された証明書を検証する — SAN が target の名前と一致すること,鍵が一致すること,まだ失効していないことなどを確認する.
- Certificate Store に格納する — ここでのみ証明書と秘密鍵が永続化される.
- 作業ディレクトリを破棄する —
deferにより,成功・失敗を問わずすべての経路で実行される.これが秘密鍵のローカルコピーを消す唯一の処理である. - 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: シャットダウンと復旧 を参照してください.