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: "127.0.0.1" # loopback under dev auth; a real auth mode may bind 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: [] # Only 'url' is exemptible (component-scanned before forwarding)
encoded_content_action: allow # Posture for encoded runs the model emits (base64 that decodes to text, or percent-escape runs)
opaque_payload_action: allow # Posture for base64 runs that decode to non-text binary (encrypted, compressed, image bytes)
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 | 127.0.0.1 | Bind address. Loopback by default; under dev auth a non-loopback host also needs a real auth mode or GUARDAI_ALLOW_DEV_NON_LOOPBACK=1 |
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.admission_queue_timeout_ms | 10000 | How long each heavy-op (document extraction, OCR, or image redaction) admission waits for a permit before the request is refused with 503 GUARDAI_CAPACITY_EXHAUSTED, so a saturated server bounds each such wait instead of hanging. Image redaction runs two heavy ops (OCR then redaction), so its worst-case wait is twice this. Clamped to 600000 (10 minutes) |
server.upload_admission_budget_bytes | (auto) | Total upload-body bytes admitted concurrently across document and image uploads (charged from Content-Length before buffering), so a burst of large uploads cannot exhaust memory. Resident memory is a small proportional multiple, since ingest holds each body in a few forms at once. Unset scales the budget with limits.max_blocking_ops and the file body limit (headroom covers the multiple); a value below the file body limit is raised so one maximum-size upload always fits. Over budget, a request is refused with 503 GUARDAI_CAPACITY_EXHAUSTED within server.admission_queue_timeout_ms |
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: "127.0.0.1" # default; a real auth mode may bind 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; requires jwt.secret, jwt.issuer, and jwt.audience), 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. When set: non-empty, at most 128 bytes, and no whitespace or control characters. A blank or malformed value fails validation at startup |
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 idle time-to-live in seconds: a session expires this long after its last turn, and a continuation refreshes it. Must be between 1 and 315360000 (10 years); the server refuses to start otherwise. |
session.max_lifetime_seconds | 0 | Absolute session lifetime in seconds, anchored at first creation and never extended by a continuation. 0 disables the cap (idle ttl only). When set it must be at least session.ttl_seconds. Recommended in production to bound how long an actively continued session can retain its token map. A continuation past the deadline is refused. |
session.redis_url | (unset) | Redis URL. Used by the redis revocation, replay, and session backends. Set via GUARDAI_REDIS_URL |
session.redis_assume_noeviction | false | Operator attestation that the Redis server uses the noeviction maxmemory policy. When false, the server and proxy probe the policy at startup and refuse to boot against a Redis that evicts keys (eviction would silently drop idempotency claims, replay counters, or revocations). Set true only after verifying the policy out of band, for a managed Redis whose policy cannot be read; it boots with a loud warning. This config field applies to the server; the proxy is configured by environment only, so on the proxy use GUARDAI_REDIS_ASSUME_NOEVICTION, which overrides on both |
session.redis_namespace | (unset) | Optional key namespace for a shared Redis: every key of this deployment moves under guardai:<value>: so deployments sharing one logical database cannot read each other's revocations, replay counters, idempotency results, or session blobs, and one ACL grant ~guardai:<value>:* scopes an environment. Unset keeps the shared guardai:* layout, byte-identical to an un-namespaced deployment. Lowercase letters, digits, and hyphens, at most 32 characters, never a reserved segment (revocation, replay, idem, sess, ratelimit); a non-canonical value fails startup. Set the same value on every replica and the proxy. It is chosen at first deployment; change it only as a full-stop migration (stop every replica and the proxy), never a rolling one, since the idempotency claim space splits during overlap. While stopped, copy the three roots the store must never drop: the old root's revocation:* (no TTL), still-valid replay:*, and still-valid idem:* keys, or revoked values become restorable, replay protection is lost for in-flight sessions, and a completed retry re-executes; session blobs (sess:*) are not copied since a missing blob fails closed as a session-expired error. Set via GUARDAI_REDIS_NAMESPACE |
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.
A session has two independent retention windows. session.ttl_seconds is the idle window, refreshed on every continuation. session.max_lifetime_seconds is the absolute window, anchored at the session's first creation and never extended: once a session is older than this, the next continuation is refused even if the idle window is still open. The default 0 disables the absolute cap, so behavior is unchanged unless you set it. Enable it in production to bound how long an actively continued session can keep its token map alive. When you first enable it, a session sealed by an earlier build carries no clamped absolute deadline, so it can outlive the cap by at most one idle window; it converges on its first continuation under the new build.
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 | follows detector.mode | Also run NER on rehydrated output (output-guard second pass), masking model-generated names/orgs that regex misses. Left unset it follows the mode: both and advanced scan output as well as input, builtin does not. An explicit value always wins in either direction, including false, the latency opt-out. Setting true under builtin has no effect (the server logs a warning at startup) |
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_UNAVAILABLE(503, retriable) rather 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 | true | Abort startup if the chosen model cannot load, instead of silently falling back; set false to allow the gliner -> spacy -> none fallback |
GUARDAI_DETECTOR_MAX_LOADED_MODELS | 32 | Startup ceiling on the distinct NER models the configured routes plan to load; strict aborts, otherwise it warns |
GUARDAI_DETECTOR_MAX_MODEL_RSS_MB | 0 (off) | Optional resident-memory ceiling checked after warmup; strict aborts, otherwise it warns |
GUARDAI_DETECTOR_WARMUP_TIMEOUT_SECONDS | 120 | Budget for warming every configured route at startup; strict aborts, otherwise it warns and keeps warming |
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). At least 16 chars and not a placeholder; refused at startup outside dev |
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.
Ingest wall-clock budget (rag.max_ingest_duration_secs)
Each document ingest (/v1/rag/ingest and every item of /v1/rag/ingest/batch, independently) is
bounded by a per-document wall-clock budget. Past it the ingest is refused 503 GUARDAI_DEADLINE_EXCEEDED with nothing sealed and any in-flight detection cancelled, so a
pathological document cannot pin a request unbounded. The budget is finite and secure by default:
3600 seconds (one hour), tunable from 1 to 86400 (24 hours), with no off switch. A batch item's
budget is independent, so one slow document never consumes another's. A request carrying an
Idempotency-Key is also bounded by the shorter idempotency operation deadline. A trip is load
dependent (the same document can pass on an idle server), so the response is a retriable 503, and any
ingest that finishes within the budget behaves exactly as before.
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. A notification is enqueued only after its audit event is recorded, so on the non-dev
default trail (durable file backend with audit.strict, enforced at startup unless waived) a
delivered notification implies a durable audit record; under audit.allow_ephemeral or dev mode the
trail is not durable, so delivery is best-effort and does not imply a persisted record. 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. On a builtin-only server keep default (it stays loadable). On an NER deployment use default-ner-required (the NER deployment profiles and Helm with detector.enabled set it) so a request fails closed with GUARDAI_DETECTION_UNAVAILABLE (503, retriable) on a sidecar outage instead of returning names raw; pin default to keep the best-effort behaviour |
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, adds a redacted shadow block to the transform response for rollout comparison: a per-process keyed HMAC fingerprint of the input, the original and protected character lengths, and the masked-entity count. It never returns the original text, and protection still runs exactly as normal. A dev-only diagnostic: production validation rejects it as a fatal error |
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 | [] | Types exempt from output scanning. Only url is exemptible (a URL is component-scanned fail-closed before forwarding); any other entry is rejected at startup. To pass a type through use entity_actions: {<type>: allow} (untrusted channels keep the mask floor); to deliver raw values to a sink declare it in GUARDAI_TRUSTED_OUTPUT_CHANNELS |
output_protection.encoded_content_action | allow | Posture for a run the model emits that strict-decodes to plausible text (a base64 payload that decodes to readable UTF-8, or a run of consecutive percent escapes). A verdict on the encoding SHAPE, independent of detection: block refuses the response on the encoding alone, mask replaces the run with [CUSTOM:ENCODED_PAYLOAD], warn logs a count, allow forwards it and leaves any detected value to the decoded-view scan. Values: allow, warn, mask, block |
output_protection.opaque_payload_action | allow | Posture for a base64 run that strict-decodes but is NOT plausible text (encrypted, compressed, or image bytes no textual detector can scan). block refuses a decoded opaque run, mask replaces it with [CUSTOM:OPAQUE_PAYLOAD], warn logs a count, allow forwards it. Known residuals even under block: opaque runs under 24 base64 chars are not recorded, runs under 16 chars and all-hex runs are never classified, adaptively letter-only encodings pass, and an opaque payload nested inside a plausible-text outer encoding is recorded as the outer encoded run, not an opaque run. The guidance is warn, then review, then block; encoded_content_action: block covers the nested case by refusing the plausible outer decode, but no setting of these two knobs is a complete lockdown. Values: allow, warn, mask, block |
When an output NER scanner is installed (detector.mode both/advanced, or GUARDAI_OUTPUT_SCAN_NER=1
on the proxy), the output guard now also runs NER over decoded views, so a person, company, or
location the model emits base64- or percent-encoded is masked exactly as its plaintext form would be.
This is on by default whenever the scanner is installed; the two knobs above add an independent
posture on the encoding shape itself. With no scanner installed and both knobs at allow, encoded
output forwards exactly as before.
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. The
NER-capable modes both and advanced add a NER pass on output unless you set
detector.scan_output_with_ner: false; under builtin there is no such pass. 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 | Shared budget for OCR, parsing, and image redaction. Heavy work waits for capacity. Strict audit has its own separate bound (audit.strict_max_in_flight) |
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) |
limits.max_bulk_revoke_items | 1000 | 1000 | Maximum entities per bulk revocation request; a larger batch is refused 413 before any mutation (tighten-only) |
limits.max_bulk_revoke_bytes | 1048576 | 1048576 | Ceiling on the summed entity-type and value bytes in one bulk revocation request; over budget is refused 413 before any mutation (tighten-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.backend | local | Tenant rate-limit quota backend. local is a per-replica in-process token bucket (the default). redis shares one bucket across replicas so N replicas honor one ceiling (reuses session.redis_url); only the post-auth tenant limiter uses Redis, the pre-auth IP and readyz floors stay in-process. Required to be redis under high_availability, where a per-replica limiter multiplies the configured ceiling by the replica count. Set via GUARDAI_RATE_LIMIT_BACKEND |
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_cidrs | [] | Production mechanism for X-Forwarded-For trust. A list of CIDRs or bare IPs (a bare IP is a /32 or /128) covering your reverse proxies. When non-empty, the client IP is resolved by a rightmost-trusted walk over X-Forwarded-For and trusted_proxy_depth is ignored. |
rate_limit.trusted_proxy_depth | 0 | Legacy positional trust: trust this many rightmost X-Forwarded-For entries. Outside dev mode a depth above 0 is rejected at startup unless trusted_proxy_cidrs is set, because a direct peer could forge the header and dodge the pre-auth rate limit. |
Behind reverse proxies, set trusted_proxy_cidrs to the proxy address range. Every proxy tier in
front of the server must append the address it received the request from (the standard nginx
proxy_add_x_forwarded_for and ALB behavior), and every intermediate proxy address must be inside
trusted_proxy_cidrs, not just the first hop. The walk believes an entry only while its writer is
trusted, treats the first untrusted entry as the client, fails closed to the innermost trusted proxy
on a broken chain, and ignores the header entirely when the TCP peer is not in the list.
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: type_summary
local:
name: local
context_strategy: referenced_only
default:
name: default
context_strategy: fullThese profiles govern the server data path, and only while transform.context_strategy keeps its full
default. On that default, a request that names a provider in its context.provider field receives that
provider's context_strategy from this table, and a named provider with no entry falls to the default
profile. Setting transform.context_strategy to any value other than full makes the server apply that
one value to every request and ignore this table. A request that names no provider uses the
transform.context_strategy value.
The proxy does not consult these profiles. It carries no per-request provider context and forces
context_strategy: none on every egress by design, so a provider reached through the proxy receives no
entity context metadata regardless of the values above.
Audit
Where audit events are written. Every data-path request produces an audit event with no detected values. The protected durable file can retain operator labels such as tenant and policy identifiers; logs and outbound copies fingerprint those free-form fields.
| 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) | Runtime-managed append-only log in a stable, trusted, non-symlink parent. 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. Full audit queues, exhausted strict-audit admission capacity, and unavailable writers all fail closed. Also GUARDAI_AUDIT_STRICT |
audit.strict_max_in_flight | 64 | Maximum strict audit durability waits admitted concurrently, independent of limits.max_blocking_ops. A strict request past the bound fails closed immediately rather than queueing. Range 1 to 128 |
audit.require_certified_filesystem | false | Restrict audit storage to the certified allowlist (ext4, XFS, Btrfs, APFS) on local synchronous mounts. The default accepts any non-volatile filesystem with a startup warning for remote or unverified families; memory-backed filesystems are always refused for the active log and exports. Also GUARDAI_PROXY_AUDIT_REQUIRE_CERTIFIED_FS on the proxy |
audit.strict_ack_timeout_secs | 30 | Bound for each strict writer wait: first for prior work to release the writer, then for this event's own append and fsync after claim. A request can therefore spend up to twice this value in strict audit; range 1 to 3600 seconds |
audit.shutdown_timeout_secs | derived, at least 60 | Durable writer finalization budget. Omit to derive a safe floor from the strict acknowledgement timeout; an explicit value cannot undercut it |
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: true
strict_ack_timeout_secs: 30
shutdown_timeout_secs: 60The live leaf must be a regular file owned by the runtime user, have one link, and end at exact mode
0600. The runtime creates new leaves privately and can narrow a strict permission superset to
0600 without changing the contents. It rejects symlinks, non-regular files, hard-linked live files,
wrong ownership, and incompatible modes. Keep the parent directory stable, private, and under the
same administrative control as the service.
The runtime also qualifies the opened filesystem for durability. 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 naming the fsync
and locking caveat. Memory-backed filesystems (tmpfs, ramfs, hugetlbfs) are always refused for the active log
and exports. Set audit.require_certified_filesystem: true (or GUARDAI_PROXY_AUDIT_REQUIRE_CERTIFIED_FS
on the proxy) to restore the strict allowlist that also refuses non-local, asynchronous, union, or
unknown-permission mounts. A container image layer is normally overlayfs, which now works with a
warning; mount a dedicated volume at the audit directory anyway because container-layer contents do
not outlive the container. A Kubernetes PVC works whenever its storage class presents a durable
filesystem, with a block storage class recommended for the strongest guarantees.
Offline verification and export accept only owned, regular source files at mode 0400 or 0600.
Unsafe sources are rejected without mutation. See
Verifying audit-log integrity
for descriptor binding, archive keys, and export-output handling.
A 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.
Graceful shutdown
The service owns bounded shutdown phases, but its supervisor must allow the cumulative envelope. With strict file audit defaults, the envelope is 180 seconds: 30 seconds for HTTP drain, 75 seconds for request quiescence, 60 seconds for audit finalization, and a 15-second safety margin. The shipped Helm, Compose, and systemd definitions allow that full interval.
| Key | Default | Description |
|---|---|---|
shutdown.http_drain_timeout_secs | 30 | Time allowed for active HTTP connections before forced shutdown; range 1 to 3600 seconds |
shutdown.quiesce_timeout_secs | derived, normally 75 | Time allowed for request producers to quiesce. Strict file audit derives a floor of twice the acknowledgement timeout plus 15 seconds |
If you raise any shutdown or strict acknowledgement budget in a custom deployment, raise the
supervisor grace period by the same cumulative amount. A shorter Kubernetes grace, Compose
stop_grace_period, or systemd TimeoutStopSec can terminate the process before audit custody is
released.
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 |
revocation_provisional_ttl_secs | 3600 | Seconds after which an unfinished distributed revocation claim (redis backend) is folded into the durable revocation on the next connect, never dropped. Keep well above the audit acknowledgement timeout (a smaller value folds a still-live claim, fail-safe; 0 folds every claim immediately). Use the same window on every replica, including the proxy |
revocation_max_persisted | 100000 | Maximum digests the file-backed revocation store holds. A new revoke past it fails closed 422 GUARDAI_REVOCATION_LIMIT_EXCEEDED; an idempotent re-revoke of an existing value always succeeds. Loading never enforces it. Only the memory backend is bounded. Range 1 to 5000000 |
revocation_persist_max_in_flight | 8 | Concurrent revocation persistence writes admitted before a revoke fails closed 503 GUARDAI_REVOCATION_PERSIST_SATURATED, so a full-set fsync cannot park every request worker. Independent of the audit and blocking budgets. Only the memory backend is bounded; a redis backend persists O(1) per revoke and is unbounded. Range 1 to 64 |
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 | 450 | How long an IN-PROGRESS claim is held before it becomes reclaimable, so a request abandoned mid-flight frees its key quickly. Must not exceed ttl_seconds. Also GUARDAI_IDEMPOTENCY_IN_PROGRESS_TTL_SECONDS |
idempotency.max_entries | 100000 | Hard cap on distinct live keys the store retains across all partitions. Enforced by the in-memory backend as a store-wide ceiling; on the Redis backend, aggregate memory is bounded instead by per-key expiry plus the boot-enforced noeviction policy. |
idempotency.max_entries_per_partition | 10000 | Hard cap on distinct live keys any single authorization-context partition may hold, so one caller cannot consume the whole store. Enforced by both backends; the Redis backend tracks it with an admission ledger that also detects an evicted live claim. |
idempotency:
backend: memory
ttl_seconds: 86400
in_progress_ttl_seconds: 450Environment Variables
Environment variables override configuration file values.
| Variable | Default | Description |
|---|---|---|
GUARDAI_SESSION_SECRET | (required) | Secret for AES-256-GCM session encryption. A non-dev server requires at least 32 non-whitespace characters (openssl rand -base64 32); trivially repeated material is rejected at startup. Api keys and the dedicated purpose secrets keep a 16-character floor |
GUARDAI_REVOCATION_SECRET | (none) | Dedicated secret rooting the revocation digest key (16 char minimum). Unset derives the key from the session secret instead. Setting it isolates the revocation store from a leaked or rotated session secret; rotating it is a fail-closed reset (re-issue revocations). |
GUARDAI_CORPUS_SECRET | (none) | Dedicated secret rooting corpus/token identity (16 char minimum). Unset derives the root from the session secret instead. Set it once before the first production corpus, since changing it later requires a re-ingest. |
GUARDAI_AUDIT_SECRET | (none) | Dedicated secret rooting the audit-chain HMAC key (16 char minimum). Unset derives the key from the session secret instead. Before adopting or rotating it, stop the writer and move the current file to a private archive because its records use the previous root. List the old root in audit.retired_secrets for verification only. oguardai-server audit verify and oguardai-server audit export select the writing key while holding one opened source. |
GUARDAI_HOST | 127.0.0.1 | Server bind address (loopback by default under dev auth) |
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_REDIS_NAMESPACE | (none) | Overrides session.redis_namespace: isolates this deployment's keyspace under guardai:<value>: when several deployments share one Redis. Must match on every replica and the proxy |
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_REVOCATION_PROVISIONAL_TTL_SECS | 3600 | Proxy: reclaim window for an abandoned redis revocation claim |
GUARDAI_REPLAY_BACKEND | memory | Overrides replay_backend (memory | redis) |
GUARDAI_RATE_LIMIT_BACKEND | local | Overrides rate_limit.backend (local | redis); required redis under high_availability |
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_SHUTDOWN_HTTP_DRAIN_TIMEOUT_SECS | 30 | Overrides shutdown.http_drain_timeout_secs (1 to 3600 seconds) |
GUARDAI_SHUTDOWN_QUIESCE_TIMEOUT_SECS | derived, normally 75 | Overrides shutdown.quiesce_timeout_secs (1 to 7215 seconds) |
GUARDAI_AUDIT_SHUTDOWN_TIMEOUT_SECS | derived, at least 60 | Overrides audit.shutdown_timeout_secs (1 to 3615 seconds); cannot undercut the strict-audit floor |
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 | 450 | Overrides idempotency.in_progress_ttl_seconds |
GUARDAI_RAG_MAX_INGEST_DURATION_SECS | 3600 | Overrides rag.max_ingest_duration_secs, the per-document ingest wall-clock budget (1..=86400) |
GUARDAI_DETECTOR_API_KEY | (none) | Shared secret for the server-to-detector hop (X-Detector-API-Key); set on both sides |
GUARDAI_LICENSE_FILE | (unset) | Path to a signed licence file, reported in the license block on GET /v1/health. Local Ed25519 verification (no network call) is built but NOT operative in this release, which compiles in no verifying key: a licence present at startup reports malformed, runs the developer edition, and does not abort the start. Unset also runs the developer edition. Nothing is gated by it. A licence file that cannot be read or is oversized still aborts a non-dev start. See Licensing |
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 | Shared blocking-operation budget override (1..=256); strict audit fails closed immediately when full |
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 | warn,oguardai_server=info,guardai_server=info,guardai::audit=info,tower_http=info | Log filter. Setting it replaces the default outright (see below) |
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 a built-in OGuardAI entity type |
GUARDAI_SPACY_LABEL_MAP | (built-in) | Python detector: JSON object mapping a spaCy label (e.g. FAC) to a built-in entity type. Deployment-wide and built-ins-only, same contract and GET /labels validation as the GLiNER map |
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, and their targets must be built-in entity types: the detector advertises its static emit set overGET /labelsat startup, and a runtime that cannot resolve an advertised target refuses to start when NER is required (it warns and falls back to request-time validation when NER is optional). Domain-specific custom types belong in a policy'sdetection.ner_labels/detection.ner_label_map(additive, fail-closed), not in the deployment-wide map; 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. A non-dev server requires at least 32 characters (dev tolerates the shorter default); it 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. Left unset, the server applies
warn,oguardai_server=info,guardai_server=info,guardai::audit=info,tower_http=info. Setting
RUST_LOG replaces that filter outright, so keep three things in whatever you set. The bare warn
floor and the oguardai_server target (the binary, distinct from the guardai_server library)
carry the startup report and the output guard's own block reasons, and a filter naming only
guardai_server mutes both, leaving refused requests with no server-side explanation.
guardai::audit=info is what lets the audit trail past that floor: the default log audit backend
emits every event at INFO on that target, so a filter without the directive leaves the deployment
with no audit trail at all. The durable file backend writes regardless of the log filter.
# The default, with session handling turned up to debug
RUST_LOG=warn,oguardai_server=info,guardai_server=info,guardai::audit=info,tower_http=info,guardai_session=debug
# Trace everything (very verbose, development only)
RUST_LOG=trace
# Quieter: keep the warn floor, the startup and guard diagnostics and the audit trail, drop request logging
RUST_LOG=warn,oguardai_server=info,guardai_server=info,guardai::audit=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=warn,oguardai_server=info,guardai_server=info,guardai::audit=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=warn,oguardai_server=info,guardai_server=info,guardai::audit=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).