Mozilla SOPSとAgeでGitシークレットを安全に管理する:軽量なHashiCorp Vault代替手段

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

どのチームも最終的に同じミスを犯します。データベースのパスワード、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割のチームには十分対応できます。

Share: