Skip to content

Config Reference

Complete reference for /etc/ezyshield/config.yaml — log sources, enforcement backends, notifications, AI, enrichment, and the dashboard. The file is strictly validated: unknown keys are rejected with exact line numbers.

ezyshield init and the ezyshield config <component> wizards write to /etc/ezyshield and must run with sudo — they fail fast with a hint before asking any question. Validate any manual edit with ezyshield config validate.

Top level

FieldTypeDefaultDescription
data_dirstring/var/lib/ezyshieldRequired (config validate rejects an empty value). State directory for the dashboard command — its auth database is <data_dir>/dashboard.db. It does not set the daemon's SQLite database path: that is the run --db flag (default /var/lib/ezyshield/ezyshield.db).
socket_pathstring/run/ezyshield/ezyshield.sockControl-socket path the dashboard connects to (unix socket — there is never a TCP listener for control). It does not set the daemon's socket: the daemon binds the run --socket flag (default /run/ezyshield/ezyshield.sock), so a custom value must match run --socket or the dashboard points at a socket the daemon never creates.
rules_dirstring/etc/ezyshield/rules.dDrop-in rule customizations: every *.yaml here merges over the built-in rules by name and survives updates (see the rules guide)
rules_pathstringDeprecated. Replaces the built-in rules entirely (no merge; rules.d ignored) — freezes the install out of upstream rule tuning
log.levelstringinfodebug | info | warn | error
collectorslist[]Log sources to tail (see below). An empty list is valid — config validate warns and the daemon simply tails nothing.
enforceobjectEnforcement backends (optional — without it, decisions are log-only)
notifyobjectNotification channels (optional)
aiobjectAI provider for ambiguous traffic (optional)
enrichobjectGeoIP/ASN enrichment (optional)
dashboardobjectDashboard bind address and auth DB (optional)

The daemon ignores data_dir and socket_path; the dashboard command consumes them (and data_dir is additionally required by config validate). The daemon (ezyshield run) takes its database path and control socket from its own --db and --socket flags (defaults /var/lib/ezyshield/ezyshield.db and /run/ezyshield/ezyshield.sock) and does not read these two keys. Setting them in config.yaml moves the dashboard's auth DB and its connection target, not the daemon's files — so keep socket_path in step with run --socket. See the CLI reference for the run and dashboard flags.

collectors

Each entry tails one log source. kind selects the source; one extra field is required per kind.

yaml
collectors:
  - kind: journald
    unit: ssh                    # systemd unit to follow

  - kind: file
    path: /var/log/nginx/access.log

  - kind: docker
    container: wordpress-nginx   # name, short ID, or full ID
    parser: nginx                # optional parser override
FieldRequiredDescription
kindyesfile | journald | docker
pathfor filefile to tail
unitfor journaldsystemd unit to follow
containerfor dockercontainer name, short ID, or full ID
parsernoforce a parser: nginx | ssh | apache | apache-error | traefik | caddy (default: routed automatically from the source). Honored only for file and docker collectorsjournald ignores it and always routes its parser from the unit.

SSH collector (unit name varies by distro)

The SSH systemd unit name depends on the distro: it's ssh on Debian/Ubuntu and sshd on RHEL/CentOS/Fedora/Rocky/Alma, Arch, and SUSE. Use whatever name systemctl status <unit> resolves on your host — an alias that journalctl -u doesn't recognize collects zero events.

yaml
collectors:
  - kind: journald
    unit: ssh    # Debian/Ubuntu; use "sshd" on RHEL/CentOS/Arch/SUSE

To read SSH from a file instead of journald, point at your distro's auth log — /var/log/auth.log (Debian/Ubuntu) or /var/log/secure (RHEL family). Both timestamp formats are accepted: the legacy syslog format (Jan 1 12:00:00) and modern ISO-8601 (2026-07-13T22:57:35+00:00).

Configure only one SSH collector per host — journald or the file it feeds, never both. Reading both ingests every event twice, which double-counts toward detection thresholds. (An already-banned IP is never banned again, so this never causes duplicate bans, only earlier detection.)

enforce

yaml
enforce:
  nftables: {}                   # local enforcement on; defaults are fine

  cloudflare:
    api_token: env:CF_API_TOKEN  # secrets are env: references, never inline
    account_id: "abc123..."      # required in the default "lists" mode
    # mode: lists                # "lists" (default) or "rulesets"
    # list_name: ezyshield_blocked
    # zone_ids: [ ... ]          # required only when mode: rulesets
    # action: block              # default

nftables

