PII Detection and Redaction Patterns for Generative AI on AWS - Layered Defenses with Amazon Comprehend, Bedrock Guardrails, and Macie

First Published:
Last Updated:

1. Introduction

Personally identifiable information (PII) is not something a generative AI application protects in a single place. The same phone number can enter your system as a user's chat message, sit inside a document you ingest into a knowledge base, surface in a model's generated answer, and settle into logs and traces long after the conversation ends. Each of those is a different surface, at a different layer, with a different owner and a different remedy. Treating "PII protection" as one control — a filter bolted onto the model call — leaves most of those surfaces uncovered.

This article is about the opposite approach: a layered PII defense pipeline on AWS, where each layer has a distinct job. Amazon Macie inventories and classifies the sensitive data already sitting in Amazon S3. Amazon Comprehend detects and redacts PII in text before it is indexed for retrieval or migrated between stores. Amazon Bedrock Guardrails' sensitive information filters sit at the model boundary and either block or mask PII in prompts and responses. The ApplyGuardrail API, combined with Amazon Cognito group membership, lets you apply different masking rules to different roles. And an audit layer records what each control did. The point of separating these layers is that PII detected early is cheapest to contain, while PII handled late has the most context to judge it correctly — and you usually want both.

The intended reader is building a real generative AI application and needs to decide, concretely, which layer handles PII, with which service, and whether to detect, mask, or block at each point. This is an implementation guide, not a survey of privacy principles. For the broader responsible-AI control architecture — content filters, denied topics, contextual grounding, and how safety sits as an independent layer around the model — see the companion article Responsible-AI Guardrails Architecture on AWS. This article deliberately narrows to PII and goes deep across the layers instead.

A word of honesty up front, because the subject invites overconfidence. Every detector discussed here is probabilistic. Amazon Comprehend and Bedrock Guardrails both use machine learning that scores likelihood; neither guarantees that it catches every instance of PII, and neither guarantees zero false positives. "The guardrail passed" is not the same as "there is no PII here," and "the field was masked" is not the same as "this data is anonymized." Throughout, the goal is defense in depth: layers that each reduce risk, in the knowledge that any one of them can miss.

2. The Reference Architecture at a Glance

The pipeline this article implements has five layers. A single named architecture makes the rest of the article concrete: every code example maps to one of these layers, and the end-to-end walkthroughs in section 8 trace one request and one document through all of them.

Multi-Layer PII Defense Reference Architecture on AWS
Multi-Layer PII Defense Reference Architecture on AWS

The layers, and the one thing each is responsible for:

  • Data layer — Amazon Macie. Discovers and classifies PII already stored in S3, so you know which buckets and prefixes hold sensitive data before you point a retrieval pipeline at them. Column-level and table-level access control for structured data lakes is a separate concern, delegated to AWS Lake Formation (series No.18).
  • Preprocessing layer — Amazon Comprehend. Detects and redacts PII in text before it is embedded into a knowledge base or moved between stores. This is where a one-time bulk redaction of a historical corpus, or a redaction step in an ingestion pipeline, belongs.
  • Model boundary layer — Amazon Bedrock Guardrails sensitive information filters. Evaluate prompts on the way in and responses on the way out, and either block the whole payload or mask individual entities. This is the runtime, per-request control.
  • Application layer — Amazon Cognito × ApplyGuardrail. Applies role-appropriate masking: a support agent and an auditor asking the same question can receive differently redacted answers, because the application chooses which guardrail to apply based on the caller's Cognito group.
  • Audit layer — CloudWatch / model invocation logging. Records every guardrail intervention and every redaction decision, so you can measure coverage, investigate leaks, and prove what happened — without letting the raw PII leak into the logs themselves.

* You can sort the table by clicking on the column name.
LayerServicePrimary actionWhen it acts
DataAmazon MacieClassify / inventoryContinuously, and before onboarding a corpus
PreprocessingAmazon ComprehendDetect / redactAt ingestion or migration, before embedding
Model boundaryBedrock GuardrailsBlock / mask / detectPer request, on prompt and response
ApplicationCognito and ApplyGuardrailRole-based maskPer request, by caller identity
AuditCloudWatch / loggingRecord decisionsAlways, across all layers

The remainder of the article takes these one layer at a time (sections 4–7), then follows data through the whole pipeline (section 8), then examines what breaks and how to tell (section 9).

3. Where to Handle PII: A Layering Principle

Before the implementation detail, it is worth stating the principle that decides which layer should handle a given piece of PII, because it recurs in every design decision below.

The earlier the layer, the safer; the later the layer, the more context. PII removed at the data or preprocessing layer never reaches the model, never reaches retrieval, and never reaches a log — the blast radius is smallest. But an early layer sees the least context: a nine-digit number in a raw document could be a Social Security Number or an order ID, and a batch redaction step has little to disambiguate it. A late layer — the model boundary, or the application deciding what to show a specific user — has the full request context and the caller's identity, so it can make a more precise judgment, but by then the data has already flowed through more of the system, so a mistake there has already had more chances to leak.

This tension produces a few working rules that the rest of the article applies:

  • Data that should never have entered the corpus is removed early. If a knowledge base is being built from documents that contain customer PII irrelevant to the answers, redact at the preprocessing layer (Comprehend) during ingestion, so the embeddings and the retrieved chunks are already clean.
  • Data whose treatment depends on who is asking is handled late. If the same record should be shown in full to an auditor and masked to a support agent, you cannot decide that at ingestion time — you decide it at the application layer, per request, based on identity.
  • The model boundary is the backstop, not the primary control. Guardrails sensitive information filters catch PII that slipped through the earlier layers and PII the model itself generated. Relying on them alone means every prompt and every response is the first and only place PII is checked — a single probabilistic pass with no redundancy.
  • Never let a raw value survive into a log. Logs are the layer most often forgotten. Whatever you redact upstream, make sure the audit trail records the decision (entity type, action, location) and not the value.

