はじめに
今回は、Kong AI Gateway で個人情報(PII: Personally Identifiable Information)を検出して置き換える、AI PII Sanitizer プラグインについての記事です。
LLM を業務で利用する際にまず気になるのが、「プロンプトに個人情報や認証情報が含まれてしまう」というリスクです。
うっかり顧客のメールアドレスや氏名、API キーをプロンプトに貼り付けて送信すると、その情報は外部の LLM 事業者へ流出します。
Kong AI Gateway は、この課題に対応するための AI PII Sanitizer を提供しています。
これは、LLM へ渡すリクエスト(入力)と、LLM から返ってくるレスポンス(出力)に含まれる機密情報を仮の文字列に置き換えるプラグインです。
この記事では、AI PII Sanitizer の概要、仕組み、設定方法、検証結果をまとめます。
PII とは
PII とは、氏名、メールアドレス、電話番号、住所、クレジットカード番号、マイナンバー、免許証番号など、「個人を一意に特定できる情報」を指します。
LLM を利用するうえで PII が問題になる主な理由は、情報漏えいのリスクです。
一度 LLM 事業者へ送ったデータは、事業者のログに残ったり、学習に使われたりする可能性があります。
(※有料プランでは学習に利用しないと明言している事業者もありますが、必要以上のデータを送らないに越したことはありません)
また、日本の個人情報保護法の観点では、社外の LLM 事業者へ不用意に PII を送信すると、規程や法令に抵触するリスクも生じます。
しかしながら、PII をすべて取り除いてしまうと、プロンプトの文脈が失われ、LLM の応答品質が下がる場合があります。
AI PII Sanitizer は、このトレードオフを解消します。
具体的には、「PII を仮の値に置き換えることで、情報を保護しながら文脈(カテゴリ情報)は保つ」ことを目指しています。
これにより、規制に対応しながら、これまでどおり LLM を業務で利用できます。
AI PII Sanitizer について
概要と位置づけ
AI PII Sanitizer は、Kong AI Gateway が提供する「入力のガード」に位置づけられるポリシーです。
クライアントからのリクエストに含まれる PII を、上流の LLM へ届く前に検出・置換します。
-
対象:
- 入力データ(リクエスト)
- 出力データ(レスポンス)※ Kong Gateway 3.12 / AI Gateway 2.0 以降
- 入力と出力の両方 ※ 同上
仕組み
AI PII Sanitizer は、単体では PII を検出しません。
外部の AI PII Anonymizer Service(今回の検証環境では Docker コンテナ)に本文を送り、検出と置換を任せる構成です。
入力のみをサニタイズする場合の流れは次のとおりです。
LLM が見るのは「置き換え後の文章だけ」です。
利用者から見れば、プレースホルダーを意識せず、いつもどおりに LLM を利用できます。
入力と出力の両方に適用する場合は、上流へ渡す前と、クライアントへ返す前の両方でサニタイズが行われます。
AI PII Anonymizer Service
検出と置換を担う AI PII Anonymizer Service は、Kong が Docker イメージとして提供しています(Presidio なども利用できます)。
-
イメージ:
kong/ai-pii-service -
言語タグ: 言語ごとのモデルを同梱したタグがあります。
たとえば-en(英語)、-ja(日本語)、-all(全言語)などです。
※-allタグは全言語のモデルをまとめたものです(約 8 GB)。
匿名化のモード(redact_type)
検出した PII をどう置き換えるかは、redact_type で指定します。
-
placeholder: 固定の仮の文字列(例:PLACEHOLDER1、PLACEHOLDER2)に置き換えます。
どの値がどのプレースホルダーに対応するかは Gateway が対応表として保持します。 -
synthetic: カテゴリごとの特徴を保った合成値(例: 実在しないが「それらしい」名前や電話番号)を生成します。
プレースホルダーより自然な文になるため、LLM の応答品質を保ちやすい一方で、復元には向きません。
検出できる PII の種類
anonymize(検出対象)には、次のようなカテゴリを指定できます。
all を指定すると、カスタム定義を含むすべての対象を検出します。
| カテゴリ | 内容 |
|---|---|
general |
氏名、地名、組織名などの一般 PII |
phone |
電話番号(携帯・固定) |
email |
メールアドレス |
creditcard |
クレジットカード番号 |
crypto |
暗号資産アドレス |
date |
日付・タイムスタンプ |
ip |
IP アドレス(IPv4/IPv6) |
nrp |
国籍、宗教、政治的グループ |
ssn |
社会保障番号など(SSN、ITIN、NIF、ABN など) |
domain |
ドメイン名(非推奨。url を使用) |
url |
URL |
medical |
医療識別子(医療ライセンス番号、NHS 番号など) |
driverlicense |
運転免許証番号 |
passport |
パスポート番号 |
bank |
銀行口座番号など(VAT コード、IBAN など) |
nationalid |
各国の国民 ID(Aadhaar、PESEL、NRIC など) |
custom |
正規表現で定義したカスタム PII(カスタムパターンを指定した場合のみ) |
credentials |
認証情報(/sanitize_credentials に相当) |
all |
上記すべて(カスタム定義を含む) |
設定方法
前提として、AI Proxy プラグインが有効になっている必要があります。
AI PII Anonymizer Service を起動する
PII の検出と置換を担当するサービスを、Docker で起動します。
今回の検証では guard プロファイルで起動します。
全言語モデルを同梱した kong/ai-pii-service:v0.2.0-all を使い、Data Plane からは pii-service:8080 で参照します。
services:
kong:
# 省略
pii-service:
profiles: ["guard"]
image: ${PII_SERVICE_IMAGE:-kong/ai-pii-service:v0.2.0-all}
restart: unless-stopped
ai-sanitizer プラグインを設定する
_format_version: "3.0"
plugins:
- name: ai-sanitizer
config:
anonymize:
- all_and_credentials
sanitization_mode: INPUT # INPUT, OUTPUT, BOTH
redact_type: placeholder
recover_redacted: true # レスポンスでもマスクする場合は false
stop_on_error: true
scheme: http
host: pii-service
port: 8080
services:
- name: ollama-service
host: localhost
port: 11434
protocol: http
routes:
- name: ollama-chat
paths:
- /ollama
strip_path: true
plugins:
- name: ai-proxy
config:
route_type: llm/v1/chat
llm_format: openai
model:
provider: llama2
name: gpt-oss:120b-cloud
options:
llama2_format: openai
upstream_url: http://xxx.xx.xx.xxx:11434/v1/chat/completions
# 確認用httpbin
- name: httpbin-service
host: httpbin
port: 80
protocol: http
routes:
- name: httpbin
paths:
- /httpbin
strip_path: true
ai-sanitizer の設定内容は次のとおりです。
| 項目 | 設定 |
|---|---|
| 対象 | 入力のみ |
| 検出する種類 | すべての個人情報(氏名、メール、電話、住所、カード番号など)と認証情報 |
| 置き換え方 | 仮の文字列(プレースホルダー) |
| 応答で元に戻すか | 戻す(recover_redacted: true) |
| 検出サービスを呼べないとき | リクエストを失敗させる(stop_on_error: true) |
「入力のみ」「プレースホルダー」「応答で復元」「エラー時は失敗」という組み合わせは、利用者の利便性を保ちながら PII を確実に保護するための基本的な設定です。
PII が上流へ漏れないことを最優先にしながら、応答に含まれる氏名などは利用者に見える形に戻すことで、普段どおりの対話を実現します。
検証
設定が正しく動作しているかを、実際に PII を含むリクエストを送って確認します。
入力のサニタイズを確認する
PII を含むプロンプトを Data Plane(http://localhost:8000/ollama)へ送信します。
ここでは置き換え後の値を見るため、recover_redacted: false に設定しています。
curl -s --request POST \
--url http://localhost:8000/ollama \
--header 'Content-Type: application/json' \
--data '{
"model": "gpt-oss:120b-cloud",
"stream": false,
"messages": [
{
"role": "user",
"content": "繰り返してください。田中太郎さんのメールアドレスはtanaka@example.comです。"
}
]
}' | jq -r '.choices[0].message.content'
実行結果
サニタイズ内容をログで監査する
どの PII が何に置き換えられたかは、File Log プラグインなどで確認できます。
今回は、次のように File Log プラグインを有効にしてログを確認します。
# File Log プラグインを有効化(例)
echo '_format_version: "3.0"
plugins:
- name: file-log
config:
path: "/tmp/file.json"' | deck gateway apply -
ログファイルには、サニタイズの詳細が JSON で記録されます。
docker compose exec kong tail -n 1 /tmp/file.json | jq
出力例(抜粋)
"ai": {
"proxy": {
"meta": {
"response_model": "gpt-oss:120b",
"operation_name": "chat",
"llm_latency": 1580,
"request_model": "gpt-oss:120b-cloud",
"request_mode": "oneshot",
"provider_name": "llama2",
"otel_provider_name": "llama2",
"plugin_id": "7fda154d-afe6-4bae-9be6-7a5ced89804b"
},
"payload": {
"response": "{\"id\":\"chatcmpl-114\",\"object\":\"chat.completion\",\"created\":1792074879,\"model\":\"gpt-oss:120b\",\"system_fingerprint\":\"fp_ollama\",\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"PLACEHOLDER1さんのメールアドレスはPLACEHOLDER2です。\",\"reasoning\":\"The user wrote in Japanese: \\\"繰り返してください。PLACEHOLDER1さんのメールアドレスはPLACEHOLDER2です。\\\" Means \\\"Please repeat. PLACEHOLDER1's email address is PLACEHOLDER2.\\\"\\n\\nThey likely want the assistant to repeat that exact sentence, perhaps with the placeholders filled? But we have placeholders: PLACEHOLDER1 and PLACEHOLDER2. Probably they want the assistant to repeat the sentence exactly as given, maybe do not replace placeholders.\\n\\nWe must be careful not to reveal personal data; placeholders are not real data. So we can safely repeat: \\\"PLACEHOLDER1さんのメールアドレスはPLACEHOLDER2です。\\\" Also could include the preceding \\\"繰り返してください。\\\" Actually they said \\\"繰り返してください。\\\" So we should say the repeated phrase.\\n\\nThus answer: \\\"PLACEHOLDER1さんのメールアドレスはPLACEHOLDER2です。\\\" Possibly with quotes. We can also optionally acknowledge we repeated as requested. We'll comply.\"},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":90,\"completion_tokens\":234,\"total_tokens\":324,\"prompt_tokens_details\":{\"cached_tokens\":64}}}\n",
"request": "{\"model\":\"gpt-oss:120b-cloud\",\"messages\":[{\"role\":\"user\",\"content\":\"繰り返してください。PLACEHOLDER1さんのメールアドレスはPLACEHOLDER2です。\"}],\"stream\":false}"
},
"usage": {
"cost": 0,
"total_tokens": 324,
"time_per_token": 6.7521367521368,
"time_to_first_token": 1578,
"completion_tokens_details": {},
"completion_tokens": 234,
"prompt_tokens_details": {
"cached_tokens": 64,
"cached_tokens_details": {}
},
"prompt_tokens": 90
}
},
"sanitizer": {
"input_block_source": "",
"input_block_consumer_id": "",
"output_block_source": "",
"output_block_consumer_id": "",
"sanitized_items": [
{
"count": 1,
"entity_type": "PERSON",
"redact_text": "PLACEHOLDER1",
"original_text": "田中太郎"
},
{
"count": 1,
"entity_type": "ORGANIZATION",
"redact_text": "PLACEHOLDER2",
"original_text": "tanaka@example.com"
}
],
"identified": 1,
"input_processing_latency": 9,
"output_processing_latency": 0,
"output_masked": false,
"sanitized": 1,
"input_masked": false,
"duration": 7,
"output_block_reason": "",
"input_block_reason": "",
"mode": "INPUT"
}
},
このログから、検出して置き換えた PII の内容と、Ollama へ送信したプロンプト内の個人情報がプレースホルダーに置き換わっていることを確認できます。
復元(recover)を確認する
入力のみをサニタイズし、recover_redacted: true にしている場合、LLM の応答に含まれるプレースホルダーは、クライアントへ返す前に元の値へ戻されます。
実行結果
ログ全体
{
"response": {
"size": 1385,
"headers": {
"x-kong-upstream-latency": "705",
"x-kong-request-id": "c51a7f24d8b1fd4672d6846971909f4f",
"date": "Thu, 15 Oct 2026 14:44:44 GMT",
"x-build-commit": "07cd222f476bb4f71fd2f4f2603fe31122ff8ce6",
"x-cloud-trace-context": "f834e9d911b01763b1d65d582b2cbafd/17312681518261007832",
"x-frame-options": "DENY",
"x-request-id": "afd4a4ca-a005-430e-ad47-043f694500cf",
"alt-svc": "h3=\":443\"; ma=2592000",
"x-build-time": "2026-10-14T19:27:01-07:00",
"traceparent": "00-f834e9d911b01763b1d65d582b2cbafd-f043001d456af5d8-00",
"content-type": "application/json",
"server": "Google Frontend",
"content-length": "676",
"connection": "close",
"via": "1.1 kong/3.16.0.0-enterprise-edition",
"x-kong-proxy-latency": "16",
"x-kong-llm-model": "llama2/gpt-oss:120b-cloud"
},
"status": 200
},
"request": {
"size": 369,
"headers": {
"accept-encoding": "identity",
"accept": "*/*",
"user-agent": "curl/8.18.0",
"content-length": "183",
"host": "localhost:8000",
"content-type": "application/json"
},
"method": "POST",
"url": "http://localhost:8000/ollama",
"uri": "/ollama",
"id": "c51a7f24d8b1fd4672d6846971909f4f",
"querystring": {}
},
"workspace_name": "default",
"upstream_uri": "/v1/chat/completions",
"ai": {
"proxy": {
"meta": {
"response_model": "gpt-oss:120b",
"operation_name": "chat",
"llm_latency": 707,
"request_model": "gpt-oss:120b-cloud",
"request_mode": "oneshot",
"provider_name": "llama2",
"otel_provider_name": "llama2",
"plugin_id": "7fda154d-afe6-4bae-9be6-7a5ced89804b"
},
"payload": {
"response": "{\"id\":\"chatcmpl-954\",\"object\":\"chat.completion\",\"created\":1792075484,\"model\":\"gpt-oss:120b\",\"system_fingerprint\":\"fp_ollama\",\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"田中太郎さんのメールアドレスはtanaka@example.comです。\",\"reasoning\":\"User requests: \\\"繰り返してください。田中太郎さんのメールアドレスはtanaka@example.comです。\\\" They want the assistant to repeat that phrase. So respond with the same statement. Possibly just repeat exactly. Should not add extra.\"},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":90,\"completion_tokens\":80,\"total_tokens\":170,\"prompt_tokens_details\":{\"cached_tokens\":64}}}\n",
"request": "{\"model\":\"gpt-oss:120b-cloud\",\"messages\":[{\"role\":\"user\",\"content\":\"繰り返してください。PLACEHOLDER1さんのメールアドレスはPLACEHOLDER2です。\"}],\"stream\":false}"
},
"usage": {
"cost": 0,
"total_tokens": 170,
"time_per_token": 8.8375,
"time_to_first_token": 707,
"completion_tokens_details": {},
"completion_tokens": 80,
"prompt_tokens_details": {
"cached_tokens": 64,
"cached_tokens_details": {}
},
"prompt_tokens": 90
}
},
"sanitizer": {
"input_block_source": "",
"input_block_consumer_id": "",
"output_block_source": "",
"output_block_consumer_id": "",
"sanitized_items": [
{
"count": 1,
"entity_type": "PERSON",
"redact_text": "PLACEHOLDER1",
"original_text": "田中太郎"
},
{
"count": 1,
"entity_type": "ORGANIZATION",
"redact_text": "PLACEHOLDER2",
"original_text": "tanaka@example.com"
}
],
"identified": 1,
"input_processing_latency": 11,
"output_processing_latency": 0,
"output_masked": false,
"sanitized": 1,
"input_masked": false,
"duration": 9,
"output_block_reason": "",
"input_block_reason": "",
"mode": "INPUT"
}
},
"client_ip": "172.25.0.1",
"started_at": 1792075483518,
"source": "upstream",
"latencies": {
"kong": 16,
"redis": 0,
"dns": 0.220928,
"http_client": 12.242688,
"kong_internal": 3.8480999261475,
"proxy": 705,
"request": 722,
"third_party": 12.463616,
"client": 0.795392,
"receive": 1,
"socket": 0
},
"workspace": "0dc6f45b-8f8d-40d2-a504-473544ee190b",
"upstream_status": 200,
"route": {
"paths": [
"/ollama"
],
"preserve_host": false,
"strip_path": true,
"regex_priority": 0,
"updated_at": 1792071565,
"priority": 0,
"id": "62598741-43c4-4489-8350-9349440f149d",
"ws_id": "0dc6f45b-8f8d-40d2-a504-473544ee190b",
"created_at": 1792071565,
"protocols": [
"http",
"https"
],
"name": "ollama-chat",
"https_redirect_status_code": 426,
"request_buffering": true,
"response_buffering": true,
"service": {
"id": "7c631234-3f04-4f61-9513-04775f066c9c"
},
"path_handling": "v0"
},
"service": {
"port": 11434,
"id": "7c631234-3f04-4f61-9513-04775f066c9c",
"ws_id": "0dc6f45b-8f8d-40d2-a504-473544ee190b",
"created_at": 1792071564,
"retries": 5,
"updated_at": 1792071564,
"name": "ollama-service",
"read_timeout": 60000,
"enabled": true,
"write_timeout": 60000,
"host": "localhost",
"connect_timeout": 60000,
"protocol": "http"
},
"tries": [
{
"balancer_latency": 0,
"balancer_latency_ns": 14848,
"ip": "xxx.xxx.xx.xxx",
"port": 11434,
"balancer_start_ns": 1.7920754835342E+18,
"target_id": "unknown",
"hostname": "xxx.xxx.xx.xxx",
"balancer_start": 1792075483534,
"keepalive": true
}
]
}
このように、LLM には元の PII を見せずに、利用者には自然な応答を返せます。
まとめ
-
PII は、LLM の業務利用にあたって管理が必要な個人情報です。
不用意に社外へ送信すると、規制対応や情報漏えいのリスクが生じます。 -
AI PII Sanitizer は、AI Gateway の「入力のガード」として、リクエスト/レスポンスの PII を外部の AI PII Anonymizer Service で検出し、仮の文字列(
placeholder)または合成値(synthetic)に置き換えます。 - 単に削除するのではなく、「文脈を保ちながら情報を保護」する点が特徴で、応答内の値を元に戻す復元機能(
recover_redacted)も備えています。 - 検証では、PII を含むリクエストを送り、置き換えが行われていることと、監査ログに記録されることを確認しました。
参考

