Ban lists: where Guard and fail2ban ban addresses
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>
This commit is contained in:
1 parent
6f56eefae2
commit
500bfc7bdd
44 files changed
+2541
-39
No files matched your search
+32
-5
@@ -108,15 +108,15 @@ Lists replace the old ones. `probation_seconds` 0 is the default (120).
|
||||
|
||||
### Objects
|
||||
|
||||
Zones, sets, services, rules, port forwards, source NAT and redirects
|
||||
share one pattern, `/{kind}` with kind `zones`, `sets`, `services`,
|
||||
`rules`, `forwards`, `snats` or `redirects`:
|
||||
Zones, sets, services, rules, port forwards, source NAT, redirects and
|
||||
ban lists share one pattern, `/{kind}` with kind `zones`, `sets`,
|
||||
`services`, `rules`, `forwards`, `snats`, `redirects` or `banlists`:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `GET /{kind}` | all of them, in order; `GET /rules?chain=input` for one chain; `GET /services` includes the built-in ones |
|
||||
| `POST /{kind}[?before=ID\|after=ID\|first=1]` | create; rules, forwards and NAT rules go last unless given a `position`, or placed before or after another of their chain, or first, in the same change (one history entry; staged in a changeset, the place goes with it) |
|
||||
| `GET /{kind}/{ref}` | by ID, or name for zones, sets, services and forwards |
|
||||
| `GET /{kind}/{ref}` | by ID, or name for zones, sets, services, forwards and ban lists |
|
||||
| `PATCH /{kind}/{ref}` | change the fields given (a JSON merge: `null` clears) |
|
||||
| `DELETE /{kind}/{ref}` | refused while something uses it |
|
||||
| `POST /rules/{ref}/reset-quota` | start a rule's quota again now (`config:write`; audited, not staged). Optional body: `{"address": "192.168.1.23"}` starts only that address's again (a per-address quota; an IPv6 address counts for its /64; 404 if it hasn't used the quota); `{"give": 5368709120}` takes that many bytes off what was used instead (the address's, or every address's), until the next reset |
|
||||
@@ -145,7 +145,18 @@ The objects:
|
||||
|
||||
A source NAT rule is a match (as a rule's, without the rule's other
|
||||
fields) with `"to"` (empty: masquerade), a redirect a match with
|
||||
`"to_port"`. Times are seconds (`timeout`).
|
||||
`"to_port"`. A ban list ([bans.md](bans.md)):
|
||||
|
||||
```json
|
||||
{"name": "web", "scope": "hosts", "hosts": ["10.0.10.57"], "never": ["trusted"],
|
||||
"default_time": 3600, "max_time": 604800, "max_entries": 10000, "group_v6": 64, "comment": "", "disabled": false}
|
||||
```
|
||||
|
||||
`scope` is `firewall`, `hosts` or `everything`; 0 is the default for the
|
||||
numbers, and `max_time` -1 is no limit. A ban list can't be renamed.
|
||||
Someone with a `bans` grant but no `config:read` may read the ban lists
|
||||
the grant covers (`GET /banlists`, `GET /banlists/{ref}`). Times are
|
||||
seconds (`timeout`).
|
||||
|
||||
### Statistics
|
||||
|
||||
@@ -196,6 +207,22 @@ These need `sets:read` or `sets:write`, for that set
|
||||
([permissions.md](permissions.md)); they aren't on probation or in the
|
||||
history, but are audited.
|
||||
|
||||
### Bans
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `GET /bans` | the active bans of every ban list you may read: `id`, `list_id`, `list`, `address` (an address, or an IPv6 prefix), `protocol` and `port` (none: every port), `expires` and `timeout` (seconds left; none: no end), `created`, `who`, `source`, `reason` |
|
||||
| `GET /banlists/{ref}/bans` | one list's |
|
||||
| `POST /banlists/{ref}/bans` | ban: `{"address": "203.0.113.9", "protocol": "tcp", "ports": "ssh", "time": 3600, "source": "guard/ssh", "reason": "…"}`. `ports` is a number, a string (`"22"`, `"http,https"`, `"0:65535"` or `"all"` for every port) or a list; without a protocol a number is TCP and UDP, a service its own protocols. `time` 0 is the list's default, -1 its longest; longer is cut down. 201 with `{"bans": [ban…, "existing": true for a renewed one], "closed": N}` (connections closed) |
|
||||
| `DELETE /banlists/{ref}/bans/{id}` | lift one ban |
|
||||
| `DELETE /banlists/{ref}/bans?address=A` | lift all of an address's bans (404 if it has none) |
|
||||
|
||||
These need `bans:read` or `bans:write` for the list
|
||||
([permissions.md](permissions.md)); bans aren't on probation or in the
|
||||
history, but are audited (`ban.add`, `ban.remove`). Refusals: `422
|
||||
never_ban` (inside, the firewall's own, the caller's own, or in the
|
||||
list's `never`), `422 list_full`, `409` for a disabled list.
|
||||
|
||||
### Probation
|
||||
|
||||
| | |
|
||||
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
# Ban lists
|
||||
|
||||
A **ban list** is where a program bans addresses: Guard (Housecarl's
|
||||
companion, which reads logs), fail2ban with Housecarl's action, or you
|
||||
with `hc ban`. The program says only *which address*, and perhaps a
|
||||
protocol and ports. The ban list says what that address is kept from.
|
||||
So a web server that sees password guessing in its own logs can ban the
|
||||
guesser at the router without knowing anything about the router.
|
||||
|
||||
## Ban lists
|
||||
|
||||
```sh
|
||||
hc banlist add router --scope firewall # the firewall itself
|
||||
hc banlist add web --scope hosts --host 10.0.10.57 --host 2001:db8:10::57
|
||||
hc banlist add edge --scope everything --never trusted
|
||||
```
|
||||
|
||||
| Scope | Banned addresses are kept from | Where the drops are |
|
||||
|---|---|---|
|
||||
| `firewall` | the firewall itself: its SSH, DNS, web UI | `input` |
|
||||
| `hosts` | the list's hosts (addresses or sets), behind the firewall | `forward`, to those addresses |
|
||||
| `everything` | the firewall and everything behind it | `input` and `forward` |
|
||||
|
||||
A `hosts` list covers port forwards to its hosts as well: the forward
|
||||
chain sees a forwarded connection's translated address and port, the
|
||||
host's own. A web server's Guard bans `tcp/22`, and that is right even
|
||||
when the router forwards its port 2222 to it.
|
||||
|
||||
Other settings, with their defaults:
|
||||
|
||||
| Setting | Default | |
|
||||
|---|---|---|
|
||||
| `never` | none | addresses, prefixes or sets this list never bans |
|
||||
| `default_time` (`--time`) | 1h | a ban's length when the program asks for none |
|
||||
| `max_time` (`--max-time`) | 7d | the longest ban; longer ones are cut to it. `-1` (`none`): no limit, so bans may be permanent |
|
||||
| `max_entries` | 10000 | the most bans the list holds; more are refused (`list_full`) |
|
||||
| `group_v6` | 64 | the IPv6 prefix length banned for an address. One customer usually has a whole /64 and can move around in it; 128 bans the one address. IPv4 addresses are banned alone |
|
||||
| `disabled` | no | the list's bans aren't in force (they are kept) |
|
||||
|
||||
A ban list can't be renamed: the tokens and programs that ban into it
|
||||
know it by its name. Make a new one instead.
|
||||
|
||||
## Bans
|
||||
|
||||
```sh
|
||||
hc ban edge 203.0.113.9 --time 1d --reason "port scan"
|
||||
hc ban web 203.0.113.9 --tcp ssh --tcp 8443
|
||||
hc bans # every list's
|
||||
hc bans web
|
||||
hc unban web 203.0.113.9 # all of the address's bans in web
|
||||
hc unban web 17 # one, by ID
|
||||
```
|
||||
|
||||
A ban is of **every port**, or of **a protocol's ports** (numbers or
|
||||
service names, built-in or yours). A port without a protocol is banned
|
||||
for TCP and UDP; a service name takes the service's protocols. Banning an
|
||||
address again renews the ban: its new time counts from now.
|
||||
|
||||
Bans are dropped **ahead of every rule** in their chains, and ahead of
|
||||
the built-in that lets established connections through, so a ban cuts a
|
||||
connection that is already open. Housecarl also closes the banned
|
||||
address's tracked connections the ban covers when it bans it, because
|
||||
connections offloaded to the flowtable no longer pass the rules at all.
|
||||
Replies to connections the inside opened to a banned address are
|
||||
dropped too.
|
||||
|
||||
Bans are kept in the database, apart from the configuration (like a
|
||||
dynamic set's entries): they aren't in the history, can't be undone,
|
||||
aren't on probation, and don't go through changesets. They end by
|
||||
themselves; every change, restart and reboot puts the active ones back
|
||||
with the time they have left. Each ban and unban is in the audit log
|
||||
(`ban.add`, `ban.remove`), with the program's source and reason.
|
||||
|
||||
### What is never banned
|
||||
|
||||
Whatever a list's `never` says, Housecarl refuses (`422 never_ban`) to
|
||||
ban:
|
||||
|
||||
* the inside: addresses in networks of inside zones' interfaces, and
|
||||
private addresses outside the external zones' networks;
|
||||
* the firewall's own addresses, and its own prefixes (settings);
|
||||
* the address of whoever asks (a program can't lock itself out);
|
||||
* loopback, link-local, multicast and unspecified addresses.
|
||||
|
||||
A ban grouped to a prefix (IPv6) is refused when the prefix overlaps any
|
||||
of these.
|
||||
|
||||
## Programs and tokens
|
||||
|
||||
Give each program a token for its list alone. A `bans` grant can name
|
||||
ban lists, as a `sets` grant names sets:
|
||||
|
||||
```sh
|
||||
hc user add guard-web --no-password --grant bans:write:web
|
||||
hc token create web --user guard-web # as root on the firewall
|
||||
```
|
||||
|
||||
With it, the program may ban into `web` and lift its bans, list them,
|
||||
and read the list itself (`GET /banlists/web`: its grouping and times),
|
||||
and nothing else. `viewer` has `bans:read`, `operator` `bans:write`.
|
||||
|
||||
## Guard
|
||||
|
||||
Guard reads log files and the systemd journal on any machine, and bans
|
||||
the addresses that misbehave into a ban list (its own documentation).
|
||||
Run it on the web server with a token for `web`, and on the router with
|
||||
one for `router` (or the local socket).
|
||||
|
||||
## fail2ban
|
||||
|
||||
Housecarl ships an action for fail2ban,
|
||||
`/usr/share/doc/housecarl/contrib/fail2ban/action.d/housecarl.conf`
|
||||
(`contrib/` in the source). Copy it to `/etc/fail2ban/action.d/`, put
|
||||
the token in `/etc/fail2ban/housecarl.token` (mode 0600), and use it in a
|
||||
jail:
|
||||
|
||||
```ini
|
||||
[sshd]
|
||||
enabled = true
|
||||
action = housecarl[list=web, url="https://router.lan:8070"]
|
||||
```
|
||||
|
||||
The jail's `port` and `protocol` go with each ban: `port = ssh` bans only
|
||||
`tcp/22`, fail2ban's default `0:65535` bans every port. A `bantime` of
|
||||
`-1` is the list's longest ban. When fail2ban unbans an address Housecarl
|
||||
already let go (its own time ran out first), nothing happens. Add
|
||||
`curlopts = --cacert /etc/fail2ban/housecarl-ca.pem` for a private CA.
|
||||
|
||||
## Shadow mode
|
||||
|
||||
In shadow mode the ban rules count what they would drop
|
||||
(`[would drop]`) and let it through, like every other rule, and banning
|
||||
closes no connections.
|
||||
|
||||
## The ruleset
|
||||
|
||||
Each list has four sets, `ban_<id>_v4`, `ban_<id>_v6` (every port) and
|
||||
`ban_<id>_v4_svc`, `ban_<id>_v6_svc` (address, protocol and port), all
|
||||
with timeouts:
|
||||
|
||||
```
|
||||
ip saddr @ban_3_v4 counter drop comment "hc:ban3"
|
||||
ip saddr . meta l4proto . th dport @ban_3_v4_svc counter drop comment "hc:ban3"
|
||||
ip6 saddr and ffff:ffff:ffff:ffff:: @ban_3_v6 counter drop comment "hc:ban3"
|
||||
```
|
||||
|
||||
Set names starting with `ban_` are therefore Housecarl's own
|
||||
(`set_name_reserved`).
|
||||
+13
@@ -204,6 +204,19 @@ accept|drop|reject|log`, `--reject-with tcp-reset`, `--log`,
|
||||
|
||||
See [nat.md](nat.md).
|
||||
|
||||
### banlist, ban
|
||||
|
||||
| Command | |
|
||||
|---|---|
|
||||
| `banlist list`, `banlist show BANLIST` | ban lists, with their bans' count |
|
||||
| `banlist add NAME --scope firewall\|hosts\|everything [--host ADDR\|SET]... [--never ADDR\|SET]... [--time 1h] [--max-time 7d\|none] [--max-entries N] [--group-v6 64] [--disabled] [--comment C]` | |
|
||||
| `banlist update BANLIST [flags]`, `banlist delete BANLIST` | `--host` and `--never` replace the lists; no renaming |
|
||||
| `bans [BANLIST]` | the active bans |
|
||||
| `ban BANLIST ADDRESS [--tcp PORT\|SERVICE]... [--udp …]... [--time 1h\|none] [--reason R]` | ban every port, or some; again renews |
|
||||
| `unban BANLIST ADDRESS\|ID` | lift one ban, or all of an address's |
|
||||
|
||||
See [bans.md](bans.md).
|
||||
|
||||
### settings
|
||||
|
||||
| Command | |
|
||||
|
||||
@@ -309,6 +309,22 @@ down, the fallback and a missing ruleset. Reach it from where the input
|
||||
rules let the monitor in (a rule for the Checkmk server's address and
|
||||
port 8070).
|
||||
|
||||
## Failed logins in the journal
|
||||
|
||||
A failed login to the web UI or the API, and a refused API token, are
|
||||
written to the journal as well as the audit log, with the address first:
|
||||
|
||||
```
|
||||
level=WARN msg="login failed" ip=203.0.113.9 user=admin reason="wrong username or password"
|
||||
level=WARN msg="token refused" ip=203.0.113.9 reason="wrong token secret"
|
||||
```
|
||||
|
||||
Guard's `housecarl` preset, or a fail2ban filter, can ban such addresses
|
||||
([bans.md](bans.md)). The address comes before the user name, which a
|
||||
caller chooses, so a pattern anchored on `msg="login failed" ip=` can't
|
||||
be fooled by a name. (With `log.format: json` they are JSON objects with
|
||||
the same fields, in the same order.)
|
||||
|
||||
## Checks
|
||||
|
||||
`housecarld check` validates the configuration in the database against
|
||||
|
||||
+9
-4
@@ -9,6 +9,7 @@ else gets **grants**, `resource:access`:
|
||||
|---|---|---|
|
||||
| `config` | settings, zones, sets, services, rules, NAT, the ruleset, history, interfaces, probation, changesets (the web UI shows the forms locked) | change them, stage and apply changesets (your own, or anyone's for administrators), undo, import, confirm and roll back; close connections; start and stop traces |
|
||||
| `sets` | dynamic sets' entries | add and remove entries |
|
||||
| `bans` | ban lists' bans, and the lists the grant names | ban and unban ([bans.md](bans.md)) |
|
||||
| `log` | hit counters, the log, connections, traces | – |
|
||||
| `audit` | the audit log | – |
|
||||
| `*` | all of the above | |
|
||||
@@ -20,6 +21,10 @@ program, such as Steward's captive portal, which should be able to let a
|
||||
client in and nothing more. A dynamic set's entries need a `sets` grant
|
||||
even for those with `config:write`.
|
||||
|
||||
A `bans` grant names ban lists the same way: `bans:write:web` may ban
|
||||
into the list `web` (and read it) and nothing else, the grant for Guard
|
||||
or fail2ban on the web server.
|
||||
|
||||
Only administrators may delete other tables (`hc takeover`), and manage
|
||||
users and the certificate (`hc cert`, Server → Certificate).
|
||||
|
||||
@@ -27,8 +32,8 @@ users and the certificate (`hc cert`, Server → Certificate).
|
||||
|
||||
| Role | Grants |
|
||||
|---|---|
|
||||
| `viewer` | `config:read sets:read log:read` |
|
||||
| `operator` | `config:write sets:write log:read audit:read` |
|
||||
| `viewer` | `config:read sets:read bans:read log:read` |
|
||||
| `operator` | `config:write sets:write bans:write log:read audit:read` |
|
||||
| `log` | `log:read`, e.g. for a monitoring script |
|
||||
|
||||
```sh
|
||||
@@ -82,6 +87,6 @@ on probation like any other.
|
||||
|
||||
Logins (and failed ones), users, tokens, every configuration change (with
|
||||
its history entry), changesets created, applied and discarded,
|
||||
confirmations and rollbacks, imports, takeovers and
|
||||
dynamic set entries are recorded with who, from where and how: `session`,
|
||||
confirmations and rollbacks, imports, takeovers,
|
||||
dynamic set entries and bans are recorded with who, from where and how: `session`,
|
||||
`token` or `socket`.
|
||||
@@ -268,6 +268,13 @@ In shadow mode ([shadow.md](shadow.md)) a raw rule's final `drop` or
|
||||
(NAT, `meta mark set`, `jump`, `notrack`, a verdict map…) is left out,
|
||||
with a warning (`shadow_raw_left_out`).
|
||||
|
||||
## Ban lists
|
||||
|
||||
Programs such as Guard and fail2ban ban addresses into ban lists, and
|
||||
those bans come first of all in the input and forward chains (after
|
||||
loopback in input), before the built-ins and every rule. See
|
||||
[bans.md](bans.md).
|
||||
|
||||
## Built-ins
|
||||
|
||||
Housecarl adds the rules every firewall needs before yours. Each can be
|
||||
|
||||
@@ -116,6 +116,9 @@ firewall's NAT still does the work.
|
||||
* **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
|
||||
|
||||
+15
-1
@@ -51,7 +51,7 @@ are unique per kind, ignoring case.
|
||||
|
||||
| Code | | When |
|
||||
|---|---|---|
|
||||
| `set_name_reserved` | error | a set name starting with `meter_`, `conn_`, `quota_` or `stats_`: Housecarl's own sets (rate limits, quotas, statistics) use those |
|
||||
| `set_name_reserved` | error | a set name starting with `meter_`, `conn_`, `quota_`, `stats_` or `ban_`: Housecarl's own sets (rate limits, quotas, statistics, ban lists) use those |
|
||||
| `set_entry` | error | an entry that isn't an address, prefix (without host bits) or range |
|
||||
| `set_empty` | warning | a set (not dynamic) with no entries: rules using it match nothing |
|
||||
| `set_dynamic_entries` | error | configured entries on a dynamic set: its entries come at run time |
|
||||
@@ -138,3 +138,17 @@ configuration: the dashboard and `hc status` show them.
|
||||
|
||||
In shadow mode `nftables_service` and `takeover_pending` aren't raised:
|
||||
another firewall beside Housecarl is the point there.
|
||||
|
||||
## Ban lists
|
||||
|
||||
| Code | | When |
|
||||
|---|---|---|
|
||||
| `banlist_name` | error | not a valid name (as for zones and sets) |
|
||||
| `banlist_name_taken` | error | two ban lists with one name |
|
||||
| `banlist_scope` | error | a scope other than `firewall`, `hosts` or `everything` |
|
||||
| `banlist_hosts` | error | scope `hosts` without hosts, or hosts with another scope |
|
||||
| `banlist_address` | error | a host or never entry that isn't an address, prefix or range |
|
||||
| `unknown_set` | error | a host or never entry naming a set that doesn't exist |
|
||||
| `banlist_time` | error | negative times (but `max_time` -1), or a default longer than the longest |
|
||||
| `banlist_entries` | error | `max_entries` below 0 or above 1000000 |
|
||||
| `banlist_group` | error | `group_v6` outside 16 to 128 |
|
||||
Reference in new issue
Block a user