Skip to content

Policies

Policies define which subnets, hosts, user agents, SNI patterns, or other request attributes are permitted, denied, warned about, or shown an Acceptable Use Policy gate before they reach the proxy upstream. They are the primary mechanism by which operators control web traffic in EnforceGate vX.

Policies are ordered sets of rules. The fine-grained control they provide protects endpoints and infrastructure against a wide range of threats — C2 channels, malware downloads, data exfiltration, and other malicious activity.

The policy file format closely resembles JavaScript Object Notation (JSON) but is significantly more permissive: it supports inline and block comments, multi-line values, and unquoted keys and string values.

Rule files

Policy rules are defined in .policy files under /etc/enforcegate/rules.d/ inside the standalone container. The directory holds an arbitrary number of files; they are loaded in lexicographical order and the engine assigns rule ids in load order. The lowest-id matching rule wins on every match path — so the conventional two-digit precedence prefix (e.g. 05-, 40-, 90-) controls precedence by way of controlling the id. A rule in 10-allow-internal.policy always wins over a conflicting rule in 90-block-categories.policy. This approach is modelled on the popular udevd rules directory and significantly simplifies policy management.

Policies

A rule file consists of one or more policy blocks. Each block must contain a name and at least one match attribute.

10-packages-repo.policy
# Allow Package Mirrors

accept-ubuntu-initseven: {
    match-uri: https://mirror.init7.net/ubuntu/      # (1)!
    action: permit
    description: Local Ubuntu Mirror
}

accept-mirror-switch: {
    match-uri-regex: ^(https?:\/\/)?.*mirror\.switch\.ch.*   # (2)!
    action: permit
    description: SWITCH mirror
}
  1. Match all URIs that start with https://mirror.init7.net/ubuntu/.
  2. Match all URIs (HTTP or HTTPS, with or without scheme) whose domain is mirror.switch.ch. Regular-expression syntax — see the Squid connector reference for the full set of request attributes available to match against.

Both policies above use URI matching. The first uses a literal match-uri prefix; the second uses match-uri-regex (a regular expression). The action: permit allows the request to proceed. The scheme (http / https) is implicit in the URL itself — a separate application: attribute is not required and would produce a load-time Warning as of 2026.46.0.

No-match verdict — [policy].default_action

Requests that no rule matches receive the engine's synthesised default verdict, set by [policy].default_action. The shipped default is "permit" — non-disruptive for new deployments. To flip to a strict default-deny posture, set default_action = "deny" in engine.conf; for warn-by-default or AUP-gate-by-default, set "warn" or "aup". The verdict is engine-synthesised — it does not occupy a rule id, so it can never shadow a rule in [policy].shared_path. The verdict pages and show policy match report the synthesised rule name as default-permit / default-deny / default-warn / default-aup.

Pre-2026.32.0 deployments shipped a 99-default-permit.policy catch-all rule to encode the no-match verdict. That file is retired in 2026.32.0 — see the changelog.

Comment lines or blocks

.policy files support comments in three styles:

  • # this is a comment — line comment.
  • // this is also a comment — line comment.
  • /* this is a block comment */ — supports multi-line.

Available match attributes

The full match-attribute reference is part of the policy DSL specification (currently proprietary). The most commonly used:

