OGuardAI
Getting Started

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: 200

Server

KeyDefaultDescription
server.host0.0.0.0Bind address
server.port3000Listen 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_bytes10485760Maximum request body size in bytes for non-file routes (default 10 MB). File-upload routes use file_upload.max_size_bytes instead
server.tls.enabledfalseTerminate 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.key

Auth

KeyDefaultDescription
auth.modedevAuthentication 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
FieldRequiredDescription
keyyesThe raw key value. Compared with constant-time equality; never serialized back out
identityyesIdentity label recorded in the audit log
namenoHuman-readable label for the key (audit only, does not expose the key)
tenant_idnoTenant scope for multi-tenancy
scopesyesGranted 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_rolenoRole bound to the key. When set, it overrides any request-supplied caller_role, so a caller cannot self-assert a privileged role
caller_purposenoPurpose 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:

VariableEffect
GUARDAI_API_KEY_<NAME>Registers the key. If auth is still dev, the server auto-upgrades to api_key mode
GUARDAI_API_KEY_<NAME>_SCOPESComma-separated scopes. When omitted, the key defaults to transform,rehydrate,detect only
GUARDAI_API_KEY_<NAME>_CALLER_ROLEDeployment-bound caller_role the caller cannot self-assert
GUARDAI_API_KEY_<NAME>_CALLER_PURPOSEDeployment-bound caller_purpose the caller cannot self-assert

Session

KeyDefaultDescription
session.backendsealedSession 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_seconds3600Session 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_sessions100000Maximum 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

KeyDefaultDescription
detector.modebuiltinDetector 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_secs5Timeout 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_nerfalseAlso 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 its detector_mode and warnings fields, so a caller can see that NER did not run.
  • advanced (fail-closed): NER is required, so a sidecar outage fails the request closed with GUARDAI_DETECTION_FAILED rather than silently returning regex-only results. Choose advanced when 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 varDefaultWhat it selects
NER_BACKENDglinergliner, spacy, or none
GLINER_MODELurchade/gliner_medium-v2.1Any 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_STRICTfalseAbort startup if the chosen model cannot load, instead of silently falling back
GUARDAI_NER_MIN_CONFIDENCE0.65NER precision floor for person/company/location; may only be raised (stricter)
GUARDAI_NER_MIN_LENGTH3Minimum 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 request

The 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 events

Language 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, _mode absent 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

KeyDefaultDescription
policy.defaultdefaultDefault policy name applied when no policy is specified in the request
policy.directory./policiesDirectory containing policy YAML files
policy.watchfalseWatch 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)
KeyDescription
policy.integrity.manifestPath to the signed relpath -> HMAC manifest produced by oguardai-server policy sign-dir. Also GUARDAI_POLICY_SIGNATURES_MANIFEST
policy.integrity.secretHMAC signing secret, at least 16 characters. Also GUARDAI_POLICY_SIGNING_KEY
policy.integrity.kidOptional key id recorded in the manifest (informational)

Transform

KeyDefaultDescription
transform.context_strategyfullHow much entity context metadata to include for the LLM
transform.max_context_tokens4096Maximum tokens for context metadata
transform.shadow_modefalseWhen true, includes original (unprotected) text alongside safe text for comparison during rollout. Never enable in production.

Output Protection

KeyDefaultDescription
output_protection.enabledtrueEnable second-pass output scanning for newly generated PII
output_protection.modestrictScanning mode
output_protection.default_actionmaskAction 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 max

The same per-policy form lives in policy YAML:

restore_strategies:
  credit_card:
    kind: reveal_last_n
    n: 4
    mask_char: "*"

Five kinds are available:

kindBehaviorRequiredConstraints
reveal_last_nReveal the last n characters, mask the rest. Whitespace grouping is preserved.nn is 1..64. mask_char is a single character, default *.
reveal_first_nReveal the first n characters (a fixed prefix such as an IBAN country code), mask the rest.nn is 1..64. mask_char optional.
reveal_rangeReveal a len-character window starting at 0-based start (an interior span such as a phone area code), mask the rest.start, lenstart is 0..64, len is 1..64. mask_char optional.
maskMask the whole value.nonenone
bucketParse the value as a number and map it into the first matching ranges band, then reveal only that band's label.rangesranges 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_n masks the whole value when the value is no longer than n. bucket masks when the value does not parse as a number or no range matches.
  • Invalid config aborts startup. A reveal_last_n with n outside 1..64, an empty bucket ranges, 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

KeyDefaultDescription
prompt_security.enabledtrueEnable prompt injection defense
prompt_security.input_scanningtrueScan input for prompt injection attempts
prompt_security.actionstripAction 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

KeyDefaultDescription
file_upload.max_size_bytes52428800Maximum 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.

