Skip to content

SSL inspection

EnforceGate vX ships with a complete Squid ssl-bump infrastructure: a bump CA at /etc/enforcegate/ssl-bump/{ca.pem,ca.key}, a per-host certificate cache at /var/spool/squid/ssl_db, and the sslcrtd_program helper. What is active at any moment is driven by the ENFORCEGATE_SSL_INSPECT environment variable consumed by the init-ssl-bump boot one-shot on every container start.

Legal and operational consequences

Enabling bump mode decrypts all HTTPS traffic that flows through this proxy. This may be regulated in your jurisdiction (employee privacy, GDPR, sector-specific telecom laws) and requires every client device to trust the deployment's bump CA. The container refuses to start bump mode without an explicit binding acknowledgement — see Activating bump mode safely.

Modes

Mode What Squid does Client trust required Set in .env
off CONNECT tunnel only. Squid sees the destination hostname but cannot read inside HTTPS. Behaviour identical to a proxy without ssl-bump. No ENFORCEGATE_SSL_INSPECT=off (default)
peek Squid extracts the TLS SNI from the ClientHello and hands it to the engine for per-host decisions, then splices the rest. No termination. No ENFORCEGATE_SSL_INSPECT=peek
bump Full HTTPS decryption. Squid terminates the client's TLS, forges a per-host cert from the bump CA, re-encrypts to the origin. Yes — every client device must trust the deployment's bump CA. ENFORCEGATE_SSL_INSPECT=bump plus ENFORCEGATE_SSL_INSPECT_ACK=1

What each mode enables for ACL matching:

  • off — only SNI (via Squid acl directives) and connection-layer attributes match HTTPS. The engine's URL-rewrite policies cannot match URL paths or any application-layer content.
  • peek — SNI-based policy matching at the engine, but bodies remain opaque. URL-path matching does not work.
  • bump — the engine sees fully decrypted HTTPS traffic. All URL-rewrite policies (path, headers, user-agent, referer, …) apply. The captive portal's verdict redirect for HTTPS is delivered inside the bumped TLS session — the browser follows it cleanly.

Activating bump mode safely

Edit the bundle's .env:

.env
ENFORCEGATE_SSL_INSPECT=bump
ENFORCEGATE_SSL_INSPECT_ACK=1
ENFORCEGATE_SSL_INSPECT_OPERATOR=jdoe@example.com   # required identifier

Then restart the stack:

eghost restart enforcegate
eghost logs enforcegate | head -50 | grep "SSL inspection"
# → [system]    SSL inspection: bump (full HTTPS decryption) ............. [ WARN ]

Without ENFORCEGATE_SSL_INSPECT_ACK=1, the container refuses to start:

[system]    SSL inspection: bump requested but acknowledgment not set ... [ FAIL ]
    SSL inspection in "bump" mode decrypts ALL HTTPS traffic that
    flows through this proxy. This may be regulated in your
    jurisdiction (employee privacy, GDPR, sector-specific telecom
    laws, etc.) and requires every client device to trust this
    deployment's bump CA at /etc/enforcegate/ssl-bump/ca.pem.

    Set ENFORCEGATE_SSL_INSPECT_ACK=1 in your environment to confirm
    you have the authority + user notification + CA distribution in
    place. See /EULA.md, /LICENSE.md, and /WARRANTY.md inside this
    container, or https://docs.exosys.ch/enforcegate-vx/latest/license/.

This fail-fast pattern is intentional: enabling TLS interception has legal, contractual, and trust-distribution consequences that should never be a default and never a one-character typo away.

Pinned-destination splice list

Some platforms pin the upstream certificate at the application layer — Windows Update, the Microsoft Store, Defender update channels, parts of Apple MDM, and a number of mobile / banking apps. When Squid bumps an HTTPS connection it serves a forged leaf signed by the bump CA; that leaf fails the pin check and the dependent feature stops working (Windows Update silently halts, Apple DEP enrolment fails, etc.).

