個人情報防衛線: Microsoft Presidio

Presidioは、文章中の個人情報(PII, Personally Identifiable Information)を特定し、隠蔽または置換するオープンソースのツールキットです。LLMが「この文章は安全である」と判断するのとは別に、メールアドレス、電話番号、アカウント識別子のような形式を持つ機密情報をコードで検査する防衛線を構築します。

以前のNova LiteとNeMo Self-checkがリクエストの意味と意図を分類したのに対し、Presidioはデータ内の個人情報区間を特定します。これらは競合製品ではなく、異なる種類の失敗を減らすための階層です。今回の実習では、合成データのみを使用してAnalyzerで位置を特定し、Anonymizerでマスキングした後、NeMo Retrieval Railに接続します。

名前にMicrosoftが残っていますが、現在の公式ドキュメントでは、Presidioがコミュニティ所有プロジェクトに移行中であると案内されています。新しいコンテナイメージは、Data Privacy Stack組織のGitHub Container Registryから配布されます。この記事は、Microsoftから始まったPresidioの現在の公式ドキュメントと配布経路に基づいています。

個人情報を含む文書の断片が、検出と匿名化のゲートウェイを経て精製され、LLMに渡される様子

Presidioの役割と非役割

Presidioの核は2つの段階です。AnalyzerEngineがテキストから個人情報候補の種類、開始・終了位置、信頼スコアを返し、AnonymizerEngineがその位置をReplace・Redact・Mask・Hash・EncryptのようなOperatorで変換します。

원문
  ↓
Analyzer: 개인정보 후보와 위치 탐지
  ↓
정책: 종류·점수·처리 방식 결정
  ↓
Anonymizer: 치환·삭제·마스킹·암호화
  ↓
정제된 문장

Presidioは、LLMパイプライン全体を調整するオーケストレーターではありません。ユーザー認証、テナント権限、文書アクセス制御、ツール認可も担当しません。アプリケーションやNeMoのCustom ActionがPresidioを呼び出し、最終的なアクセス権限は信頼できるサービス層が強制する必要があります。

— — —
コンポーネント 役割 結果
Presidio Analyzer Regex、NER、ルール、チェックサム、コンテキストでPII候補を検出 エンティティタイプ、位置、スコア
Presidio Anonymizer 検出された位置にオペレーターを適用 非識別処理されたテキスト
アプリケーション・NeMo 呼び出しタイミングと失敗ポリシーの決定 許可、ブロック、制限モード
権限レイヤー ユーザー・テナント・文書権限の検証 アクセス許可範囲

LLMセキュリティパイプラインでの配置

ユーザー入力はLLMに送信する前に検査されます。RAG(Retrieval-Augmented Generation)は、質問に関連する文書の断片を検索してプロンプトに追加する方式ですが、この際には埋め込みベクトルではなく、検索後に返された原文チャンクを検査する必要があります。ベクトルのみを検査しても、モデルに実際に入力される文字列を確認することはできません。

Browser
  ↓
Application: 인증·Tenant 확정
  ↓
NeMo Input Rail: 입력 Safety 분류
  ↓
권한 있는 Retrieval API: Tenant Filter 검색
  ↓
NeMo Retrieval Rail → Presidio: 원문 Chunk 검사·익명화
  ↓
Prompt 조립 → LLM → Output Rail
  ↓
Application: 최종 권한·감사 기록

出力にもPresidioを配置できます。ただし、入力と検索文書をすでに精製していても、モデルが対話コンテキストやツール結果から情報を再生成する可能性があるため、機密性の高いサービスではOutput Railで再度検査します。どの時点でも、検出エラーをALLOWに変更するか、ブロックするか、制限付き応答に切り替えるかは、業務への影響に応じて明示する必要があります。

実習1: Pythonで最小の検出・匿名化を作成する

Python仮想環境を作成し、公式パッケージをインストールします。公式インストールドキュメントのサポートバージョンとNLPモデルの要件は、実行時に再度確認してください。

mkdir presidio-privacy-lab
cd presidio-privacy-lab

python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
pip install presidio-analyzer presidio-anonymizer
python -m spacy download en_core_web_lg

実際の顧客情報をコピーせず、予約済みの.testドメインと仮想識別子を使用します。

from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine
from presidio_anonymizer.entities import OperatorConfig

text = "문의 주소는 student@example.test 입니다."

analyzer = AnalyzerEngine()
anonymizer = AnonymizerEngine()

findings = analyzer.analyze(
    text=text,
    entities=["EMAIL_ADDRESS"],
    language="en",
    score_threshold=0.5,
)

