blacklistd 始めました

# はじめに
ＦｒｅｅＢＳＤで `blacklistd` が導入されて早３年[^備考１]。全く新しく環境構築を行う機会を得たので、この際だから乗り換えてみようと検証してみた。
結論から言うと微妙かな。半日検証してみての結論なので判断が早すぎかも知れないけど。とは言え定石的なものは見えたのでメモがてら残しておく。とりあえず長期的に運用してみて判断かな。

[^備考１]: ２０２０年末現在。２０１７年０７月２６日リリースの１１．１－Ｒが初出。

# 検証環境
- ＯＳ： ＦｒｅｅＢＳＤ（`12.2-R`）
- ファイアウォール： ＩＰＦＷ[^備考２]バックエンド
- サーバーソフトウェア： ＳＳＨｄ（`OpenSSH_7.9p1, OpenSSL 1.1.1h-freebsd  22 Sep 2020`）[^備考３]

[^備考２]: 実際のフィルタリングにはＩＰＦＷ、ＮＰＦ、ＰＦ、ＩＰＦのどれかをバックエンドで指定する。
[^備考３]: 取り急ぎＳＳＨｄのみで。他にも `blacklistd` 対応謳ってるサーバーソフトウェアあるけど、今回は詳細追わず。

# 設定項目
1. ＩＰＦＷの設定
2. `blacklistd` の設定
3. ＳＳＨｄの設定

## ＩＰＦＷの設定
取り急ぎＩＰＦＷを使用「していない」ものとして、ゼロからセットアップする。既にＩＰＦＷを運用中で、そのルールの中に入れたい場合は、本説明を参考に組み込んで欲しい。

### /etc/rc.conf
`/etc/rc.conf` に下記の設定を追加する。

```shell
firewall_enable="YES"
firewall_type="/etc/ipfw.conf"
```

あとはお好みで `firewall_quiet="YES"` を加えてもいい。
また色々と解説しているサイトを見てると `firewall_type="OPEN"` を指定してる例もあるが、それでも十分ではある。今回はそれらの半歩先ゆく設定ということで。

### /boot/loader.conf
`/boot/loader.conf` に下記の設定を追加する。

```
net.inet.ip.fw.default_to_accept="1"
```

本設定後再起動するまでは、下記コマンドを実行しておくこと。

```shell-session
kenv net.inet.ip.fw.default_to_accept="1"
```

この設定はファイアウォールのデフォルトルール（ルール番号６５５３５）を「全てのアクセスを許可する」というものである。本設定はＩＰＦＷ有効後には変更できないので注意。
一応ＩＰＦＷが有効になった直後にアクセスできなくなることを防ぐための設定だが、ポリシー上分かって設定してるなら無視してもよい。

以後の説明ではこの設定があることを前提に話を進める[^備考４]。

[^備考４]: 「全てのアクセスを許可」する前提で `/etc/ipfw.conf` の説明を行うので…。

### /etc/ipfw.conf
```
add    1 check-state :default
add 3000 allow tcp from any to any 22 in via インターフェース名 setup keep-state :default
```

ラベル（`:default`）はサービス毎に定義してもいいかもしれないけど、そこまでのファインチューニングは不要でしょう。

このファイルの中身を空っぽにすれば `firewall_type="OPEN"` と同等になる[^備考４]。

- ルール番号１： 後に指定する `keep-state` のルールで許可した通信を【最速】処理する（以後のルールの解釈をさせない）。
- ルール番号３０００： 指定された「インターフェース名」に「ｉｎ」してくる、ＳＳＨ（ポート２２）にセットアップパケット（ＴＣＰ ＳＹＮ）をステートを維持してアクセスを許可する[^備考５]。

[^備考５]: 日本語でＯＫ。

ルール番号１はともかく、ルール番号３０００なのは後に説明する。また、`blacklistd` が要求するＩＰＦＷのルールについても、この時点では取り上げない。

## `blacklistd` の設定

### /etc/rc.conf
`/etc/rc.conf` に下記の設定を追加する。

```shell
blacklistd_enable="YES"
```

### /etc/ipfw-blacklist.rc
`/etc/ipfw-blacklist.rc` の中身はとりあえず空でもいいので、ファイルを作成する[^備考６]。

