Claude Code 時代の .env 管理 — 「平文で置かない」秘密情報の新しい守り方

@yousukezan 氏のポストが、AI 駆動開発における秘密情報管理の盲点を端的に指摘しています。

Claudeが社内に広がるほど、.envが危ない。Cowork時代に必要なのは「便利さ」より秘密情報の置き場所

引用元の Qiita 記事では、Claude Code や Cowork が「チャットで質問するだけのツール」から「ローカルファイルに直接アクセスする開発エージェント」へ進化したことで、従来の .gitignore だけでは守りきれない脅威が生まれていると論じています。本記事では、この問題の技術的背景と実践的な対策を掘り下げます。

何が変わったのか — 脅威モデルの転換

従来の開発ワークフローでは、.env ファイルの脅威モデルは明確でした。

脅威対策
Git リポジトリへの混入.gitignore に記載
本番環境への漏洩環境変数やシークレットマネージャで注入
他人のマシンへの流出ローカルに置く前提なので問題なし

ところが、Claude Code のような AI エージェントがローカルファイルを直接読み書きする時代になると、第三の脅威が加わります。

新しい脅威内容
AI エージェントによる読み取り.env がツールの入力コンテキストに載る
意図しないクラウド送信読み取った内容が LLM の API リクエストに含まれる
組織内の横展開Cowork で複数人が同じプロジェクトを触る際の露出

IPA「情報セキュリティ 10 大脅威 2026」でも「AI の利用をめぐるサイバーリスク」が初選出で 3 位にランクインしており、この脅威モデルの転換は業界全体の認識となりつつあります。

Claude Code は .env をどう扱うのか

自動読み込み問題

セキュリティ研究者 Dor Munis 氏の調査によると、Claude Code は .env.env.local などのファイルを自動的に読み込み、API キーやトークンをメモリに展開していることが判明しています。プロキシ認証情報が意図せず読み込まれ、HTTP 407 エラーとプロキシ料金の異常な高騰として問題が顕在化しました。

settings.json の deny ルール — 期待と現実

Claude Code の公式ドキュメントでは、settings.json.env へのアクセスを拒否する設定が案内されています。

