Skip to content

Configuration Reference

Secret Guard starts from a TOML file (by default secret-guard.toml; change the path with --config), plus a state file the program writes back automatically (secret-guard.state.toml, the result of WebUI edits). Their division of labor:

File Role Written by
secret-guard.toml Declarative config (the subject of this document); read-only in-process, changes need a restart You, by hand
secret-guard.state.toml WebUI edit results + override decisions over declarative entries; delete to reset The program

An entity can have both a static and a dynamic source; the latter can override the former. Override modes (OverrideMode): Default (dynamic acts as an incremental override), PreferStatic (static wins), Disabled (disable the entry).

The config file may contain only what you need — every section is optional; omitted entries are empty and numeric values fall back to the defaults in the tables below.

[[providers]]
id = "openai-main"
kind = "direct"
api_key = "sk-your-upstream-key"
[[providers.endpoints]]
protocol = "openai"
base_url = "https://api.openai.com"
[[secrets.entries]]
id = "my-github-token"
value = "ghp_0123456789abcdefghijklmnopqrstuvwxyz"

[server] — Listening and Forwarding (changes need a restart)

Section titled “[server] — Listening and Forwarding (changes need a restart)”
Field Type Default Description
host string "127.0.0.1" Listen address. Defaults to loopback only, to avoid accidental exposure to the LAN.
port u16 8787 Listen port. Can also be overridden via --port / the SG_PORT environment variable.
records_capacity usize 1024 Upper bound of forward records kept in memory (evicted FIFO beyond that).
upstream_connect_timeout_secs u64 15 TCP+TLS handshake timeout towards the upstream (seconds). 0 = unlimited.
upstream_response_header_timeout_secs u64 60 Timeout waiting for upstream response headers (seconds), streaming tier: only applies to requests with an explicit stream: true (streaming response headers return once the first token is generated; 60s covers large-context prefill). Times out with a 504. 0 = unlimited.
upstream_nonstream_response_header_timeout_secs u64 300 Timeout waiting for upstream response headers (seconds), non-streaming tier: applies to all other requests (a missing stream field counts as non-streaming). Non-streaming response headers only arrive after the entire response is generated — with large contexts (tens of k tokens) total time easily exceeds 60s, hence the 300s default. Times out with a 504. 0 = unlimited.
upstream_stream_idle_timeout_secs u64 120 Maximum idle time between two chunks of a streaming response (seconds). 0 = unlimited.

[redact] — Redaction Behavior (changes need a restart)

Section titled “[redact] — Redaction Behavior (changes need a restart)”
Field Type Default Description
global_mock_prefix string "" A unified prefix for all Auto-mode Mocks (e.g. "sgm_"), making them easy to spot in logs / the WebUI. Once set, Secret values may not contain the prefix.
on_probe_exhausted "fail_open" | "fail_closed" "fail_open" Strategy when Mock candidate probing is exhausted (extremely rare; requires adversarial construction): fail_open skips that Secret and forwards as-is; fail_closed rejects the entire request (503) — fail rather than leak.

[usage] — Model Usage Stats (changes need a restart)

Section titled “[usage] — Model Usage Stats (changes need a restart)”

Token usage data comes 100% from the usage field echoed by upstream responses (zero local estimation); costs are estimated against the models.dev price table (always an estimate; models without pricing show “—”). Details are persisted as an append-only <stem>.usage.jsonl next to the config file (one line per request).

Field Type Default Description
enabled bool true Master switch; false = no collection, no persistence (zero overhead).
retention_days u32 90 Detail retention in days; 0 = forever. Expired rows are cleaned at startup.
pricing_url string models.dev api.json Pricing data source; can point at a self-hosted mirror.
pricing_refresh_secs u64 86400 Price table refresh interval (seconds).
pricing_override model → price table empty Custom model pricing (takes precedence over models.dev); see the example below.

Pricing override example ($ / 1M tokens; cache_read / cache_write may be omitted — they fall back to the input price / 1.25×input):

