Clients & Policies
Use clients and policies to decide who can reach what.
Client Configuration
Clients map source IP addresses to policies. Define them in clients.toml or
files in clients.d/.
Client Fields
| Field | Type | Required | Description |
|---|---|---|---|
name |
String | Yes | Unique identifier for the client |
ip |
String | One of ip/cidr | Single IP address (e.g., "127.0.0.1", "::1") |
cidr |
String | One of ip/cidr | CIDR block (e.g., "10.0.0.0/8", "2001:db8::/32") |
policies |
Array | Yes | List of policy names to apply in order |
fallback |
Boolean | No | Mark as fallback client (exactly one required) |
Matching
Client selectors must not overlap, so each source IP maps to at most one non-fallback client. If nothing matches, ExfilGuard uses the fallback client.
Validation Rules
- Client names must be unique
- Either
iporcidrmust be specified, not both - Non-fallback selectors must not overlap (IP vs IP, IP vs CIDR, CIDR vs CIDR)
- Exactly one client must have
fallback = true - All referenced policies must exist
Example
# Analytics workers subnet
[[client]]
name = "analytics-workers"
cidr = "10.42.16.0/27"
policies = ["analytics-policy", "fallback-deny"]
# Payment gateway subnet
[[client]]
name = "payments-gateway"
cidr = "10.42.48.0/28"
policies = ["payments-policy", "fallback-deny"]
# Localhost for testing
[[client]]
name = "loopback"
ip = "127.0.0.1"
policies = ["local-allow"]
# Fallback: deny everything else
[[client]]
name = "fallback"
cidr = "0.0.0.0/0"
policies = ["default-deny"]
fallback = true
Policy Configuration
Policies contain ordered rules that allow or deny requests. Define them in
policies.toml or files in policies.d/.
Policy Structure
Rules run in order. The first matching rule decides the action.
Rule Fields
| Field | Type | Default | Description |
|---|---|---|---|
action |
String | Required | "ALLOW" or "DENY" |
methods |
Array | ["ANY"] |
HTTP methods to match (non-CONNECT by default) |
url_pattern |
String | None | URL pattern to match (see syntax below) |
https_mode |
String | "inspect" |
HTTPS handling mode: "inspect" or "tunnel" |
cache |
Table | None | Cache configuration (see below) |
status |
u16 | Required for DENY | HTTP status code for denial response |
reason |
String | None | HTTP reason phrase (DENY only) |
body |
String | None | Response body (DENY only) |
ALLOW vs DENY
ALLOW Rules
- Permit the request to proceed upstream
- Must not set
status,reason, orbody - Can use
https_mode = "tunnel"for explicit CONNECT tunneling
DENY Rules
- Block the request with specified response
- Must set
status(HTTP status code) - Optional:
reasonandbody
HTTP Methods
Valid method values:
"ANY"- Matches all non-CONNECT methods (default)"GET","POST","PUT","PATCH","DELETE""HEAD","OPTIONS","TRACE","CONNECT"
# Single method
methods = ["GET"]
# Multiple methods
methods = ["GET", "POST", "DELETE"]
# Any method (default)
methods = ["ANY"]
Note
Cannot mix "ANY" with explicit methods in the same array.
"CONNECT" must be the only method in its rule.
ExfilGuard evaluates CONNECT requests against explicit CONNECT tunnel rules first. If no tunnel rule matches, it can still perform a TLS bump preflight when matching HTTPS inspect rules exist for the same host and port. It then attaches the real policy decision to the inner HTTP request. ExfilGuard still blocks private upstream addresses in every mode. That includes plain HTTP, tunneled CONNECT, and bumped HTTPS. The point is to reduce SSRF risk and stop clients from reaching internal address space by mistake.
URL Pattern Syntax
URL patterns follow the format: scheme://host[:port][/path]
Scheme
Use http or https.
Host Matching
| Pattern | Matches |
|---|---|
example.com |
Exact domain |
*.example.com |
Exactly one subdomain label of example.com |
**.example.com |
Any depth of subdomains of example.com (one or more) |
example.** |
Any suffix depth under example |
* |
Any host |
192.0.2.1 |
Exact IPv4 address |
[2001:db8::1] |
Exact IPv6 address (bracketed) |
Note
Host matching is case-insensitive. Wildcards must be whole labels:
*.example.com and **.example.com are valid, but a*b.com is not.
* matches one label. ** matches one or more labels.
Port
Optional. Defaults to 80 for HTTP, 443 for HTTPS.
Path Matching
| Pattern | Matches |
|---|---|
/api/v1/users |
Exact path |
/users/* |
Single segment: /users/123, /users/abc |
/api/** |
Any depth: /api/v1, /api/v1/users/123 |
/users/*/profile |
/users/123/profile, /users/abc/profile |
Note
Policy path matching uses a canonical path view. It ignores query strings,
normalizes literal . and .. segments, and rejects ambiguous path
syntax instead of rewriting it. ExfilGuard still preserves the raw request
target for upstream forwarding, so signed requests keep their original
path bytes.
Note
Requests are rejected if the path contains invalid escapes, backslashes,
encoded path separators, or encoded dot-segments such as %2e%2e.
Complete Examples
# HTTPS to specific API endpoint
"https://api.example.com/v1/exports/**"
# Any subdomain of partner.com on custom port
"https://*.partner.com:8443/payments/**"
# HTTP to any host, specific path
"http://*/health"
# IPv6 address
"https://[2001:db8::1]/api/**"
HTTPS Modes
https_mode controls how ExfilGuard handles HTTPS traffic.
https_mode = "inspect" (default)
- Decrypted HTTP requests are checked after TLS interception
- TLS is terminated and re-encrypted
- Used for normal HTTPS
GET/POST/PUT/DELETEstyle rules - Matching HTTPS rules authorize a TLS bump preflight for the same host/port
- The real policy decision is attached to the inner HTTP request
- Private upstream resolution is still blocked
- Enables normal request logging, metrics, and caching when configured
- Request metrics use
effective_mode="bump"on the inspected inner request
https_mode = "tunnel"
- Traffic is tunneled without inspection
- Only valid with
methods = ["CONNECT"] - Only valid with URL pattern path
/**(e.g.,https://secure.partner.com/**) - CONNECT tunnel rules must appear before non-CONNECT rules inside each policy
- Request metrics use
effective_mode="tunnel"on the CONNECT tunnel request - Useful for certificate-pinned services that refuse TLS interception
Warning
Tunnel mode bypasses content inspection. Use it only when necessary, such as payment gateways with certificate pinning.
Response Caching
Rules can enable caching for matched responses when the shared cache is configured globally.
[[policy.rule]]
action = "ALLOW"
methods = ["GET"]
url_pattern = "https://cdn.example.com/**"
[policy.rule.cache]
force_cache_duration = 3600 # Fallback: cache for 1 hour
| Field | Type | Description |
|---|---|---|
force_cache_duration |
u64 | Fallback cache lifetime in seconds (used only when upstream sends no cache headers) |
How Caching Works
The cache follows standard HTTP caching headers from upstream:
- Cache-Control:
s-maxage,max-age,public,private,no-cache,no-store - Expires: HTTP date for expiration
- Vary: Responses vary by specified request headers
force_cache_duration is a fallback only. It does not override upstream
freshness. It applies when the upstream response omits s-maxage, max-age,
and Expires, including cases where only public is set.
Note
Only GET and HEAD responses with status 200, 203, 204, 205, 206, 301, or 302 are cached. See Cache Settings for full details.
Complete Examples
Deny-All Fallback
[[policy]]
name = "fallback-deny"
[[policy.rule]]
action = "DENY"
status = 470
reason = "Policy Blocked"
body = "Blocked by ExfilGuard\n"
Allow Specific API Endpoints
[[policy]]
name = "api-policy"
[[policy.rule]]
action = "ALLOW"
methods = ["GET", "POST"]
url_pattern = "https://api.trusted.com/v1/exports/**"
[[policy.rule]]
action = "ALLOW"
methods = ["ANY"]
url_pattern = "https://reports.trusted.com/dashboards/**"
Certificate-Pinned Service (Tunnel)
[[policy]]
name = "pinned-payments"
[[policy.rule]]
action = "ALLOW"
methods = ["CONNECT"]
url_pattern = "https://secure.partner.com/**"
https_mode = "tunnel"