Skip to content
Witen

Host protection

Warden operator guide

Warden reads your SSH and web-server logs and bans attackers in the Linux firewall. It works on its own with no account. Watch first, then block. Connect it to Witen for shared blocklists and reports.

Quickstart

  1. Download the build for your host and check its checksum. Install it without changing existing configuration or service state.
  2. Keep the service stopped. Run sudo warden discover, pick readable logs, and set backend = "null" for observation mode.
  3. Add your real management address or CIDR to never_block. Run sudo warden validate --config /etc/witen/warden.toml, start the service, and check sudo warden sources and sudo warden events.
  4. Keep independent console access. Once client addresses and detections look right, choose the firewall backend and restart the service.
  5. Confirm sudo warden blocked --source local matches Warden's firewall table. Test with a separate staging address and remove it with sudo warden unban IP.

Everything above works without a Witen account. Connecting is optional.

Choose a build

Native packages

DEB for Debian or Ubuntu; RPM for AlmaLinux, Rocky Linux, RHEL, or Fedora; APK for Alpine. Packages install the service account, configuration, init definition, manual, state directories, and lifecycle hooks.

Portable builds

The glibc archive for Gentoo and other glibc systems; the musl archive for musl systems other than Alpine. Both support systemd and OpenRC. The 386 glibc archive is for legacy 32-bit x86 only.

Downloads are public. Pick the artifact for your distribution, architecture, and libc, and check its SHA-256 checksum before you install. Never put a glibc binary into an Alpine package.

Public Linux builds and SHA-256 checksums
BuildPlatformSHA-256
Debian package

witen-warden_0.1.42-1_amd64.deb

Debian or Ubuntu · amd6486ea23328e103d5be8e3528f052115fa96ea230de8cac88f1e6d68bbaa9b14da
Portable glibc archive

witen-warden-0.1.42-linux-amd64-glibc.tar.gz

Linux · amd64 · glibc 2.31+88c4778f01eb956d6274c4a55c66d658b1b42d9626f94e2b0040c891dc378403
Portable musl archive

witen-warden-0.1.42-linux-amd64-musl.tar.gz

Linux · amd64 · musl2a861deb1ae1bb26e6ba70d01794998c86b1cafac9f82a5f74a7a0c3003b32fc
Legacy portable archive

witen-warden-0.1.42-linux-386-glibc.tar.gz

Linux · 386 · glibc 2.31+4117275a141f843e68a0f28ffab3f4b2acf1726b460453472df054478162c16c
Alpine package

witen-warden_0.1.42-r1_x86_64.apk

Alpine Linux · x86_64aaf182723e99f4b05c3c7da6f4f4c92ec4933f2c9b85abc3cd7699f73196593b
RPM package

witen-warden-0.1.42-1.x86_64.rpm

AlmaLinux, Rocky, RHEL, or Fedora · x86_64ca03b178cf0aa3392785192e40b073bd84a505319913f39f946d487e3a8bbc3f

Install Warden

Debian package

sha256sum ./witen-warden_*.deb
sudo apt install ./witen-warden_*.deb
warden --version
man 8 warden

RPM package

sha256sum ./witen-warden-*.x86_64.rpm
sudo dnf install ./witen-warden-*.x86_64.rpm
warden --version
man 8 warden

Alpine package

sha256sum ./witen-warden_*.apk
sudo apk add --allow-untrusted ./witen-warden_*.apk
warden --version

The APK is distributed directly, not through an Alpine repository, so install it from the checked local file.

Portable archive

sha256sum ./witen-warden-*-linux-amd64-glibc.tar.gz
tar -xzf ./witen-warden-*-linux-amd64-glibc.tar.gz
cd ./witen-warden-*-linux-amd64-glibc
sudo ./install.sh
sudo warden validate --config /etc/witen/warden.toml

The portable installer doesn't enable or start a service. On OpenRC it installs the service files; on other hosts, install the included init definition that fits.

Verify a download

Every Warden release is signed. For each version we publish a signed release statement that lists all six files, the DEB, RPM, and APK packages and the three portable archives, with each file's size and SHA-256. A release key on our build host signs the statement. An offline root key, which never touches a server, certifies that release key. Warden has the root's public key built in, so it can check any download without trusting the server it came from.

With Warden installed

Download the statement and its signature next to the file, then let the installed Warden check it before you install. It refuses a file the statement doesn't list, a statement that was changed, and a key that was revoked.

