OGuardAI
Security

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

BackendData LocationRetentionDeletion
Sealed (default)Client-held encrypted blobExpires after TTL (default: 1 hour)Automatic -- blob becomes invalid after TTL
MemoryServer process memoryUntil restart or TTLAutomatic on read (TTL check)
RedisRedis server (AES-256-GCM at rest)TTL-based expiry (SET EX) + explicit DELAutomatic by Redis

Process memory: the server and proxy deny core dumps in process (they zero RLIMIT_CORE and, on Linux, clear PR_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 their execve, 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 piped core_pattern (which ignores RLIMIT_CORE at 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

StagePII LocationControlled ByDeletion Mechanism
HTTP request in flightServer memory (ephemeral)OGuardAI runtimeFreed after response returns
Transform processingServer memory (ephemeral)OGuardAI runtimeFreed after response returns
Sealed session blobClient applicationClientClient discards blob; TTL auto-expires
Memory sessionServer RAMServer processAuto-evicted on TTL check or process restart
Redis sessionRedis server (AES-256-GCM at rest)Redis TTLAutomatic TTL expiry or explicit DEL command
RAG tokenized chunksExternal vector storeApplicationApplication deletes from vector DB
Audit eventsLog sink (stdout/file/SIEM)Log infrastructureLog rotation/retention policy
Idempotency cacheIn-process map (memory, the default) or Redis (guardai:idem: keyspace)OGuardAI runtimeTTL expiry (idempotency.ttl_seconds, default 86400)
OCR temp files/tmp directoryOGuardAI runtime + OSInput 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 returns 409 GUARDAI_IDEMPOTENCY_RESULT_UNAVAILABLE rather than the image. It is not encrypted at rest. The rehydrate routes are mounted outside the idempotency layer, so a keyed /v1/rehydrate call is never deduplicated and never leaves its restored output in this cache. Keep idempotency.ttl_seconds short 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 .tsv output, 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 .tsv back. 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).

  1. 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.
  2. If TTL has NOT passed: Replace the session key material in oguardai.yaml: change session.secret (single-key default) or remove the prior key(s) from session.keyring. Blobs whose kid no 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.
  3. 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.

  1. Delete by session ID: DEL guardai:sess:<session_id>
  2. Delete all sessions: redis-cli --scan --pattern "guardai:sess:*" | xargs redis-cli DEL (use SCAN, not KEYS, in production)
  3. Rotate the key ring, exactly as for sealed: a session whose kid no longer maps to a key becomes undecryptable instantly.
  4. Wait for TTL: sessions auto-expire (the Redis TTL and the encrypted expires_at both 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_ROLLBACK rather than restoring the stale state. Still run Redis with restricted access and maxmemory-policy noeviction as 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 scoped guardai ACL user is the only credential, noeviction is set, and Redis is isolated on its own network.

RAG Data

  1. Call POST /v1/rag/delete with the ingest session_state
  2. 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
  3. Application MUST delete corresponding chunks from its vector store
  4. 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:

  1. Call revocation_table.revoke("email", "erika.mustermann@example.com")
  2. HMAC-SHA-256 digest stored (32 bytes, no raw PII)
  3. During rehydrate_with_revocation(), the token's value is checked against the table
  4. If revoked: resolves to [DELETED] instead of original value
  5. If not revoked: normal restore behavior

Complete "Forget User" Procedure

What OGuardAI Controls

Data LocationOGuardAI Controls?Deletion Mechanism
In-flight request memoryYesFreed after response
Sealed session blobNo (client-held)TTL expiry, or removing its sealing key from the ring
Memory session backendYesbackend.remove() or restart
Redis session backendYesTTL or explicit DELETE
Revocation tableYesPOST /v1/revoke -- prevents future restore
RAG sessionYesPOST /v1/rag/delete
Idempotency cacheYesTTL 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 metricsNo (counters only)No PII in metrics
Audit log eventsNo (tracing sink)Log rotation policy
Vector store chunksNo (application-owned)Application deletes from vector DB

Step-by-Step: Forget User "erika.mustermann@example.com"

  1. Revoke the entity (prevents future restoration; requires the revoke scope, and global_revoke too when the key has no tenant binding, which admin implies):

    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"}'
  2. 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 to POST /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"}
      ]}'
  3. 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>"}'
  4. Application: delete vector store chunks containing this user's data

  5. For immediate invalidation of ALL sealed sessions: Replace the key material so no retained kid can decrypt. Single-key default shown; with a session.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 file backend 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::audit tracing event fingerprints identifiers and every free-form audit label.
  • Notification webhooks and oguardai-server audit export use 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
# 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=info

Data Flow Summary

Compliance Recommendations

  1. 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-Key leaves its verbatim JSON response body in the idempotency cache for idempotency.ttl_seconds (a binary body such as a redacted image is never retained, only a small result-unavailable marker).
  2. Configure TTL appropriately: shorter TTLs reduce exposure window.
  3. Rotate encryption keys periodically (the session.secret config value).
  4. Ship audit logs to a dedicated SIEM system for long-term retention and analysis.
  5. Enable TLS for all network communication (server, Redis, LLM provider).