安全設計

このページは,ACME Conductor が何を資産とみなし,どんな攻撃者を想定し, それぞれの脅威に対して何を対策しているかを図解します.元になっているのは リポジトリの 脅威モデルと アーキテクチャ文書で, このページで断定的に書いていることは,すべてそちらの文書で詳しく裏付けられています. 「安全か」という単一の答えではなく,今日保証されていること・されていないことを 正直に分けて示します.

何を守るのか

脅威モデルが挙げる資産です.いちばん価値が高いのは秘密鍵で,Runner の一時領域と Certificate Store の外に決して存在してはならないとされています.

秘密鍵

最も価値の高い資産.Runner の一時領域と Certificate Store の外に決して存在してはならない.

DNS 書き込みの資格情報/ワークロード ID

ACME の DNS-01 チャレンジを完了するために使う.

Certificate Store の資格情報/ワークロード ID

Store への書き込み(および読み取り)に使う.

ACME アカウントの資材

External Account Binding(EAB)の HMAC 鍵を含む.

Target レジストリと CertificatePolicy

どの FQDN がどの制約の下で証明書を受け取れるかの支配権.ここの改ざんは不正発行への経路.

監査ログ

悪用の検出と調査のよりどころ.追記専用.

JobSpec / Result コントラクト

コントロールプレーンとデータプレーンの間の唯一のチャネル.その完全性が両者の分離を意味あるものにする.

Conductor と Runner の ID

各プロセスが実行に用いる資格情報またはワークロード ID.

発行された証明書そのものも資産ですが,公開のものであり,発行と有効性の正しさがセキュリティに関わります.

秘密はどこにあるか

Conductor は常駐するコントロールプレーンで,秘密鍵・DNS 資格情報・Store 資格情報のいずれも持ちません. 秘密鍵は 1 回ごとに起動される Runner の一時領域でだけ生まれ,Certificate Store に直接書き込まれた後に破棄されます. ADR 0005 は, Conductor のデータベーススキーマに秘密鍵・証明書本体・PFX・クラウド資格情報の列がそもそも存在しないこと, 秘密鍵を返す API エンドポイントが存在しないことを設計上の不変条件としています.

信頼境界と秘密鍵の在りか 管理者・UI から Conductor までが信頼できないネットワーク側.Conductor はコントロールプレーンで DNS・Store の資格情報も秘密鍵も持たない.Conductor と Runner の境界は JobSpec と Result の文書だけが越える.Runner はデータプレーンで秘密鍵はここにのみ一時的に存在する.Runner から先,DNS プロバイダ・ACME CA・Certificate Store との境界がもう一つの信頼境界であり,証明書と秘密鍵は Runner から Certificate Store へ直接書き込まれる. 信頼できないネットワーク 管理者 / UI 自由形式の入力 コントロールプレーン acme-conductor Target / Policy / 監査 ✕ 秘密鍵は持たない ✕ DNS 資格情報なし ✕ Store 資格情報なし JobSpec Result データプレーン(1 回限り) acme-runner 秘密鍵はここにのみ一時的に存在 1 つの DnsBinding・1 つの AcmeBinding・ 1 つの StoreBinding にスコープ 実行基盤のワークロード ID で認証 信頼境界 ― Runner だけが会話する外部システム DNS プロバイダ DNS-01 の TXT ACME CA(外部) 例: Let's Encrypt Certificate Store FS / Key Vault TXT 書き込み 発行要求 証明書 + 秘密鍵
コントロールプレーン データプレーン 秘密鍵が置かれる場所 外部システム
重要な境界は 4 つです.① 管理者/UI ↔ Conductor(自由形式の外部入力を受け付ける唯一の境界).② Conductor ↔ Runner(JobSpec と Result の文書だけが越える).③ Runner ↔ 実行基盤(Runner のワークロード ID は起動したものがプロビジョニングし,Conductor が資格情報を手渡すことは決してない).④ Runner ↔ DNS プロバイダ / ACME CA / Certificate Store(Runner だけが話す.証明書と秘密鍵は Runner から Certificate Store へ直接書き込まれる).

2 つの ID と最小権限

Conductor と Runner は別々のワークロード ID で動きます.Azure Container Apps へのデプロイでは, Bicep テンプレートが 2 つのユーザ割り当て ID をそれぞれ 1 つのカスタムロールにだけ紐付けます.Conductor の ID には DNS,Key Vault,ストレージデータのいずれの権限も与えません.