[usage.pricing_override."my-relay/gpt-fork"]
input = 0.5
output = 2.0

Known limitation: OpenAI streaming requests only get usage echoed by the upstream when the client sets stream_options.include_usage = true — such requests appear in the Usage page with a request count only and no token stats (the page shows a notice).

[[providers]] — Upstream LLM Endpoints (flat array)

Section titled “[[providers]] — Upstream LLM Endpoints (flat array)”

Each Provider is one of two constructions: Direct (a real upstream endpoint) or Router (a routing endpoint that dispatches by the request’s model). The construction is discriminated by the kind field ("direct" / "router", required — missing it fails fast at startup), and the fields of the chosen construction live in the same [[providers]] table. Fields that don’t match the construction are silently ignored: base_url/api_key on a Router entry, or routes on a Direct entry, have no effect — don’t rely on that; treat kind as the source of truth.

Shared fields (either construction):

Field Type Default Description
id string (required) Unique identifier. 1..=64 chars, starts with a letter/digit, only letters, digits, _, -. Appears in forwarding URLs (/o/<id>/...).
kind string (required) Construction discriminator: "direct" (direct upstream) or "router" (routing).
enabled bool true When false, forwarding to this Provider returns 503.
name string — Optional human-readable name (WebUI display only).

Direct construction (kind = "direct"):

Field Type Default Description
protocol string (required) Upstream protocol: openai / anthropic / openairesponses (OpenAI Responses API); gemini / ollama are experimental pass-through (no codec — Secret Redact does not apply, and requests on those protocols are rejected by default when secrets are configured; see the protocol support matrix).
base_url string (required) Upstream base URL. Must start with http:// or https:// and not end with / (Secret Guard appends the path). E.g. https://api.openai.com.
api_key string "" Upstream API key in plaintext. Mutually exclusive with api_key_file (setting both fails at startup).
api_key_file path — Read the API key from a file (suits external injection like sops-nix / systemd LoadCredential, keeping the toml itself free of secrets). Read per request; an unreadable file is treated as an empty key with a WARN. Leading/trailing whitespace is trimmed.

Router construction (kind = "router") — no protocol / base_url / api_key fields:

A Router doesn’t forward by itself; it matches the request’s model against routes and resolves the chain down to a Direct Provider. The ingress protocol is decided by the request URL and the egress protocol by the chain’s tail entity (cross-protocol translation happens automatically when they differ) — hence the Router has no protocol field (leftovers are silently ignored).

Routes live in the [[providers.routes]] sub-table array (at least one; an empty table fails at startup):

Field Type Default Description
model_pattern string (required) Model-name wildcard: only * is a metacharacter (matches any string, including empty); all other characters are literal, case-sensitive. "*" matches everything. Non-empty, ≤64 chars.
target string (required) Target Provider id; may point at another Router (resolved down the chain to a real entity). A dangling target (nonexistent) is accepted at write time and 503s per request. An enabled self-loop fails at startup (disabled routes don’t count as cycle-check edges); cross-entry cycles are rejected at WebUI write time (cycles introduced by hand-editing config return 503 per request, without hanging).
upstream_model string — (passthrough) Outbound model rewrite: the top-level model field of matching request bodies is replaced with this value. Omit to pass the client’s model through. Multi-hop rewrites along the chain are pipeline-semantics: the rewritten value participates in the next Router’s matching; the later hop wins. Non-empty / not only whitespace / ≤128 chars (clear it by omitting the field). Cost: while a rewrite is in effect the request gives up byte-passthrough (upstream prefix-cache invalidated); Gemini/Ollama have no codec to rewrite: WARN + verbatim passthrough.
priority i64 — (disabled) Priority; higher wins. Omitted (null) = the route is disabled (excluded from matching and from cycle-check edges). Ties are broken by order of appearance in the list.
[[providers]]
id = "anthropic-main"
kind = "direct"
api_key_file = "/run/credentials/anthropic.key" # either this or api_key; also entry-level, before [[providers.endpoints]]
[[providers.endpoints]]
protocol = "anthropic"
base_url = "https://api.anthropic.com"