Choosing the wrong layer is a common failure. Handling everything at the model boundary over-couples PII policy to inference and leaves your stored data and logs exposed; handling everything at ingestion means you can never vary treatment by role. The layered pipeline exists so each kind of PII is handled where it is cheapest and most accurate to handle it.

4. Data Layer: Classification with Amazon Macie

The first question a PII program has to answer is not "how do we redact" but "where is the sensitive data." Amazon Macie answers that for Amazon S3.

4.1 What Macie does

Macie uses managed data identifiers — built-in criteria that combine machine learning and pattern matching — to detect sensitive data in S3 objects. Each managed data identifier targets one type of sensitive data, and they span three categories: credentials (for example private keys and AWS secret access keys), financial information (credit card numbers, bank account numbers), and personal information, which includes both protected health information (PHI) and PII such as driver's license numbers and passport numbers. There is a large and growing catalog of these identifiers covering many countries and regions, and you can supplement them with custom data identifiers defined by your own regular expressions.

Macie offers two ways to run this analysis:

  • Automated sensitive data discovery continually samples objects across your S3 estate to build a sensitivity profile for each bucket at a predictable, low sampling rate. It uses a recommended default set of managed data identifiers — a dynamic set that AWS updates over time — and starts constructing a baseline within roughly 48 hours of being enabled. This is the right tool for continuous, estate-wide visibility.
  • Sensitive data discovery jobs are targeted, deeper scans you configure and run against specific buckets, choosing exactly which managed and custom data identifiers to apply. This is the right tool when you need a thorough inventory of a specific corpus — for instance, the documents you are about to ingest into a knowledge base.

4.2 Using classification results downstream

For a generative AI pipeline, Macie's value is that it tells you which S3 locations hold PII before you build retrieval on top of them. A practical pattern:

1. Enable automated sensitive data discovery at the account or Organization level for estate-wide baselines.
2. Before onboarding a new document corpus into a knowledge base, run a targeted discovery job over just that prefix.
3. Route Macie findings to Amazon EventBridge, and use them to gate ingestion: a prefix that comes back with unexpected PII categories is redacted at the preprocessing layer (section 5) before anything is embedded, or held for review, rather than indexed as-is.

Two refinements make the classification fit a generative AI corpus. First, custom data identifiers extend the managed set with your own regular expressions plus optional keyword and proximity requirements, so organization-specific identifiers — an internal customer number, a policy ID — are classified alongside the built-in categories rather than slipping through as unrecognized text. Second, allow lists suppress known-benign text (a fixed sample record, a public example address) so it does not inflate a bucket's sensitivity score or trigger needless redaction downstream. Tuning both keeps the signal that gates ingestion accurate rather than noisy — which matters, because a false "this prefix is clean" is exactly the kind of miss the later layers then have to catch.

Macie is a discovery and classification service; it reports where sensitive data is and how sensitive a bucket is, but it does not itself redact object contents or enforce row- or column-level access. Redaction is the preprocessing layer's job. Fine-grained access control over the classified data — column-level permissions, LF-Tags, cross-account sharing for a structured data lake — belongs to AWS Lake Formation and is covered in Fine-Grained Access Control for AI Data with AWS Lake Formation. Keep Macie scoped to what it is for: knowing where the PII is.

5. Preprocessing Layer: Comprehend Detection and Redaction

Once you know a corpus contains PII, the preprocessing layer decides whether that PII should exist in the text at all by the time it reaches retrieval or the model. Amazon Comprehend is the service that detects and removes it.

5.1 Detection versus redaction

Comprehend separates two operations that are easy to conflate:

  • Detection tells you what PII is present and where. The real-time DetectPiiEntities API returns, for each entity it finds, the entity type, the character offsets where it begins and ends, and a confidence score. It changes nothing about the text — it is a labeling operation.
  • Redaction produces a new version of the text with the PII removed or replaced. For batch workloads, the asynchronous PII redaction job takes documents from S3 and writes redacted output back to S3, replacing detected entities with either a mask character or the entity type label.

A third operation, ContainsPiiEntities, is a cheaper boolean gate: it returns only whether each PII label is present, not where, which makes it a fast pre-filter for deciding whether a document is worth a full DetectPiiEntities pass at all.

Detection is what you use when you need to decide (route a document, alert, score sensitivity); redaction is what you use when you need to transform (produce a clean corpus). A RAG ingestion pipeline typically does both: detect to decide whether a chunk is safe to index, and redact to produce the version that actually gets embedded.

5.2 Real-time detection

The synchronous DetectPiiEntities operation inspects one text string and returns the entities. Its input is a UTF-8 string with a documented maximum size of 100 KB; larger inputs raise TextSizeLimitExceededException, which is your signal to chunk or switch to the asynchronous path. The LanguageCode parameter is required.

import boto3

comprehend = boto3.client("comprehend")

resp = comprehend.detect_pii_entities(
    Text="Please update the record for Jane Doe, SSN 123-45-6789, "
         "reachable at jane.doe@example.com.",
    LanguageCode="en",
)

for entity in resp["Entities"]:
    print(entity["Type"], entity["Score"], entity["BeginOffset"], entity["EndOffset"])
