OGuardAI
Security

Compliance Controls Mapping

Article-by-article mapping of OGuardAI controls to GDPR, HIPAA, SOC 2, and PCI DSS requirements

GDPR Alignment

GDPR ArticleRequirementOGuardAI ControlHow to Verify
Art. 5(1)(c)Data minimizationTokenization removes PII from LLM contextTransform API returns safe_text with detected, non-whitelisted PII tokenized or removed
Art. 5(1)(e)Storage limitationSealed sessions auto-expire, no server-side retentionCheck session TTL configuration
Art. 25Data protection by designTrust boundary model, sealed sessionsSecurity whitepaper Section 1
Art. 28Processor obligationsSelf-hosted, no subprocessors in sealed modeDeployment guide
Art. 30Records of processingStructured audit events per operationAudit module, diagnostics endpoint
Art. 32Security of processingAES-256-GCM encryption, key rotationSecurity whitepaper Section 2
Art. 17Right to erasureThree mechanisms: session expiry, entity revocation, key retirement. Expiry alone is a runtime refusal, not cryptographic destructionGDPR, Right to Erasure
Art. 20Data portabilitySealed sessions are portable (client-held)Session model
Art. 35Data protection impact assessmentAudit trail, policy validation, shadow (dry-run) mode for staging evaluationAdmin endpoints

Note on shadow mode: shadow (dry-run) mode returns a redacted comparison (a fingerprint hash plus aggregate deltas), not raw PII, and is a dev-only diagnostic rejected as a fatal production-validation error. It is a staging-only evaluation aid for previewing detection and tokenization behavior, not a production compliance control.

HIPAA Alignment

HIPAA SectionRequirementOGuardAI ControlImplementation Detail
SS 164.312(a)(1)Access controlJWT/API key auth + scope modelAuth middleware with per-endpoint scopes (Transform, Rehydrate, Detect, Policy, Batch, Rag, Revoke, Global Revoke, Session, Detect Values, Admin)
SS 164.312(a)(2)(iv)Encryption/decryptionAES-256-GCM sealed sessionsSession crate with AEAD encryption
SS 164.312(b)Audit controlsStructured audit eventsAudit module emits events via tracing
SS 164.312(c)(1)Integrity controlsAEAD authentication tag on session blobsTamper detection built into AES-GCM
SS 164.312(e)(1)Transmission securityTLS support, encrypted session stateDeployment guide covers TLS setup
SS 164.502(b)Minimum necessaryTokenization + policy-based restore modes6 restore modes: full, partial, masked, formatted, abstract, none
SS 164.308(a)(5)(ii)Security awarenessPolicy validation and dry-run endpointsAdmin can preview impact before changes

SOC 2 Type II Alignment

ControlDescriptionOGuardAI FeatureEvidence
CC6.1Logical accessAuth middleware with scopesAPI key/JWT validation on every request
CC6.3Role-based accessPer-endpoint scopes (Transform, Rehydrate, Detect, Policy, Batch, Rag, Revoke, Global Revoke, Session, Detect Values, Admin)Scope-based authorization checks
CC6.6System boundary protectionTrust boundary modelDetected, non-whitelisted PII tokenized before leaving the runtime boundary
CC6.7Restrict data mobilityPolicy engine controls entity exposurePer-entity-type action rules
CC7.1Detect security eventsPrompt security engine, output guardInput/output scanning for injection and PII leaks
CC7.2System monitoringStructured audit events, diagnostics endpointAudit trail, health checks
CC8.1Change managementPolicy diffing, validation endpointPreview impact of policy changes before deployment
CC9.1Risk mitigationMulti-layer detection (regex + NLP)Builtin + advanced detector modes

PCI DSS Alignment (Where Applicable)

PCI DSS RequirementOGuardAI Control
3.4 -- Render PAN unreadableCredit card tokenization with Luhn validation
3.5 -- Protect cryptographic keysSession key derived from configured secret, key rotation supported
10.1 -- Audit trailsStructured audit events per operation
10.3 -- Record audit trail entriesTimestamp, tenant, entity types, policy, duration in every event

