SSHへのアクセスを国内に限定する方法



●概要

 ※CentOS 8からTCP Wrapperは使用できなくなりました。

 参考URL:hosts.allow で sshd への接続を日本国内のみに制限する

 海外から自宅サーバのSSHへの不正アクセスが多く、ルータで一つ一つ拒否設定を設定していく作業が煩わしい。
 このため、自宅サーバのSSHへのアクセスは原則国内限定とし、更に国内から不正アクセス検知時のみルータで拒否設定する方針に変更したいと思いました。
 国内限定するにしても、そのネットワークアドレスは非常に多く、/etc/hosts.allow、/etc/hosts.denyを利用することとします。
 これまで利用してきた、firewalld、denyhosts、fail2ban等はそのまま利用します。

●設定(Rocky 8、Rocky 9)

 参考URL:システムエクスプレス株式会社
 参考URL:ipsetで大量のBlock指定を行う

 国内のIPアドレスのリストを作成します。
# mkdir /root/iptables

# vi /root/iptables/jpip_setup.sh
#!/bin/bash
COUNTRYLIST='JP'
curl -s -o /root/iptables/delegated-apnic-latest http://ftp.apnic.net/stats/apnic/delegated-apnic-latest
:> /root/iptables/jpip
for country in $COUNTRYLIST
do
	for ip in `cat /root/iptables/delegated-apnic-latest | grep "apnic|$country|ipv4|"`
	do
		COUNTRY=`echo $ip | awk -F"|" '{ print $2 }'`
		IPADDR=`echo $ip | awk -F"|" '{ print $4 }'`
		TMPCIDR=`echo $ip | awk -F"|" '{ print $5 }'`

		FLTCIDR=32
		while [ $TMPCIDR -ne 1 ];
		do
			TMPCIDR=$((TMPCIDR/2))
			FLTCIDR=$((FLTCIDR-1))
		done
		echo "$IPADDR/$FLTCIDR" >> /root/iptables/jpip
	done
done

# chmod 700 /root/iptables/jpip_setup.sh
# /root/iptables/jpip_setup.sh
 firewallを設定します。
# domestic(国内)というゾーンを作成します。
firewall-cmd --permanent --new-zone=domestic
# domestic という ipset を作成して type を hash:net にします。
firewall-cmd --permanent --new-ipset=domestic --type=hash:net
# 上記で作った国内のIPリストファイルをこの ipset に読み込みます。
firewall-cmd --permanent --ipset=domestic --add-entries-from-file=/root/iptables/jpip
# この ipset に国内IPリストが読み込まれているか確認します。
firewall-cmd --permanent --info-ipset=domestic
# 読み込まれた ipset をこのゾーンに適用します。
firewall-cmd --permanent --zone=domestic --add-source=ipset:domestic
# publicゾーンからSSHサービスを削除します。
# firewall-cmd --permanent --zone=public --remove-service=ssh
# domesticゾーンにSSHサービスを追加します。
# firewall-cmd --permanent --zone=domestic --add-service=ssh
firewalld を reload します。
# firewall-cmd --reload
 以上でsshdへの接続は日本国内のみに制限されました。

 ipsetの再適用

 現在適用されたipsetを削除するには下記のようにします。
# firewall-cmd --permanent --ipset=domestic --remove-entries-from-file=/root/iptables/jpip
# firewall-cmd --reload
# ipset --list domestic
Name: domestic
Type: hash:net
Revision: 6
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 376
References: 0
Number of entries: 0
Members:
 エントリー数が「0」であれば、(問題なく)全て削除されています。

 ただし、削除するエントリーのファイル名が同じでも追加した時の内容と異なっている場合、削除しても下記のようにエントリーが残ってしまうことがあります。
# firewall-cmd --permanent --ipset=domestic --add-entries-from-file=/root/iptables/jpip_local_ip ← 故意に、ローカルネットワークが余計に追加されているセットを適用
# firewall-cmd --reload

