Configuration

Vultrino uses a TOML configuration file located at:

  • macOS: ~/Library/Application Support/vultrino/config.toml
  • Linux: ~/.config/vultrino/config.toml
  • Windows: %APPDATA%\vultrino\config.toml

Default Configuration

# Vultrino Configuration

[server]
bind = "127.0.0.1:7878"
mode = "local"

[storage]
backend = "file"

[storage.file]
path = "~/.local/share/vultrino/credentials.enc"

[logging]
level = "info"
# audit_file = "~/.local/share/vultrino/audit.log"

[mcp]
enabled = true
transport = "stdio"

Configuration Options

Server Section

[server]
bind = "127.0.0.1:7878"  # legacy `serve` default; see note below
mode = "local"            # "local" or "server"
OptionDescriptionDefault
bindParsed but not used by any command. The listen address is set with vultrino web --bind (default 127.0.0.1:7879).127.0.0.1:7878
modeDeployment mode: server sets require_auth = true on the in-process default (local otherwise).local

The JSON API and admin UI are served by vultrino web on 7879, not by this [server].bind. vultrino serve without --mcp exits with an error that points you to vultrino web (see the CLI reference).

Storage Section

[storage]
backend = "file"  # only "file" is implemented; "keychain" and "vault" parse but fail with "not yet implemented" when storage is opened

[storage.file]
path = "~/.local/share/vultrino/credentials.enc"
OptionDescriptionDefault
backendStorage backend typefile
pathPath to encrypted credentials fileOS-specific

Logging Section

[logging]
level = "info"  # parsed but not used; set verbosity with -v / -vv or RUST_LOG
# audit_file = "~/.local/share/vultrino/audit.log"  # parsed but not used: no audit log is written
OptionDescriptionDefault
levelParsed but not used. Set verbosity with -v / -vv or RUST_LOG.info
audit_fileParsed but not used: no audit log file is written. Credential use and admin changes are recorded as events in the event outbox (GET /api/v1/events) and in structured logs.none

MCP Section

[mcp]
enabled = true
transport = "stdio"  # "stdio" or "http"

The [mcp] keys are parsed but not used: vultrino mcp always serves MCP over stdio, and vultrino web always serves the networked /mcp route.

OptionDescriptionDefault
enabledParsed but not usedtrue
transportParsed but not usedstdio

Enforcement Section

Controls what the policy engine decides for a credential that matches no policy at all.

[enforcement]
default_action = "deny"  # "deny" (fail-closed, default) or "allow" (fail-open)
OptionDescriptionDefault
default_actionDecision for a credential matched by no policy: deny or allowdeny

With deny (the default, and the recommended posture for shared/server deployments), an un-policied credential is denied with a distinct no_policy reason — closing the historical fail-open gap. Use allow for the legacy behavior where an un-policied credential is permitted. If the section is omitted the built-in default is deny. The config produced by vultrino init also ships with deny and prints a reminder that you must add an allow policy (or switch to allow) before credentials will work.

When default_action = "deny" and no policies are configured, every credential is denied. Vultrino logs a loud warning at startup in this case (and the symmetric allow + no-policies fail-open case is warned about too).

Upgrading (breaking change)

Before this change the engine was fail-open: a credential matching no policy was allowed. It is now fail-closed by default. A config that has no [enforcement] section will start denying un-policied credentials after upgrade. To preserve the pre-upgrade behavior, add:

[enforcement]
default_action = "allow"

Otherwise, add allow policies for the credentials your agents legitimately use.

Spend Extractors

For SpendCap policies (V3), Vultrino needs to know where the amount lives in the request body. Each extractor matches an action + credential and reads the amount (an integer in minor units, e.g. cents) from a JSON pointer, plus an asset (literal or a second pointer).

[[spend_extractors]]
action_pattern = "http.request"     # glob over plugin.action
credential_pattern = "stripe-*"     # glob over credential alias
amount_pointer = "/body/amount"     # JSON pointer to the integer amount
asset = "usd"                        # literal asset...
# asset_pointer = "/body/currency"  # ...or read it from the body

If a SpendCap policy applies to a credential but no extractor yields an amount (missing extractor or unparseable body), the request is denied (fail-closed) and a spend_unparseable warning is logged.

Egress Controls