Audit Event Schema

The audited operations are: text, JSON, and chat transform; rehydrate; the RAG ingest/query/context/answer/delete pipeline; detection (/v1/detect, /v1/batch/detect); revocation (/v1/revoke, /v1/revoke/bulk); and image redaction (/v1/redact/image). Each event carries type and count metadata, never a detected value.

Emission is per route rather than exactly one event per call, which matters when you reconcile an audit export against request volume. Batch transform emits one per-item Transform event and no separate batch event, so an empty batch transform emits none. Batch detect instead emits a single aggregate BatchDetect event, recorded even for an empty batch. RAG delete emits two events, a write-ahead intent record before the revocation table is mutated and an outcome record after the durable persist. Failure events are emitted only for the security-relevant error categories (policy denial, output-guard block, output-guard outage, token-repair failure, session expiry, session replay, session rollback, replay-store outage, detection failure, auth failure); plain input-validation and internal errors are deliberately not audited, and a request rejected by route validation before it reaches the audit step produces no audit event at all.

A structured event looks like:

{
  "timestamp_unix": 1700000000,
  "event_type": "transform",
  "tenant_id": "tenant_acme",
  "session_id": "9c1f4e2a7b0d6538",
  "policy_applied": "gdpr-strict",
  "policy_content_hash": "4f1c...e93a",
  "policy_version": "1.0.0",
  "entity_types": ["email", "phone"],
  "entity_count": 3,
  "entities_blocked": 1,
  "entities_tokenized": 2,
  "duration_ms": 12.5,
  "detector_mode": "both",
  "output_guard_triggered": false,
  "prompt_security_triggered": false
}

That is the serialized shape the file backend writes. policy_content_hash is a SHA-256 over the canonicalized resolved policy, so a record identifies which policy content ran rather than only a reusable name; policy_version is the version the policy file declares. Both are omitted where they do not apply, for example on a failure event built at the request boundary. The same event on the default log backend is a tracing event on the guardai::audit target whose field names differ slightly (policy, output_guard, prompt_security). session_id and trace_id are keyed HMAC-SHA-256 fingerprints in both, never the raw identifier.

Key property: no audit event carries a detected value.

Identifier handling then differs by destination, which is what decides where a record may be sent:

CopyTenant idFree-form audit labelsEntity labels
Durable file record on the hostVerbatimVerbatimVerbatim
guardai::audit log lineKeyed fingerprintField-separated keyed fingerprintsReduced to the built-in set
Notification webhook and audit exportKeyed fingerprintField-separated keyed fingerprintsReduced to the built-in set

The durable record is raw on purpose. It is the protected compliance record, and the per-tenant HMAC chain binds the tenant itself, so fingerprinting it there would break the integrity model. Treat audit.file.path as regulated data on access-controlled storage, not as a log stream. Everything else goes through one shared projection. Tenant id, policy name, policy version, purpose, and data category never leave the host verbatim. The exact policy_content_hash and closed legal basis remain readable. Tenant ids are operator-chosen labels; choose them so they are not themselves personal data, because the durable record stores them as given.

The event schema also reserves three optional GDPR Art. 30 record-of-processing fields: purpose (a processing-purpose label), legal_basis (an Art. 6(1) lawful basis from a closed set: consent, contract, legal_obligation, vital_interests, public_task, legitimate_interests), and data_category (a category-of-data label). Only legal_basis is constrained by the type system. The durable record writes purpose and data_category verbatim. Logs and outbound copies replace each with a deterministic keyed fingerprint in its own field domain, while the closed legal basis remains readable. Each field is omitted when unset, and current production constructors populate none of the three. The full Records of Processing, including retention and data-subject categories, remains organizational and lives in your RoPA.

Audit Export

