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 application, 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/
    application: https
    action: permit
    description: Debian package mirror
}

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

allow-python-pypi: {
    match-domain-list: /etc/enforcegate/rules.d/lists/pypi-mirrors.txt
    application: https
    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.txt
    application: https
    action: warn
    description: Social media — proceed with caution
}

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

rules.d/lists/social-media.txt
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.

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.txt
    application: https
    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.txt
    application: https
    action: deny
    description: Known malware distribution domain — blocked
}

block-phishing-domains: {
    match-domain-list: /etc/enforcegate/rules.d/lists/phishing-domains.txt
    application: https
    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/
    application: https
    action: permit
    description: Internal corp services
}

allow-saas-tools: {
    match-domain-list: /etc/enforcegate/rules.d/lists/approved-saas.txt
    application: https
    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/
    application: https
    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.

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.]+$
    application: https
    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/
    application: http
    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.