デプロイを何度も壊したファイアウォール
半年前、私は3つの異なるクラウドプロバイダーにまたがる12台のLinux VPSインスタンスを管理していた。各サーバーにはiptablesルールが複雑に絡み合っていた――Stack Overflowからコピーしたもの、前任のシステム管理者から引き継いだもの、誰が書いたか誰も覚えていないものまで。毎回のデプロイは賭けだった。ip6tablesのルール漏れがあれば、IPv6トラフィックが静かにフィルタリングされないまま放置された。KVMホストでのarptablesの不一致が、誰も気づかないまま何時間も内部ルーティングを壊し続けることもあった。
VPSフリートを3年間管理して学んだことがひとつある。本番に触れる前に必ずすべてテストせよ、ということだ。しかし慎重なテストをもってしても、iptablesの根本的な設計上の問題は解消できない。ひとつのファイアウォールを管理しているのではなく、互いに何も知らない4つの独立したサブシステムを同時に扱わなければならないのだ。
iptablesが時代遅れになりつつある理由
私がファイアウォールで頭を悩ませる原因のほとんどは、iptablesの設計に起因していた――IPv6がまだ理論上の存在で、ARPフィルタリングが後付けだった、Linux 2.4時代に作られたものだ。
モダンなサーバーでiptablesを使う場合、実際に管理しなければならないのはこれだけある:
- iptables — IPv4パケットフィルタリングとNAT
- ip6tables — まったく同じルール、ただしIPv6用
- arptables — ARPレベルのフィルタリング
- ebtables — イーサネットブリッジフィルタリング
IPv4用に書いたルールはIPv6には一切効果がない。ポリシーを変更するたびに、少なくとも2つの独立したルールセットを編集しなければならない。iptablesにセキュリティ修正を適用しながら、ip6tablesをまるごと忘れてしまうことが驚くほど簡単に起きる。IPv4では鉄壁に固められているのに、IPv6アドレスはどこからでも接続を受け付けている本番サーバーを、私は実際に見てきた。
負荷の高いサーバーではパフォーマンスが予測どおりに劣化する。パケットの評価のたびにルールのリニアリストを走査するからだ。マルチテナント環境では珍しくない300件超のルールを超えると、レイテンシのベンチマークにオーバーヘッドとして現れてくる。ルールのリロードも真の意味でアトミックではない。iptables-restoreはそれに近いが、リロード中に一瞬、トラフィックが意図せずすり抜けたりドロップしたりするウィンドウが存在する。
代替手段:実際に何を選ぶべきか
この問題を解決しようと決意したとき、私は3つの選択肢に絞った。
選択肢1:よりよいツールでiptablesを使い続ける
fermやiptables-save/restoreスクリプトといったツールを使えば重複を減らせる。ルールを一度書いてiptablesとip6tablesの両方の出力を生成するわけだ。機能はするが、ひびの入った土台に継ぎを当てているに過ぎない。リニア評価の問題はなくならないし、arptables/ebtablesは依然として別物のままだ。
選択肢2:UFWまたはFirewalld
UFW(Ubuntu)とfirewalld(RHEL/Fedora)は、裏でiptablesのルールを生成する高レベルの抽象化レイヤーだ。シンプルなサーバー――ポート22を開ける、HTTPを許可する、それ以外はすべてブロックする――には問題ない。しかしNATルールやカスタムチェーン、あるいは基本的な許可/拒否以上のことが必要になった瞬間、抽象化レイヤーと格闘する羽目になる。Firewalldはすでに最近のバージョンでnftablesバックエンドを追加した。行き先を示すヒントだ。
選択肢3:nftables — モダンな後継者
nftablesはiptables、ip6tables、arptables、ebtablesをひとつのフレームワークに統合する。ルール評価はリニアリストではなくセットベースのデータ構造を使うため、数百件のルールを超えると劇的に高速になる。アトミックなルール置換が近似ではなくネイティブに機能する。構文は十分に読みやすく、新しいエンジニアがマニュアルを参照せずにルールセットを理解できる。
カーネル3.13(2014年1月)でマージされたnftablesは、現在Debian 10以降、RHEL 8以降、Fedora 32以降でデフォルトになっている。主要なディストリビューションはすべてすでにコミットしている。
nftablesを始める
最近のディストリビューションのほとんどでは、nftablesはすでにインストールされている。現在の状態を確認しよう:
nft list ruleset
出力が空ならルールセットが空の状態だ――クリーンなスタートを切れる。iptablesから移行する場合は変換ツールを使う:
# 既存のiptablesルールをnftables構文に変換する
iptables-save | iptables-translate > /tmp/nftables-migrated.nft
ip6tables-save | ip6tables-translate >> /tmp/nftables-migrated.nft
# 適用前に内容を確認する
cat /tmp/nftables-migrated.nft
基本的なルールセット構造
nftablesはルールをチェーンを持つテーブルに整理する。テーブルがfilter、nat、mangleとしてハードコードされているiptablesとは異なり、nftablesではテーブルに自由に名前をつけ、アドレスファミリーを明示的に選択できる。
# シンプルなinetテーブルを作成する(IPv4とIPv6の両方を処理)
nft add table inet main_firewall
# デフォルトdropポリシーでinputチェーンを追加する
nft add chain inet main_firewall input { type filter hook input priority 0\; policy drop\; }
# 確立済み接続を許可する
nft add rule inet main_firewall input ct state established,related accept
# ループバックを許可する
nft add rule inet main_firewall input iif lo accept
# SSHを許可する
nft add rule inet main_firewall input tcp dport 22 accept
# HTTPとHTTPSを許可する
nft add rule inet main_firewall input tcp dport { 80, 443 } accept
inetファミリーに注目してほしい――ひとつのチェーンがIPv4とIPv6を同時に処理する。このたった一つの変更で「ip6tablesの更新忘れ」というバグのクラス全体が消える。
設定ファイルを使う
個別のnft addコマンドはテスト用には問題ない。本番のルールは/etc/nftables.confに置くべきだ。iptablesが生成するどんなものより構文がはるかにクリーンだ:
#!/usr/sbin/nft -f
flush ruleset
table inet firewall {
# セットを使って既知の悪意あるIPをブロックする — ルールの重複なし
set blocklist {
type ipv4_addr
flags interval
elements = { 192.168.100.0/24, 10.0.0.5 }
}
chain input {
type filter hook input priority 0; policy drop;
# ブロック対象のIPを即座にドロップする
ip saddr @blocklist drop
ct state invalid drop
ct state { established, related } accept
iif "lo" accept
# ICMP — pingは許可するがレートを制限する
ip protocol icmp icmp type echo-request limit rate 10/second accept
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
# SSHに接続レート制限を適用する(ブルートフォース対策)
tcp dport 22 ct state new limit rate 5/minute accept
# Webトラフィック
tcp dport { 80, 443 } accept
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}
適用して永続化する:
# ルールセットを適用する
nft -f /etc/nftables.conf
# ブート時に有効化する(systemd)
systemctl enable nftables
systemctl start nftables
# 確認する
nft list ruleset
NAT設定 — nftablesが真価を発揮する場面
コンテナやVMを動かすサーバーではマスカレードとポートフォワーディングが頻繁に登場する。このNAT設定はDNATとマスカレードの両方をひとつのファイルで処理する――並行するip6tablesコマンドも、ツール間の手動状態同期も不要だ:
table ip nat {
chain prerouting {
type nat hook prerouting priority -100;
# ホストのポート8080を内部コンテナの172.16.0.10:80に転送する
tcp dport 8080 dnat to 172.16.0.10:80
}
chain postrouting {
type nat hook postrouting priority 100;
# eth0経由で出ていくトラフィックをマスカレードする
oif "eth0" masquerade
}
}
2度助けてくれたテストプロトコル
アトミックリロードは本物であり有用だ。それでも、新しいポリシーがすべてのインバウンドトラフィックをドロップしてしまえば、ロックアウトを防ぐことはできない。本番サーバーのファイアウォールルールに触れる前の私の標準的な手順はこうだ:
# まず現在の動作中のルールセットを保存する
nft list ruleset > /root/nft-backup-$(date +%Y%m%d-%H%M).conf
# 適用せずに構文をテストする
nft -c -f /etc/nftables.conf
# アクセスを失った場合の自動ロールバックをスケジュールする
# (新しいルールを適用する前に実行すること)
echo "nft flush ruleset && nft -f /root/nft-backup-*.conf" | at now + 5 minutes
# 新しいルールを適用する
nft -f /etc/nftables.conf
# 別のマシンから接続を確認してから、ロールバックをキャンセルする
atrm $(atq | tail -1 | cut -f1)
このatジョブのトリックで2台のサーバーを救った――1回はコピペミスでSSHトラフィックがすべてドロップされたとき、もう1回は設定ミスのforwardチェーンがコンテナネットワークをブロックしたときだ。ロールバックが発動する前に問題に気づくには、5分あれば十分だ。
本番サーバーの移行
12台すべてのサーバーでnftablesを6ヶ月運用したことで、メンテナンスの様相が一変した。ルールの更新は4つの個別ファイルを編集することから、1つを編集することへと変わった。IPv6カバレッジは「たぶん大丈夫」から実際に保証されるものへと変わった。ブロックリストへの50件のIPの追加は、その後のすべてのパケット評価を遅くする50件のルール追記ではなく、単一のセット操作で完結するようになった。
ほとんどのサーバーで、移行は半日で終わる。iptables-translateを実行して出発点を得て、出力をクリーンアップし(10分未満でレビューできる程度に読みやすい)、ロールバックタイマーを仕掛けてテストし、切り替える。もしさらに前の状態に戻す必要があっても、ほとんどのディストリビューションではiptablesの互換レイヤーが引き続き使用可能だ。
Debian、RHEL、Fedoraはすべて現在デフォルトでnftablesを採用している。Firewalldはバックエンドとして使用している。DockerはネイティブnftablesサポートをアクティブIに開発中だ。エコシステムは標準化された。2026年になってもiptablesのルールを手作業で管理しているなら、主要ディストリビューションがすでに移行を終えたインフラに対して技術的負債を抱えていることになる。