ENFORCEGATE_SSL_INSPECT_SPLICE_DOMAINS opts specific destinations out of bump. Spliced traffic flows through unmodified: the client sees the original upstream cert, its pin check passes, and the feature keeps working — at the cost that the engine sees only the SNI for those flows.

.env
ENFORCEGATE_SSL_INSPECT_SPLICE_DOMAINS=*.windowsupdate.com;*.windowsupdate.microsoft.com;*.update.microsoft.com;download.microsoft.com;*.delivery.mp.microsoft.com;*.dl.delivery.mp.microsoft.com;*.do.dsp.mp.microsoft.com;emdl.ws.microsoft.com

Syntax: *.foo.com matches any subdomain of foo.com plus the apex; bare FQDNs match exactly. The list is opt-in (empty by default); the canonical Microsoft list is shipped commented out in .env.example for one-line activation. Setting the variable in peek or off mode is harmless — the rule has no effect and the boot card surfaces a WARN.

The boot card surfaces the active splice list:

[system]    SSL inspection: bump (full HTTPS decryption) ............. [ WARN ]
[system]    Operator splice list active — 8 pinned-SNI patterns bypass bump [  OK  ]

Distributing the bump CA to clients

Every client device that routes HTTPS through the proxy must trust the deployment's bump CA. Extract the CA cert:

docker exec enforcegate cat /etc/enforcegate/ssl-bump/ca.pem > enforcegate-bump-ca.pem

Hand this PEM off to your MDM (Intune, Jamf, Workspace ONE), AD GPO, or update-ca-trust-style automation to install in every client device's trust store.

Do not distribute /etc/enforcegate/ssl-bump/ca.key — that's the private key. Treat it as long-lived secret material; back up the enforcegate-config volume before any major operation.

For unmanaged devices (BYOD, guests, hot-desk machines), the captive portal exposes a self-service install page at /ca plus a direct download at /ca.crt. Both routes are gated to 404 unless the bump CA file is present in the shared volume — they appear only when bump mode is active.

Audit log

Every activation of bump mode appends one JSONL record to a hash-chained audit log inside the persistent config volume:

/etc/enforcegate/audit/ssl-inspect-ack.log

Each record pins:

  • timestamp,
  • active mode,
  • operator identifier from ENFORCEGATE_SSL_INSPECT_OPERATOR,
  • container host + image version,
  • SHA-256 of /EULA.md, /LICENSE.md, and /WARRANTY.md as bundled in the image (so the exact legal text in force at activation is reconstructable),
  • previous record's hash (prev_hash),
  • self-hash of this record (this_hash).
{"t":"2026-05-14T14:32:00Z","mode":"bump","op":"jdoe@example.com",
 "host":"enforcegate-prod-01","image":"2026.30.0-EA",
 "eula_sha256":"abc...","license_sha256":"def...",
 "warranty_sha256":"789...",
 "prev_hash":"00000000...","this_hash":"xyz123..."}

The chain makes tampering detectable: each record's this_hash covers the contents of that record plus the previous record's this_hash. Modifying a past entry changes its recomputed this_hash, which no longer matches the prev_hash of the next entry. The container refuses to activate bump mode if the last line of the log lacks a parseable this_hash — silent chain restarts would hide tampering.

Tamper EVIDENCE, not tamper PREVENTION

A malicious container owner can still rewrite the file; the chain break is detectable but not prevented by the in-container mechanism. For stronger guarantees, ship the log out of the container (docker cp, periodic sync to an append-only object store such as S3 with object lock, or a syslog forwarder).

Retention. The log lives in enforcegate-config and survives container removal and image upgrades. EULA § 3.2 obliges the Licensee to preserve it for the duration of the Agreement plus at least five (5) years and to produce it on reasonable request. Wiping the volume (factory reset — docker compose ... down -v) destroys the log — back up first:

