Skip to content

Toolbox

A companion toolbox sidecar container ships alongside the engine on every standalone deployment. It carries a bash and Python environment plus the standard EnforceGate CLI tooling pre-installed, and gives operators a sandboxed place to run their own scripts against a running deployment — without installing dependencies on the host or shelling into the engine container.

The toolbox is off by default and opt-in. Operators turn it on at install time on the wizard's Customize Options page, or later via eghost toolbox enable. Once active, it runs as a dedicated container (enforcegate-toolbox) with its own hardening posture and its own writable volume, and hands generated artefacts off to the engine through the shared enforcegate-shared volume.

Canonical use case: category-based filtering

The primary reason operators reach for the toolbox is category-based filtering. Upstream feeds (adult content, gambling, social media, malware / phishing threat-intel, ad-tracker block-lists) ship as flat domain lists and re-publish on a regular cadence — daily for threat-intel, weekly or monthly for category corpora. A typical operator flow:

  1. A scheduled job inside the toolbox downloads the latest version of each upstream list.
  2. The script verifies the download (signature, checksum, sanity-check line count) and writes the cleaned domain list to the shared lists area: /etc/enforcegate-shared/lists/<name>.list.
  3. If anything changed, the script writes (or refreshes) a matching .policy file to the shared rules area: /etc/enforcegate-shared/rules.d/<name>.policy.
  4. The script triggers a policy reload — see the helper library below. The engine picks both up on the next reload.
  5. The script emits a one-line JSON status log to stdout that a SIEM can ingest via docker logs.

The handoff path between the toolbox and the engine is the shared volume. Operator-authored .policy rules in [policy].path always win precedence over toolbox-generated rules in [policy].shared_path — a misbehaving script can propose but never silently override an operator deny. See [policy] for the precedence model and policy recipes for the pattern that pairs with toolbox-refreshed lists.

What ships inside the container

Debian (bookworm-slim) base — the one shipped image that runs on glibc. Every other container in the stack (the engine, the squid connector, the TLS terminator, the captive portal) is Alpine because it runs Exosys code against pinned dependencies; the toolbox is categorically different — it runs arbitrary operator code (your git repos, your pip install, possibly third-party prebuilt binaries) — and glibc maximises compatibility for that workload.

The image carries:

  • Shells and core toolsbash, coreutils, curl, git, tar, gzip, xz-utils, zstd, unzip, jq, whois, dnsutils (dig / host / nslookup), openssl, openssh-client.
  • Python 3 (Debian 3.11) — with python3-requests, python3-yaml, and python3-urllib3 pre-installed from Debian main. pip install --user is first-class — manylinux wheels load against glibc, so most of PyPI is available out of the box. User installs land in the persistent operator volume (/var/lib/enforcegate-toolbox/.local/), so they survive container restarts and image upgrades, and the read-only rootfs still protects the system site-packages.
  • The EnforceGate operator CLIsegctl, egpolicy, egctl --cli.
  • A small Python helper library at /usr/lib/python3/dist-packages/enforcegate_toolbox/:

    from enforcegate_toolbox import lists, policies, engine, log
    
    lists.write("category-adult", entries)
    policies.write("40-deny-adult", policy_text)
    engine.reload()           # POST to the engine's control API
    log.info("category-adult refreshed: 412903 hosts")
    

    The helper exists so the common refresh-and-reload script can be 5–10 lines without operators having to know the engine's reload API or the shared-volume path conventions.

    name is a bare stem, not a path

    lists.write(name, entries) and policies.write(name, content) each take a bare filename stem — no .list / .policy extension and no path separators. The library constructs the full path internally (/etc/enforcegate-shared/lists/<name>.list and /etc/enforcegate-shared/rules.d/<name>.policy) and raises ValueError if name contains a /. Pass "40-deny-adult", not "40-deny-adult.policy" and not "/etc/enforcegate-shared/rules.d/40-deny-adult.policy". Use a numeric prefix on the stem to order toolbox-written rules among themselves (engine load order means every toolbox rule sorts after every operator rule regardless of prefix).

Container hardening

