FedoraでIPsetとfirewalldを活用する:O(1)の低遅延で数万件の不正IPを遮断する実践ガイド

Linuxにおける大量IPアドレス遮断手法の比較

ボットネットからSSHへの猛烈な総当たり攻撃を受けた際、多くのシステム管理者が最初にとる行動は、ターミナルを開いてIPアドレスを1つずつ手動で遮断することでしょう。攻撃元が数十件程度であればこの方法でも通用しますが、ブラックリストが数万件規模に達すると、サーバーのリソースは瞬く間に枯渇してしまいます。

実際の運用現場では、通常以下の3つのアプローチが検討されます。

  • 手法1: リッチルールや従来のiptablesを用いた個別ルールの順次追加
    IPアドレスやサブネットごとに個別のルールを定義します(例: firewall-cmd --add-rich-rule='rule family="ipv4" source address="x.x.x.x" drop')。
  • 手法2: リバースプロキシ層(Nginx、Caddy、WAFなど)での遮断
    Webサーバーのgeoip2モジュールなどを利用し、TLS/HTTPハンドシェイクの段階で接続を切断します。
  • 手法3: カーネル層のIPsetを活用する手法
    すべてのIPリストをカーネル空間のハッシュテーブルに集約します。ファイアウォール側では単一の照合ルールを実行するだけで済みます。

実環境でのパフォーマンス比較:3つの手法を徹底検証

それぞれのアプローチは、ネットワークスタック内でのパケット処理メカニズムが根本的に異なります。

評価項目 個別ルールの順次評価 (firewalld/iptables) Webサーバー層 (Nginx GeoIP) IPset + firewalld (カーネルハッシュ)
検索の計算量 O(N) – 線形探索 O(log N) – 木構造探索 O(1) – ハッシュテーブル
大規模リスト時のCPU負荷 2,000ルールを超えると急上昇 中程度(ただしワーカープロセスのリソースを消費) ほぼゼロ、極めて安定
保護範囲 すべてのポート(SSH、DB、Webなど) HTTP/HTTPSのみ すべてのネットワークポート(L3/L4)
リストの反映時間 非常に遅い(リロードに数十秒〜数分) 高速(設定リロード時) ほぼ瞬時(数ミリ秒)

個別ルールを順次追加していく手法の致命的な弱点は、計算量がO(N)である点です。ブラックリストが30,000行ある状況を想像してみてください。正当なユーザーからのパケットであっても、通過を許可される前に最大30,000回の比較処理を1つずつ経由しなければなりません。これによりサーバーのCPU使用率は90〜100%まで跳ね上がり、ネットワーク遅延や広範なパケットロスを引き起こします。

NginxはWebトラフィックの処理に非常に優れています。しかし、SSH(ポート22)やPostgreSQL(ポート5432)などの他のポートは無防備なままです。IPsetをファイアウォールと直接連携させることこそが、カーネルの入口で攻撃を完全に遮断できる唯一の方法です。

最適な構成:FedoraのネイティブIPset機能を活用

Fedoraは以前からfirewalldのデフォルトバックエンドとしてnftablesを採用しています。これにより、従来のiptables向けの独立したipsetパッケージを追加インストールすることなく、firewall-cmdコマンドから直接IPsetを作成・管理できます。

実装手順はシンプルな3つのステップで構成されます。

  1. firewalld上にhash:netタイプのIPsetを作成する。
  2. このIPsetをdropゾーンに割り当て、パケットをサイレントに破棄する。
  3. IPdenyからCIDRブロックを自動取得し、定期的にハッシュテーブルへ投入するスクリプトを設定する。

Fedoraにおける詳細な構築手順

ステップ1: firewalldでIPsetを作成

まず、firewalldが正常に稼働していることを確認します。

sudo systemctl status firewalld

次に、blacklist-countryという名前でhash:netタイプのハッシュテーブルを作成します。このデータ型を使用すると、192.168.1.0/24や10.0.0.0/8といったCIDR形式のネットワークブロックを格納できます。

# IPv4向けに最大200,000件のネットワーク範囲を収容可能な新規ipsetを作成
sudo firewall-cmd --permanent --new-ipset=blacklist-country --type=hash:net --option=family=inet --option=hashsize=4096 --option=maxelem=200000

# 設定を再読み込みしてfirewalldに新しいsetを認識させる
sudo firewall-cmd --reload

maxelem=200000の設定値は、ボットネット発信元の多い数カ国分のIPレンジ全体を格納するのに十分な容量であり、メモリ枯渇の心配もありません。

ステップ2: IPsetをdropゾーンへ割り当て

最も確実な対策は、このIPsetに該当するすべてのトラフィックをdropゾーンに割り当てることです。リスト内のIPからパケットが到達すると、カーネルは何の応答も返さずにパケットを破棄します。サーバー側からRSTパケットやICMPエラー通知を返送しないため、アウトバウンド帯域の浪費を極限まで抑えることができます。

