小規模チームの多環境シークレット管理:SOPS + age + ドキュメント権限=クレデンシャル

2026-08-06#secret-management#devops#security#infra#tooling

背景:解くべきは暗号化ではなくプロセス

あるマルチサービスのバックエンド案件で、ランタイム設定は一時期三か所に散らばっていた:リポジトリにコミットされた開発用設定ファイル、デプロイ先ホストで手作業維持される平文ファイル、そして .gitignore だけに守られた大量のローカル env。本物のクレデンシャル(モデルベンダーの API key、JWT secret、データベースのパスワード)には「レビューされ、バージョンがある」置き場が一つもなかった——README は開発者に git 追跡下のファイルへ key を貼り付けるよう教えており、恒久的な漏洩まで git add 一回の距離。ホスト側の設定は静かにドリフトしていた。

方針の目標は最初から「より強い暗号化」ではなく、プロセス上の四つだった:環境ごとに一枚のマニフェスト(manifest)へ環境依存の値をすべて載せる;暗号文をリポジトリにコミットできる;新人が clone すれば動かせる;CD は環境レベルの鍵で復号する。

中核モデル

環境ごとに一枚の env.yamlSOPS + age で暗号化してリポジトリへコミットし、その環境の全値の唯一の真実源とする。構造(shape)はテンプレートに、値は manifest に住み、レンダリング一段で両者を各サービスの起動設定へ合成する——環境ごとに複製された設定ファイル一式は存在しない。一環境に一対の age 鍵。dev の鍵で prod は決して開けない。秘密鍵の配布に新しいアカウント体系は作らない:コラボレーションプラットフォーム(飛書多維表格)の環境別に隔離されたドキュメントへ入れ、ドキュメントのアクセス権限そのものがクレデンシャルになる——新人は dev ドキュメントの権限をもらい、冪等な setup コマンド一発で鍵の取得・復号・レンダリング・スタック起動まで進む。CD パイプラインの鍵はパイプライン自身の環境レベル secret store から来て、ドキュメントプラットフォームからは決して取らない。

鍵となる決定

  1. 暗号文はリポジトリへ。リポジトリが唯一の権威。 ドキュメントプラットフォーム上の vault はミラー+復旧チャネルにすぎず、収束の向きは常にリポジトリ → vault の一方向。vault が手で書き換えられたら同期コマンドで引き戻す。
  2. ファイル単位ではなくキー単位で暗号化。 SOPS の encrypted_regex は名前がクレデンシャルらしいキー((?i)(key|secret|password|token|dsn|credential))だけを暗号化し、構造と非機密の値は PR diff で読めるまま残す。これが git-crypt 落選の直接理由だ:ファイル全体のバイナリ diff はレビュー不能で、.gitattributes の順序を書き間違えると平文が履歴に漏れる。レビューの可読性がこの方式の第一制約である。ただし弱点は直視する:カバレッジがキー名のヒューリスティクスで決まるため、名前がクレデンシャルらしくないキー(database_urlwebhook_url の類)の下にある秘密は平文で入庫し、CI が検証するのは SOPS メタデータとキーの一致性なので、この取りこぼしは捕まえられない——実際のマニフェストのルールは後からまさにこの形で database_url を補った。だからキー名の正規表現は最後の網にしかならない:新しいリーフはまず設定カタログの機密度分類を通し(第 8 条参照)、guard スクリプトが追跡ファイル内の平文接続文字列フィールドを拒否する。より徹底した設計は、カバレッジを sensitivity 分類に駆動させるか、デフォルトを反転する(全暗号化をデフォルトにし、非機密キーを明示的に免除する)ことだが、diff の可読性を一部犠牲にする。
  3. 権限境界はコラボレーションプラットフォームのドキュメント権限を流用する。 三つの環境には一つのドキュメントに三つのテーブルではなく、三つの独立ドキュメントを使う。プラットフォームの権限は下方向へ継承されるため、単一ドキュメント内で環境を分けるには高度な権限ルールに頼ることになり、設定ミス一回で prod の鍵が全員に晒される。「独立ドキュメント+メンバー制」という最も愚直な権限モデルこそ、最も設定を誤りにくい。
  4. PR の CI は永遠に無鍵。 PR 段階では平文の example manifest(偽値)だけで検証する:暗号文は SOPS メタデータを必ず持つこと、暗号文と example のキーが一致すること、環境横断のキー契約(dev にあるキーは staging にも必須、staging と prod は同型)——「dev にだけ足した」「prod を忘れた」を狙い撃ちで捕まえる。復号は信頼された リリースジョブの中でだけ起き、鍵は環境レベルの secret から来て、ジョブ終了時に平文を消す。信頼されていない PR のコードはどの鍵にも触れられない。
  5. 鍵がなくても開発は止めない。 ドキュメント権限のない人が apply すると、自動で example manifest +決定論的な mock へフォールバックし、すべてそのまま動く。ただしこの退路が許されるのは dev だけで、staging/prod はフォールバックを拒否する。
  6. クレデンシャルの所有者は一つ。 モデルベンダーの key はすべてモデルゲートウェイサービスの設定サブツリーの下にだけ存在し、消費側サービスが受け取るのはゲートウェイの base URL と内部 token のみ。どのサービスにもベンダー key の複製を許さない。ローテーション時に変える場所は一つになる。
  7. vault にはクレデンシャルと付随する身元だけを置く。 最初はすべての設定リーフを vault にミラーしていたが、非機密の値が独自の書き込み面(ローカル JSON、設定センター)を持った途端、ミラー表がドリフト源になると分かった。age 鍵・秘密値・その秘密を使うのに必要な身元(ユーザー名、アカウント ID)だけに縮小した。vault は設定ブラウザではない。
  8. まず分類し、それからチャネルを選ぶ。 機械検証される設定カタログ(catalog)が各設定リーフに注記を付ける:ライフサイクル(起動時/ホットリロード)、機密度、所有者、ロールバック方法。秘密と起動時の値は暗号化 manifest チャネルへ、非機密のホットな値はランタイム設定チャネルへ。値を変える前にそれがどの類かを調べる——感覚で置き場所を選ばない。