KeyDefaultMaximumDescription
limits.max_batch_size100100Maximum items per batch request; advertised on /v1/capabilities (tighten-only)
limits.stream_chunk_bytes500500SSE chunk size in bytes when streaming transform/rehydrate output (tighten-only)
limits.stream_buffer_bytes10241024Token-boundary buffer size in bytes per stream (tighten-only)
limits.max_blocking_opshost CPUs256Concurrent heavy blocking ops (OCR/parse/redact); tune to your hardware
limits.ocr_timeout_secs30600Hard timeout for one Tesseract OCR run before it is killed; raise for large scans
limits.pdf_extract_memory_mib256256Address-space cap (MiB) for the bounded PDF extraction subprocess (lower-only)
limits.pdf_extract_timeout_secs1010Wall-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

KeyDefaultDescription
rate_limit.enabledfalseEnable rate limiting
rate_limit.requests_per_second100Requests per second (per tenant when authenticated, per IP otherwise)
rate_limit.burst_size200Maximum burst capacity (token bucket size)
rate_limit.trusted_proxy_depth0Number 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: 100
  • allowed_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 of allowed_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: full

Audit

Where audit events are written. Every data-path request produces a PII-free audit event.

KeyDefaultDescription
audit.backendloglog 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_capacity8192Bounded queue capacity for the file backend's background writer
audit.strictfalseWhen true, a request whose audit event cannot be recorded fails closed with 503 instead of being acknowledged unaudited. Also GUARDAI_AUDIT_STRICT
audit.allow_ephemeralfalseExplicit 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

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.

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.

KeyDefaultDescription
revocation_backendmemoryRevocation store: memory (per-replica, file-backed at revocation_path) or redis (shared cross-replica). Also GUARDAI_REVOCATION_BACKEND
revocation_pathrevocations.datFile the in-memory revocation store persists to
replay_backendmemoryRequest-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:

KeyDefaultDescription
idempotency.backendmemorymemory (per-replica, in-process) or redis (shared cross-replica; uses session.redis_url). Also GUARDAI_IDEMPOTENCY_BACKEND
idempotency.ttl_seconds86400How long a COMPLETED key is retained (replayed) before a retry re-executes. Also GUARDAI_IDEMPOTENCY_TTL_SECONDS
idempotency.in_progress_ttl_seconds60How 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: 60

Environment Variables

Environment variables override configuration file values.

VariableDefaultDescription
GUARDAI_SESSION_SECRET(required)Secret for AES-256-GCM session encryption (16 char minimum)
GUARDAI_HOST0.0.0.0Server bind address
GUARDAI_PORT3000Server listen port
GUARDAI_DETECTOR_URL(none)Python NER service URL
GUARDAI_DETECTOR_TIMEOUT_SECS5detector.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_BACKENDmemoryOverrides revocation_backend (memory | redis)
GUARDAI_REPLAY_BACKENDmemoryOverrides replay_backend (memory | redis)
GUARDAI_AUDIT_BACKENDlogOverrides 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_STRICTfalseOverrides 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_BACKENDmemoryOverrides idempotency.backend (memory | redis)
GUARDAI_IDEMPOTENCY_TTL_SECONDS86400Overrides idempotency.ttl_seconds
GUARDAI_IDEMPOTENCY_IN_PROGRESS_TTL_SECONDS60Overrides 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_SIZE100limits.max_batch_size override (tighten-only)
GUARDAI_STREAM_CHUNK_BYTES500limits.stream_chunk_bytes override (tighten-only)
GUARDAI_STREAM_BUFFER_BYTES1024limits.stream_buffer_bytes override (tighten-only)
GUARDAI_MAX_BLOCKING_OPShost CPUslimits.max_blocking_ops override (1..=256)
GUARDAI_OCR_TIMEOUT_SECS30limits.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_MIB256limits.pdf_extract_memory_mib override (1..=256, lower-only)
GUARDAI_PDF_EXTRACT_TIMEOUT_SECS10limits.pdf_extract_timeout_secs override (1..=10, lower-only)
RUST_LOGguardai_server=infoLog level filter
NER_BACKENDglinerPython 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_NERfalseProxy: 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_MODELurchade/gliner_medium-v2.1Python 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_THRESHOLD0.4Python 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_LABELS and GLINER_LABEL_MAP set the deployment-wide default label set. A single policy can also request its own semantic NER labels with detection.ner_labels / detection.ner_label_map in its YAML (additive, fail-closed); see the policy authoring guide.

detection.required_for fails 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 below GLINER_CONFIDENCE_THRESHOLD is 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 32

RUST_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=info

Example: 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=info

Example: 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:9090
GUARDAI_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=info

Note: For a shared server-side session store across replicas, set session.backend: redis with GUARDAI_REDIS_URL plus replay_backend: redis and revocation_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 default sealed backend is stateless and also works for HA, but a multi-replica sealed deployment still needs revocation_backend: redis (advertise it with high_availability: true).