result = anonymizer.anonymize(
    text=text,
    analyzer_results=findings,
    operators={
        "EMAIL_ADDRESS": OperatorConfig(
            "replace", {"new_value": "<EMAIL>"}
        )
    },
)

print([(x.entity_type, x.start, x.end, x.score) for x in findings])
print(result.text)

期待される結果は、メール文字列の代わりにが含まれる文章です。正確なスコアは、インストールされているNLPエンジンとバージョンによって異なる可能性があるため、特定の数値を正解として固定しないでください。

実習2: 組織識別子をCustom Recognizerとして追加する

デフォルトのRecognizerだけでは、組織固有の社員番号、相談番号、資産番号などを特定することはできません。例えば、実習用の社員番号の形式がEMP-年-4桁の数字である場合、Pattern Recognizerを追加します。

from presidio_analyzer import AnalyzerEngine, Pattern, PatternRecognizer

employee_pattern = Pattern(
    name="synthetic_employee_id",
    regex=r"bEMP-20d{2}-d{4}b",
    score=0.85,
)

employee_recognizer = PatternRecognizer(
    supported_entity="EMPLOYEE_ID",
    patterns=[employee_pattern],
    context=["사번", "employee", "담당자"],
)

analyzer = AnalyzerEngine()
analyzer.registry.add_recognizer(employee_recognizer)

sample = "담당자 사번은 EMP-2026-0001 입니다."
findings = analyzer.analyze(
    text=sample,
    entities=["EMPLOYEE_ID"],
    language="en",
)

print(findings)

Regexは形式をうまく捉えますが、文脈を完全に理解するわけではありません。広すぎるパターンは正常な文字列を個人情報と誤認し、狭すぎると変形された値を見逃します。韓国語の名前や住所のように言語・文脈依存性が高いエンティティは、サポートされるNLPモデル、Custom Recognizer、評価データセットを組み合わせて設計する必要があります。

実習3: Operatorをポリシーで分離する

すべてのPIIを同じ方法で処理すべきではありません。LLMプロンプトでは、原文の復元が不要な場合、ReplaceやRedactが単純であり、分析過程で同一ユーザーの連続性を維持する必要がある場合は、HashやPseudonymizationを検討できます。

— — —
目的 オペレーター候補 注意点
LLMに原文を送信しない replace, redact, mask 必要な文脈まで削除しないか確認
同じ値の連結性を維持 hash 固定Saltは別途Secret Storeで管理
承認された復元が必要 encrypt Keyをプロンプト・ログ・設定ファイルに入れない
一部エンティティの保存 keep 明示的な業務根拠とアクセス制御が必要

公式ドキュメントによると、最近のHash OperatorはデフォルトでエンティティごとにランダムなSaltを使用します。同じ入力に同じHashが必要な場合は一貫したSaltを提供する必要がありますが、そのSaltが漏洩すると再識別リスクが高まります。SaltやEncrypt Keyは、KMS・Key Vault・Secret Managerのような外部の秘密管理レイヤーで扱います。

実習4: NeMo Retrieval Railから呼び出す

次の関数は、検索APIが権限チェックを終えて返した原文チャンクをPresidioで精製する最小限の例です。PresidioがVector DBを直接照会したり、テナント権限を判断したりすることはありません。

from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine

analyzer = AnalyzerEngine()
anonymizer = AnonymizerEngine()

def sanitize_retrieved_chunks(chunks: list[str]) -> list[str]:
    sanitized = []

    for chunk in chunks:
        findings = analyzer.analyze(
            text=chunk,
            entities=["EMAIL_ADDRESS", "PHONE_NUMBER"],
            language="en",
            score_threshold=0.6,
        )
        clean = anonymizer.anonymize(
            text=chunk,
            analyzer_results=findings,
        )
        sanitized.append(clean.text)

    return sanitized

NeMoでは、この関数をCustom Actionとして登録し、Retrieval Railから呼び出すことができます。または、アプリケーションが所有するRetrieval API内部で同じ検査を実行した後、精製されたチャンクのみをNeMoに渡すことも可能です。どちらの方法も可能であり、運用主体・レイテンシー・ポリシー追跡位置に合わせて選択します。

重要な順序は、テナントフィルター検索 → 権限のある原文返却 → PII検査 → プロンプト組み立てです。Presidioを先に呼び出したからといって、誤ったテナントの文書を読み取る権限が生じるわけではありません。

コンテナで分離して呼び出す

公式インストールドキュメントは、AnalyzerとAnonymizerをそれぞれHTTPサービスとして実行するコンテナイメージを提供しています。2026年9月現在、新しいリリースはghcr.io/data-privacy-stackの下で配布されており、運用ではlatestではなく検証済みのRelease Tagを固定します。

