AIエージェントにAPIキーを渡さない方法
AIエージェントは1回のツール呼び出しでAPIキーを漏えいさせる恐れがあります。実キーではなく、スコープ付き短期tokenをcredential gateway経由で渡す方法を解説します。
AI エージェントから Secret を除外する意味
AI エージェントは、コマンドを実行する通常の Linux プロセスです。そのプロセスが保持するすべての環境変数は、実行するコードから読み取れます。したがって、エージェントの環境に API key があると、エージェントは到達可能な任意のホストへその key を送信できます。エージェントから Secret を除外するとは、key 自体ではなく、短時間だけ有効なスコープ付き token や、ネットワーク境界で別の仕組みが実際の値に置き換える placeholder を渡すことです。
これは、モデルが敵対的になるという話ではありません。仕組みはもっと単純です。エージェントは、指示を含む Web ページ、README、issue のコメントを読み、その指示に従います。language model にとって、ユーザーが書いたテキストと取得したテキストに違いはないためです。これが prompt injection です。発生した場合の被害範囲を決める要素は、プロセスが読み取れるものだけです。まだ境界を設定していない場合は、サーバー上で coding agent を安全に実行する方法で、このガイドの前提となる分離レベルを確認できます。
脅威モデルを平易に説明します
エージェントが実行されるユーザーで、次を実行します。
tr '\0' '\n' < /proc/self/environ | grep -iE 'key|token|secret|password'出力されるすべての行は、見知らぬサーバーへの 1 HTTP リクエストで送信できます。次に、エージェントの近くにあるディスク上のデータを確認します。
grep -rIl --exclude-dir=.git -e 'API_KEY' -e 'SECRET' ~/projects
find ~ -maxdepth 3 -name '.env' -o -name 'credentials' -o -name '*.pem'シェルを持つエージェントが、そのデータを外部へ持ち出すために巧妙なエクスプロイトを使う必要はありません。通常の方法が 4 つあり、いずれもログ上は通常の処理に見えます。
- 任意のホストへの外向きの
curlまたはfetch。値はクエリ文字列に含めます。 - エージェントが書き込めるリポジトリへの
git commitとgit push。 - パッケージのインストールスクリプト。エージェントのユーザーとして任意のコードを実行します。
- 値を含むホスト名の DNS ルックアップ。HTTP の外向き通信をブロックしていても、値が送信されます。
レビューを重ねるだけでは、この問題は解決できません。アクセス可能な範囲に価値のあるデータを置かないことが対策です。
作業ツリー内の Secret はコンテキストウィンドウ内の Secret です
エージェントはファイルを読み取ります。エージェントが作業しているリポジトリ内に .env ファイルがあると、そのファイルは読み取られます。読み取られたファイルはコンテキストウィンドウに入り、保持しているログや、その後にエージェントが書き出す内容にも含まれます。
以前は、エージェントが作業するツリー内に key がありました。
cd ~/projects/billing
cat .env
# STRIPE_SECRET_KEY=sk_live_...
# DATABASE_URL=postgres://app:hunter2@db.internal:5432/billing現在は、ファイルをエージェントの到達範囲外へ移動しています。
sudo install -d -m 750 -o root -g agent-review /etc/agent-review
sudo install -m 640 -o root -g agent-review ~/projects/billing/.env /etc/agent-review/billing.env
rm ~/projects/billing/.env作業ツリーにファイルがないため、エージェントのユーザーはそのファイルを開けなくなります。エージェント自身の設定にある deny ルールは、最初の防御ではなく第 2 の層です。Claude Code は、プロジェクト内の .claude/settings.json から permission ルールを読み取ります。
{
"permissions": {
"deny": ["Read(./.env)", "Read(./secrets/**)", "Read(./**/*.pem)"]
}
}これにより、エージェントが調査中に誤ってファイルを開くことを防げます。このルールはファイルツールだけでなく、Claude Code が認識する Bash のファイルコマンドにも適用されます。たとえば cat や head、< .env のようなリダイレクト先も確認対象です。一方、プロジェクトディレクトリ内で注入された命令が grep -r KEY . を実行することや、短い Python スクリプトが自分で .env を開くことまでは防げません。permission のドキュメントによると、deny ルールは、ファイル名を指定せずにファイルを読み取るコマンドや、サブプロセスが独自にファイルを開く処理には適用されないためです。これらについて、ドキュメントは Claude Code の sandbox を案内しています。sandbox はオペレーティングシステムのレベルでブロックを強制します。そのようなコマンドの実行前に確認を求められるかどうかは、session の permission mode に依存します。また、auto mode は現在、対話型 Claude Code session の組み込みの開始モードです。そのため、監視していないサーバーでは、より多くのコマンドが確認なしで実行されます。同じ制限は、permission ではなくエージェントの習慣を形作る仕組みにも当てはまります。エージェントに実行可能な最小限の変更を徹底させる skill があれば、処理が本来開く必要のないファイルへ逸れることを防げます。ただし、それでもモデルが説得によって無視できる助言にすぎません。設定はガードレールとして扱い、ファイルシステムの permission を壁として扱ってください。コンテナ内でも同じ分離が適用されます。Docker Compose の env file と Secret では、この問題を 1 層下で扱う方法を説明しています。
各エージェントに専用の非特権ユーザーを割り当てる
エージェントを自分のユーザーで実行すると、SSH keys、cloud credentials、shell history が継承されます。専用ユーザーの作成に必要なのは 1 つのコマンドだけで、これらすべてへのアクセスを分離できます。
sudo adduser --disabled-password --gecos "" agent-review
sudo chmod 700 /home/agent-review
sudo -u agent-review cat ~/.ssh/id_ed25519最後の行は cat: /home/you/.ssh/id_ed25519: Permission denied で失敗しなければなりません。鍵が表示される場合は、エージェントのユーザーがそのファイルにアクセスできます。ファイルを名前で開くには、読み取り権限ではなく、上位にあるすべてのディレクトリに対する search (x) 権限が必要です。そのため、他のユーザーが一覧表示できない mode 711 の home directory でも、鍵ファイル自体が読み取り可能なら鍵に到達できます。chmod 700 ~ でパスを閉じ、chmod 600 ~/.ssh/id_ed25519 で鍵を修正します。エージェントのユーザーを sudo に追加しないでください。また、本当に必要な 1 つのコマンドより広い NOPASSWD rule も与えないでください。VPS 上の最小権限ユーザーでは、group と sudoers の詳細を説明しています。マシン上で複数の session を実行する場合は、この分離を維持してください。1 つの Claude Code session から別の session へテキストを直接送信できるため、最初の session が保持している情報は 1 通の message でその channel を通過する可能性があります。
cloud VPS では、もう 1 つ境界を追加する価値があります。instance metadata service は固定された link local address で応答し、要求した相手に role credentials を渡すことがよくあります。endpoint 側では、どの local user が呼び出しているかを確認しません。EC2 では、AWS が IMDSv1 の呼び出しに token は不要と説明しています。また、1 つの user から service へのアクセスを防ぐ方法として、その user の ID を条件にした local firewall rule を案内しています。
sudo iptables -I OUTPUT 1 -p tcp -m owner --uid-owner agent-review -d 169.254.169.254 -j REJECTrule は -A で末尾に追加せず、-I で先頭に挿入してください。default allow outgoing policy で ufw を実行している host では、ufw の最後の OUTPUT chain がすべての新しい TCP connection を許可するため、末尾に追加した rule には到達しません。iptables rules は再起動すると消えるため、永続化する必要があります。また、metadata service の IPv6 endpoint を有効にしている場合は、[fd00:ec2::254] に対して ip6tables で同じ rule を追加してください。
エージェント側から確認します。sudo -u agent-review curl -s --max-time 3 http://169.254.169.254/ は何も表示せず、non zero で終了する必要があります。packet はマシンから出る前に reject されるためです。一覧が表示される場合は、rule に到達していません。
認証情報を境界で注入する
この問題を実際に解決するパターンが、認証情報の注入です。エージェントが実際の鍵を保持することはありません。エージェントはローカルゲートウェイ経由でリクエストを送信し、ゲートウェイが送信時にプレースホルダーを実際の Secret に置き換えます。Secret はゲートウェイのストレージ内に保存され、別のプロセスが、別のユーザーとして管理します。
OneCLI は、この方式を実装したオープンソースプロジェクトの 1 つです。ee/ ディレクトリを除き Apache-2.0 ライセンスで提供されます。ee/ ディレクトリには別のエンタープライズライセンスが適用され、本番利用にはサブスクリプションが必要です。OneCLI はエージェントの隣でコンテナとして実行します。2026 年 10 月時点で、プロジェクトのドキュメントには、compose ファイルの隣にある .env ファイルから生成済みの 3 つの Secret を読み込む Docker Compose 構成が記載されています。
git clone https://github.com/onecli/onecli.git && cd onecli/docker
cat > .env <<EOF
SECRET_ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)
GATEWAY_INTERNAL_SECRET=$(head -c 32 /dev/urandom | base64)
BETTER_AUTH_SECRET=$(head -c 32 /dev/urandom | base64)
COMPOSE_PROFILES=runner
EOF
chmod 600 .env
docker compose up -d --wait.env ファイルをバックアップしてください。SECRET_ENCRYPTION_KEY を失うと、そのファイルとともに保存されたすべての Secret が孤立するためです。ダッシュボードはポート 10254 で、ゲートウェイはポート 10255 で待ち受けます。どちらもデフォルトでは 127.0.0.1 にバインドされます。実際の認証情報を 1 回保存し、各エージェントには鍵の代わりにプレースホルダー値と、エージェント専用のスコープ付きアクセストークンを渡します。エージェントはそのトークンを Proxy-Authorization ヘッダーで送信します。ゲートウェイは Host とパスで送信先リクエストを照合し、一致する認証情報を復号して置き換えます。エージェントの環境には、盗まれて困る情報が残りません。
ここで重要なのは暗号化ではありません。「このエージェントは何を、いつ使用したのか」という問いを、ログ検索で答えられるようになることです。6 つの環境のどこに鍵のコピーがあったかを推測する代わりに、1 つの監査証跡を確認できます。
プロセスに Secret を渡し、環境変数には渡さない
systemd で agent を実行する場合、環境変数はまったく必要ありません。LoadCredential= は、service の user だけが読み取れる private directory に Secret を配置します。unit file では %d として、process 内では $CREDENTIALS_DIRECTORY として公開されます。この制限は service 単位ではなく user 単位です。そのため、各 agent service に専用の user を割り当ててください。同じ user で実行される別の process は、その directory を開けます。ただし、unit で PrivateMounts=yes などの filesystem namespacing も使用すると、他の service からその directory を隠せます。値が /proc/<pid>/environ に現れることはないため、ps eww で表示できません。service が停止すると、directory も消えます。
まず machine 用に credential を暗号化します。次の command は systemd のドキュメントに記載されており、systemd 250 以降で動作します。Ubuntu 24.04 と Debian 13 が該当します。
echo -n 'sk-example-value' > /tmp/plain.txt
sudo systemd-creds encrypt --name=api_key /tmp/plain.txt /etc/credstore/api_key.cred
shred -u /tmp/plain.txt
sudo systemd-run -P --wait -p LoadCredentialEncrypted=api_key:/etc/credstore/api_key.cred \
systemd-creds cat api_key最後の command は sk-example-value を出力します。これにより、暗号化された file をこの host で復号できることを確認できます。次に、unit から参照します。
[Service]
User=agent-review
LoadCredentialEncrypted=api_key:/etc/credstore/api_key.cred
Environment=AGENT_KEY_FILE=%d/api_key
ExecStart=/usr/local/bin/agent-workeragent の code は、値が必要になった時点で $AGENT_KEY_FILE の file を開きます。file の読み取りは一時的な処理です。一方、環境変数は process の存続中、process が起動するすべての child に保持されます。
有効期間の長い鍵より短時間で期限切れになるトークンを優先する
有効期限のない鍵は、数か月後にログやトランスクリプトで見つかった場合でも有効です。サービスがセッショントークンを提供している場合は、セッショントークンを使用し、作業に必要な範囲で有効期間を最短に設定します。
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/agent-readonly \
--role-session-name agent-review \
--duration-seconds 900AWS STS (security token service) で設定できる最短の有効期間は15分で、通常は1つのエージェントタスクに十分です。GitHub では、エージェントユーザーに専用の gh ログインを割り当て、そのユーザーが作業する単一のリポジトリだけを対象とする fine-grained token を設定します。これにより、そのセッション内で gh auth token を実行しても、他の対象にはアクセスできません。まずリソースで範囲を限定し、次に時間で限定します。
確認してから、継続的に確認する
エージェントの設定を変更した後は、3 つのチェックを実行してください。自分のアカウントではなく、エージェントのユーザーとして実行します。
sudo -u agent-review env | grep -iE 'key|token|secret'
sudo -u agent-review ls -la /home/you/ 2>&1 | head -3
sudo -u agent-review curl -s -o /dev/null -w '%{http_code}\n' --max-time 5 https://api.github.com/user最初のコマンドは何も出力しないはずです。2 番目のコマンドは ls: cannot open directory '/home/you/': Permission denied を出力しますが、これはディレクトリを一覧表示できないことを示すだけです。その中のファイルを読み取れないことを証明するのは、先ほどの cat テストです。3 番目のコマンドは、エージェントのネットワーク経路がどの認証情報として認識されるかを示します。これは、ゲートウェイパターンが解決する問題です。401 は、GitHub が受け入れる認証情報なしでリクエストが GitHub に到達したことを示します。認証情報がまったくない場合と、無効または期限切れの場合があります。200 は有効な認証情報を付けて送信されていることを示すため、どのトークンなのか把握しておく必要があります。エージェントを無人で実行する場合は、VPS 上の AI エージェントのコストを管理する方法で、これらのアクセス制限と組み合わせる予算上限を説明しています。
FAQ
モデルを信頼して、鍵を漏えいさせないと思ってよいですか?
いいえ。この脅威モデルでは、モデル自体が攻撃者ではないためです。エージェントは Web ページ、リポジトリ、Issue Tracker のテキストを読み取ります。そこには命令が含まれている可能性があります。モデルには、ユーザーの指示と取得したテキストを確実に区別する方法がありません。モデルが正しく判断することに依存する制御は、巧妙な命令が注入された時点で破綻します。そのため、制御はオペレーティングシステムまたはネットワーク側に置く必要があります。
エージェントの Secret に環境変数を使うのは、本当にそれほど危険ですか?
特定の点で危険です。環境変数は継承されるためです。エージェントが起動するすべての子プロセスにコピーされます。ビルドスクリプト、テストランナー、パッケージのインストールフックも含まれます。同じユーザーであれば、/proc/<pid>/environ を通じて環境変数を読み取ることもできます。そのため、エージェントが渡さなくても、エージェントが実行するものは環境変数を読み取れます。LoadCredential= またはゲートウェイを使用して、必要な時点でファイルを読み取る方式なら、公開される時間をその時点に限定できます。
Secret を Vault に保存すれば、それだけで解決しますか?
部分的にしか解決しません。Vault は保存方法を改善します。自分で Vault をホストする場合は、独自のハードニングも必要です。Vaultwarden サーバーは通常、暗号化されたアイテムではなく、管理者トークンまたはバックアップファイルを通じて侵害されるためです。Vault から Secret を取り出し、環境変数としてエージェントに渡す最後の手順は解決しません。これでは元の状態に戻ります。重要なのは、置換を誰が実行するかです。エージェントが Secret を取得するなら、エージェントが Secret を保持します。ゲートウェイまたは init system がエージェントのプロセス外で置換を実行するなら、エージェントが Secret を保持することはありません。
エージェントがすでに何かを漏えいさせたかどうかは、どう確認できますか?
通常、事後に確認することはできません。これがゲートウェイを使うべき理由です。ゲートウェイがなければ、証拠は shell history、エージェントの transcript、そしておそらく保存していない外向き接続のログに分散します。Credential gateway があれば、Credential の使用ごとに、エージェントの identity と timestamp を含む1行の記録が残ります。漏えいを疑った場合は、最初に key をローテーションし、その後で調査してください。ローテーションは容易ですが、確実性を得ることは容易ではありません。
今日、最低限何をすべきですか?
すべての .env ファイルを、エージェントが作業するディレクトリの外へ移動してください。また、エージェントごとに1つの非特権ユーザーを作成してください。この2つの変更には約10分しかかかりません。コードの隣に置く理由のない Credential ファイルをエージェントが読み取るという、最も一般的な経路を塞げます。ゲートウェイと短期トークンは次の段階です。最初に行う対策ではありません。同じ出発点は、VPS 上で自律型エージェントを安全に実行する場合を含む、すべてのエージェントランタイムに適用できます。