iptables 設定例 (つづき)

透過型ファイヤーウォール

基本的な用語について

カッコや註だらけにならないように、これから頻出することになる基本的なネットワーク用語についてまず定義を明確にしておかなくてはならない。

ブリッジ (Bridge)
TCP/IPの第1層 ネットワークインターフェース層 (OSI参照モデルの第2層 データリンク層) で、ネットワークどうしを MACアドレス に基づいて中継する装置。ブリッジの代表といえば、いわゆるスイッチングハブ、今日では単にハブ と呼ぶことの多いアレだ。イーサネットのデータは、この層を運ばれる間、フレーム (より正確は MACフレーム) 内にカプセル化されているので、ブリッジで扱うデータの粒のことは パケット でなく フレーム と呼ぶのが正式。
ルータ (Router)
TCP/IPの第2層 インターネットット層 (OSI参照モデルの第3層 ネットワーク層) で、ネットワークどうしを IPアドレス に基づいて中継する装置。 慣例的に ゲートウェイ (Gateway) と呼ばれる場合もあるが、現今では、ゲートウェイ はプロトコル変換装置または変換プログラムを指し、参照モデルの全階層またはいずれか任意の階層で働くものと捉えられている。
ネットワークスイッチ (Network Switch)
ネットワークどうしを中継する装置の中で、何らかの情報に基づいて送り先装置を絞り込み、必要な出口だけへ信号を送るようにできている (スイッチング技術) ものを言う。幾らか曖昧な語彙であり、ブリッジの中でもMACアドレスがどのポートの先にあるかを学習する ラーニングブリッジネットワークスイッチ だし、TCP/IPの複数階層での中継先判断やデータ操作をカバーしたものを L2スイッチ とか L3スイッチ と呼んだり、守備範囲がどの階層とも割り切れないものを面倒くさいのでこう呼んだりする。当ページでは、たいてい単に スイッチ とだけ記述する。
リピータ (Repeater)
OSI参照モデルの第1層 物理層 (TCP/IPの第1層または対象外の0層) でネットワークを延長する装置。ケーブルを伝わってくる間にナマった電気信号を再増幅・整形して次のネットワーク装置へ送る。スイッチング機能はないのでこれを馬鹿ハブと呼ぶのは自分の周りだけの集団方言か? 以降の説明にはおそらく登場しない。

なお、TCP/IPのレイヤーの呼び方は、なるべくTCP/IP参照モデルの4層モデルのものを使うことにする。なるべく、基本的には。

透過型ファイヤーウォールの概要

普通一般にファイヤーウォールと言えば、ほとんどはルータベースである。しかし、今日のカーネルと iptables では、レイヤー1 でまでパケットフィルタリングができるようになった。これを利用すると、ネットワークを複数のサブネットに分割することなく、単一のIPセグメントに属するコンピュータ群を物理的に幾つかのに分けて、その間にLinuxファイヤーウォールマシンを1個のハブのように差し込んでファイヤーウォールを掛けることができる。これを、ポートベースのファイヤーウォールとか、透過型ファイヤーウォール、Bridged Firewall あるいは Bridgewall などと呼ぶ。透過型なら、ファイヤーウォール導入にあたって各コンピュータのIPアドレス体系を変えたりデフォルトゲートウェイを再設定して廻ったりといった、面倒で危険な一大パーティを催さなくて済むのだ。

Transparent FW と示したのがこのLinuxファイヤーウォールマシンだ。Host1 と Host2 はKVMホストマシン。手近な材料で済ませるため KVM仮想環境上で検証を行なった都合で、このような図になっている。透過型FWマシンには少なくとも2個のネットワークインターフェースが必要だ。一方のインターフェースは信頼できないマシンたちを繋いだスイッチ (Physical Backyard Switch-2 の系統) に、もう一方は信頼できるつまり保護したいマシンたちを繋いだスイッチ (Physical Backyard Switch-1 系) に接続する。Backyard Switch-2 は、複数の仮想環境ホストがある場合に他のホスト上の仮想マシンにも透過型FWが使えるよう橋渡しをするだけのためにあり、決して信頼系のスイッチやその他の上流スイッチに繋いではいけない。フレームは、例えば上図の2本の矢印線のような経路でネットワークを往き来する。