実運用で検証済みのやり方

  • オンボーディングはコマンド一発:ツールチェーンの導入、プラットフォームへのログイン、鍵の自動取得(0600 のローカルファイルへ書き込み)、復号・レンダリング・スタック全体の起動。人が鍵を手渡しする工程はどこにもない。
  • 値の変更もコマンド一発:新しい値のサイレント入力、その場での再暗号化、ローカル設定の再レンダリング、vault へのミラー。変更は PR の形で現れる。Reviewer が diff で見るのはキー名と構造の変化で、具体的な値は対応環境の vault で照会する——「何を変えたか」のレビューと「何に変えたか」の秘匿が分離されている。
  • クレデンシャル行の削除は明示的な操作:通常の同期は vault の行を決して消さない。掃除はまず dry-run で削除予定の行を列挙し、人が確認した上で明示フラグ付きで実行する。「同期のついでにまだ使っているクレデンシャルを消す」ことを防ぐ。
  • ローテーションは二層あり、混同が最も危険な誤解:age 鍵の交換は止血にすぎず、しかも操作は data key を再生成する sops rotate でなければならない——updatekeys古い data key を新しい鍵で包み直すだけで、過去の任意のバージョンを持っていた人はその data key を取り出せば、それ以降の新しい暗号文まで開け続けられる。そして rotate してもなお、旧鍵を持っていた人は全履歴コミットを復号できる——これは暗号文をリポジトリに入れる方式の固有のコストだ。本当の修復は、実物のクレデンシャル(ベンダー key、JWT secret、DB パスワード)ごと交換すること。メンバーの離脱や PC 紛失時は二層とも回し、runbook に固定の手順として書き込む。
  • 平文が履歴に入っても履歴は書き換えない:guard がすり抜けられ平文の secret がコミットに入ったら、正しい動きはその値を焼失扱いにし、実物のクレデンシャルを即座にローテーションし、それから manifest を直すこと——そのコミットを rebase で消して何もなかったことにするのではない。
  • Guard rails は全自動:pre-commit と CI は同一のチェック——追跡下の manifest はすべて SOPS メタデータ必須、リポジトリのどこであれ age 秘密鍵が現れたら即拒否、example に本物の形をした秘密が入っていたら拒否。post-merge フックはデプロイディレクトリに触れる取り込みの後にローカル設定を自動再レンダリングし、git pull の後に覚えておくべき作業はない。
  • prod が未整備でも暗号化した骨組みを先にコミットする:本番インフラが存在する前に「レビュー済みの形」をリポジトリへ固定し、値は空またはプレースホルダにしておく。リリース前チェックはプレースホルダ・空値・mock ベンダーが残っている限り公開を拒否する。形が先、値は後、前チェックが受け止める。

代替案とトレードオフ

  • git-crypt:透過的な smudge/clean は楽だが diff がレビュー不能。本方式にはもともと明示的な apply 段階があるので透過性に利得はなく、レビュー性は純損失。却下。
  • クラウド KMS/マネージド Vault:管理と監査は強いが小規模チームには運用が重く、clone-and-go のオンボーディングを失う。day one の選択ではなく、prod 硬化期に再評価する選択肢として保留。
  • 一人一鍵の age(multi-recipient):失効性は良い——離脱者を recipients から外して sops rotate を続ける(理由は上記:updatekeys だけでは離脱者がその後の新しい暗号文を開くのを止められない)だけで、共有鍵の再配布が要らない。代償は recipients リストの維持と、メンバー変動のたびの再暗号化。チームが小さいうちは共有 dev 鍵を使い、アップグレードの道筋を runbook に書いておけばよく、初日からこのコストを払う必要はない。

移植できる判断

シークレット管理方式の成否を決める制約の順序:変更がどうレビューされるか > 新人がどう鍵を得るか > 暗号強度。キー単位の暗号化が第一を守り、ドキュメント権限=クレデンシャルが第二を解き、第三は SOPS + age でとうに足りている。身元・権限・監査をチームが既に使っているコラボレーションプラットフォームへ外注し、新しいアカウント体系を作らないことが、小規模チームの方式が本当に回る理由だ。そして「CI を信頼面で二層に分ける(無鍵の検証面/有鍵のリリース面)」と「封筒鍵のローテーション ≠ 秘密そのもののローテーション」の二つはチーム規模と無関係で、暗号文をリポジトリに置くあらゆる方式の共通基盤である。