インフラ用のシークレットが十数個ある。DB のパスワード、Terraform の変数、バックアップ先の設定。これを git-secret で管理していた。リポジトリに GPG で暗号化して置き、git secret reveal で平文に戻す。

ある日、reveal が全停止した。削除したはずのファイルの残骸が管理リストに残っていて、それに引っかかった。直せば済む話だが、これを機に構造ごと見直した。

git-secret の何が悪かったか

  • GPG の鍵が単一障害点。 鍵を失うと全部読めない。これは後述の age でも同じだが、GPG の鍵管理は age より手間が多い
  • 暗号ブロブが git 履歴に永久に残る。 一度コミットした暗号化ファイルは、消しても履歴に残る。将来 GPG 鍵が漏れたら、過去の全バージョンが読める
  • 監査もローテーション支援も無い。 誰がいつ何を取り出したかは分からない
  • 壊れやすい。 削除の残骸で全停止したのがまさにそれ

正本をリポジトリの中に置くこと自体を、やめたかった。

候補を退けた理由

置き場所の候補は4つあった。

候補退けた理由
GCP Secret Manager機能は最良(IAM・監査・バージョン)。でも微額でも従量課金。「1円もかけたくない」で却下
Cloudflare Secrets Store書き込み専用の設計。Terraform apply のときに値を読み出す用途に使えない
Vault 等のセルフホスト常駐デーモンとアンシールの運用が、十数個のシークレットには過剰
開発機の age ストア採用。下記

要件は「1円もかけない」「Terraform から読める」「常駐させない」の3つで、これを全部満たすのが age だった。

作り

開発機(自宅の Linux 機)に、age で暗号化したファイルを置く。

  • 鍵: ~/.secrets/age.key(600)
  • ストア: ~/.secrets/store/<名前>.age
  • 操作: secret get|set|list|rm <名前> という小さな CLI

CLI はこれだけだ。

secret get db-passwords-env      # 値を stdout へ
printf %s "$v" | secret set foo  # stdin の内容を暗号化して保存
secret list
secret rm foo

開発機以外(Mac など)で実行すると、自動的に ssh 経由で同じ操作になる。 鍵は開発機から出ない。Mac には平文もコピーされない。取り出した値の扱いは呼び出し側の責任だ。

if [ ! -f "$KEY" ]; then
  # ストア本体(開発機)以外のマシン → ssh で委譲
  exec ssh forge "~/dev/symbio/core/scripts/secret" "$@"
fi

Terraform の変数や、リポジトリ内の決まったパスに展開したいファイルは、manifest に書いておいて別のコマンドで展開する。.gitignore 済みのパスにだけ展開する。

「1本の鍵で全部が開く」の意味

十数件を個別の鍵で守っているのではない。age.key 1本で、ストアの全部が復号できる。

だから age.key の扱いが、そのままストア全体の強度になる。鍵を失えばストア全滅で、鍵が漏れれば全部漏れる。オフラインの控えを1つ、ストアとは別の場所に置く。これは移行時の宿題として残していて、まだ済んでいない。

ストアのバックアップは暗号化済みなので、そのままコピーしてよい。ただし鍵と同じ場所に置かない。鍵とストアが同じバックアップに入っていたら、暗号化の意味が無い。

ストアと manifest は別のもの

これは混同しやすかったので書いておく。

ストアにある(secret list)13件。暗号化された控えの全部
manifest にある7件。リポジトリ内の決まったパスへ自動展開されるものだけ

manifest に無いものは「控えとして持っているだけ」で、使う場所は各自が知っている前提だ。新しくシークレットを作ったら、置きっぱなしにせず必ず secret set する。 これを忘れると、そのシークレットは平文でしか存在しない状態になる。

実際、あるサービスの OAuth 設定がストアに入っておらず、平文しか無い状態で1週間近く過ごしていた。開発機が飛べば、バックアップ以外に控えが無い。照合したときに気づいて登録した。

守れないもの

ここが、この記事で一番書きたかったところだ。

暗号化しても、開発機の同じユーザーで走るコードからは secret get が普通に通る。 鍵が同じマシンの同じユーザーの下にあるからだ。

ストアが効くのは、こういう場面までだ。

  • リポジトリの事故(誤 push、公開設定ミス)
  • バックアップの漏洩
  • 電源断状態でのディスク盗難

効かないのは、こういう場面だ。

  • 悪性の npm パッケージが開発機で動いたとき
  • この環境で動く AI エージェントが secret get を呼んだとき
  • 開発機そのものが侵害されたとき

つまり「置く場所を暗号化した」だけで、「使える場所を絞った」わけではない。そこを守りたいなら、置く場所を変えるのではなく、置く量を減らすしかない。

これは git-secret でも同じ構造だった。GPG の鍵が同じマシンにあれば、同じユーザーのコードは reveal できる。移行で強くなったのは「リポジトリの外に出した」「ブロブが履歴に残らない」「壊れにくい」の3点で、実行環境に対する防御は1ミリも上がっていない。そこを勘違いしないために、限界を先に書いておく。

残っている宿題

git 履歴には旧 git-secret の暗号ブロブが残っている。GPG 鍵が漏れない限り露出しないが、作法として中身のシークレットを全ローテーションする。DB のパスワードは Hyperdrive の Terraform apply と同期して1本ずつ。落ち着いたタイミングで。

まとめ

  • git-secret は、GPG 鍵が単一障害点、暗号ブロブが履歴に永久残存、削除の残骸で全停止、の3点で筋が悪かった
  • 置き場所の要件は「1円もかけない」「Terraform から読める」「常駐させない」。Secret Manager・Secrets Store・Vault はどれか1つで落ちた
  • age のストアは、鍵1本・ファイル数個・CLI 1つ。他マシンからは ssh で透過的に同じコマンドにすると、鍵が外に出ない
  • 1本の鍵で全部が開く。 鍵の扱いがストア全体の強度。控えは鍵と別の場所に
  • ストア(控えの全部)と manifest(自動展開する分)は別。作ったら必ず secret set
  • 同じユーザーで走るコードには無力。 悪性パッケージにもエージェントにも効かない。守るなら置く量を減らす

暗号化は「どこに置くか」の答えで、「誰が読めるか」の答えではなかった。後者は、置く量でしか調整できない。