Compliance Controls Mapping
Article-by-article mapping of OGuardAI controls to GDPR, HIPAA, SOC 2, and PCI DSS requirements
GDPR Alignment
| GDPR Article | Requirement | OGuardAI Control | How to Verify |
|---|---|---|---|
| Art. 5(1)(c) | Data minimization | Tokenization removes PII from LLM context | Transform API returns safe_text with detected, non-whitelisted PII tokenized or removed |
| Art. 5(1)(e) | Storage limitation | Sealed sessions auto-expire, no server-side retention | Check session TTL configuration |
| Art. 25 | Data protection by design | Trust boundary model, sealed sessions | Security whitepaper Section 1 |
| Art. 28 | Processor obligations | Self-hosted, no subprocessors in sealed mode | Deployment guide |
| Art. 30 | Records of processing | Structured audit events per operation | Audit module, diagnostics endpoint |
| Art. 32 | Security of processing | AES-256-GCM encryption, key rotation | Security whitepaper Section 2 |
| Art. 17 | Right to erasure | Three mechanisms: session expiry, entity revocation, key retirement. Expiry alone is a runtime refusal, not cryptographic destruction | GDPR, Right to Erasure |
| Art. 20 | Data portability | Sealed sessions are portable (client-held) | Session model |
| Art. 35 | Data protection impact assessment | Audit trail, policy validation, shadow (dry-run) mode for staging evaluation | Admin 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 Section | Requirement | OGuardAI Control | Implementation Detail |
|---|---|---|---|
| SS 164.312(a)(1) | Access control | JWT/API key auth + scope model | Auth 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/decryption | AES-256-GCM sealed sessions | Session crate with AEAD encryption |
| SS 164.312(b) | Audit controls | Structured audit events | Audit module emits events via tracing |
| SS 164.312(c)(1) | Integrity controls | AEAD authentication tag on session blobs | Tamper detection built into AES-GCM |
| SS 164.312(e)(1) | Transmission security | TLS support, encrypted session state | Deployment guide covers TLS setup |
| SS 164.502(b) | Minimum necessary | Tokenization + policy-based restore modes | 6 restore modes: full, partial, masked, formatted, abstract, none |
| SS 164.308(a)(5)(ii) | Security awareness | Policy validation and dry-run endpoints | Admin can preview impact before changes |
SOC 2 Type II Alignment
| Control | Description | OGuardAI Feature | Evidence |
|---|---|---|---|
| CC6.1 | Logical access | Auth middleware with scopes | API key/JWT validation on every request |
| CC6.3 | Role-based access | Per-endpoint scopes (Transform, Rehydrate, Detect, Policy, Batch, Rag, Revoke, Global Revoke, Session, Detect Values, Admin) | Scope-based authorization checks |
| CC6.6 | System boundary protection | Trust boundary model | Detected, non-whitelisted PII tokenized before leaving the runtime boundary |
| CC6.7 | Restrict data mobility | Policy engine controls entity exposure | Per-entity-type action rules |
| CC7.1 | Detect security events | Prompt security engine, output guard | Input/output scanning for injection and PII leaks |
| CC7.2 | System monitoring | Structured audit events, diagnostics endpoint | Audit trail, health checks |
| CC8.1 | Change management | Policy diffing, validation endpoint | Preview impact of policy changes before deployment |
| CC9.1 | Risk mitigation | Multi-layer detection (regex + NLP) | Builtin + advanced detector modes |
PCI DSS Alignment (Where Applicable)
| PCI DSS Requirement | OGuardAI Control |
|---|---|
| 3.4 -- Render PAN unreadable | Credit card tokenization with Luhn validation |
| 3.5 -- Protect cryptographic keys | Session key derived from configured secret, key rotation supported |
| 10.1 -- Audit trails | Structured audit events per operation |
| 10.3 -- Record audit trail entries | Timestamp, 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:
| Copy | Tenant id | Free-form audit labels | Entity labels |
|---|---|---|---|
Durable file record on the host | Verbatim | Verbatim | Verbatim |
guardai::audit log line | Keyed fingerprint | Field-separated keyed fingerprints | Reduced to the built-in set |
Notification webhook and audit export | Keyed fingerprint | Field-separated keyed fingerprints | Reduced 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-serverThis 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 Type | When Emitted | What It Records |
|---|---|---|
policy_loaded | Server startup | Each policy loaded from the policies directory |
policy_validated | Admin validation endpoint | Proposed 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.yamlVerifying 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 acmeExit 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 v1policy:
directory: policies
integrity:
manifest: policies/.signatures.json
secret: "${GUARDAI_POLICY_SIGNING_KEY}"
kid: v1The 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.secretis the preferred independent audit root. When it is unset, the root falls back tosession.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 inaudit.retired_secrets.- Tail truncation (deleting the most recent records) leaves a valid prefix; publish each chain
head_hashbetween audits as an external anchor to detect it. - For a portable evidence copy,
audit exportproduces an integrity-attested NDJSON bundle (or a--tenant/--since/--untilslice) 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 1717200000The 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, orOidc(notDev) - 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)