# NAME   0.99  29 37
# SSN    0.99  43 54
# EMAIL  0.99  70 89

The offsets are the important part: because Comprehend returns positions rather than a rewritten string, you control the redaction. You can replace each span with a mask, with the type label ([NAME]), with a tokenized placeholder you can later reverse, or leave high-confidence entities alone and only act on ones above a threshold you choose. The confidence Score lets you set that threshold explicitly rather than accepting every detection.

Because you own the redaction, a threshold-based selective redaction is a few lines: keep only the entities above your confidence bar, then rewrite the spans from the back of the string so earlier offsets stay valid.

def redact_above_threshold(text: str, entities: list, threshold: float = 0.9) -> str:
    kept = [e for e in entities if e["Score"] >= threshold]
    for e in sorted(kept, key=lambda e: e["BeginOffset"], reverse=True):
        text = text[: e["BeginOffset"]] + f"[{e['Type']}]" + text[e["EndOffset"] :]
    return text

redacted = redact_above_threshold(document_text, resp["Entities"])

Rewriting from the highest offset downward is the detail that matters: replace the last entity first and every remaining BeginOffset / EndOffset still points at the right characters, so you never recompute positions after a substitution. Raising threshold lets fewer, higher-confidence detections through to redaction; lowering it redacts more aggressively at the cost of more false positives — the trade-off from section 5.4, expressed as one number.

On language coverage, be precise and do not over-claim. AWS documents real-time PII detection for English and Spanish. The LanguageCode field is a shared enumeration across Comprehend's detection APIs and will accept other codes, but PII entity recognition is documented for English and Spanish — confirm the current supported languages before relying on any other language, and do not assume that an accepted language code implies validated PII coverage.

5.3 A bulk redaction pipeline for ingestion

Real-time detection handles a single request-sized string. For a corpus — a historical archive being migrated, or a batch of documents entering a knowledge base — you use the asynchronous analysis job, which reads from S3 and writes redacted output back to S3 without your code touching each document. The asynchronous path removes the 100 KB per-call ceiling and processes documents in bulk.

import boto3

comprehend = boto3.client("comprehend")

job = comprehend.start_pii_entities_detection_job(
    JobName="kb-corpus-redaction",
    LanguageCode="en",
    Mode="ONLY_REDACTION",  # produce redacted output, not just labels
    RedactionConfig={
        "PiiEntityTypes": ["ALL"],       # or an explicit subset
        "MaskMode": "REPLACE_WITH_PII_ENTITY_TYPE",  # or MASK
    },
    InputDataConfig={
        "S3Uri": "s3://my-ingest-raw/corpus/",
        "InputFormat": "ONE_DOC_PER_FILE",
    },
    OutputDataConfig={"S3Uri": "s3://my-ingest-redacted/corpus/"},
    DataAccessRoleArn="arn:aws:iam::111122223333:role/ComprehendPiiRedactionRole",
)
print(job["JobId"], job["JobStatus"])

The Mode distinguishes a detection-only job (ONLY_OFFSETS, which emits labels and offsets) from a redaction job (ONLY_REDACTION, which emits transformed documents). RedactionConfig.PiiEntityTypes scopes which entity types to act on — redacting only the ones that matter for your corpus avoids destroying identifiers you actually need downstream. MaskMode chooses between replacing an entity with its type label ([EMAIL], useful when a human or a retrieval step benefits from knowing something was removed) and overwriting it with a mask character (removing even the shape of the data).

Two design choices to make deliberately at this layer:

  • Redact before embedding, not after. If you redact after building embeddings, the vectors still encode the PII and the retrieval index still returns it. The redaction has to happen on the text that becomes the embedding input. In a Bedrock Knowledge Bases pipeline, that means redacting in the S3 source (or a custom transformation) before ingestion, not in the answer.
  • Decide whether redaction is reversible. REPLACE_WITH_PII_ENTITY_TYPE is one-way — the original value is gone from the redacted corpus. If a downstream workflow legitimately needs to recover the value for authorized users, redaction alone will not give you that; you need a tokenization scheme (storing the mapping in a separate, access-controlled store) layered on top, which is a larger design than this article covers. Do not assume the redacted corpus can be "un-redacted."

5.4 The honesty caveat at this layer

Comprehend's PII detection is a probabilistic model. It will miss some entities (a name in an unusual format, PII embedded in a way the model does not recognize) and it will over-detect others (flagging a benign number as an identifier). Setting the redaction to act only above a confidence threshold trades one error for the other: a high threshold reduces over-redaction but lets more PII through; a low threshold catches more but mangles more legitimate text. There is no threshold that eliminates both errors, which is exactly why this is one layer of several rather than the only line of defense.

6. Model Boundary: Guardrails Sensitive Information Filters

The preprocessing layer cleans data at rest. The model boundary handles data in motion, per request: the prompt a user just typed, and the response the model just generated. Amazon Bedrock Guardrails' sensitive information filters are the control here.

6.1 What the filter is, and what it is not

A sensitive information filter detects PII in input prompts or model responses. Like Comprehend, it is a probabilistic, context-dependent machine learning solution — it scores sensitive information based on the surrounding context, not by exact pattern alone. It works across both natural language and code, so it can catch PII embedded in variable names, string literals, comments, and hardcoded credentials, not just prose. It offers a set of built-in PII types and also supports custom regular expressions for organization-specific identifiers that pattern-matching handles better than ML.

