Policy audit and history¶
EnforceGate vX can optionally maintain a per-commit audit trail of every policy reload, on top of the snapshot-based recovery system covered in policy rollback. The feature is opt-in and idle by default — snapshot-only deployments behave exactly as they always have. When enabled, the engine records who changed what, when, and why, and exposes the history through a small set of operator verbs modelled on Junos.
The audit trail is implemented as a git repository inside [policy].path. Operators don't need to invoke git directly to use the feature; the engine wraps every commit it writes behind verbs like show policy log, show policy blame <rule>, and request policy tag.
When to enable¶
Enable when:
- You have a single engine writing to a single local policy directory (the supported shape today).
- Audit / compliance obligations make per-commit attribution worth the operational discipline of running
request policy git-initonce. - Your operations team has Junos or Cisco IOS muscle memory and values a canonical history-and-diff verb surface (
show configuration log/show configuration | compare/show system commit).
Stay snapshot-only when:
- You have multiple engines pulling
.policyfiles from a shared NFS export, an external git repository, or a configuration-management system (Ansible, Puppet, Salt). The engine's commit-on-reload would fight an external authoritative source. [policy].pathlives on a volatile filesystem where the.git/directory might disappear between reboots.- Your operators have no git muscle memory and the audit-trail value isn't compelling enough to justify the learning curve — the default
automode keeps these deployments on snapshot-only automatically.
Enabling on an existing deployment¶
The migration is one command from the REPL. No data migration; the existing snapshot history stays valid and new reloads add commits on top.
eghost cli
host> enable
host# request policy git-init
Initialising git tracking at /etc/enforcegate/rules.d/...
baseline commit: a7f3e2c (12 files, 47 rules tracked)
.gitignore written (8 patterns)
Git mode is now active. Reloads will be recorded as commits.
host# show policy fingerprint
Current HEAD
SHA: a7f3e2c2...
Author: admin@host01
Date: 2026-06-12 14:00
Signature: (unsigned)
The request policy git-init verb wraps the equivalent shell steps so operators don't have to leave the REPL. If you prefer to drive git init from the host (for example, to control the initial branch name or to seed the repo from an existing template), the equivalent shell flow is:
# Stop the engine cleanly, initialise from the host, restart.
eghost down
cd /etc/enforcegate/rules.d
git init --initial-branch=main
git add -A
git commit -m "baseline"
eghost up
Either path produces the same result. The engine auto-detects the .git/ directory on its next start and begins committing reloads automatically.
The standalone bundle's container image includes the git binary on PATH. Hand-rolled deployments need to ensure git is installed in the engine container — if it isn't, [policy].git_mode = "auto" silently falls back to snapshot-only and operators will see no log entries.
Auto-managed .gitignore
On first detection of .git/, the engine writes (or merges) a .gitignore at the policy repo root that excludes sensitive material — *.pem, *.crt, *.key, *.lic, passwd, license.lic, the /ssl/ subdirectory, swap files. If you already wrote a .gitignore, the engine merges — your patterns are preserved verbatim and the engine's patterns are appended under a labelled section. The merge is idempotent: on subsequent boots with all patterns already present, the engine doesn't re-append the header.
The engine also pre-checks every commit and refuses to stage any file matching the sensitive-pattern list, regardless of .gitignore state — a second layer that prevents an inadvertent git add license.lic from leaking credentials into a shared remote.
Reading history¶
show policy log is the entry-point verb. It lists every commit with its generation number, short SHA, date, author (rendered as <user>@<engine-hostname>), and the comment the operator entered with their reload.
host> show policy log limit 5
Gen SHA Date Author Comment
1 a7f3e2c 2026-06-07 14:22 admin@host01 tighten t.co regex (SEC-1421)
2 b910d4f 2026-06-06 09:13 ops-bot@host01 quarterly review
3 c4d8a01 2026-06-05 16:48 jdoe@host01 add allow-list for partner CDN
...
From there:
show policy commit <N>— metadata + full diff for the Nth-most-recent commit.show policy fingerprint— HEAD SHA + author + date + signature status, in a single-row format suitable for pasting into a change-management ticket or external audit log.
Per-rule blame¶
When a rule fires unexpectedly in production, the operator wants to know who added it, when, and what their stated reason was. show policy blame <rule> takes the rule name (as it appears in the Name column of show policy list) and renders per-line author + commit attribution for the rule's block. The engine resolves the rule name to its source .policy file server-side — operators don't need to remember which file the rule lives in.
host> show policy blame gambling-extras
File: rules.d/30-categories/gambling.policy
Rule: gambling-extras
SHA Date Author Line Content
a7f3e2c 2026-06-07 admin@host01 41 {
a7f3e2c 2026-06-07 admin@host01 42 name: "gambling-extras",
a7f3e2c 2026-06-07 admin@host01 43 match-uri-regex: "(bet365|stake|pinnacle)\\.com",
c4d8a01 2026-06-05 jdoe@host01 44 action: deny,
c4d8a01 2026-06-05 jdoe@host01 45 }
Previewing a rollback¶
show policy diff rollback <N> renders the change that request policy rollback <N> would apply — the operator's "are you sure?" verb. Run it first, review the diff, then run the rollback.
host> show policy diff rollback 1
--- a/rules.d/30-categories/gambling.policy (HEAD~1)
+++ b/rules.d/30-categories/gambling.policy (HEAD)
@@ -42,7 +42,7 @@
{
name: "gambling-extras",
- match-uri-regex: "(bet365|stake)\\.com",
+ match-uri-regex: "(bet365|stake|pinnacle)\\.com",
action: deny,
}
host> enable
host# request policy rollback 1
If a reload fails after the engine has already written its commit, the engine **git revert**s the just-made commit and records the revert as a new commit on top. The recorded history mirrors what actually happened; the engine never git reset --hards history. See policy rollback for the snapshot-side rollback semantics.
Naming baselines with tags¶
Snapshot generation numbers shift as new reloads accumulate — a generation-1 today is generation-15 in two weeks. To name a baseline you may want to refer back to later, create an annotated tag:
host> enable
host# request policy tag pre-blackfriday comment "snapshot before BlackFriday rules"
Tag `pre-blackfriday` created at HEAD (a7f3e2c).
Annotation: snapshot before BlackFriday rules
host#
The comment is mandatory; the engine refuses lightweight tags (they don't carry the metadata needed for audit). Tag names are unique; the engine refuses to overwrite. List tags via show policy tags.
The [policy].git_mode tri-state¶
The configuration knob in engine.conf controls the feature:
"auto"(default) — enable if[policy].path/.git/exists; fall back to snapshot-only otherwise. The recommended setting for almost every deployment."force-on"— require.git/to be present. Compliance-leaning deployments use this to remove the "what if someone runsrm -rf .git/" branch from the threat model. In the current release the engine logs Critical if.git/is missing and continues in snapshot-only mode for the rest of startup; a future release may convert that to a hard refusal-to-start."force-off"— never enable the feature, even if.git/is present. Useful when you want to keep an inert.git/around (for re-enabling later) but suppress the engine's commit-on-reload writes.
See [policy] for the full schema.
Disabling¶
Two paths, depending on whether you want to keep the history around:
# Option A — quick disable, keep .git/ for re-enable later.
# Set [policy].git_mode = "force-off" in engine.conf, restart.
eghost restart enforcegate
# Option B — remove git history entirely.
rm -rf /etc/enforcegate/rules.d/.git
eghost restart enforcegate
# Engine logs "policy git mode: snapshot-only" at Info on next boot.
Either way the snapshot-based policy rollback continues to work as it did before — the audit-trail layer is purely additive.
Multi-engine deployments¶
The current release supports a single writing engine per policy directory. The engine's reload mutex serialises the commit step, so two operators on the same engine cannot race the git writes. Two different engines writing to the same shared [policy].path (NFS-mounted across hosts, for example) is not a supported shape today — the engines have no coordination protocol and concurrent writes would produce a non-linear history. Stay on snapshot-only mode for those deployments, or use an external configuration-management system as the authoritative source and [policy].git_mode = "force-off" on every engine.