Conductor と Runner,2 つの ID の権限 Conductor の ID は Conductor Job Execution Observer ロールを持ち,Runner Job の実行の読み取り・一覧・停止だけができ,開始はできない.Runner の ID は DNS ゾーンに TXT の読み書き権限を持つ Runner DNS TXT Writer ロールと,Key Vault に証明書の読み取り・取り込み権限を持つ Runner Key Vault Certificate Writer ロールを持つ. Conductor の ID Job Execution Observer Runner Job(実行) ✓ jobs/execution/read ✓ jobs/executions/read ✓ jobs/stop/execution/action ✕ jobs/start/action は含まない 読み取り・一覧・停止のみ Runner の ID DNS TXT Writer / KV Writer DNS ゾーン ✓ ゾーン読み取り ✓ TXT の読み書き ✓ TXT の削除 Key Vault ✓ certificates/read ✓ certificates/import/action ✕ secrets/get は不要
Conductor の ID Runner の ID 意図的に持たせない権限
Runner の ID は Job・アプリ・ストレージには何も持たず,Conductor の ID は DNS・Key Vault・ストレージデータのいずれの権限も持ちません.どちらの ID もストレージアカウントキーを読めません.権限の付与は deploy/azure のカスタムロール定義としてレビュー可能に管理されています.

なぜ Conductor は Job を開始できないか. Job の「開始」API は実行テンプレート(イメージ・コマンド・マウント)を丸ごと差し替えられます. 侵害された Conductor にそれを許すと,任意コードを Runner の ID で実行できてしまいます. そのため Conductor の ID には jobs/start/action を持たせず,実行は Job 自身のスケジュールで始まり, Conductor が差し出したジョブを取っていく形にしています(ADR 0014).

検証と認可は別物

Conductor が生成する JobSpec は,Runner にとって信頼できない入力です.Validate と Authorize は意図的に異なる問いに答えるよう分離されており, ドキュメントとコードコメントは前者に対して「認可」という語を決して使いません (architecture.md「検証と認可」).

JobSpec の検証から認可,実行までの流れ Conductor が生成した JobSpec は,署名検証・期限確認・リプレイ台帳の確認を経て,厳密デコードと自己整合性の検証(Validate)を通り,最後に Runner 自身の信頼された設定による認可(Authorize)を経てはじめて実行される.Validate は文書が内部で辻褄が合っているかだけを見る自己整合性の検査であり,認可ではない.侵害された Conductor は自己整合した文書を作れてしまうので,発行を実際に制限するのは Authorize だけである. Conductor が JobSpec を生成 ① 署名検証 Ed25519.期限内 かつ未使用の runId (リプレイ台帳) ② Validate 厳密デコード+ 自己整合性の検証 (認可ではない) ③ Authorize Runner 自身の 信頼された設定 既定拒否 実行 lego を起動
Conductor 側 Runner 側 ここだけが実際の権限境界
RunnerAuthorizationPolicy(許可 DNS サフィックス,ワイルドカードの可否,許可バインディング名)は Runner 自身の信頼された設定から読み込まれ,JobSpec からは決して読みません.署名は生成者を認証し改ざんとリプレイを防ぎますが,署名鍵を持つ侵害された Conductor は依然として好きな内容に署名できます.発行できる範囲を最終的に決めるのは Authorize です.

Validate が保証するのは「文書が自己整合している」ことだけです. 侵害された Conductor は target.fqdn と埋め込まれた policy スナップショットを一緒に書き換えれば, Validate をそのまま通る自己整合した JobSpec を任意の FQDN について作れます. それを実際に制限するのは,JobSpec の外にある Runner 側の Authorize(RunnerAuthorizationPolicy)だけです.

API で指定できるのは名前だけ

API の入力でコマンド,コンテナイメージ,クラウドリソース ID,資格情報,プロバイダ設定を直接指定することは決してできません. 指定できるのは,管理者があらかじめ登録した論理バインディングの名前(DNS ラベルに似た短い文字列)だけです.

バインディング何を指す名前か
ExecutionBindingRunner ジョブが実際にどこでどう動くか(ローカルプロセス/Azure Container Apps Job)
DnsBindingACME DNS-01 チャレンジをどの DNS プロバイダのどのゾーンに書くか
StoreBinding結果をどの Certificate Store に書くか
AcmeBindingRunner がどの ACME ディレクトリ・アカウント・(任意の)EAB として認証するか

名前が何に解決されるか(実際のディレクトリ URL,資格情報,ワークロード ID)は Runner 側の起動時設定であり, Conductor がそれを見ることは決してありません.これにより,任意のインフラ値が API 入力から完全に排除されます. バインディングはすべて起動時設定から読み込まれ,実行時に作成・変更する管理 API はありません.

FQDN のラベル境界照合

FQDN はポリシーの許可サフィックスに対して ラベル単位で検査され,生の文字列サフィックスでは検査されません. 比較の前に,前後の空白除去,末尾ドット 1 つの除去,ASCII 英字の小文字化を行い,非 ASCII と xn--(IDNA)ラベルは拒否します (architecture.md「FQDN の正規化規則」).