1
2
3
4
5
6
7
8
9
{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(./secrets/**)"
    ]
  }
}

しかし、GitHub 上では deny ルールが実際には機能しないという複数の Issue が報告されています。The Register の報道でも、Claude Code が ignore ルールを無視してシークレットファイルを読み込む問題が取り上げられました。

つまり、settings.json の deny だけに頼るのは現時点では不十分です。

PreToolUse フックによる防御

より確実な防御策として、Claude Code の PreToolUse フックが挙げられます。これはツール呼び出しの前に実行されるシェルスクリプトで、ファイルパスをチェックして .env へのアクセスをブロックできます。

1
2
3
4
5
6
#!/bin/bash
# .claude/hooks/pre-tool-use.sh
if echo "$TOOL_INPUT" | grep -q '\.env'; then
  echo "BLOCKED: .env access denied"
  exit 1
fi

ただし、これも Claude Code 内部の自動読み込みには対応できないため、根本的な解決にはなりません。

LLM Key Ring(lkr)— 「平文を置かない」アプローチ

元記事で注目されているのが、2026 年 3 月 1 日に公開された LLM Key Ring(lkr) です。Rust 製の macOS Keychain 管理 CLI で、設計思想は「そもそも平文ファイルにキーを置かない」というものです。

1Password CLI や Doppler のようなチーム向けソリューションは、外部アカウント、サブスクリプション、バックグラウンドデーモンが必要です。lkr は「個人の開発マシンで LLM キーを安全に管理する」という一点に絞り、macOS 組み込みの Keychain だけに依存する軽量設計を選んでいます。

.env ファイルの具体的な攻撃ベクトル

lkr の作者は、.env がなぜ危険かを 4 つの攻撃ベクトルで整理しています。

攻撃ベクトル説明
バージョン管理への混入.gitignore に頼るヒューマンエラー。うっかりコミットは後を絶たない
シェル履歴への漏洩export OPENAI_API_KEY=sk-...~/.bash_history に残る
プロセス情報への露出ps コマンドで環境変数が見える
AI エージェントによる抽出プロンプトインジェクションでローカルコマンド実行 → 自動的にシークレット取得

4 番目が AI 時代に特有の脅威です。AI エージェントがコマンドを実行できる以上、シークレットの抽出は「スクリプト化可能」になっています。

インストール

1
2
3
git clone https://github.com/yottayoshida/llm-key-ring.git
cd llm-key-ring
cargo install --path crates/lkr-cli

Rust 1.85 以上と macOS が必要です。現時点では Homebrew 配布やバイナリリリースは提供されていません。

コマンド詳細

lkr set — キーの保存

対話的プロンプトでキーを入力します。CLI 引数では受け付けないため、シェル履歴や ps への漏洩を防ぎます。

1
2
3
4
5
6
7
8
9
$ lkr set openai:prod
Enter API key for openai:prod: ****
Stored openai:prod (kind: runtime)

# admin キーとして保存(exec/gen からは使用不可)
$ lkr set openai:admin --kind admin

# 既存キーの上書き
$ lkr set openai:prod --force

Keychain 内では com.llm-key-ring というサービス名の下に provider:label 形式(例: openai:prod)で保存されます。

lkr exec — 推奨ワークフロー

最も安全な使い方です。キーを子プロセスの環境変数にのみ注入し、stdout やファイルには一切出力しません。

1
2
3
4
5
6
7
8
# 全 runtime キーを注入して実行
$ lkr exec -- python script.py
Injecting 2 key(s) as env vars:
  OPENAI_API_KEY
  ANTHROPIC_API_KEY

# 特定のキーだけを選択して注入
$ lkr exec -k openai:prod -k anthropic:main -- node app.js

プロバイダー名から環境変数名への変換は規約ベースです。

Keychain ラベル注入される環境変数
openai:prodOPENAI_API_KEY
anthropic:mainANTHROPIC_API_KEY

admin キーは exec では注入されません。推論用(runtime)と課金管理用(admin)を明確に分離することで、権限の最小化を実現しています。

lkr get — 手動取得(フォールバック用)

環境変数が使えない場面での手動取得手段です。

1
2
3
$ lkr get openai:prod
Copied to clipboard (auto-clears in 30s)
  sk-p...3xYz  (runtime)

デフォルトの動作:

  • 出力はマスク表示(末尾 4 文字のみ)
  • クリップボードに自動コピー、30 秒後に自動消去
  • 消去前に SHA-256 でクリップボード内容を照合し、ユーザーが別の内容をコピーしていた場合は消去をスキップ

オプション:

オプション動作TTY ガード
--show全文を表示非対話環境ではブロック
--plain生値をフォーマットなしで出力非対話環境ではブロック
--force-plainTTY ガードを手動で上書きガード無効化(自己責任)

lkr gen — 設定ファイル生成(最終手段)

テンプレートファイルから Keychain の値を解決して設定ファイルを生成します。平文ファイルが残るため、exec が使えない場合の最終手段と位置づけられています。

1
2
3
4
5
6
7
8
$ lkr gen .env.example -o .env
  Resolved from Keychain:
    OPENAI_API_KEY       <- openai:prod
    ANTHROPIC_API_KEY    <- anthropic:main
  Kept as-is (no matching key):
    DATABASE_URL

  Generated: .env (2 resolved, 1 unresolved)

動作の特徴:

  • 出力ファイルのパーミッションは 0600(所有者のみ読み書き可能)
  • .gitignore に出力ファイルが含まれていない場合は警告を表示
  • admin キーは明示的に除外

JSON テンプレートにも対応しています。MCP サーバーの設定などに便利です。

1
2
3
4
5
6
7
8
9
{
  "mcpServers": {
    "codex": {
      "env": {
        "OPENAI_API_KEY": "{{lkr:openai:prod}}"
      }
    }
  }
}
1
$ lkr gen mcp.json.template -o mcp.json

lkr list — 保存済みキーの一覧

デフォルトでは runtime キーのみ表示します。

キー分類: runtime vs admin

lkr のキー管理の中核にある設計です。

1
2
$ lkr set openai:prod               # デフォルトは runtime
$ lkr set openai:admin --kind admin  # 明示的に admin に昇格
分類用途execgenlist(デフォルト)
runtime推論 API 呼び出し使用可使用可表示
admin課金・使用量管理使用不可使用不可非表示

推論用キーと管理用キーでは侵害時の影響が異なります。推論キーが漏洩した場合は不正利用による課金増加で済みますが、admin キーが漏洩した場合はアカウント全体の制御を奪われるリスクがあります。この分離により「全キーを一括注入」という安易な運用を防ぎます。

TTY ガード — AI エージェント対策の核心

lkr の最も特徴的な防御機構です。3 つのレイヤーで非対話的なキー抽出を阻止します。

レイヤー 1: 生値出力のブロック

1
2
3
4
5
# パイプ経由(非対話環境)での --plain は拒否される
$ echo | lkr get openai:prod --plain
Error: --plain and --show are blocked in non-interactive environments.
  This prevents AI agents from extracting raw API keys via pipe.
  Use --force-plain to override (at your own risk).

検出方法は IsTerminal(fd レベルの isatty チェック)を使用しています。CITERM 環境変数は容易に偽装できるため、意図的に無視しています。

レイヤー 2: クリップボードコピーのブロック

1
2
$ echo | lkr get openai:prod
Clipboard copy skipped (non-interactive environment).

lkr get key && pbpaste のような抽出チェーンを防ぎます。

レイヤー 3: exec ワークフローの優先

そもそもシークレットを出力しない exec を推奨ワークフローとすることで、出力ブロックに依存しない設計を実現しています。

既知の制限: IDE の統合ターミナル(pty)は isatty(true) を返すため、TTY ガードはバイパスされます。完全な防御ではなく、攻撃コストを上げる設計です。

メモリ保護 — Zeroizing

全てのシークレット値は String ではなく Zeroizing<String> で保持されます。スコープを抜けてドロップされる際に、メモリ上の値がゼロ埋めされてから解放されます。

ヒープダンプやコアファイルからのシークレット復元を防ぐ仕組みで、以下の全てのパスに適用されています:

  • API キーの取得・保存時の一時バッファ
  • 暗号化操作の中間値
  • exec での子プロセス環境変数の構築時

脅威モデルの範囲

対応する脅威緩和策関連コマンド
.env の平文キーKeychain に暗号化保存set / get
CLI 引数・履歴への漏洩対話的プロンプト入力set
クリップボードの残留30 秒自動消去(ハッシュ照合)get
非 TTY からのキー抽出TTY ガードget
権限の混在runtime / admin 分離exec / gen
メモリ上の残留Zeroizing<String>全コマンド
対応しない脅威理由
root アクセスユーザーセッション内では Keychain にアクセス可能
gen 出力ファイルの読み取り同一権限のプロセスから読める
IDE 統合ターミナルisatty(true) を返すため TTY ガードが効かない
子プロセスのログ出力exec 後の環境変数を子プロセスがログに書く可能性

既存ツールとの比較

ツール対象スコープ外部依存運用コスト
1Password CLIチーム全体のシークレット外部アカウント、サブスクリプション高い
Dopplerチーム全体のシークレットクラウドプラットフォームデーモン常駐、設定複雑
aws-vaultAWS 認証情報のみAWS 固有単一プロバイダー
lkr個人の LLM API キーmacOS Keychain(組み込み)ゼロ(シングルバイナリ)

lkr は「個人の開発マシンで LLM キーだけを守る」という明確なスコープで、チーム・クラウド基盤を不要にしています。

Doppler — チーム向けシークレット管理の本命

@ryoppippi 氏は、「.env はもう作るな」という @swarm_ai_cloud 氏のポストを引用し、端的にこう述べています。

doppler.com 使おうぜ

lkr が「個人の開発マシン」に特化しているのに対し、Doppler はチーム全体のシークレット管理を担うクラウドプラットフォームです。47,000以上の組織で利用され、月間300億回以上のシークレット読み取りを処理しています。

なぜ .env ファイルはスケールしないのか

Doppler の公式ブログ「Goodbye .env Files」では、.env ファイルの構造的な問題が3つ指摘されています。

1. パース処理の不統一

.env ファイルには標準仕様が存在しません。ライブラリごとにパース挙動が異なります。

記法Python dotenvNode.js dotenvbash
KEY = value(スペースあり)動作する動作するエラー
\n(改行文字)自動変換そのまま挙動不定
複数行の値対応部分対応非対応

Doppler はシークレットを JSON 形式で管理することで、この曖昧さを排除しています。JSON は仕様が厳密で、数値・文字列・改行の扱いに曖昧さがありません。

2. 共有の非効率性

チームに新しいメンバーが加わったとき、.env ファイルはどう渡されるでしょうか。現実には Slack の DM やメールで平文のキーが送られ、最小権限の原則が破られています。メンバーの退職時にキーをローテーションする運用も、.env ベースでは困難です。

3. 環境間の不整合

開発・ステージング・本番で異なる .env ファイルを手動管理するため、「本番だけ動かない」「ステージングで検証したキーと本番のキーが違う」といった問題が日常的に発生します。

Doppler の仕組み

Doppler の中核は プロジェクト × 環境(Config) のマトリクスでシークレットを管理する設計です。

プロジェクト: my-api
├── dev         (開発環境)
├── stg         (ステージング)
├── prd         (本番)
└── ci          (CI/CD)

各 Config に対して個別のシークレットセットを定義し、環境ごとの差異を一元管理します。

インストールと初期設定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# macOS
brew install dopplerhq/cli/doppler

# Linux (Debian/Ubuntu)
sudo apt-get install doppler

# Windows
scoop install doppler

# 認証
doppler login

# プロジェクトとの紐付け
doppler setup --project my-api --config dev

doppler setup を実行すると、カレントディレクトリに .doppler.yaml が生成され、以降のコマンドは自動的にこのプロジェクト・環境を参照します。

主要コマンド

doppler run — .env 不要のシークレット注入

Doppler の最も重要なコマンドです。lkr の exec と同様に、シークレットを子プロセスの環境変数として注入します。

1
2
3
4
5
6
7
8
# Django の開発サーバーを起動(.env 不要)
doppler run -- python manage.py runserver

# Node.js アプリケーション
doppler run -- npm start

# 特定のプロジェクト・環境を指定
doppler run -p my-api -c prd -- ./deploy.sh

ファイルに平文を書き出すことなく、実行時にのみシークレットが環境変数に注入されます。プロセスが終了すればシークレットは消えます。

doppler secrets — シークレットの管理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 一覧表示
doppler secrets

# 個別に設定
doppler secrets set API_KEY "sk-xxxx"

# 複数を一括設定
doppler secrets set KEY1 "value1" KEY2 "value2"

# 既存の .env ファイルからインポート
doppler secrets upload .env

# JSON 形式でダウンロード
doppler secrets download --no-file --format json

既存の .env ファイルからの移行は doppler secrets upload .env の一コマンドで完了します。

マウントモード — ファイルベースのアプリケーション向け

環境変数ではなくファイルからシークレットを読む必要があるアプリケーション向けに、マウントモードが用意されています。

1
doppler run --mount .env -- your-command

このとき生成されるファイルは Linux の名前付きパイプ(named pipe) です。通常のファイルのように読み取れますが、ディスク上に平文が残りません。Doppler プロセスが終了すると自動的にクリーンアップされます。

セキュリティ機能

機能説明
監査ログ全てのシークレットアクセスを Git スタイルのログに記録
ロールバック変更履歴から任意のバージョンに戻せる
RBACロールベースのアクセス制御。環境ごとに権限を分離
サービストークン本番環境用の制限付きトークン。特定の Config のみアクセス可能
シークレット参照共通シークレットを参照で管理し、重複を排除
Webhookシークレット変更時にリアルタイム通知

SOC 2 および ISO 認証を取得しており、年間稼働率 99.99% の実績があります。

料金プラン

プラン料金対象
Free無料(5ユーザーまで)個人・小規模チーム
Team$21/ユーザー/月中規模チーム
Enterprise要問い合わせ大規模組織

特徴的なのは、マシン ID(サービストークン)には追加課金がない点です。CI/CD パイプラインや本番サーバーからのアクセスが増えても、人間のユーザー数のみで課金されます。

40以上のインテグレーション

Doppler は主要なクラウドプロバイダーや CI/CD ツールと直接連携できます。

カテゴリ対応サービス
クラウドAWS Secrets Manager, GCP Secret Manager, Azure Key Vault
CI/CDGitHub Actions, CircleCI, GitLab CI
PaaSVercel, Heroku, Fly.io, Railway
コンテナDocker, Kubernetes
フレームワークNext.js, Django, Rails

AWS Secrets Manager や GCP Secret Manager との連携では、Doppler をシークレットの「真実の源泉(Single Source of Truth)」として、クラウドプロバイダーのシークレットマネージャーに自動同期する構成が可能です。

lkr との使い分け

観点lkrDoppler
対象個人の LLM API キーチーム全体のシークレット
ストレージmacOS Keychain(ローカル)クラウド(Doppler サーバー)
OS 対応macOS のみ(現時点)macOS / Linux / Windows
チーム共有非対応RBAC・監査ログ付きで共有
環境管理なし(単一マシン)dev / stg / prd を一元管理
CI/CD 連携なし40以上のインテグレーション
料金無料(OSS)5ユーザーまで無料、以降有料
AI エージェント対策TTY ガード、Zeroizingrun コマンドによる実行時注入

個人開発では lkr の軽量さが魅力です。macOS ユーザーであれば、外部アカウント不要で即座に導入できます。チーム開発では Doppler の環境管理・RBAC・監査ログが不可欠です。

両者は排他的ではなく、併用も可能です。たとえば、個人の LLM API キーは lkr で管理し、プロジェクト共有のシークレット(データベース接続情報、外部 API キー等)は Doppler で管理する運用が考えられます。

Doppler 以外の選択肢 — クラウドネイティブなシークレットマネージャ

本記事の本質的なメッセージは「Doppler を使え」ではなく、「平文の .env をローカルに置くな。何らかのシークレットマネージャから実行時に注入せよ」 という点です。既にクラウドインフラを運用しているなら、そのプロバイダのシークレットマネージャを使う方が自然な場合もあります。

クラウドプロバイダ別の比較

項目AWS Secrets ManagerGCP Secret ManagerAzure Key VaultHashiCorp VaultDoppler
料金$0.40/シークレット/月 + $0.05/1万API呼び出し6バージョンまで無料、以降$0.06/バージョン/月$0.03/1万操作OSS版は無料、Enterprise版は有料5ユーザーまで無料
ローカル開発AWS CLI 経由で取得gcloud CLI 経由で取得az CLI 経由で取得vault CLI 経由で取得doppler run で注入
本番注入ECS/Lambda にネイティブ統合Cloud Run/GKE にネイティブ統合App Service/AKS にネイティブ統合Kubernetes/Nomad 統合Webhook/SDK で同期
自動ローテーションRDS/Redshift/DocumentDB 対応なし(手動/Cloud Functions)証明書・キーの自動ローテーション動的シークレット生成Webhook で外部連携
クラウド依存AWS に閉じるGCP に閉じるAzure に閉じるクラウド非依存クラウド非依存
学習コストIAM ポリシーの理解が必要IAM の理解が必要RBAC の理解が必要独自の概念が多い比較的シンプル

AWS Secrets Manager — ECS/Fargate ユーザーの最適解

AWS をインフラとして使っている場合、AWS Secrets Manager は最も自然な選択肢です。特に ECS Fargate を使っているなら、タスク定義から直接シークレットを参照でき、.env ファイルもシークレット管理ツールも不要です。

ECS タスク定義での直接参照

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
  "containerDefinitions": [
    {
      "name": "my-app",
      "image": "my-app:latest",
      "secrets": [
        {
          "name": "DATABASE_URL",
          "valueFrom": "arn:aws:secretsmanager:ap-northeast-1:123456789:secret:my-app/prod/db-AbCdEf"
        },
        {
          "name": "OPENAI_API_KEY",
          "valueFrom": "arn:aws:secretsmanager:ap-northeast-1:123456789:secret:my-app/prod/openai-GhIjKl"
        }
      ]
    }
  ]
}

この構成では、シークレットは ECS が起動時にコンテナの環境変数として注入します。Dockerfile にも docker-compose.yml にも .env にも平文は残りません。

ローカル開発での取得

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 単一の値を取得
aws secretsmanager get-secret-value \
  --secret-id my-app/dev/db \
  --query SecretString --output text

# JSON 形式のシークレットから特定のキーを取得
aws secretsmanager get-secret-value \
  --secret-id my-app/dev/config \
  --query SecretString --output text | jq -r '.DATABASE_URL'

# 環境変数に注入してコマンド実行(Doppler の run に相当)
export DATABASE_URL=$(aws secretsmanager get-secret-value \
  --secret-id my-app/dev/db --query SecretString --output text)
python manage.py runserver

シェルスクリプトでの一括注入

Doppler の doppler run に相当する仕組みは、シェルスクリプトで構築できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/bin/bash
# scripts/run-with-secrets.sh
# AWS Secrets Manager から JSON シークレットを取得し、環境変数に展開して実行

SECRET_JSON=$(aws secretsmanager get-secret-value \
  --secret-id "my-app/${ENV:-dev}/config" \
  --query SecretString --output text)

# JSON の各キーを環境変数にエクスポート
eval $(echo "$SECRET_JSON" | jq -r 'to_entries[] | "export \(.key)=\(.value)"')

# 引数のコマンドを実行
exec "$@"
1
2
3
# 使い方
ENV=dev ./scripts/run-with-secrets.sh python manage.py runserver
ENV=prd ./scripts/run-with-secrets.sh gunicorn myapp.wsgi

自動ローテーション

AWS Secrets Manager の強力な機能の1つが、RDS・Redshift・DocumentDB との自動ローテーションです。Lambda 関数を設定すると、指定した間隔でパスワードを自動的に更新し、アプリケーション側の変更なしに新しい認証情報が適用されます。

1
2
3
4
5
# 30日ごとの自動ローテーションを設定
aws secretsmanager rotate-secret \
  --secret-id my-app/prod/db \
  --rotation-lambda-arn arn:aws:lambda:ap-northeast-1:123456789:function:rotate-db \
  --rotation-rules AutomaticallyAfterDays=30

Terraform での管理

インフラを Terraform で管理しているなら、シークレットの定義もコード化できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
resource "aws_secretsmanager_secret" "app_config" {
  name        = "my-app/${var.environment}/config"
  description = "Application configuration secrets"
}

# ECS タスク定義でシークレットを参照
resource "aws_ecs_task_definition" "app" {
  container_definitions = jsonencode([
    {
      name  = "my-app"
      image = "${var.ecr_repo}:${var.image_tag}"
      secrets = [
        {
          name      = "DATABASE_URL"
          valueFrom = aws_secretsmanager_secret.db_url.arn
        }
      ]
    }
  ])

  execution_role_arn = aws_iam_role.ecs_execution.arn
}

# 実行ロールにシークレット読み取り権限を付与
resource "aws_iam_role_policy" "secrets_access" {
  role = aws_iam_role.ecs_execution.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = ["secretsmanager:GetSecretValue"]
        Resource = [aws_secretsmanager_secret.app_config.arn]
      }
    ]
  })
}

どのツールを選ぶべきか — 判断フロー

既にクラウドプロバイダを使っている?
├── Yes → そのプロバイダのシークレットマネージャを使う
│   ├── AWS (ECS/Lambda) → AWS Secrets Manager
│   ├── GCP (Cloud Run/GKE) → GCP Secret Manager
│   └── Azure (App Service/AKS) → Azure Key Vault
│
├── マルチクラウド or クラウド非依存 → Doppler or HashiCorp Vault
│   ├── チーム規模が小さい → Doppler(セットアップが簡単)
│   └── 大規模 or 高度な要件 → HashiCorp Vault(動的シークレット)
│
└── 個人開発 + macOS → lkr(ゼロコスト、ゼロ依存)

重要なのは 「どのツールを選ぶか」ではなく「.env に平文を置かない」という原則です。どのシークレットマネージャを選んでも、AI エージェントが平文ファイルを読み取るリスクは排除できます。

ローカル開発の「鶏と卵」問題 — aws-vault で解決する

AWS Secrets Manager を選んだ場合、ローカル開発では「AWS の認証情報自体をどこに置くか」という問題が残ります。~/.aws/credentials に平文のアクセスキーを置けば、Claude Code がそれを読み取るリスクがあります。

.env に API キーを置きたくない
  → AWS Secrets Manager から取得しよう
    → AWS のアクセスキーはどこに置く?
      → ~/.aws/credentials に平文で置いている...
        → Claude Code はこれも読める

この「鶏と卵」問題を解決するのが aws-vault です。

aws-vault とは

aws-vault は、AWS の認証情報を OS のセキュアなキーストア(macOS Keychain、Windows Credential Manager、Linux Secret Service)に暗号化保存し、STS(Security Token Service)経由で一時的な認証情報を生成して提供するツールです。99designs 社が開発したオープンソースソフトウェアで、GitHub スター数は8,000以上の広く使われているツールです。

設計思想は「長期的な認証情報を安全に保管しながら、短命な一時認証を動的に生成する」というもので、lkr の exec コマンドと同じ発想です。

インストール

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# macOS
brew install aws-vault

# Linux
brew install aws-vault
# または各ディストリビューションのパッケージマネージャ

# Windows
scoop install aws-vault
# または choco install aws-vault

初期セットアップ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# AWS アクセスキーを Keychain に保存(~/.aws/credentials には書き込まれない)
$ aws-vault add my-profile
Enter Access Key ID: AKIAIOSFODNN7EXAMPLE
Enter Secret Access Key: ****
Added credentials to profile "my-profile" in vault

# 保存済みプロファイルの確認
$ aws-vault list
Profile                  Credentials              Sessions
=======                  ===========              ========
my-profile               my-profile               -

この時点で ~/.aws/credentials にはアクセスキーが書き込まれません。認証情報は macOS Keychain(または選択したバックエンド)に暗号化保存されます。

基本的な使い方

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 一時認証情報を注入してコマンドを実行
$ aws-vault exec my-profile -- aws s3 ls
# → STS で一時認証情報を取得し、環境変数として注入して実行

# Django の開発サーバーを起動(AWS リソースにアクセスするアプリ)
$ aws-vault exec my-profile -- python manage.py runserver

# サブシェルを開いて作業(シェル終了まで一時認証が有効)
$ aws-vault exec my-profile
$ aws s3 ls        # 一時認証情報が環境変数に設定済み
$ exit              # サブシェル終了で認証情報も消える

# AWS マネジメントコンソールにブラウザでログイン
$ aws-vault login my-profile

aws-vault exec が生成する一時認証情報のデフォルト有効期限は 1時間 です。期限が切れれば自動的に無効になるため、万が一漏洩しても被害を限定できます。

~/.aws/config との連携

aws-vault は ~/.aws/config(認証情報ではなく設定ファイル)を読み取り、IAM ロールの切り替えや MFA を自動的に処理します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# ~/.aws/config

# ベースプロファイル(アクセスキーは Keychain に保存済み)
[profile my-profile]
region = ap-northeast-1

# 開発環境用ロール(my-profile の認証情報で AssumeRole)
[profile dev]
source_profile = my-profile
role_arn = arn:aws:iam::123456789012:role/dev-role
region = ap-northeast-1

# 本番環境用ロール(MFA 必須)
[profile prod]
source_profile = my-profile
role_arn = arn:aws:iam::123456789012:role/prod-role
mfa_serial = arn:aws:iam::123456789012:mfa/my-user
region = ap-northeast-1
1
2
3
4
5
6
# 開発環境ロールで実行(自動的に AssumeRole)
$ aws-vault exec dev -- python manage.py runserver

# 本番環境ロールで実行(MFA トークンの入力を求められる)
$ aws-vault exec prod -- ./deploy.sh
Enter MFA code for arn:aws:iam::123456789012:mfa/my-user: 123456

セッション管理と MFA

aws-vault は一度 MFA 認証を通過すると、セッションをキャッシュします。同じプロファイルで再度 exec を実行しても、セッションが有効な間は MFA の再入力を求められません。

設定デフォルト説明
AWS_SESSION_TOKEN_TTL1時間GetSessionToken の有効期限
AWS_ASSUME_ROLE_TTL1時間AssumeRole の有効期限
AWS_MIN_TTL5分残り時間がこれ以下なら再取得

キーローテーション

アクセスキーの定期的なローテーションも aws-vault から実行できます。

1
2
3
4
5
# アクセスキーをローテーション(新しいキーを生成し、古いキーを削除)
$ aws-vault rotate my-profile
Rotating credentials stored for profile 'my-profile'
Created new access key ****NEWKEY
Deleted old access key ****OLDKEY

IAM のベストプラクティスである90日ごとのキーローテーションを、コマンド一つで実行できます。

バックエンドの選択

バックエンドOS特徴
macOS KeychainmacOSデフォルト。OS 組み込みの暗号化ストレージ
Windows Credential ManagerWindowsOS 組み込み
Secret ServiceLinuxGNOME Keyring / KWallet
PassLinux/macOSGPG ベースのパスワードマネージャ
暗号化ファイル全 OSファイルベースのフォールバック
1
2
3
4
5
# バックエンドを明示的に指定
$ aws-vault --backend=keychain add my-profile

# 環境変数で設定(.zshrc 等に記載)
export AWS_VAULT_BACKEND=keychain

AWS Secrets Manager + aws-vault の組み合わせ

aws-vault で AWS 認証情報を保護した上で、AWS Secrets Manager からアプリケーションのシークレットを取得する構成が、AWS 環境での最適解です。

1
2
# aws-vault で一時認証 → Secrets Manager からシークレット取得 → アプリ起動
$ aws-vault exec dev -- ./scripts/run-with-secrets.sh python manage.py runserver

この構成では以下が全て平文ファイルから排除されます。

認証情報保存場所取得方法
AWS アクセスキーmacOS Keychainaws-vault が管理
AWS 一時認証情報メモリ上のみSTS が発行、1時間で失効
DB パスワード等AWS Secrets ManagerAWS CLI で実行時に取得
LLM API キーmacOS Keychain(lkr)lkr exec で実行時に注入

~/.aws/credentials.env もディスク上に存在しないため、Claude Code が読み取れる平文の認証情報はありません。

Claude Code の deny 設定との併用

念のため、Claude Code の設定で AWS 関連ファイルへのアクセスも拒否しておくと多層防御になります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(./secrets/**)",
      "Read(**/.aws/credentials)",
      "Read(**/.aws/sso/cache/**)"
    ]
  }
}

