インフラ用のシークレットが十数個ある。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 - 同じユーザーで走るコードには無力。 悪性パッケージにもエージェントにも効かない。守るなら置く量を減らす
暗号化は「どこに置くか」の答えで、「誰が読めるか」の答えではなかった。後者は、置く量でしか調整できない。