V=0.1.42
B=https://www.witenlabs.com/api/releases/warden
curl -fsSLO "$B/signatures/witen-warden-$V.release.json"
curl -fsSLO "$B/signatures/witen-warden-$V.release.sig"
warden verify-release ./witen-warden_$V-1_amd64.deb
sudo apt install ./witen-warden_$V-1_amd64.deb

The portable install.sh does this check itself before it changes anything: keep the archive, the .release.json, and the .release.sig in one directory and extract the archive there. If a file is missing or doesn't verify, nothing is installed. --skip-signature-check installs anyway and warns loudly; use it only after you've checked the download another way. Package hooks can't see the package file they come from, so check DEB, RPM, and APK files with warden verify-release first.

On a first install

A host without Warden has nothing it already trusts. On a first install, your trust comes from HTTPS to www.witenlabs.com and from the root fingerprint below, which is also printed in man 8 warden. The installer checks the archive with the release's own binary and prints the root fingerprint it used, so compare it. To check without any Warden binary, verify the chain with OpenSSL 3 and jq:

ROOT_PUB=...   # the root public key below
pem() { printf -- '-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA%s\n-----END PUBLIC KEY-----\n' "$1"; }
printf %s "$ROOT_PUB" | base64 -d | sha256sum        # must equal the root fingerprint
# The root signed the key set.
jq -r .keys witen-warden-$V.release.sig | base64 -d > keys.json
jq -r .keys_signature witen-warden-$V.release.sig | base64 -d > keys.sig
{ printf 'witen-release-signature/v1\n'; cat keys.json; } > keys.msg
pem "$ROOT_PUB" > root.pem
openssl pkeyutl -verify -pubin -inkey root.pem -rawin -in keys.msg -sigfile keys.sig
# A release key in that set, not revoked, signed the statement.
KEY_ID=$(jq -r .key_id witen-warden-$V.release.json)
pem "$(jq -r --arg id "$KEY_ID" '.keys[] | select(.key_id == $id and .revoked_at == 0) | .public_key' keys.json)" > release.pem
jq -r .signature witen-warden-$V.release.sig | base64 -d > statement.sig
{ printf 'witen-release-signature/v1\n'; cat witen-warden-$V.release.json; } > statement.msg
openssl pkeyutl -verify -pubin -inkey release.pem -rawin -in statement.msg -sigfile statement.sig
# The statement lists your download.
jq -r '.artifacts[] | "\(.sha256)  \(.name)"' witen-warden-$V.release.json
sha256sum ./witen-warden-$V-linux-amd64-glibc.tar.gz

Also check that the key's not_before and not_after in keys.json cover the statement's signed_at.

Transparency log

The build host's signer writes every signature it makes to an append-only log before the statement leaves the host, and we publish that log at /api/releases/warden/log. Each line is a signed entry, chained to the one before it. warden verify-release FILE --log log.txt also requires the statement to appear there. The log only grows. If a release key were ever misused through the signer, every statement it signed would show up here, and revoking the key stops Warden from trusting any of them.

Start in observation mode

Keep the service stopped while you edit /etc/witen/warden.toml, and set sources and observation mode before you start it. Leave the enrollment token and sensor credential unset and Warden uploads nothing to Witen; it still downloads reference data and checks for updates.

Find supported logs

sudo warden discover
sudo warden discover --toml

Discovery only reads. It lists non-empty supported log files and journal services; it doesn't add them or change the host.

Watch SSH

[ssh]
enabled = true
timezone = "auto"

[journal]
enabled = "true"
cursor_path = "/var/lib/witen/journal-cursors.json"

[jail]
backend = "null"
max_ssh_failures = 3
max_http_probes = 20
window_duration_sec = 300
ban_duration_sec = 3600
firewall_reconcile_interval_sec = 300
# Replace with your actual management address or CIDR before enforcement.
never_block = ["192.0.2.10"]

On OpenRC or with file-based logging, set ssh.log_file to the authentication log and grant the service account the narrowest group or ACL that can read it.

Web access logs

[[logs]]
path = "/var/log/caddy/access.log"
type = "caddy"

[[logs]]
path = "/var/log/nginx/access.log"
type = "nginx"

