第23章 ルーター設定とパケットフィルタリング
LPIC-2 212.1 相当
サーバーを外部の脅威から守る最初の砦が、ファイアウォール(パケットフィルタリング)です。ここではLinuxカーネルのパケットフィルタリング機能を扱います。
iptables と nftables
Linuxカーネルには、パケットを通す・止める・変換するといった処理を行うフィルタリング機能があります。長年 iptables が使われてきましたが、近年は後継の nftables への移行が進んでいます。
| 項目 | iptables | nftables |
|---|---|---|
| 位置づけ | 従来の標準(多くの解説記事や試験範囲で今も扱われる) | 後継の統一されたフレームワーク |
| IPv4/IPv6 | ip6tablesなど別コマンドが必要 | 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のようなルール追加コマンド自体は、成功しても画面には何も表示されません。
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 |
それぞれのフック(チェーン)で、どのテーブルが評価されるかも決まっています。
| フック | 評価されるテーブル(優先順) |
|---|---|
| PREROUTING | raw → mangle → nat(DNAT) |
| INPUT | mangle → nat → filter |
| FORWARD | mangle → filter |
| OUTPUT | raw → mangle → nat → filter |
| POSTROUTING | mangle → nat(SNAT/MASQUERADE) |
この行のこの値に注目: 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経由の接続は通りません。
プライベート・特殊用途のアドレス範囲
ファイアウォールルールや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を確認します。
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 -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ルールを別途書く必要はありません。
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の方が適しています。
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が返ってきたことを確認してから次の作業に進みます。