Vultrino keeps proxied responses from carrying secrets back to the agent (V7), applied at the execution seam for every plugin:

  1. Always-on secret-material redaction. If an endpoint reflects the credential's own injected secret in its response (a header-echoing reflector, an open redirect, etc.), the secret — and its common re-encoded forms (percent-encoded, JSON-escaped) — is scrubbed from the body and headers and replaced with the constant [REDACTED] marker before the response is returned. This is not configurable. It is defense-in-depth, not absolute: an endpoint that transforms the secret (base64, hashing, splitting it) — or returns a compressed body (the http plugin requests Accept-Encoding: identity, but a server may compress anyway) — can still leak it. Use a block rule for endpoints you don't trust. Secrets shorter than 5 bytes are not scrubbed (too little entropy to match safely); a warning is logged when such a credential is created.
  2. Egress classification. For endpoints whose response is itself a secondary secret (an STS/login/secret-read endpoint), configure [[egress]] rules:
[[egress]]
credential_pattern = "sts-*"        # glob over credential alias
action_pattern = "http.request"     # glob over plugin.action (default "*")
block = true                         # withhold the body + headers entirely

[[egress]]
credential_pattern = "secrets-api-*"
redact_patterns = ['"token":\s*"[^"]+"', "AKIA[0-9A-Z]{16}"]  # extra regexes to redact

The first matching rule applies. block = true replaces the body with a marker and drops the headers; otherwise any redact_patterns (regexes) are replaced with [REDACTED:egress] in the body and in header values (on top of the always-on redaction). A body that is not UTF-8 is not scanned by redact_patterns (a warning is logged), so use block for such endpoints. A rule with neither block = true nor redact_patterns is rejected at load.

Downstream credentials. Blocking/redacting prevents an agent from reading a downstream secret out of a response. Deleting an OAuth2 credential that carries a revocation_url metadata key also attempts to revoke its tokens at the provider (R5/V7): Vultrino calls the RFC 7009 revocation endpoint for the credential's issued access and refresh tokens before removing it locally. This is best-effort: a failed call is only logged and does not stop the delete, so a token the provider did not revoke stays valid until it expires. A credential.revoked event is emitted to the signed outbox only when at least one token was revoked. Other credential types are not revoked at the provider. Set it with vultrino meta set <oauth-cred> revocation_url https://idp/oauth/revoke (HTTPS required). Prefer credential types that mint short-lived, revocable downstream credentials (OAuth2 client-credentials, STS, SVIDs) so a revoke maps to a real resource-side revoke. OAuth2 in-path token rotation also emits a credential.rotated event.

Action Labels

Map a govder business verb to a canonical plugin.action (V8), so use-token scopes and the approval/audit trail can speak in business terms while vultrino executes the underlying plugin action. (Policy rules match on URL/method/credential/principal/spend — not on the action label — so the verb is a scoping and audit concept, not a policy-condition one.)

[[action_labels]]
label = "payments.refund"   # what govder / a token scopes against
action = "http.request"     # the canonical plugin.action vultrino runs

A request (or use-token action_scope) may then use the label payments.refund; it resolves to http.request for execution, the use-token scope is satisfied by either the label or the canonical action, and the approver sees the business verb in the approval. The typed /api/v1/execute endpoint also accepts an optional action field (default http.request) so it is no longer hardwired.

Event Outbox (V9)

