Security & Compliance Boundaries for Claims Data

A claims payload is protected health information (PHI) from the moment the NCPDP switch hands it to the adjudicator until the last audit record referencing it is retired. This page sits within the PBM Architecture & Taxonomy Foundations domain and owns one specific sub-problem: defining the trust zones a claim crosses during adjudication, the fields that must be tokenized or stripped at each boundary, and the code-level controls that keep raw member identity out of logs, caches, and downstream services. Security here is not a perimeter firewall bolted on after the fact — it is a set of architectural primitives (schema gates, tenant scoping, field tokenization, immutable audit emission) that every other subsystem in the adjudication architecture inherits, from NCPDP D.0 Message Parsing Strategies at ingress to copay settlement at egress.

Prerequisites

Before the boundary controls on this page can run, the following inputs, dependencies, and environment assumptions must already be in place:

  • A field-keyed payload, not raw bytes. These controls consume the structured AdjudicationPayload emitted by the parser, with fields addressed by NCPDP data-element reference (302-C2 Cardholder ID, 310-CA Patient First Name, 407-D7 Product/Service ID, 401-D1 Date of Service). Raw D.0 wire bytes are decoded and discarded inside the ingress boundary — they are never persisted or logged.
  • A validated payload. The trust-zone transition assumes the object has already cleared Schema Validation & Error Categorization; an unvalidated payload cannot be safely tenant-scoped because its plan_id may be malformed.
  • A key-management backend. Encryption and tokenization depend on a centralized KMS/HSM (AWS KMS, HashiCorp Vault Transit, or an on-prem HSM) exposing FIPS 140-2/140-3 validated modules. Application code never holds long-lived key material; it requests envelope keys or tokenization operations per request.
  • Library versions. The reference implementation targets Python 3.11+ (for Self and structural match), pydantic>=2.5 for declarative schema enforcement and native Decimal coercion, cryptography>=42 for AES-GCM envelope encryption, and opentelemetry-api>=1.24 for PHI-safe span and counter emission. Money fields use decimal.Decimal, never float.
  • A tenant taxonomy. Every claim resolves to exactly one tenant (payer × plan sponsor × network segment). The mapping from plan_id prefix to tenant is a versioned reference table loaded at worker start, consistent with the versioned-snapshot contract used across the adjudication architecture.

Trust Zones and Field-Level Boundary Rules

Adjudication crosses three trust zones. A field’s required treatment is a function of the zone it is entering, not the operation being performed — which keeps the rules stable as new processing steps are added. The table below is the load-bearing specification: it maps each PHI-bearing NCPDP field to the boundary control applied as it moves inward, and to the audit obligation triggered at the same point. These field-level obligations implement the addressable encryption and access-control requirements of the HHS HIPAA Security Rule.

NCPDP field Contents Ingress (edge/DMZ) Internal zone Audit obligation
302-C2 Cardholder ID Member identifier Tokenize to surrogate Surrogate only; raw stripped Log token, never raw 302-C2
310-CA / 311-CB Patient name Member PII Strip after routing derived Absent Never persisted in adjudication logs
304-C4 Date of Birth Member PII Retain for eligibility, encrypt at rest AES-256-GCM at rest Access recorded, value redacted
407-D7 Product/Service ID (NDC) Drug identifier Pass through Crosswalked to GPI Log NDC-11 (non-PHI), tie to token
401-D1 Date of Service Clinical date Pass through Retained Logged in clear (non-identifying alone)
409-D9 Ingredient Cost / copay Financial Pass through Decimal arithmetic only Amount logged, member link tokenized
Raw D.0 segment bytes Full transaction Decode then discard Never present Forbidden in any log or cache

Two rules override everything else. First, 302-C2 and 310-CA are stripped or tokenized at the earliest boundary at which routing metadata has been derived — the moment the tenant and eligibility keys are computed, the raw member identity leaves the working object. Second, no code path may serialize the raw payload to a log, a metric label, an exception message, or an ephemeral cache. Structured telemetry keys on a claim reference and a payload hash, never on member identity. Downstream systems such as PBM Portal Sync Architecture receive only adjudicated, de-identified, or explicitly authorized claim states across the same boundary contract.

PHI field lifecycle across the adjudication trust zones A timeline with three vertical zone bands — Edge/DMZ ingress, internal adjudication, and immutable audit log — separated by a dashed routing boundary at which tenant and eligibility keys are derived. Five NCPDP fields form rows. 302-C2 Cardholder ID enters raw at ingress, is tokenized via KMS at the boundary, and exists as a surrogate token only in the internal and audit zones. 310-CA Patient name enters raw, is stripped at the boundary, and is absent internally and never logged. 304-C4 Date of birth enters raw, is encrypted to AES-256-GCM, and its access is recorded with the value redacted. 407-D7 NDC and 409-D9 ingredient cost are non-PHI: they pass through the boundary unchanged, are used for GPI crosswalk and Decimal math internally, and are logged in clear as NDC-11 and amount only. Ingress — Edge / DMZ Internal adjudication zone Immutable audit log routing boundary — raw member identity leaves the working object here 302-C2Cardholder ID 310-CAPatient name 304-C4Date of birth 407-D7Product/Service ID 409-D9Ingredient cost tokenize · KMS strip encrypt pass through pass through 302-C2 raw 310-CA raw 304-C4 raw 407-D7 NDC 409-D9 cost surrogate token absent AES-256-GCM GPI crosswalk Decimal math token only never logged access · redacted NDC-11 logged amount only

