第23章 ルーター設定とパケットフィルタリング

LPIC-2 212.1 相当

サーバーを外部の脅威から守る最初の砦が、ファイアウォール(パケットフィルタリング)です。ここではLinuxカーネルのパケットフィルタリング機能を扱います。

iptables と nftables

Linuxカーネルには、パケットを通す・止める・変換するといった処理を行うフィルタリング機能があります。長年 iptables が使われてきましたが、近年は後継の nftables への移行が進んでいます。

項目iptablesnftables
位置づけ従来の標準(多くの解説記事や試験範囲で今も扱われる)後継の統一されたフレームワーク
IPv4/IPv6ip6tablesなど別コマンドが必要1つのフレームワークで統一的に扱える

iptablesの基本構造

iptablesは「テーブル」の中に複数の「チェーン」があり、チェーンの中にルールを並べて処理します。もっとも基本的なのは filter テーブルです。

チェーン対象
INPUTこのホスト宛の受信パケット
OUTPUTこのホストから送信するパケット
FORWARDこのホストを経由して転送されるパケット
$ sudo iptables -L -n -v                          # 現在のルールを表示
Chain INPUT (policy ACCEPT 128 packets, 18420 bytes)
 pkts bytes target     prot opt in     out     source               destination
    0     0 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination

Chain OUTPUT (policy ACCEPT 96 packets, 12300 bytes)
 pkts bytes target     prot opt in     out     source               destination
$ sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT   # SSH(22番)を許可(正常時は無出力)
$ sudo iptables -A INPUT -j DROP                   # それ以外はすべて破棄(末尾に追加)(正常時は無出力)

この行のこの値に注目: -L -n -vを組み合わせると、ポート番号やIPアドレスを名前解決せず数値のまま(-n)、パケット数・バイト数のカウンタ付き(-v)で表示できます。pkts・bytes列は、そのルールにこれまで何個・何バイトのパケットがマッチしたかを示し、0のままであれば、まだそのルールに該当する通信が発生していないことが分かります。-Aや-j DROPのようなルール追加コマンド自体は、成功しても画面には何も表示されません。

注意: 最後にDROPルールを追加する前に、SSHなど自分がアクセスに使うポートの許可ルールを先に入れておかないと、自分自身がサーバーにアクセスできなくなってしまいます。

iptablesはルールを上から順に評価し、最初にマッチしたルールのターゲット(-j の後ろ)に従って処理を確定します。そのため、ルールの「順序」が非常に重要です。-A(Append)は末尾に追加、-I(Insert)は先頭または指定位置に挿入するオプションで、優先させたいルールは-Iで先頭に入れる必要があります。

主なターゲット動作
ACCEPTパケットの通過を許可する
DROPパケットを黙って破棄する(送信元には何も通知しない)
REJECTパケットを拒否し、送信元にエラー(ICMPの到達不能メッセージなど)を返す

DROPは攻撃者に「フィルタされている」ことを気付かれにくくする一方、正規のクライアントから見るとタイムアウトするまで待たされて分かりにくくなります。REJECTは即座にエラーが返るため利用者にとっては親切ですが、サーバーの存在自体は把握されやすくなります。用途に応じて使い分けます。

チェーンには「デフォルトポリシー」も設定でき、どのルールにもマッチしなかったパケットの扱いを決めます。

$ sudo iptables -P INPUT DROP    # INPUTチェーンのデフォルトポリシーをDROPに設定(正常時は無出力)

netfilterのフックとパケットの通過順

ここまで扱ってきたINPUT/OUTPUT/FORWARDの3つのチェーンは、実際にはLinuxカーネルのnetfilterが提供する、より詳細な複数の「フック」に対応しています。パケットがどの経路をたどるかによって、通過するフック(=チェーン)とテーブルの組み合わせが異なります。

                          ┌──────────┐
        受信パケット ──▶ │PREROUTING│
                          └────┬─────┘
                               │
                 ┌─────────────┴─────────────┐
          ローカル宛?                  他ホストへ転送?
                 │                             │
                 ▼                             ▼
            ┌────────┐                   ┌──────────┐
            │ INPUT  │                   │ FORWARD  │
            └───┬────┘                   └────┬─────┘
                 │                             │
                 ▼                             │
          ローカルプロセス                      │
          (アプリケーション)                  │
                 │                             │
                 ▼                             │
            ┌────────┐                         │
            │ OUTPUT │                         │
            └───┬────┘                         │
                 │                             │
                 └─────────────┬───────────────┘
                                ▼
                          ┌───────────┐
                          │POSTROUTING│──▶ 送信パケット
                          └───────────┘

この図から分かるとおり、パケットは大きく3つの経路のいずれかをたどります。

経路通過するチェーン
ローカル宛パケット(このホスト自身への通信)PREROUTING → INPUT
転送パケット(ルーターとして中継するだけの通信)PREROUTING → FORWARD → POSTROUTING
ローカル発パケット(このホスト自身が送信元の通信)OUTPUT → POSTROUTING

それぞれのフック(チェーン)で、どのテーブルが評価されるかも決まっています。

フック評価されるテーブル(優先順)
PREROUTINGraw → mangle → nat(DNAT)
INPUTmangle → nat → filter
FORWARDmangle → filter
OUTPUTraw → mangle → nat → filter
POSTROUTINGmangle → nat(SNAT/MASQUERADE)
受信パケット PREROUTING raw→mangle →nat(DNAT) ローカル宛 転送 INPUT mangle→nat →filter FORWARD mangle→filter 配送 ローカルプロセス (アプリケーション) 生成 OUTPUT raw→mangle→nat →filter ローカル発 POSTROUTING mangle→nat (SNAT/MASQ) 送信パケット =ローカル宛 =転送 =ローカル発
netfilterのフックとテーブルの通過順。パケットの経路によって通過するフック(チェーン)が異なり、ローカル宛(緑)はPREROUTING→INPUT、転送(黄)はPREROUTING→FORWARD→POSTROUTING、ローカル発(青)はOUTPUT→POSTROUTINGをたどります。各フックの脇には、そこで評価されるテーブルの優先順を示しています。

