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 | Parsed 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 |
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 servewithout--mcpexits with an error that points you tovultrino 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"
| Option | Description | Default |
|---|---|---|
backend | Storage backend type | file |
path | Path to encrypted credentials file | OS-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
| Option | Description | Default |
|---|---|---|
level | Parsed but not used. Set verbosity with -v / -vv or RUST_LOG. | info |
audit_file | Parsed 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.
| Option | Description | Default |
|---|---|---|
enabled | Parsed but not used | true |
transport | Parsed but not used | 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 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_urlmetadata 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. Acredential.revokedevent 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 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 = 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 samesubject(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 latermeter.lostmarker 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 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>(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=…"whenhmac_secretis configured. Replay is not proof of completeness: a poller that falls behind retention or themax_retained_eventscap sees a sequence hole, and a non-zerometer_gap_pendingin the response means appends failed that nometer.lostmarker has announced yet. Treat either as lost events, not as caught up. - 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. 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
| Variable | Description |
|---|---|
VULTRINO_PASSWORD | Storage encryption password (avoids prompts) |
VULTRINO_POLICY_HASH_SECRET | Required by vultrino web, which refuses to start without it. Use a random value and keep it the same across restarts. |
RUST_LOG | Override 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.