Two limits are worth stating plainly, because they define where this control does and does not apply. The filter supports text output only; it does not detect PII returned in tool_use (function-call) output parameters via the supported APIs. And it is probabilistic — the same caveat as Comprehend applies. It is a strong runtime backstop, not a guarantee.

The built-in PII types are grouped into categories: General (ADDRESS, AGE, NAME, EMAIL, PHONE, USERNAME, PASSWORD, DRIVER_ID, LICENSE_PLATE, VEHICLE_IDENTIFICATION_NUMBER), Finance (CREDIT_DEBIT_CARD_NUMBER, CREDIT_DEBIT_CARD_CVV, CREDIT_DEBIT_CARD_EXPIRY, PIN, INTERNATIONAL_BANK_ACCOUNT_NUMBER, SWIFT_CODE), IT (IP_ADDRESS, MAC_ADDRESS, URL, AWS_ACCESS_KEY, AWS_SECRET_KEY), and country-specific sets for the USA, Canada, and the UK (Social Security / passport / tax numbers and their national equivalents). Rather than reproduce the full list here — it changes as AWS adds entity types — configure from the current GuardrailPiiEntityConfig reference and treat the categories above as the shape of what is available.

6.2 Block versus mask, per entity

For each PII entity type (and each custom regex), the guardrail action is configured independently, with three possible behaviors:

  • Block — if the entity is detected, the guardrail blocks the entire request or response and returns a configured message. Use this when the presence of that entity means the content should not proceed at all — for example, a public-document Q and A assistant that should never handle a customer's card number.
  • Mask (anonymize) — the guardrail redacts the entity in place and replaces it with the type identifier, such as {NAME} or {EMAIL}, letting the rest of the content through. Use this for summarization and similar tasks where the surrounding content is useful even with the PII removed — masking names while generating a summary of a support conversation, for instance.
  • Detect (detect mode) — the guardrail takes no action but reports what it detected in the trace. Mask is available only with sensitive information filters; detect mode is the evaluation setting discussed below.

Critically, the action is per entity: you can block credit card numbers while masking names and emails in the same guardrail, and you can set the input action and the output action separately. The configuration exposes inputAction and outputAction (and inputEnabled / outputEnabled) precisely so that a prompt and a response can be treated differently — you might mask on input but block on output, or vice versa.

* You can sort the table by clicking on the column name.
Entity exampleTypical actionWhy
Credit card number, password, secret keyBlockPresence means the content should not proceed at all
Name, email, phone, addressMaskSurrounding content is useful with the identifier removed
Any entity, during evaluationDetectMeasure coverage without altering the flow, then switch

These are starting points, not rules — the right action depends on the use case, and the input and output sides can differ. The configuration below blocks card numbers on both sides while masking names and emails, and adds a custom regex for an internal case identifier.

import boto3

bedrock = boto3.client("bedrock")

resp = bedrock.create_guardrail(
    name="genai-pii-guardrail",
    description="PII handling for the customer-facing assistant",
    sensitiveInformationPolicyConfig={
        "piiEntitiesConfig": [
            {
                "type": "EMAIL",
                "action": "ANONYMIZE",
                "inputAction": "ANONYMIZE",
                "outputAction": "ANONYMIZE",
                "inputEnabled": True,
                "outputEnabled": True,
            },
            {
                "type": "NAME",
                "action": "ANONYMIZE",
                "inputAction": "ANONYMIZE",
                "outputAction": "ANONYMIZE",
                "inputEnabled": True,
                "outputEnabled": True,
            },
            {
                "type": "CREDIT_DEBIT_CARD_NUMBER",
                "action": "BLOCK",
                "inputAction": "BLOCK",
                "outputAction": "BLOCK",
                "inputEnabled": True,
                "outputEnabled": True,
            },
        ],
        "regexesConfig": [
            {
                "name": "internal-case-id",
                "description": "Illustrative internal case identifier, e.g. CASE-000123 - adjust the pattern to your organization's ID scheme",
                "pattern": r"CASE-\d{6}",
                "action": "ANONYMIZE",
            }
        ],
    },
    blockedInputMessaging="I can't process input that contains that kind of information.",
    blockedOutputsMessaging="I can't share a response that contains that kind of information.",
)
print(resp["guardrailId"], resp["guardrailArn"], resp["version"])

Note the two levels of naming for the action: at configuration time the valid values are BLOCK | ANONYMIZE | NONE; at runtime the assessment reports the applied action as BLOCKED or ANONYMIZED. Same concept, different tense.

6.3 The development-to-production operational pattern

A guardrail is only as good as its coverage, and you cannot know its coverage by reading the config — you have to measure it against real traffic. This is what detect mode is for. In detect mode, the guardrail evaluates content and reports every detection in the trace but takes no action, so you can run it against representative prompts and responses and see exactly what it catches and misses without altering the user experience or blocking anything.

The operational pattern that follows:

  1. In development, run the sensitive information filter in detect mode (or with mask, which is also non-destructive to the flow while still visible) against a corpus of real, representative inputs and outputs. Inspect the traces: which entity types fire, where the false negatives are (PII you know is present but the filter did not flag), and where the false positives are.
  2. Tune the entity set and custom regexes based on that evidence. Add regexes for organization-specific identifiers the ML model does not recognize; remove or down-scope entity types that produce noise for your domain.
  3. Switch to the enforcing action for production — block for entities whose presence must stop the interaction, mask for entities that should be removed but not stop it. Now the filter is doing real work, and you have measured evidence of what it will and will not catch.

The value of this sequence is that you enter production with a quantified sense of the filter's recall on your own data, rather than discovering the gaps from an incident. It also gives you a repeatable process: when you add a new use case or a new document domain, you re-run the detect-mode evaluation before flipping to enforcement.

