Security Guarantees
What OGuardAI guarantees, what it does not guarantee, and what remains the customer's responsibility
This document states explicitly what OGuardAI guarantees, what it does not guarantee, and what remains the customer's responsibility.
Canonical Terminology
These terms have precise meanings throughout OGuardAI. If you encounter them in APIs, configs, logs, or documentation, they mean exactly this:
| Term | Definition |
|---|---|
| Tokenized | A raw PII value has been replaced by a semantic token {{type:id:cap}} in the output text. The original value is stored in the encrypted session blob. Tokenization is deterministic: the same value always produces the same token within a session. |
| Restorable | A tokenized value CAN be restored to its original form using the session state blob. Restorability depends on: (1) the session blob being valid and non-expired, (2) the entity not being revoked, (3) the policy allowing restore for that entity type and output channel. A value that was tokenized is restorable until its session expires or the entity is revoked. |
| Revoked | A value has been permanently marked as non-restorable. During rehydrate, revoked entities return [DELETED] instead of the original value. Revocation is persistent (survives server restart) and uses HMAC-SHA-256 hashing -- no raw PII is stored in the revocation table. Revoking a person cascades to all linked entities (email, phone, address) via belongs_to relationships. |
| Deleted | In OGuardAI's context, deletion means the original value is no longer recoverable by any means. This is achieved by: (1) revoking the entity, (2) allowing the session blob to expire, or (3) dropping the session secret. OGuardAI does NOT delete data in external systems (vector stores, databases, log sinks) -- that is the integrator's responsibility. |
| Blocked | The policy engine has determined that this entity type is not allowed in the given context. Blocked entities are removed from the output entirely (not tokenized, not passed through). The entities_blocked counter tracks this. In the output guard context, "blocked" means the entire response is rejected because new high-sensitivity PII was detected. |
| Masked | A value is shown in an obscured form. As a restore mode (masked), the first and last characters are kept and the middle is replaced with * (for example 4*********1); the raw value stays in the sealed session, so a channel whose policy resolves to a more permissive mode can still restore it. In the output guard, masking replaces newly generated PII with a type label like [EMAIL] or [SSN]. The masked text itself is one-way: the original cannot be recovered from it, only from the session. |
| Redacted | NOT the same as masked. Redaction removes a value non-restorably: the redact policy action replaces it with [REDACTED:type] at transform time and purges it from the session, so no channel and no restore mode can ever bring it back. The none restore mode similarly returns [REDACTED] in place of the value at rehydrate. Masked, by contrast, is a per-channel view: the sealed session still holds the raw value. |
| Detected | An entity span has been identified by the detection engine (regex, NER, or both). Detection does not imply tokenization -- the policy engine decides what happens to each detected entity (tokenize, block, or pass through). |
| Passed through | The value is listed in the policy whitelist, so the raw value remains in the output unchanged. This is a deliberate policy decision, not a detection failure. |
| Session state | An AES-256-GCM encrypted blob containing the token map (token-to-original-value mappings), session metadata, and policy reference. The blob is opaque to clients and LLMs. It is the ONLY artifact that can restore tokenized values. |
| Trace ID | A UUID v4 identifier that correlates all operations in a request lifecycle: transform, proxy pass, tool calls, rehydrate, output guard, and revocation. Client-supplied or auto-generated. Use the same trace_id across transform and rehydrate to enable full incident reconstruction. |
What OGuardAI GUARANTEES
Data Protection
- Detected, non-whitelisted PII values are never left raw in
safe_text; they are tokenized or removed (verified by 2,600+ Rust unit and integration tests, plus the Python, TypeScript, and Java SDK suites). This is scoped to what detection finds: a value the detector misses (see Detection Completeness below) and a value a policy deliberately whitelists (Passed through) can still appear, so treat this as a data-minimization control over detected entities, not an absolute guarantee - Tokenized text uses ONLY canonical semantic tokens of the form
{{type:id:cap}}, carrying a per-token capability cap on the wire ({{type:id:cap}}, see Token Protocol); it never contains raw values or ad-hoc placeholders - Sealed session blobs are encrypted with AES-256-GCM (authenticated encryption)
- Tampered session blobs are always rejected (cryptographic verification)
- Expired sessions produce clean errors, never data leaks
Detection
- Builtin mode (regex): the named types plus the built-in custom detectors, all detected deterministically, same input = same output
- NER mode (GLiNER): adds person/company/location plus policy-defined zero-shot labels, detection quality depends on model
- Both modes: detection is applied to ALL string content in the configured scan scope
Restore
fullrestore is byte-for-byte identical to the original value- Each of the 6 restore modes produces predictable, documented output
- Channel-specific restore rules are applied deterministically per policy
- Caller-gated restore overrides (
caller_role/caller_purpose) are re-authorized at rehydrate against the authenticated rehydrate caller, never the transform caller or a request-supplied field. A caller lacking the role degrades to the policy floor and a restrictive override still tightens, so asession_stateis not a bearer token for elevated restoration - Pre-restore raw-leak guard: before returning restored output, OGuardAI re-scans the resolved text with the
{{type:id:cap}}token spans stripped out. If a session's original value for a high-value structured type (email,phone,ssn,iban,credit_card,passport,health_id,date_of_birth) reappears as raw text, meaning the model echoed the value instead of its token and bypassed controlled restoration, rehydrate hard-blocks withGUARDAI_POLICY_DENIED. These types are detected deterministically, so a raw re-appearance is a genuine leak, not a common-word coincidence. NER-typed values (person,company,location) that reappear are logged as a warning and not blocked, because a common word the model over-tagged can legitimately recur in prose; the post-rehydrate output guard remains the backstop for those. A revoked value is the exception: its raw reappearance is blocked regardless of type, because a revoked value is deleted and must never reappear, so the warn-only path above applies only to non-revoked full-restore NER values
Internationalized Restoration
Restored values are written back into surrounding text the LLM produced. Mixed-script and partial restoration can introduce two classes of problem: a restored value reordering or spoofing the text around it, and a partial reveal that exposes the wrong part of a name. OGuardAI applies the following fail-closed safety behaviors. They reduce risk, they do not promise that every locale is rendered perfectly.
Bidi isolation of restored values (Trojan-Source defence). When a restored value contains right-to-left script or explicit bidirectional control characters, the rehydrate engine wraps it in a Unicode isolate, U+2068 (First Strong Isolate) before the value and U+2069 (Pop Directional Isolate) after it, and strips any inner bidi-control characters so the value cannot terminate or escape its own isolate. The result: a restored value cannot reorder, or be reordered by, the text around it. Left-to-right only values, including Latin, CJK, and email addresses, are returned unchanged. This is a defence against bidi / Trojan-Source style spoofing, not a claim that all directional rendering is correct in every renderer.
Family-first name masking in partial restore. Partial restore of a person name normally reveals an initial plus surname, for example J. Schneider. That ordering assumes a Western given-then-family layout. For CJK script and for the family-first languages zh, ja, ko, vi, and hu, that abbreviation would reveal a misidentified name part, so partial restore masks the name instead of producing a wrong F. Surname. Given-first scripts, including Arabic and Hebrew, keep the initial-plus-surname reveal. This applies only when the channel-resolved mode is Partial, which is gated by the channel ceiling.
30-language abstract-label floor with safe fallback. The built-in restore floor ships abstract labels (person, company, address, location, and related types) for all 30 supported languages, as data in an embedded floor file rather than hardcoded control flow. When the value's language is unknown or has no entry, lookup falls back to the English label, and if even that is missing it returns a fixed redacted label. No real value is interpolated into an abstract label. Any language can be added or an existing one tightened through the restore_templates config overlay (or GUARDAI_RESTORE_TEMPLATES). The overlay may only add or override entries on top of the floor: a malformed overlay, or one whose abstract label carries a {name} placeholder, aborts startup, fail closed.
Revocation
- Revoked entity values ALWAYS return
[DELETED]during rehydrate - Cascade revocation: revoking a person suppresses ALL linked entities (email, phone, address)
- Revocation is persistent (file backend, or a shared Redis backend via
revocation_backend: redis) - Revocation uses HMAC-SHA-256: no raw PII stored in the revocation table
Revocation Contract
The following are the canonical, binding guarantees for OGuardAI's revocation system:
- Revocation affects FUTURE rehydrate calls only: outputs already delivered to end users or downstream systems cannot be clawed back.
- Sealed session blobs remain decryptable after revocation, but any revoked value resolves to
[DELETED]instead of the original. - Revoking a person entity cascades to every entity linked via
belongs_torelationships (email, phone, address): all linked entities also resolve to[DELETED]. - Multiple entities can be revoked in a single API call (bulk revoke).
- Revocation state survives server restart when using the file backend. Cross-instance sharing uses the Redis backend (
revocation_backend: redis). - Vector stores, external databases, and application caches must still delete their own copies of data. OGuardAI cannot reach into external systems.
- Revocation is irreversible: there is no "un-revoke" operation.
- The revocation table stores only HMAC-SHA-256 hashes of entity values: no raw PII is ever stored in the revocation table itself.
Session Security
- Cross-tenant session access is always rejected
- Removing a session key from the keyring instantly invalidates every blob sealed under it; a graceful rotation that keeps the old key in the ring lets in-flight sessions stay valid until their TTL
- Session TTL is enforced: expired blobs cannot be unsealed
Policy
- Policy rules are evaluated deterministically for every entity
- Policy inheritance resolves child > parent > default
- Policy integrity can be verified via HMAC signatures
What OGuardAI DOES NOT Guarantee
Detection Completeness
- No detection system catches 100% of PII in all contexts
- Person/company/location detection REQUIRES the Python NER sidecar
- In builtin-only mode, person names, company names, and locations are NOT detected
- OCR text extraction is best-effort: noisy scans may produce detection gaps
- Custom or domain-specific entity types beyond the built-in types (regex named formats and built-in custom detectors, plus Person, Company, and Location via the NER sidecar) are not detected
External System Deletion
- OGuardAI does NOT control vector stores, external databases, or log sinks
- RAG chunk deletion in vector stores is the application's responsibility
- Log retention and purging is managed by the logging infrastructure
- OGuardAI provides guidance and signals, but cannot enforce external cleanup
Provider Behavior
- LLM output quality depends on the provider (OpenAI, Anthropic, etc.)
- Token damage patterns vary by provider and model version
- Token repair is best-effort with 3-stage pipeline (strict -> repair -> fuzzy)
- Hallucinated tokens are flagged as unresolved, never fabricated
Performance Under Load
- Latency depends on detector mode, payload size, and NER sidecar availability
- NER mode adds roughly 80ms p50 and up to several hundred ms p99 per request (indicative, depending on hardware, model, and text length)
- If NER sidecar is configured but unavailable, each request adds up to the configured NER timeout (
detector.timeout_secs) - Rate limiting is per-instance, not shared across instances
Distributed Consistency
- Sealed sessions are stateless: work across any number of instances
- Revocation with the file backend is per-instance; the Redis backend (
revocation_backend: redis) shares it across instances - Rate limiting is per-instance (use API gateway for global limits)
- Metrics are per-instance (use Prometheus for aggregation)
Customer Responsibilities
| Responsibility | Why |
|---|---|
| Start NER sidecar if person/company/location needed | NER is optional |
| Delete vector store chunks after RAG delete | OGuardAI doesn't own vector stores |
| Rotate session keys on compromise | Key management is ops responsibility |
| Configure appropriate policy for use case | Policy selection affects protection level |
| Monitor health endpoint | Detect degraded mode early |
Set auth.mode to non-dev for production | Dev mode bypasses auth |
Set session.secret to a real secret | Default secret is warned about |
| Configure log retention | OGuardAI emits audit events, retention is infra |
Failure Modes
| Failure | OGuardAI Behavior |
|---|---|
| NER sidecar down | Falls back to builtin regex (person/company/location missed) |
| Invalid session blob | Clean error returned, no data leaked |
| Malformed LLM output | 3-stage token repair attempted, unresolved tokens flagged |
| Policy not found | Default policy applied |
| Output guard catches new PII | Masked or blocked per config |
| LLM echoes a raw high-value session value at rehydrate | Restoration hard-blocked with GUARDAI_POLICY_DENIED (pre-restore raw-leak guard) |
| LLM echoes a raw NER-typed session value at rehydrate | Logged as a warning, not blocked (may be a common word); output guard remains the backstop |
| Rate limit exceeded | HTTP 429 with Retry-After header |
| File too large | HTTP 400 rejection |
| Revoked entity in rehydrate | Returns [DELETED] |
What Depends on NER Mode
Not all features work identically in builtin-only vs NER mode. This matrix clarifies:
| Capability | Builtin (regex) | NER (GLiNER) | Notes |
|---|---|---|---|
| Email detection | Yes | Yes | Regex in both modes |
| Phone detection | Yes | Yes | Regex in both modes |
| SSN / IBAN / CC | Yes | Yes | Regex in both modes |
| IP / URL / DOB | Yes | Yes | Regex in both modes |
| Order / Ticket | Yes | Yes | Regex in both modes |
| Person name | No | Yes | Requires NER sidecar |
| Company name | No | Yes | Requires NER sidecar |
| Location | No | Yes | Requires NER sidecar |
| Address (structured) | Partial | Yes | Regex detects 7 country formats; NER detects any language |
| Entity linking | Limited | Full | Person-to-entity links need person detection (NER) |
| Cascade revocation | Limited | Full | Cascade from person to linked entities needs NER for person detection |
| Detection latency | ~1-2ms | ~80ms p50, up to several hundred ms p99 | Indicative, hardware/model/length-dependent; NER adds model inference time |
| Determinism | 100% | Model-dependent | Same input may produce slightly different NER confidence scores across model versions |
Bottom line: Builtin mode is fast and deterministic but misses person/company/location. NER mode catches more entity types but adds latency and model dependency.
What Depends on Policy
| Feature | Default Policy | Strict PII Policy | Enterprise Policy |
|---|---|---|---|
| Tokenize email | Yes | Yes | Yes |
| Tokenize phone | Yes | Yes | Yes |
| Block SSN | No | Yes | Per-channel |
| Block IBAN | No | Yes | Per-channel |
| Pass through URL | Yes | No | Per-channel |
| Restore mode | Full | Masked | Per-channel |
| Output guard action | Mask | Block | Per-entity-type |
| Cascade revocation | Available | Available | Available |
| Shadow mode | Config-level | Config-level | Config-level |
Bottom line: Policy controls what happens to detected entities. Detection is independent of policy: it runs first and finds everything. Policy then decides: tokenize, block, or pass through.
Lifecycle of a Protected Value
At each stage, the value can only move forward (detected -> tokenized -> restored). It cannot be "un-tokenized" without the session blob, and it cannot be "un-revoked" once revoked.