KeyVaultを使って、ローカルにファイル(~/.configや.env)として置かれてる生のパスワードなどを削除した

はじめに

今回のkv_inject.shを用いたアプローチは、ローカル環境から生の認証情報を排除するという目的において、こちらの記事を参考に作成しました。

https://efcl.info/2023/01/31/remove-secret-from-local/

APIキーやデータベース接続文字列などのシークレット情報をどのように管理するかは、セキュリティを確保する上で極めて重要な課題です。多くの開発現場で、利便性の高さから.envファイルが利用されていますが、その管理方法には潜在的なリスクが伴います。

.envファイルが抱えるセキュリティ上の課題

.envファイルは、環境変数をローカル環境で手軽に設定できるため、開発初期段階で非常に便利なツールです。しかし、プロジェクトが成熟し、本番環境へのデプロイが視野に入ってくると、以下のようなセキュリティ上の課題が顕在化します。

  • 誤ったバージョン管理: .gitignoreへの登録を忘れた結果、シークレット情報がGitリポジトリにコミットされてしまうインシデントは後を絶ちません。一度公開されてしまうと、たとえ履歴を修正したとしても、漏洩のリスクを完全に取り除くことは困難です。
  • 平文での保存: .envファイルは暗号化されていない平文のテキストファイルであるため、開発者のPCやサーバーが不正アクセスを受けた際に、シークリッド情報が容易に窃取される可能性があります。
  • アクセスポリシーの欠如: 誰が、いつ、どのシークレットにアクセスしたかを追跡する仕組みがなく、厳密なアクセス制御を行うことができません。

これらの課題は、アプリケーションのセキュリティレベルを著しく低下させる要因となり得ます。

Azure Key Vaultによる解決策

Azure Key Vaultは、クラウド上でシークレット、キー、証明書を安全に格納し、アクセスを管理するためのサービスです。Key Vaultを活用することで、前述の課題を以下のように解決できます。

  • 中央集権的な管理: シークレットはAzure上のKey Vaultに一元的に保存され、アプリケーションは実行時に必要に応じてシークレットを取得します。これにより、コードリポジトリや開発者のローカル環境からシークレットを完全に分離できます。
  • 厳格なアクセス制御: Azure Active Directoryと連携し、アプリケーションやユーザー単位で詳細なアクセスポリシー(読み取り、書き込み、削除など)を定義できます。
  • 監査とロギング: 全てのアクセス履歴がログとして記録されるため、不正なアクセスや意図しない操作を即座に検知し、追跡することが可能です。

実装例:動的なシークレットインジェクション

今回、アプリケーションの実行環境にシークレットを動的にインジェクトするシェルスクリプト kv_inject.sh を作成しました。これにより、既存のアプリケーションコードに大きな変更を加えることなく、Key Vaultへの移行を実現しました。

kv_inject.sh の動作解説

このスクリプトは、以下の処理を実行します。

  1. Azure CLI (az) を使用して、指定されたKey Vault に格納されている全てのシークレット名を取得します。
  2. 取得したシークレット名に基づき、ループ処理で各シークレットの値を取得します。
  3. シークレット名に含まれるハイフン (-) をアンダースコア (_) に変換し、環境変数として有効な形式に整形します。(これは私がよく使う名前のため統一させておこうとした。)
  4. exportコマンドを用いて、整形したシークレットを環境変数として設定します。
  5. スクリプトの引数として渡されたコマンド(例: python main.py)を、インジェクトされた環境変数が有効な状態で実行します。
ShellScript
#!/bin/bash
# Azure Key Vault Secret Injector

COUNT=0
VAULT_NAME="kv-hoge"

echo "Fetching secrets from $VAULT_NAME..."

SECRETS=$(az keyvault secret list --vault-name "$VAULT_NAME" --query "[].name" -o tsv 2>/dev/null)

if [ -n "$SECRETS" ]; then
    for secret in $SECRETS; do
        val=$(az keyvault secret show --vault-name "$VAULT_NAME" --name "$secret" --query "value" -o tsv 2>/dev/null)
        
        if [ -n "$val" ]; then
            env_name=$(echo "$secret" | tr '-' '_')
            export "$env_name"="$val"
            ((COUNT++))
        fi
    done
else
    echo "No secrets found in $VAULT_NAME or access denied."
fi

echo "---------------------------------------------------"
echo "Success! Injected $COUNT secrets from $VAULT_NAME."
echo "---------------------------------------------------"