At runtime, the same guardrail attaches directly to a model call: the Bedrock Converse API takes a guardrailConfig (guardrail identifier, version, and trace), so the input and output evaluation happen as part of the invocation rather than as separate calls — the standalone ApplyGuardrail path (section 7) is for the cases where you need to evaluate without, or apart from, a model invocation. Guardrails can also be applied to streaming responses; consult the Converse API reference for the current streaming behavior rather than assuming a masked token stream behaves identically to a buffered response.

For the full breadth of Guardrails policy types beyond sensitive information filters — content filters, denied topics, contextual grounding, word filters — and their implementation details, see Amazon Bedrock Guardrails Implementation Deep Dive. This section stays on the sensitive information filter.

7. Application Layer: Role-Based Dynamic Masking with ApplyGuardrail

The model boundary applies one guardrail to a request. But real applications have roles: a support agent, an auditor, a data scientist, and an end customer may issue the same query and legitimately need different levels of redaction in the answer. That decision cannot live in a single static guardrail — it depends on who is asking, which is identity, which is the application's concern. This is the application layer.

7.1 An honest framing: this is a pattern, not a feature

There is no AWS feature called "Cognito-group-to-guardrail mapping." What exists are two composable building blocks, both official:

  • Amazon Cognito authenticates users and carries their group membership. When a user belongs to Cognito groups, their ID and access tokens include a cognito:groups claim, so the application can read the caller's role directly from the verified JWT.
  • The ApplyGuardrail API evaluates any text against a specified guardrail — identified by guardrailIdentifier and guardrailVersionwithout invoking a foundation model, and independently for input (source: INPUT) or output (source: OUTPUT). Because the guardrail identifier is a per-call parameter, nothing stops the application from choosing a different guardrail on each call.

Role-based dynamic masking is the composition of those two facts: read the caller's group from the Cognito token, map the group to a guardrail identifier that encodes that role's masking policy, and call ApplyGuardrail with it. The composition is an architectural pattern you assemble; the parts are supported APIs. Presenting it as a single managed capability would be dishonest, so it is presented here as what it is.

7.2 Why ApplyGuardrail, and independent application

ApplyGuardrail is decoupled from foundation models. That decoupling is what makes the pattern work and what makes it efficient:

  • You can evaluate the user input before retrieval — in a RAG flow, screen the prompt for PII before it is used to search a knowledge base, rather than waiting for the final generation.
  • You can evaluate the model output after generation with a different guardrail than you used on the input, or with a role-specific guardrail chosen from the caller's identity.
  • Because no model is invoked, applying a role-specific mask to an already-generated answer is a standalone text-evaluation call, not a second inference.

The request carries source (INPUT or OUTPUT) and the content; the response carries an action of GUARDRAIL_INTERVENED or NONE, the (possibly masked) outputs array, and an assessments block detailing each PII entity found, its matched text, and whether the action was BLOCKED or ANONYMIZED.

That assessments block is also what you feed the audit layer. For each intervention it names the entity type and the action taken; record those, along with the guardrailVersion and whether the source was input or output, and you have a complete account of what the control did — without recording the match (the matched text), which is the one field in the assessment you must not persist, because it is the PII itself. The pattern is the recurring one: log the decision, never the value.

7.3 Implementation

The application resolves the caller's role from the verified Cognito token, selects the guardrail that encodes that role's policy, and applies it to the model's output before returning it.

import boto3

bedrock_runtime = boto3.client("bedrock-runtime")

# Each role maps to a guardrail whose sensitive-information policy encodes
# that role's masking rules. Auditors get an unmasked view via a guardrail
# whose PII actions are NONE; agents get a masking guardrail.
ROLE_GUARDRAILS = {
    "auditors":  {"id": "gr-auditor-view",  "version": "3"},
    "agents":    {"id": "gr-agent-masked",  "version": "5"},
    "customers": {"id": "gr-customer-safe", "version": "5"},
}
DEFAULT_GUARDRAIL = {"id": "gr-customer-safe", "version": "5"}


def guardrail_for(cognito_groups: list[str]) -> dict:
    # Most-privileged match wins; fall back to the safest (most-masked) policy.
    for role in ("auditors", "agents", "customers"):
        if role in cognito_groups:
            return ROLE_GUARDRAILS[role]
    return DEFAULT_GUARDRAIL


def mask_for_role(text: str, cognito_groups: list[str]) -> str:
    gr = guardrail_for(cognito_groups)
    resp = bedrock_runtime.apply_guardrail(
        guardrailIdentifier=gr["id"],
        guardrailVersion=gr["version"],
        source="OUTPUT",
        content=[{"text": {"text": text}}],
    )
    if resp["action"] == "GUARDRAIL_INTERVENED":
        # Masked content is returned in the same shape, with entities replaced.
        return resp["outputs"][0]["text"]
    return text  # action == NONE: nothing sensitive detected

The cognito_groups value must come from the verified token — validate the JWT signature and claims (an API Gateway Cognito authorizer or a Lambda authorizer does this) before trusting the group claim. Treating an unverified, client-supplied role as authoritative would let a caller pick their own masking level, which defeats the control. The "most-privileged match wins, otherwise fall back to the safest policy" ordering is deliberate: an unrecognized or missing group gets the most-masked treatment, so a misconfiguration fails closed.