この行のこの値に注目: DNAT(宛先アドレスの書き換え)は、パケットが実際にどこへルーティングされるかを決める前のPREROUTINGで行われるのに対し、SNAT/MASQUERADE(送信元アドレスの書き換え)は、ルーティング先が決まった後のPOSTROUTINGで行われます。この評価順序を理解していないと、「DNATしたのにFORWARDチェーンのルールで想定通りにマッチしない」といった混乱の原因が分からなくなります。rawテーブルは主にconntrack(コネクション追跡)の対象から特定パケットを除外する用途で使われ、他のテーブルより先に評価されます。

ip6tablesとIPv6固有の注意点

iptablesはIPv4専用のツールで、IPv6のパケットフィルタリングには別コマンドのip6tablesを使います(nftablesではinetファミリーを使うことでIPv4/IPv6を統一的に扱えるため、この二重管理の煩雑さが解消されています)。

$ sudo ip6tables -A INPUT -p tcp --dport 22 -j ACCEPT    # 正常時は無出力
$ sudo ip6tables -L -n -v
Chain INPUT (policy ACCEPT 42 packets, 5040 bytes)
 pkts bytes target     prot opt in     out     source               destination
    0     0 ACCEPT     tcp      *      *       ::/0                 ::/0                 tcp dpt:22

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination

Chain OUTPUT (policy ACCEPT 30 packets, 3600 bytes)
 pkts bytes target     prot opt in     out     source               destination

この行のこの値に注目: IPv4版のiptables -L -n -vと表示形式はほぼ同じですが、source・destination列がIPv6表記(::/0など)になっている点が異なります。ip6tablesとiptablesはカーネル内部でも別々のルールセットとして管理されるため、IPv4側で許可したポートも、IPv6側で個別に許可しないとIPv6経由の接続は通りません。

注意:ICMPv6を安易にブロックしない: IPv4のICMPと異なり、IPv6のICMPv6は、単なる診断用の補助プロトコルではなく、近隣探索(Neighbor Discovery Protocol、同一リンク上の他ホストのMACアドレスを解決する仕組み)やRouter Advertisement(アドレス自動設定)など、IPv6の基本的な通信そのものに不可欠な役割を担っています。IPv4の感覚で「pingに応答しないようICMPを全部ブロックする」ような設定をIPv6にそのまま適用してICMPv6を全面的に遮断すると、近隣探索が機能しなくなり、同一セグメント内の通信そのものが成立しなくなることがあります。ICMPv6をフィルタする場合は、近隣探索に必要なメッセージタイプ(Neighbor Solicitation/Advertisementなど)を個別に許可する必要があります。

プライベート・特殊用途のアドレス範囲

ファイアウォールルールやNAT設定を書く際、どのアドレス範囲がどのような性質を持つかを正確に理解しておく必要があります。

範囲用途
10.0.0.0/8、172.16.0.0/12、192.168.0.0/16(RFC1918)IPv4のプライベートアドレス。インターネット上でルーティングされない、組織内部専用のアドレス範囲
fc00::/7(ULA、Unique Local Address)IPv6版のプライベートアドレスに相当。組織内部専用で、インターネット上ではルーティングされない
169.254.0.0/16(IPv4リンクローカル)、fe80::/10(IPv6リンクローカル)DHCPが利用できない場合などに自動的に割り当てられる、同一リンク内でのみ有効なアドレス
127.0.0.0/8(ループバック、IPv4)、::1(IPv6)自ホスト自身を指す特殊アドレス
224.0.0.0/4(IPv4マルチキャスト)、ff00::/8(IPv6マルチキャスト)複数の受信者へ同時に配信するための特殊アドレス範囲

ファイアウォールルールで「社内ネットワークからのみ許可」のような条件を書く際は、これらの範囲を正確に指定することが前提になります。特にIPv6のULAとリンクローカルは、IPv4の感覚と混同しやすいため注意が必要です。IPv6のリンクローカルアドレス(fe80::/10)は近隣探索など基本的な通信に使われるため、誤ってこれをブロックすると同一セグメント内の通信に支障をきたします。

/etc/servicesとポート番号の指定方法

ファイアウォールのルールでポート番号を指定する際、数値の代わりにサービス名を使うこともできます。この対応関係を管理しているのが/etc/servicesです。

$ grep -w ssh /etc/services
ssh              22/tcp
ssh              22/udp
$ sudo iptables -A INPUT -p tcp --dport ssh -j ACCEPT
# --dport 22 の代わりに --dport ssh とサービス名で指定できる(正常時は無出力)

/etc/servicesは、IANA(Internet Assigned Numbers Authority)が管理するポート番号の割り当てを反映した一覧で、サービス名 ポート番号/プロトコルという書式で並んでいます。数値のポート番号だけでルールを書くよりも、サービス名を使うことで設定ファイルの可読性が上がりますが、非標準のポートで運用している場合はサービス名と実際のポートが一致しなくなるため、そのような環境では数値を直接指定する方が誤解を招きません。

ステートフルなフィルタリング

1本ずつのパケットだけを見るのではなく、「どの通信のやり取りの一部か」(コネクションの状態)に基づいてルールを組むと、記述が簡潔になり安全性も高まります。