aws-vault と lkr は併用すべきか、一本化すべきか

aws-vault と lkr は仕組みが似ています。どちらも「OS Keychain に暗号化保存し、exec で実行時に注入する」設計です。両方必要なのか、一本化できるのか、判断に迷う方も多いでしょう。

守る対象が異なる

似ているのは仕組みだけで、守る対象と機能は明確に異なります。

aws-vaultlkr
守る対象AWS 認証情報LLM API キー
STS 一時認証あり(IAM ロール・MFA 対応)なし
TTY ガード(AI エージェント対策)なしあり
キーの種類分離IAM ロールで分離runtime / admin で分離
AssumeRole対応非対応
キーローテーションrotate コマンドで対応手動

aws-vault は AWS の STS・AssumeRole・MFA という AWS 固有のフローに深く統合されており、汎用的なキー管理ツールではありません。lkr は LLM API キーに特化しており、AWS の認証フローは扱えません。

3つの構成パターン

パターン A: aws-vault + lkr(併用)

1
2
3
4
5
6
7
8
# AWS の操作は aws-vault
$ aws-vault exec dev -- aws s3 ls

# LLM API キーを使うアプリは lkr
$ lkr exec -- python ai_script.py

# 両方必要な場合はネスト
$ aws-vault exec dev -- lkr exec -- python manage.py runserver
メリットデメリット
LLM キーはオフラインでも使えるツールが2つ必要
lkr の TTY ガードで AI エージェント対策チームメンバーの学習コストが増える
各ツールが最適な対象に特化