The same call shape screens input: set source="INPUT" and pass the user's prompt before it is used for retrieval, so a prompt that carries PII is masked (or blocked) before it reaches a knowledge base or the model. Reusing one guardrail-resolution function for both directions keeps the role policy in a single place, so a change to a role's masking rules is a guardrail edit, not a code change scattered across input and output paths.

The two guardrail versions doing the work here differ only in their sensitiveInformationPolicyConfig: the auditor guardrail sets the relevant PII entities to NONE (see them in full), while the agent and customer guardrails set them to ANONYMIZE. Versioning the guardrails (section 9) means you can change a role's policy without editing application code — the code references a guardrail identifier and version; the policy lives in the guardrail.

8. End-to-End Walkthroughs

The layers are clearest when you follow real data through them. Here are the two paths that matter: a live user request, and a document being ingested.

End-to-End PII Flow: User Request and Document Ingestion
End-to-End PII Flow: User Request and Document Ingestion

8.1 A PII-bearing user request

A support agent asks: "Summarize the last call with Jane Doe, jane.doe@example.com, card ending 4242."

1. Authentication (application layer). The request arrives with a Cognito ID token. The authorizer verifies it and extracts cognito:groups: ["agents"]. The application now knows the caller's role.
2. Input screening with ApplyGuardrail (source: INPUT). Before the prompt is used for retrieval, the application evaluates it. The agent guardrail is configured to ANONYMIZE NAME and EMAIL and to BLOCK CREDIT_DEBIT_CARD_NUMBER. The card number triggers GUARDRAIL_INTERVENED with a block action — the request is stopped and the agent is told the input cannot be processed, because a full card number should never flow into the model or the retrieval layer. (Had the input contained only the name and email, they would be masked to {NAME} and {EMAIL} and the flow would continue.)
3. Retrieval (preprocessing layer already applied). Assume instead the input passed screening. Retrieval runs against a knowledge base whose documents were already redacted by Comprehend at ingestion (section 5), so the retrieved chunks do not reintroduce PII from stored data.
4. Generation at the model boundary. The model is invoked with a guardrail attached (via the Converse API's guardrailConfig, or by calling ApplyGuardrail on the output). Any PII the model generates in its answer — including PII it might reconstruct — is evaluated on the way out.
5. Role-based output masking (application layer). The generated summary is passed through ApplyGuardrail with source: OUTPUT and the agent guardrail. Names and emails are masked to {NAME} and {EMAIL}; the answer returns with the sensitive spans redacted. An auditor issuing the identical request would be resolved to the auditor guardrail, whose PII actions are NONE, and would receive the unmasked summary.
6. Audit (audit layer). The guardrail interventions — which entity types fired, at input and output, with which action — are captured as decision metadata from the guardrail trace/assessment output, without persisting the raw card number or email. Note that this is distinct from Bedrock model invocation logging, whose input field always records the original unmodified request (see Section 10 for protecting the logs themselves).

import boto3

bedrock_runtime = boto3.client("bedrock-runtime")

resp = bedrock_runtime.converse(
    modelId="us.anthropic.claude-opus-4-8",  # current-generation model via a cross-Region inference profile ID
    messages=[{"role": "user", "content": [{"text": screened_prompt}]}],
    guardrailConfig={
        "guardrailIdentifier": "gr-agent-masked",
        "guardrailVersion": "5",
        "trace": "enabled",
    },
)
answer = resp["output"]["message"]["content"][0]["text"]
# The trace in resp records what the guardrail assessed and acted on.

8.2 A PII-bearing document ingestion

A batch of call-center transcripts is being added to the knowledge base.

1. Classification (data layer). Macie's discovery job over the source prefix reports that the transcripts contain NAME, EMAIL, PHONE, and US_SOCIAL_SECURITY_NUMBER. The prefix is flagged as requiring redaction before ingestion.
2. Redaction (preprocessing layer). A Comprehend asynchronous redaction job (Mode: ONLY_REDACTION) reads the raw transcripts from S3 and writes a redacted copy to a separate prefix, replacing the flagged entity types with their labels. The names and emails become [NAME] and [EMAIL]; the SSNs are removed.
3. Ingestion. The knowledge base ingests the redacted prefix, not the raw one. Embeddings and the retrieval index are built from text that no longer contains the PII, so no later query can retrieve it.
4. Runtime backstop (model boundary). Even so, at query time the sensitive information filter still evaluates prompts and responses — the backstop for anything the ingestion redaction missed and for PII the model itself generates.
5. Audit. The Macie findings, the Comprehend job identifier and its output location, and the guardrail configuration version form the record of how this corpus was handled.

The two paths meet at the same principle from section 3: the document path removes PII early (data and preprocessing layers) so it never enters retrieval, while the request path handles identity-dependent masking late (application layer) where the caller's role is known. Each piece of PII is handled at the layer where it is cheapest and most accurate to handle. This ingestion path also intersects the document-intelligence pipeline described in Multimodal Document Intelligence Pipeline on AWS, where extraction and processing happen before the redaction step shown here.

9. Failure Modes and Diagnostics

Every layer can fail, and the failures are often silent — a mask that removed too much, a detector that missed an entity, a double transformation that produced garbled text. This section is the symptom-to-cause-to-remedy guide.

9.1 Over-masking (false positives)

Symptom: answers or redacted documents have too much removed — order IDs masked as account numbers, ordinary numbers flagged as identifiers, names of public entities masked. Cause: the detector is probabilistic and errs toward flagging; the entity set is broader than the use case needs; a custom regex is too greedy. Remedy: narrow the configured entity types to those the use case actually requires; tighten custom regexes; where the service exposes a confidence threshold (Comprehend), raise it so only high-confidence detections are acted on. Verify by running detect mode and comparing what fired against a hand-labeled sample.

9.2 Under-masking (false negatives and leakage)

Symptom: PII appears in an answer, a log, or an indexed chunk that should have been removed. Cause: the entity type was not enabled; the PII was in a format or language the model does not recognize; it was in a tool_use output parameter, which the sensitive information filter does not cover; or it was reconstructed by the model in a way no single layer caught. Remedy: this is why the design is layered — no single filter is trusted to catch everything. Confirm the entity type is enabled at every layer it should be; add the language or a custom regex if the format is predictable; and for the tool_use gap, evaluate tool outputs in your application code (with ApplyGuardrail on the tool result text) rather than assuming the model-boundary filter covered them. Treat any confirmed leak as evidence to strengthen an earlier layer, since the earlier the removal, the smaller the exposure.

9.3 Double transformation across layers

Symptom: redacted text looks wrong — {NAME} inside [NAME], or masked tokens being re-masked into unreadable fragments. Cause: two layers redacting the same content in sequence, each applying its own replacement to the other's output. For example, Comprehend already replaced an email with [EMAIL] at ingestion, and then a mask-mode guardrail treats the retrieved [EMAIL] placeholder — or some other layer's output — as new content to transform. Remedy: define, per data path, which layer owns the transformation, and let later layers operate in detect (report-only) mode on content an earlier layer already redacted, rather than re-masking. The layering principle in section 3 is the antidote: know which layer is authoritative for each piece of PII, so the others do not double-handle it.

9.4 Latency

Symptom: added round-trips (an ApplyGuardrail input screen, a separate output mask, a Comprehend call) increase end-to-end latency. Cause: each layer is a network call, and screening input before retrieval plus masking output after generation adds two evaluations around the model call. Remedy: decide which evaluations are on the synchronous request path and which can be asynchronous. Corpus redaction (Comprehend batch) is entirely offline and off the request path. At runtime, prefer the guardrail attached to the model invocation (a single integrated evaluation via guardrailConfig) over multiple separate ApplyGuardrail calls where a single pass suffices; reserve separate ApplyGuardrail calls for the cases that genuinely need them, such as screening input before an expensive retrieval or applying a role-specific mask that differs from the invocation guardrail.

9.5 Verifying with the intervention log

The way to know any of this is working is the trace. With tracing enabled, both the Converse guardrailConfig path and the ApplyGuardrail response report the assessment: which policy fired, which entity types were detected, and what action was taken. Comprehend detection jobs likewise emit the entities and offsets they found. Build the audit layer on these: aggregate intervention counts by entity type over time to measure coverage and spot drift, alert on unexpected blocks, and — crucially — store the decision metadata (entity type, action, location, guardrail version) rather than the matched value, so the diagnostic record does not itself become a PII leak.

9.6 Observability of the controls

Beyond individual traces, treat the guardrail interventions as a metric stream. The ApplyGuardrail response reports processing latency and policy usage units per call, and Bedrock publishes operational metrics to Amazon CloudWatch; together these let you watch three things over time. The intervention rate per entity type — a sudden change signals either a new data pattern or a regression in an upstream layer (a Comprehend redaction step that stopped running would show up as more PII reaching the model boundary). The added latency the guardrail contributes to the request path — the input to the trade-off in section 9.4. And the block rate — a spike may be a genuine attack, a data-quality change, or an over-broad policy, and the trace tells you which. Alert on deviations from a baseline rather than on absolute counts, and keep the alerting metadata — entity type, action, guardrail version — free of the matched values, for the same reason the audit log is.

10. Cross-Cutting: Logs, Languages, and Regions

Three concerns span every layer and are easy to get wrong precisely because they are nobody's single responsibility.

Keep PII out of the logs. The audit trail must record that a NAME was masked at output, not the name itself. This applies to model invocation logs, application logs, and traces alike. When you enable Bedrock model invocation logging, be aware of a critical property: AWS documents that the input field in the invocation log always contains the original, unmodified request regardless of guardrail intervention — guardrails do not sanitize what the invocation log records. To protect sensitive information in the logs themselves, apply Amazon CloudWatch Logs data protection (masking policies) to the log group, or disable prompt/response delivery entirely and keep only metadata. The most common PII leak in a generative AI system is not the model's answer; it is the debug log that captured the unredacted prompt. Design the audit layer to store decision metadata, not values.

Match the language to the detector's documented coverage. Comprehend documents real-time PII detection for English and Spanish, and the guardrail's ML detection likewise has documented language behavior. An application that serves other languages cannot assume the same recall; verify the current documented coverage for each service and each language you support, and where a language is unsupported, do not present the pipeline as protecting it. This is a place where honest scoping matters more than optimistic defaults.

Confirm regional and cross-region behavior. All of these services are regional, and their availability and, for Bedrock, the model and guardrail behavior can vary by Region and by whether cross-region inference is in play. Deploy the guardrail, the Comprehend calls, and the Macie configuration in the Regions where your data and your inference actually run, and confirm each service and feature is available there rather than assuming parity. For governance-scale controls — condition keys, SCPs, and multi-account guardrail enforcement that make these controls organizational rather than per-application — see Amazon Bedrock Security and Governance.

Residency and encryption reinforce the same point. The Comprehend asynchronous redaction job reads from and writes to your S3 buckets in the Region where you run it, under a role you supply, and you can encrypt both the input and the redacted output with your own AWS KMS keys and run the job with a VPC configuration so the data path stays inside your network. Keeping the batch in-Region and encrypted means the redacted corpus — and the raw corpus it was derived from — never leaves the boundary you have already reasoned about, and the same discipline applies to where the guardrail and the model run: a redaction that is careful about entities but careless about where the buckets live has only moved the exposure, not removed it.

One more cross-cutting control: least-privilege IAM. Each layer's role should be able to do only its job. The Comprehend redaction job's DataAccessRoleArn needs read on the raw prefix and write on the redacted prefix — and nothing else. The application role that calls ApplyGuardrail needs bedrock:ApplyGuardrail on the specific guardrail ARNs it is allowed to use, not blanket Bedrock access.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ApplyOnlyPermittedGuardrails",
      "Effect": "Allow",
      "Action": "bedrock:ApplyGuardrail",
      "Resource": [
        "arn:aws:bedrock:us-east-1:111122223333:guardrail/gr-agent-masked",
        "arn:aws:bedrock:us-east-1:111122223333:guardrail/gr-auditor-view",
        "arn:aws:bedrock:us-east-1:111122223333:guardrail/gr-customer-safe"
      ]
    }
  ]
}

