A program bans an address (every port, or a protocol's ports) for a time; the ban list says what it is kept from: the firewall, some hosts behind it, or everything. Bans drop ahead of every rule, close the address's connections, group IPv6 by /64, are kept in the database across restarts and audited. Never banned: the inside, the firewall's own addresses, the caller, the list's never. bans grant (bans:write:LIST), hc banlist/bans/ban/unban, Traffic -> Bans, a fail2ban action in contrib/, and failed logins and refused tokens logged for Guard and fail2ban. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
175 lines
8.5 KiB
Markdown
175 lines
8.5 KiB
Markdown
# Shadow mode
|
|
|
|
In shadow mode Housecarl runs beside the firewall you have now and
|
|
changes nothing. Its ruleset is loaded, so its counters, log, tracer and
|
|
statistics show what it *would* do with your real traffic, but it blocks,
|
|
rejects and translates nothing. Your current firewall keeps deciding.
|
|
|
|
Use it to:
|
|
|
|
* **try Housecarl** on the real machine before you trust it with the
|
|
firewall: the web UI, the API, `hc`, changesets, history;
|
|
* **check an import**: import your ruleset ([import.md](import.md)) and
|
|
watch whether Housecarl would drop anything your firewall lets through;
|
|
* **develop against the API**: Steward, scripts or fail2ban can fill
|
|
dynamic sets and see them used, with nothing at stake.
|
|
|
|
## Turning it on
|
|
|
|
Shadow mode is set in `/etc/housecarl/housecarl.yaml`
|
|
([configuration.md](configuration.md)), not through the API, so no user
|
|
or token can switch enforcement off:
|
|
|
|
```yaml
|
|
shadow:
|
|
enabled: true
|
|
position: after # or before; see below
|
|
```
|
|
|
|
housecarld reads the file when it starts:
|
|
|
|
```sh
|
|
sudo systemctl restart housecarl
|
|
sudo hc status # Mode: SHADOW …
|
|
```
|
|
|
|
The web UI shows a banner on every page, `hc status` a `Mode:` line, and
|
|
`/healthz` `"mode": "shadow"`.
|
|
|
|
## Why it can't let anything through
|
|
|
|
In nftables, `accept` only ends the chain it is in. The packet then
|
|
goes on to the next base chain on the same hook, and other tables' chains
|
|
are among those. Only a drop is final. Housecarl's table in shadow mode
|
|
never drops, so it can't let through anything your firewall drops, and it
|
|
can't block anything your firewall accepts.
|
|
|
|
## What changes in the ruleset
|
|
|
|
The same configuration is rendered, with everything that acts on packets
|
|
turned into counting:
|
|
|
|
| | Enforcing | Shadow mode |
|
|
|---|---|---|
|
|
| Rules that drop or reject | `drop`, `reject` | counted, then `accept` (Housecarl's chain ends as it would have). Their comment says `[would drop]` or `[would reject]` |
|
|
| The built-ins that drop (invalid packets, antispoofing) | `drop` | counted, `[would drop]` |
|
|
| Policies | as configured | `accept`. What a drop policy would drop is counted and **always logged**, rate-limited, in all three chains |
|
|
| Port forwards, source NAT, redirects | translate | counted only (`[would dnat]`, `[would masquerade]`, `[would snat]`, `[would redirect]`) |
|
|
| MSS clamping | changes SYN packets | left out |
|
|
| The flowtable | offloads connections | left out: offloaded connections skip *every* table's forward chain, your firewall's too |
|
|
| Raw rules | as written | as written if they only match, count, log or accept. A final `drop` or `reject` becomes `accept`. Anything else that acts on packets (NAT, `set`, `jump`, `notrack`, a verdict map…) is left out, with a warning (`shadow_raw_left_out`) |
|
|
| Everything else | | the same: accepts, rate limits, connection limits, quotas, dynamic sets, per-device counters, logging |
|
|
|
|
`hc ruleset` prints the shadow ruleset, the one loaded now.
|
|
`hc ruleset --enforcing` prints the ruleset that will be loaded once
|
|
shadow mode is off.
|
|
|
|
**Every change is checked by nftables twice**: the shadow ruleset, and the
|
|
enforcing one. A change that nftables would refuse once shadow mode is off
|
|
is refused now, so leaving shadow mode can't fail on that.
|
|
|
|
## Where Housecarl's chains run
|
|
|
|
Your firewall's chains are usually at priority `filter` (0), its raw
|
|
chains at `raw`. `shadow.position` puts Housecarl's filter chains just
|
|
next to them:
|
|
|
|
* **`after`** (the default) runs them at `filter + 10` (and `raw + 10`).
|
|
Housecarl only sees what your firewall let through. So **every would-drop
|
|
is traffic that would break if you switched over**: the list to work
|
|
through before you do. What your firewall drops and Housecarl would
|
|
accept (a hole) doesn't show; compare the rules for that.
|
|
* **`before`** runs them at `filter - 10` (and `raw - 10`). Housecarl sees
|
|
every packet, as it would when enforcing, and its hits and drops are the
|
|
numbers you would get. Its would-drops include everything your firewall
|
|
drops too (the internet's background noise).
|
|
|
|
If your firewall uses other priorities, say `filter + 20`, "after" is
|
|
relative to `filter`, not to it: check with `nft list ruleset`.
|
|
|
|
NAT chains are different: the kernel asks NAT chains in turn only until
|
|
one translates. Housecarl's NAT chains always run first (`dstnat - 10`,
|
|
`srcnat - 10`), whatever the position, or they would never see a
|
|
connection your firewall translates. They translate nothing, so your
|
|
firewall's NAT still does the work.
|
|
|
|
## What you see
|
|
|
|
* **Hits** (`hc counters`, the Rules page): a drop rule's hits are what it
|
|
would have dropped. NAT counters count connections (only a connection's
|
|
first packet reaches the NAT chains).
|
|
* **The log** (`hc log`, the Log page): every packet a drop policy would
|
|
have dropped, and what rules with logging would have dropped, marked
|
|
*would drop*. `hc log` adds "(shadow mode: would drop, let through)".
|
|
* **The tracer** (`hc trace`, the Trace page): the verdict is your
|
|
firewall's, the real one. When Housecarl would have dropped the packet,
|
|
the trace says by which rule ("shadow mode: Housecarl would have
|
|
dropped it, by …").
|
|
* **Statistics** ([stats.md](stats.md)): drops, drops per chain and the
|
|
blocked sources are what Housecarl would have dropped.
|
|
|
|
## What works differently
|
|
|
|
* **No probation.** A change can't cut anyone off, so changes from the
|
|
network are applied and saved at once, with no confirmation. Changesets
|
|
work as usual; their ruleset diff is of the shadow ruleset.
|
|
* **No takeover, no closing connections.** `hc takeover` (deleting your
|
|
firewall's tables) and `hc connections close` are refused (409,
|
|
`hc` exits with 5): both would change your traffic.
|
|
* **Bans** ([bans.md](bans.md)) are kept and their rules count what they
|
|
would drop (`[would drop]`), but nothing is dropped, and banning closes
|
|
no connections.
|
|
* **The boot.** `housecarl-ruleset.service` loads the shadow ruleset at
|
|
boot. Its fallback ([operations.md](operations.md#if-the-ruleset-fails-to-load-at-boot))
|
|
is an empty table that filters nothing, since your firewall protects the
|
|
machine.
|
|
* **Your firewall reloading.** A ruleset file such as `/etc/nftables.conf`
|
|
usually starts with `flush ruleset`, and Debian's `nftables.service`
|
|
runs `nft flush ruleset` when it stops or restarts. Both delete
|
|
Housecarl's table too. housecarld watches nftables' notifications and
|
|
puts its table back within a few seconds (the journal says "Housecarl's
|
|
table was deleted … put it back"). Dynamic sets lose their entries, and
|
|
quotas go back to what was last saved (every five minutes).
|
|
* **Warnings.** `nftables_service` and `takeover_pending` aren't raised:
|
|
another firewall beside Housecarl is the point. Instead:
|
|
`shadow_unprotected` when no other table has a filter chain on the input
|
|
hook (nothing protects the machine), and `shadow_raw_left_out` for each
|
|
raw rule left out ([validation.md](validation.md#the-machine)).
|
|
* **Quotas count** what passes in shadow mode, and keep counting when
|
|
you switch over.
|
|
|
|
## A trial, step by step
|
|
|
|
From a machine whose firewall is `/etc/nftables.conf` with `table inet main`:
|
|
|
|
1. Install Housecarl and create the administrator
|
|
([install.md](install.md)).
|
|
2. Turn shadow mode on in `/etc/housecarl/housecarl.yaml`, and start
|
|
housecarld: `sudo systemctl enable --now housecarl`.
|
|
3. Import your ruleset: `sudo hc import --defines /etc/nftables.conf`
|
|
([import.md](import.md)). Leave `nftables.service` as it is.
|
|
4. Use your network for a while, and watch `hc log -f`, `hc counters` and
|
|
the dashboard. With `position: after`, each would-drop is something
|
|
your firewall allows and Housecarl wouldn't: fix the rule in Housecarl,
|
|
or decide it doesn't matter. `hc status` lists the raw rules left out.
|
|
5. When nothing you need would be dropped, leave shadow mode.
|
|
|
|
## Leaving shadow mode
|
|
|
|
1. Look at what will be loaded: `sudo hc ruleset --enforcing`.
|
|
2. Set `shadow.enabled: false` (or remove the section), and
|
|
`sudo systemctl restart housecarl`. Housecarl's ruleset is now in
|
|
force **alongside** your firewall: a packet must be accepted by both.
|
|
The restart applies it at once, not on probation, but your old firewall
|
|
is still there and still decides too.
|
|
3. Take over: `sudo hc takeover "inet main"` deletes the old table (on
|
|
probation over the network), and `sudo systemctl disable nftables`
|
|
(without `--now`) retires it ([import.md](import.md#3-take-over)).
|
|
|
|
## Going back
|
|
|
|
Set `shadow.enabled: true` and restart housecarld. If Housecarl was your
|
|
only firewall, **the machine is now unprotected**: load your other
|
|
firewall first. Until one filters the input hook, `hc status` and
|
|
`/healthz` warn (`shadow_unprotected`).
|