Vultrino records security-relevant events to a durable, ordered, replayable, signed outbox: approval requested/approved/denied/escalated/expired, agent.halted, policy.changed, credential.rotated, credential.revoked (a downstream revoke propagated to the provider on delete), policy.observed_denial (an observe-only tenant's un-enforced denial), and policy.denied (an enforce-mode denial — a DETECT signal whose created_at is a per-incident detected_at that pairs, on the same subject, with the agent.halted contained_at for an MTTD/MTTC measurement). Configure push delivery with [outbox]:

[outbox]
url = "https://govder.example.com/vultrino/events"  # delivery endpoint
hmac_secret = "shared-signing-secret"                # required to push (deliveries are signed)
max_attempts = 8                                      # retries before dead-lettering (default 8)
retention_secs = 172800                               # replay window, default 2 days
max_retained_events = 2000                            # hard cap on retained events (100 to 100000, default 2000)
  • Ordered + monotonic. Every stored event gets a process-global, increasing sequence. Events for the same subject (e.g. an approval id) are delivered in order. An event whose append failed (disk full, I/O error) gets no sequence and is missing from the feed; a later meter.lost marker announces the loss.
  • Signed. Each delivery carries Govder-Signature: sha256=<hex> = HMAC-SHA256(hmac_secret, body). A consumer recomputes it over the raw body to verify authenticity. Enabling the outbox requires both url and hmac_secret (an unsigned/undeliverable outbox is rejected at load).
  • Exactly-once-ish delivery across processes. Each event is atomically claimed (leased) under the vault lock before it is POSTed, so the web and MCP processes can't both deliver it; a failed delivery backs off (the lease holds it off the retry queue) before re-attempting, and a crashed deliverer's lease is reclaimed once stale.
  • Replayable. A consumer that drops offline replays from its last-seen sequence: GET /api/v1/events?after=<cursor> (global admin API key; a tenant-scoped key is refused) returns the next events, each as { "body": … } (the same body a push carries) plus "signature": "sha256=…" when hmac_secret is configured. Replay is not proof of completeness: a poller that falls behind retention or the max_retained_events cap sees a sequence hole, and a non-zero meter_gap_pending in the response means appends failed that no meter.lost marker has announced yet. Treat either as lost events, not as caught up.
  • Dead-letter queue. An event that fails max_attempts deliveries is parked (GET /api/v1/events/dead) and re-queued with POST /api/v1/events/{sequence}/replay — it stops blocking its subject.
  • Events are appended even when push is unconfigured (still replayable via the API). GC prunes the oldest contiguous prefix past retention_secs (keeping the retained window gap-free); the window is the replay + dead-letter-resolution SLA. A hard cap, max_retained_events, also prunes the oldest events beyond it whatever their delivery state.

Inbound Workload Identity (V10)

Resolve the principal vultrino evaluates from an inbound SPIFFE SVID or OIDC claims document instead of only the static vk_/vut_ id. A request carrying the configured header (an already transport-verified document: terminate mTLS / verify the token at the edge) has its principal resolved before policy evaluation, but only when it also carries <header>-signature, an HMAC-SHA-256 the edge computes with the shared key in VULTRINO_IDENTITY_SECRET (or VULTRINO_IDENTITY_SECRET_FILE) over the document, an expiry at most 300 s ahead and the SHA-256 of the request's bearer. A missing, wrong, expired or other-bearer signature makes Vultrino ignore the document (the static principal stands). With [identity] set, vultrino web refuses to start if that key is missing or shorter than 32 bytes.

[identity]
kind = "spiffe"                 # spiffe | oidc (the wireable resolvers)
header = "x-spiffe-verified"    # inbound header carrying the verified document
allowed = ["example.org"]       # SPIFFE trust domains (or OIDC issuers); empty = any

The resolved subject becomes the Principal.id a policy principal_pattern matches (and the SoD owner from an OIDC email/preferred_username). A malformed/untrusted document is ignored (the static principal stands). See Workload Identity.

Environment Variables

VariableDescription
VULTRINO_PASSWORDStorage encryption password (avoids prompts)
VULTRINO_POLICY_HASH_SECRETRequired by vultrino web, which refuses to start without it. Use a random value and keep it the same across restarts.
RUST_LOGOverride log level (e.g., vultrino=debug)

There is no VULTRINO_CONFIG variable. Pass the global --config <path> flag; otherwise the OS default path above is used. vultrino init and the web UI's admin.json always use the OS default config directory, whatever --config says. A config at the default path that fails to parse is ignored and the built-in defaults are used instead (a file passed with --config that fails to parse is an error).

Policy Configuration

Policies control which requests are allowed for each credential:

[[policies]]
name = "github-readonly"
credential_pattern = "github-*"  # Glob pattern for credential aliases
default_action = "deny"

[[policies.rules]]
condition = { url_match = "https://api.github.com/*" }
action = "allow"

[[policies.rules]]
condition = { method_match = ["POST", "PUT", "DELETE"] }
action = "deny"

See Policy Configuration for detailed policy options.

Using a Custom Config File

vultrino --config /path/to/config.toml list

Regenerating Configuration

To reset to defaults:

vultrino init --force

Warning: This will overwrite your existing configuration and require re-entering admin credentials.