Claude CodeのElasticsearch APIキーを検索用と書き込み用に分ける
Claude CodeにElasticsearchを触らせるとき、APIキーをindex単位の検索用と書き込み用に分けるロール設計と、権限不足を試して確かめる手順をまとめます。
Claude CodeからElasticsearchを使うなら、APIキーは「検索だけできるキー」と「書き込めるキー」の2本に分けるのが基本です。キーが漏れても、Claude Codeが想定外の操作をしても、被害は付与した権限の範囲で止まります。
この記事は、ロール記述子(role descriptor)でindex単位の権限を切る手順、Claude Code側の権限設定、そして権限不足を意図的に起こして確かめる流れを順に書きます。MCPサーバー経由の接続はElasticsearch MCPサーバーの使い方が主題です。ここでは、curlやスクリプトで直接APIを叩く使い方を前提にします。
1本のキーに全権限を載せると何が困るか
Elasticsearchのユーザー向けAPIキー(personal API key)は、作成時に何も指定しないと、作成者の権限をその時点で写し取ったものになります。Create API keyのAPI仕様では、role_descriptorsを省略するか空配列にすると「認証済みユーザーの権限の時点スナップショット」を持つキーになると説明されています。
管理者の画面から手軽に作ったキーをそのままClaude Codeに渡すと、管理者と同じ範囲を渡したことになります。検索の確認をしたいだけの場面でも、削除やインデックス作成まで通る状態です。
加えて、Kibanaの「Connection details」から作るキーは、既定の有効期限が作成から90日です。一方、API画面から作るキーは、期限を指定しなければ失効しません。放置されやすいのは後者です。
検索用と書き込み用で分ける権限の中身
Elasticsearchのindex権限(index privilege)には、用途別の名前が用意されています。今回使うのは次の4つです。
| 権限名 | 公式の説明の要旨 | 使いどころ |
|---|---|---|
read | 公式の説明の要旨読み取り系の操作(search、get、count、mgetなど) | 使いどころ検索用キー |
view_index_metadata | 公式の説明の要旨マッピングや設定などindexのメタデータの読み取り | 使いどころフィールド構成を調べさせたいとき |
write | 公式の説明の要旨ドキュメントの登録・更新・削除とbulk、動的なマッピング更新を含む | 使いどころ書き込み用キー |
create_index | 公式の説明の要旨indexやデータストリームの作成 | 使いどころ新しいindexを作る運用のとき |
readの対象に、マッピングの取得は列挙されていません。マッピングはメタデータ側の権限が扱うので、「Claude Codeにフィールド名を調べさせたい」場合だけview_index_metadataを足します。足さなければ、検索はできてもマッピングは見えない状態になります。
writeは動的なマッピング更新まで含みます。公式はこの権限でマッピングを更新しないよう注意していますが、新しいフィールドを含む文書を登録すると、マッピングが広がります。書き込み用キーには、対象indexを限定する意味がここにもあります。
ロール記述子で検索用キーを作る
作成にはKibanaのDev Toolsか、manage_own_api_key以上のクラスター権限を持つユーザーでのAPI呼び出しを使います。ここではorders-*を検索対象とします。
POST /_security/api_key
{
"name": "claude-code-search",
"expiration": "30d",
"role_descriptors": {
"search-orders": {
"cluster": [],
"indices": [
{
"names": ["orders-*"],
"privileges": ["read", "view_index_metadata"]
}
]
}
}
}ポイントは3つです。
clusterを空にして、クラスター全体の操作を渡さないnamesにワイルドカード付きのindexパターンを書き、対象を限定するexpirationで失効日を決める。この例は30日
返ってくるレスポンスのencodedが、Authorization: ApiKeyヘッダーに使う値です。表示されるのは作成時だけなので、その場で保管先に移します。
書き込み用キーは別名・別indexで作る
書き込み用は、検索対象とは別のindexパターンにするのが安全です。たとえばアプリが取り込み専用のingest-*を持っているなら、そこだけを許可します。
POST /_security/api_key
{
"name": "claude-code-ingest",
"expiration": "7d",
"role_descriptors": {
"write-ingest": {
"cluster": [],
"indices": [
{
"names": ["ingest-*"],
"privileges": ["write", "create_index"]
}
]
}
}
}書き込み用の期限は検索用より短くしています。使う場面が限られるぶん、普段は発行しない運用にもできます。
権限不足を起こして確かめる
権限は書いただけでは分かりません。「できるべきことができ、できてはいけないことが拒否される」までを確かめます。検索用キーの値を環境変数に入れて、次の3つを試します。
export ES_URL=https://<host>:9200
export ES_SEARCH_KEY=<検索用のencoded値>
# 1. 許可されるはず: orders-* の検索
curl -s -H "Authorization: ApiKey $ES_SEARCH_KEY" \
"$ES_URL/orders-*/_search?size=1"
# 2. 拒否されるはず: 検索用キーでの書き込み
curl -s -X POST -H "Authorization: ApiKey $ES_SEARCH_KEY" \
-H "Content-Type: application/json" \
"$ES_URL/orders-2026/_doc" -d '{"test": true}'
# 3. 拒否されるはず: 対象外indexの検索
curl -s -H "Authorization: ApiKey $ES_SEARCH_KEY" \
"$ES_URL/users-*/_search?size=1"1は検索結果が返ります。2と3は、権限が足りない旨のエラー応答になるはずです。返ってきたエラー本文には、拒否された操作とキーの情報が入ります。本文を読んで、狙った項目で拒否されていることを見てください。
エラーにならなかった場合は、キーの作り方に原因があります。ロール記述子を付け忘れてスナップショット権限になっていないか、namesのパターンが広すぎないかを確認します。書き込み用キーでも同じ要領で、ingest-*への登録は通り、orders-*への登録は拒否されることを確かめます。
書き込み用キーの確認
書き込み用キーも、通る操作と通らない操作を1つずつ試します。
export ES_WRITE_KEY=<書き込み用のencoded値>
# 許可されるはず: ingest-* への登録
curl -s -X POST -H "Authorization: ApiKey $ES_WRITE_KEY" \
-H "Content-Type: application/json" \
"$ES_URL/ingest-check/_doc" -d '{"test": true}'
# 拒否されるはず: orders-* への登録と、書き込み用キーでの検索
curl -s -X POST -H "Authorization: ApiKey $ES_WRITE_KEY" \
-H "Content-Type: application/json" \
"$ES_URL/orders-2026/_doc" -d '{"test": true}'
curl -s -H "Authorization: ApiKey $ES_WRITE_KEY" \
"$ES_URL/ingest-check/_search?size=1"最後の検索は、writeにreadが含まれないことの確認です。書き込んだ結果をClaude Codeに検索で確かめさせたいなら、検索用のキーを別に使います。確認用に作ったingest-checkは、作業の後で人が消しておきます。
Claude Code側でもキーを守る
Elasticsearch側で権限を絞っても、キーの文字列そのものが見えれば、そのキーでできる操作はすべて可能です。Claude Code側では、キーを読ませない設定と、実行できるコマンドの絞り込みを組み合わせます。
設定ファイルや.envを読ませない
キーを.envに置くなら、Readのdenyルールで読み取りを止めます。公式の権限ドキュメントはRead(./.env)の形を例に挙げています。
{
"permissions": {
"deny": ["Read(./.env)", "Read(./secrets/**)"]
}
}ただし、このルールはClaude Codeの組み込みファイルツールと、catなど認識できるファイルコマンドが対象です。Python/Nodeのスクリプトが自分でファイルを開く場合は対象外とされています。ファイルアクセスを全プロセスで止めたいときは、サンドボックスを有効にします。
curlを許可するより、決まったスクリプトを許可する
直感的には「curlのうち検索だけ許可」としたくなりますが、Claude Codeの権限ルールは、拒否が許可より先に評価されます。広いdenyは、より狭いallowがあっても打ち消せません。さらにBashのdenyルールは、/usr/bin/curlやsh -c経由の呼び出しを止められないと公式に書かれています。
そこで、検索の呼び出しを決まったスクリプトにまとめ、スクリプトだけを許可する構成にします。
#!/usr/bin/env bash
# scripts/es-search.sh : 検索用キーでの検索だけを行う
set -euo pipefail
index="$1"; shift
curl -s -H "Authorization: ApiKey ${ES_SEARCH_KEY:?}" \
-H "Content-Type: application/json" \
"${ES_URL:?}/${index}/_search" -d "$@"{
"permissions": {
"allow": ["Bash(./scripts/es-search.sh *)"]
}
}キー自体は、スクリプトの中身ではなく、シェルの環境変数から渡します。Claude Codeには、スクリプトの呼び出しだけを許可します。ワイルドカードは、公式の注意どおり、サブコマンドにあたる部分の後ろに置いています。
書き込み用のキーは、同じやり方で別スクリプトにし、allowではなくaskに入れておくと、実行のたびに確認が出ます。askはallowより先に評価されるので、確認が省略されることはありません。
落とし穴
スクリプトの許可だけでは中身は制限されない
Bash(./scripts/es-search.sh *)は、引数の中身までは見ません。indexに_allのような指定が渡されても、権限ルールでは止まりません。最後に効くのはElasticsearch側のロールです。だからこそ、キー側のindexパターンを絞る意味があります。
どちらのキーをいつ使うか
| 場面 | 使うキー | 補足 |
|---|---|---|
| 障害調査でログや注文データを検索する | 使うキー検索用 | 補足常用してよい。期限は30日程度で更新 |
| フィールド構成を調べて検索クエリを組ませる | 使うキー検索用(view_index_metadata付き) | 補足付けないとマッピングが見えない |
検証用データをingest-*へ登録する | 使うキー書き込み用 | 補足その都度発行し、短い期限で |
| 本番indexの更新・削除 | 使うキーどちらも渡さない | 補足人が実行する手順に残す |
本番のindexを更新する操作は、キーを分けても、そもそもClaude Codeに任せない選択肢が残ります。接続先ごとの設計の考え方は、MCPで本番環境に接続する権限設計にあります。
キーの失効と見直し
キーは、使い終えたら失効させます。Kibanaの「API keys」画面から個別またはまとめて削除でき、削除にはmanage_api_keyかmanage_own_api_keyの権限が要ります。自分が作ったキーしか見えない設定でも、管理者権限があれば全ユーザーのキーが一覧に出ます。
ロール記述子の範囲を変えたいときは、キーを作り直さずに更新できます。Kibanaの画面から、ユーザー向けキーのElasticsearch APIとリソースへのアクセス範囲とメタデータを更新できます。変えられないのはキーの名前と種別です。
見直しのときは、次の3点を確認します。
- 一覧に、用途の分からないキーが残っていないか
- 期限なしのキーが残っていないか
- ロール記述子のindexパターンが、現在の用途より広がっていないか
AWSで同じ発想を使うなら、Claude CodeでIAM最小権限をAccess Analyzerから作る手順が、実際に使われた操作から権限を絞る流れの参考になります。
まとめ
検索用と書き込み用でキーを分けると、Claude Codeに渡す権限を操作の種類とindexの範囲で区切れます。ロール記述子は作成者の権限との共通部分になるため、作成者の側でも範囲を確かめておくことが前提です。
作ったら、検索用キーでの書き込みと対象外indexへのアクセスを実際に試し、エラーが返ることを見て初めて設定が効いていると言えます。Claude Code側の権限ルールはコマンドの文字列に対するもので、Elasticsearchのロールの代わりにはなりません。両方を重ねて使うのが前提です。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
- code.claude.com/docs/en/permissions
- elastic.co/docs/deploy-manage/api-keys/elasticsearch-api-keys
- elastic.co/docs/deploy-manage/users-roles/cluster-or-deployment-auth/…
- elastic.co/docs/deploy-manage/users-roles/cluster-or-deployment-auth/…
- elastic.co/docs/api/doc/elasticsearch/operation/operation-security-cr…