Mar 5, 2020
iptablesによるファイアーウォールの設定は攻撃対象になりやすいサーバーでは必須だが私は今までこの設定を完全に怠っていた. 攻撃対象としてはダメージを受ける人があまりいないだろうし, phpなど高度なアプリケーションを使っておらずhtmlだけの構成だけだし, sshは秘密鍵を持っていない限りはアクセスできない. それでもどこかでファイアーウォールを考慮する必要はあるのだろう. その理由を深く理解しないまま, 解説記事を描き始めた.
まず確認として, iptablesがやることはパケットフィルタによるファイアーウォールでセキュリティ対策の一部を担う訳で, https://baremetal.jp/blog/2019/07/18/808/ によればSQLインジェクションやXSSなどアプリケーション層への攻撃の対策はiptablesだけではできない.
今回Arch Linuxでの環境を元に解説を書いている. Arch Linuxにはデフォルトでiptablesがインストールされているはずだ. 多くのLinuxでiptablesが元から入っている. しかしCentOS7などでは別のものを使ったりしているので一応自分の環境を確かめてiptablesを使うか決めよう. それとufw(uncomplicated fire wall)というiptablesをより簡単に使えるアプリケーションがあるが, 細かい設定はiptablesの方がしやすいのではと思ったり私はそもそもラッパーをひどく嫌っているので使わない方針だ. しかし面倒くさいならば私のようにufwも使わず放置よりはufwを使った方がいいかもしれないが, ufwの方が情報が少ないのでかえって苦労するだろうとも.
大体のイメージは https://eng-entrance.com/linux-firewall で掴むのがいいだろう. 特にChainの図がわかりやすい. INPUT, OUTPUT, FORWARDなど, パケットの経路をチェインという. FORWARDは別のサーバーなどへそのまま転送する経路だ. 基本的にINPUT, FORWARDはDROPポリシー, OUTPUTはACCEPTポリシーを適用するのがいいだろう. ポリシーというのはデフォルトの方針というもので, ルールとして明示されていない状況の場合はこのポリシーにしたがってパケットフィルタリングを行う. DROPポリシーとはパケットを閉じるポリシーで, ACCEPTはパケットを開くポリシーだ. INPUTにDROPポリシーが適用されているならば, ルールを追加することで自分のもつIPからだけアクセスを許可するといったことが可能になる. より詳細な図としては次のようになる. (https://wiki.archlinux.jp/index.php/Iptablesより引用)
XXXXXXXXXXXXXXXXXX
XXX Network XXX
XXXXXXXXXXXXXXXXXX
+
|
v
+-------------+ +------------------+
|table: filter| <---+ | table: nat |
|chain: INPUT | | | chain: PREROUTING|
+-----+-------+ | +--------+---------+
| | |
v | v
[local process] | **************** +--------------+
| +---------+ Routing decision +------> |table: filter |
v **************** |chain: FORWARD|
**************** +------+-------+
Routing decision |
**************** |
| |
v **************** |
+-------------+ +------> Routing decision <---------------+
|table: nat | | ****************
|chain: OUTPUT| | +
+-----+-------+ | |
| | v
v | +-------------------+
+--------------+ | | table: nat |
|table: filter | +----+ | chain: POSTROUTING|
|chain: OUTPUT | +--------+----------+
+--------------+ |
v
XXXXXXXXXXXXXXXXXX
XXX Network XXX
XXXXXXXXXXXXXXXXXX
図を見ればわかるように今までfilterテーブルの話をしたがnatテーブルはPREROUTING, POSTROUTINGチェインを持つ. natでやることはパケットの書き換えである.
iptables関連のコマンドはroot権限で実行しなくてはならない. そもそも今の設定はどうなっているかを次のコマンドで調べよう. このコマンドでcan't initialize iptables table `filter': Table does not exist (do you need to insmod?) Perhaps iptables or your kernel needs to be upgraded. というエラーメッセージが表示される場合, 再起動すると大体うまく行くらしい. 理由はよくわからず.
iptables -L
結果:
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain FORWARD (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
私の場合, 全部解放されていることがわかった.
iptablesの設定が書かれている場所は /etc/iptables/iptables.rules で, systemdによってロードされる. CentOSやFedoraなどRedHad系の場合は/etc/sysconfig/iptables.rules にあるらしい. このファイルの中身を見てみよう. iptablesを特にいじっていなかったので私の場合は次のような内容だった.
# Empty iptables rule file
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
COMMIT
やはり全部解放されており, ルールは何もない. ファイルがないと言われたなら次のコマンドで生成できる.
iptables-save > /etc/iptables/iptables.rules
すでにルールがあるならば, iptables.rulesをコピーしてバックアップを取ってから, 次のコマンドでデフォルトのルールへリセットできる.
iptables-restore < /etc/iptables/empty.rules
とりあえずファイアーウォールを作ってみよう. 今は全体像を理解するのが大事なので, 詳細はあとで踏み込む.
必要なチェインの作成. 次のチェインはユーザー定義チェインで, 名前はなんでもいい.
iptables -N TCP
iptables -N UDP
FORWARDチェインについて, サーバーをNATゲートウェイ(ルーターの働きをするマシン)としてセットアップしない場合はDROPする. 普通用途なら大体DROPする.
iptables -P FORWARD DROP
OUTPUTチェインは送信のチェインであるが, ここはとりあえずACCEPTでいい.
iptables -P OUTPUT ACCEPT
INPUTチェインの設定. ここが一番重要で, サーバーへの接続の設定をする. ここで一つ注意するのはサーバーへssh接続している場合はちゃんと順番通り設定しないと接続できなくなってしまう. なのでssh接続を確保しつつ接続を制限する設定をしていく. 明示的に許可するものだけをルールに記述し, 最後にINPUTにDROPポリシーを適用するという流れだ.
次のコマンドのでルールを設定する以前からの接続を許可する.
iptables -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
loopback(lo)インターフェースからのトラフィックを全て許可する. 多くのアプリケーションやサービスで必須.
iptables -A INPUT -i lo -j ACCEPT
トラフィックのうちINVALIDステートに一致するものを全て排除. トラフィックは4つのstateカテゴリーに分けることができ, NEW, ESTABLISHED, RELATED, INVALID である.
iptables -A INPUT -m conntrack --cstate INVALID -j DROP
次のルールでは ICMP エコー要求(ping)を全て許可する. 最初のパケットだけを NEW としてカウントし, あとは全て RELATED, ESTABLISHED ルールで処理する. コンピュータはルーターではないので他のICMPトラフィックをステートNEWで許可する必要はない.
ipstate -A INPUT -p icmp --icmp-type 8 -m conntrack --csate NEW -j ACCEPT
新しい接続を処理するために, INPUTチェインにTCPチェインとUDPチェインを適用する. 一旦TCPチェインやUDPチェインで接続が許可されると, 以降はRELATED/ESTABLISHEDのルールで処理される. 新しいTCP接続は必ずSYNパケットから始まる.
iptables -A INPUT -p udp -m conntrack --ctstate NEW -j UDP
iptables -A INPUT -p tcp --syn -m conntrack --ctstate NEW -j TCP
ポートを開いていない場合, TCP接続はTCP RESETパケットで, UDPストリームはICMP port unreachableメッセージで拒否する.
iptables -A INPUT -p udp -j REJECT --reject-with icmp-port-unreachable
iptables -A INPUT -p tcp -j REJECT --reject-with tcp-reset
他のプロトコルのために, icmp protocol unreachableメッセージで残りの受信トラフィックを拒否する最終ルールをINPUTチェインに追加する.
iptables -A INPUT -j REJECT --reject-with icmp-proto-unreachable
SSH接続を許可. 22番ポート以外をsshに使っている場合はその番号を指定すること.
iptables -A TCP -p tcp --dport 22 -j ACCEPT
いよいよINPUTチェインにDROPポリシーを適用する.
iptables -A INPUT DROP
これらのコマンドを実行した後に/etc/iptables/iptables.rulesを開いてみると, 何も変わっていないが実はこれらのコマンドは現在の設定に適用するだけでこれらの設定は再起動などで消えてしまう. 常に適用するには次のコマンドを使う.
iptables-save > /etc/iptables/iptables.rules
では/etc/iptables/iptables.rulesがどうなっているか確認しよう. 私の場合次のような内容だった.
# Generated by iptables-save v1.8.4 on Fri Mar 6 20:00:00 2020
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0,0]
:TCP - [0:0]
:UDP - [0:0]
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
-A INPUT -p icmp -m icmp --icmp-type 8 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -p udp -m conntrack --ctstate NEW -j UDP
-A INPUT -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j TCP
-A INPUT -p udp -j REJECT --reject-with icmp-port-unreachable
-A INPUT -p tcp -j REJECT --reject-with tcp-reset
-A INPUT -j REJECT --reject-with icmp-proto-unreachable
-A TCP -p tcp -m tcp --dport 56789 -j ACCEPT
COMMIT
# Completed on Fri Mar 6 20:00:00 2020
コマンドと若干内容が違う部分がある. このように設定を直接iptables.rulesを編集するよりコマンドで設定する方が自動的に最適化やチェックをしてくれる. あとは必要に応じて設定をどんどん追加していく.
WEBサーバーへのTCP接続を許可する.
iptables -A TCP -p tcp --dport 80 -j ACCEPT
iptables -A TCP -p tcp --dport 443 -j ACCEPT
DNSサーバーからの応答を許可する. DNSは通常はUDPだがサイズの大きいパケットだとTCPも使われる. 実はこの設定をしなくてもサーバーからDNSサーバーへ問い合わせをすると上で設定したステートが確立されてDNSからパケットを受け取ることができる. でもなぜかarchのドキュメントには当然のように記述されていたのでもしかしたら必要なのかと思い設定した.
iptables -A TCP -p tcp --dport 53 -j ACCEPT
iptables -A UDP -p udp --dport 53 -j ACCEPT
IRCポートを解放する. (IRCバウンサーを利用している人のみ)
iptables -A TCP -p tcp --dport 113 -j ACCEPT
iptables -A TCP -p tcp --dport 6667 -j ACCEPT
忘れずに設定を記録する
iptables-save > /etc/iptables/iptables.rules
逆にiptables.rulesを直接編集してその変更をすぐ反映させたい場合は次のコマンドを使う
iptables-restore < /etc/iptables/iptables.rules