iptablesをシェルスクリプトで設定していて動作が遅い場合の対処法


投稿日:2015年12月2日
  • 1
  • 0
  • 9



数十分かかる設定を数秒で実行

iptables2

ファイヤーウォールでブロックすべきIPのリストは日々変動します。
そうした動的な環境に対応するために、iptablesの設定はシェルスクリプトを利用するのが一般的です。

またlogから攻撃を察知して即座に対応するには、コマンドで設定を変更するのが最適です。

その辺の詳しい設定方法は以下のページでまとめました。
iptablesの設定ファイルをシェルスクリプトを利用して動的に作成
Swatchでログを監視して、攻撃に合わせた対策を自動で実行する方法

上記の設定で問題なく運用していたのですが、登録するルールが多くなると「新しい設定が反映されるまで数十分アクセス不能になる」という点が気になっていました。
その対策のメモです。


ボトルネックの把握

まずは遅い原因を探ります。
上のリンクで紹介した方法では、3つのループが実行されていました。それぞれのループで書き出されるコマンドは以下のとおりです。

ブラックリスト「ip_deny」による書き出し「570行」。
カントリーコードJPのIPを許可するACCEPT_JP_FILTERによる書き出し「2361行」。
国単位でのブロックリストDROP_COUNTRY_FILTERによる書き出し「16036行」。

ぱっと見でDROP_COUNTRY_FILTERが一番重そうです。
そこで「test.sh」というファイルを作り時間を計測してみます。

# vi test.sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

SECONDS=0

# 特定の国からのアクセスを拒否する
# 中国、インド、ロシア
if [ -s /tmp/iplist ]; then
	iptables -N DROP_COUNTRY_FILTER
	sed -n 's/^\(CN\|IN\|RU\)\t//p' /tmp/iplist | while read address;
	do
		iptables -A DROP_COUNTRY_FILTER -s $address -j DROP
	done
	iptables -A INPUT -j DROP_COUNTRY_FILTER
fi

time=$SECONDS

echo $time

結果をファイルに書き出すようにして実行。

# sh -x test.sh >test.txt

以下のように出力されます。

(省略)
+ time=1032
+ echo 1032

1032秒。ということは約17分かかったことになります。

新しいルールが全て書き出されるまで全てのアクセスを規制するので、他のループと合わせると約20分もの間アクセス不能になります。
月に1回もしくは週1回のメンテナンスでというならまだしも、毎日これを実行するとなると問題です。

ためしに全てのコマンドをechoでファイルに書き出すと7秒で終了しました。このことから「コマンドで大量のルール追加するのは時間がかかる」ということがわかります。


iptabelsの設定は、コマンドを利用せずに設定ファイルを直接書き出す

ということで、結論です。

大量のルールを追加する場合は、直接設定ファイルを書き出すことにしました。
書きだした設定ファイルをiptables側で読み込み直せば数万行のルールも数秒で反映できます。

以前紹介した「iptablesの設定ファイルをシェルスクリプトを利用して動的に作成」の設定を元に、そのまま設定ファイルとして動作する形式で書き出します。

この方法はiptablesの設定ファイルを直接作成します。コマンドによって補完されていたオプションを自分で指定する必要があるなど、多少難易度が高くなります。(普通のソフトはこの方法が一般的ですが…。)

設定を誤ると、最悪の場合サイトにアクセスできなくなり、サーバの再構築が必要な事態も考えられます。いきなり本番のサーバで試さず、テスト環境で試してから実行してください

iptables2.shの作成

前回は「iptables.sh」というファイルだったので、今回は「iptables2.sh」としました。

# vi iptables2.sh
#!/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

cat << EOS
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:ACCEPT_JP_FILTER - [0:0]
:DNSAMP - [0:0]
:DROP_COUNTRY_FILTER - [0:0]
:PING_ATTACK - [0:0]

