FreeBSD+PFで日本以外からのSSH接続をブロックする
Blocking IP addr except for my area with FreeBSD+PF
はじめに
現在、私の家では以前購入したWyse3040でファイアウォールサーバを構築・稼働させています。以前はログ収集などを目的に使用していましたが、紆余曲折あり現在の形に落ち着きました。
※以前のWyse3040のときの記事はこちらです
ファイアウォールとはいっても、そんな複雑な構成はしておらず、単純にポートの解放とL2スイッチのDHCP割当、Webサーバへのアクセス制御ぐらいでした。
一応Fail2banを稼働させており、それでSSHに接続しようとしてくる人たちをブロックするような形にしていました。ブロックをしても毎日平均200回〜500回ぐらいSSHアクセスを試みる人たちがいるという状況です。
このサーバは私しかアクセスしないため、地理的なアクセス制限を実装しても問題ないと判断し、実際に構築してみました。
今回はそのやり方について簡単に説明していきます。
地理的にSSH接続できるエリアを制限する
わたしは日本に住んでいる&かつ基本的に海外には行かないです。というか人生で一回も行ったことがないと思います。そのため、日本のIPのみアクセスを許可すれば、基本的に問題ないということになります。
日本のIPリストとして、APNIC(Asia Pacific Network Information Centre)からデータを取得、整形をしていきます。APNICはアジア太平洋地域インターネット番号資源管理機関で、主に一部のアジア地域のIPアドレスやAS番号の割り当てを管理している組織になっており、信頼性が高いものとなっています。
実際にAPNICからデータを取得していきます。
$ wget "http://ftp.apnic.net/stats/apnic/delegated-apnic-latest"
$ cat delegated-apnic-latest
apnic|CN|ipv6|240e:800::|21|20111214|allocated
apnic|CN|ipv6|240e:1000::|20|20181227|allocated
apnic|CN|ipv6|240e:2000::|19|20181227|allocated
apnic|JP|ipv6|240f::|24|20101224|allocated
apnic|JP|ipv6|240f:100::|24|20171027|allocated
apnic|CN|ipv6|240f:4000::|24|20190709|allocated
apnic|CN|ipv6|240f:8000::|24|20150427|allocated
apnic|CN|ipv6|240f:c000::|24|20190917|allocated
apnic|SG|ipv6|2410::|17|20241108|allocated
取得したdelegated-apnic-latestを日本のIPv4ブロックだけ抽出してCIDR形式に変換していきます。
ホスト数をCIDRのプレフィックス長に変換するので、以下のように変換をする必要があります。
256ホスト → /24
512ホスト → /23
1024ホスト → /22
4096ホスト → /20
8192ホスト → /19
変換をするための簡単なスクリプトがこちらです。
#!/bin/sh
grep '^apnic|JP|ipv4|' delegated-apnic-latest | while IFS='|' read -r registry cc type start value date status; do
case $value in
256) echo "$start/24" ;;
512) echo "$start/23" ;;
1024) echo "$start/22" ;;
2048) echo "$start/21" ;;
4096) echo "$start/20" ;;
8192) echo "$start/19" ;;
16384) echo "$start/18" ;;
65536) echo "$start/16" ;;
*) echo "$start/$(echo "32 - l($value)/l(2)" | bc -l | cut -d. -f1)" ;;
esac
done > japan-ips.txt
# IPv6の場合
grep '^apnic|JP|ipv6|' delegated-apnic-latest | while IFS='|' read -r registry cc type start value date status; do
echo "$start/$value"
done > japan-ips-v6.txt
データを確認して、以下のような形になっていれば成功です。
※IPv6での実装は今回しておりません
$ cat japan-ips.txt
223.216.0.0/13
223.223.0.0/16
223.223.160.0/22
223.223.164.0/22
223.223.208.0/21
223.223.224.0/19
223.252.64.0/19
223.252.112.0/20
それでは実際に、整形したjapan-ips.txtを使ってPFでアクセス制限をかけていきます。
table <japan_ips> persist file "/etc/japan-ips.txt"
# persist → システム再起動後もテーブル内容を保持
block in log quick on $ext_if proto tcp from !<japan_ips> to any port 22
# !<japan_ips> → 日本IPテーブルに含まれないIPアドレスすべて
# quick → 即座にブロックで以降のルールを反映しない
# log → ブロックされたパケットをログに
pass in on $ext_if proto tcp from <japan_ips> to port 22 flags S/SA keep state \
(max-src-conn 5, max-src-conn-rate 5/30, \
overload <ratelimit> flush global)
# from <japan_ips> → 日本IPテーブルからの接続のみ許可
# max-src-conn 5 → 同一IPから同時接続5個まで
# overload <ratelimit> → 制限を超えた場合は制限テーブルに追加してブロック
block in log quick on $ext_if proto tcp from ! to any port 22
↑これで、まず日本以外を遮断します。
pass in on $ext_if proto tcp from to port 22
↑その後、次に日本IPからの接続を許可するようにします。
quickが付いているため、日本以外でブロックされたパケットは次の評価まで進まないようになっています。
この設定により、海外IPからの攻撃パケットはTCP接続確立前にドロップするようになり、CPUなどの負荷軽減が見込めるようになります。
APNICのデータ定期取得
新規事業者への割り当てや既存割り当ての変更や統廃合などから、地理的IP制限を正しく機能させるためには、APNICデータの定期更新が必要になってきます。
そこでデータ取得・変換スクリプトを作成しました(IPv4のみ)。
取得・変換機能以外にも、簡単なバックアップ、PFテーブル更新機能などもつけています。
#!/bin/sh
# /usr/local/bin/update-japan-ips.sh
# APNIC公式データから日本のIPアドレス範囲を取得・変換
TARGETFILE="/etc/japan-ips.txt"
BACKUP_DIR="/var/backups"
DATE=$(date '+%Y%m%d_%H%M%S')
APNIC_URL="http://ftp.apnic.net/stats/apnic/delegated-apnic-latest"
TMPFILE="/tmp/delegated-apnic-latest"
JAPAN_BLOCKS="/tmp/japan-blocks.txt"
# バックアップ作成
if [ -f "$TARGETFILE" ]; then
cp "$TARGETFILE" "$BACKUP_DIR/japan-ips.txt.backup.$DATE"
fi
# APNICデータのダウンロード
fetch -o "$TMPFILE" "$APNIC_URL"
if [ $? -eq 0 ]; then
# ファイルサイズチェック
FILESIZE=$(stat -f%z "$TMPFILE" 2>/dev/null || echo 0)
if [ "$FILESIZE" -gt 100000 ]; then
grep '^apnic|JP|ipv4|' "$TMPFILE" | while IFS='|' read -r registry cc type start value date status; do
# Calc CIDR
case $value in
1) cidr=32 ;;
2) cidr=31 ;;
4) cidr=30 ;;
8) cidr=29 ;;
16) cidr=28 ;;
32) cidr=27 ;;
64) cidr=26 ;;
128) cidr=25 ;;
256) cidr=24 ;;
512) cidr=23 ;;
1024) cidr=22 ;;
2048) cidr=21 ;;
4096) cidr=20 ;;
8192) cidr=19 ;;
16384) cidr=18 ;;
32768) cidr=17 ;;
65536) cidr=16 ;;
131072) cidr=15 ;;
262144) cidr=14 ;;
524288) cidr=13 ;;
1048576) cidr=12 ;;
2097152) cidr=11 ;;
4194304) cidr=10 ;;
8388608) cidr=9 ;;
16777216) cidr=8 ;;
*)
# 上記以外は対数計算でCIDR算出
cidr=$(echo "32 - l($value)/l(2)" | bc -l | cut -d. -f1)
;;
esac
echo "$start/$cidr"
done > "$JAPAN_BLOCKS"
# 抽出結果の検証とPFテーブル更新
BLOCK_COUNT=$(wc -l < "$JAPAN_BLOCKS")
# データ数チェック
if [ "$BLOCK_COUNT" -gt 50 ]; then
mv "$JAPAN_BLOCKS" "$TARGETFILE"
# pfの更新
pfctl -t japan_ips -T replace -f "$TARGETFILE" 2>/dev/null
if [ $? -ne 0 ]; then
# 更新失敗時
if [ -f "$BACKUP_DIR/japan-ips.txt.backup.$DATE" ]; then
cp "$BACKUP_DIR/japan-ips.txt.backup.$DATE" "$TARGETFILE"
pfctl -t japan_ips -T replace -f "$TARGETFILE" 2>/dev/null
fi
fi
else
rm "$JAPAN_BLOCKS"
fi
rm "$TMPFILE"
else
rm "$TMPFILE"
fi
fi
# 30日以上の古いバックアップファイルの削除
find "$BACKUP_DIR" -name "japan-ips.txt.backup.*" -mtime +30 -delete 2>/dev/null
実行権限を与えて、クロンで定期的に回すようにしたら完成です。あとは自動的に更新をしてくれるようになります。
# chmod +x /usr/local/bin/update-japan-ips.sh
# crontab -e
0 2 * * 0 /usr/local/bin/update-japan-ips.sh
運用チェック
実際に正常に地理的にブロックされているか確認していきます。
今回は私が普段使っているPC(Arch Linux)にTORをインストールしてTORネットワーク経由で海外IPからの接続テストを行います
$ sudo pacman -S tor torsocks
$ sudo systemctl start tor
$ torsocks curl https://httpbin.org/ip
{
"origin": "37.114.50.142"
}
このIPアドレスはドイツのものなので、元の日本のIPアドレスから正常に変更されており、海外IPでのテスト環境が整ったことがわかります。
$ torsocks ssh -l hoge hogehoge.hoge-hoge.net -i id_ed25519_client
1756448417 ERROR torsocks[10366]: General SOCKS server failure (in socks5_recv_connect_reply() at socks5.c:527)
ssh: connect to host bokumin45.server-on.net port 22: Connection refused
海外IPからの接続が正常にブロックされています。これに対して通常の日本IPからのSSH接続では以下のように正常にアクセスできるため、地理的制限が適切に動作していることが確認できます。
# 日本からのアクセス
$ ssh -l hoge hogehoge.hoge-hoge.net -i id_ed25519_client
_ov
.,HH' #o
?&MM?.,oooo__ `MH\
|R6M&RMH&9MMMMHb. ,MMM|
|6MHMGHMHMM&M9MH6HMHHMM}
MHMHMHMH6MH6MRMHMMHMP'
iHSD6HMHMHH&MHMMMMMH'
oMHMMHMMHM$RM9MHM9M?'
-v_ |9&RHRMMHMHMHH96MMMM!
\_ "\:HHHDM9H&M&kM&6HMHMH
. `"qod' `?*MH&R6M6MRMMMH'
`+&oo$PHbd##|``H9HHHMHHM!
H9HMb\_d9MHH6M9M?
#&MHM9M&HH&MMHHMM,
"^*HHRM96M&M9MMML
`MRHMHHMMMRMM?
,RMHHMMMHMMMM,
H&RM&6MHMMHMk
HMHMH6MHMMMMb_
_H&H96RMR6M[*MM#o\_
,/:-:)&9&*#<MH9HHMHHHM, ""**HHHH#o\_.
>?:\?d?_:/v?ZMHHHHRMM9D `"""*HH#.
'''-\|?\RM&&##+*""` .db ._HMF
oo#HMHMMMMM?
`'"' '
====================================================
PRIVATE SYSTEM - AUTHORIZED ACCESS ONLY
====================================================
Enter passphrase for key 'id_ed25519_client':
おわりに
今回はAPNICのデータを活用してFreeBSD + PFで地理的なアクセス制限を実装してみました。この手法により、海外からの不正なSSH接続試行を大幅に削減でき、サーバのセキュリティ向上とリソース節約が期待できます。
しかし、大量のIPリストを処理しないといけない部分がパフォーマンスへ影響する可能性もあるので、注意が必要です。私の場合は基本的にIPv4を使用しているので、IPのリストをIPv4のみで絞っています。また、APNICのデータに完全に依存してしまう形になってしまうので、地理的ブロックに完全に依存せず、公開鍵を使ったりFail2banなどのシステムも稼働させて多層防御をすることで堅牢なサーバになると思います。
おわり