if [ $# -gt 0 ]; then
    exec "$@"
else
    echo "Starting new shell with injected secrets..."
    exec $SHELL
fi

このスクリプトを介してアプリケーションを起動することで、アプリケーション自体は従来通り環境変数から設定を読み込むだけでよく、Key Vaultの存在を意識する必要がありません。

利用イメージ

このアプローチの利点は、アプリケーションの起動方法を少し変更するだけで、コードを一切変更せずにシークレット管理の仕組みを切り替えられる点にあります。

従来の方法 (.env ファイルを使用):

ShellScript
# .envファイルを読み込んでからアプリケーションを起動

source .env

python your_app.py

新しい方法 (kv_inject.sh を使用):

ShellScript
# kv_inject.sh を介してアプリケーションを起動

# これだけで、Key Vaultから取得したシークレットが環境変数として設定される

./kv_inject.sh python your_app.py

開発者は、ローカルに.envファイルを持つ代わりに、./kv_inject.shをコマンドの接頭辞として利用するだけで、セキュアなシークレットを利用した開発が可能になります。

高度なシークレット管理:1Passwordとの比較

どちらの手法も、近年のソフトウェアサプライチェーン攻撃のリスクを鑑み、「ローカルのファイルに生のパスワードやAPI Tokenを置かない」という哲学に基づいています。

自分は、1Passwordを使っているので、.envに書くようなパスワードやAPI Tokenも1Passwordに集約することで、 ファイルとして置かれている生のパスワードやAPI Tokenの大部分を削除しました。

引用した記事で紹介されている1Passwordのアプローチは非常に洗練されています。

  1. .envファイルに参照を記述: .envファイル内の実際のシークレットを、op://という特殊な形式の参照文字列に置き換えます。
APP_GOOGLE_OAUTH_CLIENT_SECRET="op://Private/My Example/.../APP_PROD_GOOGLE_OAUTH_CLIENT_SECRET"
  1. op runコマンドで実行: アプリケーションをop run --env-file="./.env" -- npm startのように起動します。op runコマンドがop://を解釈し、1Passwordから取得した実際のシークレット値を環境変数としてプロセスに渡します。

これに対し、kv_inject.shアプローチは、1Passwordのような専用クライアントがない環境でも同様の思想を実現するものです。

  • 比較と評価:
  • 哲学: どちらも「実行時にシークレットをインジェクトする」という点で同じです。ファイルシステム上に生のシークレットが永続化されるのを防ぎます。
  • 中間ファイルの要否: 1Passwordがop://を記述した.envファイルを依然として利用するのに対し、kv_inject.sh.envファイルそのものを開発環境から完全に排除し、Azure Key Vaultという単一の信頼できる情報源(Single Source of Truth)から直接シークレットを読み込みます。
  • エコシステム: 1PasswordはSSHキーの管理や、多様なCLI(gh, awsなど)へのプラグイン機構など、より包括的なエコシステムを提供しています。対してkv_inject.shは、現時点では.envの代替という課題に特化した、シンプルで自前実装(Roll Your Own)可能な解決策です。
  • 依存関係: 1Passwordアプローチはop CLIと1Passwordのライセンスに依存します。kv_inject.shアプローチは、Azureを利用している環境であれば標準的なツールであるAzure CLIのみに依存します。

結論として、1Passwordはより多機能で統合された体験を提供しますが、kv_inject.shは特定の課題(.envの排除)に対して、追加のライセンスコストなしで、かつよりシンプル(中間ファイルが不要)なアーキテクチャで同様のセキュリティレベルを達成できると考えています。

考慮すべき点とkv_inject.shの課題

今回導入した手法は、.envファイルに起因する多くのリスクを軽減しますが、完璧な解決策ではない点には注意が必要です。

  • プロセス内での平文の存在: シークレットは環境変数としてメモリ上に展開されます。そのため、スクリプトを実行したシェルセッションや、そこから起動したアプリケーションのプロセス内では、依然として平文の状態で存在します。例えば、echo $API_KEY のようなコマンドを実行すれば、シークレットの値は画面に表示されます。.envよりマシくらいの気分で使うくらいがいい。
  • 限定的なスコープ: 環境変数が有効なのは、kv_inject.shを実行したシェルプロセスと、そこから起動された子プロセス(アプリケーションや、exec $SHELLで起動した新しいシェルなど)のみです。全く別のターミナルセッションからは、これらの環境変数を参照することはできません。

このアプローチは、開発環境や信頼された実行環境において、シークレットをファイルとしてディスクに保存するリスクを回避するための有効な手段です。しかし、ゼロトラストの原則に基づけば、より高度なセキュリティ(例:アプリケーションSDKによる実行時フェッチ、Managed Identityの活用)への移行も将来的には視野に入れた方が良さそう。

Azure

Posted by takumioda