本文へ移動
購読
GUIDE#security更新 2026.09.24

nftables入門:iptablesとの違いとサーバー用ルールセットの書き方

nftablesでWebサーバー用のファイアウォールを書く手順。iptablesとの違い、IPv6の近隣探索、構文チェックと締め出し対策、Dockerやufwと同居させる際の注意まで実例付き。

level
中級
time
約40分
stack
nftables 1.0.9 / Ubuntu 24.04
author
@ryoma_t
// 前提条件
Linuxサーバーのroot権限 / コンソールなどSSH以外の復旧手段 / TCP/IPの基礎知識
nftで設定ファイルの文法を検査して読み込み、遮断用セットに期限付きでアドレスを追加したターミナル
画像:nagoya.dev(実際の画面・実行結果をもとに作成)

nftablesとは、Linuxカーネルのパケット処理機構netfilterにルールを登録し、通信の許可・拒否やNATを行うための仕組みと、その操作コマンド nft の総称です。iptablesの後継として開発され、Debian 10やRHEL 8以降では、iptables コマンドで書いたルールも内部ではnftablesとして動いています。

このガイドでは、公開Webサーバー1台分のルールセットを /etc/nftables.conf に書き、構文チェック、締め出されない適用手順、日常の操作、Dockerやufwとの同居までを扱います。コマンドと出力は、Ubuntu 24.04(nftables 1.0.9)のコンテナを2台つなぎ、片方から実際にスキャンして確かめたものです。

この記事でわかること

  • iptablesとnftablesの違いと、複数のテーブルがどの順で評価されるか
  • サーバー用ルールセットの例と、各行をそこに置く理由(IPv6の近隣探索を含む)
  • nft -c による構文チェックと、SSHで締め出されないための適用手順
  • セットへの追加、期限付きの一時遮断、ハンドルを使った行の挿入・削除
  • Docker・ufw・firewalldと同居させるときに、ルールが消えたり素通りしたりする理由

iptablesとnftablesの違い

最初に押さえておきたいのは、今のUbuntuで iptables を実行しても、中身はnftablesだということです。iptables -V をUbuntu 24.04で実行すると iptables v1.8.10 (nf_tables) と表示され、iptables -A INPUT -p tcp --dport 8080 -j ACCEPT で追加したルールは、nft list table ip filter で tcp dport 8080 counter packets 0 bytes 0 accept として見えます。違いはカーネルの機能ではなく、どちらの書き方でルールを管理するかにあります。

観点iptablesnftables(nft)
テーブルとチェーンfilter・natなどのテーブルとINPUTなどのチェーンが最初から決まっている何もない状態から、テーブルとチェーンを自分で作る。名前も自由
IPv4とIPv6iptables と ip6tables で別々に書くinet ファミリーのテーブル1つで両方を扱える
複数のポート・アドレスmultiportモジュールや、別ツールのipsetが必要{ 80, 443 } の無名セットや、名前付きセットが標準機能
設定の反映1行ずつ追加するか、iptables-restore で入れ替えnft -f がファイル全体を1つのトランザクションとして反映し、途中で失敗すれば何も変わらない
パケット数の計測全ルールで常に計測counter と書いた箇所だけ計測
設定ファイルiptables-save の出力形式コメントや変数を書ける独自の構文(/etc/nftables.conf)

新しく書くなら、IPv4とIPv6を1か所で管理できるnftablesの構文を選びます。既存のiptablesのルールが大量にあり、それを参照している運用手順も多い環境では、無理に書き換えず、次に構成を見直す機会に移行する判断もありです。

仕組み:フック・優先度と、複数テーブルの評価順

nftablesのチェーンには、パケットの通り道に取り付ける「ベースチェーン」と、ジャンプ先として使う「通常チェーン」があります。ベースチェーンは type filter hook input priority filter; のように、どのフック(prerouting・input・forward・output・postrouting)に、どの優先度で取り付けるかを宣言します。同じフックに複数のベースチェーンがあると、優先度の数値が小さい順にすべて通ります。