$ sudo iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# 自分から始めた通信の戻りパケット(ESTABLISHED)と、それに関連する通信(RELATED、例: FTPのデータ転送)を許可(正常時は無出力)
$ sudo iptables -A INPUT -p tcp --dport 22 -m state --state NEW -j ACCEPT
# 新規のSSH接続要求のみ明示的に許可(正常時は無出力)

これにより「新規の外部からの接続はSSHなど許可したものだけ、それ以外は既存の通信の応答だけ許可する」という安全な構成をシンプルに書けます。近年のnftablesやiptablesでは、より新しい conntrack モジュール(-m conntrack --ctstate)が推奨されています。

iptablesルールの管理コマンド

ここまで-A(Append)を中心に扱ってきましたが、iptablesにはルールを管理するための様々なオプションがあります。

オプション意味
-A チェーン指定したチェーンの末尾にルールを追加する
-I チェーン [番号]指定した位置(省略時は先頭)にルールを挿入する
-D チェーン [番号]指定したルールを削除する(ルール内容の完全一致指定、または番号指定のいずれかで削除できる)
-R チェーン 番号指定した位置のルールを置き換える
-F [チェーン]指定したチェーン(省略時は全チェーン)のルールをすべて削除する(フラッシュ)
-N チェーン名ユーザー定義の新しいチェーンを作成する
-X [チェーン名]ユーザー定義チェーンを削除する(空である必要がある)
$ sudo iptables -L INPUT --line-numbers    # 各ルールに行番号を表示(削除・挿入位置の指定に使う)
Chain INPUT (policy DROP)
num  target     prot opt source               destination
1    ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:22
2    ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:80
3    ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:443
$ sudo iptables -D INPUT 3                 # INPUTチェーンの3番目のルールを削除(正常時は無出力)
$ sudo iptables -C INPUT -p tcp --dport 22 -j ACCEPT
# -Cは指定したルールがすでに存在するかを確認する(Check)。スクリプトで冪等性を保つ際に有用

この行のこの値に注目: --line-numbersを付けると、各ルールの先頭にnum列(行番号)が表示され、この番号を-D INPUT 3のように指定して個別に削除できます。-Cは該当ルールが存在すれば終了コード0(画面には何も表示されない)、存在しなければ終了コード1を返すだけで、こちらも成功・失敗いずれの場合も標準出力には何も表示されません。

ユーザー定義チェーン(-N)は、複数の関連するルールを1つの名前でまとめておき、標準のチェーンから-j チェーン名でジャンプさせることで、ルールセット全体の見通しを良くする際に使われます。

$ sudo iptables -N SSH_CHECKS                       # 正常時は無出力
$ sudo iptables -A SSH_CHECKS -m recent --name sshattempt --rcheck --seconds 60 --hitcount 4 -j DROP  # 正常時は無出力
$ sudo iptables -A INPUT -p tcp --dport 22 -j SSH_CHECKS  # 正常時は無出力

マッチ拡張によるルールの高度化

iptablesは-mオプションで様々な「マッチ拡張」モジュールを読み込み、単純なポート番号やIPアドレスだけでなく、より複雑な条件でパケットをマッチさせられます。

マッチ拡張用途
-m multiport複数の非連続なポート番号を1つのルールでまとめて指定する
-m limit一定時間あたりのマッチ回数を制限する(DoS攻撃やICMP floodの緩和に使う)
-m recent直近に一致したパケットの送信元IPを記録し、一定時間内の再接続回数などで判定する(簡易的なブルートフォース対策)
-m stringパケットのペイロード(データ部分)に特定の文字列が含まれるかで判定する
-m commentルールに管理用のコメントを付与する(動作には影響しない)
$ sudo iptables -A INPUT -p tcp -m multiport --dports 80,443,8080 -j ACCEPT   # 正常時は無出力
# 80番・443番・8080番の3ポートを1行でまとめて許可

$ sudo iptables -A INPUT -p icmp --icmp-type echo-request -m limit --limit 5/s --limit-burst 10 -j ACCEPT   # 正常時は無出力
# ICMP echo-request(ping)を1秒あたり5パケットまでに制限し、ICMP floodによるDoSを緩和する

$ sudo iptables -A INPUT -p tcp --dport 22 -m recent --set --name sshattempt   # 正常時は無出力
$ sudo iptables -A INPUT -p tcp --dport 22 -m recent --update --seconds 60 --hitcount 4 --name sshattempt -j DROP   # 正常時は無出力
# 60秒以内に4回以上SSHへ接続を試みた送信元を一時的にブロックする(fail2banに近い挙動を素のiptablesだけで実現)

$ sudo iptables -A INPUT -p tcp --dport 22 -m comment --comment "allow SSH from office" -j ACCEPT   # 正常時は無出力

この行のこの値に注目: ここまでの-Aによるルール追加は、いずれも成功時には画面に何も表示されません。ルールが実際に反映されたかどうかは、sudo iptables -L -n -vやiptables-saveで改めて確認する必要があります。-m recent --setで送信元IPを記録し始めた通信は、/proc/net/xt_recent/sshattemptを参照すると、記録済みのIPアドレスと最終アクセス時刻を直接確認できます。

ログ出力(-j LOG)

破棄・拒否したパケットの記録を残したい場合、LOGターゲットを使います。

$ sudo iptables -A INPUT -j LOG --log-prefix "iptables-denied: " --log-level 4   # 正常時は無出力
$ sudo iptables -A INPUT -j DROP   # 正常時は無出力

--log-prefixは、ログの各行の先頭に付ける識別用の文字列で、後でgrepする際の目印になります。--log-levelはsyslogの重要度(4はwarning相当)です。