Scoping the guardrail ARNs matters for the role-based masking pattern specifically: it prevents application code from applying an auditor's unmasked guardrail from a context that should only ever use a masking guardrail.

11. Frequently Asked Questions

Does masking make my data anonymized?
No. Masking replaces detected entities with type labels or characters, but it operates on what the detector finds, and it does not address quasi-identifiers or the risk of re-identification from the surrounding context. Treat masking as risk reduction, not as a guarantee of anonymization, and do not describe a masked dataset as anonymized in any compliance sense.

Should I use Comprehend or Guardrails for PII — aren't they redundant?
They operate at different layers and are complementary, not redundant. Comprehend redacts data at rest before it enters retrieval or is migrated (the preprocessing layer); Guardrails sensitive information filters evaluate prompts and responses per request at the model boundary. Use Comprehend to keep PII out of your corpus and Guardrails as the runtime backstop for what slips through and what the model generates.

Can the sensitive information filter catch PII in function-call (tool) outputs?
No. The filter supports text output only and does not detect PII in tool_use output parameters via the supported APIs. If your agent returns PII through tool outputs, evaluate that text in your own application code — for example by passing the tool result through ApplyGuardrail — rather than assuming the model-boundary filter covered it.

How do I give auditors an unmasked view but mask for support agents?
Use the application-layer pattern in section 7: read the caller's role from the verified Cognito group claim, map each role to a guardrail whose sensitive-information policy encodes that role's masking (PII actions of NONE for the auditor guardrail, ANONYMIZE for the others), and apply it with ApplyGuardrail on the output. There is no single managed "role-to-guardrail" feature; this is a composition of Cognito identity and the per-call guardrailIdentifier.

