Skip to main content

Standalone Shield Install

Most edges run elchi-shield bundled by elchi-client — the client installer drops the sidecar in the same run and manages its lifecycle. If an edge runs Envoy without elchi-client (a standalone proxy host, a third-party Envoy, a test box), you can install elchi-shield on its own with deploy/elchi-shield-install.sh. This page covers that path.

Which install do I want?

If elchi-client already runs on the host, do not use this installer — the client installs and owns elchi-shield for you. See The Bundled Shield Sidecar and the Elchi Client overview. Use the standalone installer only when there is no elchi-client on the box.

Unlike elchi-client, elchi-shield has no central connection of its own. Its policy is delivered as files into a watched directory; the standalone installer only places the binary and the systemd unit, so it takes no --host and no --token. On a client-less host you are responsible for populating the watched config directory yourself.

What the installer does

Run it as root from an elchi-shield checkout (or a downloaded copy of the script):

sudo ./deploy/elchi-shield-install.sh

By default this:

  • creates the shared elchi system user and group (idempotent — the same identity the rest of the Elchi stack uses);
  • adds Envoy's user (envoyuser) to the elchi group so it can reach the ext_proc socket;
  • builds the /etc/elchi/elchi-shield directory tree;
  • downloads the latest release binary, sha256-verified, to /etc/elchi/bin/elchi-shield;
  • writes a hardened elchi-shield.service systemd unit;
  • enables and starts the service.

Flags

FlagDescription
--version=vX.Y.ZInstall a specific release (default: latest).
--buildCompile this checkout instead of downloading a release. Needs Go 1.26+ on PATH (static, CGO_ENABLED=0).
--user=NAMEService user/group (default elchi).
--no-startInstall and enable the unit but do not start it.
--audit-clickhouse-dsn=DSNSend audit events to central ClickHouse. Omit → audit is OFF (there is no local-file sink). Env: ELCHI_SHIELD_AUDIT_CLICKHOUSE_DSN.
--metrics-otlp-endpoint=H:PPush metrics to an OTel Collector over OTLP/gRPC. Omit → only the loopback /metrics scrape exists. Env: ELCHI_SHIELD_METRICS_OTLP_ENDPOINT.
--metrics-otlp-insecureUse plaintext gRPC to the metrics collector. Env: ELCHI_SHIELD_METRICS_OTLP_INSECURE.
# Pin a release, wire audit to ClickHouse and metrics to an OTel Collector
sudo ./deploy/elchi-shield-install.sh \
--version=v0.4.5 \
--audit-clickhouse-dsn=clickhouse://user:pass@ch.internal:9000/elchi \
--metrics-otlp-endpoint=otel-collector:4317 --metrics-otlp-insecure

# Build from the local checkout instead of downloading
sudo ./deploy/elchi-shield-install.sh --build

The make install target wraps this and passes --build (compile the checkout); pass extra args with ARGS=… (e.g. make install ARGS="--version=v0.4.5").

Audit DSN handling

When you pass --audit-clickhouse-dsn, the DSN may carry credentials, so it is written to a restricted EnvironmentFile (/etc/elchi/elchi-shield/audit.env, mode 0640) that the unit reads — never to the world-readable ExecStart. Re-running without the flag drops any stale DSN and disables audit. See Shield observability.

Layout and socket

PathPurpose
/etc/elchi/bin/elchi-shieldThe binary (owned root:elchi, mode 0755).
/etc/elchi/elchi-shield/conf.dWatched policy directory (*.yaml / *.json); hot-reloaded.
/etc/elchi/elchi-shield/filesData files (threat feeds, JWKS, OpenAPI specs).
/run/elchi-shield/extproc.sockext_proc UDS Envoy connects to (systemd RuntimeDirectory, group-owned elchi).
127.0.0.1:9001Loopback health/metrics (/healthz, /readyz, /metrics).
/var/log/elchiShared log directory.

The watched conf.d starts empty — an empty directory means "no policy", so the configured default posture applies. On a standalone host, drop your policy files here yourself (there is no elchi-client to push them). Policy authoring is covered in Shield deployment.

systemd hardening

The generated unit runs the sidecar locked down: NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp, ProtectKernelTunables, ProtectControlGroups, RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6, UMask=0007, and ReadWritePaths limited to the shield tree and the log dir. Health/metrics bind loopback only; the ext_proc socket is a group-owned UDS. The sidecar is never reachable off-box by construction. An ExecStartPre=… validate step checks the config dir at boot but is non-fatal — a bad file never blackholes traffic; shield keeps the last valid config and applies the default posture.

Wire Envoy to the socket

Point Envoy's ext_proc cluster at the UDS and make sure Envoy's user is in the elchi group (restart Envoy after usermod so it picks up the new group):

unix:///run/elchi-shield/extproc.sock

The full filter/cluster configuration — and the request attributes shield reads — is in Envoy wiring. All process flags are in the Shield reference.

Run Shield in a container

elchi-shield also ships as a minimal, static, distroless non-root image — a single full binary with every engine (including the Coraza WAF and embedded OWASP CRS) and audit sink compiled in. There are no build tags and no "lean" variant.

  • make docker builds the from-source reference image (deploy/Dockerfile, multi-stage, golang:1.26gcr.io/distroless/static-debian12:nonroot).
  • The release pipeline instead bundles the prebuilt release binary with deploy/Dockerfile-release-binary (no Go toolchain, reusing the exact GitHub Release artifact).

Both images run as UID 65532 and EXPOSE 9001 (the loopback health/metrics port).

Share the ext_proc socket and config with Envoy through a mounted volume. The socket directory must be writable by uid 65532:

docker run --rm \
-v /etc/elchi/elchi-shield:/etc/elchi/elchi-shield \
-v /run/elchi-shield:/run/elchi-shield \
elchi-shield:latest \
--config-dir /etc/elchi/elchi-shield/conf.d \
--extproc-network unix \
--extproc-addr /run/elchi-shield/extproc.sock \
--http-addr 127.0.0.1:9001

Envoy (in the same pod / on the same host) then dials the shared UDS.

Exposing 9001 in a container

9001 is loopback-only by default; a non-loopback bind is refused unless you pass --allow-non-loopback. Only expose the port (-p 9001:9001 … --http-addr 0.0.0.0:9001 --allow-non-loopback) on a trusted, network-isolated interface — the sidecar must never be reachable from untrusted networks.

Uninstall

sudo ./deploy/elchi-shield-uninstall.sh # prompts for confirmation
sudo ./deploy/elchi-shield-uninstall.sh --yes # non-interactive

This removes only elchi-shield's own artifacts: the service, the binary, the /etc/elchi/elchi-shield tree (offering a tar.gz backup of conf.d/files first if policy files are present), and any legacy local audit log from pre-ClickHouse installs. The shared elchi user/group, /etc/elchi, /etc/elchi/bin, and /var/log/elchi are left intact — they belong to elchi-client and the rest of the stack. Remember to remove the ext_proc cluster/filter from Envoy afterward so it stops dialing the now-absent socket.