Data Retention & Privacy
Complete data lifecycle, deletion procedures, and retention policies for OGuardAI deployments
Data Residency
OGuardAI is self-hosted. All detection, tokenization, sealing, and rehydration happen within your infrastructure, and the runtime introduces no subprocessor of its own.
Which calls leave your network depends on what you configure. The configured LLM provider receives tokenized text (plus any value a policy whitelists, and, if the proxy's remote-image-URL opt-in is enabled, a remote image URL whose pixels were never scanned). Beyond that, five optional features reach outside the process when they are turned on: an OIDC JWKS fetch, an HTTP detector webhook (raw text, so keep it internal), a notification webhook, OTLP trace export, and the Python NER sidecar (raw text, internal network). None is required for the protect and restore path, and none is enabled by default. The full enumeration is in the Security Whitepaper.
Session Data Lifecycle
| Backend | Data Location | Retention | Deletion |
|---|---|---|---|
| Sealed (default) | Client-held encrypted blob | Expires after TTL (default: 1 hour) | Automatic -- blob becomes invalid after TTL |
| Memory | Server process memory | Until restart or TTL | Automatic on read (TTL check) |
| Redis | Redis server (AES-256-GCM at rest) | TTL-based expiry (SET EX) + explicit DEL | Automatic by Redis |
Process memory: the server and proxy deny core dumps in process (they zero
RLIMIT_COREand, on Linux, clearPR_SET_DUMPABLE), so a crash of those processes does not write a core file spilling an in-memory session or in-flight PII. The OCR (Tesseract) and PDF extraction children apply the same zero core limit in their own fork-side setup, so it survives theirexecve, and on Linux they add no-new-privileges and a Landlock filesystem allow-list that keeps them off everything but their own binary, the loader trees, and their scratch directory. This does not cover a brief window before the parent's denial runs, a host with a pipedcore_pattern(which ignoresRLIMIT_COREat the kernel level and can still dump a child while it holds raw content), swap or hibernation, or the derived key roots; see Process memory posture for the full scope.
Complete Data Lifecycle
Where PII Exists at Each Stage
| Stage | PII Location | Controlled By | Deletion Mechanism |
|---|---|---|---|
| HTTP request in flight | Server memory (ephemeral) | OGuardAI runtime | Freed after response returns |
| Transform processing | Server memory (ephemeral) | OGuardAI runtime | Freed after response returns |
| Sealed session blob | Client application | Client | Client discards blob; TTL auto-expires |
| Memory session | Server RAM | Server process | Auto-evicted on TTL check or process restart |
| Redis session | Redis server (AES-256-GCM at rest) | Redis TTL | Automatic TTL expiry or explicit DEL command |
| RAG tokenized chunks | External vector store | Application | Application deletes from vector DB |
| Audit events | Log sink (stdout/file/SIEM) | Log infrastructure | Log rotation/retention policy |
| Idempotency cache | In-process map (memory, the default) or Redis (guardai:idem: keyspace) | OGuardAI runtime | TTL expiry (idempotency.ttl_seconds, default 86400) |
| OCR temp files | /tmp directory | OGuardAI runtime + OS | Input image: RAII, removed on drop. Tesseract's .tsv text output: explicit delete, best-effort (see note below) |
Note: the idempotency cache stores the verbatim JSON response body of any POST or DELETE that carried an
Idempotency-Key, so a client can safely retry without re-executing the mutation; a binary body (the redacted image from/v1/redact/image) is never retained, only a small result-unavailable marker, so a same-key retry of that route returns409 GUARDAI_IDEMPOTENCY_RESULT_UNAVAILABLErather than the image. It is not encrypted at rest. The rehydrate routes are mounted outside the idempotency layer, so a keyed/v1/rehydratecall is never deduplicated and never leaves its restored output in this cache. Keepidempotency.ttl_secondsshort and restrict access to the Redis instance.
Note: OCR is the one path where raw content touches local disk. The input image tempfile is RAII-managed and removed when it drops. Tesseract's
.tsvoutput, which holds the extracted text, is deleted by an explicit call instead, and that call is not unconditional: the timeout, non-zero exit, and oversize-output rejections each delete it before returning, and the success path reads it and then deletes it, logging and ignoring a failed unlink. Two branches return without deleting it, a failure while polling the Tesseract subprocess and a failure reading the.tsvback. Treat the OCR temp directory as needing periodic cleanup rather than assuming every run removes its own file.
"Forget User X": Complete Procedure
Sealed Sessions (Default, Stateless)
The server keeps no token map and no session record after the HTTP response; the client is the sole
custodian of the blob. This is scoped to session storage, not to every byte of bookkeeping: the
replay backend keeps a per-session-id high-water counter (non-PII), the audit backend keeps its
events (type labels and counts, never detected values, handled as regulated data), and the idempotency
cache keeps a JSON response body when the request carried an Idempotency-Key, never a binary body
such as a redacted image (see the note above).
- If TTL has passed: The runtime refuses the blob. Expiry is enforced by a timestamp check that runs before decryption, not by cryptographic invalidation: the sealing key is still in the ring, so combine expiry with key retirement when durable unrecoverability is the goal.
- If TTL has NOT passed: Replace the session key material in
oguardai.yaml: changesession.secret(single-key default) or remove the prior key(s) fromsession.keyring. Blobs whosekidno longer maps to a key become undecryptable instantly. A rotation that keeps the old key in the ring does not erase them; they stay valid until TTL. - Per-tenant purge: Tenant-scoped sessions use a per-tenant key HKDF-derived from the shared root ring, so removing a root key invalidates that key's blobs across all tenants at once. For an isolated per-tenant purge, give that tenant its own key material (BYOK) and remove its key alone.
Redis Sessions (Encrypted at Rest)
Each session is sealed with the session key ring (AES-256-GCM) before it is written to Redis, so Redis holds only ciphertext plus non-PII routing metadata (tenant, policy, and session ids, timestamps, language), never the raw token map.
- Delete by session ID:
DEL guardai:sess:<session_id> - Delete all sessions:
redis-cli --scan --pattern "guardai:sess:*" | xargs redis-cli DEL(use SCAN, not KEYS, in production) - Rotate the key ring, exactly as for sealed: a session whose
kidno longer maps to a key becomes undecryptable instantly. - Wait for TTL: sessions auto-expire (the Redis TTL and the encrypted
expires_atboth enforce it).
Notes: tenant, policy, and session ids and timestamps are stored in the clear as routing metadata (they are AAD fields, the same exposure as the client-held sealed blob and the durable audit log, which records type labels and counts, never detected values, and is handled as regulated data); operators who treat those identifiers as sensitive should use opaque ids. A within-TTL rollback is refused at the boundary: each session id carries a monotonic high-water marker (the highest request counter ever stored) alongside its encrypted blob, so a write that presents a counter below the mark is rejected without overwriting the blob, and a load whose blob counter is below the mark (or whose marker is missing) fails closed with
GUARDAI_SESSION_ROLLBACKrather than restoring the stale state. Still run Redis with restricted access andmaxmemory-policy noevictionas defense in depth: evicting a marker makes its session fail closed rather than leak, and restricted access keeps an attacker off the store entirely. The bundled Docker Compose stacks ship this by default: the default user is disabled, a scopedguardaiACL user is the only credential,noevictionis set, and Redis is isolated on its own network.
RAG Data
- Call
POST /v1/rag/deletewith the ingest session_state - OGuardAI unseals that session and revokes every
(entity_type, value)pair in its token map, so those values resolve to[DELETED]on every future rehydrate, not only in this session. The response reports the entity count and a deletion manifest. It does not reach values that were never in this session - Application MUST delete corresponding chunks from its vector store
- Tokenized text written to the vector store carries no raw value for any entity the detector found and the policy did not whitelist
Emergency Purge
Replace the session key material and deploy: with the single-key default, change session.secret; with a session.keyring, remove every prior key (or replace all of them) so no retained kid can still decrypt. Every existing sealed blob then fails to decrypt instantly, with no data migration.
Compliance Certification
OGuardAI's sealed session architecture provides data minimization by design:
- Server retains no token map and no session record after the HTTP response (sealed mode); the replay counter carries no PII, and the audit events it does keep record type labels and counts, never detected values (handled as regulated data), and the idempotency cache retains a JSON response body only when the caller sent an
Idempotency-Key, never a binary body such as a redacted image- PII is encrypted at rest within client-held session blobs (AES-256-GCM)
- Sessions auto-expire after configurable TTL (default: 1 hour)
- Removing a key from the session keyring enables instant bulk invalidation of every blob sealed under it
- Tokenized text in external stores (vector DBs, logs) carries no raw value for any entity the detector found and the policy did not whitelist
- Audit events contain entity types and counts only, never raw values
Right to Be Forgotten (GDPR Art. 17)
OGuardAI's sealed session model supports data minimization:
- Server retains no token map and no session record after the response (sealed mode)
- Sessions auto-expire after configurable TTL
- No backend writes raw personal data to disk
Expiry alone is a runtime refusal, not erasure: the sealing key is still in the ring, so a blob whose TTL passed would restore again if the check were bypassed. GDPR is the authority here and names three mechanisms, session expiry, entity revocation, and key retirement, and states what each does and does not achieve. Use revocation for a specific value and key retirement (with every copy of the key destroyed) for durable unrecoverability.
For the Redis backend: call DEL guardai:sess:<session_id>, rotate the key ring, or wait for TTL expiry.
Entity Revocation (Restore-Time Suppression)
OGuardAI provides an entity revocation mechanism that prevents future rehydration of specific values.
What it IS:
- A restore-time suppression list
- Prevents revoked values from appearing in any future rehydrate response
- Stores only HMAC-SHA-256 digests (no raw PII in the revocation table)
- Type-scoped: revocation is per (entity_type, value) pair
- Matching follows the per-type identity form: free-text values (names, prose values) match case-insensitively via normalization, while identifier, opaque, and non-built-in custom values keep case and width and must be revoked as written (digests recorded by earlier releases keep the broader fold they were written with)
What it is NOT:
- Not a universal deletion of all historical data
- Not cryptographic erasure of sealed session blobs
- Not deletion from external systems (vector stores, logs)
- Existing sealed session blobs are unaffected (they just won't restore revoked values)
Use cases:
- GDPR Art. 17 "right to be forgotten": prevent future exposure
- Post-incident containment: suppress leaked values immediately
- User deletion requests: revoked values resolve to [DELETED]
- Emergency do-not-restore rules
How it works:
- Call
revocation_table.revoke("email", "erika.mustermann@example.com") - HMAC-SHA-256 digest stored (32 bytes, no raw PII)
- During
rehydrate_with_revocation(), the token's value is checked against the table - If revoked: resolves to
[DELETED]instead of original value - If not revoked: normal restore behavior
Complete "Forget User" Procedure
What OGuardAI Controls
| Data Location | OGuardAI Controls? | Deletion Mechanism |
|---|---|---|
| In-flight request memory | Yes | Freed after response |
| Sealed session blob | No (client-held) | TTL expiry, or removing its sealing key from the ring |
| Memory session backend | Yes | backend.remove() or restart |
| Redis session backend | Yes | TTL or explicit DELETE |
| Revocation table | Yes | POST /v1/revoke -- prevents future restore |
| RAG session | Yes | POST /v1/rag/delete |
| Idempotency cache | Yes | TTL expiry only (idempotency.ttl_seconds); there is no purge endpoint, and flushing the Redis namespace during a rolling deploy reopens the duplicate-execution window |
| Prometheus metrics | No (counters only) | No PII in metrics |
| Audit log events | No (tracing sink) | Log rotation policy |
| Vector store chunks | No (application-owned) | Application deletes from vector DB |
Step-by-Step: Forget User "erika.mustermann@example.com"
-
Revoke the entity (prevents future restoration; requires the
revokescope, andglobal_revoketoo when the key has no tenant binding, whichadminimplies):curl -X POST http://localhost:3000/v1/revoke \ -H "Content-Type: application/json" \ -H "X-API-Key: $GUARDAI_API_KEY" \ -d '{"entity_type":"email","value":"erika.mustermann@example.com"}' -
Revoke related entities (name, phone, etc.). List every value explicitly; do not rely on the person cascade. The cascade fires only for an email, phone, or address link explicitly marked exclusive ownership (
belongs_to_exclusive), and no runtime path currently marks a link exclusive: the marking is reserved for a future authoritative structured-ownership source, so in this release revoking a person does not cascade to any linked value. A link inferred from proximity, where a value is merely detected near a person in free text, is metadata only and never drives erasure, and company values never cascade. Every value is therefore retired by its own(entity_type, value)pair. List those pairs here or send them toPOST /v1/revoke/bulk(see Revocation Contract item 3):curl -X POST http://localhost:3000/v1/revoke/bulk \ -H "Content-Type: application/json" \ -H "X-API-Key: $GUARDAI_API_KEY" \ -d '{"entities":[ {"entity_type":"email","value":"erika.mustermann@example.com"}, {"entity_type":"person","value":"Erika Mustermann"}, {"entity_type":"phone","value":"+49 30 12345678"} ]}' -
Delete known RAG sessions (if applicable):
curl -X POST http://localhost:3000/v1/rag/delete \ -H "Content-Type: application/json" \ -H "X-API-Key: $GUARDAI_API_KEY" \ -d '{"session_state":"<ingest_session_blob>"}' -
Application: delete vector store chunks containing this user's data
-
For immediate invalidation of ALL sealed sessions: Replace the key material so no retained
kidcan decrypt. Single-key default shown; with asession.keyring, remove the prior keys instead.session: secret: "new-rotated-key-after-user-deletion"
What This Guarantees
- Future rehydration: Revoked values return
[DELETED] - Past sealed blobs: Invalid once their sealing key is removed or replaced
- Redis/memory sessions: Deleted or expired
- Vector store: Application responsibility (guided)
- Logs: Subject to log retention policy
What This Does NOT Guarantee
- External systems that received tokenized text still have the tokens
- Sealed blobs held by clients remain valid until TTL, or until their sealing key is removed from the ring
- Vector store cleanup depends on the application, not OGuardAI
What OGuardAI Stores
During a Request (Transient)
- Raw PII values live in the trusted zone's memory during active processing (the runtime, and the Python NER sidecar when deployed)
- After the response is sent, raw values are dropped (sealed mode) or held in the configured session backend. Dropping is not zeroization: freed pages may still hold the bytes until the allocator or the OS reuses them
- No session backend writes raw personal data to disk. OCR is the exception on the request path: it writes the raw image bytes and the extracted text to short-lived temporary files, so raw content touches local disk for the duration of that call (see the OCR note above)
In Session State (Sealed Mode)
- An AES-256-GCM encrypted blob containing the token-to-value mapping
- The blob is returned to the client; the server retains nothing
- The blob is tamper-evident: the AES-GCM authentication tag is verified on every unseal, so a modified blob is rejected rather than partially decrypted. It expires after the configured TTL
- Without the server's encryption key, the blob is unreadable
In Logs (Tracing)
- Entity types and counts (e.g., "detected 3 email, 1 phone")
- Policy names applied
- Operation durations
- Tenant, session, and trace IDs as keyed HMAC-SHA-256 fingerprints, never the raw identifier
Logs NEVER contain:
- Raw PII values
- Email addresses, names, phone numbers
- Entity text content
- Session encryption keys
Audit Events
Every durable audit event contains only operational metadata such as:
- Timestamps
- Tenant IDs
- Entity types and counts
- Policy names and content hashes
- Operation durations
Audit events NEVER contain:
- Raw PII values
- Entity text
- Email addresses, names, phone numbers
- Session encryption keys
Destination handling differs:
- The durable
filebackend keeps tenant IDs and entity labels as the protected compliance record. Its HMAC chain and retention policy apply to that raw durable record. - The
guardai::audittracing event fingerprints identifiers and every free-form audit label. - Notification webhooks and
oguardai-server audit exportuse the external projection, which applies the same field-separated keyed fingerprints and reduces unknown entity labels before the copy leaves the host. The exact policy-content hash and closed legal basis remain readable.
The live file must be a regular, runtime-owned, single-link 0600 leaf in a stable, trusted parent.
Offline source archives must be owned regular files at mode 0400 or 0600. For verification,
create-new export files, and failed-output recovery, see
Compliance Controls.
Log Retention
Configure your tracing subscriber to:
- Rotate logs (e.g., daily)
- Purge logs after retention period
- Ship to SIEM for long-term storage
Recommended Configuration
# Audit events ride the `guardai::audit` target at INFO, below the default filter's warn
# floor. Add the target to the default filter rather than replacing it: a whole-filter
# replacement silences every other crate, including fail-closed reasons.
RUST_LOG=warn,oguardai_server=info,guardai_server=info,tower_http=info,guardai::audit=infoData Flow Summary
Compliance Recommendations
- Use sealed sessions in production (default). The server then keeps no token map and no session record after the response. Two caveats survive that: an OCR call writes raw content to a temporary file for the length of the call, and a request that carried an
Idempotency-Keyleaves its verbatim JSON response body in the idempotency cache foridempotency.ttl_seconds(a binary body such as a redacted image is never retained, only a small result-unavailable marker). - Configure TTL appropriately: shorter TTLs reduce exposure window.
- Rotate encryption keys periodically (the
session.secretconfig value). - Ship audit logs to a dedicated SIEM system for long-term retention and analysis.
- Enable TLS for all network communication (server, Redis, LLM provider).