# 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](https://www.witenlabs.com/docs/warden#install) 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](https://www.witenlabs.com/docs/warden#firewall) 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](https://www.witenlabs.com/docs/warden#enroll) 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.

| Build | Platform | SHA-256 |
| --- | --- | --- |
| [Debian package](https://www.witenlabs.com/api/releases/warden/artifacts/witen-warden_0.1.42-1_amd64.deb)witen-warden\_0.1.42-1\_amd64.deb | Debian or Ubuntu · amd64 | 86ea23328e103d5be8e3528f052115fa96ea230de8cac88f1e6d68bbaa9b14da |
| [Portable glibc archive](https://www.witenlabs.com/api/releases/warden/artifacts/witen-warden-0.1.42-linux-amd64-glibc.tar.gz)witen-warden-0.1.42-linux-amd64-glibc.tar.gz | Linux · amd64 · glibc 2.31+ | 88c4778f01eb956d6274c4a55c66d658b1b42d9626f94e2b0040c891dc378403 |
| [Portable musl archive](https://www.witenlabs.com/api/releases/warden/artifacts/witen-warden-0.1.42-linux-amd64-musl.tar.gz)witen-warden-0.1.42-linux-amd64-musl.tar.gz | Linux · amd64 · musl | 2a861deb1ae1bb26e6ba70d01794998c86b1cafac9f82a5f74a7a0c3003b32fc |
| [Legacy portable archive](https://www.witenlabs.com/api/releases/warden/artifacts/witen-warden-0.1.42-linux-386-glibc.tar.gz)witen-warden-0.1.42-linux-386-glibc.tar.gz | Linux · 386 · glibc 2.31+ | 4117275a141f843e68a0f28ffab3f4b2acf1726b460453472df054478162c16c |
| [Alpine package](https://www.witenlabs.com/api/releases/warden/artifacts/witen-warden_0.1.42-r1_x86_64.apk)witen-warden\_0.1.42-r1\_x86\_64.apk | Alpine Linux · x86\_64 | aaf182723e99f4b05c3c7da6f4f4c92ec4933f2c9b85abc3cd7699f73196593b |
| [RPM package](https://www.witenlabs.com/api/releases/warden/artifacts/witen-warden-0.1.42-1.x86_64.rpm)witen-warden-0.1.42-1.x86\_64.rpm | AlmaLinux, Rocky, RHEL, or Fedora · x86\_64 | ca03b178cf0aa3392785192e40b073bd84a505319913f39f946d487e3a8bbc3f |

Runtime requirements

Warden uses SQLite through CGO, needs readable SSH or web-server logs, and needs nftables or iptables to block on Linux. The optional JA4 sniffer also needs raw-packet capability.

## Install Warden

Check service state before you install

DEB, RPM, and APK hooks don't enable a new service, but an upgrade can restart an already-enabled systemd service or an already-running OpenRC service. Before you install, check the service state, existing configuration, logs, and firewall. To try an existing installation without blocking, stop it in a maintenance window and set `backend = "null"` before you install or restart. Keep independent console access and a backup of the configuration.

### 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.

Where things live

Configuration: `/etc/witen/warden.toml`. State: `/var/lib/witen`. Admin socket: `/run/witen/warden.sock`. Ingest socket, for events only: `/run/witen/warden-ingest.sock`.

## 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`.

Release root fingerprint

SHA-256 of the raw Ed25519 root public key: `d3a04bd9492126e2c50e98672c2af65e5ccc2bbdc94f3de31a920b25e8dd8129`

Compare it with the fingerprint `install.sh` prints and with the one in `man warden`.

### 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](https://www.witenlabs.com/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.

Start conservatively

Use the `null` backend, or a real backend with conservative thresholds. Put management networks and trusted monitoring addresses in `never_block` before you block.

## 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.

Connected installations only

For a connected host, also check the latest report in your Witen account and the upload counters in `warden stats`. A local trial needs neither.

## 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.

Recovery access

Changing the backend needs a restart. Keep a known-good configuration backup and independent access to restore it if you lose your network session. Never test with your administration address. Switching back to observation mode does not remove a block that is already applied.

- `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
```

firewalld compatibility warning

On AlmaLinux, Rocky Linux, RHEL, or Fedora, validation warns when firewalld runs beside Warden's nft backend. Blocking still works; the warning means a live `firewall-cmd --reload`, followed by Warden restoring its rules, hasn't been tested on that host. Run the read-only `firewall-cmd --check-config` during setup. Test a live reload only in an approved maintenance window with independent console access, then confirm Warden's nftables objects and an expected test ban come back.

## 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](https://www.witenlabs.com/blog/linux-kernel-cve-2026-98432-tcp-ip-options): 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](https://www.witenlabs.com/docs/warden#firewall), 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
```

A mitigation is not the fix

Install the fixed kernel when your distribution releases it, reboot, and check `uname -r`. Once the feed records the release, Warden reports `not_needed` and removes the rule by itself.

## Daily operations

| Task | Command | What it shows |
| --- | --- | --- |
| Overview | [`warden tui`](https://www.witenlabs.com/docs/warden#tui) | Interactive candidates, bans, events, sources, trends, and alerts. |
| Who's close to a ban | `warden candidates` | Addresses nearing a threshold. They are not blocked yet. |
| Why one address | `warden candidate IP` | Policy, score, threshold, and the evidence behind them. |
| What's blocked | `warden blocked --source all` | Local bans plus entries downloaded from Witen. |
| What changed | `warden actions` | Recorded lifecycle, blocking, and coalescence actions. |
| Tune safely | `warden recommend --window 7d` | Small, 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](https://www.witenlabs.com/docs/warden#advisories) from 0.1.37, 0 is [Mitigations](https://www.witenlabs.com/docs/warden#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](https://www.witenlabs.com/_next/image?url=%2Fimages%2Fwarden%2Ftour-poster.webp&w=3840&q=75)

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](https://www.witenlabs.com/_next/image?url=%2Fimages%2Fwarden%2Foverview.webp&w=3840&q=75)

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](https://www.witenlabs.com/_next/image?url=%2Fimages%2Fwarden%2Fcandidate-detail.webp&w=3840&q=75)

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](https://www.witenlabs.com/_next/image?url=%2Fimages%2Fwarden%2Fsources.webp&w=3840&q=75)

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](https://www.witenlabs.com/_next/image?url=%2Fimages%2Fwarden%2Fdownloaded.webp&w=3840&q=75)

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](https://www.witenlabs.com/_next/image?url=%2Fimages%2Fwarden%2Fdrilldown-poster.webp&w=3840&q=75)

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.

Reload or restart

Reload applies jail thresholds, durations, scoring policy, behavior detection, recidivist settings, and `never_block` without a restart. Changes to sources, sockets, storage paths, or the firewall backend need a service restart.

## Upgrade and roll back

1. Download the build for the host's architecture and libc, and [check its signature](https://www.witenlabs.com/docs/warden#release-verification).
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](https://www.witenlabs.com/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
```

Do not replace state during an upgrade

Keep `/var/lib/witen`. It holds log positions, the SQLite event and outbox history, active bans, the enrolled sensor credential, and, on packaged services, SQLite migration temporary files. Deleting it can replay old events, lose queued ones, and force you to enroll again.

## 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](https://www.witenlabs.com/privacy) and collection settings first. Observation mode doesn't stop reporting on a connected host.

1. In your Witen dashboard, open [Add Protected Asset](https://www.witenlabs.com/protected-assets/add) 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.

Which credential goes where

Your fleet credential stands for your subscription and its limits; it never goes on a host. Enrollment tokens are one-time setup secrets. Each Warden gets its own credential. The local admin socket is protected by file permissions, not by any Witen credential.

## 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.

What plugins can't ban

Warden 0.1.21 and later refuse plugin-requested bans of protected addresses: private and reserved ranges, the host's own addresses, its gateway, its DNS resolvers, recent SSH login sources, and built-in CDN and search crawler ranges. Plugin bans last at most 24 hours by default, are rate-limited per caller and globally, are capped in number, and never displace Warden's own bans. Tune these limits under `[jail.plugin_bans]`. The admin and ingest sockets have separate SELinux types, so a web server can write only to the ingest socket.

## 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](https://www.witenlabs.com/blog/php2shell-site-owner-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"
```

Start in DetectionOnly

Run new WAF rules with `SecRuleEngine DetectionOnly` and check them against your own traffic before you let ModSecurity block anything. Body inspection (`SecRequestBodyAccess On`) is a WAF decision with its own performance and privacy review; don't enable it only for Warden. The full setup, including permissions, SELinux, and log rotation, is in `/usr/share/doc/witen-warden/http-evidence.md`.

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](https://www.witenlabs.com/docs/vulnerability-intelligence#telemetry) 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.

An empty status right after a start is expected

Until that first report, `warden stats` in 0.1.36 and earlier shows no package manager, and a host that has never reported shows 0 items. Wait out the delay and check again. From 0.1.37, `warden stats` says when the first report is scheduled, and `warden validate` has an `inventory` check that warns when reporting is off in the daemon, no Collector is configured, or the last report failed.

### 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](https://www.witenlabs.com/docs/vulnerability-intelligence#packages) for how results are judged.

## Package managers

See [Automation and packaging](https://www.witenlabs.com/docs/automation#package-managers) for public download endpoints and Gentoo setup.

## Agent setup

See [Automation and packaging](https://www.witenlabs.com/docs/automation#agent-setup) for the guarded installation prompt.

---
Source: https://www.witenlabs.com/docs/warden