$ sudo tail -f /var/log/kern.log | grep --line-buffered "iptables-denied"
Aug 21 09:14:02 web01 kernel: iptables-denied: IN=eth0 OUT= MAC=52:54:00:12:34:56 SRC=198.51.100.23 DST=203.0.113.10 LEN=60 TOS=0x00 PREC=0x00 TTL=48 ID=41231 PROTO=TCP SPT=51422 DPT=23 WINDOW=1024 RES=0x00 SYN URGP=0

この行のこの値に注目: ログには--log-prefixで指定した文字列に続けて、SRC(送信元IP)・DST(宛先IP)・DPT(宛先ポート)などパケットの詳細が記録されます。この例では、23番ポート(Telnet)宛の不審な接続がDROPされたことが分かります。RHEL系では/var/log/kern.logの代わりにjournalctl -kや/var/log/messagesを確認します。

LOGは終端ターゲットではない: ACCEPTやDROPと違い、LOGターゲットはパケットの処理を確定させません。LOGルールにマッチした後も、そのパケットは引き続き後続のルールの評価対象になります。そのため、上記の例のように「まずLOGでログを記録し、その直後に同じ条件でDROP(またはACCEPT)する」という2行1組の書き方が定石です。LOGルールだけを置いて後続にDROP/ACCEPTを書き忘れると、そのパケットはさらに後続の別のルールに評価が進んでしまいます。

ルールの保存と復元の詳細

iptablesコマンドで追加したルールは、メモリ上のカーネル内テーブルに反映されるだけで、再起動すると消えてしまいます。永続化には専用の保存・復元コマンドを使います。

$ sudo iptables-save > /etc/iptables/rules.v4     # 現在のルールをファイルに保存(正常時は無出力、リダイレクト先のファイルに書き込まれる)
$ sudo iptables-restore < /etc/iptables/rules.v4   # ファイルからルールを復元(正常時は無出力)

iptables-saveが出力する形式は、実際のiptablesコマンドの引数とは異なる独自の書式になっています。

$ sudo iptables-save
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [120:8400]
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
COMMIT

この行のこの値に注目: *filterはここからfilterテーブルの内容が始まることを示します。:INPUT DROP [0:0]のような行は、そのチェーンのデフォルトポリシーと、これまでにマッチしたパケット数・バイト数のカウンタ(この例ではリセット直後で0:0)を示します。-Aで始まる各行が実際のルールで、末尾のCOMMITで、そのテーブルの内容を確定させます。

この永続化の仕組みは、ディストリビューションによって使うパッケージが異なります。

系統永続化の方法
Debian系netfilter-persistent(または旧来のiptables-persistent)パッケージが、起動時に/etc/iptables/rules.v4・rules.v6を自動的にiptables-restoreする
RHEL系iptables-servicesパッケージが、/etc/sysconfig/iptablesをiptables.service経由で読み込む(新しい環境ではfirewalldが標準)

nftablesの基本例

$ sudo nft list ruleset                 # 現在のルールセットを表示
table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;
        ct state established,related accept
        tcp dport 22 accept
    }
}
$ sudo nft add rule inet filter input tcp dport 22 accept   # 正常時は無出力

この行のこの値に注目: nft list rulesetの出力は、そのまま/etc/nftables.confに書ける構文で表示されるのが特徴です。iptablesのiptables-saveと違い、独自の中間形式ではなく、実際に読み込み可能な設定ファイルの構文そのものが出力される点がnftablesの利点の1つです。

nftablesは設定ファイル(既定で /etc/nftables.conf)にルールセット全体を記述し、一括で読み込むスタイルが一般的です。

/etc/nftables.conf の例
table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;
        ct state established,related accept
        tcp dport 22 accept
        iif "lo" accept
    }
}
$ sudo nft -f /etc/nftables.conf     # 設定ファイルを読み込んで一括適用(正常時は無出力)

構文エラーがある場合は、該当行を示すエラーメッセージが表示され、ルールセット全体が反映されません。

$ sudo nft -f /etc/nftables.conf
/etc/nftables.conf:6:20-23: Error: syntax error, unexpected string, expecting end of file or newline or semicolon
        tcp dport ssh accep
                   ^^^^

この行のこの値に注目: acceptをaccepと誤って入力したことで構文エラーになっている例です。エラー行番号(この例では6行目)と、誤りの位置を示す^^^^が表示されるため、設定ファイルのどこを修正すべきかがすぐに分かります。iptables-restoreと同様、nftablesも一部だけ反映されるのではなく、エラーがあれば設定全体が反映されません。

nftablesではiptablesと違い、テーブルのfamily(ip・ip6・inet・arp・bridgeなど)を明示的に指定します。inetファミリーを使うと、IPv4とIPv6の両方を1つのルールセットでまとめて扱えるのが大きな利点です。また、フィルタだけでなくcounterを挟むことで、そのルールに何個・何バイトのパケットがマッチしたかを記録できます。

chain input {
    type filter hook input priority 0; policy drop;
    ct state established,related accept
    tcp dport 22 counter accept
    log prefix "nft-drop: " drop
}

上の例では、SSH宛のパケットにマッチした数をカウントしつつ許可し、それ以外はログに記録してから破棄しています。ログはsyslog経由で /var/log/kern.log などに記録され、後から不審な通信の傾向を分析するのに役立ちます。

nftablesへの移行

既存のiptablesルールをnftablesへ書き換える作業を支援するため、iptables-translateというツールが用意されています。

$ iptables-translate -A INPUT -p tcp --dport 22 -j ACCEPT
nft add rule ip filter INPUT tcp dport 22 counter accept

