🔑 重要なポイント
✦ AI·GEN筆者がサーバーの防御を fail2ban から CrowdSec へ乗り換えた一部始終を、そのまま真似できる手順としてまとめた記事。きっかけは侵入ではなく騒がしさで、スキャナーが一日中 /wp-login.php や /.env を叩き、監視通知を埋め尽くしていた。当時 fail2ban はすでに 614 個の IP を BAN していたが、相手がずっと IP を回していることに気づく。IP を BAN する道に終わりはないのだ。のちのデータもそれを裏づけた。送信元の上位七つはほぼすべて Azure、DigitalOcean、Alibaba Cloud といったクラウドのデータセンターで、新しい VM を一台立てるほうが IP を一つ BAN するよりずっと安い。記事にはこのマシンの六日間の実数が載る。検知した攻撃 1619 件、ファイアウォールが同時に遮断している IP は 25.22k、そのうちコミュニティブロックリストが供給する量は自前のエンジンが捕まえる量の三千倍にのぼる。構築で踏んだ三つの落とし穴は一本の因果の鎖で(apt が ESM から大昔の版を拾う、メジャー跨ぎの更新で hub が移動、symlink が全部切れて解析率 9%)、診断用の cscli explain や「metrics は累計値」といった誤読しやすい勘所も添えてある。後半がいちばん身に覚えのある部分で、筆者は自分を三回締め出している。圧巻は自分で設定した nginx のレート制限が 429 を返し、その 429 を見た CrowdSec が筆者を BAN した回だ。自分で入れた防御が二つ繋がって自分に噛みついた。最後は Discord で解除する仕組み。コンテナはリクエストファイルを置くことしかできず、実際に cscli を動かすのはホストの root watcher である。そしてそれが役に立つ理由は、bot が自分から外へ繋ぎに行っているので、IP が遮断されても指令は届くからだ。
きっかけは侵入ではなく、うるささだった。
うちの nginx は に繋いであり、異常があれば Discord に飛ぶ。こんな見た目で、その時点の Top IP と Top Path まで一緒に貼ってくれる。
診断情報は十分すぎるほど親切だ。問題はそれが一日に数百から数千回鳴ることで、しかもその大半は自分のサービスに何かあったわけではない。スキャナーが /wp-login.php や /.env、ありとあらゆる .php エンドポイントを叩いて出る 4xx である。うちのサイトに PHP など一つもないのだが、向こうはそれを知らないし、気にもしていない。ただ叩き続ける。
(上の一枚はむしろ数少ない「本当に何かあった」ときのもので、/api/spotify/audio-features が五分間で 9968 回叩かれている。あれは自分のコードがループしていた。IP は伏せた。あれはこちらの出口アドレスだからだ。)
当時このマシンにはすでに fail2ban が入っていて、数字も立派だった。
614 個も BAN したと聞けば役立っているように思える。だがその通知の列を眺めていて、一つ気づいた。相手はずっと IP を変えている。一つ潰しても、次は別の回線から来る。この調子で BAN し続けて、そもそも終わりはあるのだろうか。
この記事は、そのあと CrowdSec に乗り換えた一部始終である。なぜこの状況では fail2ban より向いているのか、構築で踏んだ三つの落とし穴、このマシンが六日間で実際に何を止めたのか、そして自分自身を三回締め出した話(三回目がいちばん間抜けで、いちばん書く価値がある)。最後は自作の Discord 解除 bot について。自分の防御に締め出されているとき、SSH こそまさに繋がらないものだからだ。
このマシンの構成#
あとに出てくる落とし穴も自己 BAN も構成に依存するので、先に環境をはっきりさせておく。
後半に効いてくる点をいくつか。
- すべて一台の物理マシンで動いている。自宅の固定回線に固定 IP、ルーターが 80/443 を転送してくる。クラウドのロードバランサーもなければ、何かあったときに肩代わりする二台目もない。
- 遮断は nftables で起きる。つまり nginx より手前だ。遮断されたリクエストは access log にすら残らず、こちら側から見えるのは接続拒否ではなく沈黙したタイムアウトになる。この位置関係が、後半の診断すべての土台になる。
- CrowdSec のエンジンと bouncer は別物。エンジンが nginx の log を読んで判断し、bouncer がファイアウォールで実行する。図の点線二本がそのループだ。
- nginx の後ろには十数個のサブドメインがぶら下がっている。ブログ、NAS、監視、MQTT、細かいツール類。どれか一つのレート制限やエラーも同じ access log に流れ込み、つまり CrowdSec に流れ込む。
もう一つ本文に関わることがある。こちらのメインドメインは Cloudflare のグレー雲(DNS だけで、通信は自宅に直結)なのだが、もう一人の管理人が自分のサブドメインをオレンジ雲経由でこちらに向けている。その経路の送信元 IP は Cloudflare のノードになる。この件は相互監視の記事で一度こちらに噛みついていて、詳しくはそちらに書いた。
なぜ fail2ban ではないのか#
fail2ban は壊れていない。仕様どおりの仕事をきちんとしている。問題はそのモデルにある。自分の log を読み、自分の失敗回数を積み上げ、自分が見た IP を BAN する。
IP を回してくる相手に対して、これはモグラ叩きだ。新しい IP はどれもこちらにとって初対面で、ゼロから閾値まで積み上げないと BAN されない。そしてその頃には、もう一巡スキャンし終えている。
CrowdSec は前提を変えた。他人がすでに遭遇した悪い IP を、そのまま使わせてもらう。
| fail2ban | CrowdSec | |
|---|---|---|
| 判断の材料 | ローカルの log だけ | ローカルの log に加えて世界規模のコミュニティブロックリスト |
| 見たことのない IP | ゼロから積み上げ、一巡ぶんは通す | 誰かが報告済みなら即座に遮断 |
| 遮断の実行 | 自分で iptables を書き換える | bouncer に任せる(nftables / nginx / Cloudflare と差し替え可) |
| ルールの出所 | 自分で書く正規表現 | Hub のコミュニティルール。CVE シナリオも大量にある |
三行目の「bouncer に任せる」は本物のアーキテクチャ上の違いだ。CrowdSec は検知と実行を切り離している。エンジンは誰を止めるか決めるだけで、ファイアウォールに触るのは別の bouncer である。この分離が後半で効いてくる。
もう一つ、こちらにとって決定的だったのは自前で立てられることだ。当時のもう一つの選択肢はサイトを Cloudflare の後ろに置いて吸収させることだったが、それは通信を他人のプロキシに預けるのと同じである。CrowdSec はローカルで log を読み、ローカルのファイアウォールで実行するので、既存の構成には一切触らない。
NOTE
コミュニティブロックリストは既定で有効
インストール時に Registering to CAPI(Central API)が自動で走るので、あの共有ブロックリストは追加設定なしで即座に守り始める。cscli console enroll は任意で、ダッシュボードとより大きな購読リストが手に入る。
三つの落とし穴、根っこは一つ#
CrowdSec のインストールは二行だ。
curl -s https://install.crowdsec.net | sudo sh
sudo apt install -y crowdsec
sudo apt install -y crowdsec-firewall-bouncer-nftables # Ubuntu 24.04 は既定で nftablesそのあと一晩まるごと溶かした。しかも三つの落とし穴は、一枚のドミノが倒れた連鎖だった。
公式スクリプトは確かに packagecloud の repo を追加してくれる。だが apt install crowdsec が実際にダウンロードした先は esm.ubuntu.com だった。には凍結された 1.4.6 があり、packagecloud のほうは 1.7.x である。
インストール時点では何のエラーも出ない。cscli version を叩いて初めてバージョンが違うと気づく。
正直に言うとこの設計は今でも腹立たしい。ESM の趣旨はサードパーティパッケージのセキュリティ維持を延長することだが、バージョンをある時点で凍結したうえで、追加したばかりの公式 repo より前に並ぶ。結果が「最新版を入れたつもり」である。この一件のあと真面目に考えた。ただ綺麗なサーバーが欲しいだけなら、Debian のほうが Ubuntu LTS よりこの手の驚きはずっと少ないだろう。
バージョンが古いと分かれば、当然上げる。そして 1.4.6 から 1.7.8 のあいだで hub データの位置が移動していた。
旧 /var/lib/crowdsec/hub/...
新 /etc/crowdsec/hub/...旧バージョンが残した大量の symlink はすべて存在しないパスを指し、切れたリンクになっていた。インストール時に流れる Ignoring file ... no such file or directory の列がまさにこれなのだが、数十行の出力に埋もれてノイズとして流し読みしてしまう。
切れた symlink は、nginx-logs のような parser がそもそも読み込まれていないことを意味する。症状は、cscli metrics 上は log を読んでいるように見えるのに解析率が 9% しかない、というものだ。
Source Lines read Lines parsed
file:/var/log/nginx/access.log 41 3ファイルは読めるが中身が理解できない。理解できなければシナリオは発火せず、シナリオが発火しなければ誰も BAN されない。表面上はサービスが緑で、実際には完全な空回りである。
直し方は、切れたリンクを掃除してルールセットを入れ直すこと。
sudo find /etc/crowdsec -xtype l -delete # 「リンク先が存在しない」symlink だけを消す
sudo cscli hub update
sudo cscli collections install \
crowdsecurity/nginx crowdsecurity/sshd crowdsecurity/linux \
crowdsecurity/whitelist-good-actors --force
sudo systemctl reload crowdsecTIP
教訓:packagecloud を pin して 1.7.x を直接入れる
この三つの落とし穴は一本の因果の鎖で、源はただ一つ、最初の一歩で ESM の古い版を入れたことだ。インストール直後に cscli version でバージョン番号を確かめれば、後半まるごと省ける。
誤読しやすい診断の勘所が二つ#
cscli metrics の数字は累計である。解析率を直したのに改善しないと思い込んだ時期があったが、新しく解析された行が過去の失敗の山に薄められていただけだった。reload ではカウンタは消えず、restart で初めて消える。「直したあと」の本当の比率を見たいなら、再起動して積み直す必要がある。
cscli explain を使えば一目瞭然になる。一行の log が実際に通った parser の連鎖を印字してくれるので、どの段で失敗したのかがすぐ分かる。
sudo cscli explain --file /var/log/nginx/access.log --type nginx | tail -25直っていれば、parser failure だらけではなく crowdsecurity/nginx-logs が緑で通過し、そのままシナリオへ流れていくのが見えるはずだ。
SSH の取り込み元は一つだけにする#
もう一つ、自分で調整した点がある。SSH のイベントは /var/log/auth.log からも journalctl からも読めるが、両者は同じイベント群の二つの入口である。両方収集するとログイン失敗が二重に数えられ、設定した閾値より早くシナリオが発火し、alert の件数も水増しされる。
実測では両者とも 261 件を解析できたが、auth.log はそこへ辿り着くのに 10.13k 行を読む必要があり、journalctl は 330 行で済んだ。というわけで、うちの acquis.yaml は journalctl だけを残している。
source: journalctl
journalctl_filter:
- "_SYSTEMD_UNIT=ssh.service"
labels:
type: syslog六日間で 1619 件#
これがこのマシンの直近六日間の実数である(cscli alerts list の全件集計)。
攻撃の種類ごとに分けるとこうなる。
| シナリオ | 件数 | 何を狙っているか |
|---|---|---|
ssh-time-based-bf | 978 | SSH の低速ブルートフォース |
http-probing | 144 | 手当たり次第のエンドポイント探り |
ssh-time-based-bf_user-enum | 121 | ユーザー名の推測 |
http-sensitive-files | 44 | .env や .git の類を探す |
http-bad-user-agent | 39 | 既知のスキャナー UA |
http-wordpress-scan | 36 | WordPress の穴探し |
http-admin-interface-probing | 36 | 管理画面のログインページ探し |
http-cve-2021-41773 | 33 | Apache のパストラバーサル |
残りのロングテールは CVE 探索の行列だ。CVE-2017-9841(PHPUnit RCE)、http-cve-2021-42013、CVE-2022-41082(Exchange)、thinkphp-cve-2018-20062、netgear_rce、fortinet-cve-2018-13379、jira_cve-2021-26086。どれ一つ入れていないのに、それでも毎日叩きに来る。
攻撃は「悪い人のパソコン」から来ていない#
送信元 AS の分布こそ、いちばん興味深い列である。
| 送信元 | 件数 |
|---|---|
| MICROSOFT-CORP-MSN-AS-BLOCK | 199 |
| Techoff Srv Limited | 191 |
| DIGITALOCEAN-ASN | 171 |
| Hangzhou Alibaba Advertising | 165 |
| Hetzner Online GmbH | 161 |
| WEB MASTER COLOMBIA SAS | 153 |
| Sai gon Postel Corporation | 137 |
上位七つはほぼすべてクラウドのデータセンターだ。Azure、DigitalOcean、Alibaba Cloud、Hetzner。スキャナーは時間課金の VM 上で動いている。一台潰したところで、別の一台を立ち上げるのは数秒の話で、しかも IP は変わる。
これが最初の疑問へのデータ側からの答えになる。IP を BAN する道では、相手のコストのほうがこちらよりはるかに安い。そしてまさにそこがコミュニティブロックリストの価値でもある。新しく立ち上がったその VM は、こちらに来る前にたいてい誰か他人を叩いているからだ。
IMPORTANT
コミュニティブロックリストが止める量は、自前で捕まえる量の三千倍
cscli metrics の bouncer の欄は二つの出所をはっきり分けている。
Origin active_decisions
CAPI (community blocklist) 25.21k
crowdsec (security engine) 8こちらのエンジンがその時点で捕まえていたのは 8 件。コミュニティブロックリストは同時に 25210 件を供給していた。累計の遮断決定を見ても、http:exploit 17249、ssh:bruteforce 6632、http:scan 2894 はすべて CAPI 由来である。
つまり、この道具の価値の大半は、インストールし終えたその瞬間に効き始める。自分の履歴が溜まるのを待つ必要はない。
遮断されるものが全部悪者とは限らない#
はっきりさせておきたい細部が一つある。ある時の遮断リストに crowdsecurity/http-bad-user-agent で止められた 167.94.146.48 があった。調べると Censys、インターネット全体を調査する研究機関のものだった。同類には Shodan がある。彼らはこちらを攻撃しようとしているのではなく、世界中の公開サービスを目録化している。
止めるかどうかは自分の判断だが、何を止めているのかは知っておいたほうがいい。ついでに言うと、善意のクローラーは心配いらない。whitelist-good-actors という collection が既定で入っていて、Google、Bing、各社の SEO クローラーはすべてホワイトリストにある。しかも User-Agent を見るだけでなく しているので、なりすませない。
自分を三回締め出した#
ここが全体でいちばん覚えておく価値のある部分だ。自動で遮断するシステムは、遅かれ早かれ自分自身を遮断する。
SSH を鍵認証に変えてパスワードを無効化した。ところが LAN 側の開発機がまだ古いやり方で再試行し続けて失敗していたので、ssh-bf シナリオがブルートフォースと判定し、bouncer がその内部 IP をまるごと drop した。
症状が妙だった。LAN からはまったく繋がらないのに、外から回り込むと普通に繋がる。外部経路の送信元 IP が別だからだ。
ファイルアップロード機能をテストしていて、その handler にバグがあった。アップロードするとマシン全体が固まる。固まったので、こちらのパソコンが SSH と HTTPS を狂ったように再接続し始めた。CrowdSec から見えたのは「ある IP が短時間に叩きまくっている」という光景で、攻撃と判定して BAN した。
そのときの画面はこうだ。スマホからの SSH は入れるのに、パソコンはどこにも繋がらず、サイトも全部開かない。スマホはモバイル回線で、別の IP だったからである。
同じ日の少しあとにもう一度やられた。この仕組みは単独で説明する価値がある。cscli decisions list を見ると:
Ip:<こちらの出口IP> crowdsecurity/nginx-req-limit-exceeded ban 6 eventsnginx-req-limit-exceeded というシナリオがやっているのは、nginx が 429 を返すのを見て、その IP は流量を浴びせていると判定することだ。そしてその 429 は、自分で設定した nginx のレート制限が出したものである。
つまりループはこうなる。
自分で入れた防御が二つ繋がって、こちらに噛みついてきた。
三つの兆候ですぐ判別できる#
CrowdSec に遮断されているのと、サービスが落ちているのとでは、症状が実際に違う。
| 兆候 | 意味 |
|---|---|
connection refused ではなく i/o timeout | パケットが沈黙のうちに破棄されている。ファイアウォールの DROP の特徴で、サービスが落ちているなら refused になる |
| スマホは正常、パソコンだけ全滅 | 送信元 IP が別なので、問題はサーバーではなく「あなたのその回線」にある |
| 画像や CSS すら読み込めない | IP がまるごと遮断されている。特定の API が壊れているのとは違う |
三つが同時に成り立つなら、ほぼ直行で cscli decisions list を開いて自分を探してよい。
直し方は二層ある#
LAN には whitelist parser を使う。s02-enrich に parser を一つ置いて、RFC1918 全体を永久に BAN されないようにする。
# /etc/crowdsec/parsers/s02-enrich/lan-whitelist.yaml
name: koimsurai/lan-whitelist
description: "Never ban LAN / RFC1918"
whitelist:
reason: "private LAN"
cidr:
- "192.168.0.0/16"
- "10.0.0.0/8"
- "172.16.0.0/12"このルールはこれまでに 16037 回通過させている。無かったらどれだけ面倒かが想像できる数字だ。
公開側の出口 IP には allowlist を使う。LAN のホワイトリストは「外出中」を救えない。そのときの送信元は公開 IP だからだ。
sudo cscli allowlists create home-dev -d "自分の開発機の IP"
sudo cscli allowlists add home-dev <あなたの出口IP>
sudo cscli allowlists inspect home-devWARNING
シナリオを一つずつ緩めるより allowlist のほうがよい
nginx-req-limit-exceeded はあらゆる nginx の 429 で発火する。どのサイトの、どの制限ルールが出したかに関係なくだ。制限を一つずつ調整するのはモグラ叩きで、allowlist なら自分の IP を一度に免疫化してループを断ち切れる。
代償は理解しておくこと。allowlist に載った IP には、そもそも遮断の決定が作られない。だから本当に信頼できる送信元だけを載せること。そして家庭用の動的 IP は再接続のたびに変わるので、載せても意味がない。
Discord で自分を出す#
三回に共通する厄介さはこれだ。BAN を解除するには、まず入れなければならない。そしてこちらが遮断されているとき、まさに繋がらないのが SSH である。
当時の代替手段はスマホだった。 で繋ぐ。普段はかなり気に入っていて、必要なものは一通り揃っている。だが外出先で、片手で、タッチキーボードで sudo cscli decisions delete --ip ... のようなコマンドを打ち、しかもその前に表全体から自分の IP を見分ける必要がある。体験としてはとても良いとは言えない。
さらに事態を面倒にすることがもう一つある。CrowdSec は誰を BAN したか既定では通知してくれない。profiles.yaml の notifications の行はすべてコメントアウトされていて、slack / http / email のプラグインを自分で繋がない限り何も来ない。だから「自分に遮断された」という事実が向こうから知らされることはない。繋がらないと気づいて、そこから推測が始まる。
# /etc/crowdsec/profiles.yaml —— 既定はこう。通知は全部コメント
decisions:
- type: ban
duration: 4h
# notifications:
# - slack_default
# - http_defaultそこで既存の Discord bot にコマンドを二つ足し、「調べる」と「解除する」をまとめて片付けることにした。だがここに構成上の問題がある。bot はコンテナの中で動いていて、CrowdSec はホストのサービスなので、コンテナからホストの cscli には触れない。
docker socket やホストの root 権限をコンテナに渡せば解決するが、それは外向きに繋がる bot にマシン全体の制御権を与えるのと同じだ。というわけでファイルキューにした。
コンテナにできるのはリクエストのファイルをキューに置くことだけだ。実際に cscli を実行するのはホスト側で root として常駐する watcher で、リクエストを読み、検証し、実行し、結果を書き戻す。仮に bot が破られても、攻撃者にできるのはリクエストファイルを置くことだけで、しかも動作は限定されており、CrowdSec の制御権には届かない。
watcher の要となる数行:
ALLOWLIST = "parole" # 解除したあと、この allowlist にも入れておく
ALLOWLIST_TTL = "4h" # 4 時間だけ。恒久的な通行証にはしない
def valid_ip(s):
try:
ipaddress.ip_address(s) # command injection を防ぐ
return True
except ValueError:
return False
if action == "unblock":
ip = str(req.get("ip", ""))
if not valid_ip(ip):
return {"ok": False, "output": f"invalid ip: {ip}"}
rc, out = run(["decisions", "delete", "--ip", ip])
ensure_allowlist()
run(["allowlists", "add", ALLOWLIST, ip, "-e", ALLOWLIST_TTL])設計上の細部が三つ。
この文字列は subprocess に渡される。リスト形式なのでシェルは経由しないが、検証していないフィールドはいずれ事故になる。ipaddress.ip_address() の一行で塞げる。
遮断の決定を消すだけだと、それを引き起こした振る舞いはたいてい続いている(たとえばまだ更新を連打している)ので、数秒後にまた BAN される。そこで解除のついでに parole という allowlist に IP を入れる。ただし 4 時間だけで、恒久的には通さない。
cog は interaction.user.id == OWNER_ID を確認し、しかも OWNER_ID が未設定のときの既定は全員を拒否であって、全員を通すではない。コマンドの返信はすべて ephemeral なので、遮断リストがチャンネルに残ることもない。
出来上がったあとは、/crowdsec_list が現在の遮断リストをそのままスマホに広げてくれる。各行に IP、発火したシナリオ、残り時間が入っている。
自分の一行を見つけたら、/crowdsec_unblock に IP を入れれば解除される。返信が parole を何時間与えたかも教えてくれる。
このリストの IP がついでに示していることがある。http-cve-2021-41773、http-wordpress-scan、http-sensitive-files はどれも先ほどの統計に出てきた顔ぶれで、AS を引くとまた Google Cloud と Azure である。例示のために取ってきたこの一群でさえ、やはりクラウドのデータセンターで動くスキャナーなのだ。
そして全体を成立させている決め手は、実に単純である。bot のほうから Discord へ繋ぎに行っている。IP がファイアウォールで DROP されて塞がるのは「入ってくる」方向であり、bot が既に張っている外向きの接続にはまったく影響しない。だから締め出されている最中でも、こちらの指令はちゃんと届く。
自分のマシンに立てた web 管理画面ではこれができない。使うためには繋がる必要があり、その繋がることこそができないからだ。スマホの SSH は遮断を迂回できる(モバイル回線は別の IP だ)が、それでもスマホで完全なコマンドを打つ必要は残る。
最初に戻る:Discord は静かになったのか#
静かになった。ただし理由は、当初やろうとしていたこととは違う。
CrowdSec を入れるのと並行して、netdata の警告を黙らせる設定ファイルも用意してあった。4xx のグループを silent に回し、5xx と「遅くなった」だけを残すつもりだった。書き上げてコマンドも準備済みだった。
ところが、いま覗いてみたら:
$ ls /etc/netdata/health.d/
(空)
$ ls ~/Server/netdata-web-noise-silence.conf
-rw-rw-r-- 1 timo9378 timo9378 2922 netdata-web-noise-silence.confその消音ファイルは一度も配置されていなかった。まだホームディレクトリに転がっている。そして確かに、4xx の警告に長いこと悩まされていない。
理由は、4xx が勝手に消えたからだ。いま同じ access log から 6038 行を抜いて数えるとこうなる。
スキャナーは nginx に届く前に bouncer がファイアウォール層で drop しているので、その 404 は log に一行も残らない。4xx の比率は自然と閾値の下に収まり、警告は発火しない。
この結果は当初やろうとしていたことよりずっと良い。消音は計器を覆うことであり、これは本当に何も起きていないということだ。もし先に消音ファイルを配置していたら、いま本当にスキャンの波が来ても netdata は黙ったままで、有効な信号を一つ失っていた。実際には、鳴ったときはたいてい本当に何かある。冒頭のあの一枚のように、あれは自分の Spotify API がループしていた。
TIP
消音を考える前に源を断つ 警告がうるさいとき、最初の直感はたいてい閾値をいじるか消音するかだ。だがうるさい理由が「本当に大量の悪いリクエストが来ている」ことなら、源を取り除けば警告も一緒に解決し、しかも警告の判別力は保たれる。消音は「その警告がそもそも存在すべきでない」場合のためのものである。
fail2ban はその後どうなったか#
正直に言うと、きちんと引退させてはいない。CrowdSec が動き出したあとは構わなくなり、あとになって気まずい状態で止まっているのに気づいた。サービスは動いておらず、設定ファイルは残ったままだ。
$ systemctl is-active fail2ban
inactive
$ systemctl is-enabled fail2ban
disabled止まった経緯も少し笑える。LAN のホワイトリストを足すために jail.d/ へ一つファイルを置いたのだが、そのファイルのせいで起動時の解析に失敗し、それきり上がってこなくなった。そして気づかなかった。その期間ずっと CrowdSec が遮断していたので、症状が何一つ出なかったからだ。
ここでの教訓は「fail2ban が良くない」ではない。こうだ。機能の重なる防御を二つ並べたとき、いちばん危険なのは衝突ではなく、両方が見張っていると思い込むことである。あのとき止まったのが CrowdSec だったら、こちらもきっと同じくらい長く気づかなかっただろう。乗り換えるなら綺麗に乗り換え、そして新しいほうが本当に動いていることを確認する。
一枚のチェックリスト#
CrowdSec 自宅運用チェックリスト
- インストール直後に
cscli version。ESM から古い版を掴んでもエラーは出ないが、以降の問題はすべてそこから始まる。 - メジャーバージョンを跨いだら切れた symlink を確認:
sudo find /etc/crowdsec -xtype l -deleteのあとcscli hub update+collections install --force。 - 解析の検証は
cscli explainで。cscli metricsの比率だけを見ない。あれは累計値で、reloadでは消えずrestartが要る。 - SSH の取り込みは journalctl だけ。auth.log も同時に収集すると失敗が二重に数えられる。
- 初日に LAN の whitelist parser を設定する。でないと SSH のパスワードを切るような操作で、自分の内部ネットワークから締め出されやすい。
- 公開側の出口 IP は allowlist に入れる。LAN のホワイトリストは外出中を救えない。
nginx-req-limit-exceededに注意。自分の 429 が遮断装置に餌を与えるので、更新の連打で自分が BAN される。- 自分に遮断されたかの見分け方:refused ではなく
i/o timeout、スマホは正常でパソコンだけ不通、静的ファイルすら読めない。 - SSH に依存しない解除経路を用意する。しかも「自分から外へ繋ぎに行く」構成でなければ意味がない。
- CrowdSec は既定では通知しない。
profiles.yamlの notifications はコメントアウトされているので、欲しければ自分で繋ぐ。 - 警告がうるさいなら消音より先に源を断つ。スキャナーを遮断すれば 4xx は勝手に消える。消音は計器を覆うだけだ。
- 古いほうを止めるなら綺麗に止める。半死半生のサービスを残して二重の保険があると勘違いしないこと。
- CrowdSec —— 公式ドキュメントdocs.crowdsec.net
- CrowdSec Hub —— シナリオ、parser、collection の一覧app.crowdsec.net/hub
- CrowdSec —— firewall bouncer(nftables / iptables)GitHub
- fail2ban —— 公式プロジェクトGitHub
- netdata —— ヘルス警告の設定learn.netdata.cloud
- Censys —— インターネット調査スキャナーの説明と除外方法about.censys.io
まだコメントがありません
✨ 最初のコメントを残しませんか