Audit events are emitted as structured tracing events on the guardai::audit target. They can be collected by any log aggregation system:

  • ELK Stack: Filebeat -> Logstash -> Elasticsearch -> Kibana
  • Datadog: Datadog Agent with JSON log parsing
  • Splunk: Universal Forwarder with JSON sourcetype
  • AWS CloudWatch: CloudWatch Logs agent
  • Google Cloud Logging: Fluentd/Fluent Bit

Set GUARDAI_LOG_FORMAT=json to emit newline-delimited JSON for easy parsing. Audit events are emitted at INFO, below the default log filter's warn floor, so add the guardai::audit target to that filter. Add it, do not replace the filter with it: a whole-filter replacement silences every other crate, including the reasons a request was refused.

GUARDAI_LOG_FORMAT=json \
  RUST_LOG=warn,oguardai_server=info,guardai_server=info,tower_http=info,guardai::audit=info \
  oguardai-server

This applies to the default log audit backend. The durable file backend writes every event to audit.file.path regardless of the log filter.

Policy Governance Events

In addition to operational audit events, OGuardAI emits policy governance events:

Event TypeWhen EmittedWhat It Records
policy_loadedServer startupEach policy loaded from the policies directory
policy_validatedAdmin validation endpointProposed policy validation result

These events are emitted as structured log records (no detected values) and create a record of policy state changes over time. With the default log backend, durability and tamper-resistance depend on the operator's log sink. The optional file audit backend adds a local append-only, HMAC-chained store with one tamper-evident hash chain per tenant, so each tenant's records verify independently; route it to durable, access-controlled storage for retention.

To enable durable, fail-closed audit in one step, start the server with --compliance (or set GUARDAI_COMPLIANCE=1 for an env/Helm deployment that cannot pass a flag; it accepts 1, true, yes, or on). The preset only tightens: it forces the file audit backend and strict audit on the data path, so a transform, rehydrate, detect, RAG, session, or revoke operation whose audit event cannot be recorded returns HTTP 503 rather than acknowledging unaudited success. A pre-claim audit failure leaves the operation uncommitted or rolls it back. A post-claim or commit-indeterminate failure can leave it committed. In particular, a commit-indeterminate revocation retains the erasure because restoring the value could contradict a durable success record. Use the same Idempotency-Key when retrying a protected mutating route; otherwise reconcile domain state and the audit chain before retrying.

The file sink hands each event to a bounded in-process queue whose background writer persists and fsyncs it. Strict mode admits the synchronous durability wait through its own dedicated audit.strict_max_in_flight bound, independent of the limits.max_blocking_ops budget that OCR, parsing, and image redaction share, so a document-processing load spike cannot deny strict-audit capacity. Strict audit fails before submission when its admission bound is full. An admitted request waits for its own acknowledgement up to the strict timeout without parking an asynchronous scheduler worker. If the timeout expires after the writer claimed the event, the request fails closed as commit-indeterminate and readiness stays withdrawn until that event reaches a known terminal state. A full audit queue, exhausted strict-audit admission capacity, or a stopped writer therefore rejects the request with 503.

Startup also reserves one dedicated finalizer thread, independent of that shared request budget, and fails if it cannot be created. Shutdown closes audit admission, drains already admitted strict writes, and then finalizes the file writer. Secondary cleanup starts after the admission phase. All shutdown work shares one absolute deadline, and any failed, late, or incomplete phase makes the report non-clean. Commit uncertainty is event-specific: readiness stays withdrawn until that exact write resolves, and completion of another event cannot clear it.

The file path must be on durable storage. The default accepts any non-volatile filesystem: the certified set (ext4, XFS, Btrfs, and APFS on macOS) is accepted silently, while remote families (NFS and EFS, SMB and CIFS including Azure Files, 9p, Ceph) and unverified families (FUSE, overlayfs, and any unknown filesystem) are accepted with a startup warning that names the fsync durability and single-writer locking caveat. Memory-backed filesystems (tmpfs, ramfs, hugetlbfs) are always refused for the active log and exports because a reboot would erase the trail. Set audit.require_certified_filesystem: true (or GUARDAI_PROXY_AUDIT_REQUIRE_CERTIFIED_FS on the proxy) to restore the strict allowlist that refuses every remote or unverified family and any unsafe mount semantics. Container image layers are normally overlayfs, which now works with a warning, but mount a dedicated volume at the audit directory anyway because container-layer contents do not outlive the container. macOS custody rejects extended ACL entries. Linux rejects audit files and pinned directories that carry POSIX ACL extended attributes and strips them from files it creates, so a parent-directory default ACL is refused at open rather than inherited; exact mode 0600 is still required.