このツールは1行ずつの変換を機械的に行うもので、複雑なルールセット全体を完全に等価な形へ変換できるとは限らないため、変換結果は必ず目視で確認する必要があります。

現在の主要ディストリビューションでは、iptablesコマンド自体が、実際にはnftablesのバックエンド(カーネルのnftablesサブシステム)を使って動作するiptables-nftという互換レイヤーに置き換えられていることが一般的です。従来通りのカーネルモジュール(ip_tables)を直接操作する実装はiptables-legacyと呼ばれ、区別されます。

$ update-alternatives --list iptables    # iptables-nftとiptables-legacyのどちらが使われているか確認(Debian系)
/usr/sbin/iptables-legacy
/usr/sbin/iptables-nft
$ sudo update-alternatives --set iptables /usr/sbin/iptables-legacy
update-alternatives: using /usr/sbin/iptables-legacy to provide /usr/sbin/iptables (iptables) in manual mode

この行のこの値に注目: --listには現在切り替え可能な実体(この例ではiptables-legacyとiptables-nft)が列挙されます。--setで切り替えた際に表示されるusing ... in manual modeは、今後自動更新(--auto)で勝手に切り替わらないよう、手動で固定したことを示しています。

iptables-nftとiptables-legacyの共存問題: 同一システム上で、あるツール・スクリプトがiptables-legacyでルールを追加し、別のツール(Dockerなど)がiptables-nft(あるいは直接nftコマンド)でルールを追加していると、それぞれが異なるバックエンドのルールセットを認識・変更しているため、iptables -Lで見えているルールと、実際にカーネルで動作しているルールセットが食い違う、意図した通りに動作しないといった混乱を招くことがあります。1つのシステム内では、iptables系コマンドの実装(legacy/nft)を統一しておくことが推奨されます。

nftablesのセット(set)と辞書(map)

nftablesには、iptablesにはなかった強力な機能として、複数の値をまとめて扱えるセット(set)と、キーと値の対応を扱える辞書(map)があります。

許可するポートをセットとして定義し、1つのルールで判定する例
table inet filter {
    set allowed_ports {
        type inet_service
        elements = { 22, 80, 443 }
    }
    chain input {
        type filter hook input priority 0; policy drop;
        tcp dport @allowed_ports accept
    }
}

この例では、allowed_portsというセットに登録された複数のポート番号を、1本のルール(tcp dport @allowed_ports accept)でまとめて判定しています。iptablesであれば-m multiportや複数行のルールが必要だった処理を、より簡潔かつ高速(カーネル内部でハッシュテーブルとして扱われるため、要素数が多くても検索が高速)に表現できます。

送信元IPごとに異なる処理をする辞書(map)の例
map vip_clients {
    type ipv4_addr : verdict
    elements = { 192.168.1.100 : accept, 192.168.1.101 : accept }
}
chain input {
    type filter hook input priority 0; policy drop;
    ip saddr vmap @vip_clients
}

辞書(map)は、キー(この例では送信元IPアドレス)ごとに異なる処理(verdict、この例ではaccept)を1つのルールで表現できる仕組みで、多数の個別条件を大量のルールとして書き並べる必要があったiptablesの構成を、大幅に簡潔化できます。

NAT(アドレス変換)の基礎

filterテーブルがパケットを通す・止めるを扱うのに対し、natテーブルはパケットの送信元・宛先アドレスを書き換える処理を扱います。ルーターとして動作させ、複数の内部ホストが1つのグローバルIPアドレスを共有してインターネットに出ていく「IPマスカレード(送信元NAT)」もこの仕組みで実現します。

$ sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE   # 正常時は無出力
# eth0から出ていくパケットの送信元アドレスを、eth0自身のアドレスに書き換える

この機能を使うには、カーネル側でIPフォワーディングを有効にしておく必要があります(net.ipv4.ip_forward = 1、第2章のsysctlの知識とつながります)。

ポートリダイレクト(DNAT)

外部から特定のポートへ届いたパケットを、別のホストや別のポートへ転送したい場合、natテーブルのDNAT(Destination NAT)ターゲットを使います。

$ sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 192.168.1.10:8080   # 正常時は無出力
# 80番ポート宛のパケットを、内部の192.168.1.10の8080番ポートへ転送する

同一ホスト内の別のポートへ転送するだけであれば、より単純なREDIRECTターゲットも使えます。

$ sudo iptables -t nat -A PREROUTING -p tcp --dport 2222 -j REDIRECT --to-port 22   # 正常時は無出力
# 2222番ポート宛のパケットを、同一ホストの22番ポートへリダイレクトする

ルールが実際に反映されたかどうかはsudo iptables -t nat -L -n -vで確認します。

$ sudo iptables -t nat -L PREROUTING -n -v
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
    0     0 DNAT       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:80 to:192.168.1.10:8080
    0     0 REDIRECT   tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:2222 redir ports 22

この行のこの値に注目: target列にDNAT・REDIRECTが表示され、右端のto:・redir portsに、それぞれ書き換え後の宛先が示されます。-t natを付けずにiptables -Lを実行するとfilterテーブルしか表示されないため、NATルールの確認には必ず-t natの指定が必要です。

ターゲット用途
DNAT宛先ホスト・ポートを別のホスト・ポートへ書き換える(内部の別サーバーへの転送に使う)
REDIRECT宛先を同一ホストの別ポートへ書き換える(DNATの特殊ケースとして実装されている)

DNAT/REDIRECTによって宛先が書き換えられたパケットへの戻りパケットは、conntrack(コネクション追跡)機能が自動的に処理し、送信元に対しては書き換え前の元の宛先から応答が返っているかのように見せかけます。管理者が戻り方向のNATルールを別途書く必要はありません。

