Configuration Reference
All VibeWarden configuration lives in vibewarden.yaml at the root of your
project. Every field can also be set via an environment variable — the variable
name is the YAML key path uppercased and prefixed with VIBEWARDEN_, with dots
replaced by underscores. Example: server.port → VIBEWARDEN_SERVER_PORT.
Environment variable substitution in YAML values is supported using ${VAR}
syntax. This is recommended for secrets:
database:
external_url: "postgres://user:${DB_PASSWORD}@db.example.com:5432/vibewarden"
profile
Type: string
Default: dev
Accepted values: dev, tls, prod
Selects the deployment profile. Affects TLS settings, credential validation
strictness, and which generated files are produced.
server
Settings for the VibeWarden HTTP listener.
| Field |
Type |
Default |
Description |
server.host |
string |
127.0.0.1 |
Host/IP to bind to |
server.port |
int |
8443 |
Port to listen on |
server.max_connections |
int |
1000 |
Maximum concurrent inbound connections per listener. 0 disables the cap. Negative values are rejected by vibew validate. |
server.mem_limit |
string |
512MB |
Memory cap for the sidecar container in the generated compose file. "0" or "" disables the cap. Generate-time only. |
server.cpu_limit |
float |
1.0 |
CPU cap for the sidecar container, in cores (0.5 = half a core). 0 disables the cap. Generate-time only. |
server.pids_limit |
int |
200 |
Cap on processes/OS threads the sidecar container may create. 0 disables the cap. Generate-time only. |
server:
host: 0.0.0.0
port: 8443
max_connections: 1000
mem_limit: "512MB"
cpu_limit: 1.0
pids_limit: 200
server.max_connections
Once the cap is reached, new connections are refused (accepted and immediately
closed) until an existing connection ends. Established connections and in-flight
requests are unaffected. This bounds file-descriptor use during a connection
flood so the sidecar degrades instead of dying, taking the app it protects with
it.
Two caveats worth knowing:
- The cap is per listener, not per process. While the port-80 HTTPS redirect
server runs (self-signed and external TLS providers) the process ceiling is
2 × max_connections.
- HTTP/3 is not covered. QUIC arrives over a single UDP socket rather than a
TCP listener, so it cannot cause the descriptor exhaustion this cap exists to
prevent, but it also means this is not a general request-concurrency limit.
For per-client request throttling use the
rate_limit block instead.
See ADR-110.
server.mem_limit, server.cpu_limit, server.pids_limit
These three keys cap the vibewarden sidecar container, so a memory leak or a
runaway loop in the sidecar cannot exhaust the host and take the app it protects
down with it. server.max_connections is the graceful in-process refusal;
these are the backstop for everything it cannot see.
They are generate-time only. vibew generate and vibew bundle render them
as mem_limit, cpus and pids_limit on the vibewarden service of the
generated docker-compose.yml; the running sidecar never reads them, so
changing one requires regenerating and recreating the container, not just a
restart with a new config.
Four things worth knowing:
- Only the sidecar is capped. Your
app container and the stack services
(kratos, postgres, redis, openbao) are untouched — VibeWarden does not
put limits on containers it does not own.
- A disabled cap omits the key. Setting a limit to
0 (or "" for
mem_limit) removes it from the generated file entirely rather than emitting
an explicit 0.
GOMEMLIMIT is derived from mem_limit at 90% of the cap and set on the
sidecar container. Without it a memory cap would make the sidecar more
likely to be OOM-killed, since the Go GC does not see the cgroup ceiling.
Disabling mem_limit drops GOMEMLIMIT too. GOMAXPROCS needs no
counterpart: Go reads the cgroup CPU quota itself.
overrides.compose_file bypasses all of this. Your compose file is copied
verbatim, so no limits are injected; set them yourself if you want them.
mem_limit accepts both spellings you are likely to type: "512MB" and
Docker's "512M", plus "1GB"/"1g" and a plain byte count.
See ADR-111.
health
Settings for the public /_vibewarden/health endpoint.
| Field |
Type |
Default |
Description |
health.expose_version |
bool |
true |
When true, the health JSON includes the running binary version. Set to false to omit it — hardening against version fingerprinting on internet-facing deployments. |
The X-Vibewarden: 1 response header is always emitted regardless of this
setting, so tooling that detects a VibeWarden sidecar should rely on that header
rather than the version field.
health:
expose_version: false # omit version from /_vibewarden/health (hardened)
upstream
Settings for the upstream application being protected.
| Field |
Type |
Default |
Description |
upstream.host |
string |
127.0.0.1 |
Upstream host |
upstream.port |
int |
3000 |
Upstream port |
upstream.health.enabled |
bool |
false |
Enable active upstream health checking |
upstream.health.path |
string |
/health |
HTTP path to probe |
upstream.health.interval |
duration |
10s |
Time between probes |
upstream.health.timeout |
duration |
5s |
Per-probe timeout |
upstream.health.unhealthy_threshold |
int |
3 |
Consecutive failures to mark unhealthy |
upstream.health.healthy_threshold |
int |
2 |
Consecutive successes to mark healthy |
upstream:
host: 127.0.0.1
port: 3000
health:
enabled: true
path: /health
interval: 10s
timeout: 5s
app
Controls how your application is included in the generated Docker Compose file.
When neither build nor image is set, no app service is rendered and
VibeWarden falls back to host.docker.internal.
| Field |
Type |
Default |
Description |
app.build |
string |
"" |
Docker build context path (e.g., .) — used in dev/tls profiles |
app.image |
string |
"" |
Docker image reference (e.g., ghcr.io/org/myapp:latest) — used in prod profile |
app:
build: . # dev workflow: build from source
# image: ghcr.io/org/myapp:latest # prod workflow: use pre-built image
tls
| Field |
Type |
Default |
Description |
tls.enabled |
bool |
true |
Enable TLS |
tls.domain |
string |
"" |
Domain for the TLS certificate. Required for all ACME providers |
tls.provider |
string |
"" |
Certificate provider: letsencrypt (falls back to ZeroSSL when tls.email is set; see ADR-083), zerossl, buypass (explicit opt-in only, upstream currently unhealthy), letsencrypt-staging, self-signed, or external |
tls.cert_path |
string |
"" |
Path to PEM certificate. Required when provider is external |
tls.key_path |
string |
"" |
Path to PEM private key. Required when provider is external |
tls.email |
string |
"" |
ACME account email. Required for ZeroSSL, recommended for Let's Encrypt (cert expiry notifications) |
tls.acme_ca |
string |
"" |
Override ACME directory URL (e.g. ZeroSSL: https://acme.zerossl.com/v2/DV90) |
tls.storage_path |
string |
"" |
Directory for ACME certificate storage. Applies to letsencrypt only |
tls:
enabled: true
provider: letsencrypt
domain: myapp.example.com
auth
auth (top-level)
| Field |
Type |
Default |
Description |
auth.mode |
string |
none |
Auth strategy and single source of truth for whether auth is on. Set to none to disable, or kratos, jwt, or api-key to enable. See ADR-065 |
auth.public_paths |
list |
[] |
Paths that do not require authentication. If a valid session cookie is present, identity headers are still set (optional auth). /_vibewarden/* is always public |
auth.session_cookie_name |
string |
ory_kratos_session |
Kratos session cookie name |
auth.login_url |
string |
/self-service/login/browser |
Redirect for unauthenticated users |
auth.on_kratos_unavailable |
string |
503 |
Behavior when Kratos is unreachable: 503 or allow_public |
auth.identity_schema |
string |
email_password |
Identity schema: email_password, email_only, username_password, social, or a file path |
auth.role_paths |
map |
{} |
Maps role names (user, admin, moderator) to URL path patterns requiring that role. Users whose Kratos role trait does not match receive HTTP 403. Only valid when auth.mode is kratos. When empty, no role enforcement is performed but X-User-Role is still set |
auth.jwt
Used when auth.mode is jwt.
| Field |
Type |
Default |
Description |
auth.jwt.jwks_url |
string |
"" |
Direct JWKS endpoint URL. Takes precedence over issuer_url |
auth.jwt.issuer_url |
string |
"" |
OIDC issuer base URL for auto-discovery |
auth.jwt.issuer |
string |
"" |
Expected iss claim value. Required |
auth.jwt.audience |
string |
"" |
Expected aud claim value. Required |
auth.jwt.claims_to_headers |
map |
see below |
JWT claims injected as upstream request headers |
auth.jwt.allowed_algorithms |
list |
[RS256, ES256] |
Accepted signing algorithms. Never include none or HS256 in production |
auth.jwt.cache_ttl |
duration |
1h |
How long the JWKS is cached locally |
Default claims_to_headers:
claims_to_headers:
sub: X-User-Id
email: X-User-Email
email_verified: X-User-Verified
auth.api_key
Used when auth.mode is api-key.
| Field |
Type |
Default |
Description |
auth.api_key.header |
string |
X-API-Key |
Request header carrying the API key |
auth.api_key.keys |
list |
[] |
Static key entries (see below) |
auth.api_key.openbao_path |
string |
"" |
KV path in OpenBao where key hashes are stored |
auth.api_key.cache_ttl |
duration |
5m |
TTL for keys fetched from OpenBao |
auth.api_key.scope_rules |
list |
[] |
Ordered path+method authorization rules |
Each entry in keys:
| Field |
Type |
Description |
name |
string |
Human-readable label |
hash |
string |
Hex-encoded SHA-256 of the plaintext key |
scopes |
list |
Permission scopes granted to this key |
Each entry in scope_rules:
| Field |
Type |
Description |
path |
string |
Glob pattern matched against the request path |
methods |
list |
HTTP methods this rule applies to (empty = all methods) |
required_scopes |
list |
Scopes the key must possess |
auth.social_providers
List of OAuth2/OIDC social login providers (used with auth.mode: kratos).
| Field |
Type |
Description |
provider |
string |
Provider name: google, github, apple, facebook, microsoft, gitlab, discord, slack, spotify, oidc |
client_id |
string |
OAuth2 client ID |
client_secret |
string |
OAuth2 client secret |
scopes |
list |
OAuth2 scopes to request (optional; provider defaults apply) |
label |
string |
Custom login button label (optional) |
team_id |
string |
Apple Developer Team ID (Apple only) |
key_id |
string |
Apple private key ID (Apple only) |
id |
string |
Unique identifier for OIDC entries |
issuer_url |
string |
OIDC issuer URL (OIDC provider only) |
auth.ui
| Field |
Type |
Default |
Description |
auth.ui.mode |
string |
built-in |
built-in or custom |
auth.ui.app_name |
string |
"" |
Application name on built-in login pages |
auth.ui.logo_url |
string |
"" |
Logo image URL shown above the form on built-in pages |
auth.ui.favicon_url |
string |
"" |
Favicon URL for built-in pages |
auth.ui.custom_css_url |
string |
"" |
Extra stylesheet loaded last on built-in pages |
auth.ui.primary_color |
string |
#7C3AED |
Accent color for built-in pages |
auth.ui.background_color |
string |
#1a1a2e |
Background color for built-in pages |
auth.ui.login_url |
string |
"" |
Custom login page URL (when mode: custom) |
auth.ui.registration_url |
string |
"" |
Custom registration page URL |
auth.ui.settings_url |
string |
"" |
Custom account settings page URL |
auth.ui.recovery_url |
string |
"" |
Custom account recovery page URL |
auth.ui.show_registration |
bool |
true |
Self-service registration: link, built-in page, and Kratos flow |
auth.ui.show_recovery |
bool |
true |
Self-service account recovery: link, built-in page, and Kratos flow |
In built-in mode VibeWarden serves the auth pages itself at
/_vibewarden/login, /_vibewarden/registration, /_vibewarden/recovery,
/_vibewarden/verification and /_vibewarden/settings, and points the
generated kratos/kratos.yml flows at them.
Branding applies to all five pages: app_name prefixes the <title> and is
shown above the form, logo_url renders as an image above it, favicon_url
is linked as the page icon, and custom_css_url is linked after the built-in
styles so its rules win. logo_url, favicon_url and custom_css_url must
each be an absolute http(s) URL or a root-relative path such as
/static/logo.svg; anything else (including javascript: and data: URIs)
is rejected at config validation. Assets served from your own app need a
public_paths entry, otherwise the login page cannot load them while the
visitor is still unauthenticated.
Set show_registration: false for a closed-registration deployment (users are
created by an admin only) and show_recovery: false when you do not offer
self-service password recovery. Each flag drives three things at once: the link
on the built-in login page, the built-in page itself (which then returns 404),
and selfservice.flows.{registration,recovery}.enabled in the generated
kratos/kratos.yml. That keeps the page and Kratos from disagreeing, which used
to leave dead links on the login page.
In custom mode the generated flows point at your auth.ui.*_url values
instead. Absolute URLs are used as-is; a path such as /login is resolved
against your public base URL (tls.domain, else localhost:<server.port>).
The verification and error flows have no key of their own and fall back to
auth.ui.login_url, as does any *_url you leave empty.
kratos
Used when auth.mode is kratos.
| Field |
Type |
Default |
Description |
kratos.public_url |
string |
http://127.0.0.1:4433 |
Kratos public API URL. In the generated Docker Compose stack, use http://kratos:4433 |
kratos.admin_url |
string |
http://127.0.0.1:4434 |
Kratos admin API URL. In the generated Docker Compose stack, use http://kratos:4434 |
kratos.dsn |
string |
"" |
Kratos database DSN (postgres URL) |
kratos.external |
bool |
false |
Connect to a user-managed Kratos instance instead of starting one |
kratos.smtp.host |
string |
localhost |
SMTP server host |
kratos.smtp.port |
int |
1025 |
SMTP server port |
kratos.smtp.from |
string |
no-reply@vibewarden.local |
Sender address for Kratos emails |
rate_limit
| Field |
Type |
Default |
Description |
rate_limit.enabled |
bool |
true |
Enable rate limiting |
rate_limit.store |
string |
memory |
Backing store: memory or redis |
rate_limit.trust_proxy_headers |
bool |
false |
Read X-Forwarded-For for client IP. Enable only behind a trusted proxy |
rate_limit.exempt_paths |
list |
[] |
Glob patterns that bypass rate limiting. /_vibewarden/* is always exempt |
rate_limit.per_ip and rate_limit.per_user
| Field |
Type |
Default (per_ip) |
Default (per_user) |
Description |
requests_per_second |
float |
10 |
100 |
Sustained token refill rate |
burst |
int |
20 |
200 |
Maximum tokens that can accumulate |
rate_limit.redis
Only read when store is redis.
| Field |
Type |
Default |
Description |
url |
string |
"" |
Redis URL (redis:// or rediss://). Takes precedence over all other fields |
address |
string |
localhost:6379 |
Redis address in host:port form |
password |
string |
"" |
Redis AUTH password |
db |
int |
0 |
Logical database index |
pool_size |
int |
0 (auto) |
Connection pool size (0 = auto based on CPU count) |
key_prefix |
string |
vibewarden |
Key namespace prefix |
fallback |
bool |
true |
Fail-open on Redis failure (true) or fail-closed (false) |
health_check_interval |
duration |
30s |
How often to probe Redis for recovery |
| Field |
Type |
Default |
Description |
security_headers.enabled |
bool |
true |
Enable security headers middleware |
security_headers.hsts_max_age |
int |
31536000 |
HSTS max-age in seconds (1 year) |
security_headers.hsts_include_subdomains |
bool |
true |
Add includeSubDomains to HSTS |
security_headers.hsts_preload |
bool |
false |
Add preload to HSTS |
security_headers.content_type_nosniff |
bool |
true |
Set X-Content-Type-Options: nosniff |
security_headers.frame_option |
string |
DENY |
X-Frame-Options: DENY, SAMEORIGIN, or "" to disable |
security_headers.content_security_policy |
string |
default-src 'self'; style-src 'self' 'unsafe-inline' |
Content-Security-Policy value |
security_headers.referrer_policy |
string |
strict-origin-when-cross-origin |
Referrer-Policy value |
security_headers.permissions_policy |
string |
"" |
Permissions-Policy value |
security_headers.cross_origin_opener_policy |
string |
same-origin |
Cross-Origin-Opener-Policy value |
security_headers.cross_origin_resource_policy |
string |
same-origin |
Cross-Origin-Resource-Policy value |
security_headers.permitted_cross_domain_policies |
string |
none |
X-Permitted-Cross-Domain-Policies value |
security_headers.suppress_via_header |
bool |
true |
Remove Via header from proxied responses |
The headers are applied to every response the sidecar serves, not just proxied
app responses: the built-in login UI (/_vibewarden/login), the admin API
(/_vibewarden/admin/*, including its 401 responses), and the Kratos
self-service surface (/self-service/*) all carry the same set.
Operator rules under response_headers still win on conflict, but only on the
app-proxied route: response_headers is not applied to VibeWarden's own routes,
so it cannot override or strip a security header on the login UI, the admin API
or the Kratos proxy.
cors
| Field |
Type |
Default |
Description |
cors.enabled |
bool |
false |
Enable CORS plugin |
cors.allowed_origins |
list |
[] |
Permitted origins. Use ["*"] for development only |
cors.allowed_methods |
list |
[GET, POST, PUT, DELETE, OPTIONS] |
Permitted HTTP methods |
cors.allowed_headers |
list |
[Content-Type, Authorization] |
Permitted request headers |
cors.exposed_headers |
list |
[] |
Response headers exposed via Access-Control-Expose-Headers |
cors.allow_credentials |
bool |
false |
Set Access-Control-Allow-Credentials: true. Must not be combined with allowed_origins: ["*"] |
cors.max_age |
int |
0 |
Preflight cache duration in seconds. 0 omits the header |
waf
VibeWarden's built-in Web Application Firewall inspects every inbound request
for common attack patterns in-process, with no external dependencies.
waf (top-level)
| Field |
Type |
Default |
Description |
waf.enabled |
bool |
true |
Enable the WAF middleware |
waf.mode |
string |
detect |
Detection mode: block (reject with 400) or detect (pass through and log) |
waf.rules
Per-rule toggles. All rules are enabled by default when waf.enabled is true.
Set a rule to false to disable that check while keeping all others active.
| Field |
Type |
Default |
Description |
waf.rules.sqli |
bool |
true |
Detect SQL injection patterns in query strings and request bodies |
waf.rules.xss |
bool |
true |
Detect cross-site scripting payloads |
waf.rules.path_traversal |
bool |
true |
Detect directory traversal sequences (e.g. ../../etc/passwd) |
waf.rules.command_injection |
bool |
true |
Detect shell command-injection metacharacters |
waf.content_type_validation
| Field |
Type |
Default |
Description |
waf.content_type_validation.enabled |
bool |
false |
Enable Content-Type validation on body-bearing requests |
waf.content_type_validation.allowed |
list |
[application/json, application/x-www-form-urlencoded, multipart/form-data] |
Permitted media types. Requests with other types receive 415 Unsupported Media Type |
waf:
enabled: true
mode: block
rules:
sqli: true
xss: true
path_traversal: true
command_injection: true
content_type_validation:
enabled: true
allowed:
- application/json
- application/x-www-form-urlencoded
- multipart/form-data
body_size
| Field |
Type |
Default |
Description |
body_size.max |
string |
"" |
Global maximum body size (e.g., 1MB, 512KB). Empty means no limit |
body_size.overrides |
list |
[] |
Per-path overrides |
Each entry in body_size.overrides:
| Field |
Type |
Description |
path |
string |
URL path prefix to match |
max |
string |
Maximum body size for this path |
body_size:
max: 1MB
overrides:
- path: /api/upload
max: 50MB
ip_filter
| Field |
Type |
Default |
Description |
ip_filter.enabled |
bool |
false |
Enable IP filter plugin |
ip_filter.mode |
string |
blocklist |
allowlist or blocklist |
ip_filter.addresses |
list |
[] |
IP addresses or CIDR ranges |
ip_filter.trust_proxy_headers |
bool |
false |
Read X-Forwarded-For for client IP |
ip_filter:
enabled: true
mode: allowlist
addresses:
- 10.0.0.0/8
- 192.168.1.100
resilience
| Field |
Type |
Default |
Description |
resilience.timeout |
duration |
30s |
Upstream response timeout. 0 disables the timeout |
resilience.circuit_breaker
| Field |
Type |
Default |
Description |
resilience.circuit_breaker.enabled |
bool |
false |
Enable circuit breaker |
resilience.circuit_breaker.threshold |
int |
5 |
Consecutive failures to trip the circuit |
resilience.circuit_breaker.timeout |
duration |
60s |
How long the circuit stays open before probing |
resilience.retry
| Field |
Type |
Default |
Description |
resilience.retry.enabled |
bool |
false |
Enable retry with exponential backoff |
resilience.retry.max_attempts |
int |
3 |
Total attempts including the initial request |
resilience.retry.backoff |
duration |
100ms |
Wait before the first retry |
resilience.retry.max_backoff |
duration |
10s |
Upper bound on computed backoff |
resilience.retry.retry_on |
list |
[502, 503, 504] |
HTTP status codes that trigger a retry |
telemetry
| Field |
Type |
Default |
Description |
telemetry.enabled |
bool |
true |
Master switch for all telemetry |
telemetry.path_patterns |
list |
[] |
URL path normalization patterns using :param syntax |
telemetry.prometheus.enabled |
bool |
true |
Enable Prometheus pull-based exporter at /_vibewarden/metrics |
telemetry.otlp.enabled |
bool |
false |
Enable OTLP push-based exporter |
telemetry.otlp.endpoint |
string |
"" |
OTLP HTTP endpoint URL. Required when otlp.enabled is true |
telemetry.otlp.headers |
map |
{} |
HTTP headers for OTLP authentication |
telemetry.otlp.interval |
duration |
30s |
Export interval |
telemetry.otlp.protocol |
string |
http |
Transport protocol. Only http is supported |
telemetry.logs.otlp |
bool |
false |
Export structured events via OTLP. Requires otlp.endpoint |
telemetry.traces.enabled |
bool |
false |
Enable distributed tracing. Requires otlp.enabled |
Legacy metrics: section
The metrics: config block is deprecated. Settings are automatically migrated
to telemetry: at startup with a deprecation warning. Migrate before the next
major version.
observability
Controls generation of the optional local observability stack (Grafana, Prometheus,
Loki, Promtail).
| Field |
Type |
Default |
Description |
observability.enabled |
bool |
false |
Activate the observability compose profile (configs are always generated) |
observability.grafana_port |
int |
3001 |
Host port for Grafana |
observability.prometheus_port |
int |
9090 |
Host port for Prometheus |
observability.loki_port |
int |
3100 |
Host port for Loki |
observability.retention_days |
int |
7 |
Log retention period in Loki |
database
| Field |
Type |
Default |
Description |
database.external_url |
string |
"" |
Full postgres:// connection URL. When set, the local Postgres container is omitted from the generated Compose file |
database.tls_mode |
string |
require |
PostgreSQL SSL mode: disable, require, verify-ca, verify-full |
database.pool.max_conns |
int |
10 |
Maximum open connections |
database.pool.min_conns |
int |
2 |
Minimum idle connections |
database.connect_timeout |
duration |
10s |
Connection establishment timeout |
egress
Configuration for the egress proxy plugin. See the Egress Proxy guide
for a full feature walkthrough.
egress (top-level)
| Field |
Type |
Default |
Description |
egress.enabled |
bool |
false |
Enable the egress proxy plugin |
egress.listen |
string |
127.0.0.1:8081 |
TCP address the egress proxy binds to |
egress.default_policy |
string |
deny |
Disposition for requests that match no route: deny or allow |
egress.allow_insecure |
bool |
false |
Permit plain HTTP egress requests globally. Per-route allow_insecure overrides this |
egress.default_timeout |
duration |
30s |
Request timeout applied when a route does not specify its own |
egress.default_body_size_limit |
string |
"" |
Global maximum request body size (e.g. 10MB). Empty means no limit |
egress.default_response_size_limit |
string |
"" |
Global maximum response body size (e.g. 50MB). Empty means no limit |
egress.dns
DNS-level SSRF protection settings.
| Field |
Type |
Default |
Description |
egress.dns.block_private |
bool |
true |
Block requests to RFC 1918 private, loopback, and reserved IP ranges |
egress.dns.allowed_private |
list |
[] |
CIDR ranges exempt from block_private. Example: ["10.0.0.0/8"] |
egress.routes
Ordered list of egress route definitions. Routes are evaluated in declaration
order; the first matching route wins.
| Field |
Type |
Default |
Description |
name |
string |
— |
Unique human-readable identifier for this route. Required |
pattern |
string |
— |
URL glob matched against outbound request URLs (must start with http:// or https://). Required |
methods |
list |
[] |
HTTP methods this route applies to. Empty means all methods |
timeout |
duration |
"" |
Per-route request timeout. Empty uses egress.default_timeout |
secret |
string |
"" |
OpenBao KV path of the secret to fetch and inject |
secret_header |
string |
"" |
Request header to inject the secret value into (e.g. Authorization) |
secret_format |
string |
"" |
Value template — {value} is replaced with the resolved secret (e.g. Bearer {value}) |
rate_limit |
string |
"" |
Rate limit expression (e.g. 100/s, 500/m). Empty means no per-route limit |
body_size_limit |
string |
"" |
Maximum request body size. Empty uses egress.default_body_size_limit |
response_size_limit |
string |
"" |
Maximum response body size. Empty uses egress.default_response_size_limit |
allow_insecure |
bool |
false |
Permit plain HTTP for this route, overriding the global setting |
egress.routes[].circuit_breaker
| Field |
Type |
Description |
threshold |
int |
Consecutive failures to trip the circuit. Must be > 0 |
reset_after |
duration |
How long the circuit stays open before allowing a probe request |
egress.routes[].retries
| Field |
Type |
Default |
Description |
max |
int |
— |
Maximum retry attempts (not counting the initial request). Must be >= 1 |
methods |
list |
[GET, HEAD, PUT, DELETE] |
HTTP methods eligible for retry. Empty uses the default idempotent set |
backoff |
string |
exponential |
Backoff strategy: exponential or fixed |
egress.routes[].mtls
| Field |
Type |
Description |
cert_path |
string |
Filesystem path to the PEM-encoded client certificate |
key_path |
string |
Filesystem path to the PEM-encoded private key |
ca_path |
string |
Filesystem path to a PEM-encoded CA bundle used to verify the server's certificate (optional; uses system roots when empty) |
egress.routes[].validate_response
| Field |
Type |
Description |
status_codes |
list |
Allowed HTTP status code expressions (e.g. ["2xx", "301"]). Empty means no validation |
content_types |
list |
Allowed MIME type prefixes (e.g. ["application/json"]). Empty means no validation |
egress.routes[].cache
| Field |
Type |
Description |
enabled |
bool |
Activate in-memory response caching for this route |
ttl |
duration |
Cache entry lifetime. Zero means never expire (not recommended) |
max_size |
int |
Maximum cached response body size in bytes. Zero means no per-entry limit |
egress.routes[].sanitize
| Field |
Type |
Description |
headers |
list |
Header names whose values are replaced with [REDACTED] in log events (forwarded request is unchanged) |
query_params |
list |
Query parameter names stripped from the URL before forwarding |
body_fields |
list |
JSON field names replaced with [REDACTED] in the request body before forwarding. Only applied when Content-Type is application/json |
| Field |
Type |
Description |
inject |
map |
Static headers added to (or overwriting) the outbound request |
strip_request |
list |
Request header names removed before forwarding upstream |
strip_response |
list |
Response header names removed from the upstream response before returning to the app. Server and X-Powered-By are always stripped |
egress:
enabled: true
listen: "127.0.0.1:8081"
default_policy: deny
default_timeout: "30s"
dns:
block_private: true
routes:
- name: stripe-api
pattern: "https://api.stripe.com/**"
methods: ["POST"]
timeout: "10s"
secret: app/stripe
secret_header: Authorization
secret_format: "Bearer {value}"
rate_limit: "100/s"
circuit_breaker:
threshold: 5
reset_after: "30s"
retries:
max: 3
backoff: exponential
secrets
| Field |
Type |
Default |
Description |
secrets.enabled |
bool |
false |
Enable secrets management plugin |
secrets.provider |
string |
openbao |
Secret store backend |
secrets.cache_ttl |
duration |
5m |
How long fetched secrets are cached in memory |
secrets.openbao
| Field |
Type |
Default |
Description |
secrets.openbao.address |
string |
"" |
OpenBao server URL (e.g., http://openbao:8200) |
secrets.openbao.mount_path |
string |
secret |
KV v2 mount path |
secrets.openbao.auth.method |
string |
"" |
Auth method: token or approle |
secrets.openbao.auth.token |
string |
"" |
Static token (used when method: token) |
secrets.openbao.auth.role_id |
string |
"" |
AppRole role_id (used when method: approle) |
secrets.openbao.auth.secret_id |
string |
"" |
AppRole secret_id (used when method: approle) |
secrets.inject
| Field |
Type |
Description |
secrets.inject.headers |
list |
Secrets injected as HTTP request headers |
secrets.inject.env_file |
string |
Path to write a .env file with secret values |
secrets.inject.env |
list |
Secrets written to the env file |
Each entry in secrets.inject.headers:
| Field |
Type |
Description |
secret_path |
string |
KV path of the secret |
secret_key |
string |
Key within the secret map |
header |
string |
HTTP header name |
secrets.dynamic
| Field |
Type |
Default |
Description |
secrets.dynamic.postgres.enabled |
bool |
false |
Enable dynamic Postgres credential generation |
secrets.dynamic.postgres.roles |
list |
[] |
OpenBao database roles to request credentials for |
Each entry in roles:
| Field |
Type |
Description |
name |
string |
OpenBao database role name |
env_var_user |
string |
Env var to write the generated username into |
env_var_password |
string |
Env var to write the generated password into |
secrets.health
| Field |
Type |
Default |
Description |
secrets.health.check_interval |
duration |
6h |
How often to run secret health checks |
secrets.health.max_static_age |
duration |
2160h |
Maximum acceptable age of a static secret (90 days) |
secrets.health.weak_patterns |
list |
[] |
Substrings that indicate a weak or default secret |
webhooks
| Field |
Type |
Description |
webhooks.endpoints |
list |
Webhook endpoints to deliver audit events to |
Each entry in webhooks.endpoints:
| Field |
Type |
Default |
Description |
url |
string |
— |
HTTP(S) endpoint to POST events to. Required |
events |
list |
— |
Event types to deliver. Use ["*"] to subscribe to all |
format |
string |
raw |
Payload format: raw, slack, or discord |
timeout_seconds |
int |
10 |
Per-request HTTP timeout in seconds |
audit
| Field |
Type |
Default |
Description |
audit.enabled |
bool |
true |
Enable the audit log sink |
audit.output |
string |
stdout |
Write destination: stdout or a file path |
log
| Field |
Type |
Default |
Description |
log.level |
string |
info |
Log level: debug, info, warn, error |
log.format |
string |
json |
Log format: json or text |
admin
| Field |
Type |
Default |
Description |
admin.enabled |
bool |
false |
Enable the admin API at /_vibewarden/admin/* |
admin.token |
string |
"" |
Bearer token for admin API authentication |
The token is accepted in either header form, so use whichever your client
prefers:
curl -H "X-Admin-Key: $VIBEWARDEN_ADMIN_TOKEN" https://localhost:8443/_vibewarden/admin/users
curl -H "Authorization: Bearer $VIBEWARDEN_ADMIN_TOKEN" https://localhost:8443/_vibewarden/admin/users
If both headers are present, X-Admin-Key wins.
Admin-token authentication has a built-in, non-configurable brute-force
lockout: 10 consecutive failed attempts from one client IP within 1 minute lock
that IP out for 1 minute. During the lockout the token is not compared and
requests get 429 Too Many Requests with a Retry-After header and the error
code too_many_failed_attempts. The attempt that trips the lockout emits a
single audit.auth.lockout audit event in place of the usual
audit.auth.failure; requests received during the cooldown emit no audit event
at all. A successful authentication resets the counter. See
Production hardening for the
full behaviour.
overrides
Escape hatches for advanced users. All fields are optional.
| Field |
Type |
Description |
overrides.kratos_config |
string |
Path to a custom kratos.yml file. When set, VibeWarden uses it instead of generating one |
overrides.compose_file |
string |
Path to a custom docker-compose.yml file |
overrides.identity_schema |
string |
Path to a custom Kratos identity schema JSON file |
Full example vibewarden.yaml
profile: dev
server:
host: 0.0.0.0
port: 8443
tls:
enabled: true
provider: self-signed
upstream:
host: 127.0.0.1
port: 3000
auth:
mode: jwt
jwt:
jwks_url: "https://dev-abc123.us.auth0.com/.well-known/jwks.json"
issuer: "https://dev-abc123.us.auth0.com/"
audience: "https://api.your-app.com"
claims_to_headers:
sub: X-User-Id
email: X-User-Email
email_verified: X-User-Verified
public_paths:
- /health
- /static/*
rate_limit:
enabled: true
store: memory
per_ip:
requests_per_second: 10
burst: 20
per_user:
requests_per_second: 100
burst: 200
security_headers:
enabled: true
hsts_max_age: 31536000
content_security_policy: ""
telemetry:
enabled: true
prometheus:
enabled: true
path_patterns:
- "/api/v1/users/:id"
- "/api/v1/orders/:order_id"
log:
level: info
format: json