Presidio is an open-source toolkit that finds and masks or replaces Personally Identifiable Information (PII) within sentences. Regardless of whether an LLM deems “this sentence is safe,” it creates a defense line that programmatically inspects sensitive information with a defined format, such as email addresses, phone numbers, and account identifiers.
While Nova Lite and NeMo Self-check from the previous installment classified the meaning and intent of requests, Presidio finds PII segments within data. They are not competing products but rather layers that reduce different types of failures. In this lab, we will use only synthetic data to locate PII with Analyzer, mask it with Anonymizer, and then connect it to the NeMo Retrieval Rail.
Although the name Microsoft remains, the current official documentation states that Presidio is transitioning to a community-owned project. New container images are distributed from the GitHub Container Registry of the Data Privacy Stack organization. This article is based on Presidio’s current official documentation and distribution paths, which originated at Microsoft.

What Presidio Does and Doesn’t Do
Presidio’s core consists of two stages. The AnalyzerEngine returns the type, start/end positions, and confidence score of PII candidates in text, and the AnonymizerEngine transforms those locations using Operators like Replace, Redact, Mask, Hash, or Encrypt.
원문
↓
Analyzer: 개인정보 후보와 위치 탐지
↓
정책: 종류·점수·처리 방식 결정
↓
Anonymizer: 치환·삭제·마스킹·암호화
↓
정제된 문장
Presidio is not an Orchestrator that coordinates the entire LLM pipeline. It does not handle user authentication, tenant permissions, document access control, or tool authorization. Applications or NeMo’s Custom Actions call Presidio, and final access permissions must be enforced by a trusted service layer.
| — | — | — |
| Component | Role | Result |
| Presidio Analyzer | Detects PII candidates using Regex, NER, rules, Checksum, and Context | Entity Type, Location, Score |
| Presidio Anonymizer | Applies Operators to detected locations | Anonymized Text |
| Application·NeMo | Determines call timing and failure policy | Allow, Block, Restricted Mode |
| Authorization Layer | Validates user·tenant·document permissions | Access Scope |
Where to Place Presidio in the LLM Security Pipeline
User input is inspected before being sent to the LLM. Retrieval-Augmented Generation (RAG) retrieves document chunks related to a query and adds them to the Prompt. In this case, it’s crucial to inspect the original chunk returned after retrieval, not just the embedding vector. Inspecting only the vector does not allow verification of the actual string that will enter the model.
Browser
↓
Application: 인증·Tenant 확정
↓
NeMo Input Rail: 입력 Safety 분류
↓
권한 있는 Retrieval API: Tenant Filter 검색
↓
NeMo Retrieval Rail → Presidio: 원문 Chunk 검사·익명화
↓
Prompt 조립 → LLM → Output Rail
↓
Application: 최종 권한·감사 기록
Presidio can also be placed in the output. However, even if input and retrieved documents have already been sanitized, the model might regenerate information from the conversation context or tool results. Therefore, sensitive services should perform an additional check in the Output Rail. At any point, whether to change detection errors to ALLOW, block them, or switch to a restricted response must be explicitly defined based on business impact.
Lab 1: Creating the Smallest Detection and Anonymization with Python
Create a Python virtual environment and install the official package. Reconfirm the supported versions and NLP Model requirements from the official installation documentation at runtime.
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
Do not copy actual customer information; use reserved .test domains and virtual identifiers.
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)
The expected result is a sentence with
Lab 2: Adding Organizational Identifiers as Custom Recognizers
Default Recognizers alone cannot identify unique organizational employee IDs, consultation numbers, or asset numbers. For example, if the employee ID format for the lab is EMP-Year-FourDigits, a Pattern Recognizer should be added.
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 is good at capturing formats but does not fully understand context. A pattern that is too broad may mistakenly identify normal strings as PII, while one that is too narrow may miss variations. For entities with high language and context dependency, such as Korean names or addresses, a supported NLP Model, Custom Recognizer, and evaluation Dataset must be designed together.
Lab 3: Separating Operators by Policy
Not all PII should be handled in the same way. For LLM Prompts where original text restoration is not needed, Replace or Redact are straightforward. For analysis where continuity of the same user needs to be maintained, Hash or Pseudonymization can be considered.
| — | — | — |
| Purpose | Operator Candidates | Caveats |
| Do not send original text to LLM | replace, redact, mask | Ensure not to remove necessary context |
| Maintain consistency for the same value | hash | Fixed Salt managed in a separate Secret Store |
| Approved restoration required | encrypt | Do not put Key in Prompt·Log·configuration files |
| Preserve some Entities | keep | Requires explicit business justification and access control |
According to the official documentation, the Hash Operator recently uses a Random Salt for each entity by default. If the same input requires the same hash, a consistent Salt must be provided, but if that Salt is leaked, the risk of re-identification increases. Salt or Encrypt Keys should be handled by external secret management layers such as KMS, Key Vault, or Secret Manager.
Lab 4: Calling from NeMo Retrieval Rail
The following function is a minimal example of refining an original chunk returned by a search API after authorization checks are complete, using Presidio. Presidio does not directly query the Vector DB or determine tenant permissions.
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
In NeMo, this function can be registered as a Custom Action and called from the Retrieval Rail. Alternatively, the same checks can be performed within an Application-owned Retrieval API, and only refined chunks can be passed to NeMo. Both methods are possible, and the choice depends on the operational entity, latency, and policy tracking location.
The important sequence is Tenant Filter search → return authorized original text → PII inspection → Prompt assembly. Calling Presidio first does not grant permission to read documents from an unauthorized tenant.
Calling Separated by Container
The official installation documentation provides Container Images to run Analyzer and Anonymizer as separate HTTP Services. As of September 2026, new releases are distributed under ghcr.io/data-privacy-stack, and in production, a verified Release Tag should be fixed instead of `latest`.
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
In the lab, `latest` was shown to illustrate the structure, but for reproducible environments, a Digest or explicit Version should be fixed. TLS, service authentication, and Network Policy should also be applied to the original text transmission section between services, and the Request Body should not be left in Access Logs.
Testing Should Not Only Focus on Detection Rate
Presidio is an automated judgment tool, so both False Positives and False Negatives can occur. The official documentation also warns that it does not guarantee finding all sensitive information and that additional systems and evaluations tailored to actual use cases are required.
| — | — |
| Testcase | Expected Result |
| Synthetic email·phone number | Detected and replaced with specified Entity |
| Organization-specific virtual employee ID | Detected by Custom Recognizer |
| Normal order number with similar format | No over-detection |
| Korean·English·spacing variations | Check target detection rate per language |
| Analyzer Timeout·Error | Fail-closed or restricted mode in sensitive paths |
| Already masked string | Context not broken by duplicate transformation |
Test Logs should record `case_id`, Entity Type, count, Score range, policy version, and processing results instead of the original text. Copying the detected string itself to Log·Trace·Metric Label creates a new leakage path for the personal information defense line.
Commonly Overlooked Limitations
First, masking is not the same as deletion. If the original was first stored in Application Log, Trace, Queue, or Cache, even masking the sentence sent to the LLM means other copies already exist. Presidio should be placed as close to the collection point as possible.
Second, even if a single field is removed, combining context can re-identify an individual. Re-identification risks combining rare job roles, dates, locations, and event descriptions are not solved by simple Regex alone.
Third, anonymization does not replace authorization. First, verify if the user has permission to read the original document, and then process only the minimum necessary information.
Fourth, performance and policy should be measured together. Inspecting long RAG Chunks with multiple Recognizers for every request increases Latency and CPU usage. Combine pre-processing at the Ingestion stage with re-inspection at the Retrieval stage, and establish a path for existing documents to be re-evaluated when policies change.
Lab Completion Checklist
- Separated the responsibilities of Presidio Analyzer and Anonymizer.
- Used .test addresses and virtual identifiers instead of actual personal information.
- Tested both default Recognizers and organization-specific Custom Recognizers.
- Separated Replace·Mask·Hash·Encrypt policies by Entity.
- In RAG, inspected the original Chunk after retrieval, not the Vector.
- Maintained Tenant Filter and document permission checks upstream of Presidio.
- Defined a failure policy to prevent silent bypass with Allow on error.
- Did not leave original text, Salt, or Encrypt Key in Prompt and Telemetry.
- Created both False Positive and False Negative Testcases.
- Preserved blocking·masking results as the next Promptfoo regression Testcase.
Conclusion
Presidio transforms the expectation that “LLMs will automatically hide personal information” into actionable inspection and transformation steps. The Analyzer finds PII candidates, and the Anonymizer de-identifies those segments according to organizational policies. NeMo or applications call this functionality at the input, search, and output boundaries.
However, Presidio does not replace authentication·authorization, tenant isolation, or overall orchestration. The safest deployment involves searching only authorized data, inspecting the original chunks, passing only refined data to the LLM, and re-verifying at the output. The next installment will cover how to connect multiple defense lines in series and manage policies, versions, and observations from a Control Plane.
Official Resources
- Presidio Official Overview
- Presidio Analyzer
- Presidio Anonymizer and Operators
- Presidio Installation and Container Execution
- Presidio Getting Started Guide
- Presidio Official Samples
※ Supported languages, NLP Models, Package APIs, and Container Registries may vary by Release. Before applying to production, validate with synthetic datasets based on the current official documentation and a fixed Version.
Leave a Reply