透過型FWは、基本的には、パケットの出入りするインターフェース (eth0 or eth1 ) に基づいてフィルタリングを行う。もちろん、追加条件として、慣れ親しんだソースIPアドレスや宛先IPアドレス、ポートナンバー等々も使える。

ファイヤーウォールマシンにはまず論理ブリッジインターフェース br0 を作成する。そして、左図で Pyhs. Interface として示した物理インターフェース (当実験では仮想マシンなので仮想NICだが) をブリッジのスレーブインターフェースとして組み込む。すると、点線で囲んだものがソフトウェアスイッチングハブと化す。eth0eth1 は言うなればハブの口だ。実際、ブリッジ br0 は、各物理インターフェースの向こう側にあるMACアドレスを学習・維持し、まんまラーニングブリッジとして振る舞う。おまけに、Linux の bridge は、ありがたくもスパニングツリーによるループ防止まで標準装備している。物理インターフェースはIPアドレスを持たず、明示的に Promiscuous(無差別)モードを有効にするまでもなく(※コラム)、全てのフレームを受け取り、出るべきインターフェースまたは透過型ファイヤーウォールマシン自体へ引き渡す。

ただ、マシンがまったく IPアドレスを持っていないと、物理コンソールや仮想マシンコンソールで直接ターミナルを開くしか管理方法がなく、ファイヤーウォールルールのメンテナンスにも困るので、IPアドレスを持たせたい。管理用IPアドレスは、br0 に振る。決して物理インターフェースにIPを振ってはいけない。あるいは、ブリッジポートとして使うNICとは別に管理用のNIC(例えば eth2) を付与するという手もある (その分ルールが複雑になるのであまりお勧めしない)。

BridgeとPromiscuousモード

感覚的にさえ分かる通り、bridgeに組み込んだ物理インターフェースは自分のMACアドレス宛てに限らず NICに触れたフレームはとにかく選り好みなく取り込んでくれないと、ブリッジとして用をなさない。通常、インターフェースをプロミスキャスモードにするには `ifconfig eth0 promisc' のようにコマンドする(ifcfg-IF ファイルでの PROMISC=yes パラメータは最近 initscripts セットから廃止された模様)。しかし、bridgeに物理インターフェースを組み込む `brctl addif br0 eth0' は eth0 を暗黙的にプロミスキャスモードにする。ここで早合点して `ifconfig eth0' とやると、PROMISC フラグの表示は見あたらず、ほら無差別モードになってないじゃないか、ということになる。実は、brctl に伴って有効にされた PROMISC フラグは ifconfig に表示されないそうである。見たければ /sys/class/net/IF/flags を読む。

# grep . /sys/class/net/*/flags
/sys/class/net/br0/flags:0x1003
/sys/class/net/eth0/flags:0x1103
/sys/class/net/eth1/flags:0x1103
/sys/class/net/lo/flags:0x9

ビットの意味のリストは /usr/src/kernels/$(uname -r)/include/linux/if.h ヘッダファイルの /* Standard interface flags (netdevice->flags). */ という節に書かれている。全部載せると場所を喰うので上記出力に一致する分だけ抜粋すると、

#define IFF_UP          0x1     /* interface is up */
#define IFF_BROADCAST   0x2     /* broadcast address valid */
#define IFF_LOOPBACK    0x8     /* is a loopback net */
#define IFF_PROMISC     0x100   /* receive all packets */
#define IFF_MULTICAST   0x1000  /* Supports multicast */

