Engine¶
The EnforceGate vX engine processes requests received from local and remote connectors. It is the central intelligence component where all policy evaluation occurs. The engine maintains highly optimised in-memory match structures to scan request attributes (URI, SNI, user-agent, client IP, …) and persists compiled ACLs in a local DuckDB database.
The engine binary is /usr/local/bin/enforcegate-engine inside the standalone image. It runs as the unprivileged enforcegate user. Its long-running siblings are Squid (/usr/sbin/squid) and the Squid connector helper (/usr/local/bin/enforcegate-squid-connector); the connector is launched by Squid as a url_rewrite_program child, not by the container init.
When bump-mode SSL inspection is active, the engine also produces encrypted verdict-redirect payloads using the shared secret from the enforcegate-shared volume. The captive portal verifies and decrypts them on the visitor's behalf — see captive portal.
Command-line flags¶
| Flag | Effect |
|---|---|
-h, --help |
Print help and exit 2. |
-v, --version |
Print engine version (e.g. 2026.30.0.328) and exit 2. |
-c <path>, --conf <path> |
Use <path> as the engine config file. Default /etc/enforcegate/engine.conf. |
-D, --daemon |
Double-fork detach plus close stdin / stdout / stderr. For bare-metal / systemd installs. |
--pidfile <path> |
Override the default PID file (/run/enforcegate/engine.pid). Daemon mode flocks this file exclusively. |
--loglevel <level> |
Override the configured logging level for this run. One of emergency, alert, critical, error, warning, notice, info, debug. |
--logtype <type> |
Override the configured logger type (file / console / syslog). |
Configuration file¶
The engine reads its configuration from a TOML file. The default location is /etc/enforcegate/engine.conf. The shipped default ships with eight sections — [global], [connectors], [license], [database], [policy], [learning], [captive_portal], [aaa] — plus an optional [logging] section. Each is described below.
Operator-tunable keys can be overridden via the bundle's .env file: matching ENFORCEGATE_* environment variables take precedence over the persisted conf at every boot, so .env wins. See your image's documentation for the variable-name to conf-key mapping.
[global]¶
Engine-wide tunables that don't fit a more specific section.
| Name | Type | Description | Default |
|---|---|---|---|
threads |
int | Size of the connector listener pool — threads serving the Defendr wire from connected enforcegate-squid-connector (and future TLS-proxy / ICAP) sessions. Connector sessions multiplex onto this pool; a busy engine can carry many more sessions than threads. The Control API HTTPS pool is independent and fixed at 4 — see show system threads for the runtime split. |
4 |
[connectors]¶
The [connectors] table contains one sub-table per declared connector. Each sub-table ([connectors.<name>]) describes one allowed peer. The engine accepts an incoming Defendr session only from a source IP that matches a configured [connectors.<name>].ip, after the connector authenticates with the shared key.
| Name | Type | Description | Default |
|---|---|---|---|
net |
string | Transport protocol. "tcp" (TCP/TLS) or "socket" (UNIX-domain socket — has known issues in current builds; prefer tcp for production). |
"tcp" |
ip |
string | Bind address for the engine's Defendr listener (use 0.0.0.0 to listen on all interfaces). |
127.0.0.1 |
port |
int | TCP port for the Defendr listener. | 11224 |
key |
string | Pre-shared authentication key. Up to 128 characters; longer is truncated. | (required) |
ip¶
In the standalone bundle the engine binds to 127.0.0.1 because the connector runs in the same container. Multi-image or distributed deployments override to 0.0.0.0.
key¶
Mandatory pre-shared key
EnforceGate enforces mutual peer authentication between the engine and the connectors. The key attribute is mandatory and must be identical on both sides — the engine's [connectors.<name>].key and the connector's [engine.<name>].key. A mismatch is a silent connect failure; show neighbor summary shows no connector, and no log line is emitted on the missing-key path.
In the standalone image, the generate-engine-key boot one-shot writes a random 32-character key into both files on first boot, so for the shipped deployment no manual configuration is required.
[license] — required for first boot¶
The license configuration. The engine refuses to start if any of serial, username, password is empty.
| Name | Type | Description | Default |
|---|---|---|---|
serial |
string | Per-deployment activation serial issued by Exosys. 22-char canonical XXXXX-XXXX-XXXX-XXXX-X format — see licensing. |
(required, no default) |
username |
string | Control-server account name authorised to bind this serial. | (required) |
password |
string | Control-server account password. | (required) |
enforce_permissions |
bool | Hard-fail the engine if license-file permissions are too permissive. Production deployments set true. |
false (dev) / true (prod images) |
Override every credential field for test/prod tiers via .env (ENGINE_LICENSE_SERIAL, ENGINE_LICENSE_USERNAME, ENGINE_LICENSE_PASSWORD). The boot card prints a [ WARN ] line for the serial and username overrides; the password override is silenced (length-only) so credentials don't leak into docker logs.
[database]¶
Where the engine's DuckDB store lives.
| Name | Type | Description | Default |
|---|---|---|---|
database_file |
path | DuckDB file holding the live policy DB and engine session state. | /var/lib/enforcegate/engine.db |
[policy]¶
Policy source and snapshot configuration.
| Name | Type | Description | Default |
|---|---|---|---|
default_action |
string | Engine-synthesised no-match verdict — what the engine returns when no rule matches a request. One of "permit" (request proceeds), "deny" (block page), "warn" (warn page with proceed-anyway CTA), "aup" (Acceptable Use Policy gate). Replaces the historical pattern of shipping a catch-all .policy rule (retired in 2026.32.0); the no-match verdict no longer occupies a rule id, so it can never shadow rules in shared_path. On the captive-portal verdict pages and in show policy match, the synthesised verdict reports rule name default-permit / default-deny / default-warn / default-aup. |
"permit" |
path |
path | Directory the engine scans recursively for *.policy files on reload. Operator-authored rules. Loaded first; gets the lower rule ids; wins precedence on conflict (the engine's lowest-rule-id-wins rule). A parse error here fails the reload loudly. |
/etc/enforcegate/rules.d/ |
shared_path |
path | Optional second directory the engine scans for *.policy files. Machine-generated rules handed off from the toolbox sidecar over a shared volume — see Toolbox. Loaded after path, so its rules get higher ids and lose the lowest-id-wins match — a generated policy can propose but never silently override an operator deny. A parse error here is logged as a Warning and the file is skipped; the rest of the load proceeds. Set to "" to disable. Snapshots and request policy rollback cover path only. |
/etc/enforcegate-shared/rules.d/ |
backup_path |
path | Where pre-apply snapshots are written. Each snapshot is a timestamped subdirectory mirroring path/. |
/var/lib/enforcegate/policy-backups/ |
backup_depth |
int | Retain the N most-recent snapshots; prune older ones after every successful reload. | 10 |
domain_backend |
string | In-memory backend for match-domain-list: data. "auto" (default) — engine picks at policy load based on entry count; "hashset" — force the smaller / faster backend; "sorted-arena" — force the larger-scale backend. The default is fine for almost every deployment. |
"auto" |
domain_backend_auto_threshold |
int | When domain_backend = "auto", the entry count above which the engine selects sorted-arena. Lower thresholds bias toward the larger-scale backend earlier; higher thresholds keep the faster backend longer. |
50000000 |
git_mode |
string | Tri-state opt-in for per-rule audit + history tracking via a git repository in [policy].path. "auto" (default) — enable if [policy].path/.git/ exists, fall back to snapshot-only otherwise; "force-on" — require .git/ to be present (engine logs Critical if it is not); "force-off" — never enable, ignore any .git/ directory present. See Policy audit and history for the operator workflow. |
"auto" |
time_window_tz |
string | Timezone the engine evaluates time-window: attributes against. "local" — engine host's local wall clock; reads /etc/localtime. Keep NTP in sync — a wrong system clock makes time-windows fire at the wrong times. "utc" — interpret HH:MM against UTC; the right choice for deployments with operators in multiple timezones. Confirm what "local" means on the box with show clock. |
"local" |
See policy rollback for the snapshot workflow and policy audit for the optional git-tracked history layer that sits on top of snapshots. The single source of truth for the live policy is the engine's DuckDB store at [database].database_file — see Inspecting the live policy for the operator-side ways to read it.
The active domain backend is reported by show database status and the detailed show system uri-engine verb — operators sizing memory against real numbers reach for the latter.
[learning]¶
Learning-mode (V1 shipped 2026-05-26) configuration. If the section is absent or database_file is unset, the learning subsystem stays silently disabled and request learning … calls return "learning subsystem not initialised".
| Name | Type | Description | Default |
|---|---|---|---|
database_file |
path | Separate DuckDB file for learning sessions. Wipe to reset learning without touching policy. | /var/lib/enforcegate/learning.db |
See learning mode for the operator workflow.
[captive_portal]¶
Captive portal v=1 integration. The engine produces encrypted verdict-redirect URLs with the shared secret; the squid connector relays the URL to client browsers as HTTP 301.
| Name | Type | Description | Default |
|---|---|---|---|
base_url |
URL | Public URL clients reach the portal at. Substituted into the redirect URL the engine builds. Override at deployment time via ENFORCEGATE_CAPTIVE_BASE_URL. |
http://127.0.0.1/ |
secret |
base64 | Shared secret with the portal. Base64-encoded; the same string must be configured on both sides. Generate fresh with openssl rand -base64 32. |
(one of secret / secret_file required) |
secret_file |
path | Path to a file holding the base64-encoded secret. Use for chmod 0400 isolation from the (potentially world-readable) main config. |
/etc/enforcegate-shared/captive-portal-secret (standalone) |
ack_token_max_age_s |
int | How long an ack_token is valid after the engine mints it. The visitor must click "Proceed" on the warn / aup page within this window. |
300 (5 minutes) |
ack_session_duration_s |
int | After a successful ack, the same (rule_name, client_ip) pair won't re-trigger the warn / aup page for this duration. |
3600 (1 hour) |
In the standalone bundle, the generate-engine-key boot one-shot mints the shared secret on first boot and writes it to the shared volume — no operator paste, no two-place drift.
Spec: see captive portal.
[aaa]¶
Authentication, authorisation and first-boot bootstrap.
| Name | Type | Description | Default |
|---|---|---|---|
passwd |
path | The user database. Same shape as /etc/passwd but with auth-algorithm + salt + hash columns. |
/etc/enforcegate/passwd |
auto_bootstrap |
bool | If true, the engine on first boot (when passwd is empty) generates a random admin password, writes it to bootstrap_credential_file, and logs the cleartext once at [critical]. Production Docker bundles set this to true. |
false |
bootstrap_credential_file |
path | Where the bootstrap credential JSON is written. The installer's docker logs consumer surfaces the password in a dialog. |
/etc/enforcegate/aaa-bootstrap-credential.json |
bootstrap_credential_ttl_seconds |
int | How long the bootstrap credential file loiters if no admin login captures it. | 86400 (24 hours) |
See Privilege model below for the four levels and what each can do.
[control]¶
The Control API is the engine's HTTPS REST interface — egctl talks to it via docker exec and the captive portal sidecar reaches it server-side over the compose network for the /api/captive/ack round-trip.
| Name | Type | Description | Default |
|---|---|---|---|
ip |
string | IP address on which the Control API listens. | 0.0.0.0 |
port |
integer | TCP port for the Control API. | 11225 |
key |
string | Path to the private key file (TLS). | conf/ssl/key.pem |
cert |
string | Path to the X.509 server certificate. | conf/ssl/cert.pem |
dhparams |
string | Path to the Diffie-Hellman parameters file (for PFS). | conf/ssl/dhparams.pem |
ip¶
The default 0.0.0.0 inside the container makes :11225 reachable from the captive-portal sidecar across the compose network. The shipped docker-compose.yml does not publish :11225 to the host. For single-image deployments without a portal sidecar, lock down to 127.0.0.1 via ENGINE_CONTROL_API_IP=127.0.0.1 in .env.
port¶
The default 11225 matches egctl.conf's default and the captive portal sidecar's ENGINE_INTERNAL_URL.
TLS material¶
The Control API serves a self-signed X.509 certificate generated by the generate-ssl-certs boot one-shot on first boot. The portal sidecar verifies with verify=False because the TLS hop is loopback-equivalent inside the compose bridge; trust comes from the compose-network boundary, not from PKI verification. For egctl from outside the container, configure cert in egctl.conf to pin the certificate the engine serves.
[logging] (optional)¶
Logger configuration. Absent → file logger with default /var/log/enforcegate/engine.log and error level.
| Name | Type | Description | Default |
|---|---|---|---|
type |
string | Where logs go. "file", "console" (writes to stderr — captured by docker logs), or "syslog" (POSIX LOG_DAEMON, identity enforcegate-engine). |
"file" |
file |
path | Log file path when type = "file". |
/var/log/enforcegate/engine.log |
level |
string | Threshold. One of critical, error, warn, info, debug. |
error |
| Level | When | What you see |
|---|---|---|
critical |
Fatal failures | License-init exit, ASan abort, segfault |
error |
Recoverable but bad | Policy parse failure, DB connection lost |
warning |
Suspicious | Permission warnings, deprecated config keys |
notice |
Normal operations to notice | Engine startup, policy reload success, connector connect / disconnect, SIGTERM handling |
info |
Normal operations (default) | As above with more detail |
debug |
Verbose tracing | Per-request URL + verdict, hyperscan match events, AAA decisions |
Debug rates the engine significantly
Per-request logging at debug level meaningfully impacts throughput. Don't leave debug on in production — use it for incident diagnosis only.
Foreground debug run from the host
eghost shell # → /bin/sh in the enforcegate container
/usr/local/bin/enforcegate-engine --loglevel debug --logtype console
Do not use --logtype console for the supervised engine
The engine's libc stdio is block-buffered when stdout is not a TTY, which starves docker logs. The shipped boot configuration uses the file logger plus a tail+awk pipeline to stream lines in real time. Only override the log type for one-off foreground instances launched interactively.
Diagnostic file¶
A top-level key (no enclosing section) that historically lived outside [license]:
| Name | Type | Description | Default |
|---|---|---|---|
diagnostic_file |
string | Absolute path inside the container where APM diagnostics land. | /var/lib/enforcegate/apm-last-error.json |
Override via ENGINE_APM_DIAGNOSTIC_FILE only for tmpfs deployments, systemd-managed paths, or read-only-rootfs containers where a different writable path is required. The reason-string set is enumerated in the troubleshooting page.
Privilege model¶
EnforceGate vX uses a four-level privilege model for operator accounts. Each Control API verb checks the requesting user's privilege against a minimum threshold; verbs that mutate state require higher privileges than verbs that read state.
The four levels¶
| Level | Numeric | Typical bearer |
|---|---|---|
| Disabled | 0 | The account exists in /etc/enforcegate/passwd but cannot authenticate. Used to lock an account out without deleting it. |
| Monitoring user | 1 | Read-only analyst. Sees the engine state, neighbour table, policy backups, user list, learning sessions, and the verdict of show policy match <url> (without the matched regex pattern). |
| Standard user | 2 | Everything monitoring can do, plus request neighbor teardown. Operational role for someone who manages connector sessions but not policy or accounts. |
| Service account | 3 | Capability-scoped, not level-scoped. Can call request policy reload and the show status family — and nothing else. No user / license / neighbor / reboot ops. Intended bearer is automation that only needs to refresh policy (e.g. the toolbox sidecar's reload helper). Created via the R) Service account (policy-reload only) entry in request user add. New in 2026.33.0. |
| Administrator | 10 | Full policy and user management. Loads, reloads and rolls back policies; adds, removes and re-passwords users; sees the matched regex pattern from show policy match <url>. |
| Super Administrator | 11 | Same operational surface as Administrator, plus the unique ability to create, re-password, or remove Administrator-level accounts. The must-outrank invariant (see below) is what makes this distinct: only a Super Administrator outranks an Administrator. The shipped admin account is at this level. |
Verb privilege thresholds¶
A representative sample of which egctl verbs each level can call:
| Verb | Minimum privilege |
|---|---|
show status |
Monitoring (1) |
show version |
Monitoring (1) |
show neighbor summary |
Monitoring (1) |
show neighbor detail |
Monitoring (1) |
show users |
Monitoring (1) |
show policy backups |
Monitoring (1) |
show learning sessions |
Monitoring (1) |
show policy match <url> (verdict only) |
Monitoring (1) |
show policy match <url> (with matched regex) |
Administrator (10) |
request neighbor teardown |
Standard (2) |
request policy reload |
Administrator (10) — also callable by a Service account (3) |
request policy rollback |
Administrator (10) |
request engine restart |
Administrator (10) |
request user add |
Administrator (10) — and must outrank the new user |
request user passwd <self> |
Monitoring (1) |
request user passwd <other> |
Administrator (10) — and must outrank the target |
request user remove <other> |
Administrator (10) — and must outrank the target; refuses self-remove |
request learning create/start/stop/delete/analyze |
Administrator (10) |
Operational guidance¶
- Principle of least privilege. Give an analyst Monitoring (1), not Administrator (10), unless they need to mutate state.
- The
request user *verbs enforce a must-outrank invariant: an Administrator cannot edit a Super-Administrator, and no user can remove themselves. - The shipped default account is
admin / enforcegate-changemeat Super Administrator (11). The first thing to do on a fresh deployment is change that password — either viarequest user passwd admin(after auth) or by removing the account entirely once another Super Administrator exists. - The bootstrap admin is provisioned at Super Administrator (11) so it can mint additional Administrator-level accounts via
request user adddirectly out of the box — the must-outrank invariant requires the requestor to outrank any account it creates.
First-boot bootstrap¶
With [aaa].auto_bootstrap = true in engine.conf, the engine on first boot (when passwd is empty) generates a random admin password, writes the cleartext to bootstrap_credential_file, and logs it once at [critical]. The appliance installer surfaces the password in the setup wizard; on Docker bundles operators read it from the boot log.
File paths summary¶
For quick reference — what the engine reads and writes when configured with the shipped defaults:
| Path | Role |
|---|---|
/etc/enforcegate/engine.conf |
The config file documented above. |
/etc/enforcegate/passwd |
User database (Administrator + monitoring + standard accounts). |
/etc/enforcegate/license/cs.key.pub |
Control-server public key (shipped). |
/etc/enforcegate/license/license.key |
Per-host private key (first-boot-generated). |
/etc/enforcegate/license/license.key.pub |
Per-host public key. |
/etc/enforcegate/license/license.lic |
Signed license blob. |
/etc/enforcegate/rules.d/*.policy |
Operator-authored policy files. |
/etc/enforcegate/ssl/{cert,key,dhparams}.pem |
Defendr listener + Control API TLS material. |
/var/lib/enforcegate/engine.db |
Live compiled policy DuckDB. |
/var/lib/enforcegate/learning.db |
Learning sessions + captured URIs. |
/var/lib/enforcegate/policy-backups/<timestamp>/ |
Pre-reload snapshots of rules.d/. |
/var/lib/enforcegate/apm-last-error.json |
Structured APM diagnostic written on fatal license-init exits. |
/var/lib/enforcegate/.time-ratchet |
Engine-only internal state file (do not edit or delete). |
/var/log/enforcegate/engine.log |
File-logger sink when [logging].type = "file". |
/run/enforcegate/engine.pid |
Daemon-mode PID file. |
/run/enforcegate/ (runtime sockets) |
UNIX-domain sockets used internally when the connector runs on the same host. |