プライベートアドレスが記載されていないエントリーファイルでipsetを削除
# firewall-cmd --permanent --ipset=domestic --remove-entries-from-file=/root/iptables/jpip
# firewall-cmd --reload
# ipset --list domestic
Name: domestic
Type: hash:net
Revision: 6
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 568
References: 0
Number of entries: 3
Members:
10.0.0.0/8
172.16.0.0/12
192.168.0.0/16
 このように新たなエントリーファイルを再適用するには注意して対応する必要があります。

 Rocky 9における、インターネット側からhttp及びhttpsでアクセスできない現象への対応

 CentOS Stream 8、Rocky 8では起きていなかった現象がRocky 9で発生しました。
 tcpdumpで調べると(157.65.27.0/24は、今回ポート開放確認のために使用したサイト
# tcpdump -i eno1 net 157.65.27.0 mask 255.255.255.0
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eno1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
17:39:02.885522 IP 157-65-27-7.vpscloud.static.arena.ne.jp.49354 > hostA.http: Flags [S], seq 894457425, win 29200, options [mss 1420,sackOK,TS val 232270987 ecr 0,nop,wscale 7], length 0
17:39:02.885625 IP hostA > 157-65-27-7.vpscloud.static.arena.ne.jp: ICMP host hostA unreachable - admin prohibited filter, length 68
のようになり、デフォルトでフィルターがかかっているっぽい。

 そこで、nftコマンドでフィルタールールを調べてみます。
・CentOS Stream 8
# nft list ruleset | grep -n reject
214:		reject with icmpx admin-prohibited
222:		ip6 daddr { ::/96, ::ffff:0.0.0.0/96, 2002::/24, 2002:a00::/24, 2002:7f00::/24, 2002:a9fe::/32, 2002:ac10::/28, 2002:c0a8::/32, 2002:e000::/19 } reject with icmpv6 addr-unreachable
230:		reject with icmpx admin-prohibited
237:		ip6 daddr { ::/96, ::ffff:0.0.0.0/96, 2002::/24, 2002:a00::/24, 2002:7f00::/24, 2002:a9fe::/32, 2002:ac10::/28, 2002:c0a8::/32, 2002:e000::/19 } reject with icmpv6 addr-unreachable

・Rocky Linux 9
# nft list ruleset | grep -n reject
2350:		reject with icmpx admin-prohibited
2359:		ip6 daddr { ::/96, ::ffff:0.0.0.0/96, 2002::/24, 2002:a00::/24, 2002:7f00::/24, 2002:a9fe::/32, 2002:ac10::/28, 2002:c0a8::/32, 2002:e000::/19 } reject with icmpv6 addr-unreachable
2361:		reject with icmpx admin-prohibited
2368:		ip6 daddr { ::/96, ::ffff:0.0.0.0/96, 2002::/24, 2002:a00::/24, 2002:7f00::/24, 2002:a9fe::/32, 2002:ac10::/28, 2002:c0a8::/32, 2002:e000::/19 } reject with icmpv6 addr-unreachable
2415:		reject with icmpx admin-prohibited
2467:		reject with icmpx admin-prohibited
2544:		reject with icmpx admin-prohibited
2626:		reject with icmpx admin-prohibited
 明らかに違いがあります。

 いろいろ調べた結果(firewalldを停止すると接続できるようになる)、domesticゾーンによる影響でした。

 イントラネットからでもアクセスできるようにpublicゾーンに設定済みの、http及びhttpsはそのまま残しておきます。
(残しておかないと、イントラでの接続が出来なくなります。)
# firewall-cmd --permanent --zone=domestic --add-service={http,https}
# firewall-cmd --reload
# firewall-cmd --list-all --zone=domestic
   :
    services: http https ssh
   :
 このように対応することで、インターネット側から接続できるようになりました。

●設定(CentOS 7)

 複数のCIDRブロックを集約 (aggregation) するため、aggregateをyumでインストールします。
# yum install -y aggregate
 各ファイルを格納するためのディレクトリを作成します。
# mkdir /root/countryfilter
# cd /root/countryfilter
 
# vi conv.pl
#!/bin/perl

use strict;

while (<STDIN>) {
    chomp;
    my ($registry, $cc, $type, $start, $value, undef, $status) = split(/\|/);

    unless ($type eq 'ipv4' && ($status eq 'allocated' || $status eq 'assigned')) { next; }
    #unless ($cc eq 'JP') { next; }

    my $SubnetMaskBin = sprintf('%b', scalar($value));
    if ($SubnetMaskBin !~ /^1(0+)$/) {
        # CIDR 表記できない
        next;
    }
    my $prefix = 32 - length($1);

    my $num;
    $num = ($num << 8) + $_ foreach (split(/\./, $start));

    if (($num % $value) != 0) {
        # 分割しなければならない
        next;
    }

    print $start.'/'.$prefix."\t".$cc."\n";
}
while (<STDIN>) {
    chomp;
    my ($registry, $cc, $type, $start, $value, undef, $status) = split(/\|/);

    unless ($type eq 'ipv4' && ($status eq 'allocated' || $status eq 'assigned')) { next; }
    #unless ($cc eq 'JP') { next; }

    my $SubnetMaskBin = sprintf('%b', scalar($value));
    if ($SubnetMaskBin !~ /^1(0+)$/) {
        # CIDR 表記できない
        next;
    }
    my $prefix = 32 - length($1);

    my $num;
    $num = ($num << 8) + $_ foreach (split(/\./, $start));

    if (($num % $value) != 0) {
        # 分割しなければならない
        next;
    }

    print $start.'/'.$prefix."\t".$cc."\n";
}
 日本国内のIPアドレスリスト(ip.txt)を生成してhosts.allowに書き込むシェルスクリプト(update.sh)を作成します。
# vi update.sh
R=$(cd $(dirname $0); pwd)
cd $DIR

# aggregate がインストールされているかチェック
aggregate=`which aggregate 2> /dev/null | wc -l`
if [ $aggregate -ne 1 ]; then
        echo "aggregate: command not found" >&2
        exit 1
fi

# 各 RIP から IP アドレスの割当リストを取得
rm -f ./delegated-*
wget ftp://ftp.arin.net/pub/stats/arin/delegated-arin-latest > /dev/null 2>&1
wget ftp://ftp.ripe.net/pub/stats/ripencc/delegated-ripencc-latest > /dev/null 2>&1
wget ftp://ftp.apnic.net/pub/stats/apnic/delegated-apnic-latest > /dev/null 2>&1
wget ftp://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-latest > /dev/null 2>&1
wget ftp://ftp.afrinic.net/pub/stats/afrinic/delegated-afrinic-latest > /dev/null 2>&1

if [ ! -f delegated-apnic-latest ]; then
        echo "delegated-apnic-latest was not found." >&2
        exit 1
fi

# 日本国内のもののみを取り出して集約
cat delegated-* | perl ./countryfilter/conv.pl | grep -E 'JP$' | sort -n | aggregate -q > jp.txt

# CIDR 表記をサブネットマスク表記に変換
sed -i 's/\/8$/\/255.0.0.0 /' ./jp.txt
sed -i 's/\/9$/\/255.128.0.0 /' ./jp.txt
sed -i 's/\/10$/\/255.192.0.0 /' ./jp.txt
sed -i 's/\/11$/\/255.224.0.0 /' ./jp.txt
sed -i 's/\/12$/\/255.240.0.0 /' ./jp.txt
sed -i 's/\/13$/\/255.248.0.0 /' ./jp.txt
sed -i 's/\/14$/\/255.252.0.0 /' ./jp.txt
sed -i 's/\/15$/\/255.254.0.0 /' ./jp.txt
sed -i 's/\/16$/\/255.255.0.0 /' ./jp.txt
sed -i 's/\/17$/\/255.255.128.0 /' ./jp.txt
sed -i 's/\/18$/\/255.255.192.0 /' ./jp.txt
sed -i 's/\/19$/\/255.255.224.0 /' ./jp.txt
sed -i 's/\/20$/\/255.255.240.0 /' ./jp.txt
sed -i 's/\/21$/\/255.255.248.0 /' ./jp.txt
sed -i 's/\/22$/\/255.255.252.0 /' ./jp.txt
sed -i 's/\/23$/\/255.255.254.0 /' ./jp.txt
sed -i 's/\/24$/\/255.255.255.0 /' ./jp.txt
sed -i 's/\/25$/\/255.255.255.128 /' ./jp.txt
sed -i 's/\/26$/\/255.255.255.192 /' ./jp.txt
sed -i 's/\/27$/\/255.255.255.224 /' ./jp.txt
sed -i 's/\/28$/\/255.255.255.240 /' ./jp.txt

# /etc/hosts.allow をバックアップ
NOW_TIME=`date +%Y%m%d_%H%M%S`
cp -p /etc/hosts.allow /etc/hosts.allow_bk${NOW_TIME}

# /etc/hosts.allow をリセット
sed -i -e '/#countryfilter/d' /etc/hosts.allow

# 日本国内の IP アドレスリストを書込
cat ./jp.txt | awk '{print "sshd: "$1" #countryfilter"}' >> /etc/hosts.allow
 hosts.denyを編集して、sshdへのアクセスを原則禁止とします。
# vi /etc/hosts.deny
sshd: ALL
 ※ sshで作業している場合、この時点で切断すると再接続できなくなるので注意してください。

 ローカルホストからのアクセスは許可するために、hosts.allowの先頭に次の1行を追加します。
# vi /etc/hosts.allow
ALL: 127.0.0.1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, (その他のNWアドレス等)
  :
 作成したシェルスクリプトに実行権を与えて、実行します。
# chmod 700 update.sh
# ./update.sh
 cronで一週間ごとに実行するように設定します。
# ln -s /root/countryfilter/update.sh /etc/cron.weekly/
 以上でsshdへの接続は日本国内のみに制限されました。