よくある間違い: DNATルールをnatテーブルに追加しただけで満足してしまい、転送先ホストへのパケットがFORWARDチェーンのfilterテーブルで拒否されてしまうケースがよくあります。DNATはあくまで宛先アドレスを書き換える処理であり、書き換え後のパケットが実際に転送されるには、FORWARDチェーンでもそのパケットを許可するルールが別途必要です。

SNATとMASQUERADEの使い分け

すでに触れたMASQUERADEは、送信元アドレスを書き換えるSNAT(Source NAT)の一種ですが、より直接的なSNATターゲットも存在します。

$ sudo iptables -t nat -A POSTROUTING -o eth0 -j SNAT --to-source 203.0.113.10   # 正常時は無出力
# eth0から出ていくパケットの送信元を、固定IPアドレス203.0.113.10に書き換える
$ sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE   # 正常時は無出力
# eth0のその時点でのIPアドレスを動的に調べて送信元として使う

この行のこの値に注目: SNATは書き換え先のIPアドレスを設定に明示的に固定値で書くのに対し、MASQUERADEはパケットを送信するインターフェースの、その時点でのIPアドレスを動的に調べて使います。固定のグローバルIPアドレスを持つ環境(多くのオンプレミス・サーバー環境)では、インターフェースのアドレスをその都度動的に調べる処理が不要な分だけ、SNATの方がわずかに高速に動作します。一方、DHCPやPPPoEなどでIPアドレスが動的に変わりうる環境(家庭用ルーターなど)では、IPアドレスの変化を自動的に追従してくれるMASQUERADEの方が適しています。

① DNAT(宛先アドレスの書き換え) 外部クライアント 203.0.113.5 宛先:ルーター:80 ルーター PREROUTING: DNAT 宛先アドレスを書き換え 宛先:192.168.1.10:8080 内部サーバー 192.168.1.10:8080 ② SNAT/MASQUERADE(送信元アドレスの書き換え) 内部クライアント 192.168.1.20 送信元:192.168.1.20 ルーター POSTROUTING: SNAT/ MASQUERADE 送信元:203.0.113.10 外部サーバー (インターネット上) ※戻りパケットは、conntrack(コネクション追跡)が書き換えを自動的に元に戻すため、 管理者が戻り方向のルールを別途書く必要はない。
NAT(SNAT/MASQUERADEとDNAT)によるアドレス書き換え。DNATは外部から届いたパケットの宛先アドレスをPREROUTINGで内部サーバーへ書き換え、SNAT/MASQUERADEは内部から出ていくパケットの送信元アドレスをPOSTROUTINGでルーターのグローバルIPへ書き換えます。戻りパケットの書き換えはconntrackが自動的に処理します。

IPフォワーディングの有効化

NATやFORWARDチェーンを使ってルーターとして機能させるには、まずカーネルのIPフォワーディング機能を有効にしておく必要があります。

$ sudo sysctl net.ipv4.ip_forward=1               # IPv4のフォワーディングを一時的に有効化
net.ipv4.ip_forward = 1
$ sudo sysctl net.ipv6.conf.all.forwarding=1       # IPv6のフォワーディングを一時的に有効化
net.ipv6.conf.all.forwarding = 1

この行のこの値に注目: sysctl 名前=値の形式で値を書き込むと、設定後の値がそのまま名前 = 値の形式でエコーバックされます。この出力が表示されれば、その場でカーネルパラメータが変更されたことが確認できます(ただし前述の通り、再起動すると失われる一時的な変更です)。

この設定はsysctlの一時変更にすぎず、再起動すると失われます。恒久的に有効化するには、/etc/sysctl.d/にドロップインファイルを作成します。

/etc/sysctl.d/99-ip-forward.conf
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
$ sudo sysctl --system     # /etc/sysctl.d/以下を含めてすべて再読み込み
* Applying /usr/lib/sysctl.d/50-pid-max.conf ...
kernel.pid_max = 4194304
* Applying /etc/sysctl.d/99-ip-forward.conf ...
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
* Applying /etc/sysctl.conf ...

この行のこの値に注目: * Applying ファイル名 ...という行が、読み込んだ設定ファイルごとに順番に表示されます。複数のファイルに同じパラメータが書かれている場合、後から読み込まれたファイルの値で上書きされるため、/etc/sysctl.d/99-ip-forward.confが意図通り最後に適用されているかをこの出力で確認できます。

IPフォワーディングを有効にせずにDNAT/FORWARDのルールだけを設定しても、カーネル自体がパケットを別インターフェースへ転送する処理を行わないため、ルーティングは機能しません。ルーターやVPNゲートウェイとして構築する際は、必ずこの設定が有効になっているかを最初に確認します。

その他のアクセス制御

アプリケーション単位での簡易的なアクセス制御として、古くから /etc/hosts.allow と /etc/hosts.deny(TCP Wrappers)を使う方法もあります(対応するサービスは限られます)。

/etc/hosts.allow
sshd: 192.168.1.0/24

/etc/hosts.deny
sshd: ALL

ファイアウォールルールの設定手順としての鉄則

サーバー作業の中でも、ファイアウォール設定はリモート接続そのものを切断してしまうリスクが特に高い操作です。安全に作業するための基本原則をまとめておきます。

  • 先に許可ルールを確認・追加してから、デフォルトポリシーを厳格化する: 「まずSSHを許可 → 動作確認 → その後にdrop/denyへ切り替え」の順で進め、逆の順序は避けます。
  • 一定時間後に自動でルールを元に戻す仕組みを併用する: atコマンドで「5分後に設定をリセットする」ジョブを仕込んでおき、もし新しいルールで接続できなくなっても自動的に復旧できるようにしておくと安心です。
  • コンソール(KVMやクラウドのシリアルコンソール)へのアクセス手段を事前に確保しておく: SSH経由の操作で自分自身を締め出してしまった場合の最終手段として、SSH以外の経路でサーバーにアクセスできる状態を確認しておきます。