Router Endpoints (routes) — Forwarding by Request Model

Section titled “Router Endpoints (routes) — Forwarding by Request Model”

Clients connect to a fixed router endpoint URL (e.g. /o/my-model/v1/chat/completions); which upstream actually serves the request is decided by the model name in the request body — dispatch extracts the top-level model after the body is collected and matches it against the routes:

[[providers]]
id = "openai-main"
kind = "direct"
api_key = "sk-..."
[[providers.endpoints]]
protocol = "openai"
base_url = "https://api.openai.com"
[[providers]]
id = "anthropic-main"
kind = "direct"
api_key_file = "/run/credentials/anthropic.key"
[[providers.endpoints]]
protocol = "anthropic"
base_url = "https://api.anthropic.com"
[[providers]]
id = "my-model"
kind = "router"
[[providers.routes]]
model_pattern = "claude-*" # claude-family models go to Anthropic, rewritten to a pinned version
target = "anthropic-main"
upstream_model = "claude-sonnet-4"
priority = 100
[[providers.routes]]
model_pattern = "*" # fallback: everything else (incl. requests without a model) goes to OpenAI
target = "openai-main"
priority = 0

Semantics in brief:

  • Route selection: among enabled routes (priority non-null) whose model_pattern matches the request model, the one with the highest priority wins; ties are broken by order of appearance. The request model is the JSON top-level string model field — non-JSON / missing / non-string / empty body is treated as "" (only "*"-style patterns can match it).
  • Model rewrite is a pipeline: a hit route’s upstream_model rewrite takes effect immediately — the next Router in the chain matches against the rewritten model; with multi-hop rewrites the later hop wins. This is how you switch between model families like gpt-4o and claude.
  • Switches affect new requests only (resolved per request, after body collection); in-flight requests complete against their already-resolved target.
  • Error behavior (all 503): no matching route / dangling target on the chain / target enabled = false / cycle. Enabled self-loops fail at startup (disabled routes don’t count as edges); cross-entry cycles are rejected at WebUI write time.
  • When the target’s protocol differs from the URL’s protocol, cross-protocol translation kicks in automatically (non-streaming; streaming cross-protocol returns 501, matching the direct behavior).
  • The way the WebUI overrides a static Router is by submitting the complete routes array (see the WebUI Guide).

[[secrets.entries]] — Secrets to Protect (nested under [secrets])

Section titled “[[secrets.entries]] — Secrets to Protect (nested under [secrets])”
Field Type Default Description
id string (required) Unique identifier; same rules as Provider ids.
name string — Optional human-readable name (WebUI display only).
category string "apikey" Category: password / apikey / token / cookie / privatekey / other.
value string "" Secret plaintext. At least 3 bytes; must not contain global_mock_prefix; must not contain Unicode private-use characters (U+E000..U+F8FF). Mutually exclusive with value_file.
value_file path — Read once at startup (suits external injection like sops-nix); a failed read refuses to start (fail-fast: a missing Secret silently disables protection, so it must surface at deploy time). Leading/trailing whitespace is trimmed.
mock_strategy table (see below) Generation strategy for the stand-in (Mock). Defaults to inferring from the real value: an equal-length, same-charset deterministic random value.
[[secrets.entries]]
id = "my-github-token"
value = "ghp_0123456789abcdefghijklmnopqrstuvwxyz"
[secrets.entries.mock_strategy]
# Dimension one: initial value. Defaults to "auto" (system-generated).
# Fixed stand-in style:
# initial = { kind = "fixed", value = "ghp_FAKEplaceholder" }
# Dimension two: generation strategy (only in effect when initial = "auto").
# Defaults to inferring from the real value.
[secrets.entries.mock_strategy.gen]
prefix = "ghp_mock_" # fixed prefix
length_range = [24, 36] # [min, max] character count (inclusive)
[secrets.entries.mock_strategy.gen.charset]
lowercase = true
digits = true
# other switches: uppercase / underscore / hyphen / other = ['$', '#']

