Security model
The defaults are on. This page lists what they protect against, how, and where the protection ends.
Where this fits: Run and operate. What Kervan protects against, for every tool and for spec tools.
On this page
Defaults for every server
- Timeouts: 30 seconds per tool call by default (
limits.toolTimeoutMs); cancellation and timeouts reach the handler asctx.signal. - Size limits: request bodies (4 MiB over HTTP), argument size (10,000 array elements and object members), and for spec tools response sizes, output length and spec size.
- Error masking: only
ToolErrormessages reach the client. Anything else becomesInternal error in tool "x" (ref: ...), with the real error in your logs under that ref. - HTTP: binds to
127.0.0.1by default;HostandOriginare checked againstallowedHostsandallowedOrigins(DNS rebinding protection); a per-client rate limit (300 requests a minute) on Node; logs go to stderr, never stdout. - Validation: arguments are checked against the input schema before a handler runs, and structured results against the output schema.
Network (SSRF) protection
Spec tools make HTTP requests to URLs a spec author writes and a model fills in. These checks keep them on the public internet:
Every request, and every redirect hop, goes through the same checks, and each fails closed:
- The URL is parsed with the WHATWG parser, which turns encodings such as
2130706433,0x7f.1or0177.0.0.1into127.0.0.1; the scheme, host and port are literal in the spec. - The host name is resolved once, within the request timeout. Empty answers, resolver errors
and anything that is not a strictly valid IP address (including IPv6 zone IDs such as
%eth0) block the request. - Every address must be public unicast: the address classifier (
ipaddr.js) must sayunicastand the address must be outside an independent list of internal ranges (loopback, private, link-local and cloud metadata such as169.254.169.254, CGNAT, unique local, multicast, documentation, benchmarking, NAT64, 6to4, Teredo, every IPv4-mapped IPv6 address, and all IPv6 space outside the global unicast block2000::/3). One internal address among public ones blocks the whole request. - The connection is pinned to the checked addresses through a custom
lookup(no second DNS query, so no DNS rebinding window), on a fresh connection, and the socket’s remote address is checked again when it connects. - Redirects are not followed unless
followRedirectsallows it. Each hop is resolved and checked again;httpstohttpdowngrades, credentials in the location and other schemes are refused; when the origin changes, every header from the spec is dropped exceptAcceptandUser-Agent(templated or literal, any of them may be a credential), and a request body is never sent to another origin. - This machine’s own addresses (every address of
os.networkInterfaces(), public ones included) are refused, because services listening on all interfaces are reachable through them. If the list cannot be read, requests are refused. - At most a few DNS lookups run at once, process-wide (half of libuv’s thread pool, see below); others queue and give up when the request times out.
Blocked requests report the reason but never the resolved address, which could reveal internal DNS names.
The order of the rules is: cloud metadata (always refused, see below), network.denyList (always
refused), network.allowPrivate (explicitly allowed), this machine’s addresses, then the
public-unicast checks.
Cloud metadata endpoints are refused before any other rule, so neither allowPrivate nor
--allow-private-network reaches them (METADATA_RANGES):
- all of link-local
169.254.0.0/16(the metadata service of AWS, GCP, Azure, Oracle, DigitalOcean, OpenStack; ECS/EKS credential agents; Tencent); fd00:ec2::/32(AWS over IPv6);100.100.100.200(Alibaba);168.63.129.16(Azure WireServer, a public address);192.0.0.192(Oracle, legacy). For local development,kervan run --allow-private-network(refused withNODE_ENV=production) ornetwork.allowPrivatein code allows internal addresses;--deny-network <cidr>ornetwork.denyListblocks more. A spec file can set neither.
DNS lookups and UV_THREADPOOL_SIZE
dns.lookup runs on libuv’s thread pool, which also serves file system and crypto work, and a
lookup cannot be cancelled: a name server that never answers keeps its thread busy until the
operating system gives up. Kervan therefore lets at most half of the pool (2 of the default 4
threads) resolve names at once; a lookup’s slot is freed only when the lookup itself ends, and
queued requests fail with “timed out waiting for a DNS lookup slot”. To allow more concurrent
lookups, start Node.js with a larger pool, e.g. UV_THREADPOOL_SIZE=16 (8 lookups).
Secrets and untrusted output
- Secret values are redacted from results, errors and logs, in their raw, URL-encoded,
form-encoded and JSON-escaped forms, and from the response before
selectruns. Encodings Kervan does not know (base64, HTML entities) are not redacted: bind secrets only to APIs you trust not to echo them. See secrets. - An API response is untrusted input for the model.
selectlimits it to chosen fields, but text inside a chosen field can still carry instructions (prompt injection); Kervan cannot judge content.
How the checks are tested
Each address check, redaction form and limit has tests that show it working. They were also
mutation-tested: each check was broken on purpose, one at a time, and the run counted only when a
test failed. That was done by hand during development, not on every change; the open questions the
security reviews left are listed in the repository’s docs/REVIEW-NOTES.md.
Known limits
- Node.js only: the executor uses
node:httpandnode:dns. Fetch runtimes (Workers, Deno) cannot pin DNS, so specs are not supported there. - No proxy support:
HTTPS_PROXY/HTTP_PROXYare ignored. Behind a mandatory outbound proxy, spec tools cannot reach the internet. - Regular expressions in schemas (
pattern) run on the JavaScript engine; the length limit reduces, but does not remove, the risk of slow patterns (ReDoS) from spec authors. - Rate limits are per process.
Reporting a vulnerability
Report it privately, through the repository’s private vulnerability reporting or to security@getkervan.dev, never in a public issue. Reports are acknowledged within 5 working days, and disclosure is coordinated (90 days). The security.txt of this site has the contact too.