# 拒否IPリストに記載されたIPを拒否する
EOS
if [ -s /root/deny_ip ]; then
	for ip in `cat /root/deny_ip`
	do
		echo -A INPUT -s $ip -j DROP
	done
fi

cat << EOS

# IP Spoofing攻撃対策
-A INPUT -i eth0 -s 127.0.0.1/8 -j DROP
-A INPUT -i eth0 -s 10.0.0.0/8 -j DROP
-A INPUT -i eth0 -s 172.16.0.0/12 -j DROP
-A INPUT -i eth0 -s 192.168.0.0/16 -j DROP
-A INPUT -i eth0 -s 192.168.0.0/24 -j DROP

# Ping攻撃対策 + Ping Flood攻撃対策
-A PING_ATTACK -m length --length 0:85 -m limit --limit 1/sec --limit-burst 4 -j ACCEPT
-A PING_ATTACK -j LOG --log-prefix "[IPTABLES PINGATTACK] : " --log-level 7
-A PING_ATTACK -j DROP
-A INPUT -p icmp --icmp-type 8 -j PING_ATTACK

# Smurf攻撃対策+不要ログ破棄
-A INPUT -d 255.255.255.255/32 -j DROP 
-A INPUT -d 224.0.0.1/32 -j DROP 
-A INPUT -d 153.122.40.255/32 -j DROP

# Auth/IDENT用の113番ポートは拒否
-A INPUT -i eth0 -p tcp -m tcp --dport 113 -j REJECT --reject-with tcp-reset

# 日本からのアクセスを許可するチェインを作成
EOS
if [ -s /tmp/iplist ]; then
	sed -n 's/^JP\t//p' /tmp/iplist | while read address;
	do
		echo -A ACCEPT_JP_FILTER -s $address -j ACCEPT
	done
fi

cat << EOS

# 特定の国からのアクセスを拒否するチェインを作成して追加
# 中国、インド、エジプト、パキスタン、ロシア
EOS
if [ -s /tmp/iplist ]; then
	sed -n 's/^\(CN\|IN\|EG\|\PK|RU\)\t//p' /tmp/iplist | while read address;	do
		echo -A DROP_COUNTRY_FILTER -s $address -j DROP
	done
	echo -A INPUT -j DROP_COUNTRY_FILTER
fi

cat << EOS

# ステートフル・パケットインスペクションで正しいTCPと既に許可された接続を許可
-A INPUT -p tcp -m tcp ! --tcp-flags SYN,RST,ACK SYN -m state --state NEW -j DROP
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

## ここから個別のサービスで利用するポートを開放する ##

# lo(ループバックインターフェース)の許可
-A INPUT -i lo -j ACCEPT
-A OUTPUT -o lo -j ACCEPT

# SSH用のポートを日本からのみ許可
-A INPUT -p tcp -m state --state NEW -m tcp --dport 42424 -m hashlimit --hashlimit-upto 1/hour --hashlimit-burst 100 --hashlimit-mode srcip --hashlimit-name ssh-limit --hashlimit-htable-expire 3600000 -j ACCEPT_JP_FILTER

# http用のポート許可
-A INPUT -p tcp -m state --state NEW -m tcp --dport 80 -j ACCEPT
-A INPUT -p tcp -m state --state NEW -m tcp --dport 8080 -j ACCEPT

# DNS用ポートの許可。DNS Ampやカミンスキー攻撃対策に適宜ログを取る
-A INPUT -i eth0 -p udp -m state --state NEW -m udp --dport 53 -j DNSAMP
-A DNSAMP -m recent --set --name dnsamp --rsource 
-A DNSAMP -m recent --rcheck --seconds 60 --hitcount 10 --name dnsamp --rsource -j LOG --log-prefix "[IPTABLES DNSAMP] : " --log-level 7 
-A DNSAMP -m recent --rcheck --seconds 60 --hitcount 10 --name dnsamp --rsource -j DROP 
-A DNSAMP -j ACCEPT