パターン B: aws-vault のみ(LLM キーも AWS Secrets Manager に集約)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# LLM API キーも AWS Secrets Manager に保存
$ aws-vault exec dev -- aws secretsmanager create-secret \
    --name llm/openai --secret-string "sk-xxxx"

# 全てのシークレットを aws-vault 経由で取得
$ aws-vault exec dev -- bash -c '
  export OPENAI_API_KEY=$(aws secretsmanager get-secret-value \
    --secret-id llm/openai --query SecretString --output text)
  export DATABASE_URL=$(aws secretsmanager get-secret-value \
    --secret-id my-app/dev/db --query SecretString --output text)
  python manage.py runserver
'
メリットデメリット
ツールが1つで済むローカル開発でも毎回 AWS API 呼び出しが必要
シークレットの管理場所が一元化オフラインで動かない
チームメンバーの学習コストが低いAWS Secrets Manager の料金が発生
IAM ポリシーでアクセス制御可能lkr の TTY ガードは使えない

パターン C: lkr のみ(AWS を使わない場合)

AWS を使わないプロジェクトであれば、lkr だけで十分です。

メリットデメリット
最軽量。外部依存ゼロAWS 認証情報の管理は別途必要
macOS のみで完結チーム共有機能なし
TTY ガードで AI 対策Linux / Windows 未対応(現時点)