Attribute Matches
match-uri URI literal prefix.
match-uri-regex URI regular expression.
match-domain-list Path to a file containing one domain per line. The file must end in .list — the engine rejects any other extension at policy load. The engine generates one host-anchored regex per line, with dots auto-escaped.
match-url-list Path to a file of literal URLs, one per line (# comments and blanks skipped — URLhaus / OpenPhish native format). The file must end in .list. Matched exactly after canonicalization (http/https unified, default ports stripped, dot-segments resolved, percent-encoding normalised; query kept, fragment dropped, trailing slash significant). Semantics changed in 2026.50.0 — lines were previously treated as left-anchored regexes; that behaviour moved to match-uri-regex-list below. A line that looks like a regex (leading ^ or a backslash) is skipped with a Warning.
match-urls Inline literal-URL array (match-urls: [ "https://evil.example/dropper.exe" ], 256-entry cap). Same canonical exact-match semantics as match-url-list. New in 2026.50.0.
match-uri-regex-list Path to a file of left-anchored regexes, one per line — the behaviour match-url-list had before 2026.50.0, under an honestly-named attribute. Use for pattern lists; use match-url-list for literal IOC feeds.
match-url-prefix-list Path to a file of literal URL prefixes, one per line — each entry covers its whole path subtree: evil.com/malware matches evil.com/malware and evil.com/malware/dropper.exe but not the sibling evil.com/malware-tools (boundaries are path segments). Query dropped and trailing slash stripped at canonicalization; a host-only entry (no path) is skipped — host-level blocking stays match-domain-list's job. New in 2026.50.0.
match-url-prefixes Inline prefix array (256-entry cap). Same subtree semantics as match-url-prefix-list. New in 2026.50.0.
match-client-ip Client source IP — single IPv4 / IPv6 address or CIDR (e.g. 10.1.65.9, 10.20.30.0/24). An inline array is accepted for a small set: match-client-ip: [ "10.1.65.0/24", "10.2.0.5" ]. Enforced in 2026.46.0+.
match-user-agent HTTP User-Agent regular expression. Canonical spelling as of 2026.46.0; historical match-useragent still accepted as a legacy alias.
match-method HTTP method (GET, POST, …), case-insensitive. Enforced in 2026.46.0+.
match-sni TLS SNI literal — a single hostname (match-sni: bank.example.com) or an inline array (match-sni: [ "a.com", "b.com" ]). Enforced on the SSL-bump peek path from 2026.47.5 onwards. Requires a pin: verdict — a match-sni on an action: rule (rather than pin:) is rejected at reload; SNI matching only exists on the bump path. Host-suffix walking, same as match-domains (bank.example.com matches login.bank.example.com too). See Pinned destinations.
match-sni-regex TLS SNI regular expression. Roadmap — the pin index is a host-suffix exact map, not a regex engine; the literal match-sni above covers the common case via the suffix walk.
match-category One or more categories from the Exosys Curated Domain Lists feed — a bareword (match-category: gambling) or a quoted array (match-category: [ "social", "streaming" ]). See Managed categories.

Multiple match-* attributes on the same rule are AND-combined — the rule matches only when the URL / domain dimension and every other match-* gate (client IP, method, user-agent, time window) hold. For an OR, split into separate rules. match-client-ip, match-method, match-user-agent, and time-window: are secondary gates in the verdict path: a URL-matched rule is accepted only if the request's client IP, method, User-Agent, and time-of-day satisfy every gate the rule specifies; otherwise the request falls through to the next lowest-id rule.

match-url-list changed meaning in 2026.50.0

Before 2026.50.0, each line of a match-url-list file was treated as a left-anchored regex (metacharacters live). From 2026.50.0 each line is a literal URL, matched exactly after canonicalization — so ? and . no longer act as regex operators, and lists pulled from threat feeds (URLhaus, OpenPhish) work verbatim without escaping. Operators who relied on the regex behaviour should rename the attribute to match-uri-regex-list — the file format and the matching are identical to the old code. The engine flags likely-regex lines (leading ^ or a backslash) in a literal list with a load-time Warning naming the fix.

Hand-written regex pitfalls

Operators writing match-uri-regex by hand should remember to escape ., anchor with ^https?:// or ^(https?://)?, and end with (/|$) to avoid sub-string matches against unrelated hosts. See troubleshooting for the canonical pitfalls list. match-domain-list does these transformations automatically. For literal URL lists (threat-feed IOCs), skip regex entirely — match-url-list matches exactly with no escaping needed.

Time-scheduled rules

Any rule can carry an optional time-window: attribute. The rule only matches during the window; outside it, the rule behaves as if it were not loaded and the request falls through to the next rule. A rule with no time-window: is always active — the attribute is purely additive, and every existing policy keeps working unchanged.

The grammar is time-window: [<days>] HH:MM-HH:MM:

Token Meaning
<days> (optional) One of daily (default), weekdays (Mon–Fri), weekend (Sat + Sun), or a comma-separated list of 3-letter day names: mon, tue, wed, thu, fri, sat, sun (any subset, any order).
HH:MM-HH:MM 24-hour clock range. If start > end, the window wraps past midnighttime-window: daily 22:00-06:00 is active from 22:00 until 06:00 the next morning. start == end is rejected as ambiguous.
40-block-social-media.policy
block-social-business-hours: {
    match-domain-list: /etc/enforcegate/rules.d/lists/social-media.list
    action: deny
    description: No social media during work hours
    time-window: weekdays 08:00-18:00
}

time-window: works on every rule shape — match-uri, match-uri-regex, match-domain-list, match-url-list / match-urls, match-url-prefix-list / match-url-prefixes, match-uri-regex-list, match-category, match-sni, match-client-ip, match-user-agent, match-method.

Fall-through, not flip — the important mental model

When a rule's window is closed, the engine treats the rule as non-existent for that request. The match falls through to the next-applicable rule, and ultimately to the catch-all baseline. A closed window does not turn a permit into a deny (or vice versa).

This composes with the lowest-id-wins precedence operators already know. Worked example:

10-allow-vendor.policy   permit  time-window: weekdays 08:00-18:00
90-deny-vendor.policy    deny
  • During weekday business hours: the id-10 permit wins (lowest id), vendor traffic is allowed.
  • Outside that window: the id-10 rule does not match, so the id-90 deny takes over.

A time-limited permit placed above a broad deny therefore gives "allow only during these hours, deny the rest of the time" with no extra rules. A time-limited deny placed above a permit gives the inverse.

The engine re-evaluates every request — there is no "session already allowed" carry-over. When the clock crosses the window edge, the next request gets the new verdict immediately.

Timezone

Windows evaluate against the engine's local wall clock by default. The [policy].time_window_tz knob in engine.conf switches this to UTC — see [policy] reference. On a local deployment, keep NTP in sync: a wrong system clock makes time-windows fire at the wrong times. Confirm what "local" means on the engine with show clock.

Validation

  • A malformed time-window: in a hand-authored [policy].path file fails the reload loudlyegpolicy load exits non-zero and a live request policy reload reports failure, naming the file, the rule, and the problem. The previous good policy stays live (same strict-file behaviour as any other parse error).
  • A malformed time-window: in a toolbox shared-dir file ([policy].shared_path) is not fatal — that one rule loads always-active with a Warning, and the rest of the load proceeds.

Scope of the 2026.31.0 implementation

The time-window: attribute described above is shipped and supported as of 2026.31.0 — author it directly in any .policy file under [policy].path or [policy].shared_path and the engine enforces it. Two adjacent capabilities other gateway products expose are deferred to a future release and are not yet available:

  • Absolute one-shot windows — a specific calendar date range (Cisco IOS absolute / Palo Alto non-recurring). Today's time-window: is recurring weekly/daily only.
  • Named, reusable time-range objects — defining BUSINESS-HOURS once and referencing it from many rules. Today the window is inline per rule; if two rules need the same hours, each spells out its own time-window:.

Pinned destinations

Certificate-pinned destinations — Windows Update, Apple MDM, mobile banking, several enterprise SaaS clients — refuse the forged leaf certificate Squid mints during bump-mode SSL inspection and fail closed. The historical workaround was a static bypass file outside the policy system; from 2026.35.0 onwards, pinned destinations are handled by policy, using the same match-* attributes and the same loader as block / permit rules.

A pin rule carries a pin: attribute instead of action::

20-pinned.policy
pin-microsoft-update: {
    match-domain-list: /etc/enforcegate/rules.d/lists/pinned-microsoft.list
    pin: splice
    description: Microsoft cert-pinned endpoints
}

deny-pinned-app: {
    match-domain-list: /etc/enforcegate/rules.d/lists/blocked-pinned.list
    pin: terminate
    description: Block this pinned app entirely
}

The three pin: verdicts:

Verdict What Squid does at the peek step
splice Pass the connection through unchanged — hostname-only visibility, no content inspection. The right answer for "let it work without breaking pinning."
bump Inspect as normal. Only useful for destinations that don't actually pin (or where the operator has installed the bump CA into the client trust store).
terminate Refuse the connection at the TLS handshake. The client sees a connection failure; no captive-portal verdict page is rendered (the TLS tunnel is never established).

Matching is suffix-walk, identical to match-domain-list: for action: rules. A list entry windowsupdate.com pins *.windowsupdate.com; push.apple.com pins only *.push.apple.com, not apple.com. A host with no matching pin: rule defaults to bump — the engine inspects as normal.

pin: rules are orthogonal to action: rules. The pin: verdict is consulted at the SslBump peek step, before any certificate is forged; action: is consulted post-bump against the inspected request. A destination can be pinned (splice) and also have a permit / deny action: rule — the splice decides whether Squid sees the cleartext, the action decides what the engine does with it. For a host pinned to splice, the engine sees the SNI hostname only and any action: rule must match on that.

Pinning trades visibility for compatibility

A pinned destination cannot be content-inspected — splice lets it through with hostname-only visibility; terminate blocks it at the handshake. This is a property of how certificate pinning works, not a limitation of EnforceGate vX — every gateway product faces the same constraint. The value of the policy-based approach is that the engine's policy decides which destinations are pinned (not a static bypass file outside the policy system), so pin decisions are reloaded, snapshotted, git-tracked, and visible in show policy list alongside every other rule.

The connector's [ssl_bump_acl].fail_action knob controls what happens when the connector can't reach the engine for a peek-step verdict (engine down, session timeout). Default splice for availability — pinned apps keep working through an engine blip instead of breaking under a forced bump. Switch to bump for inspection-first deployments or terminate for fail-closed postures.

Pin rules are introspectable through the same verbs as action: rules — see show policy match, show policy list, and show policy summary for the operator-side views.

Available actions

Action Effect
permit Allow the request to proceed unchanged.
deny Block the request. The captive portal renders the block page.
warn Block the request with a Proceed-anyway CTA. The captive portal renders the warn page.
aup Show an Acceptable Use Policy gate. Visitor must accept before proceeding.

warn and aup verdicts require the engine to see a usable client IP — that is, request_line_format = 1 (QF-3) on the connector side, since only QF-3 carries the client IP all the way through to the engine. With request_line_format = 0, warn/aup degrade to a terminal block page with no actionable CTA. See Squid connector — [redirector].

Managed categories

Managed-category feed

The match-category: attribute draws on the Exosys Curated Domain Lists feed, which ships in every edition. An engine running on the reduced-capability floor (see never bricks on a license lapse) has the feed disabled, and match-category rules will not match until a valid license is restored.

Exosys Curated Domain Lists are a signed, on-box managed-category feed — 50+ maintained categories (adult content, gambling, malware, phishing / threat-intel, ad-trackers, social networks, streaming, and more) that operators enforce with ordinary .policy rules. Unlike the community script ecosystem (where the operator sources and refreshes their own lists), the curated feed is maintained by Exosys and shipped as a single sealed file.

A rule gates on one or more categories with match-category::

30-managed-categories.policy
deny-malware-and-phishing: {
    match-category: [ "malware", "phishing" ]
    action: deny
    description: Known-bad — blocked by Exosys curated threat feed
}

warn-gambling: {
    match-category: gambling
    action: warn
    description: Gambling — proceed with caution
}

match-category: composes with every other rule dimension — time-window:, match-client-ip:, match-method:, ack_scope: — and all four actions, inside the usual lowest-id-wins precedence. Matching is a single bounded host-suffix walk per request (host, then parent labels down to the registrable domain, Public-Suffix-List-aware so shared-host tenants stay isolated), then a bitwise category test — independent of the taxonomy size, so a rule against many categories costs no more than a rule against one.

Offline by design. The feed is a single file (/var/lib/enforcegate/managed.egsf by default) downloaded once at install and loaded at engine boot — no cloud lookup on the request path, and the deployment stays fully air-gapped after the initial download. Operators who opt in can pull incremental feed generations from the Exosys CDN (the feed publishes several times a day); a new generation is applied with request feed reload without an engine restart. The feed and its refresh cadence are configured in the [feed] block of engine.conf; the loaded generation and category counts are visible with show feed.

Managing policies

Two operator interfaces author and load policies:

  • eghost policy — host-side operator commands. The recommended path for day-to-day work. Wraps file authoring (new, edit, show, list, remove) plus the compile-and-reload cycle, with $EDITOR integration. new, edit and remove recompile the policy set and tell the engine to reload on save / confirm — no separate reload step needed.
  • egpolicy — the engine compilation utility that eghost policy calls. Use it directly only for bind-mount or docker cp edits, CI lints, and offline rebuilds.

For the online policy-reload verb operators run from the egctl REPL, see the egctl reference.