問題:サーバーログがノイズだらけだった
以前、東南アジアと日本にユーザーベースを持つクライアントの本番サーバーを管理していたとき、ある朝アクセスログを確認すると、数千件ものSSH認証失敗、ポートスキャン、不審なエクスプロイト試行が記録されていた——しかもすべて、クライアントとは一切ビジネス関係のない国から来ていた。fail2banはすでに動いていたし、レートリミットも設定済みだった。それでもノイズは止まらず、ログ領域を圧迫してアラートを鳴らし続けた。
そこでファイアウォールレベルでのGeoIPブロッキングを実装した——アプリケーションスタックに到達する前に、特定国のIPレンジ全体からのトラフィックを遮断する手法だ。公開Linuxサーバーを運用しているなら、これはツールキットに加えるべき対策だ。私が実際に使っているセットアップを紹介する:ファイアウォールルールにnftables、国→IPマッピングにMaxMind GeoLite2を使う。
基本概念:実際に何をやるのか
nftablesとセット機能
nftablesはiptablesの後継として登場したモダンなフレームワークだ。Linuxカーネル3.13以降に組み込まれており、Ubuntu 22.04+、Debian 11+、および最近のほとんどのディストリビューションでデフォルトになっている。GeoIPフィルタリングに最適な理由は、インターバルサポート付きセットというネイティブ機能にある——数万件のCIDRレンジを名前付きセットに読み込み、たった1つのルールでそれらすべてにマッチングできる。ipsetの管理も追加デーモンも不要で、クリーンなnftables構文だけで完結する。
MaxMind GeoLite2
MaxMindはIPレンジを国にマッピングするデータベースを提供している。GeoLite2は無料枠で、アカウントを作成してライセンスキーを発行すればダウンロードできる。Countryデータベースは月2回更新される。すべてのVPN出口ノードを捕捉できるわけではないが、特定地域からの大量スキャントラフィックを削減するには十分な効果がある。
仕組みの全体像
- MaxMindからGeoLite2 Country MMDBデータベースをダウンロード
- Pythonスクリプトで対象国のIPレンジを抽出
- そのレンジをnftablesセットに読み込む
- セットにマッチするインバウンドトラフィックをdropするルールを書く
- cronで2週間ごとにデータベースを自動更新する
実際のセットアップ手順
ステップ1:GeoLite2データベースのダウンロード
maxmind.comでアカウントを作成し、無料ライセンスキーを取得する。その後、MMDBファイルをダウンロードする:
sudo apt install mmdb-bin wget -y
MAXMIND_KEY="YOUR_LICENSE_KEY_HERE"
wget -q "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=${MAXMIND_KEY}&suffix=tar.gz" \
-O /tmp/GeoLite2-Country.tar.gz
tar -xzf /tmp/GeoLite2-Country.tar.gz -C /tmp/
sudo mkdir -p /etc/maxmind
sudo cp /tmp/GeoLite2-Country_*/GeoLite2-Country.mmdb /etc/maxmind/
rm -rf /tmp/GeoLite2-Country*
ステップ2:国別IPレンジの抽出
MMDBファイルはバイナリ形式だ。特定国のCIDRレンジを取り出すには小さなPythonスクリプトが必要になる。まずライブラリをインストールする:
pip3 install maxminddb
#!/usr/bin/env python3
# /usr/local/bin/extract_country_ips.py
import maxminddb
import sys
MMDB_PATH = "/etc/maxmind/GeoLite2-Country.mmdb"
TARGET_COUNTRIES = set(sys.argv[1:]) # 例:CN RU KP
ranges = []
with maxminddb.open_database(MMDB_PATH) as db:
for record, network in db:
if record and "country" in record:
iso = record["country"].get("iso_code", "")
if iso in TARGET_COUNTRIES:
ranges.append(str(network))
for r in sorted(set(ranges)):
print(r)
sudo chmod +x /usr/local/bin/extract_country_ips.py
# テスト — CN + RU の合計は通常10,000〜13,000行になる
python3 /usr/local/bin/extract_country_ips.py CN RU | wc -l
ステップ3:nftables設定の作成
専用ファイルにベーステーブルとセットを定義する——管理しやすく、再読み込みも簡単だ:
sudo nano /etc/nftables-geoip.conf
#!/usr/sbin/nft -f
# GeoIPブロッキングテーブル
table inet geoip_filter {
set blocked_countries {
type ipv4_addr
flags interval
auto-merge
}
set blocked_countries_v6 {
type ipv6_addr
flags interval
auto-merge
}
chain input {
type filter hook input priority 0; policy accept;
# オプション:テスト中はdropの前にログを記録する
# ip saddr @blocked_countries log prefix "geoip-block: " drop
ip saddr @blocked_countries counter drop
ip6 saddr @blocked_countries_v6 counter drop
}
}
理解しておくべき2つのディレクティブ:flags intervalはセットが個別IPではなくCIDRレンジを保持することをnftablesに伝える。auto-mergeは重複するレンジを自動的に統合する。どちらも、数千の重複プレフィックスが存在しうる国レベルのデータセットを扱う際に不可欠だ。
ステップ4:更新スクリプトの作成
このスクリプトは必要に応じて新しいデータベースをダウンロードし、レンジを抽出して、ファイアウォールを完全再起動することなくnftablesセットをリロードする:
sudo nano /usr/local/bin/update-geoip-block.sh
#!/bin/bash
set -e
MMDB_PATH="/etc/maxmind/GeoLite2-Country.mmdb"
MAXMIND_KEY="${MAXMIND_LICENSE_KEY}"
BLOCKED_COUNTRIES="CN RU KP IR" # 必要に応じて変更する
# 15日以上古い場合はデータベースを更新する
if [ ! -f "$MMDB_PATH" ] || find "$MMDB_PATH" -mtime +15 | grep -q .; then
echo "[+] GeoLite2データベースを更新中..."
wget -q "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=${MAXMIND_KEY}&suffix=tar.gz" \
-O /tmp/GeoLite2.tar.gz
tar -xzf /tmp/GeoLite2.tar.gz -C /tmp/
cp /tmp/GeoLite2-Country_*/GeoLite2-Country.mmdb "$MMDB_PATH"
rm -rf /tmp/GeoLite2*
fi
echo "[+] IPレンジを抽出中: $BLOCKED_COUNTRIES"
python3 /usr/local/bin/extract_country_ips.py $BLOCKED_COUNTRIES > /tmp/blocked_ranges.txt
# IPv4とIPv6を分離する
IPV4=$(grep -v ':' /tmp/blocked_ranges.txt | paste -sd ',' -)
IPV6=$(grep ':' /tmp/blocked_ranges.txt | paste -sd ',' -)
# テーブルが存在しない場合はベース設定を読み込む
nft list table inet geoip_filter &>/dev/null || nft -f /etc/nftables-geoip.conf
# セットをフラッシュして再読み込みする
nft flush set inet geoip_filter blocked_countries 2>/dev/null || true
nft flush set inet geoip_filter blocked_countries_v6 2>/dev/null || true
[ -n "$IPV4" ] && nft add element inet geoip_filter blocked_countries "{ $IPV4 }"
[ -n "$IPV6" ] && nft add element inet geoip_filter blocked_countries_v6 "{ $IPV6 }"
TOTAL=$(wc -l < /tmp/blocked_ranges.txt)
echo "[+] 完了。${TOTAL}件のレンジを読み込みました。"
sudo chmod +x /usr/local/bin/update-geoip-block.sh
# ライセンスキーを安全に保存する
echo "MAXMIND_LICENSE_KEY=your_key_here" | sudo tee /etc/geoip.env
sudo chmod 600 /etc/geoip.env
ステップ5:実行と確認
# 初回実行
sudo bash -c 'source /etc/geoip.env && /usr/local/bin/update-geoip-block.sh'
# レンジが読み込まれたか確認する
sudo nft list set inet geoip_filter blocked_countries | head -5
# パケットカウンターを確認する
sudo nft list ruleset | grep counter
# 期待される出力:
# ip saddr @blocked_countries counter packets 1842 bytes 147360 drop
ステップ6:再起動後の永続化と自動更新
現在のルールセットを保存し、起動時にnftablesを有効化する。その後、再起動のたびにGeoIPセットをリロードするsystemdサービスを作成する:
# ベースルールを永続化する
sudo nft list ruleset > /etc/nftables.conf
sudo systemctl enable --now nftables
sudo nano /etc/systemd/system/geoip-reload.service
[Unit]
Description=GeoIP nftablesセットのリロード
After=nftables.service
Wants=nftables.service
[Service]
Type=oneshot
EnvironmentFile=/etc/geoip.env
ExecStart=/usr/local/bin/update-geoip-block.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now geoip-reload
2週間ごとのデータベース更新には、cronジョブを追加する:
sudo nano /etc/cron.d/geoip-update
MAXMIND_LICENSE_KEY=your_key_here
0 3 1,15 * * root /usr/local/bin/update-geoip-block.sh >> /var/log/geoip-update.log 2>&1
本番運用で得た教訓
5つの異なる本番環境にデプロイしてきた経験から、同じ落とし穴が繰り返し現れる。余計な苦労を避けるためにまとめておく:
- まず自分のIPをホワイトリストに追加する。ブロッキングルールを読み込む前に、管理用IPに対して明示的なacceptルールを追加すること。自国をブロックしてクラウドVPSからロックアウトされるのは、午後を台無しにする最悪な体験だ。
- 最初はdropではなくloggingから始める。新しいサーバーでは、最初の48時間は
dropをlog prefix "geoip-block: " dropに変えておく。本格的なブロックを確定する前に/var/log/kern.logを確認して予期しないものがないか調べる。 - 小さなVPSインスタンスではメモリに注意する。中国とロシアを同時にブロックすると、約10,000〜12,000件のIPレンジが読み込まれる。nftablesはこれを効率的に処理するが、512MB VPSでは初回読み込み時にわずかなメモリ増加が見られる。読み込み前後で
free -mを一度確認しておくといい。 - VPNはブラインドスポットだ。この手法は、同じ/24から延々とポート22を叩き続けるような単純な大量スキャンには効果的だ。しかしブロック対象外の国を経由するルーティングを使う意図的な攻撃者は完全に回避できる。GeoIPは多層防御の1つとして捉え、完全なソリューションとは思わないこと。fail2ban、SSH鍵認証、アプリケーションレベルのレートリミットと組み合わせて使う。
- 過剰なブロッキングは避ける。CDNエッジノード、死活監視サービス、サードパーティAPIはブロック対象地域を経由してくることがある。ノイズが最も多く関連性の最も低い国から始めて、徐々に拡大していく——逆順ではなく。
実際の効果
あるクライアントのサーバーでは、デプロイ後1週間以内にSSHブルートフォース試行が70%以上減少した。アクセスログは、まともに読めないノイズだらけの状態から、人間が5分でスキャンできるものに変わった。自動更新スクリプトは数ヶ月間、一度も失敗せずに動き続けている。
初期セットアップにかかる時間は1時間以下だ。その後はcronジョブとsystemdサービスがすべてを自動で処理する。リージョナルなユーザーを持つサーバーで、正規ユーザーがゼロの国から攻撃を受けているなら、これほどわずかな継続的手間でこれほどの静寂を手に入れられるファイアウォール設定は他にそうそうない。

