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)!
    application: https
    action: permit
    description: Local Ubuntu Mirror
}

accept-mirror-switch: {
    match-uri-regex: ^(https?:\/\/)?.*mirror\.switch\.ch.*   # (2)!
    application: https
    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 apply to HTTPS traffic (application: https) and 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.

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 engine generates one host-anchored regex per line, with dots auto-escaped (post-2026-05-27 fix).
match-url-list Path to a file containing one URL per line. Similar to match-domain-list but matches against the full URI, not just the host.
match-sni TLS SNI literal (works in peek and bump modes; not in off).
match-sni-regex TLS SNI regular expression.
match-client-ip Client source IP (single address or CIDR).
match-user-agent HTTP User-Agent regular expression.
match-method HTTP method (GET, POST, …).

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.

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.txt
    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-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 in 2026.31.0 (EA) — 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 (EA) 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.txt
    pin: splice
    description: Microsoft cert-pinned endpoints
}

deny-pinned-app: {
    match-domain-list: /etc/enforcegate/rules.d/lists/blocked-pinned.txt
    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].

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.