Figure: The lifecycle of each PHI-bearing NCPDP field across the three trust zones. The dashed routing boundary is the single point where tenant and eligibility keys are derived — after it, raw 302-C2 is a surrogate token, 310-CA is stripped and never logged, and 304-C4 survives only as AES-256-GCM ciphertext. Non-PHI 407-D7 and 409-D9 pass through in clear.

Reference Python Implementation

The pattern below implements the ingress boundary as an explicit gate. It uses pydantic v2 for NCPDP-aligned contract enforcement, a KMS-backed tokenizer for 302-C2, contextvars for per-request tenant scoping, a PHI-safe logging filter that drops designated fields before persistence, and decimal.Decimal for the one financial field it touches. Every field reference carries its NCPDP data-element code as an inline comment — those codes are the identifiers this workload is searched and audited by.

python
import logging
from contextvars import ContextVar
from dataclasses import dataclass
from datetime import date
from decimal import Decimal
from typing import Any, Self

from pydantic import BaseModel, Field, field_validator, ValidationError

# Per-request tenant scope; set once at the ingress boundary, read everywhere.
tenant_ctx: ContextVar[str] = ContextVar("tenant")

# PHI fields that must never survive into a persisted log record.
PHI_FIELDS = ("cardholder_id", "patient_first_name", "patient_dob")  # 302-C2, 310-CA, 304-C4


class PHISafeFilter(logging.Filter):
    """Redact designated PHI attributes before any handler serializes the record."""

    def filter(self, record: logging.LogRecord) -> bool:
        for f in PHI_FIELDS:
            if hasattr(record, f):
                setattr(record, f, "[REDACTED]")
        return True


class AdjudicationPayload(BaseModel):
    # Field-keyed, already schema-validated upstream. NCPDP refs in comments.
    cardholder_id: str = Field(min_length=2, max_length=20)   # 302-C2 Cardholder ID
    patient_first_name: str = Field(default="", max_length=35)  # 310-CA Patient First Name
    patient_dob: date                                          # 304-C4 Date of Birth
    ndc: str = Field(pattern=r"^\d{11}$")                      # 407-D7 Product/Service ID (NDC-11)
    date_of_service: date                                     # 401-D1 Date of Service
    ingredient_cost: Decimal = Field(gt=0)                    # 409-D9 Ingredient Cost Submitted
    plan_id: str = Field(min_length=5)                        # 524-FO / plan routing key

    @field_validator("ingredient_cost", mode="before")
    @classmethod
    def coerce_decimal(cls, v: Any) -> Decimal:
        # Never accept float for money; quantize at the source.
        return Decimal(str(v)).quantize(Decimal("0.01"))


@dataclass(frozen=True)
class RoutedClaim:
    """The internal-zone object: raw 302-C2 / 310-CA are gone by construction."""
    tenant: str
    cardholder_token: str   # surrogate for 302-C2
    ndc: str                # 407-D7, non-PHI, safe to log
    date_of_service: date   # 401-D1
    ingredient_cost: Decimal
    audit_hash: str


class IngressBoundary:
    """Single choke point where a claim crosses from edge to internal trust zone."""

    def __init__(self, tokenizer, tenant_map: dict[str, str]) -> None:
        self._tokenizer = tokenizer          # KMS/HSM-backed; app holds no key material
        self._tenant_map = tenant_map        # versioned plan_id-prefix -> tenant
        self._log = logging.getLogger("adjudication.boundary")
        self._log.addFilter(PHISafeFilter())

    def cross(self, raw: dict[str, Any]) -> RoutedClaim:
        # 1. Contract enforcement — reject before any PHI is handled further.
        payload = AdjudicationPayload.model_validate(raw)

        # 2. Tenant scoping from the plan routing key (524-FO prefix).
        tenant = self._tenant_map[payload.plan_id.split("-")[0]]
        tenant_ctx.set(tenant)

        # 3. Tokenize 302-C2 via KMS; derive an audit hash over the routing key set.
        token = self._tokenizer.tokenize(payload.cardholder_id)          # 302-C2 -> surrogate
        audit_hash = self._tokenizer.hash(
            f"{token}|{payload.ndc}|{payload.date_of_service.isoformat()}"
        )

        # 4. Build the internal object. Raw 302-C2 and 310-CA are dropped here
        #    (never copied into RoutedClaim) — the moment routing is derived.
        routed = RoutedClaim(
            tenant=tenant,
            cardholder_token=token,
            ndc=payload.ndc,                 # 407-D7, safe to log
            date_of_service=payload.date_of_service,
            ingredient_cost=payload.ingredient_cost,
            audit_hash=audit_hash,
        )

        # 5. PHI-safe telemetry: token + hash + NDC only. No raw member identity.
        self._log.info(
            "claim crossed ingress boundary",
            extra={"tenant": tenant, "ndc": routed.ndc, "audit_hash": audit_hash},
        )
        return routed