これらは地味に見える手順ですが、「ファイアウォール設定でリモートサーバーから自分を締め出してしまった」という失敗は経験者の間でも語り草になるほどよくある事故であり、事前の備えが最も効果的な対策です。

フロントエンドツールとしてのufw/firewalld

iptables/nftablesは強力ですが、ルール記述がやや複雑になりがちです。そのため多くのディストリビューションでは、より扱いやすいフロントエンドツールが標準で用意されています。Ubuntu系ではufw(Uncomplicated Firewall)、RHEL/CentOS系ではfirewalldがよく使われます。

$ sudo ufw allow 22/tcp       # SSHを許可
Rule added
Rule added (v6)
$ sudo ufw enable            # ファイアウォールを有効化
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup
$ sudo ufw status verbose    # 現在のルールを確認
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW IN    Anywhere
22/tcp (v6)                ALLOW IN    Anywhere (v6)

この行のこの値に注目: ufw enable実行時には「既存のSSH接続が切断される可能性がある」という確認プロンプトが表示されます。ここでyと答える前に、必ずufw allow 22/tcpなどSSHの許可ルールを先に入れておかないと、有効化した瞬間にリモート接続が切れて操作不能になります。status verboseのDefault:行は、明示的なルールがないポートへの受信(deny)・送信(allow)・転送(disabled)それぞれのデフォルト動作を示します。

これらのツールは内部的にはiptables/nftablesのルールを生成しており、直接ルールを書く場合と実現できることは大きくは変わりません。ただし、複数のツールを併用すると意図しないルールの上書きや競合が起きやすいため、1台のサーバーではどちらか一方の管理方式に統一しておくのが運用上の鉄則です。

firewalldのゾーンとfirewall-cmd

RHEL系ディストリビューションで標準的に使われるfirewalldは、ネットワークインターフェースを「信頼度」に応じたゾーンに割り当てて管理するのが最大の特徴です。

ゾーン想定する信頼度
trustedすべての通信を許可する、最も信頼できるネットワーク
internal社内ネットワークなど、ある程度信頼できる内部ネットワーク
publicインターネットなど、信頼できない公共のネットワーク(既定のゾーン)
drop受信パケットをすべて無条件で破棄する、最も制限が厳しいゾーン
$ sudo firewall-cmd --get-active-zones          # 現在有効なゾーンとインターフェースの対応を表示
public
  interfaces: eth0
$ sudo firewall-cmd --zone=public --list-all     # publicゾーンの現在の設定を表示
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: eth0
  sources:
  services: ssh dhcpv6-client
  ports:
  protocols:
  forward: no
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:

$ sudo firewall-cmd --zone=public --add-service=http --permanent
success
# publicゾーンでHTTPサービスを永続的に許可
$ sudo firewall-cmd --zone=public --add-port=8080/tcp --permanent
success
# 個別のポート番号でも許可できる
$ sudo firewall-cmd --reload                     # --permanentで指定した変更を実際に反映
success

この行のこの値に注目: --list-allのservices行には、サービス名で許可された項目(この例ではsshと、DHCPv6応答を受け取るためのdhcpv6-clientが既定で許可済み)が並びます。--add-service・--add-port・--reloadのように設定を変更するコマンドは、成功すると必ずsuccessという1行が返るのがfirewall-cmdの特徴で、この文字列が返ってこない場合は何らかのエラーが発生しています。

この行のこの値に注目: --permanentを付けずに実行したコマンドは、現在稼働中の設定にのみ即座に反映されますが、次回のfirewall-cmd --reloadや再起動で消えてしまいます。--permanentを付けた変更は永続化されますが、その場では反映されず、--reloadを実行して初めて有効になります。両方の効果を同時に得たい場合は、--permanent付きで設定した後に--reloadを実行するのが基本パターンです。

単純なサービス名・ポート番号の許可だけでなく、送信元アドレスや複数条件を組み合わせた、より柔軟なリッチルールも記述できます。

$ sudo firewall-cmd --zone=public --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" service name="ssh" accept' --permanent
success
# 192.168.1.0/24からのSSH接続のみを許可するリッチルール
$ sudo firewall-cmd --reload
success

この行のこの値に注目: リッチルールも通常の--add-serviceなどと同様に--permanent付きで登録した後、--reloadで反映させる2段階の手順が必要です。設定ミスでYAML/XMLの構文が誤っている場合はsuccessではなくエラーメッセージが表示されるため、successが返ってきたことを確認してから次の作業に進みます。

確認クイズ

Q1. Linuxのパケットフィルタリング機能で、iptablesの後継として位置づけられているフレームワークはどれですか?

解説: nftablesはiptablesの後継として開発された、より統一的なパケットフィルタリングフレームワークです。

Q2. iptablesのINPUTチェーンが対象とするパケットはどれですか?

解説: INPUTチェーンは、このホスト自身が宛先になっている受信パケットを対象とします。転送パケットはFORWARDチェーンです。

Q3. iptablesでファイアウォールルールを設定する際、最後にDROPルールを追加する前に注意すべきことはどれですか?

解説: 許可ルールより先にDROPルールを置いてしまう、あるいは必要なポートの許可を忘れると、リモートからのアクセス自体ができなくなる恐れがあります。