# set内のすべてのIPトラフィックをdropゾーンへルーティング
sudo firewall-cmd --permanent --zone=drop --add-source=ipset:blacklist-country

# 設定を即座に適用
sudo firewall-cmd --reload

dropゾーンの送信元設定を確認し、正しく割り当てられているかを検証します。

sudo firewall-cmd --zone=drop --list-sources

出力結果にipset:blacklist-countryと表示されれば、設定が有効化されています。

ステップ3: GeoIPリスト自動取得スクリプトの作成

IPdenyサービスでは、国別のCIDRリストがプレーンテキスト形式で無償提供されています。例えば、SSHブルートフォース攻撃が頻発している国・地域のIPブロックを取得できます。

システム保守用スクリプトを配置するための専用ディレクトリを作成します。

sudo mkdir -p /opt/firewall-scripts
sudo nano /opt/firewall-scripts/update-geoip.sh

自動取得・更新を行うスクリプトの内容は以下の通りです。

#!/usr/bin/env bash
set -euo pipefail

IPSET_NAME="blacklist-country"
TEMP_FILE="/tmp/zone_data.txt"
# 例: IPdenyからIPリストをダウンロード
URL="https://www.ipdeny.com/ipblocks/data/countries/cn.zone"

echo "[+] IPリストのダウンロードを開始..."
curl -sSL --fail "${URL}" -o "${TEMP_FILE}"

echo "[+] 既存リストをクリア中..."
# 不要なデータの肥大化を防ぐため既存のエントリを削除
sudo firewall-cmd --permanent --ipset="${IPSET_NAME}" --remove-entries-from-file="${TEMP_FILE}" 2>/dev/null || true

echo "[+] 新しいリストをカーネルへ登録中..."
sudo firewall-cmd --permanent --ipset="${IPSET_NAME}" --add-entries-from-file="${TEMP_FILE}"

echo "[+] ファイアウォールをリロード中..."
sudo firewall-cmd --reload

rm -f "${TEMP_FILE}"
echo "[+] IPsetの更新が完了しました!"

実行権限を付与し、スクリプトの動作テストを行います。

sudo chmod 750 /opt/firewall-scripts/update-geoip.sh
sudo /opt/firewall-scripts/update-geoip.sh

ステップ4: 動作検証と結果の確認

Fedoraのfirewalldはnftablesをバックエンドとして動作しているため、投入されたリストはfirewalldコマンドだけでなくnftコマンドからも直接確認できます。

# IPsetの構成情報とエントリ数を確認
sudo firewall-cmd --info-ipset=blacklist-country

# またはnftables内の実際のハッシュテーブルを確認
sudo nft list set inet firewalld blacklist-country | head -n 30

リソース消費の面では、20,000件のネットワーク範囲を保持してもカーネル空間で消費されるRAMはわずか2〜4MB程度です。メモリ1GBの低スペックなVPSであっても全く問題ありません。

特定のIPアドレスが実際にブロック対象に含まれているかをテストするには、以下のコマンドを実行します。

sudo firewall-cmd --ipset=blacklist-country --query-entry="1.2.4.0/24"

コマンドの返り値がyesであれば、このネットワーク範囲からのすべての接続が即座に遮断されます。

ステップ5: Systemd Timerによる自動更新の設定

インターネット上のIP割り当ては日々変動します。出力ログの追跡が煩雑になりがちなcrontabの代わりに、Fedoraではjournalctlでログを一元管理できるsystemdタイマーの利用が最適です。

/etc/systemd/system/update-geoip.serviceにサービスファイルを作成します。

[Unit]
Description=Firewalld用GeoIP IPsetの更新
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/opt/firewall-scripts/update-geoip.sh
StandardOutput=journal
StandardError=journal

続いて、毎週日曜日の午前3時に自動実行されるよう/etc/systemd/system/update-geoip.timerにタイマーファイルを作成します。

[Unit]
Description=毎週のIPset更新トリガー

[Timer]
OnCalendar=Sun *-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target

タイマーをリロードして有効化します。

sudo systemctl daemon-reload
sudo systemctl enable --now update-geoip.timer

# 次回実行スケジュールの確認
sudo systemctl list-timers update-geoip.timer

本番環境に導入する際の2つの注意点

  • 管理アクセスのロックアウト対策を徹底する: 自身の固定IPアドレスやVPNネットワーク(WireGuardやOpenVPNなど)は、必ず事前にtrustedゾーンへ追加しておきましょう。trustedゾーンのルールはdropゾーンよりも優先度が高いため、誤ってサーバーへの管理アクセスを遮断してしまう事故を防止できます。
  • 極端に巨大なリストの取り扱いに注意する: 一度に10万件以上のサブネットを登録する場合は、HTTPSでの通信を徹底し、curlコマンドのステータスコードを必ず検証してください。ダウンロードエラーや空白のHTMLエラーページをそのままファイアウォールに投入してしまい、スクリプトがクラッシュする事態を防ぎましょう。
Share: