Configuration Reference
OGuardAI configuration file format and environment variables
oguardai.yaml
The primary configuration file controls all server behavior. Pass it with --config oguardai.yaml when starting the server.
Parsing is strict and fails closed. An unrecognized top-level key aborts startup with an error that names it, and nested sections such as tenants reject an unknown or misspelled key the same way. The server never silently drops a setting it validates, so a typo like allowed_polcies cannot quietly disable a control and leave the server running with an unintended default.
server:
host: "0.0.0.0"
port: 3000
auth:
mode: dev # dev | api_key | jwt | oidc
session:
backend: sealed # sealed | memory | redis (managed is planned)
ttl_seconds: 3600
detector:
mode: builtin # builtin | both | advanced
# advanced_url is unset by default; set it to the Python NER sidecar
# URL when mode is both or advanced, e.g. http://detector:9090
policy:
default: default
directory: ./policies
watch: false
transform:
context_strategy: full
max_context_tokens: 4096
shadow_mode: false
output_protection:
enabled: true # Enabled by default — set false to disable
mode: strict
default_action: mask
exempt_types: []
prompt_security:
enabled: true
input_scanning: true
action: strip
file_upload:
max_size_bytes: 52428800 # 50MB
rate_limit:
enabled: false
requests_per_second: 100
burst_size: 200Server
| Key | Default | Description |
|---|---|---|
server.host | 0.0.0.0 | Bind address |
server.port | 3000 | Listen port |
server.allowed_origins | (unset) | CORS allowlist. When unset, all origins are allowed (open by default), so set it in production. Also settable via GUARDAI_CORS_ORIGINS |
server.max_body_size_bytes | 10485760 | Maximum request body size in bytes for non-file routes (default 10 MB). File-upload routes use file_upload.max_size_bytes instead |
server.tls.enabled | false | Terminate TLS in-process |
server.tls.cert_path | (unset) | PEM certificate path (also GUARDAI_TLS_CERT) |
server.tls.key_path | (unset) | PEM private-key path (also GUARDAI_TLS_KEY) |
server:
host: "0.0.0.0"
port: 3000
allowed_origins:
- "https://app.example.com"
max_body_size_bytes: 10485760
tls:
enabled: true
cert_path: /etc/guardai/tls/server.crt
key_path: /etc/guardai/tls/server.keyAuth
| Key | Default | Description |
|---|---|---|
auth.mode | dev | Authentication mode: dev (no auth), api_key, jwt (HMAC shared secret), or oidc (asymmetric tokens verified against an OIDC provider's JWKS) |
Do not use auth.mode: dev in production. Always set a real auth mode and API keys for production deployments.
API keys (auth.api_keys)
In api_key mode, callers authenticate with the X-API-Key header and each key is declared under
auth.api_keys. Starting in api_key mode with no keys configured aborts startup.
auth:
mode: api_key
api_keys:
- key: "sk-live-a1b2c3..." # the raw secret compared in constant time
identity: "billing-service" # identity label recorded in the audit log
name: "production-key-1" # optional human-readable label
tenant_id: "acme-corp" # optional tenant scope for multi-tenancy
scopes: [transform, rehydrate, detect]
caller_role: "support-agent" # optional; overrides any request-supplied caller_role
caller_purpose: "support" # optional; overrides any request-supplied caller_purpose| Field | Required | Description |
|---|---|---|
key | yes | The raw key value. Compared with constant-time equality; never serialized back out |
identity | yes | Identity label recorded in the audit log |
name | no | Human-readable label for the key (audit only, does not expose the key) |
tenant_id | no | Tenant scope for multi-tenancy |
scopes | yes | Granted route scopes: any of transform, rehydrate, detect, policy, batch, rag, revoke, global_revoke, session, detect_values, admin. revoke erases within the key's tenant; an untenanted key also needs global_revoke (which admin implies) to write a cross-tenant revocation |
caller_role | no | Role bound to the key. When set, it overrides any request-supplied caller_role, so a caller cannot self-assert a privileged role |
caller_purpose | no | Purpose bound to the key. When set, it overrides any request-supplied caller_purpose. Same trust rationale as caller_role |
Keys can also be injected from the environment (for Helm or other orchestrators) without touching the
config file. GUARDAI_API_KEY_<NAME>=<key> registers a key whose identity is <name> lowercased with
underscores turned into hyphens. Companion variables bind extra attributes to that key:
| Variable | Effect |
|---|---|
GUARDAI_API_KEY_<NAME> | Registers the key. If auth is still dev, the server auto-upgrades to api_key mode |
GUARDAI_API_KEY_<NAME>_SCOPES | Comma-separated scopes. When omitted, the key defaults to transform,rehydrate,detect only |
GUARDAI_API_KEY_<NAME>_CALLER_ROLE | Deployment-bound caller_role the caller cannot self-assert |
GUARDAI_API_KEY_<NAME>_CALLER_PURPOSE | Deployment-bound caller_purpose the caller cannot self-assert |
Session
| Key | Default | Description |
|---|---|---|
session.backend | sealed | Session backend: sealed (encrypted client-side blob, stateless), memory (in-process HashMap, dev/testing, single-instance), or redis (each session encrypted at rest in a shared Redis, cross-replica HA; requires session.redis_url, replay_backend: redis, and revocation_backend: redis). managed is planned. |
session.ttl_seconds | 3600 | Session time-to-live in seconds |
session.redis_url | (unset) | Redis URL. Used by the redis revocation, replay, and session backends. Set via GUARDAI_REDIS_URL |
session.max_sessions | 100000 | Maximum live sessions for the in-memory backend before new sessions are rejected |
session.keyring | (unset) | Optional multi-key sealing ring for zero-downtime secret rotation (see below) |
The sealed backend (default) stores session state in an AES-256-GCM encrypted blob returned to the client, so it is stateless and already works across replicas with no shared session store (a multi-replica deployment still needs revocation_backend: redis). The memory backend stores sessions server-side in an in-process HashMap, suitable for development and single-instance testing (sessions are lost on restart and are not shared across replicas). The redis backend stores each session encrypted at rest in a shared Redis (AES-256-GCM under the same key ring as sealed), giving cross-replica, server-side sessions the client references by session_id. It requires session.redis_url, replay_backend: redis, and revocation_backend: redis; Redis holds only ciphertext plus non-PII routing metadata (tenant, policy, and session ids, timestamps, language), never the raw token map. managed is planned.
Key rotation (session.keyring)
With no keyring, a single sealing key is derived from GUARDAI_SESSION_SECRET. To rotate the sealing
secret without invalidating live sessions, declare a keyring: new sessions seal under current_kid,
while any listed key can still decrypt. Rotate by adding a new key, switching current_kid, and
dropping the old key after one session TTL.
session:
keyring:
current_kid: 2
keys:
- { kid: 1, secret: "previous-secret-at-least-16-chars" }
- { kid: 2, secret: "current-secret-at-least-16-chars" }The ring fails closed at startup on an empty keys, a duplicate kid, a current_kid not present in
the ring, or a secret shorter than the derivation minimum.
Detector
| Key | Default | Description |
|---|---|---|
detector.mode | builtin | Detector mode: builtin (Rust regex only), both (Rust regex + Python NER, graceful fallback to regex on a NER outage), or advanced (Rust regex + Python NER with NER required, fail-closed on a NER outage). See the degraded-mode note below |
detector.advanced_url | (unset) | URL for the Python NER detector service; used when mode is both or advanced. The config default is unset; when unset in a NER mode the runtime falls back to http://localhost:9090, so set it explicitly for any non-local sidecar |
detector.default_language | (unset) | Default language hint applied when a request does not specify one, feeding language-aware detection and enrichment |
detector.timeout_secs | 5 | Timeout for a single NER detector HTTP request, in seconds (1 to 120); env GUARDAI_DETECTOR_TIMEOUT_SECS |
detector.custom_detectors | [] | Deployment regex detectors run alongside the built-ins; each match becomes a custom:<type> entity |
detector.vocab | {} | Additive vocabulary overlay (see below). Only strengthens detection; built-in defaults stay the floor |
detector.scan_output_with_ner | false | Also run NER on rehydrated output (output-guard second pass), masking model-generated names/orgs that regex misses. Extra latency; effective only in both/advanced mode |
Degraded mode: both vs advanced on a NER outage
Both both and advanced run the built-in Rust regex detectors alongside the Python NER sidecar. They
differ only in what happens when the sidecar is unreachable or times out:
both(graceful fallback): the request continues with builtin regex only. The NER-only entity types (person, company, location) are simply not detected for that request. No crash, no error. The transform response reports the actual result on itsdetector_modeandwarningsfields, so a caller can see that NER did not run.advanced(fail-closed): NER is required, so a sidecar outage fails the request closed withGUARDAI_DETECTION_FAILEDrather than silently returning regex-only results. Chooseadvancedwhen passing input through undetected is worse than rejecting it.
Independently of the configured mode, a policy that lists a type under detection.required_for fails a
request closed when NER did not actually run for it, so a mandatory type is never dropped by a silent
fallback. GET /v1/health reports the CONFIGURED mode, not live NER reachability (so liveness/readiness
probes stay stable). Read ner_active on GET /v1/capabilities to see whether NER is configured, and
the transform response's detector_mode and warnings to see whether NER ran for a given request.
Bring your own or fine-tuned NER model
The NER models are not pinned into the runtime, only the shipped defaults are. The variables below
configure NER without a code change. The model-selection variables (NER_BACKEND, GLINER_MODEL,
GUARDAI_SPACY_MODELS, NER_BACKEND_STRICT) are set on the Python detector service; the
detection-floor variables (GUARDAI_NER_MIN_CONFIDENCE, GUARDAI_NER_MIN_LENGTH,
GUARDAI_NER_ACRONYM_ALLOW, GUARDAI_NER_ACRONYM_STOPLIST) are read by the server runtime; and
GUARDAI_DETECTOR_API_KEY secures the hop between them:
| Env var | Default | What it selects |
|---|---|---|
NER_BACKEND | gliner | gliner, spacy, or none |
GLINER_MODEL | urchade/gliner_medium-v2.1 | Any GLiNER model: a HuggingFace id (e.g. a fine-tune you published) or a local path mounted into the container |
GUARDAI_SPACY_MODELS | {"en":"en_core_web_sm"} | JSON {language: model_name} so each language uses your chosen spaCy model (a larger or domain-tuned one) |
NER_BACKEND_STRICT | false | Abort startup if the chosen model cannot load, instead of silently falling back |
GUARDAI_NER_MIN_CONFIDENCE | 0.65 | NER precision floor for person/company/location; may only be raised (stricter) |
GUARDAI_NER_MIN_LENGTH | 3 | Minimum NER span length; lower it to catch short names. A policy's detection.required_for bypasses it, so a mandatory type is never dropped for being short |
GUARDAI_NER_ACRONYM_ALLOW | - | Comma-separated built-in stoplist acronyms to un-suppress, so an acronym-shaped real entity (a program or brand name) is detected |
GUARDAI_NER_ACRONYM_STOPLIST | - | Comma-separated deployment-specific acronyms to suppress as NER noise, on top of the built-in stoplist |
GUARDAI_DETECTOR_API_KEY | - | Shared secret for the runtime-to-detector hop (X-Detector-API-Key). The detector refuses to start without it unless GUARDAI_DETECTOR_ALLOW_INSECURE=true (development only) |
Because GLINER_MODEL accepts a local path, a fine-tuned model is used by mounting it into the
detector container and pointing GLINER_MODEL at it. The pinned defaults exist for reproducible
builds; they never prevent an operator from running their own model. Zero-shot detection labels are
still driven per policy via detection.ner_labels, so a domain vocabulary needs no model change at
all.
Tuning detection for your domain and language (detector.vocab)
The detector ships defaults for ~30 languages and the common PII formats. Extend it for your
domain or locale without recompiling via detector.vocab. The overlay is additive: it can
only add or strengthen detection, never silently weaken it, and invalid config fails startup.
detector:
vocab:
# Context-boost words per entity type (raise confidence when seen near a match).
context_words:
money: ["honorar", "rechnungsbetrag"]
person: ["mandant", "ansprechpartner"]
# Currency markers beyond ISO 4217 (each 2-8 uppercase ASCII alphanumerics).
currency_codes: ["XBT"]
# Extend ANY entity type with locale/domain regexes. A built-in label (address,
# date_of_birth, ...) yields the built-in entity; use "custom:<name>" for a new type.
extra_patterns:
address: ['〒\d{3}-\d{4}[^\n]{0,40}'] # Japanese postal address
date_of_birth: ['\b\d{4}年\d{1,2}月\d{1,2}日\b'] # Japanese date
"custom:vat_id": ['\bVAT-\d{6}\b']Narrowing money detection is the only detector.vocab control that reduces detection, so it is
a gated break-glass requiring explicit acknowledgement (logged at startup):
detector:
vocab:
money_colliding_codes: ["TRY"]
allow_money_detection_suppression: true
money_suppression_reason: "TRY collides with the verb 'try' in our corpus"Per-policy detection (custom regex detectors, required-NER fail-closed, confidence floors) is set
in policy YAML via custom_patterns and a detection block. The proxy reads the detection
vocabulary overlay from GUARDAI_DETECTOR_VOCAB (JSON). For the end-to-end extension model that ties
these overlays to custom entity types and channels, see the
extensibility guide.
External HTTP detector (detector.webhook)
Register an external detector over HTTP. Per request the runtime POSTs {text, language} to the
endpoint and merges the returned spans additively, so a domain-specific detector augments the
built-in and NER detectors. Absent = detection unaffected; invalid config aborts startup.
detector:
webhook:
url: https://detector.internal/scan
entity_types: [employee_id, case_ref] # only these types are accepted; no built-in shadowing
api_key: ${DETECTOR_KEY} # optional, sent as X-Detector-API-Key
timeout_secs: 5
required: false # true => a failure (timeout/error/malformed) rejects the requestThe plugin can only ADD coverage: a span overlapping a built-in or NER span is dropped, and an offset that disagrees with a returned value is rejected. See the plugin guide for the full contract.
Chunk detection concurrency (rag.chunk_detection_concurrency)
During RAG ingest, per-chunk detection runs concurrently up to this bound (0 = host CPU count,
maximum 256). Tokenization stays ordered, so output is identical regardless of the value.
Notifications (notifications)
Deliver the runtime's PII-free audit events to an external endpoint (a SIEM or observability pipeline). Delivery is fire-and-forget and fail-open, so a slow or down endpoint never blocks or fails a request. The payload carries entity types and counts, never values, with session id, trace id, and tenant id as one-way fingerprints. Absent = disabled.
notifications:
webhook_url: https://sink.internal/guardai-events
api_key: ${SINK_KEY} # optional, sent as X-Notification-Key
timeout_secs: 5
events: [transform, policy_denied, session_expired] # empty => all eventsLanguage packs
Language enrichment (gender, honorifics, formality) is fully data-driven: every language ships a
built-in JSON lexicon (the immutable floor), and operators add, extend, or override any language
from data, with no source change, via the Python detector's GUARDAI_LANGUAGE_PACKS (a JSON file
mapping a language code to a lexicon). Each entry's optional _mode selects the operation:
{
"yue": { "female_titles": ["女士"], "male_titles": ["先生"] },
"de": { "male_names": ["acmebob"], "informal_markers": ["servus "] },
"fr": { "_mode": "replace", "female_titles": ["Mme "], "male_titles": ["M. "] }
}- add a brand-new language (no built-in for the code): registered as-is.
- extend a built-in (default,
_modeabsent or"extend"): the overlay can only widen the built-in. The built-in's gender/honorific stay authoritative (the overlay only fills gaps for inputs the built-in cannot classify), so an extend overlay can never flip a built-in confident result; new formality markers join the built-in's vote; structural fields (rtl,name_order) stay built-in. - replace a built-in (
_mode: "replace"): full override.
A malformed pack fails loudly at startup. Lexicon fields: female_titles, male_titles,
female_names, male_names, formal_markers, informal_markers, honorific_suffixes
([suffix, label] pairs), patronymic_suffixes ({ "female": [...], "male": [...] }), rtl,
name_order ("given_first" | "given_last"), and default_formality.
Policy
| Key | Default | Description |
|---|---|---|
policy.default | default | Default policy name applied when no policy is specified in the request |
policy.directory | ./policies | Directory containing policy YAML files |
policy.watch | false | Watch policy.directory and hot-reload on change. A reload runs the same fail-closed chain as startup and keeps the running policy if the new set is invalid or empty in non-dev. The POST /v1/admin/policy/reload endpoint triggers the same reload on demand regardless of this flag. |
Policy integrity (policy.integrity)
Optional signed-manifest verification of the policy directory. It is fail-closed and both-or-neither:
when both manifest and secret are set, the server verifies every policy file against the signed
manifest at startup and refuses to start on any mismatch. Both absent disables it; setting exactly one
is a config error (a security control is not half-enabled).
policy:
integrity:
manifest: /etc/guardai/policies/.signatures.json # relpath -> HMAC map from `oguardai-server policy sign-dir`
secret: "${GUARDAI_POLICY_SIGNING_KEY}" # HMAC signing secret (min 16 chars)
kid: "policy-signer-1" # optional key id (informational)| Key | Description |
|---|---|
policy.integrity.manifest | Path to the signed relpath -> HMAC manifest produced by oguardai-server policy sign-dir. Also GUARDAI_POLICY_SIGNATURES_MANIFEST |
policy.integrity.secret | HMAC signing secret, at least 16 characters. Also GUARDAI_POLICY_SIGNING_KEY |
policy.integrity.kid | Optional key id recorded in the manifest (informational) |
Transform
| Key | Default | Description |
|---|---|---|
transform.context_strategy | full | How much entity context metadata to include for the LLM |
transform.max_context_tokens | 4096 | Maximum tokens for context metadata |
transform.shadow_mode | false | When true, includes original (unprotected) text alongside safe text for comparison during rollout. Never enable in production. |
Output Protection
| Key | Default | Description |
|---|---|---|
output_protection.enabled | true | Enable second-pass output scanning for newly generated PII |
output_protection.mode | strict | Scanning mode |
output_protection.default_action | mask | Action taken when new PII is found in output |
output_protection.exempt_types | [] | Entity types exempt from output scanning |
output_protection.enabled is a production invariant: with auth.mode other than dev, the
server refuses to start when it is false (use auth.mode=dev to bypass for testing).
The output guard runs the built-in regex detectors on every response. The model only ever sees
tokens, so a real-looking name it emits is generated and structured-only regex would miss it. Set
detector.scan_output_with_ner: true (modes both/advanced) to add a NER pass on output: it runs
additively over the regex floor (only adds detections, never suppresses one). Because output
scanning is an explicit safety net, it fails closed: a NER-sidecar outage blocks the response rather
than silently degrading to regex-only (independent of the ingress mode). The proxy enables the same
pass with GUARDAI_OUTPUT_SCAN_NER=1, covering both plain-text output and structured JSON response
field values (object keys stay regex-only, since per-key NER multiplies sidecar calls for little gain).
Restore Templates
Localized restore output (honorifics, abstract labels) merged over the built-in defaults for ~30 languages. Override only what you need; everything else keeps its default.
restore_templates:
person_titles:
de:
formal_female: "Sehr geehrte Frau {name}"
formal_male: "Sehr geehrter Herr {name}"
tr:
formal_female: "Sayın {name}"
company_abstract_labels:
ja: "(会社名)"The proxy reads the same overrides from GUARDAI_RESTORE_TEMPLATES (JSON).
Restore strategies (partial reveal)
A partial restore reveals a bounded, deterministic fragment of a value instead of the full original.
You configure it per entity type under restore_templates.partial_formats (deployment-wide) or per
policy under restore_strategies (same shape, policy-scoped). Each entry is keyed by the entity type
name, for example credit_card or salary.
restore_templates:
partial_formats:
# Reveal only the last n characters, mask the rest.
credit_card:
kind: reveal_last_n
n: 4
mask_char: "*"
# Always mask, never reveal.
ssn:
kind: mask
# Generalize a numeric value into a labelled band (e.g. salary bands).
salary:
kind: bucket
ranges:
- { max: 50000, label: "<50k" }
- { max: 100000, label: "50k-100k" }
- { label: "100k+" } # open-ended top band: omit maxThe same per-policy form lives in policy YAML:
restore_strategies:
credit_card:
kind: reveal_last_n
n: 4
mask_char: "*"Five kinds are available:
kind | Behavior | Required | Constraints |
|---|---|---|---|
reveal_last_n | Reveal the last n characters, mask the rest. Whitespace grouping is preserved. | n | n is 1..64. mask_char is a single character, default *. |
reveal_first_n | Reveal the first n characters (a fixed prefix such as an IBAN country code), mask the rest. | n | n is 1..64. mask_char optional. |
reveal_range | Reveal a len-character window starting at 0-based start (an interior span such as a phone area code), mask the rest. | start, len | start is 0..64, len is 1..64. mask_char optional. |
mask | Mask the whole value. | none | none |
bucket | Parse the value as a number and map it into the first matching ranges band, then reveal only that band's label. | ranges | ranges has at least one entry. Each entry has a string label and an optional integer max; omit max for an open-ended top band. |
Use restore_strategies in a policy when the format should be scoped to that policy, and
restore_templates.partial_formats when the format should apply deployment-wide across policies.
These rules hold:
- Channel-ceiling gated. A partial format applies only when the channel-resolved restore mode is Partial. It never widens what the channel allows: if the channel ceiling is Masked or stricter, the value stays masked regardless of the format.
- Fail closed. If a strategy cannot produce a bounded result, it masks.
reveal_last_nmasks the whole value when the value is no longer thann.bucketmasks when the value does not parse as a number or no range matches. - Invalid config aborts startup. A
reveal_last_nwithnoutside1..64, an emptybucketranges, or a missing required field is rejected at load time, so a misconfigured format cannot silently reveal more than intended.
For the broader extension model (custom entity types, caller-role conditions, custom output channels, detector and language overlays), see the extensibility guide.
Prompt Security
| Key | Default | Description |
|---|---|---|
prompt_security.enabled | true | Enable prompt injection defense |
prompt_security.input_scanning | true | Scan input for prompt injection attempts |
prompt_security.action | strip | Action on detected injection: warn, block, or strip |
prompt_security.extra_patterns | [] | Deployment injection patterns added to the built-in multilingual baseline (any language). Additive |
The built-in injection patterns are an immutable floor: English plus a baseline for the canonical
attacks (instruction override, system-prompt extraction) in German, French, Spanish, Italian,
Portuguese, and Dutch. Config can only add, never remove them. Extend detection to any other
language or house style with extra_patterns (each a Rust regex; set case-insensitivity inline with
(?i)); an entry that fails to compile is skipped with an error log so the floor still applies.
prompt_security:
action: strip
extra_patterns:
- pattern: '(?i)önceki\s+talimat.{0,20}(?:yok\s*say|unut)' # Turkish instruction override
label: "instruction_override (tr)"File Upload
| Key | Default | Description |
|---|---|---|
file_upload.max_size_bytes | 52428800 | Maximum upload size in bytes (default 50 MB) |
Request Limits
Operational request limits, each bounded by a built-in maximum. The batch and streaming caps default to their maximum, so they are tighten-only (a DoS ceiling you can only lower). The heavy-op knobs default below their maximum, so they are tunable to your workload up to a built-in safety maximum. A value above its maximum, or zero, aborts startup, so a misconfiguration can never weaken the bound.
| Key | Default | Maximum | Description |
|---|---|---|---|
limits.max_batch_size | 100 | 100 | Maximum items per batch request; advertised on /v1/capabilities (tighten-only) |
limits.stream_chunk_bytes | 500 | 500 | SSE chunk size in bytes when streaming transform/rehydrate output (tighten-only) |
limits.stream_buffer_bytes | 1024 | 1024 | Token-boundary buffer size in bytes per stream (tighten-only) |
limits.max_blocking_ops | host CPUs | 256 | Concurrent heavy blocking ops (OCR/parse/redact); tune to your hardware |
limits.ocr_timeout_secs | 30 | 600 | Hard timeout for one Tesseract OCR run before it is killed; raise for large scans |
limits.pdf_extract_memory_mib | 256 | 256 | Address-space cap (MiB) for the bounded PDF extraction subprocess (lower-only) |
limits.pdf_extract_timeout_secs | 10 | 10 | Wall-clock timeout (seconds) for the PDF extraction subprocess (lower-only) |
PDF text extraction runs in a short-lived child process under a hard RLIMIT_AS address-space cap
and a wall-clock timeout, so a decompression-bomb PDF (a small file whose compressed streams inflate
to gigabytes) exhausts the bounded child and is rejected with a clean error instead of exhausting the
server. Both limits are security ceilings: a deployment may only lower them, and a value above the
maximum or zero aborts startup. The raw PDF never leaves the runtime: the sandbox child is a re-exec
of the server binary, not an external service.
The address-space cap is a hard fail-closed extraction ceiling. A genuinely large or structurally complex PDF whose extraction would need more than the cap is rejected rather than risk the server, so a small fraction of legitimate documents may be refused. This is deliberate: raising the ceiling above the built-in maximum requires a code change, since config may only lower it.
Rate Limiting
| Key | Default | Description |
|---|---|---|
rate_limit.enabled | false | Enable rate limiting |
rate_limit.requests_per_second | 100 | Requests per second (per tenant when authenticated, per IP otherwise) |
rate_limit.burst_size | 200 | Maximum burst capacity (token bucket size) |
rate_limit.trusted_proxy_depth | 0 | Number of trusted proxy hops. Set to 1 behind nginx/ALB to use the IP the proxy wrote, skipping attacker-injected entries. |
Tenants
Per-tenant overrides allow different policies and rate limits per tenant:
tenants:
acme-corp:
default_policy: gdpr-strict
allowed_policies:
- gdpr-strict
- healthcare
rate_limit:
requests_per_second: 50
burst_size: 100allowed_policies: Optional allowlist of policy names this tenant may use. When set, request-level policy overrides are validated against this list. If absent or empty, all loaded policies are permitted.- Unknown tenants fall through to global defaults.
- A misspelled key inside a tenant entry aborts startup, it is not ignored. For example
allowed_polcies(a typo ofallowed_policies) fails the load rather than leaving the allowlist unset, which would otherwise permit every loaded policy.
RAG access control
The RAG access-control lattice (used to filter document chunks by access_level vs each chunk's
classification) defaults to public < internal < confidential < restricted. Override it for your
domain scheme without code:
rag:
classification_levels: ["unclassified", "cui", "confidential", "secret", "top_secret"]Levels are ordered least to most restrictive. An access level grants a chunk when its rank is greater than or equal to the chunk's. Any label not in the lattice ranks most-restrictive (fail closed), so an unknown or misspelled classification is never over-exposed. The list must be non-empty, unique, and at most 255 entries.
Providers
Per-provider behavior profiles. Each key is a provider name, and its context_strategy sets how much
entity context metadata that provider receives. Built-in defaults exist for openai, anthropic, and
local; the reserved key default sets the profile used for any provider without an explicit entry.
When the providers map is omitted, the built-in defaults apply.
providers:
openai:
name: openai
context_strategy: full
anthropic:
name: anthropic
context_strategy: full
local:
name: local
context_strategy: minimal
default:
name: default
context_strategy: fullAudit
Where audit events are written. Every data-path request produces a PII-free audit event.
| Key | Default | Description |
|---|---|---|
audit.backend | log | log emits events through tracing; file writes a durable, hash-chained append-only log. Also GUARDAI_AUDIT_BACKEND |
audit.file.path | (none) | Path to the append-only log. Required when backend: file; startup fails without it. Also GUARDAI_AUDIT_FILE_PATH |
audit.queue_capacity | 8192 | Bounded queue capacity for the file backend's background writer |
audit.strict | false | When true, a request whose audit event cannot be recorded fails closed with 503 instead of being acknowledged unaudited. Also GUARDAI_AUDIT_STRICT |
audit.allow_ephemeral | false | Explicit waiver of the non-dev durable, strict audit requirement (see below). Also GUARDAI_ALLOW_EPHEMERAL_AUDIT=1 |
audit:
backend: file
file:
path: /var/log/guardai/audit.log
queue_capacity: 8192
strict: trueA non-dev start (auth.mode of api_key, jwt, or oidc) requires a durable, strict trail:
audit.backend: file AND audit.strict: true. Startup fails otherwise, so a successful request can
never have its audit record silently dropped. Set audit.allow_ephemeral: true (or
GUARDAI_ALLOW_EPHEMERAL_AUDIT=1) to explicitly accept an ephemeral or non-strict trail. The
compliance preset ignores this waiver and always forces durable, strict.
To pin both at once, start the server with the --compliance flag or GUARDAI_COMPLIANCE=1. The
compliance preset only tightens: it forces audit.backend: file and audit.strict: true, and it fails
closed at startup if the resulting audit trail is not both durable and strict (for example when
audit.file.path is unset). It also floors detect.include_values_default to false when the operator
has not set it. The gate runs regardless of auth.mode, so it holds even in dev mode. The audit event
schema additionally reserves optional GDPR Art. 30 labels (purpose, legal_basis, data_category),
PII-free controlled labels a deployment may populate from policy/config; see the
compliance controls page.
Revocation and Replay backends
Two top-level backends control session revocation and request-replay protection. Both default to
memory and can be switched to redis for a shared store across replicas. A redis backend requires
session.redis_url (or GUARDAI_REDIS_URL) and fails startup validation without it.
| Key | Default | Description |
|---|---|---|
revocation_backend | memory | Revocation store: memory (per-replica, file-backed at revocation_path) or redis (shared cross-replica). Also GUARDAI_REVOCATION_BACKEND |
revocation_path | revocations.dat | File the in-memory revocation store persists to |
replay_backend | memory | Request-replay store: memory (per-replica, in-process) or redis (shared cross-replica). Rejects a captured session_state continuation whose counter does not advance. Also GUARDAI_REPLAY_BACKEND |
When running more than one replica, use redis for both so a revoked session and a replayed
session_state are recognized on every instance. Redis stores only HMAC digests, never raw PII.
revocation_backend: redis
replay_backend: redis
session:
redis_url: "redis://redis:6379"Idempotency
Mutating, non-streaming routes (transform, batch transform, RAG ingest/delete, revoke, session
delete) accept an Idempotency-Key header so a client can safely retry without executing the
mutation twice. The store is configured under idempotency:
| Key | Default | Description |
|---|---|---|
idempotency.backend | memory | memory (per-replica, in-process) or redis (shared cross-replica; uses session.redis_url). Also GUARDAI_IDEMPOTENCY_BACKEND |
idempotency.ttl_seconds | 86400 | How long a COMPLETED key is retained (replayed) before a retry re-executes. Also GUARDAI_IDEMPOTENCY_TTL_SECONDS |
idempotency.in_progress_ttl_seconds | 60 | How long an IN-PROGRESS claim is held before it becomes reclaimable, so a request abandoned mid-flight frees its key quickly. Also GUARDAI_IDEMPOTENCY_IN_PROGRESS_TTL_SECONDS |
idempotency:
backend: memory
ttl_seconds: 86400
in_progress_ttl_seconds: 60Environment Variables
Environment variables override configuration file values.
| Variable | Default | Description |
|---|---|---|
GUARDAI_SESSION_SECRET | (required) | Secret for AES-256-GCM session encryption (16 char minimum) |
GUARDAI_HOST | 0.0.0.0 | Server bind address |
GUARDAI_PORT | 3000 | Server listen port |
GUARDAI_DETECTOR_URL | (none) | Python NER service URL |
GUARDAI_DETECTOR_TIMEOUT_SECS | 5 | detector.timeout_secs override (1..=120) |
GUARDAI_REDIS_URL | (none) | Redis URL for the shared revocation, replay, and session backends |
GUARDAI_AUTH_MODE | (from config) | Overrides auth.mode (dev | api_key | jwt | oidc). An invalid value aborts startup |
GUARDAI_COMPLIANCE | (unset) | Truthy (1/true/yes/on) applies the compliance preset (same as --compliance): forces audit.backend: file + audit.strict: true and fails closed at startup unless the audit trail is durable and strict |
GUARDAI_API_KEY_<NAME> | (none) | Registers an API key with identity <name>; auto-upgrades dev to api_key. Companions: _SCOPES (default transform,rehydrate,detect), _CALLER_ROLE, _CALLER_PURPOSE (see Auth) |
GUARDAI_POLICY_DIR | (from config) | Overrides policy.directory |
GUARDAI_DEFAULT_POLICY | (from config) | Overrides policy.default |
GUARDAI_POLICY_SIGNING_KEY | (none) | HMAC secret for policy.integrity.secret (signed-manifest verification) |
GUARDAI_POLICY_SIGNATURES_MANIFEST | (none) | Path override for policy.integrity.manifest |
GUARDAI_REVOCATION_BACKEND | memory | Overrides revocation_backend (memory | redis) |
GUARDAI_REPLAY_BACKEND | memory | Overrides replay_backend (memory | redis) |
GUARDAI_AUDIT_BACKEND | log | Overrides audit.backend (log | file). Non-dev requires file + strict unless waived |
GUARDAI_AUDIT_FILE_PATH | (none) | Sets audit.file.path (required with the file backend) |
GUARDAI_AUDIT_STRICT | false | Overrides audit.strict (fail closed on a dropped audit event) |
GUARDAI_ALLOW_EPHEMERAL_AUDIT | (unset) | Truthy sets audit.allow_ephemeral (waive the non-dev durable, strict audit requirement) |
GUARDAI_IDEMPOTENCY_BACKEND | memory | Overrides idempotency.backend (memory | redis) |
GUARDAI_IDEMPOTENCY_TTL_SECONDS | 86400 | Overrides idempotency.ttl_seconds |
GUARDAI_IDEMPOTENCY_IN_PROGRESS_TTL_SECONDS | 60 | Overrides idempotency.in_progress_ttl_seconds |
GUARDAI_DETECTOR_API_KEY | (none) | Shared secret for the server-to-detector hop (X-Detector-API-Key); set on both sides |
GUARDAI_CORS_ORIGINS | (none) | Comma-separated CORS allowlist → server.allowed_origins |
GUARDAI_TLS_CERT | (none) | Overrides server.tls.cert_path |
GUARDAI_TLS_KEY | (none) | Overrides server.tls.key_path |
GUARDAI_MAX_BATCH_SIZE | 100 | limits.max_batch_size override (tighten-only) |
GUARDAI_STREAM_CHUNK_BYTES | 500 | limits.stream_chunk_bytes override (tighten-only) |
GUARDAI_STREAM_BUFFER_BYTES | 1024 | limits.stream_buffer_bytes override (tighten-only) |
GUARDAI_MAX_BLOCKING_OPS | host CPUs | limits.max_blocking_ops override (1..=256) |
GUARDAI_OCR_TIMEOUT_SECS | 30 | limits.ocr_timeout_secs override (1..=600) |
GUARDAI_OCR_LANGUAGE_PACKS | (none) | OCR language overrides, CSV iso=tesseract (e.g. hi=hin,el=ell,zh-tw=chi_tra). Extends the built-in 12-language map; any installed tessdata language also works by sending its Tesseract code (hin, chi_tra). Requires the matching Tesseract language pack installed. |
GUARDAI_PDF_EXTRACT_MEMORY_MIB | 256 | limits.pdf_extract_memory_mib override (1..=256, lower-only) |
GUARDAI_PDF_EXTRACT_TIMEOUT_SECS | 10 | limits.pdf_extract_timeout_secs override (1..=10, lower-only) |
RUST_LOG | guardai_server=info | Log level filter |
NER_BACKEND | gliner | Python detector engine: gliner, spacy, none |
GUARDAI_DETECTOR_VOCAB | (none) | Proxy: JSON detector.vocab overlay (context_words, currency_codes, extra_patterns) |
GUARDAI_RESTORE_TEMPLATES | (none) | Proxy: JSON restore-template overrides (honorifics, abstract labels) |
GUARDAI_OUTPUT_SCAN_NER | false | Proxy: when truthy (1/true/yes/on) and mode is both/advanced, run NER on output too |
GUARDAI_LANGUAGE_PACKS | (none) | Python detector: path to a JSON file that adds, extends, or overrides (_mode: replace) language enrichment for any language from data. See Language packs. |
GLINER_MODEL | urchade/gliner_medium-v2.1 | Python detector: GLiNER model identifier |
GLINER_LABELS | (built-in) | Python detector: JSON array of extra GLiNER labels (additive to built-ins) |
GLINER_LABEL_MAP | (built-in) | Python detector: JSON object mapping each extra label to an OGuardAI entity type |
GLINER_CONFIDENCE_THRESHOLD | 0.4 | Python detector: GLiNER model confidence threshold in [0,1]. Spans the model scores below this are not returned, so policy never sees them. Lower it for a high-recall (more fail-closed) deployment, accepting more low-confidence candidates and over-masking; an out-of-range value aborts startup |
GLINER_LABELSandGLINER_LABEL_MAPset the deployment-wide default label set. A single policy can also request its own semantic NER labels withdetection.ner_labels/detection.ner_label_mapin its YAML (additive, fail-closed); see the policy authoring guide.
detection.required_forfails a request closed when the required NER backend is unavailable or degraded, and the Rust-side confidence floors exempt required types after detection. It does not override the GLiNER model boundary above: a span the model scores belowGLINER_CONFIDENCE_THRESHOLDis never delivered to the policy. A high-recall deployment lowers that threshold.
GUARDAI_SESSION_SECRET
This is the only required environment variable. It has a 16 character minimum and is used for AES-256-GCM encryption of session state blobs. Generate a strong one with openssl rand -base64 32. All server instances in an HA deployment must share the same secret.
# Generate a suitable secret
openssl rand -base64 32RUST_LOG
Controls log verbosity using the tracing filter syntax:
# Info level for server, debug for session handling
RUST_LOG=guardai_server=info,guardai_session=debug
# Trace everything (very verbose, development only)
RUST_LOG=trace
# Include HTTP request/response logging
RUST_LOG=guardai_server=info,tower_http=infoExample: Minimal Production Config
server:
host: "0.0.0.0"
port: 3000
session:
backend: sealed
ttl_seconds: 3600
policy:
default: default
directory: /etc/guardai/policies
auth:
mode: api_key # requires at least one API key (env or auth.api_keys)
audit:
backend: file # non-dev requires the durable, strict trail
strict: true
file:
path: /var/log/guardai/audit.log
detector:
advanced_url: ""GUARDAI_SESSION_SECRET=$(openssl rand -base64 32)
GUARDAI_API_KEY_1=$(openssl rand -hex 32)
RUST_LOG=guardai_server=info,tower_http=infoExample: Full Stack with NER
server:
host: "0.0.0.0"
port: 3000
session:
backend: sealed
ttl_seconds: 7200
policy:
default: default
directory: /etc/guardai/policies
auth:
mode: api_key # requires at least one API key (env or auth.api_keys)
audit:
backend: file # non-dev requires the durable, strict trail
strict: true
file:
path: /var/log/guardai/audit.log
transform:
context_strategy: full
max_context_tokens: 4096
detector:
advanced_url: http://detector:9090GUARDAI_SESSION_SECRET=$(openssl rand -base64 32)
GUARDAI_API_KEY_1=$(openssl rand -hex 32)
GUARDAI_DETECTOR_API_KEY=$(openssl rand -hex 32) # same value on the detector service
RUST_LOG=guardai_server=info,tower_http=infoNote: For a shared server-side session store across replicas, set
session.backend: rediswithGUARDAI_REDIS_URLplusreplay_backend: redisandrevocation_backend: redis. A redis session or replay backend advertises a multi-replica topology, so startup fails closed unless revocation is the shared (redis) authority too. Each session is encrypted at rest (AES-256-GCM). The defaultsealedbackend is stateless and also works for HA, but a multi-replica sealed deployment still needsrevocation_backend: redis(advertise it withhigh_availability: true).