Which languages are covered?
Amazon Comprehend documents real-time PII detection for English and Spanish; confirm the current supported-languages documentation for both Comprehend and Guardrails before relying on any other language, and do not assume that an accepted language code implies validated PII coverage.

Do I have to redact before or after embedding?
Before. If you redact after building embeddings, the vectors still encode the PII and retrieval can still surface it. The redaction has to happen on the text that becomes the embedding input — in the S3 source or a transformation step ahead of ingestion.

Is redaction reversible?
Not by default. Replacing an entity with its type label removes the original value from the output. If an authorized workflow legitimately needs to recover it, layer a tokenization scheme (with the mapping in a separate, access-controlled store) on top — redaction alone does not provide reversibility.

Should the guardrail be attached to the model or called standalone?
Prefer attaching it to the invocation (the Converse API's guardrailConfig) when a single evaluation of the prompt and response is what you need — it is one integrated pass rather than extra round-trips. Reach for standalone ApplyGuardrail when you need to evaluate without invoking a model (screening input before an expensive retrieval) or with a different guardrail than the invocation used (the role-based output mask in section 7). Both are appropriate; the choice is about how many independent evaluations the flow genuinely requires, which is also the latency lever in section 9.4.

What about PII in images, audio, or other non-text content?
The controls in this article operate on text. Comprehend PII detection and the Guardrails sensitive information filter both work on text (the filter explicitly supports text output only). PII in images, audio, or scanned documents has to be turned into text first — through the document-intelligence pipeline that extracts it — and then flows through the same layers. Do not assume a text-oriented pipeline protects a modality it never sees; scope the claim to what the detectors actually process.

12. Summary

PII protection for a generative AI application on AWS is a layered problem, and the layers have distinct jobs. Amazon Macie tells you where sensitive data lives in S3. Amazon Comprehend detects and redacts it before it enters retrieval or is migrated, keeping your corpus clean at the source. Amazon Bedrock Guardrails' sensitive information filters evaluate prompts and responses at the model boundary, blocking or masking per entity, with a development-to-production path (detect mode to measure coverage, then enforce) that lets you enter production with quantified confidence. The ApplyGuardrail API, decoupled from the model and composed with Amazon Cognito group membership, applies role-appropriate masking so the same query can be answered differently for an auditor and a support agent. And an audit layer records the decisions — not the values.

The principle that ties it together: handle each piece of PII at the layer where it is cheapest and most accurate to handle. Remove early what should never have entered; decide late what depends on who is asking; and treat the model boundary as a backstop, not the only line. Above all, hold the honest position that every detector here is probabilistic — masked is not anonymized, and "the guardrail passed" is not proof of a clean payload. Defense in depth is the design precisely because any single layer can miss.

13. References

Related Articles


References:
Tech Blog with curated related content

Written by Hidekazu Konishi