ラベル境界でのサフィックス照合の例 許可サフィックスが example.ac.jp のとき,www.example.ac.jp と *.example.ac.jp は完全一致か直前にドットがあるため一致する.evil-example.ac.jp は文字としては example.ac.jp で終わるが,直前にドットがなく一致しない. 許可サフィックス: example.ac.jp www.example.ac.jp 一致(境界あり) *.example.ac.jp 一致(ワイルドカード可) evil-example.ac.jp 不一致(境界なし)
許可サフィックスの下にある 許可サフィックスの下にない
evil-example.ac.jp は文字としては example.ac.jp で終わりますが,境界がちょうどラベル区切りに落ちる「完全一致,または直前に .」を満たさないため,許可サフィックスの下には ない と判定されます.ワイルドカードは常に左端ラベル全体としてのみ受理され,*.ac.jp のような TLD 丸ごとのワイルドカードは拒否されます.この正規化とラベル境界照合は Conductor と Runner の両方で同一コード(internal/policy/fqdn.go)により適用されます.

脅威と対策の一覧

脅威モデルが列挙する T1〜T16 の要約です.詳細な説明と実装状況はthreat-model.md「脅威」を参照してください.

ID脅威主な対策
T1Conductor の侵害Conductor は秘密鍵/DNS/Store 資格情報を持たない.Runner 側の認可ポリシーが既定拒否.Container Apps では Conductor の ID は Job の開始ができない.
T2転送中・保存中の JobSpec 改ざん厳密デコード.Ed25519 署名付きエンベロープ(JobSpec と Result の両方).改変は署名検証で拒否.
T3JobSpec のリプレイと期限署名エンベロープの期限(既定 15 分)とノンス.Runner のリプレイ台帳.target 無効化・リビジョン変更時のキャンセル.
T4DNS 権限の悪用DnsBinding の設定検証(危険なプロバイダは拒否).実際のゾーンスコープはデプロイ側の運用上の責務として残る(残存リスク).
T5FQDN ポリシーのバイパス正規化+ラベル境界照合.重複 JSON キー拒否.Validate(自己整合性)と Authorize(信頼された設定による認可)の分離.
T6ログ/結果/エラーへのシークレット漏洩Result への生の外部出力コピーを禁止.固定テンプレートの error.summary.lego 出力の値ベース秘匿(網羅的ではない,残存リスク).
T7同じ target の二重実行target ごとに最大 1 つのアクティブ run(DB の部分一意インデックス).楽観ロック.アドバイザリロックで store 書き込みを直列化.
T8サプライチェーンlego はバージョン固定.ダイジェスト固定のベースイメージ.SBOM/provenance/署名付き証明.govulncheck.GitHub Actions のタグ固定は未対策.
T9巨大/敵対的な文書による DoS64 KiB 上限.ネスト深さ上限(8 段)で超線形コストを回避.
T10Conductor/Runner の権限分離の失敗Bicep でレビュー可能な 2 つの ID.Conductor には DNS・Store 権限を与えない配線.ローカルランチャーの passthroughEnv は開発専用(残存リスク).
T11無効化と purge の混同MVP に purge 操作なし.無効化は履歴を残す.target/run/policy の DELETE は監査で中断される.
T12テストからの本番 CA の誤用フェイルクローズの許可規則(ステージング/テスト以外は allowProductionCA が必須).テストは偽の lego のみを使用.
T13意図しない呼び出し元による API 到達localhost-dev はループバック限定+Host/Origin/Sec-Fetch-Site 検査.oidc モードは署名・issuer・audience・ロールを検証するベアラートークン必須.
T14盗まれた/過剰付与のベアラートークンTLS 必須.GUI はトークンをタブのセッションストレージにのみ保持.監査ログは安定識別子+issuer で記録.失効検査は未対応(残存リスク).
T15攻撃面としての GUIDOM 描画のみで XSS を防止.厳格な CSP.frame-ancestors 'none'.PKCE 付き認可コードフロー.
T16(削除)入力チャネルとしての移行一覧移行ツールの削除により対象がなくなりました(ADR 0025).

残っているリスク

脅威モデルの「残存リスクと未対策事項」から,特に評価者が知っておくべきものを挙げます. 網羅的な一覧はthreat-model.mdを参照してください.

脆弱性の報告

セキュリティ脆弱性が疑われる場合は,公開の issue や議論チャンネルには投稿せず,このリポジトリの GitHub のプライベート脆弱性報告を使ってください: CITS-NUE/acme-conductor → Security タブ → Report a vulnerability. これによりメンテナだけが閲覧できる非公開のアドバイザリが開かれます.

対象範囲は FQDN/ポリシーの迂回,シークレットの漏えい,ID 分離の破れ,JobSpec/Result コントラクトの迂回, サプライチェーンの問題,コンテナ/ランタイム強化の退行です.すでに管理者権限を持っていることを前提とする発見や, このページの「残っているリスク」にすでに挙げられている既知の未対策事項は対象外です. 詳しくはSECURITY.mdを参照してください. 本プロジェクトは専任のセキュリティチームも正式な SLA も持たない,活発に開発中の若いプロジェクト(v1 前の v0.x)であり,対応はベストエフォートです.