ここで効いてくるのが判定の性質です。drop はその場で最終決定になりますが、accept は「そのチェーンでは通す」という意味しかなく、パケットは次のベースチェーンへ進みます。検証では、iptablesで8080番を許可したうえで、後述の host_fw テーブル(8080番の許可なし、既定drop)を読み込み、別のコンテナからスキャンしました。結果は 8080/tcp filtered で、iptables側の許可は効きませんでした。ufwやDockerと同居させるときの問題は、ほとんどがこの性質から起きます。

nftablesの設定例:Webサーバー用のルールセット

公開するのはHTTP・HTTPS(HTTP/3用のUDP 443を含む)だけ、SSHは管理元のIPアドレスからだけ、という1台構成の例です。Ubuntuなら sudo apt install nftables で入り、設定は /etc/nftables.conf に書きます。アドレスはドキュメント用の例なので、自社の値に置き換えてください。

/etc/nftables.conf
#!/usr/sbin/nft -f
# /etc/nftables.conf — 例: Webサーバー1台分のホストファイアウォール
# 自分のテーブルだけを作り直す(flush ruleset は使わない)
table inet host_fw
delete table inet host_fw

table inet host_fw {
    # SSHを許可する管理元(例の値。自社の固定IPに置き換える)
    set admin_v4 {
        type ipv4_addr
        flags interval
        elements = { 192.0.2.0/28, 198.51.100.7 }
    }
    set admin_v6 {
        type ipv6_addr
        flags interval
        elements = { 2001:db8:10::/64 }
    }
    # 手作業で一時的に遮断するアドレス(要素ごとに期限を付けられる)
    set blocklist_v4 {
        type ipv4_addr
        flags timeout
    }

    chain input {
        type filter hook input priority filter; policy drop;

        ct state invalid drop
        ip saddr @blocklist_v4 drop
        ct state { established, related } accept
        iif "lo" accept

        # ICMPv4はエラー通知とping。IPv6は近隣探索(ND)を止めると通信自体ができない
        icmp type { echo-request, destination-unreachable, time-exceeded, parameter-problem } accept
        icmpv6 type { echo-request, destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept

        # SSHは管理元だけ。IPv4とIPv6を別々に許可する
        tcp dport 22 ip saddr @admin_v4 accept
        tcp dport 22 ip6 saddr @admin_v6 accept

        # 公開サービス(HTTP/3を使わないなら udp 443 の行は不要)
        tcp dport { 80, 443 } accept
        udp dport 443 accept

        # ここまでで許可されなかったものを記録(毎分5件まで)してから既定のdropへ
        limit rate 5/minute burst 10 packets log prefix "nft-in-drop: " level info
        counter
    }

    # Dockerを動かすホストやルーターでは、このチェーンを書かない(本文参照)
    chain forward {
        type filter hook forward priority filter; policy drop;
    }

    chain output {
        type filter hook output priority filter; policy accept;
    }
}

上から順に、書いた理由を説明します。

  • 先頭の3行:table inet host_fw で「なければ作る」、delete table で消し、改めて定義します。nft -f はこれを1つのトランザクションとして反映するので、ルールが空になる瞬間がなく、何度読み込んでも同じ結果になります。他のテーブルには触りません。
  • ct state の2行:接続追跡(conntrack)が壊れていると判断したパケットを捨て、確立済みの通信への応答を通します。サーバーから外へ出た通信の戻りもここで通るので、個別の許可が要りません。一時遮断のセットを確立済みの許可より前に置いているのは、遮断した相手の既存の接続も即座に切るためです。
  • ICMPv6:IPv6では、同じネットワーク上の相手のMACアドレスを近隣探索(Neighbor Solicitation / Advertisement)で調べます。検証でこの行を消して読み込むと、IPv6では -Pn を付けたnmapでも 0 hosts up になり、スキャン元の近隣キャッシュは INCOMPLETE のまま、dropした近隣探索パケットが末尾のcounterに数えられていました。
  • SSHの2行:ip saddr はIPv4のパケットにしか一致しません。IPv4とIPv6の両方に、許可する側の条件を1行ずつ書きます。
  • logとcounter:最後まで許可されなかったパケットを、毎分5件までカーネルログに残し、件数を数えます。limit を付けないと、スキャンを受けたときにログがディスクとjournaldを圧迫します。
  • forwardとoutput:ルーターではないサーバーは転送を一切しないので、forwardは既定drop、outputは既定acceptにしています。外向き通信まで絞るのは、許可すべき宛先(アップデートの配布元、監視、DNS)を列挙できてからで十分です。

SSHを「管理元以外は捨てる」否定形で書くと、IPv6が素通りします。tcp dport 22 ip saddr != @admin drop の次に tcp dport 22 accept を置いた構成を検証したところ、IPv4からは 22/tcp filtered、IPv6からは 22/tcp open でした。IPv6のパケットは ip saddr の条件に一致しないので、drop行を通り抜けて次のaccept行に拾われるためです。許可は肯定形で、IPv4とIPv6の両方に書いてください。

構文チェックと、締め出されないための適用手順

nft -c -f は、カーネルに反映せずに構文と参照先だけを確かめます。エラーの位置が行番号と桁で示されるので、書き換えるたびに必ず通します。検証で実際に出たエラーは次のとおりです。

terminal
$ sudo nft -c -f /etc/nftables.conf && echo "syntax OK"
syntax OK

# policy の綴りを間違えた場合
/tmp/bad.conf:23:56-59: Error: syntax error, unexpected string, expecting accept or drop or '$'
        type filter hook input priority filter; policy dorp;
                                                       ^^^^

# セット名を間違えた場合(候補も示してくれる)
/tmp/bad2.conf:34:34-43: Error: No such file or directory; did you mean set ‘ssh_admins’ in table inet ‘host_fw’?
        tcp dport 22 ip saddr != @ssh_admin counter drop
                                 ^^^^^^^^^^

構文が正しくても、許可の書き漏れでSSHが切れることはあります。リモートで初めて適用するときは、一定時間後に元の状態へ戻す予約を入れてから反映し、新しいSSHセッションで入れることを確かめてから予約を取り消します。次の例は、systemdのタイマーで5分後にテーブルを消す予約です。既存のルールを置き換える場合は、nft delete table inet host_fw の代わりに、退避しておいた前の設定ファイルを nft -f で読み込む予約にします。

terminal(対象サーバー)
# 5分後に自動で元に戻す予約
sudo systemd-run --on-active=5m --unit=nft-rollback /usr/sbin/nft delete table inet host_fw

# 反映
sudo nft -f /etc/nftables.conf

# 別の端末から新しいSSHセッションで入れたら、予約を取り消す
sudo systemctl stop nft-rollback.timer

# 起動時に読み込ませる
sudo systemctl enable nftables

設定を反映し直すときは systemctl reload nftables を使い、restart と stop は使わないでください。パッケージ付属の nftables.service(Ubuntu 24.04)は、ExecReload が nft -f /etc/nftables.conf、ExecStop が nft flush ruleset です。restartは停止処理でカーネル上の全テーブルを消すため、Dockerやufw、CrowdSecのブロッカーが作ったルールまで巻き添えになります。

日常の操作:セットへの追加、一時遮断、行の挿入と削除

管理元の追加や攻撃元の遮断は、ルールを書き換えずにセットの要素を出し入れするだけで済みます。セットはハッシュや区間木で引くため、要素が数千件になっても、ルールを数千行並べる場合のように遅くはなりません。期限付きの要素は時間が来ると自動で消えます。

terminal
# 管理元を追加する
sudo nft add element inet host_fw admin_v4 '{ 203.0.113.25 }'

# 1時間だけ遮断する
$ sudo nft add element inet host_fw blocklist_v4 '{ 172.30.97.2 timeout 1h }'
$ sudo nft list set inet host_fw blocklist_v4
table inet host_fw {
	set blocklist_v4 {
		type ipv4_addr
		flags timeout
		elements = { 172.30.97.2 timeout 1h expires 59m59s997ms }
	}
}

# 期限を待たずに外す
sudo nft delete element inet host_fw blocklist_v4 '{ 172.30.97.2 }'

検証では、遮断中はスキャン元から80番が filtered になり、要素を外すと open に戻りました。ただし、コマンドで足した要素は設定ファイルには書かれません。検証で管理元を追加した後に nft -f /etc/nftables.conf で読み込み直すと、セットは elements = { 192.0.2.0/28, 198.51.100.7 } に戻り、追加した管理元からのSSHは再び filtered になりました。恒久的な変更は必ずファイルにも書きます。

ルールを1行だけ足したり消したりするときは、-a で各行のハンドル番号を表示してから、番号で位置を指定します。insert は指定した行の前、add は後ろに入ります。

terminal(検証環境)
$ sudo nft -a list chain inet host_fw input | grep 'dport { 80, 443 }'
		tcp dport { 80, 443 } accept # handle 19

# handle 19 の前に、社内からだけ8080番を許可する行を入れる
$ sudo nft insert rule inet host_fw input position 19 tcp dport 8080 ip saddr 172.30.97.0/24 accept
$ sudo nft -a list chain inet host_fw input | grep 8080
		tcp dport 8080 ip saddr 172.30.97.0/24 accept # handle 23

# 不要になったら番号で消す
$ sudo nft delete rule inet host_fw input handle 23

管理元を固定できない場合:SSHの新規接続を制限する

在宅勤務の回線などで管理元のIPアドレスを固定できないなら、SSHを全体に開けたうえで、送信元ごとに新規接続の頻度を制限します。動的セットに送信元アドレスを記録し、1分間に10回(最初は5回まで連続可)を超えた分を捨てる書き方です。

/etc/nftables.conf(該当部分の差し替え例)
    set ssh_flood_v4 {
        type ipv4_addr
        flags dynamic, timeout
        timeout 10m
    }
    set ssh_flood_v6 {
        type ipv6_addr
        flags dynamic, timeout
        timeout 10m
    }

    # chain input の SSH の2行をこの3行に置き換える
        tcp dport 22 ct state new update @ssh_flood_v4 { ip saddr limit rate over 10/minute burst 5 packets } drop
        tcp dport 22 ct state new update @ssh_flood_v6 { ip6 saddr limit rate over 10/minute burst 5 packets } drop
        tcp dport 22 accept

検証で1台から20回続けて接続を試すと、最初の7回が通り、残りの13回が捨てられました。送信元の状態は nft list set inet host_fw ssh_flood_v4 で、elements = { 172.30.96.2 limit rate over 10/minute burst 5 packets expires 9m58s911ms } のように、送信元ごとの制限と残り時間として見えます。ただし、これは総当たりを遅くするだけで、防ぐものではありません。鍵認証だけにしたうえで使い、可能ならSSHをVPNの内側に移すほうを優先します。ログを見て攻撃元を自動で遮断したい場合は、CrowdSecのようなログ解析型のツールを組み合わせます。

Docker・ufw・firewalldと同居させる

同じホストでファイアウォールを操作するソフトが複数あると、前述の「dropは最終、acceptは次へ進む」が効いてきます。よくある組み合わせごとの注意は次のとおりです。

同居する相手相手が作るもの起きること対処
Docker(既定のiptablesバックエンド)ip nat・ip filter テーブルのDOCKER系チェーン公開したコンテナのポートへの通信はforwardを通るため、inputのルールでは絞れない。forwardを既定dropにするとコンテナの通信が止まるhost_fwにforwardチェーンを書かない。外に出さないポートは -p 127.0.0.1:8080:80 で公開する
ufwiptables(nft)で書いた ip filter などのufw用チェーン両方が有効だと、ポートを両方で許可しないと届かないどちらか一方に寄せる。Ubuntuで簡単に済ませたいならufw
firewalld(RHEL系の既定)inet firewalld などのテーブル自作テーブルと二重に判定され、どちらのせいで止まったか追いにくいRHEL系は firewall-cmd で管理する。自作するならfirewalldを止める
CrowdSecのファイアウォールブロッカー優先度 filter - 10 の独自テーブル遮断対象だけを先に捨てるので、競合はしないrestartで消さない(前述の注意)

Dockerの問題は2つあります。1つは、-p 8080:80 で公開したポートがホストのinputチェーンを通らないことです。宛先の書き換え(DNAT)を受けたパケットはコンテナへ転送される扱いになるので、host_fwで8080番を許可していなくても外から届きます。もう1つは flush ruleset です。検証環境でiptablesのNATルールを入れた後に実行すると、iptables -S には既定のポリシー3行しか残りませんでした。Dockerのホストで同じことをすればコンテナの通信が止まり、systemctl restart docker でコンテナごと作り直すまで戻りません。執筆時点のDocker Engine 29系では、daemon.json の firewall-backend でnftablesを直接使う方式も選べますが、既定はiptablesのままです。

既存のiptablesルールを移行する

既存のルールは iptables-translate でnftの書式に変換できます。1行ずつ試して書き方を覚えるのに向いており、保存済みのファイル全体なら iptables-restore-translate -f /etc/iptables/rules.v4 で変換できます。

terminal
$ iptables-translate -A INPUT -p tcp -s 192.0.2.0/24 --dport 22 -m conntrack --ctstate NEW -j ACCEPT
nft 'add rule ip filter INPUT ip saddr 192.0.2.0/24 tcp dport 22 ct state new counter accept'

$ iptables-translate -A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT
nft 'add rule ip filter INPUT ip protocol tcp tcp dport { 80, 443 } counter accept'

$ ip6tables-translate -A INPUT -p ipv6-icmp -j ACCEPT
nft 'add rule ip6 filter INPUT meta l4proto ipv6-icmp counter accept'

保存済みファイルの変換も試しました。INPUTを既定DROPにし、確立済み・ループバック・80と443・管理元からの22番を許可する5行の rules.v4 は、add chain ip filter INPUT { type filter hook input priority 0; policy drop; } と、counter 付きの add rule 4行に変換されました。変換結果は ip filter と ip6 filter に分かれ、全行に counter が付いた、iptablesの構造そのままの形です。動作は同じですが、IPv4とIPv6の重複はそのまま残ります。移行を機に、前掲の例のように inet テーブル1つにまとめ、送信元の列挙をセットに置き換えると、ルールの行数が大きく減ります。

動作確認

反映後は、サーバーの内側でルールとカウンタを見て、外側から実際に届くかを確かめます。外側からの確認は、管理元の回線と、管理元以外の回線(スマートフォンのテザリングなど)の2か所から行うと、許可と拒否の両方を一度に確かめられます。

terminal
# 内側:読み込まれているテーブルと、dropに落ちた件数
sudo nft list tables
sudo nft list chain inet host_fw input | grep -E 'counter packets'
# 例: counter packets 4 bytes 216 が増えていれば、拒否が働いている

# 内側:記録されたログ(prefixで絞る)
sudo journalctl -k -g 'nft-in-drop' --since '10 min ago'

# 外側(管理元以外の回線から):22が filtered、80・443が open なら想定どおり
sudo nmap -Pn -p 22,80,443 203.0.113.10
sudo nmap -6 -Pn -p 22,80,443 2001:db8:10::10

検証環境で、管理元に入っていないコンテナからスキャンすると、IPv4・IPv6とも 22/tcp filtered、80/tcp open になり、スキャン元をセットに加えると22番も open に変わりました。想定と違う判定になったときは、トレースで「どのチェーンのどの行で決まったか」を追います。調べたい通信にだけ印を付ける一時テーブルを作り、nft monitor trace で表示します。

terminal(検証環境)
# 172.30.97.2 から3306番への通信にだけ印を付ける
$ sudo nft add table inet trace_dbg
$ sudo nft add chain inet trace_dbg pre '{ type filter hook prerouting priority -350; }'
$ sudo nft add rule inet trace_dbg pre ip saddr 172.30.97.2 tcp dport 3306 meta nftrace set 1
$ sudo nft monitor trace
trace id b2507ac2 inet host_fw input packet: iif "eth0" ip saddr 172.30.97.2 ip daddr 172.30.97.20 ... tcp dport 3306 tcp flags == syn ...
trace id b2507ac2 inet host_fw input rule limit rate 5/minute burst 10 packets log prefix "nft-in-drop: " level info (verdict continue)
trace id b2507ac2 inet host_fw input rule counter packets 0 bytes 0 (verdict continue)
trace id b2507ac2 inet host_fw input policy drop

# 調べ終わったら一時テーブルを消す
$ sudo nft delete table inet trace_dbg

最後の policy drop は、どの許可行にも一致せず、チェーンの既定動作で捨てられたことを示しています。許可したはずの通信がここに来ていれば、条件の書き方(IPv4とIPv6の取り違え、ポート番号、セットの中身)を疑います。

つまずきポイント

「Error: syntax error, unexpected string, expecting accept or drop or '$'」

キーワードの綴り間違いです。エラー行の /tmp/bad.conf:23:56-59 は「23行目の56〜59桁目」を指し、次の行の ^^^^ が該当箇所です。policy dorp のような誤字のほか、行末の ; の付け忘れでも同じ系統のエラーになります。

「Error: No such file or directory; did you mean set …?」

存在しないセットやチェーンを参照しています。近い名前の候補を did you mean set ‘ssh_admins’ のように示してくれる(nftables 1.0.9)ので、綴りを合わせます。セットを使う行より前にセットの定義があるか、テーブル名とファミリー(inet か ip か)が一致しているかも確かめます。

nftablesを再起動したら、Dockerのコンテナが外と通信できなくなった

systemctl restart nftables の停止処理(nft flush ruleset)か、設定ファイル先頭の flush ruleset で、DockerのNATルールが消えています。sudo nft list tables で ip nat がなければこの状態です。systemctl restart docker で作り直し、以後は自分のテーブルだけを作り直す書き方と reload に切り替えます。

ルールを入れたらIPv6だけ通信できなくなった

ICMPv6の近隣探索を許可していないと起きます。同じネットワークの別ホストで ip -6 neigh show を実行し、サーバーのアドレスが INCOMPLETE や FAILED のままなら確定です。nd-neighbor-solicit、nd-neighbor-advert、nd-router-advert を含むicmpv6の許可行を、policy drop に落ちる前の位置へ入れます。

よくある質問

nftablesの構文チェックは?

sudo nft -c -f /etc/nftables.conf で、カーネルに反映せずに確かめられます。エラーがあれば行番号・桁位置と該当箇所が示され、終了コードが1になります。存在しないセットの参照のように、構文は正しくても整合しない誤りも検出されます。

firewalldとnftablesの違いは何ですか?

カーネルにルールを登録する仕組みそのものがnftablesで、firewalldはその上で「ゾーン」や「サービス」の単位でルールを管理するデーモンです。RHEL 8以降のfirewalldは、裏側でnftablesの inet firewalld などのテーブルを操作しています。RHEL系ではfirewalldを使い、細かい制御が必要なときだけ直接書くのが無難です。

iptablesの後継は?

後継はnftablesです。Linux 3.13で取り込まれ、Debian 10やRHEL 8以降では iptables コマンド自体もnftablesの上で動く iptables-nft に置き換わっています。既存の書き方はそのまま使えますが、新しく書くなら inet テーブルとセットを使えるnftの構文が扱いやすくなります。

iptablesとufwの違いは何ですか?

ufwは、ufw allow 443/tcp のような短いコマンドから、iptables(Ubuntu 24.04では内部でnftables)のルールを生成する管理ツールです。細かい条件を書くには向きませんが、1台のサーバーで数ポートを開けるだけならufwで十分です。自作のnftablesのテーブルと同時に使うと二重に判定されるので、どちらかに統一します。

まとめ

nftablesでは、同じフックのベースチェーンがすべて通過され、acceptは次のチェーンへ進み、dropだけが最終判定になります。この性質を前提に、自分の inet テーブルだけを作り直す書き方にし、許可は肯定形でIPv4とIPv6の両方に書き、ICMPv6の近隣探索を通してください。反映は nft -c と戻し予約を付けて行い、再読み込みは reload、Dockerのホストではforwardに手を出さない。ここまで守れば、既存のサービスを巻き込まずにホストの入口を絞れます。

次に読む

$ subscribe --weekly
毎週月曜、企業ITとホームラボの要点を5分で。