Skip to content

Policy recipes

Concrete .policy examples covering the patterns operators (and the LLMs they ask to draft policies for them) reach for most often. Each recipe is a complete, drop-in file: copy it into /etc/enforcegate/rules.d/, run eghost policy reload, and the rule is live.

The pages it draws on:

  • Policies — file location, evaluation order, comment styles.
  • Policy reference — full grammar: every match attribute, every action.
  • Policy rollback — snapshot and recovery after a bad reload.

How LLMs should use this page

Recipes are full file contents, not snippets — each block has a name, at least one match attribute, an action, and a description. When adapting one, change the name, the match value, and the description to your case; the rest of the shape is usually correct as-is.

.policy is a permissive JSON-like grammar. Unquoted keys and string values are fine, but a syntactically-broken file is rejected at compile time — always run eghost policy reload after editing and read the validation output.

Allow specific package mirrors

Default-permit deployments still benefit from explicit allow rules early in the file: the engine logs the matched rule name on every verdict, which makes audit grep significantly easier.

05-allow-package-mirrors.policy
allow-debian-mirror: {
    match-uri: https://deb.debian.org/debian/
    action: permit
    description: Debian package mirror
}

allow-ubuntu-mirror: {
    match-uri-regex: ^https?://[^/]*\.ubuntu\.com/(ubuntu|releases)/
    action: permit
    description: Ubuntu archives and releases (any mirror)
}

allow-python-pypi: {
    match-domain-list: /etc/enforcegate/rules.d/lists/pypi-mirrors.list
    action: permit
    description: PyPI and trusted mirrors
}

Why: match-uri is a literal prefix — cheap and unambiguous for a single host. match-uri-regex covers a host pattern. match-domain-list lets ops curate a long allow-list of mirrors in a separate file without touching the policy.

Block social media (warn-with-proceed pattern)

Friendlier than a hard deny. The user lands on the captive portal, reads the explanation, and can click Proceed anyway — the engine records the acknowledgement and permits subsequent requests from the same (rule_name, client_ip) pair for a configured ack window.

40-warn-social-media.policy
warn-social-media: {
    match-domain-list: /etc/enforcegate/rules.d/lists/social-media.list
    action: warn
    description: Social media — proceed with caution
}