なので、eth0 (0x1103) は UP BROADCAST PROMISC MULTICAST のフラグが立っていることが晴れて明らかになる。こう書いてしまってから何だが、kernels/ 配下の一式は kernel-devel パッケージで提供されるもので、当ページの通りにインストールしたのならインストールされていないので存在しない、悪しからず。

もうひとつ肝心なのが、CentOS/RedHat系ディストリビューションのデフォルト状態では、ブリッジを通っているフレームは iptables から見えないという点だ。本来 iptables が操作対象とするのはTCP/IP参照モデルのレイヤー2と3 (及びレイヤー4の一部)。ネットワークインターフェース層の住人であるフレームは、カーネルの備える bridge-nf (bridge-netfilter/br-nf) コードの助けを借りて初めて iptables で扱えるようになる。その使用/不使用は、sysctl ツリーにある iptables, ip6tables, arptables 毎の net.bridge.bridge-nf-call-* キーでオン/オフできる。有効にすると、bridge-nf がフレームからパケットを取り出して、カーネルの Netfilterスタックつまり iptables から観測・操作できるテーブルやチェーンの中を通らせる。bridge-nfNetfilter/iptables との関係は、Ralf Spenneberg が Linux-Kongress に寄稿した Bridgewalling - Using Netfilter in Bridge Mode を読むとよく分かる。

ちなみに、レイヤー1 に特化したフレームマニピュレーションプログラムに ebtables (Ethernet Bridge Tables) がある。ARPヘッダやVLANタグに基づいた操作、MACアドレスNATなど、普通には考えられない強力な機能があるようだ。

bridge-nf は、正確に言うと、カーネルにもともと備わっているbridge コードを拡張する、カーネルパッチの呼び名。しかし、いちいち「bridge-nfによって拡張されたbridgeコードが」などと書いていたら書く方も読む方もやりきれないので、単に「bridge-nf コード」のように記す。少なくともCentOS/RHEL 6 にパッケージされているカーネルでは、初めからこのパッチが当たっていて、bridgeはカーネルモジュールとしてコンパイルされている。

当サイトではこれまでファイヤーウォール専用マシンを作るケースには触れてこなかったので、当節では、OSのインストールから順を追って述べていくことにする。ただし、話題がとっちらかっては分かりにくくなると思うので、たまたま筆者の場合そうであった仮想インフラ系の準備については、特段の必要がない限り触れない。検証は CentOS 6.7 で行なった。

OSのインストール

今回は、X-Windowもなし、ssh でリモートメンテナンスができれば充分というスパルタンな構成にした。べつに、ご自分の流儀で入れてもらって構わない。

1. OSをMinimal構成でインストール

筆者は CentOS-6.X-x86_64-minimal.iso でなく通常の *-x86_64-bin-DVD1.iso を使い、インストールタイプで Minimal を選択した。仮想マシンということもあり、メモリ量を後々いくらに変更しても一応過不足のないよう、スワップは 6GB に。とりあえず、普通に eth0 でインターネットにつながる設定でインストールを進める。インストール結果は、パッケージ数たかだか250、ディスク使用量はわずか 1.2GB となった。あのうっとうしい NetworkManager も入らず、ああ何と幸せなことだ。

2. 基本整備

# yum install bridge-utils ntp vim-enhanced nano man mutt

3. ネットワークコンフィグの整備

基本ファイルをきちんとする

hostsファイルに自ホストレコードを書き加える。

10.0.2.10    fw1.hoge.cxm  fw1

resolv.confdomainを書き加える。

domain hoge.cxm

その代わり、sysconfig/networkHOSTNAME をホスト部だけにする。

HOSTNAME=fw1

sysconfig/network に下記を書き加える。

NOZEROCONF=yes
ブリッジを形成する

sysconfig/network-scripts/ifcfg-br0

DEVICE=br0
TYPE=Bridge
ONBOOT=yes
NM_CONTROLLED=no
BOOTPROTO=none
IPADDR=10.0.2.10
NETMASK=255.255.255.0
NAME=br0
#STP=on
#DELAY=15
#AGEING=300

