Configuration Reference
These are the global settings in exfilguard.toml and optional
config.d/*.toml fragments. ExfilGuard reads them at startup.
Core Settings
These settings are required.
| Field | Type | Required | Description |
|---|---|---|---|
listen |
String | Yes | Listen address and port (e.g., "127.0.0.1:3128") |
proxy_protocol |
String | "off" |
PROXY protocol mode: "off", "optional", or "required" |
proxy_protocol_allowed_cidrs |
Array | None | CIDR allowlist for peers allowed to send PROXY headers (required when proxy_protocol is "optional" or "required") |
proxy_protocol_max_pending_connections |
usize | 1024 | Maximum connections from allowlisted peers concurrently awaiting PROXY header processing |
vault |
Table | When used | Shared Vault connection, PKI trust, and authentication settings |
ca |
Table | Yes | Explicit TLS interception CA source: builtin, files, or vault |
clients |
Path | Yes | Path to clients configuration file |
policies |
Path | Yes | Path to policies configuration file |
clients_dir |
Path | No | Directory containing additional client config files (*.toml) |
policies_dir |
Path | No | Directory containing additional policy config files (*.toml) |
authorization |
Table | No | Named services for delegated request policy and authentication |
Note
Relative paths are resolved from the directory containing the main config file.
Note
Use ExfilGuard as an explicit proxy. Clients should know they are talking to one.
Note
When proxy_protocol is "optional" or "required", ExfilGuard
auto-detects PROXY protocol v1 or v2 headers for peers in
proxy_protocol_allowed_cidrs.
In "optional" mode, peers outside that allowlist are treated as normal
client connections.
In "required" mode, peers outside that allowlist are rejected before
HTTP parsing, and allowlisted peers must send a valid PROXY header.
Allowlisted peers are trusted to send the complete PROXY header promptly as
the first bytes of a backend connection. ExfilGuard bounds this setup with
request_header_timeout and proxy_protocol_max_pending_connections.
Excess pending connections are closed without spawning a handler task.
Note
Set proxy_protocol_allowed_cidrs when PROXY protocol is enabled.
Note
ExfilGuard reads exfilguard.toml and config.d/*.toml only at startup.
SIGHUP reloads only the client and policy data read from the configured clients,
clients_dir, policies, and policies_dir paths. If you change fields
in exfilguard.toml itself, including listen, metrics, cache, TLS,
logging, and timeout settings, restart the server.
Treat all configured client and policy files as one generation: finish
replacing every file before sending SIGHUP, and do not modify them during
the reload. ExfilGuard validates and atomically publishes the resulting
in-memory snapshot; if loading fails, the previous snapshot remains active.
Global Configuration Fragments
ExfilGuard reads regular *.toml files from a config.d directory beside the
selected main configuration file. Fragments load in lexicographic filename
order after the main file and before EXFILGUARD__* environment overrides.
Later values replace earlier values, and fragments may contain only the fields
they override.
For the default /etc/exfilguard/exfilguard.toml, a long-polling override could
be written as /etc/exfilguard/config.d/50-long-polls.toml:
All relative paths, including paths set by fragments, are resolved from the directory containing the main configuration file. Changing a fragment requires restarting ExfilGuard.
Client Connection Admission
Every configured client has a simultaneous downstream connection budget. The default is 1,024 connections and can be overridden on an individual client:
[[client]]
name = "build-workers"
cidr = "10.42.16.0/27"
policies = ["build-egress"]
max_connections = 2048
The budget counts ordinary HTTP keep-alive connections, raw CONNECT tunnels,
and inspected CONNECT sessions. A CIDR client shares one budget across every
machine it matches. When the budget is full, ExfilGuard closes new connections
without evicting established ones. max_connections must be greater than zero.
Client and policy reloads apply a changed limit to new admission without resetting active connection accounting. An existing connection's permit remains charged to the client identity assigned when it was accepted, even if a reload later maps its peer address to another client. Subsequent requests still use the latest client mapping and policies. The limit is an operational resource guard, not a retroactively reconciled tenant quota.
TLS / Certificate Settings
These settings control TLS interception and leaf certificate generation.
| Field | Type | Default | Description |
|---|---|---|---|
leaf_ttl |
u64 | 86400 | TLS certificate leaf TTL in seconds (must be > 0) |
leaf_cache_capacity |
usize | 4096 | Maximum number of generated TLS leaves retained in memory (must be > 0) |
leaf_mint_concurrency |
usize | 4 | Maximum concurrent blocking TLS leaf mint jobs (must be > 0) |
CA Sources
ExfilGuard uses a root CA and a path-length-zero intermediate CA. The
intermediate signs generated leaves, ExfilGuard sends Leaf -> Intermediate ->
Root, and clients trust the root. Choose the component that owns this
lifecycle explicitly; ExfilGuard never guesses from the files it finds and
never falls back to another source. In every mode, leaf validity is capped by
the active intermediate's remaining validity, with a safety margin; issuance
stops when too little safe lifetime remains.
ExfilGuard enforces the CA semantics needed for arbitrary policy-selected leaf
names. It rejects name-constrained hierarchies and critical certificate
extensions other than basicConstraints and keyUsage; accepting either would
require applying additional constraints during every leaf issuance. Noncritical
informational extensions remain permitted.
builtin
On the first start with an empty directory, ExfilGuard generates certificates valid for approximately ten years and persists exactly:
The root key exists only while the hierarchy is generated and is never written
to disk. First-run creation is a recoverable filesystem transaction: ExfilGuard
stages and validates the complete hierarchy, publishes it durably, and resumes
or discards only its own recognizable staging state after interruption.
Operator-created incomplete material still fails startup rather than silently
creating a new trust anchor. Consequently, builtin cannot renew or replace
its intermediate under the same root. Before the hierarchy expires, or after a
key compromise, generate a new hierarchy and distribute its new root to clients.
The Debian package selects builtin, creates /var/lib/exfilguard/ca as
exfilguard:exfilguard mode 0700, and runs the service with UMask=0077.
It therefore works on a clean installation without an operator permission
step.
files
files treats all three artifacts above as externally managed. ExfilGuard
neither creates nor renews them. Provision a new intermediate certificate and
matching key as one generation, replace the directory contents safely, and
restart ExfilGuard. The new process starts with an empty in-memory leaf cache.
Startup validates that the intermediate key matches its certificate, the
intermediate chains to the root, both certificates are currently valid, and
CA basic constraints, path length, key usage, and issuer validity allow safe
leaf issuance. root.key is always rejected: neither file-backed source uses
it, and leaving a root signing key beside the online intermediate needlessly
increases exposure.
File permissions
For builtin and files, ExfilGuard enforces owner-only control at every
startup:
- The directory must be real, not a symlink, owned by the process UID, and
mode
0700or read-only mode0500. - Every CA file must be a regular, non-symlink file owned by the process UID.
intermediate.keymust have mode0600or read-only mode0400.- Certificates must be owner-readable and non-executable, and must not be
writable by group or other users. Modes
0600,0644, and read-only equivalents are suitable.
Startup reports a remediation command instead of changing provisioned files. For the packaged service, a typical repair is:
sudo chown -R exfilguard:exfilguard /var/lib/exfilguard/ca
sudo chmod 0700 /var/lib/exfilguard/ca
sudo chmod 0600 /var/lib/exfilguard/ca/intermediate.key
sudo chmod 0644 /var/lib/exfilguard/ca/root.crt \
/var/lib/exfilguard/ca/intermediate.crt
Package upgrades restore the CA directory ownership and mode but deliberately do not rewrite operator-provisioned files.
vault
ExfilGuard has one shared HashiCorp Vault connection. The inspection CA and authorization services can both use it. HCP Vault Secrets is a different product and API.
Vault HTTPS uses the platform trust store. tls_ca_cert adds certificates to
that trust store for the Vault connection; it does not replace the platform
roots. This transport trust is separate from vault.pki.expected_root_certs,
which pins the roots accepted for certificates issued by Vault.
Configure the Vault connection, PKI trust, and authentication once:
[vault]
address = "https://vault.internal.example:8200"
tls_ca_cert = "/etc/exfilguard/vault-tls-ca.crt"
# namespace = "team-a"
# tls_server_name = "vault.internal.example"
# request_timeout = 10
# tls_client_cert = "/etc/exfilguard/vault-client.crt"
# tls_client_key = "/etc/exfilguard/vault-client.key"
[vault.pki]
mount = "pki"
expected_root_certs = "/etc/exfilguard/exfilguard-roots.pem"
[vault.auth]
method = "approle"
mount = "approle"
role_id = "00000000-0000-0000-0000-000000000000"
secret_id_file = "/etc/exfilguard/vault-secret-id"
Then select Vault for the inspection CA:
[ca]
source = "vault"
issuer = "exfilguard-parent"
# intermediate_ttl = 2592000
# renewal_threshold = 1296000
ExfilGuard generates the intermediate private key and CSR in memory. Vault signs the CSR using the selected issuer. Before the intermediate expires, ExfilGuard generates a new key, obtains a new certificate, and switches the complete issuer generation atomically. Generated origin keys also remain in memory.
vault.pki.mount defaults to pki and vault.request_timeout to 10 seconds.
ca.intermediate_ttl defaults to 30 days and ca.renewal_threshold to 15
days. The threshold must be shorter than the requested lifetime and should
leave enough time to recover from a Vault outage.
The renewal scheduler rechecks certificate expiry against wall-clock time at least hourly rather than sleeping for the complete multi-day interval. This keeps renewal responsive after host suspend or a wall-clock adjustment. Failed renewals retry with jittered exponential backoff from 5 seconds to 15 minutes.
Set tls_server_name when address uses an IP literal but the Vault server
certificate names a DNS host. It controls certificate verification while the
client still connects to the configured IP.
For the inspection CA, ExfilGuard calls only:
Vault must support selected issuers. ExfilGuard does not fall back to the
older default-issuer endpoint because that could select a different trust
hierarchy. Give the AppRole or token update permission only on that exact
path. If possible, use a selected signing issuer constrained to pathlen:1 so
the ExfilGuard intermediate is cryptographically limited to pathlen:0.
vault.pki.expected_root_certs is a PEM bundle independent of the TLS CA used
to reach Vault. ExfilGuard accepts the returned issuer only when it chains to
one of these explicitly pinned roots and passes the same key, constraint,
usage, and validity checks as file mode. Put old and new roots in the bundle
during a planned root rotation, switch the Vault issuer, then remove the old
root after the overlap period. Changing the bundle requires restarting
ExfilGuard.
Vault supports three authentication transports:
approleperforms a just-in-time login at/v1/auth/<mount>/login. Keeprole_idin TOML and the durable SecretID in the dedicated owner-onlysecret_id_file. Prefer a narrowly scoped AppRole, CIDR bindings where addresses are stable, no default policy, and a one-use token for the signing request. The reusable SecretID remains durable signing authority: keeping CA keys in memory does not make a compromised host or credential harmless.token_filereads a token supplied and renewed by another component:
proxysends the request without a token so a local Vault Proxy can attach its auto-auth token. Pointvault.addressat its restricted listener:
Credential files must be regular, non-symlink, owned by the ExfilGuard process UID, and readable only by that owner. Literal SecretIDs and tokens are not accepted in TOML or command-line arguments. Vault requests do not follow redirects or inherit ambient HTTP proxy settings, preventing credentials from being forwarded to another origin or back through ExfilGuard itself. Vault connections must use HTTPS; plaintext HTTP is accepted only for a loopback IP listener such as a local Vault Proxy.
ExfilGuard generates Vault-backed private keys in memory and does not save them. It must therefore reach Vault after each restart to obtain certificates for the new keys. A failed renewal leaves the current valid certificate in place and triggers bounded retries. An invalid Vault response never replaces it. Once no valid certificate remains, the affected operation fails closed. Inspection never falls back to another CA, direct forwarding, or CONNECT tunneling.
Configuration migration
The old top-level ca_dir setting is not accepted. Replace it with one of the
explicit configurations above. Existing installations must also remove
root.key from the active CA directory before starting this version. If it is
a real root key that must be retained, move it to appropriately protected
offline storage; ExfilGuard never reads it. Remove the older compatibility
workaround that copied intermediate.key to root.key.
Vault connection settings that were previously nested under [ca] now belong
under [vault], [vault.pki], and [vault.auth]. Keep only source, issuer,
intermediate_ttl, and renewal_threshold under [ca]. This makes the same
Vault connection available to authorization services without coupling them to
the inspection CA source.
For an existing locally generated directory, the direct migration is:
Retain root.crt, intermediate.crt, and intermediate.key, remove
root.key, restore the documented ownership and modes, then restart.
Delegated authorization
ExfilGuard can ask an external authorization service whether a client may call an API. The client sends an authorization token, which ExfilGuard passes to the service unchanged. ExfilGuard does not interpret the token.
The service returns rules for the token, and ExfilGuard caches them briefly. Both the client's local policy and the service's policy must allow a request.
The service can also provide authentication headers for an allowed request. A policy rule names the credential with a reference that only the service understands. ExfilGuard sends the token, the reference, and the exact outgoing request to the service, then adds the returned headers before forwarding it. The client can therefore call the API without ever having the credential.
This is configured per client. A client with an authorization_service must send a token. Other
clients on the same listener continue to use ordinary static policy.
Define one or more named services in the main configuration:
[[authorization.service]]
name = "central"
audience = "deployment-prod"
policy_url = "https://authorization.example.net/v1/policy"
credential_url = "https://authorization.example.net/v1/credential"
server_ca_cert = "/etc/exfilguard/exfilguard-roots.pem"
[authorization.service.client_certificate]
source = "vault"
role = "exfilguard-authorization-client"
common_name = "exfilguard-production"
This uses the shared [vault] configuration described above. ExfilGuard generates the client
private key and CSR in memory, sends the CSR to
POST /v1/<mount>/sign/<role>, and accepts the result only if it is a client-only certificate for
the configured common name and chains to vault.pki.expected_root_certs. The Vault role chooses
the issuer and certificate lifetime. ExfilGuard renews the certificate halfway through its actual
lifetime and switches new service requests to it atomically. Requests already in flight may finish
using the previous certificate.
Give the AppRole update permission on the exact signing-role path. Configure the Vault PKI role
to permit only the intended common name, issue client certificates rather than CA or server
certificates, and impose an appropriate maximum lifetime. When Vault manages both the inspection
CA and these client certificates, the one AppRole policy contains both exact signing paths.
File-backed client certificates remain available as an explicit alternative:
[authorization.service.client_certificate]
source = "files"
cert = "/etc/exfilguard/authorization/client.pem"
key = "/etc/exfilguard/authorization/client.key"
ExfilGuard supplies secure defaults for all timeouts and resource limits. The following settings are optional:
| Setting | Default | Meaning |
|---|---|---|
max_token_header_size |
1024 bytes | Largest accepted proxy authorization value |
policy_cache_capacity |
4096 | Cached policy results per service |
max_policy_cache_duration |
30 seconds | Longest ExfilGuard will reuse a service policy |
negative_cache_duration |
1 second | How briefly to remember a denied policy lookup |
max_policy_response_size |
256 KiB | Largest accepted policy response |
max_policy_rules |
1024 | Most rules accepted in one policy response |
max_credential_response_size |
32 KiB | Largest accepted authentication-header response |
max_protected_headers |
16 | Most authentication headers allowed on one request |
max_buffered_body_size |
1 MiB | Largest request body the service may receive |
max_buffered_body_capacity |
16 MiB | Total memory available for bodies and their JSON copies |
These settings belong under [authorization]. Each [[authorization.service]] also accepts an
optional timeout (5 seconds by default) and max_concurrency (32 simultaneous calls by default).
Both policy and credential calls use these values.
Then assign the service in the client configuration:
[[client]]
name = "build-client"
cidr = "192.0.2.0/24"
policies = ["build-ceiling"]
authorization_service = "central"
[[client.credential_limit]]
credential_reference = "build-api"
origin_scope = "https://api.example.net/v1/**"
protected_headers = ["authorization"]
body_access = "bounded_payload"
A credential_limit is a local safety rule, not a credential. It says where the named reference may
be used, which headers the service may return, and whether the service may receive the request body.
A rule that does not ask for credentials does not need a matching limit. If a rule asks for
credentials outside the limit, ExfilGuard denies the request.
The first request from a client that uses delegated authorization must contain
Proxy-Authorization: ExfilGuard <base64url-token>. ExfilGuard keeps that token with the
connection. Later requests may omit it but cannot replace it. For inspected HTTPS, the outer
CONNECT carries the token and all decrypted requests use it. Raw CONNECT tunnels are denied
because ExfilGuard cannot check the requests inside them.
ExfilGuard calls the configured URLs directly over HTTPS with the service's client certificate. It
trusts only server_ca_cert, does not follow redirects, and ignores proxy settings from the
environment. Public CA files must be regular files, not symlinks, owned by root or the ExfilGuard
process, and not writable by group or other users. A file-backed client key must be owned by the
ExfilGuard process, must not be a symlink, and must have mode 0400 or 0600. Relative paths are
resolved from the main configuration directory. Vault-backed client keys are never written to
disk.
Each named service has its own policy cache. Policy and credential calls to that service share its
max_concurrency limit.
body_access = "none" permits credentials only on requests without a body. "bounded_payload"
lets the service receive a size-limited body, for example to sign it. Requests that use credentials
cannot have trailers or Expect: 100-continue. If the service does not return valid headers,
ExfilGuard sends nothing to the API. The request is not retried or cached. All requests that use
delegated authorization bypass the shared response cache, even when no credential is needed.
max_buffered_body_size limits one body. max_buffered_body_capacity limits the total memory used
by all buffered bodies and their JSON copies. It must be at least
5 * max_buffered_body_size + 2; ExfilGuard rejects a smaller value.
Service definitions and limits are read at startup. SIGHUP reloads client assignments, credential
limits, and local policies together. A client that names an unknown service causes the reload to
fail, leaving the current configuration in place.
See Authorization Service API for the two JSON operations and the exact request format.
Logging Settings
| Field | Type | Default | Description |
|---|---|---|---|
log |
String | "json" |
Log format: "json" or "text" |
log_queries |
Boolean | false | Whether to log each request query |
Timeout Settings
All timeout values are in seconds. Use 0 to disable request_total_timeout and
connect_tunnel_max_lifetime.
| Field | Type | Default | Description |
|---|---|---|---|
dns_resolve_timeout |
u64 | 2 | Maximum time to resolve DNS for upstream hosts |
upstream_connect_timeout |
u64 | 5 | Maximum total time for one upstream TCP connection across all resolved addresses |
tls_handshake_timeout |
u64 | 10 | Maximum time for TLS handshakes (client or upstream) |
request_header_timeout |
u64 | 10 | Maximum time to read an HTTP/1 request line + headers or the downstream HTTP/2 connection preface |
request_body_idle_timeout |
u64 | 30 | Maximum idle time between request body reads/writes |
response_header_timeout |
u64 | 60 | Maximum total time from waiting for the first upstream response head through receiving the final response head |
response_body_idle_timeout |
u64 | 60 | Maximum idle time between response body reads/writes |
request_total_timeout |
u64 | 0 | Maximum total time from accepting a complete request head through policy/cache work, upstream setup, and full response delivery (0 disables) |
client_keepalive_idle_timeout |
u64 | 30 | Idle time before closing an HTTP/1 keep-alive connection or an HTTP/2 connection with no active request streams |
connect_tunnel_idle_timeout |
u64 | 60 | Maximum time with no successful relay progress in either direction of a raw CONNECT tunnel; also bounds individual tunnel writes and setup/shutdown operations |
connect_tunnel_max_lifetime |
u64 | 0 | Maximum lifetime for CONNECT tunnels (0 disables) |
For downstream HTTP/2, the connection is request-idle whenever it has no
active stream task. The client must produce its next complete request within
client_keepalive_idle_timeout; PING and other control traffic do not extend
that period. This also bounds an incomplete HTTP/2 header block while no other
stream is active. Active streams remain governed by the request-body,
response, and optional total-request timeouts rather than the connection-idle
timer.
The total-request timeout begins after a complete inner HTTP request head has
been accepted. It caps cache lookup and delivery, DNS/TCP/TLS upstream setup,
request upload, retries, and response delivery. Phase-specific timeouts still
apply, so whichever deadline expires first wins. Before a final response has
started, expiry returns 504 Gateway Timeout; after response delivery has
started, ExfilGuard closes or resets the request instead of appending another
response. Outer CONNECT setup, tunnel lifetime, and cleanup after response
delivery use their own limits.
For HTTP/1 and HTTP/2 cacheable responses, response_body_idle_timeout also bounds
each wait for cache file operations on the response path. A storage error or
timeout lets response forwarding continue without waiting for caching. File
creation and already-started publication finish or clean up in the background;
publication may still store the entry after the request stops waiting.
Request Size Limits
All size values are in bytes. Header limits must be greater than 0.
Set max_request_body_size = 0 to disable the global request-body cap.
| Field | Type | Default | Description |
|---|---|---|---|
max_request_header_size |
usize | 32768 (32 KiB) | Maximum HTTP request header size, including bumped HTTP/2 header lists |
max_response_header_size |
usize | 32768 (32 KiB) | Maximum HTTP response header size; for HTTP/1 this is shared by all informational and final heads, and for HTTP/2 it limits the upstream header list |
max_request_body_size |
usize | 0 (unlimited) | Maximum HTTP request body size during forwarding (0 disables the limit) |
Connection Pool
| Field | Type | Default | Description |
|---|---|---|---|
upstream_pool_capacity |
usize | 32 | Maximum number of upstream connections to pool (must be >= 1) |
HTTP/2
| Field | Type | Default | Description |
|---|---|---|---|
http2_max_concurrent_streams |
u32 | 100 | Maximum concurrent bumped downstream HTTP/2 streams per connection (must be >= 1) |
Metrics
| Field | Type | Default | Description |
|---|---|---|---|
metrics_listen |
String | None | Optional listen address (e.g., "127.0.0.1:9090") to serve Prometheus metrics at /metrics |
metrics_tls_cert |
Path | None | PEM certificate chain to enable HTTPS for /metrics |
metrics_tls_key |
Path | None | PEM private key matching metrics_tls_cert |
ExfilGuard exports counters and histograms for per-client and per-policy decisions, latency, cache activity, and upstream reuse, plus gauges for current downstream connections globally and by client, in-flight requests, CONNECT tunnels, bumped TLS sessions, active HTTP/2 streams, upstream connections, cache usage, and the last successful policy reload time. Rejected downstream connections are counted by configured client name.
requests_method_total uses a bounded method label: the standard HTTP
methods are reported by name and all extension methods are aggregated as
OTHER. Access logs retain the exact method.
CA lifecycle metrics are intentionally low-cardinality:
| Metric | Description |
|---|---|
ca_source_info{source} |
Selected builtin, files, or vault source |
ca_certificate_not_after_timestamp_seconds{certificate} |
Root or active intermediate expiry as a Unix timestamp |
ca_issuer_usable |
1 while the active issuer can safely mint a leaf, otherwise 0 |
ca_issuer_generation |
In-process issuer generation; increments on a successful Vault replacement |
ca_vault_renewal_attempts_total{result,reason} |
Vault renewal outcomes with bounded reason labels |
ca_vault_last_renewal_attempt_timestamp_seconds |
Last Vault renewal attempt time |
ca_vault_last_renewal_success_timestamp_seconds |
Last successful Vault renewal time |
authorization_service_client_certificate_not_after_timestamp_seconds{service} |
Expiry of a Vault-backed authorization-service client certificate |
authorization_service_vault_renewal_attempts_total{service,result,reason} |
Client-certificate renewal outcomes |
authorization_service_vault_last_renewal_attempt_timestamp_seconds{service} |
Last client-certificate renewal attempt |
authorization_service_vault_last_renewal_success_timestamp_seconds{service} |
Last successful client-certificate renewal |
Scrape these metrics in every CA mode: certificate expiry matters for
builtin and files even though ExfilGuard does not renew those sources. See
prometheus-alerts.yml for example expiry, usability,
and Vault failure alerts. Adjust the warning windows to your certificate TTL
and incident-response policy.
Cache Settings
Response caching is opt-in per rule. The settings here configure the shared cache storage.
| Field | Type | Default | Description |
|---|---|---|---|
cache_dir |
Path | None | Directory for response cache storage |
cache_max_entry_size |
u64 | 104857600 (100 MiB) | Maximum size of an entry and aggregate in-progress response bodies |
cache_max_entries |
usize | 10000 | Maximum number of cached responses (LRU) |
cache_total_capacity |
u64 | 1073741824 (1 GiB) | Capacity of completed cache entries |
cache_sweeper_interval |
u64 | 300 | Interval in seconds between cache sweeper runs |
cache_sweeper_batch_size |
usize | 1000 | Maximum metadata entries inspected per sweep |
Cache Behavior
The cache follows standard HTTP cache headers from upstream servers.
Scope
Within one ExfilGuard instance, the cache is shared across all clients and
inspected HTTP/1.1 and HTTP/2 traffic. Responses are keyed by method and absolute URI. Vary headers
decide which request headers are part of the cache key. Enable caching only if
cross-client sharing is acceptable in your environment.
Each node in a multi-node deployment has an independent cache; ExfilGuard does not coordinate caches between nodes.
No Mutation-Driven Invalidation
ExfilGuard makes no attempt to invalidate cached responses after mutations, including successful POST, PUT, or DELETE requests that pass through the same node. A cached response may continue to be served for its freshness lifetime even after the resource changes upstream. Normal expiration and eviction still apply.
Operators control caching per allow rule and can restrict it to suitable clients, methods, hosts, and paths. Omit the rule's cache configuration where freshness-based reuse is unacceptable; such requests bypass existing entries as well as cache storage. Caching does not guarantee read-after-write consistency.
Supported Headers
- Cache-Control:
max-age,s-maxage,public,private,no-cache,no-store - Expires: HTTP date for cache expiration
- Date and Age: Used to account for a response's age before and while it is cached
- Vary: Cache keys include request headers specified by Vary
TTL Priority
Cache lifetime is chosen in this order:
s-maxage(shared cache max-age) - highest prioritymax-ageExpiresheaderforce_cache_durationfrom policy rule (fallback only)
The response's corrected age is subtracted from that lifetime, including when
force_cache_duration supplies the lifetime. Time spent receiving the response
body also consumes freshness. Cache hits replace any upstream Age field with
the response's current age.
What Gets Cached
- Methods: Only
GETandHEADrequests - Status codes: 200, 203, 204, 205, 301, 302
- Bypass: Requests with
AuthorizationorCookieheaders are never served from cache and are not stored. Requests with fixed-length or chunked bodies, or withRange, are also bypassed. - Not cached: Responses with
no-store,no-cache, orprivatedirectives, anySet-Cookieheader, or response trailers. HTTP/1 responses with transfer codings other than a solechunkedcoding are also forwarded without being stored.
Request Cache Directives
Request-side cache controls can force a bypass. If a request includes
Cache-Control: no-cache, Cache-Control: no-store, Cache-Control: max-age=0,
Pragma: no-cache, or Range, the cache will not be used and the response will
not be stored. ExfilGuard does not evaluate conditional validators locally;
requests with If-None-Match or If-Modified-Since are likewise forwarded
without cache lookup or storage. Otherwise, caching follows the upstream
response headers plus force_cache_duration from policy rules.
For fields named by Vary, ExfilGuard stores and compares the complete ordered
list of request field values. Requests with missing, extra, or reordered values
do not share a representation.
Eviction
The cache uses LRU eviction when capacity is reached. Expired entries are removed on lookup.
Layout and Sweeping
Cache entries live under a versioned subdirectory (v6 under the cache root).
Metadata is keyed by request URI and points to an immutable body generation.
When the layout changes, old version directories are deleted asynchronously. A
background sweeper runs every cache_sweeper_interval seconds and inspects up
to cache_sweeper_batch_size entries, removing expired entries and pruning
empty shard directories. It retains an in-memory offset so successive bounded
runs advance through the complete metadata set; restart resets the offset after
the startup scan has already examined every entry.
cache_total_capacity is an advisory bound for completed response bodies, not
a filesystem quota. In-progress cache fills share an additional
cache_max_entry_size staging allowance; when it is exhausted, those fills
skip caching without slowing or rejecting the forwarded responses. Operators
should therefore reserve at least the sum of both settings, plus space for
metadata and normal filesystem overhead. Partial staging files are removed as
soon as their fill is discarded.
Note
The cache does not support conditional revalidation (ETag/If-None-Match, Last-Modified/If-Modified-Since). Stale entries are discarded and fetched fresh from upstream.
Environment Variables
You can override any setting with environment variables. Use the
EXFILGUARD__ prefix and double underscores for nesting.
# Override listen address
EXFILGUARD__LISTEN="0.0.0.0:3128"
# Override log format
EXFILGUARD__LOG="text"
# Override timeouts
EXFILGUARD__CLIENT_KEEPALIVE_IDLE_TIMEOUT=60
EXFILGUARD__UPSTREAM_CONNECT_TIMEOUT=120
Complete Example
# Core settings
listen = "127.0.0.1:3128"
ca = { source = "builtin", dir = "./ca" }
clients = "clients.toml"
policies = "policies.toml"
clients_dir = "clients.d"
policies_dir = "policies.d"
# Logging
log = "text"
log_queries = false
# TLS
leaf_ttl = 86400
leaf_cache_capacity = 4096
leaf_mint_concurrency = 4
# Timeouts (seconds)
dns_resolve_timeout = 2
upstream_connect_timeout = 5
tls_handshake_timeout = 10
request_header_timeout = 10
request_body_idle_timeout = 30
response_header_timeout = 60
response_body_idle_timeout = 60
request_total_timeout = 0
client_keepalive_idle_timeout = 30
connect_tunnel_idle_timeout = 60
connect_tunnel_max_lifetime = 0
# Connection pool
upstream_pool_capacity = 32
# HTTP/2
http2_max_concurrent_streams = 100
# Size limits (bytes)
max_request_header_size = 32768
max_response_header_size = 32768
max_request_body_size = 0
# Cache (optional)
cache_dir = "./cache"
cache_max_entry_size = 104857600
cache_max_entries = 10000
cache_total_capacity = 1073741824
cache_sweeper_interval = 300
cache_sweeper_batch_size = 1000