Companion list file (one domain per line, comments with #):

rules.d/lists/social-media.list
facebook.com
instagram.com
tiktok.com
twitter.com
x.com
# add more as needed

Why: match-domain-list auto-handles the www. prefix and TLD dot-escaping; hand-written regex doesn't. The ack window is set on the engine side via [captive_portal].ack_session_duration_s — see the engine reference.

Multi-domain services — Discord, Slack, Teams

Some applications fan out across many destination domains and subdomains (a chat app pulling assets from one CDN, voice from another, telemetry from a third). A single warn rule that matches all of them will fire once per domain by default — the user clicks "Proceed anyway" on the captive portal, then immediately sees another warning the moment the app fetches an asset from a sibling domain.

The recommended pattern is one dedicated policy per multi-domain service, with ack_scope: policy on the rule so a single acknowledgement covers every domain in the rule's list:

50-warn-discord.policy
warn-discord: {
    match-domain-list: /etc/enforcegate/rules.d/lists/discord.list
    action: warn
    ack_scope: policy
    description: Discord — proceed with caution
}

Companion list:

rules.d/lists/discord.list
discord.com
discord.gg
discord.media
discord.app
discordapp.com
discordapp.net
discordcdn.com
# add more as Discord rolls out new properties

Why: with ack_scope: policy, the engine keys the acknowledgement on (client_ip, rule_name) — so once the user clicks "Proceed anyway" on discord.com, the same ack covers discord.media, discordcdn.com, and every other host the rule matches for the configured ack window. Without it, ack_scope defaults to hostname and each new domain re-triggers the warn page mid-session, which most users perceive as broken behaviour rather than a deliberate policy.

The same pattern fits Slack, Microsoft Teams, Zoom, and other apps whose runtime touches a fan-out of host families. Use one .policy file per service so the lists stay scoped, the audit log stays readable, and a future exception (pin: splice for the cert-pinned host, a tighter action: for one sub-domain) lives next to the rule it modifies.

ack_scope: domain for whole-site sweeps

For a single registrable site that owns many subdomains (e.g. bbc.co.uk with news., m., cdn., …), ack_scope: domain keys the acknowledgement on the registrable domain instead of the matched host — one click-through unlocks every subdomain under that site, while a sibling registrable domain still warns independently. Use domain when the list covers one site; use policy when the list spans multiple distinct sites (Discord, Slack).

Acceptable Use Policy gate on large download services

The aup action redirects through the captive portal to a separate page where the visitor must explicitly accept an AUP (typically a checkbox + a longer policy text) before proceeding. Useful for streaming, cloud storage, and large download sites where bandwidth is the concern.

50-aup-cloud-storage.policy
aup-cloud-storage: {
    match-domain-list: /etc/enforcegate/rules.d/lists/cloud-storage.list
    action: aup
    description: Cloud storage — acknowledge fair-use policy before continuing
}

Why: AUP is a distinct flow from warn — different page, different rendered copy, but the same operator ergonomics. The captive portal renders the page in the visitor's language (English, French, German, Italian — see captive portal).

Block known malware / phishing domains

Hard deny — no captive-portal click-through. The visitor lands on the block page with the rule's description as the explanation.

20-block-threats.policy
block-malware-domains: {
    match-domain-list: /etc/enforcegate/rules.d/lists/malware-domains.list
    action: deny
    description: Known malware distribution domain — blocked
}

block-phishing-domains: {
    match-domain-list: /etc/enforcegate/rules.d/lists/phishing-domains.list
    action: deny
    description: Known phishing host — blocked
}

Why: Two separate rule names so the audit log distinguishes malware blocks from phishing blocks at a glance. The companion lists are typically refreshed from a threat-intel feed — drop the new file in place and run eghost policy reload.

Allow only listed internal services (default-deny posture)

The strict pattern. Set the engine's no-match verdict to deny via [policy].default_action, then ship explicit permit rules for everything operators want to allow.

engine.conf
[policy]
default_action = "deny"
10-allow-internal-services.policy
allow-internal-corp-domain: {
    match-uri-regex: ^https?://[^/]*\.corp\.example\.com/
    action: permit
    description: Internal corp services
}

allow-saas-tools: {
    match-domain-list: /etc/enforcegate/rules.d/lists/approved-saas.list
    action: permit
    description: Approved SaaS tools
}

Why: default_action is the engine-synthesised no-match verdict — a request that no rule matched is denied without consuming a rule id, so there is no catch-all rule to shadow lower-precedence [policy].shared_path rules. Flipping posture is a one-line config change; no policy-file surgery needed. The captive-portal block page reports the rule name as default-deny for matched-by-default verdicts. See [policy].default_action for the full knob reference and Policies for the rule-id precedence model.

Restrict access by client subnet

Useful for segregating guest networks, admin-only access to internal services, or carve-outs for printer subnets that shouldn't browse.

30-guest-network.policy
guest-deny-corp-domain: {
    match-client-ip: 10.20.30.0/24
    match-uri-regex: ^https?://[^/]*\.corp\.example\.com/
    action: deny
    description: Guest subnet may not reach internal corp services
}

Why: Multiple match-* attributes in one block are an AND — the rule matches only when both the client IP is in the guest subnet and the URI is a corp host. For OR semantics, write two separate rules. match-client-ip accepts a single address, a CIDR, or an inline array for a small set (match-client-ip: [ "10.1.65.0/24", "10.2.0.5" ]).

Requires 2026.46.0 or later

match-client-ip and match-method were documented but silently ignored in older engines — a rule that used them was applied network-wide, not just to the specified subnet or method. Enforced correctly as of 2026.46.0; earlier engines emit a load-time Warning naming the file and the ignored attribute. Verify with show policy match <url> source-ip <ip> before shipping subnet-scoped rules to production.

Per-segment policy — guest networks, server farms, printer VLANs — is now expressible in a single engine by scoping rules with match-client-ip and ordering them by rule id, no separate proxy path required. The canonical shape is a low-id permit for the segment's allowed destinations followed by a low-id match-client-ip + match-uri-regex: ^https?:// deny; category rules for everyone else sit at higher ids.

Allow a specific User-Agent everywhere (CI / automation carve-out)

Common request: a CI runner needs unrestricted egress without weakening the policy for users. Match by User-Agent and place the rule early.

03-allow-ci-runner.policy
allow-ci-runner: {
    match-user-agent: ^my-ci-runner/[0-9.]+$
    action: permit
    description: Internal CI runner — unrestricted egress
}

Why: 03- puts this before the threat-block rules at 20-, so the CI runner isn't accidentally caught by them. The regex anchors with ^ and $ so unrelated agents that happen to contain the string my-ci-runner somewhere don't match.

Force HTTPS-only on a sensitive domain

Block plain HTTP to a particular host (useful when you're confident the host is HTTPS-capable and want to refuse clients that don't upgrade).

35-https-only-bank.policy
deny-http-bank: {
    match-uri-regex: ^http://[^/]*\.bank\.example\.com/
    action: deny
    description: Bank domain must be reached over HTTPS
}

Why: application: http scopes the rule to the plain-HTTP listener. The companion permit for HTTPS is the default (no explicit rule needed if your default is permit).


Companion CLI

After dropping any of the above into /etc/enforcegate/rules.d/, validate and apply:

# Dry-run parse + compile without activating
eghost policy reload --dry-run

# Apply
eghost policy reload

# Verify a specific URL against the live policy
eghost cli
> show policy match https://facebook.com/

show policy match returns the matched rule name and verdict — the fastest way to verify a recipe is doing what you intended. See egctl for the full diagnostic verb.

When the engine refuses to reload

If policy reload fails, the engine keeps the previously-loaded policy live and reports the validation error. Common causes:

  • Missing , between attributes inside a block.
  • Hand-written regex with un-escaped . — use match-domain-list instead, which auto-escapes.
  • Reference to a list file that doesn't exist at the path given to match-domain-list or match-url-list.
  • Catch-all without match-* — every block needs at least one match attribute, even the default.

The validation output names the offending file and the line; see troubleshooting for the pitfalls list.