The gate rejects a malformed contract before touching PHI, scopes the request to exactly one tenant, and constructs an immutable internal object into which the raw 302-C2 and 310-CA values are simply never copied. Because RoutedClaim is frozen=True and omits those fields, no later code can reintroduce member identity into the internal path. The financial field is a quantized Decimal, so copay and cost math downstream in Tier Mapping & Copay Calculation Logic inherits exact arithmetic rather than float drift.

PHI data flow across DMZ and internal security zones A left-to-right flow. An inbound claim arrives over TLS 1.3 into the DMZ/Edge zone, where it is decrypted and validated, then tokenized and PHI-masked. It then crosses into the internal zone to the adjudication engine, which fans out to an encrypted store (AES-256 at rest) and an immutable, PHI-safe audit log. A separate dashed edge shows the decrypt-and-validate step emitting an ingress-audit event straight to the audit log, so the crossing is recorded even before tokenization. DMZ / Edge — untrusted perimeter Internal Zone — trusted TLS 1.3 ingress audit Inbound claimover TLS 1.3 Decrypt & validateschema gate Tokenize & mask302-C2 → surrogate Adjudication enginetoken-only payload Encrypted storeAES-256 at rest Immutable audit logappend-only · PHI-safe

Figure: PHI data flow across security zones, from TLS 1.3 ingress through DMZ decrypt, validate, and PHI tokenization into AES-256 internal adjudication and an immutable, PHI-safe audit log. The dashed edge records the crossing at the moment of decrypt, before any PHI is tokenized.

Engineering Constraints and Known Failure Modes

The boundary pattern is deceptively simple; the failure modes are where compliance is actually won or lost.

  • PHI leaking through exception messages. A naive except ValidationError as e: log.error(e) will serialize the offending field values — including 302-C2 — into the log. Validation errors must be logged by field name and reject code only, never by value. Catch, classify, and emit a hash; discard the exception payload.
  • Tenant-map misses. If plan_id.split("-")[0] has no entry in the versioned tenant map, the claim cannot be scoped and must be quarantined, not defaulted. A silent default is a cross-tenant PHI contamination event. Route the miss to a dead-letter path and alert; do not process under an assumed tenant.
  • Token reuse across tenants. A surrogate for 302-C2 must be scoped to its tenant. A globally shared token space lets one payer’s audit trail correlate to another’s member, which is itself a disclosure. Tokenization requests carry the tenant context.
  • Batch cross-contamination. When claims are grouped for throughput by Asynchronous Batch Adjudication Workflows, a batch must be homogeneous in tenant. A mixed batch that shares a cache, a log context, or a connection pool can bleed PHI across the boundary. Partition batches by tenant before the boundary, not after.
  • Cache as an accidental PHI store. An in-memory or Redis cache keyed on 302-C2 re-introduces raw member identity outside the encrypted store. Cache on the surrogate token or the audit hash exclusively.
  • Retry replays. Idempotent retry after a transient failure (e.g. a KMS timeout) must reuse the original audit_hash so the audit trail shows one logical claim, not a duplicate. A fresh hash per attempt breaks forensic reconstruction and can double-count in reconciliation.

Reject and routing outcomes at this boundary map to stable categories: a contract failure is a permanent FATAL reject (surfaced upstream as the appropriate NCPDP 511-FB code by the validation tier), a KMS/tokenizer timeout is TRANSIENT and retryable with the preserved hash, and a tenant-map miss is QUARANTINE pending manual reconciliation.

Performance and Correctness Tuning

  • Tokenization latency. A per-claim synchronous KMS round trip will dominate adjudication latency at peak. Use a local envelope-key cache with a short TTL and a KMS-backed data key, so tokenization is a local AES-GCM operation rather than a network call, while key rotation still propagates within the TTL window.
  • Decimal discipline. Set a module-level decimal context (prec=12, ROUND_HALF_UP) and quantize 409-D9 and all derived copay figures to Decimal("0.01") at every boundary crossing. Mixing quantized and unquantized Decimal values silently changes reconciliation totals.
  • Idempotency keys. Derive the audit_hash from a stable field set (token | 407-D7 | 401-D1 | plan) so a replay produces the identical key. Store processed keys with a retention window matching the payer’s duplicate-claim SLA.
  • Structured, sampled telemetry. Emit one span per boundary crossing with attributes limited to tenant, NDC, and hash. Never attach PHI to span attributes or metric labels — cardinality controls and PHI controls point the same way here.
  • Immutable audit persistence. Write audit events append-only (e.g. an object-lock bucket or a WORM table) capturing schema version, processing node, timestamp, and the cryptographic hash of the transaction. Correct audit trails let a payer examination reconstruct exactly which reference and formulary versions adjudicated a claim without ever exposing raw clinical data. For key rotation and secure random generation, follow the Python secrets module documentation.

In This Section

← Up to PBM Architecture & Taxonomy Foundations