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.
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.
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 #):
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:
warn-discord: {
match-domain-list: /etc/enforcegate/rules.d/lists/discord.list
action: warn
ack_scope: policy
description: Discord — proceed with caution
}
Companion 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.
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.
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.
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.
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.
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).
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
.— usematch-domain-listinstead, which auto-escapes. - Reference to a list file that doesn't exist at the path given to
match-domain-listormatch-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.