docker run --rm -v standalone_enforcegate-config:/data \
    alpine cat /data/audit/ssl-inspect-ack.log >> ack-log-backup.jsonl

Rotating the bump CA

The bump CA at /etc/enforcegate/ssl-bump/{ca.pem,ca.key} is generated once on first boot and persists indefinitely (10-year validity, no auto-rotation). Plan a rotation for:

  • Compromise — bump CA private key was exposed (laptop loss, hostile insider). Rotate immediately.
  • Scheduled refresh — internal policy requires periodic key rotation.
  • Pre-expiry — image was deployed 9+ years ago and the cert is approaching its 10-year notAfter.
  • Organisational change — Subject DN needs to change (rebrand, M&A).

Every client that trusts the CA has it pinned in its trust store, so any rotation is a client-fleet trust-distribution event, not just a server-side swap. Two procedures, pick one based on urgency:

Distributes the new CA to every client trust store before the server swaps, then swaps when fleet coverage is verified.

# 1. Generate the new CA OFFLINE (any host with openssl).
openssl genrsa -out new-ca.key 2048
openssl req -x509 -new -nodes -sha256 -key new-ca.key -days 3650 \
    -subj '/CN=EnforceGate SSL Inspection CA/O=Exosys Sarl' \
    -addext 'basicConstraints=critical,CA:TRUE' \
    -addext 'keyUsage=critical,digitalSignature,keyCertSign,cRLSign' \
    -out new-ca.pem

# 2. Push new-ca.pem to every client trust store ALONGSIDE the existing one.
#    Both CAs trusted simultaneously — clients trust whichever cert chain
#    squid presents.

# 3. Verify fleet coverage. Do not proceed until you confirm >99% of
#    in-scope clients have new-ca.pem in their trust store.

# 4. Swap on the server.
eghost down
docker run --rm \
    -v standalone_enforcegate-config:/data \
    -v "$(pwd)":/in \
    alpine sh -c '
      cp /in/new-ca.pem /data/ssl-bump/ca.pem
      cp /in/new-ca.key /data/ssl-bump/ca.key
      chown 101:103 /data/ssl-bump/ca.pem /data/ssl-bump/ca.key
      chmod 0640    /data/ssl-bump/ca.pem /data/ssl-bump/ca.key
    '
eghost up

# 5. Smoke-test from a client (curl through the proxy).

# 6. After a soak period (typically 1–2 weeks), push a second trust-store
#    update that REMOVES the old CA. Old key is now retired.

Procedure B — Hard rotation (compromise only)

Generates a fresh CA in-container in one step, before fleet readiness. Every client sees TLS errors until it receives the new CA — accept the outage.

# 1. (optional) Back up the existing CA in case the compromise diagnosis is wrong.
docker exec enforcegate sh -c \
    'tar c /etc/enforcegate/ssl-bump' > bump-ca-before-rotation-$(date +%F).tar

# 2. Wipe + restart. The init-ssl-bump oneshot generates a fresh CA when
#    neither ca.pem nor ca.key exists.
eghost down
docker run --rm -v standalone_enforcegate-config:/data alpine \
    sh -c 'rm -f /data/ssl-bump/ca.pem /data/ssl-bump/ca.key'
eghost up

# 3. Extract the new CA and rush-distribute to clients.
docker exec enforcegate cat /etc/enforcegate/ssl-bump/ca.pem > new-bump-ca.pem

Verifying a rotation

# What CA does the container now serve?
docker exec enforcegate openssl x509 \
    -in /etc/enforcegate/ssl-bump/ca.pem -noout -fingerprint -sha256

# From a client with the new CA installed, exercise the proxy
curl -sS --proxy http://<proxy-host>:3128 \
    https://www.exosys.ch -o /dev/null -w "HTTP: %{http_code}\n"

# Should report HTTP: 200 with no TLS error.