FieldDefaultDescription
tableinet ezyshieldnftables table (all EzyShield rules live inside it). <name> or inet <name>; the inet family is the only one supported (dual-stack v4+v6 layout). Names: letters, digits, underscore
setblockedset holding banned IPv4 addresses; the IPv6 twin is derived automatically as <set>6 (default blocked6). allowed/allowed6 are reserved for the allowlist sets
socket/run/ezyshield-enforcer/enforcer.sockprivileged enforcer helper socket

Both are optional and genuinely honored: the daemon passes them to the privileged enforcer, which re-validates them independently before any rule is written. Two operational notes for custom names:

  • The enforcer must support them (same version as the daemon). Against an older ezyshield-enforcer, the daemon refuses to enforce with a clear error instead of silently using the defaults.
  • The enforcer applies one name set per run. After changing table/set, restart both services (sudo systemctl restart ezyshield-enforcer ezyshield); a previous table left behind by a rename can be removed with nft delete table inet <old-name>.

cloudflare

FieldRequiredDescription
api_tokenyesenv:VARNAME reference to a scoped API token
modenolists (default — account-level IP List + WAF rules) or rulesets (per-zone rules)
account_idwhen mode: listsCloudflare account ID
list_namenoIP list name (default ezyshield_blocked)
zone_idswhen mode: rulesetszones to attach rules to
actionnoblock (default), challenge, or js_challenge
namenolabel shown in status/test output
debouncenohow long rapid ban/unban mutations are coalesced before one batched API push (Go duration, default 15s)
expire_flush_intervalnocadence for batched item removals in lists mode (Go duration, default 3m) — expired bans and unbans accumulate and go out in one API call per interval

Multiple Cloudflare accounts are supported: cloudflare also accepts a list of these objects. See the Cloudflare guide.

Tuning the two cadences trades edge-propagation speed for fewer API calls. The defaults keep a busy server comfortably inside Cloudflare's Lists API throttle; raise them if ezyshield status still reports throttling (ratelimited in the enforcement detail), lower debounce if a new ban must reach the edge faster. Removals are deliberately the slow path: an expired IP staying blocked at the edge for up to expire_flush_interval is fail-closed and harmless, while a delayed ban is real exposure — which is why bans ride debounce and only removals wait for the flush interval. Manual ezyshield unban also propagates to the edge on the flush cadence (the local nftables unban is immediate).

notify

yaml
notify:
  rate_limit_per_minute: 5       # default — cap on notifications per minute
  dedup_window_sec: 600          # default — identical alerts collapsed

  telegram:
    bot_token: env:EZYSHIELD_TELEGRAM_TOKEN
    chat_ids: ["123456789"]
    severity: [warn, critical]   # optional filter: info | warn | critical

  email:
    host: smtp.example.com
    port: 587
    username: alerts@example.com
    password: env:EZYSHIELD_SMTP_PASSWORD
    tls: starttls                # starttls (default) | tls | none
    from: alerts@example.com
    to: [admin@example.com]

  slack:
    webhook_url: env:EZYSHIELD_SLACK_WEBHOOK
    channel: "#security"         # optional override

  discord:
    webhook_url: env:EZYSHIELD_DISCORD_WEBHOOK

  webhook:
    url: env:EZYSHIELD_WEBHOOK_URL
    headers:
      Authorization: env:EZYSHIELD_WEBHOOK_TOKEN   # value must be a full env: reference

Shared fields: rate_limit_per_minute (default 5) and dedup_window_sec (default 600) protect against notification storms. Every channel accepts an optional severity list (info | warn | critical).

Secret-typed fields (bot_token, password, webhook_url, webhook url) only accept env:VARNAME references — inline values are rejected at load time. They are also required for their channel: a telegram block without bot_token, an email block without password, or a slack/discord/webhook block without its URL fails validation (the daemon resolves them at startup). Webhook header values are sent verbatim unless the entire value is an env: reference, which is resolved.

Email tls: starttls fails closed: if the SMTP server does not advertise STARTTLS (or a capability-stripping proxy hides it), the send errors instead of silently downgrading to plaintext. Set tls: none explicitly if you really intend to send unencrypted.

ai

Optional — with no ai block, the deterministic rule engine handles everything.

yaml
# Single provider
ai:
  provider: anthropic            # anthropic | openai | ollama
  model: claude-haiku-4-5-20251001
  api_key: env:ANTHROPIC_API_KEY
  ambiguous_band: [30, 69]       # scores in this band consult the AI (keep high < ban_threshold)
  token_budget_daily: 50000      # hard daily cap; rule engine takes over beyond it
  cache_ttl: 15m                 # identical-verdict cache (default 15m)