-A INPUT -p tcp -m state --state NEW -m tcp --dport 53 -j ACCEPT

# smtps用にポートを許可
-A INPUT -p tcp -m state --state NEW -m tcp --dport 465 -j ACCEPT

# imaps(pop3 protocol over TLS/SSL)用にポートを許可
-A INPUT -p tcp -m state --state NEW -m tcp --dport 993 -j ACCEPT

# pop3s(imap4 protocol over TLS/SSL)用にポートを許可
-A INPUT -p tcp -m state --state NEW -m tcp --dport 995 -j ACCEPT

# smtp tcp用にポートを許可
-A INPUT -p tcp -m state --state NEW -m tcp --dport 25 -j ACCEPT

# サブミッションポート用にポートを許可
-A INPUT -p tcp -m state --state NEW -m tcp --dport 587 -j ACCEPT

# https(http protocol over TLS/SSL) tcp用にポートを許可
-A INPUT -p tcp -m state --state NEW -m tcp --dport 443 -j ACCEPT

# NTP用にポートを許可
-A INPUT -p udp -m state --state NEW -m udp --dport 123 -j ACCEPT

# 上記の条件に当てはまらない通信を記録して破棄
-A INPUT -m limit --limit 1/sec -j LOG --log-prefix "[IPTABLES DROP INPUT] : " --log-level 7
-A INPUT -j DROP

COMMIT
EOS

スクリプトとして実行せずにファイルへそのまま書き出す部分はechoでもいいですが複数行あると面倒なため「cat << EOS」と「EOS」を組み合わせて書き出しています。

コマンドを使った例と違い、ルールを挿入する「-I」や、チェインの追加に使う「-N」等は必要ありません。「--log-level=debug」も「--log-level 7」に直しています。
また、念のため、暗示的に呼び出してた「-m tcp」といった記述も追加しています。

作成したファイルを「/usr/script/」へ保存して実行権限を与えます。ディレクトリが無い場合は作成してください。

# cp iptables2.sh /usr/script/iptables2.sh
# chmod 755 /usr/script/iptables2.sh

cyber_attack_block.shの作成

Smurf攻撃対策やSYN flood攻撃対策はコマンドで実行する必要があるので別ファイルに分けます。
これは1度設定すれば何度も実行する必要はないのですが、個人的に忘れやすいので一緒くたに処理してます。

# vi cyber_attack_block.sh
# Smurf攻撃対策
sysctl -w net.ipv4.icmp_echo_ignore_broadcasts=1 > /dev/null
sed -i '/# Disable Broadcast Ping/d' /etc/sysctl.conf
sed -i '/net.ipv4.icmp_echo_ignore_broadcasts/d' /etc/sysctl.conf
echo "# Disable Broadcast Ping" >> /etc/sysctl.conf
echo "net.ipv4.icmp_echo_ignore_broadcasts=1" >> /etc/sysctl.conf

# SYN flood攻撃対策でSYN cookiesを有効に設定
sysctl -w net.ipv4.tcp_syncookies=1 > /dev/null
sed -i '/# Enable SYN Cookie/d' /etc/sysctl.conf
sed -i '/net.ipv4.tcp_syncookies/d' /etc/sysctl.conf
echo "# Enable SYN Cookie" >> /etc/sysctl.conf
echo "net.ipv4.tcp_syncookies=1" >> /etc/sysctl.conf

こちらも同じように「/usr/script/」に移動して実行権限を与えます。

# cp cyber_attack_block.sh /usr/script/cyber_attack_block.sh
# chmod 755 /usr/script/cyber_attack_block.sh

iplist_check.shの作成

以前の記事では「iplist_check.sh」で全世界のカントリーコードとIPリストをCronで毎日取得をしていました。
そのなかでついでに「iptables.sh」を実行していました。今回は新しく作った「iptables2.sh」を使ってiptablesの設定ファイルである「/etc/sysconfig/iptables」へ設定を書き出します。
そして新しい設定を有効にするべく「iptables reload」を実行します。