Fail-closed does not extend to every server event. Failure audits for requests that are already erroring, admin diagnostics, and startup events are recorded fail-open. A sink error is logged, not propagated. Those paths never carry raw detected values, so a dropped record loses operational evidence rather than exposing customer data. The audit file path stays operator-configured (audit.file.path); startup fails closed if it is unset, so a compliance deployment must point the durable log at a real, access-controlled path. The gate runs regardless of auth.mode, so it holds even in dev mode where the general config validation is skipped.

The transparent proxy has its own audit configuration. Both successful and failed proxy operations use GUARDAI_PROXY_AUDIT_BACKEND, which defaults to log; file also requires GUARDAI_PROXY_AUDIT_FILE. The file backend uses the same access-controlled, HMAC-chained writer and drains accepted records during a clean shutdown. Proxy recording is still buffered and fail-open on the request path. A full queue or stopped writer can omit a successful forwarded operation, so the proxy file is a verifiable chain for accepted records, not a per-request durability guarantee. The server --compliance preset does not make proxy audit strict.

# Durable, fail-closed audit; audit.file.path must be set in the config.
oguardai-server --compliance --config oguardai.yaml

# Same preset via env (Helm/container deployments).
GUARDAI_COMPLIANCE=1 oguardai-server --config oguardai.yaml

Verifying audit-log integrity

The file backend is tamper-evident; the oguardai-server audit verify command makes it operator-provable. It resolves the dedicated audit secret when configured, otherwise the session-secret fallback, and can select a previous root from audit.retired_secrets. The source header key id identifies which configured root wrote a current archive. For a legacy header without a key id, the command verifies configured candidates against the same opened source.

The command opens the source once. That one descriptor supplies the header key id, key selection, whole-file verification, and, for audit export, record collection. Replacing the pathname after it opens cannot switch the evidence being verified or exported. A source must be a regular file owned by the effective user at mode 0400 or 0600. Symlinks, non-regular files, broad permissions, and wrong ownership are rejected without changing the source. A verifier deliberately run as root may read another user's private archive; there is no other ownership exception.

# Verify the audit file named in the config with the configured audit roots.
oguardai-server audit verify --config oguardai.yaml

# Verify an archived or relocated copy on a forensic host.
oguardai-server audit verify --config oguardai.yaml --path /backups/audit-2026-06.jsonl

# Machine-readable result for monitoring; --tenant scopes the per-chain breakdown.
oguardai-server audit verify --config oguardai.yaml --json --tenant acme

Exit codes are distinct so monitoring can act on them: 0 intact, 2 broken (a definitive tamper or corruption verdict; any break in any tenant fails the whole file), 1 cannot verify (a missing, unreadable, or unsafe source, an unavailable writing key, or audit.backend is not file). A 1 is an operational or custody error; a 2 is an integrity incident.

The output carries no event content at all: chain identifiers, counts, sequence ranges, head hashes, timestamps, and a closed set of break reasons. The audit HMAC key is never printed, only a domain-separated 8-character fingerprint so two runs can be confirmed to use the same key.

Record MACs and chain links must be exact lowercase hexadecimal values. Verification decodes them to fixed-width bytes and compares the HMAC values in constant time. Uppercase, truncated, overlong, or non-hexadecimal encodings fail verification.

Verifying policy integrity

The policy directory itself can be signed and verified fail-closed at startup, so a modified, added, removed, or swapped policy file refuses to start the server. A policy can never be silently weakened on disk. Sign the directory with a dedicated key, then enable verification in the config.