判断基準

AWS(ECS/Fargate)を運用している?
├── Yes → aws-vault は必須
│   │
│   LLM API キーの管理をどうする?
│   ├── シンプルさ重視 → AWS Secrets Manager に集約(パターン B)
│   └── オフライン動作・AI対策重視 → lkr を併用(パターン A)
│
└── No → lkr のみ(パターン C)

ECS Fargate を運用しているなら、aws-vault は日常の AWS 操作で毎日使うため、導入コストは既に払っています。その上で LLM API キーをどこに置くかは、「毎回 AWS API を叩く煩わしさ」と「ツールを増やす煩わしさ」のどちらを許容するかで決まります。

ローカル開発で python manage.py runserver を起動するたびに AWS API を叩くのが気にならないなら パターン B(aws-vault 一本化) が最もシンプルです。オフラインでも作業したい、または Claude Code の TTY ガード対策を重視するなら パターン A(併用) が適しています。

アーキテクチャと今後の展開

ワークスペース構成は lkr-core(ビジネスロジック、KeyStore トレイト)と lkr-cli(CLI インタフェース)に分離されています。KeyStore トレイトがストレージバックエンドを抽象化しているため、テスト時は MockStore で差し替え可能です。

今後のロードマップ:

  • Linux: libsecret 統合
  • Windows: Credential Manager 統合