Supported access logs: nginx, Apache, Caddy, lighttpd, Traefik, Envoy, HAProxy, Varnish, and Pound. Have Caddy write its native JSON with client IP fields that respect trusted proxies.

Validate and start

Confirm the null backend and your log sources, and fix any validation failure before you start.

sudo warden validate --config /etc/witen/warden.toml
sudo systemctl enable --now witen-warden
sudo systemctl status witen-warden
sudo journalctl -u witen-warden -f
sudo warden sources
sudo warden stats --resources

Validation checks the parsed configuration, paths, log readability, the selected firewall tools, daemon sockets, XDP or JA4 prerequisites, and live sources. It installs no rules and changes no state. On OpenRC, use rc-update add witen-warden default and rc-service witen-warden start.

  • The service is active and reports the version you installed.
  • Every source you need is healthy and advancing as logs arrive.
  • The backend is null during the trial, so detections are observations, not firewall blocks.
  • Review warden events --limit 100 --json and source health. A quiet log may have no security events yet.
  • warden blocked and warden stats reach the running daemon through the admin socket.

Turn on firewall blocking

  1. Review the observed events and thresholds, with an independent console or recovery session open.
  2. Replace the documentation examples in never_block with your real management addresses and networks.
  3. When you're ready to block, set backend = "nft" under [jail], validate, and restart the service in a maintenance window.
  4. Check Warden's rules and local blocks. If a block is wrong, remove it with warden unban IP and find the cause.
  • nft is the recommended Linux backend. iptables, experimental PF, XDP, and observation-only null are also available.
  • With nft, Warden keeps its rules in its own table, inet witen. It never changes your other tables, chains, or INPUT policy.
  • Upgrading from 0.1.19 or earlier moves Warden's sets out of the shared inet filter table automatically. Your own rules stay exactly as they are.
  • With UFW on iptables-nft, Warden's rules don't appear in iptables -L. Inspect them with nft list table inet witen.
  • Temporary bans survive restarts, expire locally, and can renew while blocked traffic continues.
  • Addresses and CIDRs in never_block override automatic, manual, blocklist, and immediate policy bans.
sudo nft list table inet witen
sudo nft list set inet witen witen
sudo warden blocked --source local
sudo warden stats

Kernel mitigations

From 0.1.39, Warden protects the host from a kernel flaw that has no fixed package yet, when a firewall rule can block the traffic the flaw needs. It decides from the kernel that is running, applies the rule while that kernel is affected, and removes it once a fixed kernel runs. The first is CVE-2026-98432: Warden drops IPv4 TCP packets that carry IP options, which legitimate TCP doesn't use, before connection tracking or the TCP stack sees them.

From 0.1.40, which kernels still need a mitigation comes from a signed feed that Witen updates from the Ubuntu, Red Hat, and kernel trackers. Warden fetches it every six hours, verifies its signature, and keeps the last good copy; without it, the data built into Warden applies. The key that signs the feed is certified by the same offline root that signs Warden releases and is never on the server that publishes it, so a compromised server can delay the feed but cannot mark a kernel fixed. A copy that expires (90 days after signing) can keep a mitigation on but never takes one off. Only a fix the vendor has released counts. The feed can only pick among firewall rules compiled into Warden, so nothing in it can block other traffic.

sudo warden mitigations
sudo warden mitigations --json
  • Each mitigation reports enforcing, observing, not_needed, off, unavailable, held, or unsupported, with the reason, the backend, and the packets, bytes, and distinct senders it caught, then which data it was judged with: the signed feed, its saved copy, or the built-in data. The same appears in warden tui (key 0) and in warden validate.
  • Each mitigation has a risk class. Only low applies automatically; one above max_risk reports held and names the setting that applies it. A feed entry that needs a rule this Warden doesn't have reports unsupported until you upgrade.
  • In observation mode (backend = "null") Warden changes no firewall rules, so mitigations report unavailable and say how to turn them on: turn on firewall blocking, or add the rule by hand from the advisory.
  • The rules live apart from Warden's bans and from ufw or firewalld: the table ip witen_mitigations with nftables and XDP, and the raw-table chain WITEN-MITIGATIONS with iptables. They stay in place while Warden restarts and are removed when the package is.
  • Senders are counted, never banned. The packets are dropped before any handshake, so their source addresses are unverified and could be forged.
  • With iptables-legacy the rule needs the iptable_raw, xt_u32, xt_recent, and xt_comment modules. warden config mitigation-modules prints a modules-load file for them.