The toolbox image is hardened the same way as the engine, tightened where the toolbox doesn't need what the engine does:

  • read_only: true — the rootfs is mounted read-only. Scripts live on the persistent operator volume; transient scratch goes to tmpfs.
  • cap_drop: [ALL] — no Linux capabilities are granted. Network egress is a kernel feature; volume writes are unprivileged; no setuid binaries.
  • security_opt: [no-new-privileges:true] — privilege escalation via setuid is blocked.
  • pids_limit: 128 — bounded process table.
  • Non-root user — the container runs as the toolbox user (uid 1100), never root.
  • Separate trust store — the toolbox ships only a public CA bundle. The engine's SSL-bump CA is not mounted into the toolbox, so scripts cannot forge traffic-leaf certificates. Operators who need to fetch from an internal HTTPS source with a private CA (an internal Gitea, a corporate mirror) import the CA cert with eghost toolbox ca import <name> and reference it on the relevant repo / fetch.

Volume layout

Two volumes are wired into the toolbox container:

Volume Mount inside container Purpose
enforcegate-toolbox-scripts /var/lib/enforcegate-toolbox Operator-writable. Scripts, schedule entries, git-checked-out repos, imported SSH keys + CA certs, script-private state, the credentials file.
enforcegate-shared /etc/enforcegate-shared Shared with the engine (and the captive-portal sidecar). The toolbox writes to the lists/ and rules.d/ subdirectories.

Layout inside the operator-writable volume:

/var/lib/enforcegate-toolbox/
├── scripts/                   # operator-supplied stand-alone scripts (manual-drop path)
│   ├── 01-squidblacklist.py
│   ├── 02-oisd-default.py
│   └── 99-investigate.sh
├── cron.d/                    # cron schedule entries
│   ├── 01-squidblacklist      # "0 3 * * * scripts/01-squidblacklist.py"
│   └── 02-oisd-default        # "0 4 * * 0 scripts/02-oisd-default.py"
├── repos/                     # cloned git repos (git-repo delivery path)
│   └── <repo-name>/
├── state/                     # script-private working state (last-etag, last-run timestamps, etc.)
├── .ssh/                      # SSH deploy keys imported via `eghost toolbox keys import`
├── ca/                        # private CA certs imported via `eghost toolbox ca import`
└── .creds                     # reload-only service-account credential for engine control-API auth (auto-provisioned)

The handoff layout inside the shared volume:

/etc/enforcegate-shared/
├── lists/
│   ├── squidblacklist-adult.list
│   └── oisd-default.list
└── rules.d/
    ├── 40-deny-categories.policy
    └── 50-warn-trackers.policy

*.list files are domain-per-line text consumed by match-domain-list: directives. *.policy files use the standard policy DSL — see the policies reference.

Operator surface — eghost toolbox

All operator interactions go through the eghost toolbox verb family on the host. See eghost for the verb table. The most common verbs:

eghost toolbox                          # show status (default)
eghost toolbox enable                   # flip ENFORCEGATE_TOOLBOX_ENABLED=true + start the container
eghost toolbox disable                  # stop the container; volumes preserved
eghost toolbox status                   # container state + loaded lists summary
eghost toolbox shell                    # interactive bash inside the container
eghost toolbox run <script>             # one-shot invoke a script under scripts/ now
eghost toolbox logs [-f]                # tail container logs (json-line ingestable into a SIEM)
eghost toolbox cron list                # show the current crontab
eghost toolbox cron edit                # open the crontab in $EDITOR
eghost toolbox lists                    # enumerate currently loaded *.list files

# Git-repo script delivery (multi-repo by name):
eghost toolbox repo add <name> <url> [--branch B] [--entrypoint E] [--ssh-key K] [--ca C]
eghost toolbox repo list
eghost toolbox repo show <name>
eghost toolbox repo pull <name>|--all
eghost toolbox repo run <name> [--pull]
eghost toolbox repo remove <name>

# Credential / trust material:
eghost toolbox keys import <name>       # import an SSH deploy key (paste, Ctrl-D)
eghost toolbox ca import <name>         # import a self-signed CA cert (paste, Ctrl-D)

# Manual drop:
eghost toolbox unpack <archive>         # unpack a tarball/zip docker-cp'd into the container

--ssh-key K and --ca C reference a name you previously imported with keys import / ca import, not a host path — the toolbox stores them in its own volume so they survive container recreate and never leave the sandbox.

Provisioning scripts — three paths

Pick whichever fits your operations:

Git repo (primary)

eghost toolbox repo add myfilters https://git.example.com/ops/filters.git \
    --entrypoint scripts/update.sh

Multiple repos are supported, each addressed by <name>. The toolbox clones into its persistent volume under repos/<name>/. Three transports:

  • Public git over HTTPS (GitHub, GitLab) works out of the box.
  • Internal git with a self-signed cert (e.g. an internal Gitea on gitea.example.local) — eghost toolbox ca import gitea-ca (paste the PEM, Ctrl-D), then eghost toolbox repo add … --ca gitea-ca. The toolbox ships no internal CA by design.
  • Git over SSH with a deploy key — eghost toolbox keys import deploy-key (paste the key, Ctrl-D), then eghost toolbox repo add … --ssh-key deploy-key.

