どのチームも最終的に同じミスを犯します。データベースのパスワード、APIキー、クラウドの認証情報がGitリポジトリにコミットされてしまうのです。知識不足の新人開発者がやることもあれば、深夜2時の急ぎの修正でやってしまうこともある。いずれにしても、シークレットが一度Gitの履歴に入ったら、それは漏洩したも同然です。次のコミットで削除しても関係ありません。
「シークレットをコミットしなければいい」というのは当然の答えです。しかしそれは本質的な問題を解決していません。では、そのシークレットはどこに置けばいいのか? 専用のシークレット管理ツールを持たない小規模チームでは、たいていGoogle Docs、SlackのDM、あるいは誰かのノートPC上の.envファイルという話に落ち着いてしまいます。それはむしろ悪化しています。
HashiCorp Vaultは業界標準のソリューションです。しかし運用にはコストがかかります。専用サーバー、ポリシー管理、トークンの更新、スタック内のすべてのツールとのインテグレーションが必要です。数人のチームでマイクロサービスを数個管理する場合、面倒を見るべきインフラが多すぎます。
私はまさにこの状況に置かれていました。Mozilla SOPSとAgeの組み合わせが、その状況から抜け出すためのツールになりました。暗号化されたシークレットをGitリポジトリの中に置けます。バージョン管理され、差分確認も、レビューもできて、外部インフラのメンテナンスは一切不要です。
クイックスタート — 5分で動かす
AgeとSOPSのインストール
AgeはPGPの実用的な代替として設計されたモダンな暗号化ツールです。SOPS(Secrets OPerationS)はMozillaのツールで、Age(またはPGP/KMS)を使ってYAML、JSON、.envなどの構造化ファイルを暗号化します。フィールド名はプレーンテキストのまま保持されるため、差分が読みやすい状態を維持できます。
macOSの場合:
brew install age
brew install sops
Linuxの場合:
# Ageのインストール
curl -Lo age.tar.gz https://github.com/FiloSottile/age/releases/latest/download/age-v1.1.1-linux-amd64.tar.gz
tar -xf age.tar.gz
sudo mv age/age age/age-keygen /usr/local/bin/
# SOPSのインストール
curl -Lo sops https://github.com/getsops/sops/releases/latest/download/sops-v3.9.0.linux.amd64
chmod +x sops
sudo mv sops /usr/local/bin/
Ageキーの生成
age-keygen -o ~/.config/sops/age/keys.txt
# 出力例:
# Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
公開鍵はチームメンバーと共有しても問題ありません。keys.txt内の秘密鍵はあなたのマシン上に保管し、安全な場所にバックアップしてください(詳しくは後述)。
最初のシークレットファイルを暗号化する
secrets.yamlというファイルを作成します:
database:
password: "supersecret123"
host: "prod-db.internal"
api_key: "sk-live-abc123xyz"
SOPSで暗号化します:
sops --encrypt \
--age age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p \
secrets.yaml > secrets.enc.yaml
生成されたファイルは次のようになります。値は暗号化され、キーはそのまま読めます:
database:
password: ENC[AES256_GCM,data:xyz123...,tag:abc==,type:str]
host: ENC[AES256_GCM,data:def456...,tag:ghi==,type:str]
api_key: ENC[AES256_GCM,data:jkl789...,tag:mno==,type:str]
sops:
age:
- recipient: age1ql3z7...
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
...
secrets.enc.yamlをGitにコミットし、secrets.yamlを.gitignoreに追加します。必要なときに復号します:
sops --decrypt secrets.enc.yaml > secrets.yaml
# または復号してその場で編集:
sops secrets.enc.yaml
深掘り — SOPSとAgeの仕組み
暗号化モデルの理解
SOPSはハイブリッド暗号化を使用しています。ファイルを暗号化する際、ランダムなデータ暗号化キー(DEK)を生成し、そのDEKを使ってAES-256-GCMでファイルの内容を暗号化します。次にDEK自体をAgeの公開鍵で暗号化します。複数の受信者を追加すると、それぞれがDEKの暗号化済みコピーを受け取ります。チームが増えても、すべてのデータを再暗号化する必要はありません。
フィールド名は設計上プレーンテキストのまま保持されます。プルリクエストで実際の値を公開することなく、どんなシークレットが存在するかを確認し差分をレビューできます。コンプライアンス対応やコードレビューにおいて、最初にセットアップした多くのチームが思っていた以上にこれが重要になります。
プロジェクト用の.sops.yamlを設定する
コマンドのたびにAgeの受信者を指定するのはすぐに面倒になります。代わりにプロジェクトルートに.sops.yamlを置きましょう:
creation_rules:
# *secrets* または *.enc.yaml に一致するファイルをすべて暗号化
- path_regex: .*(secrets|enc)\.yaml$
age: >
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p,
age1another_team_member_public_key_here
# 本番環境用のより厳格なルール
- path_regex: environments/prod/.*\.yaml$
age: age1prod_key_only_here
これでsops --encrypt secrets.yamlを実行するだけで、ファイルパスに基づいて適切なキーが自動的に選択されます。フラグもキー文字列のコピー貼り付けも不要です。
応用的な使い方
複数の受信者によるチームセットアップ
SOPSが真価を発揮するのはここです。各開発者が自分のAgeキーペアを生成し、公開鍵を共有します。それらをすべて.sops.yamlに追加します:
creation_rules:
- path_regex: .*secrets.*
age: >
age1alice_public_key,
age1bob_public_key,
age1carol_public_key
Aliceがファイルを暗号化すると、BobとCarolはそれぞれの秘密鍵で復号できます。誰かがチームを離れたとき、.sops.yamlからそのキーを削除してsops updatekeys secrets.enc.yamlを実行します。Git履歴に残った暗号化済みコピーは、対応する秘密鍵がなければ無意味です。
オンボーディング用に、リポジトリルートにkeys/ディレクトリを用意し、チームメンバー全員の公開鍵をプレーンテキストファイルとして保管しています。新メンバーは自分のキーを追加してPRを出し、メンテナーが更新された受信者リストで共有シークレットを再暗号化します。全体のプロセスは約10分で完了します。
CI/CDパイプラインとの統合
GitHub Actionsでは、Ageのプライベートキーをリポジトリのシークレットとして保存し、ワークフロー中に注入します:
- name: シークレットの復号
env:
SOPS_AGE_KEY: ${{ secrets.AGE_PRIVATE_KEY }}
run: |
sops --decrypt secrets.enc.yaml > secrets.yaml
source <(sops --decrypt --output-type dotenv app.enc.env)
Kubernetesユーザーへ:Flux CDにはSOPSインテグレーションがあり、Kubernetesシークレットに保存されたキーを使ってデプロイ時にシークレットを復号します。マニフェストはクラスターに届くまでGit上で暗号化されたままです。
キーローテーション
.sops.yamlを新しいキーで更新し、再暗号化します:
# 単一ファイルのキーを更新
sops updatekeys secrets.enc.yaml
# チーム変更後にすべてを再暗号化
find . -name "*.enc.yaml" -exec sops updatekeys {} \;
実践的なヒント
暗号化されていないファイルは必ず.gitignoreに追加する。 最もすっきりしたパターンは、暗号化ファイルをsecrets.enc.yaml、非暗号化ファイルをsecrets.yamlと命名し、非暗号化版だけをgitignoreすることです。
SOPSはdotenv形式にも対応している。 先にYAMLに変換する必要はありません:
sops --encrypt --input-type dotenv --output-type dotenv app.env > app.enc.env
Ageの秘密鍵を必ずバックアップする。 SOPSには復旧の仕組みがありません。あるファイルの唯一の受信者になっている場合に秘密鍵を失うと、そのシークレットは永久に失われます。パスワードマネージャー(1Password、Bitwarden)がこのバックアップに最適な場所です。
ついでに言えば、シークレットを暗号化する前に、保護する価値のある十分に強いものを生成しておきましょう。私はこのためにToolCraftのパスワードジェネレーターを常に開いています。完全にブラウザ内で動作し、どこにもデータが送信されないため、プライバシーの心配なく高エントロピーなデータベース認証情報やAPIトークンを生成できます。マシン間でキーを転送する際は、同サイトのハッシュジェネレーターがSHA-256チェックサムをクライアントサイドで処理してくれます。
受信者を定期的に監査する。 暗号化されたファイルの末尾にあるsops:ブロックには、すべての受信者がプレーンテキストで列挙されています。特に自動化パイプラインで使用するシークレットは定期的に確認し、予期しないキーが追加されていないかチェックしましょう。
pre-commitフックを追加する。 暗号化されていないシークレットが最初からステージングされないように防ぎます:
# .git/hooks/pre-commit
#!/bin/bash
for file in $(git diff --cached --name-only | grep -E 'secrets\.yaml$|app\.env$'); do
echo "エラー: 暗号化されていないシークレットファイルがステージングされています: $file"
echo "実行してください: sops --encrypt $file > ${file%.yaml}.enc.yaml"
exit 1
done
SOPS vs Vault — 適切なツールの選び方。 SOPS + Ageが適しているのは、シークレットのローテーションがリクエストごとではなく月単位、チームが20人未満、追加インフラを持ちたくない場合です。HashiCorp Vault(またはそのオープンソースフォークであるOpenBao)が適しているのは、動的なシークレット、サービスごとのアクセスポリシー、自動的に失効するリースベースの認証情報が必要な場合です。多くのチームはSOPSから始め、本当にその限界に達したときだけVaultに移行します。
シークレットの散在——Slackスレッドやメールやローカルのdotfileに散らばる認証情報——は、何かが大きく壊れるまで見えないままの技術的負債です。SOPSはすべてのチームメンバーにデフォルトで安全なワークフローを提供し、管理が必要なサーバーもありません。Vaultが対応するすべてのユースケースをカバーするわけではありませんが、まだVaultの複雑さを必要としない8割のチームには十分対応できます。