[mitigations]
# "enforce" drops, "observe" counts without dropping, "off" disables
mode = "enforce"
# "auto" applies only while the running kernel is affected; "always" applies regardless
apply = "auto"
# highest risk class applied automatically: "low", "medium", or "high" (0.1.40)
max_risk = "low"
# fetch the signed feed from collector_url; false uses the built-in data (0.1.40)
feed = true

Daily operations

TaskCommandWhat it shows
Overviewwarden tuiInteractive candidates, bans, events, sources, trends, and alerts.
Who's close to a banwarden candidatesAddresses nearing a threshold. They are not blocked yet.
Why one addresswarden candidate IPPolicy, score, threshold, and the evidence behind them.
What's blockedwarden blocked --source allLocal bans plus entries downloaded from Witen.
What changedwarden actionsRecorded lifecycle, blocking, and coalescence actions.
Tune safelywarden recommend --window 7dSmall, safe suggestions. Warden never applies them itself.
sudo warden ban 192.0.2.10 3600
sudo warden unban 192.0.2.10
sudo warden reload --config /etc/witen/warden.toml
sudo warden events --limit 100 --json

The terminal interface

Run sudo warden tui for a live view of the daemon. Press 0 to 9 to switch views (9 is Advisories from 0.1.37, 0 is Mitigations from 0.1.39), / to filter by address, and Enter on a row for its details. b bans the selected address after you confirm a duration, u lifts a ban, and q quits.

Animation moving through the Warden TUI Overview, Candidates, Bans, Events, Trends, and Downloaded views
Eight views, one keypress each.
Warden TUI Overview listing active candidates, enforced bans, the nftables backend and jail verdicts, active window sizes, event counts and scores, and blocked packets and bytes
Overview: the firewall backend, jail windows, scores, and blocked traffic at a glance.
Warden TUI candidate details for an IPv6 address: its five most recent contributing events with category, jail, score, and policy, and the 3 SSH or 12 HTTP points it still needs before a ban
Candidate details: the evidence behind a score, and how far the address is from a ban.
Warden TUI Sources view showing the nginx access log and the SSH auth log, each with its read offset, last read, last match, and a healthy status
Sources: each log Warden follows, how far it has read, and whether it is healthy.
Warden TUI Downloaded view listing inbound and outbound blocklist entries, single addresses and CIDR ranges, received from a connected feed
Downloaded: inbound and outbound entries from a connected blocklist, kept apart from local bans.
Animation of filtering the Warden TUI Bans view to one address, opening its ban details, and switching between trend metrics
Filter to one address, open its ban, and step through its history.

Screens show a demonstration host with synthetic attack traffic from documentation address ranges. warden tui --json prints the same snapshot for scripts.

Upgrade and roll back

  1. Download the build for the host's architecture and libc, and check its signature.
  2. Record warden --version, warden stats, and the service state.
  3. Back up the database. Schema 30 changes event uniqueness, so rolling back the binary also means restoring the database that matches it.
  4. Install the package or run the new portable installer. Configuration and state are kept.
  5. Run warden validate before you restart.
  6. Restart, then confirm the new version, source positions, active bans, reporting to Witen, and each plugin's background connection to Warden.
warden --version
sudo warden validate --config /etc/witen/warden.toml
sudo systemctl restart witen-warden
warden --version
sudo warden sources
sudo warden blocked --source all

Portable installs restart the service

From 0.1.37, the portable install.sh checks the release signature, then stops a running Warden service, replaces the binary, starts the service again, and checks that exactly one Warden runs the new binary. Don't restart it again afterwards. A stopped service stays stopped. On OpenRC it installs its own init script first, which waits up to 50 seconds for the old daemon to finish its last upload. If anything fails after the stop, it starts the service again. --no-restart replaces only the files, and the running daemon keeps the old binary until you restart it.

# Keep the .tar.gz, .release.json and .release.sig together
tar xzf witen-warden-0.1.37-linux-amd64-glibc.tar.gz
cd witen-warden-0.1.37-linux-amd64-glibc
sudo ./install.sh

Update notices

Warden 0.1.37 and later tell you when a newer release is out. You'll see update available: 0.1.38 in the TUI header, an update check: line in warden stats with the installed version, the latest version, and the last check time, and a warning in warden validate. Each new version is logged once. Warden never downloads or installs anything on its own. Upgrade with the steps above.