"Bridge" はこの通り頭大文字で書かないと読み取ってもらえないので注意。NM_CONTROLLEDNetworkManager がインストールされていない場合不要だが書いても害はない。最後の3つは Linux bridge の調整パラメータで、書き方とデフォルト値を示す意味で載せているだけ。AGEING0 にするとMACアドレスのラーニングをせず常に全ブリッジポートへフレームを送出するようになる。

sysconfig/network-scripts/ifcfg-eth0

DEVICE=eth0
TYPE=Ethernet
HWADDR=xx:xx:xx:xx:xx:xx
ONBOOT=yes
NM_CONTROLLED=no
BOOTPROTO=none
BRIDGE=br0
NAME=eth0

sysconfig/network-scripts/ifcfg-eth1

DEVICE=eth1
TYPE=Ethernet
HWADDR=xx:xx:xx:xx:xx:xx
ONBOOT=yes
NM_CONTROLLED=no
BOOTPROTO=none
BRIDGE=br0
NAME=eth1

設定ファイルが一通りできあがったので、機能していることを見る。スタートした際、STPの安全マージンのため $DELAY 秒間は物理インターフェースがブロックされる。また、両端に存在するマシンの数や他のネットワーク機器構成によっては、ブリッジが機能し始めるまで2分ほどかかる場合がある。

# service network restart
# ifconfig -a
# brctl show
# brctl showstp
# brctl showmacs