yaml
# Or multi-provider failover
ai:
  providers:
    - name: anthropic
      priority: 1
      model: claude-haiku-4-5-20251001
      api_key: env:ANTHROPIC_API_KEY
    - name: ollama
      priority: 2
      model: llama3
      endpoint: http://localhost:11434
FieldDescription
provideranthropic | openai | ollama (single-provider form)
modelmodel name
api_keyenv:VARNAME reference (never inline)
endpointbase URL for the ollama provider only (default http://localhost:11434). The anthropic and openai providers ignore it and always call their official APIs (https://api.anthropic.com, https://api.openai.com) — there is no OpenAI-compatible-endpoint override. Same in the single-provider and providers failover forms.
ambiguous_band[low, high] — only scores inside the band consult the AI. Omitted (or [0, 0]) defaults to [30, 69]; any other band with low >= high or values outside 0–100 is rejected at load. Keep high below the policy ban_threshold: a score at or above the threshold has already decided a ban on rules alone, so the daemon never consults the AI for it — a band reaching into the threshold only triggers a startup/validate warning
token_budget_dailydaily token cap; when exhausted, decisions fall back to rules
cache_ttlverdict cache duration; omitted or 0 means the default 15m (the cache cannot be disabled — it is the second brake on repeated consults for the same behavior). Entries are keyed by behavior signature (event kind counts + window), not by IP, so identical attack patterns from different IPs reuse one verdict; on a hit the cached verdict is re-targeted to the IP being evaluated. Allowlist-clamped verdicts are never cached
providersmulti-provider failover list (name, priority, model, api_key, endpoint, token_budget_daily); takes precedence over the single-provider fields

The AI verdict is always advisory: schema-validated, clamped by policy, and never able to ban an allowlisted IP.

enrich

GeoIP/ASN enrichment — enables block_countries / block_asns in policy and the country/ASN columns in list and report. Optional: without an enrich: section the daemon runs normally with empty enrichment (no country/ASN anywhere, and those policy keys never match).

FieldDescription
db_pathpath to GeoLite2-Country.mmdb
asn_pathpath to GeoLite2-ASN.mmdb
auto_updatelet the daemon download and refresh the databases (weekly)
license_keyenv:VARNAME reference to a MaxMind license key — required when auto_update: true, inline values are rejected

The easiest path is the wizard, which walks through all of the below:

bash
sudo ezyshield config enrich maxmind
sudo systemctl restart ezyshield

Where the databases come from. EzyShield uses MaxMind's free GeoLite2 databases, which require a (free) MaxMind account: sign up, then generate a license key under Manage License Keys. With auto_update: true the daemon downloads both databases itself on startup when the files are missing and refreshes them weekly — you never handle the files:

yaml
enrich:
  db_path: /var/lib/ezyshield/GeoLite2-Country.mmdb
  asn_path: /var/lib/ezyshield/GeoLite2-ASN.mmdb
  auto_update: true
  license_key: env:MAXMIND_LICENSE_KEY

The key is a secret like any other: put MAXMIND_LICENSE_KEY=... in /etc/ezyshield/.env (mode 0600 — the wizard does this for you) and reference it as env:MAXMIND_LICENSE_KEY. It is only ever used in the download URL and never logged.

Manual alternative. With auto_update: false no account key is needed at runtime: download GeoLite2-Country.mmdb and GeoLite2-ASN.mmdb from your MaxMind account (or mirror them from a host that has them) and place them at the configured paths. Missing or unreadable files are not an error — the daemon logs a warning and runs with empty enrichment until they appear.

dashboard

FieldDefaultDescription
addr127.0.0.1:9090Bind address — loopback only; non-loopback binds are refused at startup
auth_db_path<data_dir>/dashboard.dbDashboard auth database

Minimal example

yaml
data_dir: /var/lib/ezyshield

collectors:
  - kind: journald
    unit: ssh

enforce:
  nftables: {}

Secrets

Every secret field takes an env:VARNAME reference and is resolved by the daemon (ezyshield run) from its environment. The wizards write secret values to /etc/ezyshield/.env (mode 0600), which the systemd unit loads via EnvironmentFile=. Secrets never appear in config.yaml, logs, or error messages.

This is also enforced in reverse: if a value pasted into a non-secret field (provider, model, endpoint, ...) looks like a credential — a known key prefix such as sk-, or a long high-entropy token — the config is rejected at load with an error that names the field but never prints the value. Webhook header values are the one exemption (raw values are legal there and are redacted in config show).

Validation

bash
sudo ezyshield config validate   # strict schema + constraints, exact line numbers on errors
sudo ezyshield doctor            # environment check (files, permissions, sockets)
sudo ezyshield test enforcer all # exercise enforcement backends for real
sudo ezyshield test notifier all # send a test notification to every channel