Control utility (egctl)¶
egctl is the in-container command-line utility for the EnforceGate engine. It speaks to the engine's Control API over HTTPS and provides a Cisco-style verb hierarchy familiar to network and system administrators.
The binary is /usr/local/bin/egctl inside the enforcegate container. The shipped egctl.conf configures it to talk to the local engine on 127.0.0.1:11225. The recommended way to reach it from the host is the eghost cli operator command, which drops into an interactive egctl REPL with admin credentials already plumbed:
Control API specification
The Control API specification is currently proprietary. We plan to publish it once it reaches a production-ready stable state. As of this writing the API is still evolving and is not yet considered stable for public release.
Two front-ends, one set of verbs¶
egctl exposes the same operations through two front-ends:
-
nicli— non-interactive, scriptable. Each verb is a hyphenated subcommand typed directly afteregctl:Use from shell scripts, CI pipelines, automation, and
docker execone-liners. Output is plain text; exit code is zero on success. -
libexocli— interactive REPL with tab completion, contextual help (?), command history, and a three-mode Cisco / Juniper Junos / PAN-OS-style flow: Operational (host>), Privileged (host#), and Configuration (host(config)#). See Modes below for what each carries and how to switch.host> show <subject> # Operational — read-only verbs host> enable # raise to Privileged (Administrator+ only) host# request <action> [args...] # Privileged — destructive verbs host# configure # enter staged-edit mode (shorthand for configure terminal) host(config)# edit policy <name> # Configuration — stage / commit / revertReached via
egctl --cli(oreghost clion a deployed bundle, which prompts for credentials host-side and then enters the REPL at Operationalhost>).
The mapping between the two forms is one-to-one for the verbs. What differs is the modal gating: in libexocli, destructive request * verbs require enable first; in nicli, every verb is a flat one-shot and no mode switching is involved — your scripts, CI pipelines, and the eghost wrapper continue to work without changes. Examples below show the libexocli form (because that is what eghost cli opens) with the nicli equivalent in a callout where it differs meaningfully.
Modes¶
The libexocli interactive REPL exposes three modes, mirroring the Cisco / Juniper Junos / PAN-OS conventions network and system administrators already know. The modes apply to the interactive egctl --cli / eghost cli path only — nicli flat one-shot verbs (egctl <verb> ...) are unaffected and stay in their always-allowed scripted form.
| Mode | Prompt | Carries | Enter via |
|---|---|---|---|
| Operational | host> |
Most show * verbs (engine status, policy introspection, system introspection), show policy match <url>, learning * verbs, and request neighbor teardown. Read-only and diagnostic surface. show users is the exception — it is gated to Privileged. |
Default mode on session start. |
| Privileged | host# |
Destructive request * verbs — policy reload / rollback, license reload, URI engine restart, user management — plus show users. |
enable from Operational. Administrator (10) or above required. |
| Configuration | host(config)# |
Staged-edit verbs — edit policy <name>, show policy diff, commit, revert, end. |
configure or configure terminal from Privileged. |
enable¶
Raise the session from Operational to Privileged. Requires the authenticated user to be at Administrator (10) or above.
A Monitoring (1) or Standard (2) user sees the refusal and stays at host>:
host> enable
% Privilege denied — administrator rights required (you are at 2, admin threshold 10)
host>
The privilege check uses the engine's Control API; non-administrator operators keep their full show * surface in Operational and can use scripted nicli one-shots for anything the server-side privilege thresholds let them call.
disable / exit / quit¶
exit and quit are mode-aware and reliable from every mode — operators cannot get stranded in the REPL. quit is a hard alias of exit (Cisco IOS convention; the two are identical).
| Mode | What exit / quit does |
|---|---|
| Operational | End the REPL session and return to the host shell. |
| Privileged | Pop back to Operational. |
| Configuration | Fully unwind to Privileged (discards no on-disk edits; use revert for that). |
disable from Privileged is equivalent to exit from Privileged — pops back to Operational. end from Configuration unwinds to Privileged without committing or reverting (the on-disk edits persist but aren't loaded until a manual reload).
host# disable
host>
host(config)# end # leaves Configuration without commit/revert; edits persist on disk but aren't loaded
host#
host> exit # ends the REPL session
configure / configure terminal¶
Enter Configuration mode from Privileged. The engine snapshots the current rules.d/ state as the candidate baseline — show policy diff and revert use this snapshot to compute / restore. configure is the short alias; both forms are equivalent and both appear in ? listings. See Staged edits for the full edit / commit / revert flow.
Re-entry is refused — once you are in Configuration mode, running configure terminal again returns:
Command shape¶
The list of available subcommands:
$ docker exec enforcegate egctl help
egctl sub-commands:
show-status Display EnforceGate status
show-version Display EnforceGate version information
show-license Display licensed edition, expiry, connector counts
show-clock Display engine wall time (UTC + local + epoch)
show-tech-support Aggregate paste-into-ticket bundle of ten show-* outputs
show-neighbor [N|<id>] Display neighbor(s) — all, by entry-num, or by ID
show-neighbor-detail [N|<id>] Display neighbor(s) sessions details
show-database-status Display database connection status
show-users Display engine user accounts
show-policy-summary Display one-glance overview — default action, rule + host totals, dirs
show-policy-list Display loaded rules — id, name, action, type, source file, counts
show-policy-detail <name> Display per-rule detail (pattern, description, host counts, source file)
show-policy-files Display .policy files under [policy].path — size, mtime, rule counts
show-policy-file <name> Display the raw contents of one .policy file
show-policy-domain-lists Display match-domain-list paths, per-file line counts, aggregate hashset size
show-policy-backups Display policy snapshots available for rollback
show-policy-match <url> Dry-run a URL through the live policy engine
show-policy-learning-sessions Display all learning sessions
show-policy-learning-session <id> Display one learning session and its captured URIs
show-system-uri-engine Display URI scanner state — regex / domain backend / size
show-system-uptime Display engine uptime
show-system-memory Display process and URI-engine memory consumption
show-system-version Display engine + runtime versions
show-system-threads Display thread pool sizes + active connector sessions
show-system-listeners Display engine listeners (Control API, Defendr, captive portal)
show-system-logs [N] Tail last N lines of the configured log file
request-neighbor-teardown [N|<id>] Request neighbor(s) session(s) teardown — all, by entry-num, or by ID
request-system-uri-engine-restart Request restart of the URI scanning engine
request-system-reboot Graceful engine shutdown (orchestrator restarts) — Super-Administrator only
request-policy-reload [--dry-run] Request reload of the policy graph (--dry-run to validate)
request-policy-rollback Restore policy from a previous snapshot
request-license-reload Re-read the license file from disk and atomically swap
request-user-add Add a new user
request-user-passwd <user> Change a user's password
request-user-remove <user> Remove a user
request-policy-learning-create <kind> <value> <uri-cap> [--keep-query-strings]
Provision a learning session
request-policy-learning-start <id> Activate the session
request-policy-learning-stop <id> Stop the session
request-policy-learning-delete <id> Delete the session + its captured URIs
request-policy-learning-analyze <id> <action> [--no-stats]
Synthesise a .policy document from captures
Command naming convention
Commands that display information are prefixed with show. Commands that perform actions are prefixed with request. Learning-mode verbs are an exception kept short for ergonomics.
Verb renames in libexocli
Several historical verbs were folded into the canonical namespaces during the 2026.21.0 → 2026.24.x window. Every old form continues to dispatch for muscle-memory compatibility — they are hidden from ? listings but still callable, and scripts using the nicli flat forms are not affected. Use the canonical names in new operator scripts and documentation:
Old (still callable, hidden from ?) |
Canonical |
|---|---|
show database-status |
show database status |
test-policy-uri <url> |
show policy match <url> |
request engine restart / request uriengine restart |
request system uri-engine restart |
show learning sessions |
show policy learning sessions |
show learning session <id> |
show policy learning session <id> |
request learning create … |
request policy learning create … |
request learning start <id> |
request policy learning start <id> |
request learning stop <id> |
request policy learning stop <id> |
request learning delete <id> |
request policy learning delete <id> |
request learning analyze <id> <action> |
request policy learning analyze <id> <action> |
show enforcegate <verb> (two-word prefix) |
bare <verb> form |
The nicli flat verbs map symmetrically — egctl learning-create → egctl request-policy-learning-create, etc. — and the old kebab-cased forms continue to dispatch unchanged.
request * and clear * are hidden from Operational ?
The request * and clear * namespaces dispatch only in Privileged mode and disappear entirely from the Operational ? listing — operators have to enable first to see them. The show * family stays visible at every mode. This is the modal-help-listing gate only; server-side AAA thresholds are unchanged.
Status¶
Retrieve the current engine + database + API status. Inside eghost cli:
host> show status
EnforceGate Engine Status: UP
License: pro (18/25 connectors active)
EnforceGate Database Status: UP
EnforceGate API Status: UP
host> show version
EnforceGate vX Engine
Engine version: 2026.30.0.328
Client API version: local 0.0.9.4 / remote 0.0.9.4
License: Pro (active 18 of 25)
Copyright © 2024-2026 Exosys Sàrl. All rights reserved.
The License line on the third indented row of show version shows the licensed edition (Lite / Pro / Enterprise) and the active-vs-licensed connector-session count. For the full breakdown — expiry, bundled vs add-on split — use show license.
show version output format changed in 2026.23.2
Pre-2026.23.2 show version emitted three bare Label: Value lines (EnforceGate Engine Version: X, Local Control API Version: Y, Remote Control API Version: Z). The current format is a five-line Cisco IOS show ver-style block (shown above). Scripts that parse the output need to update their regex — the engine-version line now reads ^\s+Engine version: (two leading spaces) instead of ^EnforceGate Engine Version:. The nicli egctl show-version and the libexocli host> show version emit the same format.
Scripted equivalent (nicli)
The host-side eghost status and eghost version commands render augmented views — eghost version adds the host CLI's own version and the per-image versions read from each running container's OCI label, which is useful for catching a half-applied upgrade.
License¶
The licensing verbs surface the engine's licensed edition, expiry, and connector-session usage, and let the operator reload the license file without restarting the engine.
show license¶
Display the engine's licensed edition, expiry, and connector counts. Read-only; safe to run frequently. No arguments.
host> show license
Licensed edition: pro
Expires at: 2027-06-01 00:00:00 UTC
Active connector sessions: 18
Licensed connector cap: 25
of which bundled: 25
of which add-on: 0
Fields:
- Licensed edition —
lite,pro, orenterprise. See editions for the feature mapping. - Expires at — when the license stops validating. UTC.
- Active connector sessions — number of Defendr connectors currently registered against this engine. Live counter, refreshed at request time.
- Licensed connector cap — total connector sessions this license allows. Bundled count plus any add-on bundles purchased.
- of which bundled — connector sessions included with the edition (10 for Lite, 25 for Pro, 50 for Enterprise).
- of which add-on — extra sessions purchased on top of the bundled count, in 5-session increments.
Privilege: Monitoring (1) — every operator role can read.
request license reload¶
Privileged. Re-read the license file from disk and atomically swap the engine's in-memory edition, cap, and expiry. The expected flow is:
- Operator receives a new license file (renewal, edition upgrade, add-on bundle purchase).
- Operator copies it over the existing license file path.
- Operator raises to Privileged (
enable) and runsrequest license reload.
The engine validates the new file (signature + host fingerprint + expiry) and on success swaps it in without restarting — no traffic interruption, no connector sessions disconnect.
Success output:
Cap-underflow refusal. If the new license's cap is below the engine's current active connector count, the engine refuses the swap and keeps the previous license live:
host# request license reload
License reload refused (cap underflow): new license cap (10) is below
the current active connector count (18). Refusing to swap — stage the
reload when fewer sessions are active, or issue a higher-cap license.
Resolve by either draining active sessions before retrying (request neighbor teardown from Operational works for an orderly drain), or by issuing a license with a cap at least equal to the current active count — the typical case is a renewal at the same edition, which would not trigger underflow.
Privilege: Administrator (10) — same threshold as request policy reload.
Neighbors¶
In the engine's vocabulary a neighbor is a connected squid-connector process. The standalone image spawns 5 connector processes by default (matching the url_rewrite_children 5 directive in squid.conf), so a healthy deployment shows five UP/ACTIVE neighbors.
show neighbor and request neighbor teardown both take a single optional positional argument that classifies its input — bare integer is the entry-num filter, anything else is a neighbor ID (IPv4 / IPv6 address). Omit the argument to operate on all neighbors.
host> show neighbor # list all
host> show neighbor 3 # entry-num 3
host> show neighbor 192.0.2.10 # neighbor ID 192.0.2.10
host> show neighbor 2001:db8::1 # neighbor ID (IPv6)
host> show neighbor 3 detail # entry-num 3, detailed view
host> show neighbor
Types: C - Connector, CT - Connector over TLS, ? - Unknown
# T Neighbor ID State Address Connected since
83 C 127.0.0.1 UP/ACTIVE 127.0.0.1:36392 0d 9h 55m 20s
84 C 127.0.0.1 UP/ACTIVE 127.0.0.1:36394 0d 9h 55m 20s
85 C 127.0.0.1 UP/ACTIVE 127.0.0.1:36396 0d 9h 55m 20s
86 C 127.0.0.1 UP/ACTIVE 127.0.0.1:36398 0d 9h 55m 20s
87 C 127.0.0.1 UP/ACTIVE 127.0.0.1:36400 0d 9h 55m 20s
For detailed per-session diagnostics on one neighbor:
host> show neighbor 83 detail
Neighbor 127.0.0.1, AS Number 64515
Type is Connector (Squid)
Using transport protocol tcp
with plaintext connnection
Session authentication method is SHA256
Connected from address 127.0.0.1 port tcp/36392
to address 127.0.0.1 port tcp/11224
Session state is UP/ACTIVE
Has sent defendr protocol messages:
Open 1 msg OpenConfirm 0 msg
Request 112 msg Acknowlegment 1 msg
Response 0 msg Keepalive 513 msg
Teardown 0 msg Notification 0 msg
Unknown 0 msg
Connected on Wed May 27 11:00:05.694 2026 CEST
Last heared on Wed May 27 20:58:35.690 2026 CEST
Current uptime is 0w 0d 9h 59m 23s
Registered with ID #83 (uuid:117d43c6-4b88-4423-8b9f-a8d4805a9507)
Advertised version: 2026.30.0.328 (EA) (capflag:0x0)
To manually terminate a neighbour session (the connector re-establishes shortly afterwards). Same positional shape as show neighbor, plus the Cisco IOS alias clear neighbor:
host> enable
host# request neighbor teardown # tears down all (confirms first)
host# request neighbor teardown 3 # entry-num 3
host# request neighbor teardown 192.0.2.10
host# clear neighbor 3 # IOS alias of `request neighbor teardown 3`
Tear down neighbor entry 3? [y/N] y
Requested neighbor session teardown: success
Policy introspection¶
Read-only verbs that let an operator see what policy is loaded right now without dumping the .policy files by hand. All are Operational > (Monitoring privilege and above) in libexocli and flat one-shots in nicli.
show policy summary¶
One-glance overview of the loaded policy — complementary to the per-rule show policy list below. Use this on a fresh login (or in a status check) to confirm the engine is enforcing what you expect without scrolling through individual rules.
host> show policy summary
Policy summary
Default action (no rule matched): permit
Rules loaded: 17 (7 regex + 8 domain-list + 2 pin)
Domain hosts: 4,631,536
Pinned hosts: 28 across 2 pin rule(s)
By source: 12 operator + 5 toolbox
Time-gated: 2 rule(s) with a time-window
operator dir: /etc/enforcegate/rules.d/ (9 file(s)) [loaded first]
toolbox dir: /etc/enforcegate-shared/rules.d/ (3 file(s)) [loaded after]
Domain backend: sorted-arena
Fields:
- Default action — the engine-synthesised no-match verdict from
[policy].default_action. Thepermit/deny/warn/aupvalue reported here is what every request that no rule matches will get. - Rules loaded — total rule count with the regex / domain-list / pin split. Match-uri / match-sni / other
action:rule kinds roll up into the parenthetical totals. The+ N pinsegment appears from 2026.35.0 onwards when at least one pin rule is loaded. - Domain hosts — aggregate count of distinct hostnames across every
match-domain-list:referenced byaction:rules. - Pinned hosts — aggregate count of distinct hostnames across
pin:rules. Only printed when at least one pin rule is loaded. - By source — operator-authored rules (
[policy].path) versus toolbox / shared-dir rules ([policy].shared_path). - Time-gated — how many rules carry a
time-window:attribute. - operator dir / toolbox dir — resolved paths and per-directory file counts.
- Domain backend —
hashset/sorted-arena/auto(configured via[policy].domain_backend). For sizing detail, seeshow system uri-engine.
show policy list¶
Lists every loaded rule with its id, name, action, match kind, window, source, source file, and aggregate match-attribute counts. Use this to confirm a recent reload produced the rule set you expected.
host> show policy list
ID Name Action Type Window Source File Hosts/Regexes
---- -------------------------------- --------- --------------------- --------------------- --------- ---------------------------------- -------------
0001 allow-debian-mirror permit match-uri — operator 05-allow-package-mirrors.policy -
0002 allow-ubuntu-mirror permit match-uri-regex — operator 05-allow-package-mirrors.policy 1
0003 block-malware-domains deny match-domain-list — operator 20-block-threats.policy 28,453
0020 pin-microsoft-update splice pin — operator 20-pinned.policy 14
0040 block-social-business-hours deny match-domain-list weekdays 08:00-18:00 operator 40-block-social-media.policy 7
0102 category-adult deny match-domain-list — toolbox 40-deny-categories.policy 412,903
...
Total: 49 rules — 3 regexes, 41 domain-list rules, 3 other + 2 pin; 4.6M aggregate domain entries.
Default action (no rule matched): permit
The footer reports the engine-synthesised no-match verdict ([policy].default_action) below the per-rule listing. The Default action line is the only place the no-match verdict shows up in show policy list — it does not occupy a rule id.
The Type column (renamed from Match-kind in 2026.35.0) shows what shape the rule has — match-uri, match-uri-regex, match-domain-list, match-sni, match-client-ip etc. for action: rules, and pin for Pinned destinations rules. When the Type is pin, the Action column carries the peek-step verdict (splice / bump / terminate) rather than the post-bump verdict.
The Source column reports where each rule was loaded from:
operator— hand-authored rules from[policy].path(typically/etc/enforcegate/rules.d/).toolbox— rules loaded from the shared directory ([policy].shared_path, typically/etc/enforcegate-shared/rules.d/), written by the toolbox sidecar over a shared volume. Useful when debugging a verdict to understand whether the matching rule came from your own ruleset or a generated drop.
The Window column reports the rule's time-window: attribute in normalised compact form (weekdays 08:00-18:00, daily 22:00-06:00, mon,wed,fri 09:00-17:00). An em dash (—) means the rule is always active. The window is evaluated against the engine's [policy].time_window_tz (default local) — use show clock to confirm what "local" means on the engine.
The File column reports the source .policy file the rule was loaded from — the same value show policy detail <name> prints under Source file. Use it to jump straight from "this rule fired" to the file you need to edit, without round-tripping through show policy detail.
Lowest rule id wins. When two rules can match the same request, the engine applies the one with the lower id. Operators control precedence by number-prefixing policy filenames — 10-allow-internal.policy loads before 90-block-categories.policy, so its rules get lower ids and win conflicts. This is the mental model for layering a specific allow above a broader deny: prefix the allow's filename with a lower two-digit number than the deny's.
Migrating from a hand-rolled catch-all
Pre-2026.32.0 deployments commonly shipped (or hand-wrote) a 99-default-permit.policy rule to encode the no-match verdict. From 2026.32.0 that file is retired. If show policy list still reports a catch-all permit rule at a high id on your deployment, delete the file and set [policy].default_action = "permit" in engine.conf — the verdict is preserved, and lower-precedence rules in [policy].shared_path (typically toolbox-written rules) finally enforce instead of being shadowed by the catch-all.
show policy detail <name>¶
Per-rule deep view: the literal match value or compiled regex, the description shown to visitors on block / warn / aup, host counts for domain-list rules, the source .policy file the rule came from, and sibling counts when the same name appears in multiple files.
host> show policy detail block-malware-domains
Name: block-malware-domains
Action: deny
Match kind: match-domain-list
List path: /etc/enforcegate/rules.d/lists/malware-domains.txt
Domain hosts: 28,453
Source file: /etc/enforcegate/rules.d/20-block-threats.policy
Description: Known malware distribution domain — blocked
The source-file field is what edit policy <name> resolves the rule name to when the operator enters Configuration mode — operators don't have to know which file their rule lives in to edit it.
show policy files¶
Lists every .policy file under [policy].path, with size, modification time, approximate rule count, and the disabled-file audit (files the engine skipped because of suffix or other rules).
host> show policy files
File Size Modified Rules
------------------------------------ -------- -------------------- -----
05-allow-package-mirrors.policy 412 B 2026-05-30 09:14:02 3
20-block-threats.policy 1.2 KiB 2026-06-02 17:02:11 2
40-warn-social-media.policy 204 B 2026-06-01 11:30:55 1
(Disabled: 0)
show policy file <name>¶
Returns the raw contents of one .policy file. Path-traversal guarded — only files under [policy].path are reachable.
host> show policy file 40-warn-social-media
warn-social-media: {
match-domain-list: /etc/enforcegate/rules.d/lists/social-media.txt
application: https
action: warn
description: Social media — proceed with caution
}
show policy domain-lists¶
Lists every match-domain-list: path referenced by a loaded rule, with per-file line count, last-modified timestamp, and the engine's live aggregate hashset size (the number of distinct domain entries the engine is matching against, deduplicated across all referenced files).
host> show policy domain-lists
List path Lines Modified Live hashset
----------------------------------------------------- --------- -------------------- ------------
/etc/enforcegate/rules.d/lists/malware-domains.txt 28,453 2026-06-02 17:00:00
/etc/enforcegate/rules.d/lists/phishing-domains.txt 4,612 2026-05-29 22:14:00
/etc/enforcegate/rules.d/lists/social-media.txt 7 2026-06-01 11:30:55
----------------------------------------------------- --------- -------------------- ------------
Aggregate 33,072 33,069
Scripted equivalents (nicli)
docker exec enforcegate egctl show-policy-summary
docker exec enforcegate egctl show-policy-list
docker exec enforcegate egctl show-policy-detail block-malware-domains
docker exec enforcegate egctl show-policy-files
docker exec enforcegate egctl show-policy-file 40-warn-social-media
docker exec enforcegate egctl show-policy-domain-lists
See also Inspecting the live policy for the show policy match flow and offline duckdb engine.db inspection.
System introspection¶
Read-only verbs that surface engine internals — runtime, memory, threads, listeners, log tail. Operators reach for these when sizing container memory limits, performance-triaging a slow deployment, or assembling the context to attach to a support ticket. All seven are Operational (Monitoring privilege and above) in libexocli and flat one-shots in nicli.
show system uri-engine¶
State of the URI scanning engine — regex rule count, compiled regex DB size, the in-memory domain backend (its name, total entries, distinct rule IDs, resident bytes), and the backend-selection mode (auto / hashset / sorted-arena — see [policy].domain_backend).
host> show system uri-engine
Regex rules: 12 compiled
Regex DB size: 12.3 KiB
Domain backend: hashset
Domain entries: 4,612,083
Domain distinct rule IDs: 8
Domain resident: 287.4 MiB
Backend mode: auto (threshold 50,000,000)
show system uptime¶
Engine process uptime in Cisco IOS format, plus the ISO-8601 boot time:
host> show system uptime
Engine uptime: up 2 weeks, 3 days, 4 hours, 19 minutes
Boot time (UTC): 2026-05-13T07:41:18Z (1747122078)
show system memory¶
Process resident-set size from /proc/self/status, with the URI-engine subtotals broken out. Use this together with the memory sizing guide to size ENFORCEGATE_MEMORY against real numbers.
host> show system memory
Engine Memory
Process VmRSS: 3.2 GiB
URI engine: regex compiled DB: 12.3 KiB
URI engine: domain backend: 287.4 MiB
URI engine subtotal: 287.4 MiB
show system version¶
Engine build banner — engine version, Client API version, runtime libraries, regex runtime, build type. Attach the output to any support ticket so the engineering team can reproduce against the right build.
host> show system version
EnforceGate vX Engine
Engine version: 2026.30.0.328
Client API version: 0.0.9.5
Database engine: DuckDB 1.4.2
Regex runtime: <version>
TLS: OpenSSL 3.5.1
Build type: release
show system threads¶
Sizes of the engine's two listener pools, the database and neighbor-monitor threads, and the live count of active connector sessions. Useful for performance triage — the thread counts are static (sized at boot), while the connector-session count is live (sessions multiplex onto the connector listener pool, so a healthy busy engine can show many more sessions than threads).
host> show system threads
Engine Threading Model
Main thread: 1
Control-API listener pool: 4
Connector listener pool: 4
Database thread: 1
Neighbor monitor thread: 1
Total threads: 11
Active connector sessions: 18
The two listener pools serve different traffic and are tuned separately:
- Control-API listener pool — fixed at 4 threads; serves the engine's HTTPS REST API on
tcp/11225(whategctltalks to). - Connector listener pool — sized by
[global].threads(default 4); serves the Defendr wire from connectors (enforcegate-squid-connectortoday; future TLS-proxy and ICAP connectors).
show system listeners¶
Confirms the engine is listening where the operator expects it to be — the Control API and Defendr bind addresses with their TLS state, plus the captive-portal base URL.
host> show system listeners
Control API (HTTPS): 0.0.0.0:11225 (TLS on, always)
Defendr listener: 127.0.0.1:11224 (TLS on)
Captive portal base URL: https://portal.example.internal
The Control API is HTTPS unconditionally. The [global].tls knob in engine.conf toggles TLS on the Defendr wire only.
show system logs [N]¶
Tail the last N lines of the configured log file (defaults to 50; capped at 5000). Useful when the operator is in a eghost cli session and doesn't want to break out to eghost logs.
host> show system logs 5
[engine] 2026-06-05T11:42:18Z [info] policy reload: 4,612,094 rules applied (12 regex + 4,612,082 domain)
[engine] 2026-06-05T11:42:18Z [info] match engine swapped
…
show clock / show calendar¶
Engine wall time — UTC and local timezone with the abbreviation, plus epoch seconds for scripts that correlate against engine log timestamps. show calendar is a hard alias of show clock (Cisco IOS convention).
host> show clock
Engine clock
UTC: 2026-06-06T08:42:15Z
Local: 2026-06-06 10:42:15 CEST
Epoch: 1781340135 (unix seconds)
show tech-support¶
Single command that aggregates ten of the most-asked-for show * outputs into one paste-into-ticket block, each behind a === <section> === banner. The first thing to run before filing a support ticket — the engine team gets the engine version, uptime, memory, threads, listeners, status, license, neighbors, and the last 50 log lines in one paste.
host> show tech-support
EnforceGate Engine — Tech-Support Bundle
Paste-into-ticket aggregate of: version, uptime, memory, threads,
listeners, status, license, neighbors, and the last 50 log lines.
================================================================
=== show system version
================================================================
[…]
================================================================
=== show system uptime
================================================================
[…]
[… eight more sections …]
================================================================
=== end of tech-support bundle
================================================================
If any individual section errors during the bundle build (network blip, transient lock, etc.), the bundle still completes — the failed section is marked inline with a clear note, the rest is collected as normal. The value is "everything that worked plus a clear note about what didn't."
Scripted equivalents (nicli)
docker exec enforcegate egctl show-system-uri-engine
docker exec enforcegate egctl show-system-uptime
docker exec enforcegate egctl show-system-memory
docker exec enforcegate egctl show-system-version
docker exec enforcegate egctl show-system-threads
docker exec enforcegate egctl show-system-listeners
docker exec enforcegate egctl show-system-logs 50
docker exec enforcegate egctl show-clock
docker exec enforcegate egctl show-tech-support
URI engine restart¶
Privileged. Restart the in-engine URI scanning subsystem without restarting the whole engine. Useful for clearing accumulated state without dropping connector sessions:
Engine reboot¶
Super-Administrator only (level 11). Graceful engine shutdown — the orchestrator restarts the container per its configured restart policy. The first verb that requires the Super-Administrator privilege level; an Administrator (10) who is in Privileged mode still gets a server-side refusal.
Two verbs invoke the same operation: request system reboot is the canonical form; reload is the Cisco IOS muscle-memory alias.
host> enable
host# request system reboot
Reboot the engine container (graceful shutdown; orchestrator restarts)? [y/N] y
Engine reboot requested: success
(graceful shutdown in ~500ms; the container orchestrator restarts the engine)
host# reload # IOS alias of `request system reboot`
Reboot the engine container (graceful shutdown; orchestrator restarts)? [y/N] y
Engine reboot requested: success
Operator notes:
-
Privilege. Super-Administrator (11). An Administrator (10) in Privileged mode sees the refusal:
-
In-flight state. The graceful shutdown drains in-flight requests and flushes the policy snapshot. Learning sessions in the
runningstate are stopped cleanly; captured URIs persist. - Per-engine, not cluster-wide. The verb operates on one engine. A deployment with N engines requires N separate invocations.
- Restart guarantee. Coming back up is the orchestrator's job — Docker's
--restartpolicy, Kubernetes'restartPolicy, the appliance's systemd unit. If the orchestrator is configured not to restart, the engine stays down.
Policy reload¶
Privileged. Re-parse *.policy files under /etc/enforcegate/rules.d/, validate, snapshot, and apply. Tells the engine to refresh its in-memory policy graph from the DuckDB store. Same path eghost policy new/edit/remove invoke automatically after writing a policy file.
host> enable
host# request policy reload
Parsed rules: 14
Snapshot taken: /var/lib/enforcegate/policy-backups/20260528-1503.001
Policy reload: success
To validate without applying (the recommended first step on any policy edit) — dry-run is now a bare modifier flag, no argument:
host# request policy reload dry-run
Parsed rules: 14
Validation: success (no snapshot taken, no live state changed)
Scripted equivalent (nicli)
For policy authoring operators should prefer eghost policy … (see policies reference) — this manual reload is only needed when .policy files were modified by some external path (bind-mount, docker cp, external orchestration).
Policy rollback¶
Privileged for the request half; Operational for show policy backups. Snapshots of rules.d/ are taken before every successful request-policy-reload and stored under /var/lib/enforcegate/policy-backups/. List them (no enable required — it's a show verb):
host> show policy backups
Gen Snapshot Files Note
1 20260528-1503.001 12 (currently applied)
2 20260528-1320.001 11
3 20260527-0945.001 10
…
Preview a rollback before applying — shows which files would be removed (orphans added since the snapshot) and which would be overwritten:
host> enable
host# request policy rollback 2 dry-run true
Rollback dry-run from gen 1 → gen 2:
- would remove orphan file: 75-temp-experiment.policy
- would overwrite operator-edited file: 50-deny-shorteners.policy
Apply (interactive — the client prompts with the diff before proceeding):
To skip the interactive prompt in scripted use:
Scripted equivalent (nicli)
The engine refuses to roll back over operator-added files unless yes true (or the nicli --yes flag) is present — the gate is intentional so a careless rollback doesn't wipe new policy work. Snapshot retention is bounded by [policy].backup_depth in engine.conf (default 10 — older snapshots are pruned after every successful reload).
Policy history (git tracking)¶
Optional. When [policy].path contains a .git/ directory and [policy].git_mode allows it (default auto), every successful policy reload writes a git commit into the same repository before the snapshot is taken. The commit is authored as <user>@<engine-hostname> so the log shows who made the change and which engine processed it. The result is a per-rule audit trail that sits on top of the existing snapshot system — see Policy audit and history for the enable / migration / disable workflow.
Six read-only verbs (Operational) and two mutating verbs (Privileged) expose the audit trail. The verb shape is modelled on Junos so operators with that muscle memory translate directly — show policy log lines up with Junos show configuration log; show policy diff rollback <N> lines up with show configuration | compare; show policy commit <N> lines up with show system commit. Cisco IOS / PAN-OS operators get the same verbs without leaning on the network-gear analogy.
show policy log¶
The entry-point verb. Lists policy commits in reverse chronological order — generation number, short SHA, date, author, and the operator's comment (the message they typed when they ran the reload, captured automatically into the commit).
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
4 d2e0b7c 2026-06-03 11:02 admin@host01 Revert "broaden gambling category"
5 e6f1c9b 2026-06-03 10:55 admin@host01 broaden gambling category
host>
Gen 4 is the auto-revert that paired with gen 5's bad deploy — on a reload failure the engine git reverts the just-made commit so the recorded history matches the engine's live state (honest audit trail, never rewritten).
show policy blame <rule>¶
Per-rule attribution. The killer triage verb — operators don't need to remember which file the rule lives in. The engine resolves the rule name server-side, opens the right .policy file, and renders per-line author + commit attribution for the rule block.
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 }
host>
show policy commit <N>¶
Metadata plus diff for one specific commit — the equivalent of git show wrapped behind the operator-friendly verb. <N> is the generation number from show policy log.
host> show policy commit 1
Generation: 1
SHA: a7f3e2c
Date: 2026-06-07 14:22
Author: admin@host01
Comment: tighten t.co regex (SEC-1421)
--- a/rules.d/40-warn-social-media.policy
+++ b/rules.d/40-warn-social-media.policy
@@ -3,7 +3,7 @@
warn-social-media: {
- match-uri-regex: "^https?://(www\\.)?t\\.co/.+",
+ match-uri-regex: "^https?://t\\.co/[A-Za-z0-9]+/?$",
application: https,
action: warn,
}
host>
show policy diff rollback <N>¶
Preview what request policy rollback <N> would change — the "are you sure?" verb operators run before pulling the trigger.
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>
After reviewing the preview, run request policy rollback <N> to actually apply the change.
show policy fingerprint¶
Snapshot of the current HEAD — useful for cross-referencing against an external audit log or pasting into a change-management ticket.
host> show policy fingerprint
Current HEAD
SHA: a7f3e2c2...
Author: admin@host01
Date: 2026-06-07 14:22
Signature: (unsigned)
host>
The Signature: line is reserved for a future release that will support signed commits (GPG or SSH-signed). Today every row reads (unsigned).
show policy tags¶
Lists annotated tags — named baselines the operator created at specific commits via request policy tag (below).
host> show policy tags
Name SHA Tagged at Author Annotation
pre-blackfriday a7f3e2c 2026-06-07 14:25 admin@host01 snapshot before BlackFriday rules
gold-baseline b910d4f 2026-06-06 09:14 admin@host01 end-of-Q2 vetted configuration
host>
request policy git-init¶
Privileged. Initialise git tracking of the policy directory directly from the REPL — equivalent to running git init inside [policy].path by hand, but without shelling out of eghost cli. The engine creates the repo, stages every current .policy file, writes the auto-managed .gitignore, and produces a baseline commit. Subsequent reloads start writing commits on top.
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#
If [policy].path/.git/ already exists, the engine refuses politely and reports the existing repo's HEAD SHA — no destructive re-init.
See policy audit and history for the migration shape and the fallback shell-out command if the operator prefers to drive git init from the host.
request policy tag <name> comment <text>¶
Privileged. Create an annotated tag at the current HEAD. The comment is mandatory — lightweight tags without an annotation are refused at the server (they don't carry the metadata operators need for audit). Tag names are unique; the engine refuses to overwrite an existing 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#
Use tags to name important baselines you may want to roll back to — the snapshot generation numbers shift as new reloads accumulate, but a tag's SHA is stable.
Failure semantics¶
If a request policy reload fails after the engine has already written the commit (a validation pass succeeded but the apply step hit a runtime issue), 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; the audit trail stays honest about both the failed attempt and the recovery.
Scripted equivalents (nicli)
docker exec enforcegate egctl show-policy-log
docker exec enforcegate egctl show-policy-blame gambling-extras
docker exec enforcegate egctl show-policy-commit 1
docker exec enforcegate egctl show-policy-diff-rollback 1
docker exec enforcegate egctl request-policy-git-init
docker exec enforcegate egctl show-policy-fingerprint
docker exec enforcegate egctl show-policy-tags
docker exec enforcegate egctl request-policy-tag pre-blackfriday \
--comment "snapshot before BlackFriday rules"
Policy match — test a URL against the live policy¶
The canonical "why is this URL blocked?" diagnostic. Sends a URL through the live policy engine and reports the verdict, matched rule, action, reason — without the operator actually browsing to the URL. Operational mode; lives under the show policy * namespace alongside the introspection verbs.
host> show policy match https://example.com/foo
URI: https://example.com/foo
Matched: yes
Code: 300 (deny — redirect to portal)
Rule ID: 14
Rule name: deny-some-rule_e14
Action: deny
Reason: <the rule's description field>
Pattern: ^(https?:\/\/)?([^/]+\.)?example\.com(/|$)
The Pattern field (the matched regex) is admin-only — monitoring and standard users see the verdict and reason but not the underlying regex. The rule_name suffix _e<N> is the global rule ID; the same suffix appears on the captive portal's verdict pages so operators can cross-reference.
For a non-matched URL:
host> show policy match https://www.exosys.ch/
URI: https://www.exosys.ch/
Matched: no (permit-by-default)
Code: 200 (permit)
For a host that matches a pin rule — independent of any action: match — show policy match prints an additional TLS pin: line reporting the peek-step verdict the SslBump helper would return, and which pin rule produced it:
host> show policy match https://download.windowsupdate.com/
URI: https://download.windowsupdate.com/
Matched: no (permit-by-default)
Code: 200 (permit)
TLS pin: splice (pass through, hostname-only) [rule: pin-microsoft-update]
The TLS pin: line is omitted when the host is unpinned (no pin rule matched) — the engine defaults to bump (normal inspection) in that case.
Scripted equivalent (nicli)
This is the first tool to reach for on any "URL blocked unexpectedly" ticket — see troubleshooting.
Learning mode¶
Learning mode lets the engine observe URLs hit by a specific client / subnet / user-agent for a window, then synthesises a starter .policy document from the captures. Useful for first-deployment baseline discovery. See learning mode for the full operator workflow.
The verbs live under the policy learning namespace — show * forms are Operational, request * forms are Privileged. clear policy learning session <id> is the Cisco-style alias of request policy learning delete <id>.
host> enable
host# request policy learning create subnet 10.1.0.0/16 5000
host# request policy learning start <id>
host> show policy learning sessions # Operational — back at `>`
host> show policy learning session <id>
host# request policy learning stop <id> # Privileged
host# request policy learning analyze <id> warn
host# clear policy learning session <id> # IOS alias of `request policy learning delete <id>`
Per-session detail:
host> show policy learning session 3
Session 3:
Filter: subnet 10.1.0.0/16
URI cap: 5000
State: stopped
URIs captured: 4827
Created: 2026-05-26T09:14:00Z
Stopped: 2026-05-26T17:42:00Z
Top URIs (by hit count):
hits host path
1247 www.google.com /search
913 mail.google.com /
708 www.youtube.com /watch
...
request policy learning analyze writes a .policy document to stdout — redirect to a file under rules.d/ then reload:
docker exec enforcegate egctl request-policy-learning-analyze 3 warn --no-stats \
> /tmp/50-warn-learned.policy
docker cp /tmp/50-warn-learned.policy enforcegate:/etc/enforcegate/rules.d/
docker exec enforcegate egctl request-policy-reload
The --no-stats flag strips the (N hits during learning session M) provenance tag from the synthesised rule descriptions — use it for production-bound policies.
User management¶
request user * verbs are Privileged in libexocli; show users stays Operational. The host-side wrapper eghost users add <name> performs the same operation with a friendlier prompt — use that path when possible (it dispatches the nicli flat verb under the hood, no enable needed there).
The Available User types: menu is privilege-aware — operators only see the types they are allowed to create. An Administrator (10) sees Monitoring User, Standard User, and the reload-only service account; a Super Administrator (11) additionally sees Administrator and S) Super Administrator, so a Super Administrator can mint a peer Super Administrator. The bootstrap admin was previously the only Super Administrator possible because the must-outrank invariant has no level above 11.
host> enable
host# request user add
You are about to add a new user to EnforceGate. Enter '.' if you want to leave a field blank.
-----
Available User types: 1) Monitoring User, 2) Standard User, R) Service account (policy-reload only),
A) Administrator, S) Super Administrator
User Type: A
[i] Valid usernames must start with a lowercase character, can include special characters such as '_' or '-', and must be between 3 and 128 characters long.
Username: backup-admin
User Description: Secondary administrator
Password: **********
Repeat Password: **********
R) Service account — least-privilege automation
The reload-scoped service account (new in 2026.33.0) carries a tightly scoped capability set — it can call request policy reload and the show status read-only verbs, and nothing else: no user management, no license operations, no neighbor teardown, no engine reboot. The intended use is automation that only needs to trigger reloads — most notably the toolbox sidecar's helper library, which currently authenticates with an Administrator credential. Migrating that automation to a service account follows the principle of least privilege without losing functionality. See Privilege model for how this slots between the existing levels.
If the engine cannot determine the operator's privilege level, the command aborts cleanly with a clear message instead of presenting a menu.
See privilege model for the four levels and what each can do. Self-target password change works without admin privilege; other-target requires admin + outrank.
Password file
When a new user is added, the password file at /etc/enforcegate/passwd is automatically updated with a fresh per-user salt + PBKDF2-HMAC-SHA256 hash. Existing accounts on older sha256 / sha256-salted hashes continue to authenticate and migrate to pbkdf2-sha256 the next time their password is changed.
Username naming convention
- Usernames must begin with a lowercase letter (a–z).
- Permitted characters: lowercase letters, digits (0–9), underscore (
_), hyphen (-). - Length: 3 to 128 characters.
List users — Privileged mode in libexocli (the user-list metadata is gated behind enable); still callable as a flat nicli one-shot at the server-side AAA threshold:
host> enable
host# show users
Username UID Privilege Hash
admin 1000 Super Administrator pbkdf2-sha256$<params>:hash
monitor 1001 Monitoring User pbkdf2-sha256$<params>:hash
backup-admin 1002 Administrator pbkdf2-sha256$<params>:hash
legacy-ops 1003 Standard User sha256$salt:hash
No secrets are exposed. The Hash column shows the algorithm — new and password-changed accounts use pbkdf2-sha256 (deliberately slow, brute-force-resistant); accounts that haven't had a password change since the upgrade keep their legacy sha256 / sha256-salted hash until the next change. Change a password (uses getpass — no echo, never on argv):
Remove a user (refuses self-remove, refuses if you don't outrank the target):
Staged edits¶
Configuration mode. A configure terminal → edit → commit flow that lets an Administrator-level operator stage .policy edits inside the REPL, diff them against the engine-snapshotted baseline, and either atomically apply or roll back — all without leaving libexocli.
The flow:
host> enable
host# configure terminal
host(config)# edit policy 40-warn-social-media
host(config)# show policy diff
host(config)# commit
host#
Entering Configuration¶
configure terminal from Privileged. The engine takes a candidate snapshot of rules.d/ at entry — this is what show policy diff and revert compare against.
edit policy <name>¶
<name> is resolved as a rule name first, then as a file basename:
- If
<name>matches theNamecolumn ofshow policy list(the bareword in a.policyblock likeblock-malware-domainsorwarn-social-media), the engine opens the.policyfile containing that rule. Operators don't have to know which file their rule lives in. - If no rule by that name exists,
<name>is treated as a file basename —edit policy 40-warn-social-mediaopens40-warn-social-media.policyunder[policy].path.
Either form shells out to your preferred editor. The editor lookup chain is $VISUAL → $EDITOR → vi. The REPL session pauses while the editor is open and resumes (with terminal state restored) on editor exit. The file is opened in place — saves on disk are immediate, but the engine does not load them until commit.
host(config)# edit policy warn-social-media
Resolved rule `warn-social-media` → 40-warn-social-media.policy
[ editor opens; operator edits the file; editor exits ]
host(config)#
If neither form resolves, the engine refuses:
host(config)# edit policy nonsense
% No rule named `nonsense` is currently loaded, and no file `nonsense.policy` exists under [policy].path.
% Run `show policy list` to see the canonical names of loaded rules.
host(config)#
show policy diff¶
Engine-computed diff between the entry-time candidate snapshot and the current on-disk state. Available at any point during the Configuration session.
host(config)# show policy diff
-warn-social-media: action=warn, list=social-media.txt (7 hosts)
+warn-social-media: action=warn, list=social-media.txt (12 hosts)
+block-tiktok: action=deny, host=tiktok.com
commit¶
Atomically apply via the policy-reload path (same compile + DuckDB import + in-memory reload as request policy reload). On success, the candidate snapshot is consumed and the session returns to Privileged.
host(config)# commit
Parsed rules: 16
Snapshot taken: /var/lib/enforcegate/policy-backups/20260604-0918.001
Policy reload: success
host#
If validation fails, the engine keeps the previously-loaded policy live and reports the diagnostic; the candidate stays open so the operator can fix and retry.
revert¶
Restore rules.d/ from the candidate snapshot taken at configure terminal entry. Discards any on-disk edits made during the session. The engine state does not change (it was never reloaded), so no policy-reload is performed.
end¶
Leave Configuration without committing or reverting. On-disk edits persist; the engine state is unchanged. Useful when you want to keep edits for review later but not load them yet.
Authentication¶
egctl authenticates per request with HTTP basic against the engine's /etc/enforcegate/passwd file. Each verb additionally checks the requesting user's privilege level against a per-verb threshold.
Passwords are read in this order: command-line -P argv → EGCTL_PASSWORD env var → interactive getpass prompt (no echo). Never pass a password on argv outside CI — it appears in ps and shell history.
Fail-fast on bad credentials at REPL entry¶
egctl --cli (and eghost cli) authenticates once at session start and refuses to open the REPL on a credential failure. Previously the shell opened, the prompt rendered, and the first command failed with an auth error — which looked like the operator was logged in. Current behaviour exits immediately with a clear message:
$ egctl --cli
% Authentication failed: user `monitor` was rejected by the engine.
% Check -U / -P arguments, the `EGCTL_PASSWORD` env var, or the [control] section of egctl.conf.
$ echo $?
1
The same message renders on eghost cli; the host-side wrapper exits non-zero and the operator stays on their shell prompt. Pre-2026.23.1.279 versions opened the shell anyway and surfaced the failure on the first command — operators upgrading from that line will see the failure earlier now.
Configuration file¶
egctl reads its configuration from /etc/enforcegate/egctl.conf.
[global]¶
The [global] section contains settings that apply globally to egctl.
| Name | Type | Description | Default |
|---|---|---|---|
host |
string | Destination hostname or IP address of the engine. | 127.0.0.1 |
port |
integer | Destination port for the Control API. | 11225 |
cert |
string | Path to the engine's TLS certificate (for pinning). | /etc/enforcegate/ssl/cert.pem |
username |
string | Username used for Control API authentication. | admin |
The password is never in the config file — it always comes from -P argv, the EGCTL_PASSWORD env var, or an interactive prompt.
The shipped default is suitable for the standalone container, where egctl runs inside the same container as the engine and reaches it over loopback.