Shortly after it starts, and then about once a day, Warden sends one plain HTTPS request to /api/releases/warden/latest, the signed statement of the latest release. It reports a new version only after the signatures check out against the root built into Warden, the statement hasn't expired, and it isn't older than one it has already seen. The request carries only a User-Agent with the installed version, an Accept header, and a random request ID. It sends no sensor credential, hostname, or configuration. Like any web request, it shows us the connecting address. An answer that doesn't verify shows as update check failed: signature in warden stats and warden validate. A failed check never affects blocking or reporting. Behind a proxy, set HTTPS_PROXY in the service environment.

# /etc/witen/warden.toml: turn the check off, for example on offline hosts
[update_check]
enabled = false

Troubleshooting

The service won't start

  • Run warden validate --json and fix the first failing check.
  • Check the configured logs exist and the service account can read them.
  • Check the selected firewall binary and kernel support are present.
  • Don't start a foreground daemon while the service holds the runtime lock or admin socket.

Why was this address blocked?

  • Run warden decisions to list recorded local blocking decisions.
  • Run warden explain RECEIPT_ID for the subject, reason, evidence count, actuator, timing, and lifecycle state.
  • Add --json for output you can attach to automation or a support request.
  • Undo a block with warden unban IP; the receipt then records rolled_back.

Outbound protection

Outbound blocking is off unless you set outbound_protection_enabled, and needs a firewall backend with direction-specific rules. The feed is active only when Warden status shows both outbound_configured=true and outbound_enforced=true.

Events are missing

  • Run warden sources to check the selected backend, offset, last event, and collection error.
  • Apache and nginx error logs listed under [[logs]] are read from their files even when journald collection is on (Warden 0.1.31 and later). Earlier versions skipped them on journald hosts.
  • Error, mail, FTP, and database logs carry no timezone. With timezone = "auto", Warden 0.1.32 and later read them in the host's zone; earlier versions read them as UTC, which can make events look hours old.
  • Check the log format and client IP fields, especially behind trusted proxies.
  • From 0.1.37, warden stats lists a source Warden can't read as log sources: N of M UNHEALTHY, with the reason (permission_denied, not_found, or read_error), when it started, and how to fix it. Warden also reports each source's health to Witen with its uploads.
  • On systemd hosts, 0.1.37 repairs the service account's access to Apache logs by itself: witen-warden-log-access.path runs when the log directory's permissions change and witen-warden-log-access.timer every 15 minutes. OpenRC hosts repair it when the service starts.
  • From 0.1.38, a log that file permissions allow but Warden's AppArmor profile or SELinux policy refuses is listed as confinement_denied, with the rule or file context that fixes it; the ACL repair can't help there. warden stats also shows a confinement: line, and warns when Warden runs outside its packaged profile or SELinux domain. On systemd hosts witen-warden-confinement.path and .timer load the profile, reinstall the SELinux module, and restore labels by themselves, and leave a profile you disabled with aa-disable or a module you disabled with semodule -d alone.
  • Run warden backfill --since 1h to import history without blocking. Add --enforce only after you've reviewed what it would block.

Witen is unreachable

Local detection and blocking carry on. Events wait in the on-disk outbox, which has a size limit, and send in order once the connection returns. Check the upload counters in warden stats, DNS and TLS reachability, and the sensor credential file. Don't replace the credential just because the collector is briefly unreachable.

More detail

The package includes the full offline manual, man 8 warden and info warden, covering XDP, JA4, backfill, policy rules, sockets, and the TUI.

Connect to Witen (optional)

Connecting adds shared blocklists and account reports, and sends security events to Witen; read the privacy policy and collection settings first. Observation mode doesn't stop reporting on a connected host.

  1. In your Witen dashboard, open Add Protected Asset and choose Server or VPS.
  2. Give the asset a stable, unique name. Never use one hostname for two installations enrolled at the same time.
  3. Generate the short-lived, single-use setup token.
  4. Put the values in /etc/witen/warden.toml, readable only by root and the Warden service group.
  5. Start Warden once. It trades the token for a credential of its own and stores it in the configured credential file.
  6. Remove the used token from the configuration and check the asset reports under Protected Assets.
[server]
server_name = "edge-1"