また「cyber_attack_block.sh」も実行します。

# vi /etc/cron.daily/iplist_check.sh
#!/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# 実行時間の計測
SECONDS=0

# ファイルの取得
wget -q http://nami.jp/ipv4bycc/cidr.txt.gz -P /tmp

# 解凍してリネーム
gunzip -q -f -c /tmp/cidr.txt.gz > /tmp/iplist.new

# /tmp下なので240時間で自動的に削除されますが、念のため削除
rm -f /tmp/cidr.txt.gz

# iplistが存在しているかチェック
if [ -s /tmp/iplist ]; then

	# diffでエラーが出るので実行権限の変更。「-f」を付けて初回にファイルがない場合に対応
	chmod -f 700 iplist.new
	chmod -f 700 iplist
	
	# 差分チェック(正常にファイルが取得できたかチェック)
	# grep -c パターンに一致した行の行数のみを出力する。diffで差分(違い)のある行は「< hoge」と表示されるため、行頭に「<」がある行をカウントしている。「=」の前後にスペースがあると正常に代入できないので注意。
	# $()の中に記述しているので、変数ipdiffに代入。
	ipdiff=$(diff /tmp/iplist /tmp/iplist.new | grep -c "^<")

	# 上記が存在していて、差分の数が1000行以上の場合
	# (通常考えられない量の変更=ファイルが正常に取得できていないと判断)
	if [ $ipdiff -gt 1000 ]; then
		# メールでIPリストが正常に取得できなかった由を伝える。
		cat <<EOS | mail -s "iplist_check" info@example.com
		iplist false
EOS
		# 正常に取得できなかったリストを破棄
		rm -f /tmp/iplist.new
	else
		# 正常に取得できた場合ファイル名をiplistに変更
		mv /tmp/iplist.new /tmp/iplist

		# 新しく取得したリストでiptablesの設定スクリプトを実行

		# まずは設定を保存する /etc/sysconfig/iptables.bakとして保存される
		/sbin/service iptables save

		# 設定を/etc/sysconfig/iptablesに書き込む
		sh /usr/script/iptables2.sh >/etc/sysconfig/iptables

		# SYN cookies等の設定
		sh /usr/script/cyber_attack_block.sh

		# 書き込んだファイルをiptablesで読み込む(reloadだと不具合あるので注意)
		/sbin/service iptables restart

		# メールでIPリストが正常に取得できた由と変更されたIPの数を伝える。
		cat <<EOS | mail -s "iplist_check" info@example.com
		iplist true change IP $ipdiff
EOS
	fi
else
	# 初回実行時は差分をチェックすべきバックアップがないのでそのままファイル名を変更して利用
	mv /tmp/iplist.new /tmp/iplist
	/sbin/service iptables save
	sh /usr/script/iptables2.sh >/etc/sysconfig/iptables
	sh /usr/script/cyber_attack_block.sh
	/sbin/service iptables restart
fi

time=$SECONDS
echo "run time:" $time

実行速度も確認できるように「run time:<num>」も追加しました。

送信するメールアドレスは「info@example.com」から自分のアドレスに変更してください。

こうすることでroot宛に「iplist_check」というタイトルのメールが届きます。

また、コメントにもある通り速度重視で「reload」を利用して設定を読み込んで運用していましたが、一部のポートが開放できないなどの不具合が発生したため、「restart」で再起動するように変更しました。
ただ設定が大きく変更になったとしても再起動にかかるのは数十秒程度です。当初のiptablesの更新作業を早くするという目的は達成できています。



現在のページを共有する



現在のページに関連する記事


おすすめの記事


コメントを残す

コメントは認証制のため、すぐには反映されません。

プログラミングに関する質問は「日本語でプログラミングの悩みを解決するQ&Aサイト sukegra」をご利用ください。