fail2ban (0.10系)をファイアウォールpfとの組み合わせで使う

fail2banはログを監視して設定したキーワード(ログイン認証に失敗など)の行を見つけたらログのその行に書かれたIPアドレスをファイアウォールに一定時間登録、期限が来たら解除するツール。インターネットにサーバーを公開していたら怪しいアクセスが大量にくるけど、キーワード部分の設定しだいではしつこいやつらの多くをBANすることができるかも。操作するのはファイアウォールに限らず他のコマンドも実行できる。1時間の間に同一のIPアドレスで認証失敗が3回あったらBAN(本来の用途)、ログに攻撃パターンが出現したらスクリプトを起動して攻撃し返して「攻性防壁を発動」っていう中二病的なことも設定次第でいろいろ。

で、そのfail2banが昨年夏頃にVer.0.9系からVer0.10系になってようやくIPv6対応になったのは良いんだけど、いろいろ変わった部分があって、Linuxとiptableの組み合わせではどうか知らないけどFreeBSDとpfの組み合わせで使っている人は困っているかも。というか、頭のカタい「がとらぼ」の中の人がしょっちゅう間違える。だからそのfail2ban v0.10系の設定備忘録。

v0.9系までの設定

/usr/locall/etc/fail2ban/jail.local
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[DEFAULT]
ignoreip = 127.0.0.1/8 192.168.0.0/24
bantime  = 3600    ←たしか秒指定のみだった
findtime  = 7200   ←たしか秒指定のみだった
maxretry = 2

[wordpress-auth]
enabled  = true
filter   = wordpress-auth
action   = pf
           sendmail-whois[name=wordpress-auth, dest=foobar@example.com, sender=fail2ban@example.com]
logpath  = /var/log/wordpress-access.log

そのままでも、おそらくv0.10系で読み込んだら起動はする。けど実際はエラーだらけ。

v0.10系の設定

/usr/locall/etc/fail2ban/jail.local
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1/128 192.168.0.0/24 2001:2c0:d800:6701::/64
bantime  = 12h30m
findtime  = 1d12h
maxretry = 2

destemail = foobar@example.com
sender = fail2ban@<fq-hostname>

