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"
| Option | Description | Default |
|---|---|---|
bind | Listen address for the serve subcommand. vultrino web (the HTTP JSON API + admin UI) defaults to 127.0.0.1:7879 and the --bind flag overrides per process. | 127.0.0.1:7878 |
mode | Deployment mode: server sets require_auth = true on the in-process default (local otherwise). | local |
The JSON API and admin UI are served by
vultrino webon7879, not by this[server].bind.vultrino serve(which this key configures) no longer starts an API server on its own — see the CLI reference.
Storage Section
[storage]
backend = "file" # Storage backend: "file", "keychain", or "vault"
[storage.file]
path = "~/.local/share/vultrino/credentials.enc"
| Option | Description | Default |
|---|---|---|
backend | Storage backend type | file |
path | Path to encrypted credentials file | OS-specific |
Logging Section
[logging]
level = "info" # Log level: error, warn, info, debug, trace
# audit_file = "~/.local/share/vultrino/audit.log" # Optional audit log
| Option | Description | Default |
|---|---|---|
level | Logging verbosity | info |
audit_file | Path to audit log (optional) | disabled |
MCP Section
[mcp]
enabled = true
transport = "stdio" # "stdio" or "http"
| Option | Description | Default |
|---|---|---|
enabled | Enable MCP server | true |
transport | Transport method | stdio |
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)
| Option | Description | Default |
|---|---|---|
default_action | Decision for a credential matched by no policy: deny or allow | deny |
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 symmetricallow+ 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:
- 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 requestsAccept-Encoding: identity, but a server may compress anyway) — can still leak it. Use ablockrule 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. - 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 scrubbed
from the body (on top of the always-on redaction).
Downstream credentials. Blocking/redacting prevents an agent from reading a downstream secret out of a response. Deleting an OAuth2 credential that carries a
revocation_urlmetadata key now propagates the revoke to the provider (R5/V7): Vultrino calls the RFC 7009 revocation endpoint for the credential's issued access and refresh tokens before removing it locally, so an already-issued downstream secret is actively revoked rather than left to expire, and acredential.revokedevent is emitted to the signed outbox. Set it withvultrino 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 acredential.rotatedevent.
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 = 604800 # replay window, default 7 days
- Ordered + monotonic. Every event gets a process-global, gap-free
sequence. Events for the samesubject(e.g. an approval id) are delivered in order. - 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 bothurlandhmac_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>returns the next events — each as{ "body": …, "signature": "sha256=…" }, the same body a push carries plus its signature — with no gaps and no dupes, within the retention window. - Dead-letter queue. An event that fails
max_attemptsdeliveries is parked (GET /api/v1/events/dead) and re-queued withPOST /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.
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.
[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
| Variable | Description |
|---|---|
VULTRINO_PASSWORD | Storage encryption password (avoids prompts) |
VULTRINO_CONFIG | Path to config file |
RUST_LOG | Override log level (e.g., vultrino=debug) |
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.