podman pull ghcr.io/data-privacy-stack/presidio-analyzer:latest
podman pull ghcr.io/data-privacy-stack/presidio-anonymizer:latest

podman run --rm -p 5002:3000 
  ghcr.io/data-privacy-stack/presidio-analyzer:latest

podman run --rm -p 5001:3000 
  ghcr.io/data-privacy-stack/presidio-anonymizer:latest

実習では構造を確認するためにlatestを示しましたが、再現可能な環境ではDigestや明示的なVersionを固定します。サービス間の原文転送区間にもTLS、サービス認証、ネットワークポリシーを適用し、リクエストボディをアクセスログに残さないようにします。

テストは検出率だけを見てはいけない

Presidioは自動判定ツールであるため、False PositiveとFalse Negativeの両方が発生する可能性があります。公式ドキュメントもすべての機密情報を必ず見つけると保証しているわけではなく、実際の用途に合った追加のシステムと評価が必要であると警告しています。

— —
テストケース 期待結果
合成メール・電話番号 指定したエンティティとして検出後、置換
組織固有の仮想社員番号 Custom Recognizerで検出
形式が似ている正常な注文番号 過検出しない
韓国語・英語・スペースの変形 言語別の目標検出率を確認
Analyzerタイムアウト・エラー 機密経路ではFail-closedまたは制限モード
すでにマスキングされた文字列 重複変換で文脈が壊れない

テストログには、原文の代わりにcase_id、エンティティタイプ、個数、スコア区間、ポリシーバージョン、処理結果を残します。検出された文字列自体をログ・トレース・メトリックラベルにコピーすると、個人情報防衛線が新たな漏洩経路となります。

見落としがちな限界

まず、マスキングは削除と同じではありません。元のデータがアプリケーションログ、トレース、キュー、キャッシュに先に保存されている場合、LLMに送信する文章を隠蔽しても、すでに他のコピーが残っています。Presidioは可能な限り収集地点の近くに配置する必要があります。

次に、単一のフィールドを削除しても、文脈を組み合わせることで個人を再識別できる可能性があります。稀な職務、日付、地域、事件の説明が結合された再識別リスクは、単純なRegexだけでは解決されません。

第三に、匿名化は権限を代替しません。ユーザーが原文文書を読み取る権限があるかまず確認し、必要な最小限の情報のみを処理する必要があります。

第四に、パフォーマンスとポリシーは一緒に測定します。長いRAGチャンクをリクエストごとに複数のRecognizerで検査すると、レイテンシーとCPU使用量が増加します。取り込み段階での事前精製と検索段階での再検査を組み合わせつつ、ポリシー変更時に既存文書が再評価される経路を設けます。

実習完了チェックリスト

  • Presidio AnalyzerとAnonymizerの責任を区別した。
  • 実際の個人情報の代わりに.testアドレスと仮想識別子を使用した。
  • デフォルトのRecognizerと組織用Custom Recognizerをそれぞれ試験した。
  • エンティティ別にReplace・Mask・Hash・Encryptポリシーを分離した。
  • RAGでは、ベクトルではなく検索後の原文チャンクを検査した。
  • テナントフィルターと文書権限検査をPresidioの前段に維持した。
  • エラー時にAllowで静かに迂回しないよう、失敗ポリシーを定めた。
  • 原文・Salt・Encrypt Keyをプロンプトとテレメトリーに残さなかった。
  • False PositiveとFalse Negativeのテストケースを両方作成した。
  • ブロック・マスキング結果を次のPromptfoo回帰テストケースとして保存した。

まとめ

Presidioは、「LLMが自動的に個人情報を隠すだろう」という期待を実行可能な検査と変換の段階に変えます。Analyzerは個人情報候補を特定し、Anonymizerは組織のポリシーに従ってその区間を非識別化します。NeMoやアプリケーションは、この機能を入力・検索・出力の境界で呼び出します。

しかし、Presidioは認証・認可、テナント分離、全体的なオーケストレーションを代替するものではありません。最も安全な配置は、権限のあるデータのみを検索し、原文チャンクを検査し、精製されたデータのみをLLMに渡し、出力でも再度確認する構造です。次回では、複数の防衛線を直列に接続し、ポリシー・バージョン・観測をコントロールプレーンで管理する方法について説明します。

公式資料

※ サポート言語、NLPモデル、パッケージAPI、コンテナレジストリはリリースによって異なる場合があります。運用適用前に、現在の公式ドキュメントと固定されたバージョンを基準に、合成データセットでまず検証してください。


Comments

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です