[^備考６]: 設定の意味とファイアウォールバックエンドの選択については後述する。

```shell-session
touch /etc/ipfw-blacklist.rc
```

この設定ファイルは `/usr/libexec/blacklistd-helper` というシェルスクリプトより参照され、`ipfw_offset` という設定が行える。この設定はデフォルトで `2000` と定義されている。

`blacklistd` ではＩＰＦＷルール番号「２０００＋ポート番号」にアクセス不許可（パケットをドロップさせる）のルールを追加する[^備考７]。よってＳＳＨｄ（ポート２２）の場合、ルール番号は２０２２となる。

[^備考７]: そういうルール（命名規則？）なので細かく制御できない。

また拒否するＩＰアドレステーブルとして `portポート番号` という名前のテーブルが使用される。ＳＳＨｄ（ポート２２）の場合、テーブル `port22` という名前になる[^備考７]。

```shell-session
# ipfw show
  :
02022     0        0 deny tcp from table(port22) to any 22
  :
# ipfw table port22 list
```

### /etc/blacklistd.conf
いよいよ本機能が期待されるアタック避けの設定である。どのようにチューニングするかが腕の見せ所。
取り急ぎデフォルト設定をベースに解説する。問題を限定するために、ＳＳＨｄに限定する。

```
# $FreeBSD: releng/12.2/usr.sbin/blacklistd/blacklistd.conf 336977 2018-07-31 16:39:38Z brd $
#
# Blacklist rule
# adr/mask:port type    proto   owner           name    nfail   disable
[local]
ssh             stream  *       *               *       3       24h
ftp             stream  *       *               *       3       24h
smtp            stream  *       *               *       3       24h
submission      stream  *       *               *       3       24h
#6161           stream  tcp6    christos        *       2       10m
*               *       *       *               *       3       60

# adr/mask:port type    proto   owner           name    nfail   disable
[remote]
#129.168.0.0/16 *       *       *               =       *       *
#6161           =       =       =               =/24    =       =
#*              stream  tcp     *               =       =       =
```

#### `[local]` と `[remote]`
本設定ファイルは `[local]` と `[remote]` のスタンザ（節）に分かれる。[マニュアル](https://www.freebsd.org/cgi/man.cgi?blacklistd.conf(5))によればそれぞれ以下の役割となっている。

- `[local]` スタンザ（節）はデフォルト設定を記述する。
- `[remote]` スタンザ（節）はカスタム（例外）設定を記述する。

そこで `[local`] スタンザ（節）の `ssh stream * * * 3 24h` に注目する。

#### `ssh stream * * * 3 24h` という設定

| ロケーション | ソケット型 | プロトコル | オーナー | 名前 | 失敗回数 | 無効期間 |
| ------------ | -----------| ---------- | -------- | ---- | -------- | -------- |
| `ssh`        | `stream`   | `*`        | `*`      | `*`  | `3`      | `24h`    |

- `*`： デフォルトまたはワイルドカード一致を意味する。
- `=`： `[local]` の設定を踏襲する。
- ロケーションは `[<アドレス>|<インターフェース>][/<マスク値>][:<ポート>]` という書式[^備考８][^備考９]で記述する。
  - 「アドレス」はＩＰｖ４の場合は数字表記（`XXX.XXX.XXX.XXX`）で、ＩＰｖ６の場合は `[]` にくくられた数字表記（`[XXXX:XXXX::XXXX]`）である。
  - 「インターフェース」はインターフェース名（`re0`、`vmx0` 等）。
  - 「マスク値」は数字表現（０～３２または０～１２８）。
  - 「ポート」はポート番号ないしはポート名。ポート名は `/etc/services` をもってポート番号を解釈する。
- ソケット型は「`stream`」、「`dgram`」が指定可能である。
- プロトコルは「`tcp`」、「`udp`」、「`tcp6`」、「`udp6`」が指定可能である。
- ソケットストリーム型とプロトコルは基本的[^備考10]に「`stream *`」と指定するのが無難かな。
- オーナーはイベントを報告してくるデーモン（ここではＳＳＨｄ）の実行ユーザー名（またはユーザーＩＤ）である。
- 名前はパケットフィルター規則の名前ですが、ＩＰＦＷでは使用してない（ＮＰＦとＰＦだけ）ので無視でいいです。
- 失敗回数はアクセスがブロックされる前に試行した失敗した回数である。`*` の場合無限回許される。
- 無効期間は `*` （永遠）または時間（単位が無い場合は秒数）で、単位としては「`s`」（秒）、「`m`」（分）、「`h`」（時）、「`d`」（日）が指定できる。