iptables は停まっているので、全体図の例えばマシンAからB、BからAと、まるで島分割もブリッジファイヤーウォールも存在しないかのように通信ができるはずだ。疑い深い人はFWマシン上で `service network stop' してみるのもいい。

iptablesの設定

いよいよ本題である。が、未だ少し埋まっていない外堀があるのでまずはそこから。

sysctl 設定

/etc/sysctl.conf に下記を追加。

# Enable bridged traffic filtering
net.bridge.bridge-nf-call-arptables = 0
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 0
net.netfilter.nf_conntrack_max = 655360

コメントの下の2行目が、前述した iptables に対して bridge-nf を有効化する設定。最後の行は、iptables のコネクション追跡管理レコードの上限数を既定値 (FWマシンの搭載メモリ2GBの時) の10倍にしている。トラッキングレコードひとつあたり最大350バイトのメモリを占めるということを頭に置いて、搭載メモリ量と相談して値を決めよう。このようにほぼ純粋なブリッジファイヤーウォールの場合、NATやルーティングによってパケットをインターフェース間でフォワードするわけではないので net.ipv4.ip_forward 0 のままにしておくのが妥当だ。

sysconfig/iptables-config にある下記のコメント記号を外して有効化する。

IPTABLES_SYSCTL_LOAD_LIST=".nf_conntrack .bridge-nf"

これによって、iptablesSysVinitスクリプトで start する度に、前記 sysctl.conf を引数のリストで fgrep してマッチした行が "sysctl -w" される。sysctl.conf に書かれている全ては本来システムブート時に反映されるものなのだが...。ブート時にbridgeカーネルモジュールのロードされるタイミングによってはこれらのsysctl値投入時にエラーが出る場合があるという この問題(libVirtの記事) への対処策なのではなかろうか。何にせよ、これらの値を再チューニングした時には iptables をリスタートすれば反映されるのだから、これはこれでありがたい。

ファイヤーウォールログを専用ファイルへ分ける

デフォルト状態では iptables からのログも messages へ行ってしまう。ここはファイヤーウォール専用マシンらしく、ログファイルを専用にしよう。/etc/rsyslog.conf に定義を追加する。基本整備項目に挙げたように、rsyslog は Ver.5 から rsyslog7 にアップグレードしてあるものとする。

rsyslog.conf.diff

--- rsyslog.conf.org	2016-01-29 23:35:42.000000000 +0900
+++ rsyslog.conf	2016-01-29 18:18:36.000000000 +0900
@@ -33,6 +33,10 @@
 
 #### RULES ####
 
+# Firewall log
+if $programname == 'kernel' and $msg startswith 'iptables: ' then  /var/log/firewall.log
+& stop
+
 # Log all kernel messages to the console.
 # Logging much else clutters up the screen.
 #kern.*                                                 /dev/console

これを書いただけでは役に立たない。本丸である iptables ルールファイルで、LOG ターゲットの --log-prefix 引数と協調させて初めてこのログフィルタが効果を発揮する。

このログファイルをローテーションするため logrotrate の設定もすべき。logrotate.d/syslog にファイルパスを追加すればいい。ピンと来ない人はこちらを参照

iptablesポリシーファイル

やっとここに辿り着いた。本題であるのとは裏腹に、百聞に一見は如かずで最も説明の要らない部分だろう。bridge-nf がフレームから取り出して Netfilter に差し出すパケットがブリッジのどの物理インターフェースを入口/出口にしたものかを特定する拡張マッチが physdev であることは言わずもがなである。filter テーブルの FORWARD チェーンが重要な役割を演ずるこのポリシーでは、INPUTチェーンはFWマシン宛てのパケットだけが、FORWARDチェーンはFWマシンを素通りするパケットだけが通る、といった iptables の基本を頭に置いておくとすんなり読めるはずだ。気絶しそうになったら、IptablesチュートリアルChapter 6. テーブルとチェーンの道のり を手元に広げることをお勧めする。

当サンプルポリシーの概略

sysconfig/iptables

# Firewall configuration
#
# My IP(br0) = 10.0.2.10
# Bridge                    = br0
# Trusted physdev on bridge = eth0
# Untrust physdev on bridge = eth1
 
*filter
 
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT DROP [0:0]
# User defined chains
:badphys_in -
 
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A OUTPUT -o lo -j ACCEPT
-A INPUT -s 10.0.2.10 -j DROP
-A INPUT -m physdev ! --physdev-in eth1 -d 10.0.2.10 -j ACCEPT
-A OUTPUT -s 10.0.2.10 -j ACCEPT
 
-A badphys_in -m state --state INVALID -j DROP
-A badphys_in -p icmp --icmp-type 8 -j ACCEPT
-A badphys_in -p icmp --icmp-type 11 -j ACCEPT
-A badphys_in -p udp --sport 68 --dport 67 -j ACCEPT
-A badphys_in -p udp --dport 53 -j ACCEPT
-A badphys_in -p udp --dport 123 -j ACCEPT
-A badphys_in -m hashlimit --hashlimit 3/sec --hashlimit-mode srcip,dstport --hashlimit-name ipt_badphys_in --hashlimit-burst 15 -j LOG --log-level info --log-prefix "iptables: FORWARD packs died "
-A badphys_in -p all -j DROP
 
-A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT
-A FORWARD -m physdev --physdev-in eth0 -j ACCEPT
-A FORWARD -m physdev --physdev-in eth1 -j badphys_in
 
-A INPUT -m hashlimit --hashlimit 3/sec --hashlimit-mode srcip,dstport --hashlimit-name ipt_input_hosts --hashlimit-burst 15 -j LOG --log-level info --log-prefix "iptables: INPUT packs died "
-A OUTPUT -m hashlimit --hashlimit 3/sec --hashlimit-mode srcip,dstport --hashlimit-name ipt_output_hosts --hashlimit-burst 15 -j LOG --log-level info --log-prefix "iptables: OUTPUT packs died "
-A FORWARD -m hashlimit --hashlimit 3/sec --hashlimit-mode srcip,dstport --hashlimit-name ipt_forward_hosts --hashlimit-burst 15 -j LOG --log-level info --log-prefix "iptables: FORWARD packs died "
COMMIT

そしてもちろん、

# service iptables start
# chkconfig iptables on
幾つか注意点
デバグに役立つ情報源