現時点では SaaS バックエンドやマルチマシン同期、チームコラボレーション機能は計画されていません。

Claude Code の apiKeyHelper との連携

Claude Code 自身にも、API キーを動的に取得する apiKeyHelper という仕組みがあります。settings.json にシェルスクリプトを指定すると、Claude Code は静的な環境変数の代わりにスクリプトの出力を API キーとして使用します。

1
2
3
{
  "apiKeyHelper": "lkr get anthropic:prod --plain"
}

lkr と apiKeyHelper を組み合わせることで、Anthropic API キー自体も .env から排除できます。デフォルトでは 5 分ごとにキーが再取得され、CLAUDE_CODE_API_KEY_HELPER_TTL_MS 環境変数で TTL のカスタマイズも可能です。

なお、apiKeyHelper は Claude Code 自身が対話的に実行するため、--plain オプションが TTY ガードにブロックされることはありません。

多層防御の実践ガイド

1 つの対策に頼るのではなく、複数の層で防御する設計が重要です。

レイヤー 1: ファイルシステムから平文を排除

1
2
3
4
5
6
7
8
9
# lkr で Keychain に保存
lkr set openai:prod
lkr set anthropic:main

# .env ファイルを削除
rm .env .env.local

# .env.example はキー値を含めずにコミット
echo "OPENAI_API_KEY=<lkr:openai:prod>" > .env.example