Day-2 updates: eghost toolbox repo pull <name> (or --all) fetches and hard-resets to upstream.

Community projects

The open-source community maintains script collections that drop into the toolbox via this same repo add flow. The most prominent today is EGGuard (maintained by Parsymonie) — a script suite for scheduled category-list refresh and policy synthesis built on the toolbox's helper library. For the operator deployment walkthrough (categories, config, cron schedule, licence chain), see the EGGuard primer. EGGuard, like every project on the Community page, is a third-party contribution — Exosys Sàrl does not author, audit, endorse, or support it. Read the source and run against staging first.

Manual file-drop

For operators who don't want a git flow at all:

docker cp my-scripts.tar.gz enforcegate-toolbox:/var/lib/enforcegate-toolbox/
eghost toolbox unpack my-scripts.tar.gz

Or shell in (eghost toolbox shell) and edit by hand.

Inline edit

eghost toolbox shell drops you into a sh in the container; write or edit scripts directly under /var/lib/enforcegate-toolbox/scripts/.

Scheduling

The toolbox bundles a cron daemon. Operators declare schedules in /var/lib/enforcegate-toolbox/cron.d/, one cron-format file per job, with the command typically pointing at a script under scripts/ or a repo entrypoint:

/var/lib/enforcegate-toolbox/cron.d/refresh-categories
0 3 * * * scripts/01-squidblacklist.py

The container's local timezone defaults to UTC; override with ENFORCEGATE_TOOLBOX_TZ in the bundle's .env (e.g. ENFORCEGATE_TOOLBOX_TZ=Europe/Zurich) if you'd rather interpret schedules against a local wall clock.

Edit interactively with eghost toolbox cron edit.

Engine authentication

The toolbox calls the engine's control API to trigger reloads. From 2026.33.1 onwards, it authenticates with a least-privilege, reload-only service account rather than an Administrator credential.

On the standalone bundle's first boot, the engine auto-provisions a level-3 Service account named enforcegate-toolbox-svc — the engine validates it can call request policy reload and the show status family, and the Control API rejects every other verb. The engine writes the credential to the shared volume at /etc/enforcegate-shared/toolbox-svc-creds (mode 0600, owned by the toolbox uid), and the toolbox's entrypoint copies it into /var/lib/enforcegate-toolbox/.creds. No operator paste, no .env plumbing — the pattern mirrors how the captive-portal shared secret is auto-provisioned.

If a stolen .creds reaches an attacker, it grants a policy reload at worst — not Administrator. That is the entire point of the migration.

Older engine fallback

Against an engine on 2026.33.0 or older — which does not auto-provision the service account — the toolbox falls back to reading ENFORCEGATE_ADMIN_USERNAME / _PASSWORD from the bundle's .env, the way pre-2026.33.1 toolbox releases did. The fallback is automatic; operators upgrading the engine to 2026.33.1+ and the toolbox in lockstep get the service-account path with no action required.

Configuration knobs (.env)

Three knobs in the bundle's .env:

Variable Purpose Default
ENFORCEGATE_TOOLBOX_ENABLED Master on/off switch. eghost toolbox enable / disable flip this; the eghost compose wrapper reads it on every invocation and conditionally passes --profile toolbox to compose. false
ENFORCEGATE_TOOLBOX_TAG Image tag for the toolbox image. Defaults to tracking the standalone image's version (same tier suffix convention as ENFORCEGATE_TAG). tracks engine version
ENFORCEGATE_TOOLBOX_TZ Container-local timezone for cron schedules. UTC

What the toolbox cannot do

The hardening posture is deliberately tight, so a misbehaving script's blast radius stays inside the container:

  • It cannot read or modify the engine's config, license, certificates, snapshot history, or DuckDB.
  • It cannot forge traffic-leaf certificates (the bump CA is not mounted in).
  • It cannot mint or modify engine users — the reload-only service account it authenticates with is rejected by every Control API verb outside request policy reload and the show status family.
  • It cannot bypass the engine's own validation — a malformed .list or .policy written into the shared volume is rejected at the engine's next reload (the previously-loaded ruleset stays live).
  • Its rules cannot silently override hand-authored policy — the engine loads [policy].path first, so operator rules always get lower ids and win the lowest-id-wins precedence rule. See [policy] and the policies reference.