[^備考８]: 指定不可能な組み合わせ（`re0/24` など）があるみたいだけど未調査。
[^備考９]: `ssh` がポート名というのは書式に合わない気がす。`:` はどうした。`:` は。微妙にマニュアルの表現が甘い気が。
[^備考10]: 原則とは言わない。`blacklistd` に対応したサーバーソフトウェアとプロトコル考えると、大抵はＴＣＰ／ＩＰだとは思うので。

### アロー（許可）リストの作成
`[remote]` スタンザ（節）に `#129.168.0.0/16 * * * = * *` という例があるが、こういう形（もちろんコメントインで）でトラステッドネットワークからのアクセスに対し、`blacklistd` がアクセス拒否しないように設定する。
特にコンソールにアクセスできないような環境の場合、間違ってフィルター登録され、２４時間もアクセスできないとなると被害が大きいこともあり、注意深く設定することが要求される。

## ＳＳＨｄの設定
カジュアルに設定するなら `/etc/rc.conf` に下記の設定をする。

```shell
sshd_flags="-o UseBlackList=yes"
```

運用に問題無いようであれば、`/etc/ssh/sshd_config` に反映する。

```diff
--- /etc/sshd_config.orig       2020-12-17 08:21:52.421625000 +0900
+++ /etc/sshd_config    2020-12-26 16:40:21.836724000 +0900
@@ -104,7 +104,7 @@
 #MaxStartups 10:30:100
 #PermitTunnel no
 #ChrootDirectory none
-#UseBlacklist no
+UseBlacklist yes
 #VersionAddendum FreeBSD-20200214

 # no default banner path
```

# 運用開始
再起動すれば全て立ち上がるといえば立ち上がるのだけど、設定ミスがあったりすると目も当てられないこともあり、手動で立ち上げる手順を示す。

## ＩＰＦＷの起動
```shell
service ipfw start
```

起動直後は `ipfw show` で設定確認すればＯＫ。

```shell-session
# ipfw show
00001      0        0 check-state :default
00100      0        0 allow ip from any to any via lo0
00200      0        0 deny ip from any to 127.0.0.0/8
00300      0        0 deny ip from 127.0.0.0/8 to any
00400      0        0 deny ip from any to ::1
00500      0        0 deny ip from ::1 to any
00600      0        0 allow ipv6-icmp from :: to ff02::/16
00700      0        0 allow ipv6-icmp from fe80::/10 to fe80::/10
00800      0        0 allow ipv6-icmp from fe80::/10 to ff02::/16
00900      0        0 allow ipv6-icmp from any to any icmp6types 1
01000      0        0 allow ipv6-icmp from any to any icmp6types 2,135,136
03000      0        0 allow tcp from any to any 22 in via インターフェース setup keep-state :default
65535      0        0 allow ip from any to any
```

なおルール番号１００から１０００までは自動挿入である[^備考11][^備考12]。

[^備考11]: `/etc/rc.firewall` 由来。`setup_loopback` と `setup_ipv6_mandatory` による。
[^備考12]: 強制的に入るので順番を変えたい等、何かしら調整したいなら `/etc/ipfw.conf` の最初に `-f flush` 入れるしかない。

## `blacklistd` の起動
```shell-session
service blacklistd start
```

## ＳＳＨｄの再起動
```shell-session
service sshd restart
```

## モニタリング
稼動状態をモニタリングするのであれば `ipfw show` と `ipfw table port22 list` を実行することで確認できる。
ＩＰＦＷの場合「ルール番号」「パケット数」「バイト数」「ルール」という並びになるので、ＳＳＨ接続があればルール番号３０００のパケット数（例では５７８）を数えればよい。実際にアクセス拒否した回数はルール番号２０２２となる（例では２１６）。

```shell-session
# ipfw show
  :
02022    216    12960 deny tcp from table(port22) to any 22
03000    578    99786 allow tcp from any to any 22 in via インターフェース setup keep-state :default
65535 125299 44785979 allow ip from any to any
```