レイヤー 2: Claude Code の設定で防御

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(./secrets/**)",
      "Read(./**/credentials*)"
    ]
  }
}

deny ルールの不具合が報告されていますが、修正された際に即座に有効になるよう設定しておくことに意味があります。

レイヤー 3: コンテナによる隔離

最も確実な方法は、Claude Code をコンテナ内で実行することです。

1
2
3
4
5
# Docker で Claude Code を実行(.env はマウントしない)
docker run -it --rm \
  -v $(pwd)/src:/workspace/src \
  -e ANTHROPIC_API_KEY=$(lkr get anthropic:main --plain) \
  claude-code-image

シークレットはコンテナ起動時の環境変数として注入し、ファイルとしてはマウントしません。

レイヤー 4: チーム全体の設定配布

プロジェクトの .claude/settings.json をリポジトリにコミットすることで、チーム全員に同じ deny ルールを適用できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# プロジェクトルートに配置
mkdir -p .claude
cat > .claude/settings.json << 'EOF'
{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(./secrets/**)"
    ]
  }
}
EOF
git add .claude/settings.json

エンジニアの役割の変化

元記事が指摘するように、AI ツールの導入が進むほど、エンジニアに求められるスキルが変化します。

従来のスキルAI 時代に加わるスキル
コード設計AI がアクセスしてよい範囲の設計
テスト設計秘密情報フローの分離設計
デプロイ設計事故防止のための初期設定配布
コードレビューAI が生成したコードのセキュリティ監査

技術選定の基準も「AI を導入できるか」から「AI を安全に常用できるか」へシフトしています。Cowork のように複数の AI エージェントが並行してコードベースに触る環境では、この設計能力がプロジェクトの安全性を左右します。

まとめ

  • 脅威モデルの転換: AI エージェントがローカルファイルを直接読む時代になり、.gitignore だけでは .env を守れない
  • Claude Code の自動読み込み: .env ファイルが意図せず LLM のコンテキストに載るリスクが確認されている
  • deny ルールの限界: settings.json の deny 設定には既知の不具合があり、単独では不十分
  • LLM Key Ring (lkr): 「平文を置かない」「必要な瞬間だけ注入」という設計で、AI 時代のキー管理を実現
  • 多層防御が必須: ファイル排除、設定、コンテナ隔離、チーム設定配布の 4 層で守る
  • エンジニアの新しい役割: AI が触れる範囲の設計と秘密情報フローの分離が、コード記述と同等に重要になる
  • 1 日で OSS を公開する文化: 問題を発見して即座にツール化する開発文化が、AI 時代のセキュリティを支える

参考