[warden]
enrollment_token = "wte-single-use-token"
credential_file = "/var/lib/witen/sensor-credential"

Warden reports to https://collector.witenlabs.com by default. Set collector_url only to use a different collector.

Register local plugins

Warden checks each plugin's own credential and Unix user, separate from the hosted sensor credential. Use current plugin releases and create credentials before you restart Warden.

Plugins save each report, then send it in the background. A socket timeout or rejected credential leaves reports queued for retry; plugins never fall back to the collector API. Once Warden confirms it stored a report, Warden owns it. Queues and diagnostic storage have size limits; the plugin dashboard shows delivery errors, rejected records, and dropped counts.

sudo install -d -m 0700 -o apache -g apache /var/lib/witen-wordpress
sudo warden --config /etc/witen/warden.toml callers add \
  --id EXISTING_SITE_ID --type wordpress --uid "$(id -u apache)" \
  --token-file /var/lib/witen-wordpress/caller.token
sudo warden --config /etc/witen/warden.toml callers list

Use the installation's existing reporting ID, not a display label. New packages create the witen-plugins socket group. When you upgrade an existing configuration, set ingest_socket_group to witen-plugins and add each PHP user to that group. Keep PHP users out of the Warden service group, and restart PHP workers after changing groups.

Keep credentials outside web roots, mode 0600, owned by the PHP user, and keep the database and caller registry private. Point WordPress at the credential with WITEN_WARDEN_CREDENTIAL_FILE, or Matomo with its Warden credential file setting. Web and cron workers need the same configuration. None of these settings turn on event sharing.

Replace a credential with callers rotate and a new --token-file path; attribution is kept. Cut access immediately with callers revoke --id ID. Sites that don't trust each other need separate Unix users: a shared UID can read its applications' credentials. Registered plugins feed the host's shared firewall policy. The collector still treats the parent Warden as the account boundary and receives each installation ID with its events.

Detection rules and WAF findings

Two optional features add detection beyond Warden's built-in rules. Both are off by default, and neither bans anyone on its own: local rules observe unless you set them to score, and WAF findings never score or ban. Witen receives them as sensor-reported observations with no shared confidence, so they never raise an address's reputation for other customers.

Local rules

A local rule matches the method, path, query, status, User-Agent, or, with request metadata turned on, the media type and declared length of a request. Paths are checked as sent and after decoding, so encoded variants still match. List rule files under [signatures], test them offline, then run warden reload; no restart is needed.

[signatures]
files = ["/etc/witen/signatures/site.toml"]
schema_version = 1

[[rule]]
id = "local.old-admin-probe"
revision = 1
description = "Requests for a removed admin tool"
mode = "observe"   # or "score" to count toward a local ban

[[rule.all]]
field = "path"
op = "prefix"      # equals, prefix, contains, or regex
value = "/old-admin/"
warden signatures validate --rules /etc/witen/signatures/site.toml
warden signatures test --rules /etc/witen/signatures/site.toml --input fixtures.jsonl
sudo warden signatures replay --rules /etc/witen/signatures/site.toml   --format apache --input /var/log/httpd/access_log
sudo warden reload --config /etc/witen/warden.toml

Replay reads a log you already have and reports what would have matched, without touching the daemon, the database, or the firewall.

PHP2Shell examples

Warden 0.1.29 and later ship two opt-in examples for the PHP2Shell flaws (CVE-2026-109618 and CVE-2026-109619) in /usr/share/doc/witen-warden/examples/php2shell/, or examples/php2shell/ in the portable archive. Updating PHP is the fix; see the PHP2Shell guide. These only report.

  • php2shell-tooling.toml is a local rule for User-Agents that advertise php2shell tooling. It needs no WAF but misses tools that send a browser User-Agent.
  • php2shell-modsecurity.conf holds ModSecurity rules 9420000 to 9420003, derived from the PHP fix. They inspect request bodies, pass and log only, and call a match a probe or check, since public checkers send the same requests. Rule 9420000 also matches ordinary upload-progress forms.

ModSecurity findings

If the host already runs ModSecurity, Warden can report which rules fired, as http_waf_observation events with the rule ID. It never copies request bodies, headers, matched data, or the raw log line. Three inputs are supported, and enabling any of them needs a restart.

  • libModSecurity v3 (nginx connector) or ModSecurity 2.9 (Apache mod_security2) writing a JSON audit log with SecAuditLogFormat JSON. Parts AHZ are enough and keep headers out of the log.
  • ModSecurity 2.9 builds without JSON support, including EPEL's for RHEL and AlmaLinux 10: set apache_error_log = true and Warden reads findings from your apache-error logs.

