Skip to content

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 Non-fallback only: one of ip/cidr Single IP address (e.g., "127.0.0.1", "::1")
cidr String Non-fallback only: 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. The fallback has no selector and is never part of IP/CIDR matching. A fallback that sets ip or cidr is invalid.

IP literals in URL patterns are compared by address value, so equivalent compressed and expanded IPv6 spellings match the same rule.

Validation Rules

  • Client names must be unique
  • Every non-fallback client must specify either ip or cidr, not both
  • The fallback client must not specify ip or cidr
  • Non-fallback selectors must not overlap (IP vs IP, IP vs CIDR, CIDR vs CIDR)
  • IPv4-mapped IPv6 selectors such as ::ffff:10.0.0.5 are rejected; use the ordinary IPv4 address or CIDR because mapped peers have IPv4 client identity
  • 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"
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

[[policy]]
name = "policy-name"
  [[policy.rule]]
  # rule fields...

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 Final error status from 400 through 599
reason String None HTTP/1 reason phrase (DENY only; ignored by HTTP/2)
body String None Response body (DENY only)

ALLOW vs DENY

ALLOW Rules

  • Permit the request to proceed upstream
  • Must not set status, reason, or body
  • Can use https_mode = "tunnel" for explicit CONNECT tunneling

DENY Rules

  • Block the request with specified response
  • Must set status to a final error code from 400 through 599; unregistered codes within that range, such as 470, are supported
  • Optional: reason and body. The reason phrase is used only for HTTP/1 and may not contain status-line control bytes; HTTP/2 carries no reason phrase

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 ALLOW rules exist for the same host and port. It then attaches the real policy decision to the inner HTTP request. A deny-only authority fails at the outer CONNECT without opening an upstream connection. 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.

url_pattern = "https://example.com:8443/api/**"

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
/files/\* Exact path containing a raw asterisk: /files/*
/files/%2A Exact path containing a percent-encoded asterisk: /files/%2A

Note

Policy path matching uses a canonical path view. It ignores query strings, decodes percent-encoded RFC-unreserved characters, normalizes other valid escapes to uppercase hex, 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.

Patterns themselves must already use that canonical shape. A pattern with a . or .. segment or repeated / separators is rejected instead of being accepted as a rule that requests cannot match. Literal path characters follow RFC 3986 pchar syntax. Retained percent escapes must use uppercase hex, while escapes for unreserved characters must be written as the decoded character (for example, use ~, not %7E).

Note

An unescaped * is a wildcard. Use \* to match a raw asterisk exactly; backslash cannot escape any other character. This is distinct from %2A, which matches a percent-encoded asterisk, and %252A, which matches the literal text %2A in a request path.

TOML literal strings keep the backslash as written:

url_pattern = 'https://example.com/files/\*'

In a TOML basic (double-quoted) string, write the same pattern with a doubled backslash: "https://example.com/files/\\*".

Note

Requests are rejected if the path contains characters outside RFC 3986 path syntax, invalid escapes, raw or encoded backslashes, or encoded dot-segments such as %2e%2e.

Percent-encoded reserved characters remain encoded for policy matching, with uppercase hexadecimal digits. For example, npm's scoped-package path /@scope%2fname has the canonical policy path /@scope%2Fname, which remains distinct from /@scope/name.

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/DELETE style rules
  • Matching HTTPS ALLOW rules authorize a TLS bump preflight for the same host/port; deny-only authorities fail at the outer CONNECT
  • 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. The same rule behavior and cache entries apply to inspected HTTP/1.1 and HTTP/2 traffic.

[[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, at most 2147483648 (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 the complete ordered value list of each specified request header

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. Zero, expired, malformed, or duplicated freshness metadata makes the response stale; it is not treated as omitted metadata and does not activate the fallback. Oversized origin max-age and s-maxage values use HTTP's standard 2147483648-second overflow value.

Note

Only bodyless GET and HEAD responses with status 200, 203, 204, 205, 301, or 302 are cached. Requests with a fixed-length or chunked body, or with Range, bypass both cache lookup and storage. 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"

Cached Static Content

[[policy]]
name = "cached-content"
  [[policy.rule]]
  action = "ALLOW"
  methods = ["GET"]
  url_pattern = "https://cdn.example.com/**"
    [policy.rule.cache]
    force_cache_duration = 3600