実際にアクセス拒絶となったＩＰアドレスは下記の通り確認できる。

```shell-session
# ipfw table port22 list
XXX.XXX.XXX.XXX/32 0
XXX.XXX.XXX.XXX/32 0
```

`blacklistd` が認識している状態（ブロック状態についても含む）は `blacklistctl` コマンドを使用して確認する。

```shell-session
# blacklistctl dump -a
        address/ma:port id      nfail   last access
XXX.XXX.XXX.XXX/32:22   OK      2/2     YYYY/mm/Dd HH:MM:SS
XXX.XXX.XXX.XXX/32:22   OK      2/2     YYYY/mm/dd HH:MM:SS
```

[マニュアル](https://www.freebsd.org/cgi/man.cgi?blacklistctl(8))にはヘッダーの意味が書いてないため調べたところ、`id` は無視していいです（ＮＰＦのみ使用）。今のところ「`OK`」と「」（ヌル）しか見てません。

# チューニングポイント
サンプルでは３回間違えると２４時間アクセスを拒否する設定になっているが、これを２回にして１分程度で解除するのも面白い。

```
# Blacklist rule
# adr/mask:port type    proto   owner           name    nfail   disable
[local]
ssh             stream  *       *               *       2       60s
```

一つには本当に間違ってアクセスしてしまった時のリカバリが１分で済む点と、アタックはそう長時間に渡って行われないためである。
実際に試してみるとフィルタリング能力としては、半日様子見て高々０．５％未満くらいである。なお２４時間設定にして、２時間程度様子を見た場合は高々１６％未満なので、短期間にアクセスして、またしばらくしてアクセスするといったパターンなのが分かる。
正直短期間のペナルティと長期間のペナルティを分けて設定できるようにして欲しいところ。 orz

また他のツールと違い、ポートスキャンレベルでは失敗にアタックと解釈されないため、鍵認証を必須にしているような環境では、ある程度回数を絞り込める余地はある。それでも流石に１回までしか許さないのはキツイと思うけど。

また拒否時間も２４時間は長すぎる。確実に解除問い合せが来るので、長くても３分程度に抑えるべきであると考える。

# よくある質問とその答え

## Ｑ．半歩先行く設定って？他の説明と何が違うの？
Ａ．ステートフルインスペクション機能により、ＳＳＨのＴＣＰセッションが生きてる間は、`blacklistd` の登録の影響を受けない点が違います。
実験中に誤って自分が接続しているＩＰがブロックされても、今生きてるＳＳＨのセッションが生きてる間はリカバリできる余地があります。
また通信を遮断するにしても、ＴＣＰ ＲＳＴパケットを送信しきれることから、ＴＣＰセッションタイムアウトを待たなくていいメリットがあります。
これは逆に、攻撃者側にとってもメリットを有することでもあるのですが、そもそも認証に失敗すればＴＣＰセッションが切れることから、言うほどのメリットは無いです。むしろＴＣＰセッションが確実に切れるメリット（リソース的に）の方が高いです。

## Ｑ．「微妙」という評価はなんで？ `blacklistd` はよろしくない？
Ａ．この手のツール[^備考13]としては悪くないと思います。むしろ良い。
まず何よりも、従来のツールが各種ログを拾いながら、ブロックリストを生成するのと違い、各種サーバーソフトウェアが `blacklistd` に即座に通知する仕組みであるため、極めて迅速[^備考14]にブロックリストが作られます。この点が他のツールとは大いに違うところになります。

[^備考13]: 自分が他に試したのは [SSHGuard](https://www.sshguard.net/) くらいですが、他にも [Fail2ban](https://www.fail2ban.org/wiki/index.php/Main_Page) など。
[^備考14]: 拒絶要件に達した直後、次の接続ではフィルタリングされているくらい早い。

それ故に期待値が上がってしまっているのですが、いわゆるログインエラーの類いについては上手く機能しています。ポートスキャンレベルの攻撃？は上手く拾えていません。もちろん拾えてない原因はＳＳＨｄ側にあるのですが、この結果、あまりブロックしてる感じはしません。
またＳＳＨｄが全面的に悪いか、というとそうでも無く、[マニュアル](https://www.freebsd.org/cgi/man.cgi?blacklist(3))によれば、認証結果（成功・失敗）しか拾わないようです。
まあポートスキャンレベルで…という話はあるのでしょう。実際、`ssh-keyscan` や各種監視ツールではポートスキャンレベルといえばそうではありますが。

もう一つ期待外れな点としては、認証成功の記録を活用していない点です。認証成功をもってペナルティに対するガードとして機能してくれれば、アローリストの作成的にも嬉しいのですが。

## Ｑ．ブラックリスト…ホワイトリスト…
Ａ．君のような勘の良いｒｙ。この変更は極めて影響が大きいので、メジャーバージョンアップ時（１３－Ｒ）に変わるのではないかな、とみてます。

## Ｑ．ファイアウォールは必要ですか？
Ａ．`blacklistd` そのものはフィルタリング処理はしないです。その手の処理はファイアウォールにアウトソースする仕組みになっています。
また対応しているファイアウォールも、ＩＰＦＷ、ＮＰＦ、ＰＦ、ＩＰＦに限られています。優先順位も下記の通りとなります。

1. `/etc/ipfw-blacklist.rc` というファイルがある場合、ＩＰＦＷをバックエンドに使う。
2. `/etc/npf.conf` というファイルがある場合、ＮＰＦをバックエンドに使う。
3. `/etc/pf.conf` というファイルがある場合、ＰＦをバックエンドに使う。
4. `/etc/ipf.conf` というファイルがある場合、ＩＰＦをバックエンドに使う。

この辺りは `/usr/libexec/blacklistd-helper` と[マニュアル](https://www.freebsd.org/cgi/man.cgi?blacklistd(8))の `-C` オプションの記述を参考に、ガチカスタマイズすることは可能です[^備考15]。

繰り返しますが、ＩＰＦＷ、ＮＰＦ、ＰＦ、ＩＰＦしか対応してないのはベースシステムの制限であって、他のファイアウォールの選択を拒絶するものではありません。それ以外のファイアウォール（外部を含む）を使用したい場合は、上手く作り込んでください。

[^備考15]: 具体的なカスタマイズ例（外部ファイアウォールへのアウトソース）を考えてみたのだけど、意外と大変そうなのでアイデアのみで。

## Ｑ．`ipfw_offset` とか `2000` とか `3000` ってなんなんだよ。
Ａ．ＩＰＦＷのルール番号の指定です。`blacklistd` に対応を謳ってるサーバーが対象とするプロトコルはおおよそ３桁ポート番号なので、`ipfw_offset` に対して＋１０００した値で許可設定を入れてみました。ある意味ルール番号２９９９というのもありですが。
`blacklistd` がルールを挿入する位置とバランスを取ってルール番号を決めてください。

逆にガッチガチに制御したい向けは、`blacklistd` に `-C` オプションを指定して、完全に自前で運用するしか無いです。

## Ｑ．やっべ、自分の接続元ＩＰアドレスがブロックリストに登録されてしまった！
Ａ．大丈夫だ。問題無い。現在 `blocklistctl` に解除する機能が無いので `ipfw table port22 delete XXX.XXX.XXX.XXX/32` とコマンドを打って消そう！
なお拒否期限が残ってる間は `blacklistd` を再起動する度に登録されるのでよろしく！[^備考16]

[^備考16]: と[ハンドブック](https://www.freebsd.org/doc/handbook/firewalls-blacklistd.html#idp70732920)に書いてある。

## Ｑ．何か改善点ありますか？
色々課題（`blacklistctl` の充実など）はあると思いますが、最低でもベイジアンフィルター、ナウでヤングには機械学習の実装が期待されるかと。
とりあえず仕組みは簡単なので、ＬＬ言語でプロトタイピングするのが吉かな。

# 参考文献
- [blacklistd を使ってみる](https://www.slideshare.net/YuichiroNaito/how-to-use-blacklistd)
- [Blacklistd](https://www.freebsd.org/doc/handbook/firewalls-blacklistd.html) （ＦｒｅｅＢＳＤハンドブック）
- [blacklistd.conf(5)](https://www.freebsd.org/cgi/man.cgi?blacklistd.conf(5))
- [blacklistctl(8)](https://www.freebsd.org/cgi/man.cgi?blacklistctl(8))
- [blacklist(3)](https://www.freebsd.org/cgi/man.cgi?blacklist(3))