With a JSON audit log:

[modsecurity]
enabled = true
log_files = ["/var/log/modsecurity/audit.json"]

With ModSecurity 2.9 and no JSON, reading the Apache error logs instead:

[modsecurity]
enabled = true
apache_error_log = true

[[logs]]
type = "apache-error"
path = "/var/log/httpd/error_log"

Findings from a DetectionOnly engine still reach warden events and Witen, but they don't mark a CVE Seen on your sensors in Vulnerability Intel. That takes a ModSecurity rule written for that specific CVE, such as 9420000 for CVE-2026-109619, firing while the engine enforces. Coraza findings never mark a CVE observed. What counts as observed exploitation has the full rule.

Search and AI crawlers

Behavior detection flags an address that keeps changing which bot it claims to be. From 0.1.36, Google's own crawler tokens, such as Googlebot, Googlebot-Image, and AdsBot-Google, count as one identity when they come from Google's published ranges, so genuine Google crawling is no longer identity churn. The same tokens from any other address still count separately.

When a connected sensor reports a request from Google, Bing, Apple, DuckDuckGo, GPTBot, or OAI-SearchBot, Witen checks the claim against the operator's published ranges or reverse and forward DNS. A verified crawler request with no specific hostile signal is filed as verified crawler traffic and carries no reputation weight. A sensitive path, an exploit or web-shell shape, or a campaign match is still judged on its own. A claim the operator's sources contradict stays suspicious, and so does one that can't be verified.

Vulnerability checks for installed packages

From 0.1.34, Warden can report the host's installed packages so Vulnerability Intel can show which ones your distribution has fixed newer versions of. It is off by default. Each report is the complete list, up to 20,000 packages, with each package's name, version, source package, and architecture, plus the ID, ID_LIKE, VERSION_ID, and VERSION_CODENAME fields of /etc/os-release. Nothing else about the host is sent. Turning it off and restarting removes the list from Witen.

[inventory]
enabled = true
interval_sec = 86400

Warden reads /var/lib/dpkg/status on Debian and Ubuntu, /lib/apk/db/installed on Alpine, and /var/db/pkg on Gentoo, and runs rpm -qa on the RHEL family and SUSE.

When the first report arrives

The first report goes out after a random delay of 0 to 10 minutes from the moment Warden starts, then every interval_sec seconds. The default is 86400, warden validate accepts 3600 to 604800, and each interval is jittered by up to 10%. A failed upload is retried after 15 minutes, doubling up to the interval.

See what affects this host

From 0.1.37, Warden shows the result on the host itself. Press 9 in warden tui, or run warden advisories, for every package with a known advisory: installed and fixed versions, the distribution's advisory IDs with links, severity, whether it is in CISA KEV or exploitation has been observed, and its status. Enter lists a package's advisories and CVEs, f filters by status, and r refreshes. Links are printed as text; nothing opens a browser.

Warden asks Witen for this host's result at most once a minute, with its own sensor credential, and keeps the last good answer, so warden advisories --cached works offline and when the daemon is down. Add --json for scripts. With inventory off, both show the stanza above that turns it on.

sudo warden advisories
sudo warden advisories --cached --json

Gentoo slots

From 0.1.36, Warden reports every installed slot of a package as its own item, with a slot field read from Portage's SLOT file without the sub-slot. With Python 3.13 and 3.14 installed, dev-lang/python arrives twice, with slot set to 3.13 and 3.14, and each is judged against its own GLSA ranges. Earlier versions reported only the first slot they found. Entries with an unreadable or invalid SLOT are skipped and counted.

From 0.1.35 the same switch lets Witen Blocker on this server share its plugin and theme list through Warden when the site connects only through Warden's socket. Warden labels each site separately and refuses those reports while the switch is off; the plugin's own sharing setting must be on as well.

Restart Warden after changing it; a reload doesn't restart the reporter. warden stats shows the package manager, item and skipped counts, and the last and next report times. See Vulnerability intelligence for how results are judged.

Package managers

See Automation and packaging for public download endpoints and Gentoo setup.

Agent setup

See Automation and packaging for the guarded installation prompt.