Q4. 設定ファイルの/etc/hosts.allow と /etc/hosts.deny による古くからのアクセス制御の仕組みを何と呼びますか?

解説: TCP Wrappersは、対応するサービスに限定されるものの、/etc/hosts.allowと/etc/hosts.denyでアクセス制御を行う古くからの仕組みです。

Q5. 転送パケット(ルーターとして中継するだけの通信)が通過するnetfilterのチェーンの並びとして正しいものはどれですか?

解説: 転送パケットはPREROUTING → FORWARD → POSTROUTINGの順に通過します。ローカル宛パケットはPREROUTING → INPUT、ローカル発パケットはOUTPUT → POSTROUTINGを通過します。

Q6. DNAT(宛先アドレスの書き換え)とSNAT/MASQUERADE(送信元アドレスの書き換え)がそれぞれ評価されるフックの組み合わせとして正しいものはどれですか?

解説: DNATはルーティング先が決まる前のPREROUTINGで、SNAT/MASQUERADEはルーティング先が決まった後のPOSTROUTINGで評価されます。

Q7. IPv6のICMPv6を安易に全面ブロックしてはいけない理由はどれですか?

解説: ICMPv6は近隣探索やRouter Advertisementなど、IPv6の基本的な通信そのものに不可欠な役割を担っているため、全面的にブロックすると同一セグメント内の通信が成立しなくなることがあります。

Q8. IPv6版のプライベートアドレスに相当し、組織内部専用でインターネット上ではルーティングされないアドレス範囲はどれですか?

解説: fc00::/7はULA(Unique Local Address)と呼ばれ、IPv4のRFC1918プライベートアドレスに相当する、組織内部専用のIPv6アドレス範囲です。

Q9. iptablesのDNATとREDIRECTターゲットの違いとして正しいものはどれですか?

解説: DNATは任意のホスト・ポートへ宛先を書き換えられますが、REDIRECTは同一ホストの別ポートへの書き換えに限定された特殊ケースです。

Q10. DNATルールを設定したにもかかわらず、転送先ホストへのパケットが届かない場合に確認すべきこととして適切なものはどれですか?

解説: DNATは宛先アドレスを書き換える処理にすぎず、書き換え後のパケットが実際に転送されるにはFORWARDチェーンでの許可が別途必要です。

Q11. 固定のグローバルIPアドレスを持つ環境でSNATがMASQUERADEよりわずかに高速とされる理由はどれですか?

解説: MASQUERADEはパケット送信時にインターフェースのIPアドレスを動的に調べる処理が必要ですが、SNATは固定値を使うためその分だけ高速です。

Q12. IPv6のフォワーディングを恒久的に有効化するために/etc/sysctl.d/に設定するパラメータはどれですか?

解説: net.ipv6.conf.all.forwardingがIPv6のフォワーディングを有効化するパラメータです。IPv4の場合はnet.ipv4.ip_forwardを使います。

Q13. iptablesで、指定したルールがすでに存在するかどうかを確認するオプションはどれですか?

解説: -C(Check)は指定したルールがすでに存在するかを確認するオプションで、スクリプトで冪等性を保つ際に有用です。

Q14. iptablesの-m limitマッチ拡張の主な用途はどれですか?

解説: -m limitは一定時間あたりのマッチ回数を制限するマッチ拡張で、DoS攻撃やICMP floodの緩和によく使われます。

Q15. iptablesのLOGターゲットについて正しい説明はどれですか?

解説: LOGはACCEPT/DROPと異なり終端ターゲットではないため、LOGでログを記録した後、通常は同じ条件でDROP/ACCEPTするルールを続けて書く必要があります。

Q16. iptables-saveの出力にある末尾のCOMMIT行の意味はどれですか?

解説: COMMIT行は、そのテーブル(*filterなど)の内容をここまでで確定させることを示します。

Q17. Debian系でiptablesルールを起動時に自動的に復元する仕組みを提供する代表的なパッケージはどれですか?

解説: Debian系ではnetfilter-persistent(旧iptables-persistent)が、起動時に保存済みルールを自動的にiptables-restoreします。iptables-servicesはRHEL系の仕組みです。

Q18. iptables-nftとiptables-legacyの違いとして正しいものはどれですか?

解説: iptables-nftは内部的にnftablesのカーネルサブシステムを使う互換レイヤー、iptables-legacyは従来通りip_tablesモジュールを直接操作する実装です。混在させるとルールセットの食い違いが起きることがあります。

Q19. nftablesのセット(set)を使う利点はどれですか?

解説: セットは複数の値をまとめて扱え、カーネル内部でハッシュテーブルとして実装されるため、多数の値を持つ場合でも高速に判定できます。

Q20. nftablesの辞書(map)が実現できることはどれですか?

解説: mapはキーと値(verdict)の対応を定義し、キーごとに異なる処理を1つのルールで表現できるため、大量の個別ルールを簡潔化できます。

Q21. firewalldにおけるゾーンの概念として正しいものはどれですか?

解説: firewalldは、ネットワークインターフェースを信頼度(trusted/internal/public/dropなど)に応じたゾーンに割り当て、ゾーンごとに異なるルールセットを適用します。

Q22. firewall-cmdで--permanentを付けて変更を加えた場合、その変更を実際に反映させるために必要な操作はどれですか?

解説: --permanentを付けた変更は永続化されますがその場では反映されないため、firewall-cmd --reloadを実行して初めて有効になります。

Q23. firewalldのリッチルールが実現できることはどれですか?

解説: リッチルールは、送信元アドレスなど複数条件を組み合わせた、より柔軟なルール記述を可能にする機能です。