banaction = pf
action_pf = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", actiontype=<allports>]
            %(mta)s-whois[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s"]

action = %(action_pf)s

[wordpress-auth]
enabled  = true
filter   = wordpress-auth
logpath  = /var/log/wordpress-access.log
#port = http,https

[hoge]
enabled  = true
filter   = hoge
logpath  = /var/log/hoge.log

[DEFAULT]セクションと各Jailセクション(上の例だと[wordpress-auth]や[hoge])に書くのは以前と同じくDEFAULTに書いたら全てのセクションで標準値扱い、Jailセクション側に書いたらそのセクションでは標準値を上書きなのでどちらに書かなくてはならないというのはない。
時間の指定は従来通り数値のみで秒扱い、その他1s (1秒), 1m (1分), 1h (1時間), 1d (1日), 1mo (1ヶ月), 1y (1年)のような単位付きの書き方もできる。

変数banactionでファイアウォールのpfを指定(action.d/pf.conf参照)。オプションとしてactiontype=<allports>を指定した。またはactiontype=<multiport>を指定するが、その場合は変数action_pfのbantimeの後ろあたりにport="%(port)s", を追加し、各セクションにもport= hogeを書くことになるかと。 (上の例ではコメントになっているport = http,https)
何で直でaction指定しないでaction_pf変数を使ってるのかはツッコまないで。

pfのアクション周りは全面刷新と言って良いくらいなのでaction.dディレクトリの中の.pf.confを確認しておいた方が良いかも。

フィルタに使うfailregexやignoreregexは変わってないように見えるのでfilter.d内に書くファイルはv0.9以前用がそのまま使えそう。

ファイアウォールpf周り

pf周りが完全に変わってしまっているのでv0.9系から更新する場合は要対応。

v0.9系までは以下な感じ。

/etc/pf.conf
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
ext_if = "em0"
table <private> const { 10/8, 172.16/12, 192.168/16 }
table <blacklist> persist file "/etc/pf_blacklist"
table <fail2ban> persist

set skip on lo0
scrub in all
scrub out on $ext_if all random-id
block all
block in  quick on $ext_if from { <private>, <blacklist>, <fail2ban> } to any
後略

上の例だと許可が無いので外部と通信できないけど後略の部分に許可ルールを書くとする。
と、いうことで、v0.9系までのfail2banではfail2banテーブルを作成してそのテーブルに含まれるIPアドレスからの通信を不許可にするルールを書いていた筈(4,10行目ね)。fail2banはそのfail2banテーブルにIPアドレスを追加・削除していた。

v0.10系ではこんな感じ。

/etc/pf.conf
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
ext_if = "em0"
table <private> const { 10/8, 172.16/12, 192.168/16 }
table <blacklist> persist file "/etc/pf_blacklist"

set skip on lo0
scrub in all
scrub out on $ext_if all random-id

anchor "f2b/*"

block all
block in  quick on $ext_if from {<private>, <blacklist>} to any
後略

9行目のアンカーがキモ。pfの設定に追加するのはこれだけ。別な書き方はマニュアル見てね。
アンカーは「IPアドレスの集合」ではなく「fail2banが作成した不許可ルール」がインクルードされると思えば良いかと。そのアンカーのルールは(fail2banが作るルール)はテーブルに含まれるIPアドレスからのパケットをドロップ。

つまり、アンカーにドロップルールが書かれているのでpf.confの独自ルールでドロップさせない(12行目ね)
fail2banが作ったf2b-hogeのテーブル名を使ったルールを他に作ってはダメということではない。

念の為に確認してみるとこんな感じ。

actiontype=<allports>指定の場合
# pfctl -a f2b/wordpress-auth -s rules
block drop quick proto tcp from <f2b-wordpress-auth> to any
# pfctl -a f2b/hoge -s rules
block drop quick proto tcp from <f2b-hoge> to any
actiontype=<multiport>指定の場合 (wordpress-authだけ)
# pfctl -a f2b/wordpress-auth -s rules
block drop quick proto tcp from  to any port = http
block drop quick proto tcp from  to any port = https

上のjail.local設定例の[wordpress-auth]のport指定のように2つのポートを指定すると2行できるみたい。 block drop quick proto tcp from to any port { www, https } みたいな書き方で良いのにねぇ。

f2b-wordpress-authやf2b-hogeはfail2banが自動作成するテーブルで、これにIPアドレスが入っている。
ルールとしてはテーブルに含まれるIPアドレスからの通信を不許可という簡単なもの。
そのテーブルを見てみる。

# pfctl -a "f2b/wordpress-auth" -t f2b-wordpress-auth -T show
   192.168.2.149
   192.168.2.150
# pfctl -a "f2b/hoge" -t f2b-hoge -T show
   192.168.2.151
   192.168.2.152

普通のテーブルの確認は pfctl -t テーブル名 -T show だけど、アンカーの場合は pfctl -a "アンカー名" -t テーブル名 -T show になるので間違いなく。

と、いうことで、アンカーの名前は「f2b/セクション名」でテーブルの名前は「f2b-セクション名」となる。これさえ憶えておけば間違わない筈。

誤登録IPアドレスの削除・誤削除IPアドレスの登録

基本的にはfail2ban-clientコマンドを使う。

# fail2ban-client unban --all    #全てのセクションに登録されている全IPアドレスを削除する場合
# fail2ban-client set セクション名 unbanip IPアドレス    #特定のIPアドレスを削除する場合
# fail2ban-client set セクション名 banip IPアドレス        #特定のIPアドレスをBANする場合

手動でpfctlを使ってIPアドレスを削除する場合

# pfctl -a "f2b/セクション名" -t f2b-セクション名 -T delete IPアドレス
1/1 addresses deleted.

v0.9系まではpfctl -t テーブル名 -T delete IPアドレスだったが、当然これもアンカー指定が必要。

fail2banの稼働ステータスを見る

# fail2ban-client status
Status
|- Number of jail:      13
`- Jail list:   wordpress-auth, hoge, hage,・・・ 


# fail2ban-client status wordpress-auth
Status for the jail: wordpress-auth
|- Filter
|  |- Currently failed: 33
|  |- Total failed:     2259
|  `- File list:        /var/log/wordpress-access.log
`- Actions
   |- Currently banned: 33
   |- Total banned:     98
   `- Banned IP list: 192.168.0.149
                         以下略

ELK Stackでシステム監視 kibanaでDNSサーバの情報表示

DNSサーバBINDの統計情報を取得する記事の続き。

BINDの統計情報をデータを可視化するために前回に引き続きTimelion, Timeseriesを使用する。グラフの作り方も前回と変わらない。

kibanaでDNSサーバの情報表示 1
収集したBINDの統計情報のデータの形はこんなの。ホスト名、collectd_typeで絞って、type_instanceで分ければ良さそうに見える。

kibanaでDNSサーバの情報表示 2
(上の画像のグラフは差分にしていない)
この統計情報も各値はBIND起動からの総量なので、数値は増加するのみ。BINDを再起動するなどで0に戻る。一定時間前の値との差分を表示するとその時間で増えた値を見ることができるのでTimelion, Timeseriesを使うことにする。

1 .es(q='host:hoge AND collectd_type:dns_request', split='type_instance.keyword:10', metric='max:value')

type_instanceで分けるのでsplit=hogeを使用する。splitを使う際は幾つまで分けるかを指定してやらないとエラーになるのでそこだけ注意。(上の例では :10 で10個)

で、1分間の差分を取るなら .es(hoge).subtract(.es(hoge, offset='-1m'))のように書くのを前回やった。
でも、splitを使っているとsubtract()がエラーになるのでsplitは諦めて1グラフずつ差分を作って並べることにする。(kibana6.2以降はできるような話を見たような気がするけど本当かしら?)

dns_request別
1
2
3
4
5
6
7
8
.es(q='host:hoge AND collectd_type:dns_request AND type_instance:IPv6', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_request AND type_instance:IPv6', metric='max:value', offset='-1m')).label('IPv6').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_request AND type_instance:IPv4', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_request AND type_instance:IPv4', metric='max:value', offset='-1m')).label('IPv4').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_request AND type_instance:TCP', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_request AND type_instance:TCP', metric='max:value', offset='-1m')).label('TCP').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_request AND type_instance:SIG0', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_request AND type_instance:SIG0', metric='max:value', offset='-1m')).label('SIG0').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_request AND type_instance:TSIG', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_request AND type_instance:TSIG', metric='max:value', offset='-1m')).label('TSIG').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_request AND type_instance:EDNS0', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_request AND type_instance:EDNS0', metric='max:value', offset='-1m')).label('EDNS0').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_request AND type_instance:BadEDNSVer', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_request AND type_instance:BadEDNSVer', metric='max:value', offset='-1m')).label('BadEDNSVer').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_request AND type_instance:BadSIG', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_request AND type_instance:BadSIG', metric='max:value', offset='-1m')).label('BadSIG').bars(width=3).title('DNS Request').yaxis(min=0)
dns_response別
1
2
3
4
5
.es(q='host:hoge AND collectd_type:dns_response AND type_instance:SIG0', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_response AND type_instance:SIG0', metric='max:value', offset='-1m')).label('SIG0').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_response AND type_instance:EDNS0', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_response AND type_instance:EDNS0', metric='max:value', offset='-1m')).label('EDNS0').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_response AND type_instance:TSIG', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_response AND type_instance:TSIG', metric='max:value', offset='-1m')).label('TSIG').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_response AND type_instance:normal', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_response AND type_instance:normal', metric='max:value', offset='-1m')).label('normal').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_response AND type_instance:truncated', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_response AND type_instance:truncated', metric='max:value', offset='-1m')).label('truncated').bars(width=3).yaxis(min=0).title('DNS Response')
dns_qtype別
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:AAAA', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:AAAA', metric='max:value', offset='-1m')).label('AAAA').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:A', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:A', metric='max:value', offset='-1m')).label('A').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:TXT', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:TXT', metric='max:value', offset='-1m')).label('TXT').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:CNAME', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:CNAME', metric='max:value', offset='-1m')).label('CNAME').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:SOA', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:SOA', metric='max:value', offset='-1m')).label('SOA').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:NS', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:NS', metric='max:value', offset='-1m')).label('NS').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:DNSKEY', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:DNSKEY', metric='max:value', offset='-1m')).label('DNSKEY').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:RRSIG', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:RRSIG', metric='max:value', offset='-1m')).label('RRSIG').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:NAPTR', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:NAPTR', metric='max:value', offset='-1m')).label('NAPTR').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:PTR', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:PTR', metric='max:value', offset='-1m')).label('PTR').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:SSHFP', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:SSHFP', metric='max:value', offset='-1m')).label('SSHFP').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:A6', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:A6', metric='max:value', offset='-1m')).label('A6').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:ANY', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:ANY', metric='max:value', offset='-1m')).label('ANY').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:SRV', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:SRV', metric='max:value', offset='-1m')).label('SRV').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:Others', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:Others', metric='max:value', offset='-1m')).label('Others').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:SPF', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:SPF', metric='max:value', offset='-1m')).label('SPF').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:MX', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:MX', metric='max:value', offset='-1m')).label('MX').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:LOC', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:LOC', metric='max:value', offset='-1m')).label('LOC').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:TLSA', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:TLSA', metric='max:value', offset='-1m')).label('TLSA').bars(width=3), 
.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:AXFR', metric='max:value').subtract(.es(q='host:hoge AND collectd_type:dns_qtype AND type_instance:AXFR', metric='max:value', offset='-1m')).label('AXFR').bars(width=3).yaxis(min=0).title('DNS QType')

この記事用としてdns_requestとdns_responsesとdns_qtypeの3つだけ例として挙げる。splitが効けば楽だったんだけど、使えないのでとにかく並べた。足りないのがあったらゴメンなさい。(たぶん足りない)

また、BIND再起動時を含むグラフが差分のマイナスのせいで見られたものではなくなるのを防ぐために最後に .yaxis(min=0) を付けてマイナス値を表示させないようにした。(チャート全体のタイトルも付けている)

kibanaでDNSサーバの情報表示 3
一番上のグラフは前回やったネットワークインターフェースの送受信。その下5つ(の内3つ)がこの記事のBIND関係のグラフ。

関連記事:
Up