HMAC鍵配布サーバの締め出し事故を防ぐ設計(複数秘密+使い捨てenrollトークン+失敗の自己通報)
共有秘密で設定ファイルを配る構成は、秘密をローテーションした瞬間に未受領の端末を永久に締め出す。複数秘密の同時受付・使い捨てenrollトークン・失敗を端末自身が通報する設計と、実経路e2eテストの書き方まで。
約3.9万トークンの節約 (API料金換算で約59円分)。 要件定義・技術調査・試行錯誤ぶんのトークンがまるごと不要になります。※ 出品者申告とレビューに基づく推定値。モデル・タスク内容により変動します。
この巻物について
「HMAC鍵配布サーバの締め出し事故を防ぐ設計(複数秘密+使い捨てenrollトークン+失敗の自己通報)」は、開発プロセスカテゴリのAI指示書(MDファイル)です。共有秘密で設定ファイルを配る構成は、秘密をローテーションした瞬間に未受領の端末を永久に締め出す。複数秘密の同時受付・使い捨てenrollトークン・失敗を端末自身が通報する設計と、実経路e2eテストの書き方まで。この巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約3.9万トークン(API料金換算で約59円)・87%のトークンを節約できます。
- カテゴリ
- 開発プロセス
- 対応AI
- claude-code、cursor、codex-cli
- ライセンス
- 商用利用可 (再販不可)
- 価格
- 無料
- ゼロから開発時
- 約4.5万トークン
- この巻物使用時
- 約6,000トークン
- 節約量
- 約3.9万トークン (約59円)
- 更新日
- 2026-08-20
使い方 (AIに渡す3つの方法)
いちばん簡単なのはワンライナー。Claude Code のターミナルに貼るだけです。
claude "https://makimono-md.vercel.app/api/v1/files/hmac-enroll/raw を読み込んで、この指示書どおりに実装して"
中身
HMAC認証つき設定配布サーバの堅牢化と、失敗が「見える」クライアント設計
何を解決するか
複数の端末(社員PC等)へ、APIキー等を含む設定ファイルを中央サーバから配布する構成を考える。サーバは共有秘密によるHMAC認証でクライアントを検証する。クライアントは現在時刻 ts と HMAC-SHA256(secret, ts) をヘッダに載せて送り、サーバは ±300秒の時刻窓(リプレイ・時計ずれ対策)で検証する。
この構成で必ず発生するのが 秘密ローテーション時のブートストラップ問題 である。サーバが新しい秘密「1本だけ」を受け付ける実装にすると、ローテーションを実行した瞬間に「まだ新しい秘密を受領していない端末」は認証に失敗する。そして認証に失敗する端末には以後1本も設定ファイルが降りてこないため、新しい秘密を受け取る手段そのものが失われ、永久に締め出される。鶏と卵である。
さらに厄介なのは、この事故が 静かに進行する ことだ。配布スクリプトが失敗をログファイルにだけ書く実装だと、人はログを見に行かないので誰も気付かない。実際に数日間放置された事例がある。
本書は次の3点を、他環境でもそのまま再現できる形で規定する。
- A. 複数秘密の同時受付(ローテーション中も既存端末を切らない)
- B. 使い捨て enroll トークン(締め出された端末の自己回復)
- C. 失敗の能動通報(ログに埋もれさせない)
よくある壊れ方(アンチパターン)
- 秘密を1本だけ受け付ける: ローテーション即全滅。回復手段も同時に失われる。
- 失敗をログファイルにだけ書く: 人はログを能動的に見に行かない。数日〜数週間気付かれない。
- HMAC一致で早期 return する: 一致位置で処理時間が変わり、タイミング側信道から候補の並びや一致位置の情報が漏れうる。
- 秘密やHMAC値をログに出す: ログ集約基盤経由で秘密が拡散する。
- 「既存ファイルには上書きしない」配布: 全端末に既にあるファイル名で新しい値を配ろうとしても永久に届かず、エラーも出ない(後述)。
- 通報失敗で本体を止める: webhook障害でセッション開始フック等が巻き添えで落ち、被害が拡大する。
設計
サーバ: 3系統の秘密を同時受付
- primary: 現行の正規秘密。1本。
- legacy: ローテーション猶予期間中だけ有効な旧秘密。複数本可。
- enroll: 使い捨ての一時トークン。複数本可(端末ごとに発行する想定)。
クライアントは常に1本の秘密で署名してくるだけなので、クライアント側の実装変更は不要 なのがこの設計の利点である。サーバ側だけが複数候補を試す。
環境変数からの読み込みは、運用者の書き方の揺れを吸収するため、JSON配列・改行区切り・カンマ区切りのどれでもパースできる パーサを用意する(["a","b"] / a\nb / a,b をすべて受理)。
検証の原則
- 時刻窓:
|now - ts| <= 300秒。窓外は即拒否。 - 比較は必ず
timingSafeEqual相当の定時間比較を使う。 - 一致しても早期 return せず全候補を評価 し、一致した候補が1つ以上あれば受理。タイミング差で「何番目の候補で一致したか」を外部から観測されにくくする。
- ログには 一致した候補のラベル(
primary/legacy#0/enroll#2等)だけを出す。秘密の値・HMAC値は絶対に出さない。
enroll トークンによる自己回復
締め出された端末への回復手順:
- 管理者が端末ごとにランダムな enroll トークンを1本発行し、サーバの enroll リストに追加する。
- そのトークンを 1回だけ 端末へ(別経路で)渡す。
- 端末は enroll トークンで認証し、正規の primary 秘密が入った設定ファイルを自分で受け取る。
- 回復が確認できたら、サーバの enroll リストからそのトークンを削除する。
これにより、マスター秘密を人手で配らなくて済む。enroll トークンは漏れても使い捨てで、回復後に無効化できる。
クライアント: 失敗を「見える化」する
配布処理が失敗したら:
- (a) 人が見る標準出力に1行 出す(セッション開始フック等、端末利用者の目に入る場所)。
- (b) その端末が既に持っている別の通知経路(チャットの webhook 等)へ 自分から通報する。
通報の設計:
- 抑止: 状態ファイルに「最後に通報した時刻」を保存し、24時間に1回までに制限する(障害時の通知洪水を防ぐ)。
- 本文に秘密値・HMAC値を含めない。含めるのはホスト名、エラー種別、HTTPステータス、発生時刻程度。
- 通報自体の失敗は握り潰す。例外を外に投げず、本体(セッション開始等)を絶対に止めない。
- webhook へサーバ以外(スクリプト)から POST する場合、User-Agent ヘッダが必須 のことがある。無いと 429 等で弾かれるので必ず付ける。
配布物の書き込み規則
「既に存在するファイルには書かない」(flag: 'wx' 相当)実装は罠である。全端末に既に存在するファイル名で新しい値を配ろうとしても、永久に届かず、エラーも出ない。新しい値は 必ず新規ファイル名 で配ること。古い値の無効化は配布物とは別の仕組み(サーバ側でのキー失効等)で行う。
実装(擬似コード)
サーバ
function parseSecretList(raw): // 3形式を吸収
if raw is empty: return []
if raw starts with "[": return JSON.parse(raw) // JSON配列
if raw contains "\n": return split(raw, "\n").trim().nonEmpty()
return split(raw, ",").trim().nonEmpty() // カンマ区切り
SECRETS = {
primary: [env.PRIMARY_SECRET],
legacy: parseSecretList(env.LEGACY_SECRETS),
enroll: parseSecretList(env.ENROLL_TOKENS),
}
function verify(ts, sig):
if abs(now() - ts) > 300: return REJECT("stale")
matched = []
for label, candidates in SECRETS:
for i, secret in candidates:
expected = HMAC_SHA256(secret, ts)
// 早期returnしない。全候補を必ず評価する
if timingSafeEqual(expected, sig):
matched.append(label + "#" + i)
if matched is empty: return REJECT("bad_signature")
log("auth ok: " + matched[0]) // ラベルのみ。秘密値は出さない
return ACCEPT
クライアント(配布 + 通報)
function fetchConfig():
ts = now()
sig = HMAC_SHA256(loadSecret(), ts)
res = HTTP_GET(<配布サーバのURL>, headers={X-Ts: ts, X-Sig: sig})
if res.status != 200: throw DistError(res.status)
for item in res.body.files:
writeFileAtomic(item.path, item.content) // 新規ファイル名のみ
function reportFailure(err): // 失敗しても絶対に投げない
try:
state = readJson(STATE_FILE) or {}
if now() - state.lastReportedAt < 24h: return // 抑止
body = { text: "[配布失敗] host=" + hostname
+ " status=" + err.status + " at=" + isoNow() }
HTTP_POST(<通報用webhookのURL>, json=body,
headers={"User-Agent": "config-dist-agent/1.0"})
state.lastReportedAt = now()
writeJson(STATE_FILE, state)
catch _: pass // 握り潰す
function main():
try:
fetchConfig()
catch err:
printStdout("[警告] 設定配布に失敗しました: status=" + err.status)
reportFailure(err) // 本体は止めない
検証手順
単体テスト(純粋関数)
- 抑止判定:
lastReportedAtが23時間前→抑止される、25時間前→通報される、未設定→通報される。 - 本文生成: 秘密値・HMAC値が本文に含まれないことを、本文文字列に対する否定アサーションで確認する。
- パーサ: JSON配列・改行区切り・カンマ区切り・空文字の4系統。
e2eテスト(実経路)
モックではなく 実プロセス・実HTTP で検証する。外部ネットワークには出ない。
node:http相当で 常に401を返す偽の配布サーバ を localhost に立てる。- 同じく 通報を受け取る webhook 受け口 を立て、受け取ったリクエストを配列に記録する。
tmpdirに 一時HOME を作り、環境変数(HOME、配布サーバURL、webhook URL、状態ファイルパス)を差し替えて、配布スクリプトを子プロセスとして実際に実行 する。- アサート:
- webhook 受け口に 1件 POST が届いた。
- 本文にホスト名とステータスが含まれ、秘密値が含まれない。
User-Agentヘッダが存在する(無いと本番で429になるため必須)。- 2回目の実行では通報が抑止される(受信は1件のまま)。
- 子プロセスの終了コードが正常(通報失敗時も本体が死なないこと)。
この構成なら CI でも再現性よく通る。
運用チェックリスト
- ローテーション手順が「新秘密を primary に、旧秘密を legacy に追加 → 全端末の回収を確認 → legacy から削除」の順になっているか(いきなり差し替えていないか)。
- legacy / enroll の環境変数が3形式どれで書かれても動くことを確認したか。
- 認証ログにラベル以外(秘密値・HMAC値)が出ていないか。
- 締め出し端末用に enroll トークンを発行→回復→削除する runbook があるか。
- 配布失敗時に標準出力1行 + webhook 通報の両方が出ることを、偽サーバで定期的に確認しているか。
- 通報が24時間抑止で洪水にならないことを確認したか。
- 新しい値を 新規ファイル名 で配っているか(既存ファイル名の値を変えようとしていないか)。
- webhook POST に User-Agent を付けているか。
- 通報経路が落ちていても端末の本体処理が止まらないことを確認したか。
よくある質問
+「HMAC鍵配布サーバの締め出し事故を防ぐ設計(複数秘密+使い捨てenrollトークン+失敗の自己通報)」とは何ですか?
共有秘密で設定ファイルを配る構成は、秘密をローテーションした瞬間に未受領の端末を永久に締め出す。複数秘密の同時受付・使い捨てenrollトークン・失敗を端末自身が通報する設計と、実経路e2eテストの書き方まで。
+どれくらいトークン(費用)を節約できますか?
ゼロから開発すると約4.5万トークンかかりますが、この巻物を使えば約6,000トークンで済みます。差し引き約3.9万トークン(API料金換算で約59円)・87%の節約です。
+どうやって使いますか?
無料です。MDファイルを Claude Code などのAIに読み込ませるだけ。ワンライナーをターミナルに貼れば実装が始まります。要件定義や技術調査を省いて実装だけにトークンを使えます。
+どのAIツールに対応していますか?
claude-code、cursor、codex-cli に対応しています。
+商用利用できますか?
ライセンスは「商用利用可 (再販不可)」です。
🤝 自分でAIを動かすのは、まだ不安…という方へ
この巻物の内容を、AIを使うプロに丸ごと任せることもできます。姉妹サービスAI代行堂なら「LINEで頼むだけで、仕事が完成」。
関連する巻物
ドキュメント駆動開発プロセス CLAUDE.md — 作るものを固めてから書かせる
「AIが暴走して意図と違うものを作る」を根絶する開発プロセス指示書。UI仕様→機能設計→実装の順をAIに強制し、1ファイルごとに承認ゲートを挟む。受託開発・チーム開発向け。
AIに指示書マーケットを自動参照させ、終了時に自動出品させるMD
開発依頼を受けた瞬間にマーケットの完成済み指示書を検索してAIに読ませ、セッション終了時には汎用ノウハウを自動出品させる仕組みの作り方。全台配布・秘密情報スキャン・実際に踏んだ配布バグ3つの回避込み。
「そのPCにしか直せない障害」をAIに自分で気付かせて着手させる
特定の1台にしかリポジトリが無い機能は、修正手順を書いても誰にも実行されず放置される。SessionStart hook で当該PCのAIだけに指示を出し、完了後は指示書へ状態を書き戻して再実装事故を防ぐ型。走査の時間予算とセッション跨ぎの再開、メール一致だけの自動承認がなりすまされる理由と署名キー方式、状態問い合わせAPI、鍵の自動配布、no-op通知の抑止まで、実際に94件の滞留を解消した実例に基づく手順。
この巻物、誰かのトークンも救えます
𝕏 で節約レシートをシェア