Constraints: both values of length_range must be greater than 0 with min not exceeding max; the prefix length must not exceed min; at least one charset class must be enabled. A fixed stand-in must not equal the real value nor contain long substrings of it (validated at startup).

[auth] — Authentication (off by default = single-user mode)

Section titled “[auth] — Authentication (off by default = single-user mode)”

By default enabled = false: all routes are unauthenticated, suiting personal local use. Enable OIDC login + API keys for multi-user / LAN exposure:

Field Type Default Description
enabled bool false When true: the WebUI requires OIDC login, and forwarding paths require an API key.
oidc table — OIDC Provider configuration (required when enabled = true).
api_keys array [] Statically preset API keys (can also be issued in the WebUI’s API Keys page).

[auth.oidc] fields:

Field Type Description
issuer_url string OIDC issuer (e.g. https://sso.example.com/realms/main).
client_id string OAuth2 client id.
client_secret_file path Optional; path to the client secret file.
redirect_url string Optional; callback URL. Only scheme/host/port may differ — the path is fixed at /oauth2/callback. Required when the listen address isn’t reachable from the browser (e.g. behind a reverse-proxied domain).

[[auth.api_keys]] fields: label (unique identifier), and key or key_file (either one; semantics as above).

[auth]
enabled = true
[auth.oidc]
issuer_url = "https://sso.example.com/realms/main"
client_id = "secret-guard"
[[auth.api_keys]]
label = "ci-pipeline"
key = "sg_ci_abc123..."

secret-guard run starts the gateway. CLI flags / environment variables take precedence over the config file:

Flag Env var Default Description
--host SG_HOST falls back to [server] host (127.0.0.1) Listen address.
--port SG_PORT falls back to [server] port (8787) Listen port.
-c, --config SG_CONFIG secret-guard.toml Path of the declarative config file.
--state SG_STATE derived from the config path Path of the state file (secret-guard.toml → secret-guard.state.toml).
Symptom Cause and fix
``TOML parse error … [[providers]] missing field kindvalue` The old kind = "virtual" is gone: switch to kind = "router", rewrite the old route_to target as a [[providers.routes]] entry (model_pattern = "*" + target = ...), and the old model_override as the route’s upstream_model field.
Startup WARN unknown field route_to / unknown field model_override These two top-level provider fields are removed (leftover fields warn without effect): rewrite as [[providers.routes]] sub-tables (see the Router construction above).
TOML parse error ... [[secrets]] invalid type: map, expected a sequence Secrets must be written as [[secrets.entries]], not [[secrets]]. A WARN line did you mean [[secrets.entries]]? is printed just before the error.
Startup WARN unknown field ... did you mean ...? A misspelled field. Misspelled optional fields are ignored (WARN only) — “configured in name only”; fix them. A misspelled required field (e.g. protocol) warns and then fails startup with missing field.
base_url must not end with '/' Drop the trailing /; Secret Guard appends the path.
secret value too short (min 3 bytes) Secret values need at least 3 bytes.
key and key_file are mutually exclusive / api_key and api_key_file both set Pick one; delete the other.
provider ... must declare at least one route The Router construction’s routes is empty — an empty-route Router cannot forward anything and is rejected at startup. Configure at least one route (a model_pattern = "*" catch-all works).
Request returns 404 not_found The Provider id in the URL doesn’t exist, or the proto prefix is misspelled (see Quick Start — URL Routing Conventions).
Request returns 503 unavailable The Provider has enabled = false, or the route chain is broken: the message contains router provider '...' has no route matching model '...' (no matching route — check model_pattern and priority) / route target '...' not found (dangling target) / route target '...' is disabled / route cycle detected (a cycle from hand-edited config).

A missing config file still starts the process (empty config + defaults, but nothing to forward to; the startup log carries a WARN).