# Sign the policy directory (writes <dir>/.signatures.json).
oguardai-server policy sign-dir --dir policies --secret "$GUARDAI_POLICY_SIGNING_KEY" --kid v1
policy:
  directory: policies
  integrity:
    manifest: policies/.signatures.json
    secret: "${GUARDAI_POLICY_SIGNING_KEY}"
    kid: v1

The signing key is HMAC-derived and independent of the session secret, so policy governance can hold a different key than the runtime. Verification covers exactly what the server loads (each <subdir>/policy.yaml and each root-level *.yaml), rejects symlinks, and reports the first failing file as modified, unexpected, or missing. Both manifest and secret must be set; setting exactly one is a startup error, and both absent disables it (the default). Re-sign after any intentional policy change.

Notes:

  • audit.secret is the preferred independent audit root. When it is unset, the root falls back to session.secret. Rotating a session keyring entry does not change either root. Rotating the base session secret does change the fallback audit root, so archive the current file first and keep the previous root in audit.retired_secrets.
  • Tail truncation (deleting the most recent records) leaves a valid prefix; publish each chain head_hash between audits as an external anchor to detect it.
  • For a portable evidence copy, audit export produces an integrity-attested NDJSON bundle (or a --tenant / --since / --until slice) to hand a DPA or auditor:
# The whole log, attested. Refuses (exit 2) if the chain is broken.
oguardai-server audit export --config oguardai.yaml --out evidence.ndjson

# A tenant + time slice; the header still carries every chain's head hash so a
# slice cannot be passed off as the whole log.
oguardai-server audit export --config oguardai.yaml --tenant acme --since 1717200000

The export verifies the whole source first and fails closed (a broken chain publishes no usable bundle, exit 2). The first line is an attestation over the whole file: record totals, each chain's sequence range and head hash, the audit key's fingerprint, source-wide timestamp coverage, and the filter this export applied. It does not disclose the source pathname. The body is the event records. The last line is a trailer with the exported count and a completion marker, so a truncated bundle is detectable.

With --out, the destination is create-new, exact mode 0600, and never overwritten. Success is reported only after the file and its parent directory are synced. A failure after creation publishes no usable bundle, but can leave a zero-length, mode 0000 residual that reserves the destination name. Confirm that the residual is the expected regular zero-byte leaf in the trusted directory, remove that exact leaf, and rerun the export. If its type, size, mode, owner, or location differs, treat the namespace as unsafe and choose a new trusted destination instead of modifying it in place.

A bundle leaves the host, so it is the projected copy, not the durable one. Every record carries the tenant as a one-way fingerprint and any entity label outside the built-in set as a placeholder, and the header uses the same fingerprint for the chain names and for the --tenant value you passed. One fingerprint therefore ties the filter, the chains, and the records together, and the artifact still does not spell out who the tenant is. A record the export cannot project aborts the export rather than travelling raw.

Pass --tenant the tenant id as your deployment writes it: the filter selects on the stored value, while what comes out carries its fingerprint. The durable file the export reads is not modified and stays raw on the host, which is where an auditor who needs tenant identifiers intact works, under your access controls, rather than from a bundle.

The exported records would carry the per-event Art. 30 label fields (purpose, legal_basis, data_category) if the runtime populated them; today those fields are unset. The complete Records of Processing (data-subject categories, retention schedules) is organizational and lives in your RoPA template, not in this runtime export.

Verification Checklist

Use this checklist to verify OGuardAI compliance controls in your deployment:

  • Sealed session mode enabled (default)
  • Session TTL configured appropriately
  • TLS enabled for all endpoints
  • Auth mode set to ApiKey, Jwt, or Oidc (not Dev)
  • Audit events routed to SIEM/log aggregator
  • Log rotation and retention configured
  • Policy directory version-controlled (git)
  • Policy validation run before deployments
  • Encryption key rotation schedule established
  • Output guard enabled for sensitive workflows
  • Prompt security enabled (default)