kWatanabe 記事一覧へ

kWatanabe の 技術帖

某企業でOSや仮想化の研究をやっているインフラ系エンジニア。オンプレとクラウドのコラボレーションなど、興味ある技術を綴る。

nftablesでシンプルなステートフルファイアウォールを作る

iptables から nftables への移行

Debian 11 (bullseye) の Freeze がはじまったので、自宅サーバ移行検証をしていたところ、ちょっとファイアウォールが気になった。

Debian では、10 (buster) で既に nftables がデフォルトになっており、iptables は nftables の互換レイヤーとして機能している。

wiki.debian.org

wiki.debian.org

今のところ、bullseye でも iptables が無くなるという話はないので、もう1回引っ張っても問題は無いのだが、nftables がデフォルトになった次のバージョンで移行というのはタイミングとしても良いので、ここで移行することにした。

検証環境

想定する構成

目指すはシンプルな2ファイアウォール型のDMZを構成すること。抽象化したモデルとしては以下のような形になる。

f:id:kWatanabe:20210213122100p:plain
シンプルな2ファイアウォールDMZ

なお、我が家はいわゆる「逸般の誤家庭」ではないので、Y社 や C社 のエンタープライズ向けルーターなんてものはない*1。 そのため、実態としては以下のような形になる。

f:id:kWatanabe:20210213123055p:plain
本記事の想定環境

この図の「Linux server」が今回のターゲットになる。 簡単のため、以下 DMZ 側の NICeth-dmz、Trust Zone 側の NICeth-trust と記載する。目指すルールは以下。

  • Trust Zone から DMZ への通信は全て許可
  • DMZ から Trust Zone へのセッション確立済みの通信を許可
  • DMZ から Trust Zone への特定のマシンから、特定のマシンの特定のポートへの通信を許可

nftables で作るステートフルファイアウォール

IP forward の有効化

まずは、eth-dmzeth-trust 間のパケット転送を有効にする。ここは nftables に限った話じゃないので詳細は触れない。

$ sudo vim /etc/sysctl.conf
#net.ipv4.ip_forward=1
↓
net.ipv4.ip_forward=1

適用。

$ sudo sysctl -p

nftables の導入

Debian buster および bullseye では、iptables と nftables の両方が提供されているため、明示的に nftables を導入する。

なお、パッケージは nftables のみでよい。iptablesiptables-persistent のように、管理インタフェースと永続化のための仕組みで分かれていたりはしない。

$ sudo apt install nftables

nftables のルールを書く

nftables は、全ての操作が nft コマンドで完結する。

iptables と同様、テーブル、チェイン、ルールの概念がある。ただし、デフォルトのテーブルやチェインは無い。そのため「nftables はなにもしない」状態から設定を作り込んでいく*2

大まかな流れは以下のようになる。 なお、<> はパラメータ、[]は省略可能な引数を指す。

1. 設定済みのルールをクリア
$ sudo nft flush ruleset
2. テーブルを作成
$ sudo nft add table [<アドレスファミリ>] <テーブル名>

アドレスファミリには以下が設定できる。

  • ip : IPv4
  • ip6 : IPv6
  • inet : IPv4IPv6
  • arp : ARP
  • bridge : ネットワークブリッジ
  • netdev : ネットワークインタフェース

IP層でのステートフルファイアウォールでは inet を使えば事足りる。具体例は以下。

$ sudo nft add table inet FIREWALL
3. テーブルにベースチェインを登録
$ sudo nft add chain [アドレスファミリ] <テーブル名> <ベースチェイン名> \
      { type <タイプ> hook <フック> [device <デバイス>] priority <優先度>; [policy <ポリシ> ;] }

ベースチェインは、初めに呼び出されるチェイン。Cでいう main() みたいなもの*3

「タイプ」は、チェインの用途を指定する。

タイプ 用途とアドレスファミリ 利用できるフック
filter 汎用 (全ファミリ) 全て
nat NAT (ip, ip6) prerouting, input, output, postrouting
route ルーティング (ip, ip6) output

「フック」は、チェインの実行タイミングを指定する。

フック タイミング 利用できるアドレスファミリ
ingress バイスがパケットを受信した際 netdev
prerouting パケット受信後、ルーティング判定の前 ip, ip6, inet, bridge
input ルーティング判定後、宛先が自分自身と判断した際 ip, ip6, inet, bridge, arp
forward ルーティング判定後、宛先が他ホストと判断した際 ip, ip6, inet, bridge
output 自分自身が他ホスト宛のパケットを生成した際 ip, ip6, inet, bridge
postrouting 他ホストへパケットを送信する直前 ip, ip6, inet, bridge

「デバイス」は、NIC 名を指定する。

「プライオリティ」は、同じフックが指定されたベースチェインが複数ある場合の優先度を指定する。値が小さいほど、先に実行される。

「ポリシ」は、チェインの中で取り扱いが決まらなかった際の取り扱いを acceptdrop で指定する。未指定だと accept になる。

具体例は以下の通り。

$ sudo nft add chain inet FIREWALL FILTER { type filter hook prerouting priority 0 \; }
  • type filter (汎用) で
  • hook prerouting (ルーティング判定前) に
  • priority 0 (最優先) で処理するベースチェインで、
  • 合致するルールが無い場合は受け入れる

policy を accept もしくは省略した場合は、確実に不要なパケットを落とすルールを入れることを忘れてはいけない。

4. テーブルにレギュラーチェインを登録
$ sudo nft add chain [アドレスファミリ] <テーブル名> <レギュラーチェイン名>

レギュラーチェインは、他のチェインから明示的に呼び出されるチェイン。呼び出し元のチェインの設定を引き継ぐため、チェイン名の登録のみ。具体例は割愛。

5. チェインにルールを登録
$ sudo nft add rule [<アドレスファミリ>] <テーブル名> <チェイン名> <ステートメント>

当該チェインが呼び出された際に、適用するルールを実行順に登録する。

ステートメント」は、具体的な制御内容を記載するが、網羅すると膨大な数になるため参考ページを記載し、ここでは具体例の記述にとどめる。

wiki.nftables.org

「確立済みの通信を許可する」ための設定例は以下になる。

$ sudo nft add rule inet FIREWALL FILTER \
    ct state related,established accept
  • ct (コネクション) の
  • state (状態) が
  • relatedestablished なものを
  • accept (許可) する。

「特定のマシンから、特定のマシンの特定のポートへのアクセスを許可する」ための設定例は以下になる。

$ sudo nft add rule inet FIREWALL FILTER \
    ip saddr 10.0.0.100 ip daddr 172.16.0.100 tcp dport 443 accept
  • ip (IPv4ヘッダ) の
  • saddr (送信元IPアドレス) が
  • 10.0.0.100 であって
  • ip (IPv4ヘッダ) の
  • daddr (送信先IPアドレス) が
  • 172.16.0.100 であって
  • tcp (TCPヘッダ) の
  • dport (接続先ポート番号) が
  • 443 のものを
  • accept (許可) する。

「(先のステートメントから漏れたパケットで、)特定のNICから入力された全てのパケットを破棄する」ための設定例は以下になる。

$ sudo nft add rule inet FIREWALL FILTER \
    meta iifname eth-dmz drop
  • meta (その他のメタデータ) の
  • iifname (入力インターフェース名) が
  • eth-dmz のものを
  • drop (破棄) する

設定済みのルールを確認

$ sudo nft list ruleset

上記のコマンドにて、現在の設定を出力できる。出力結果の具体例は以下になる。

table inet FIREWALL {
        chain FILTER {
                type filter hook prerouting priority 0; policy accept;
                ct state established,related accept
                ip saddr 10.0.0.100 ip daddr 172.16.0.100 tcp dport https accept
                iifname "eth-dmz" drop
        }
}

ルールの永続化

list ruleset の出力は、そのまま nftables の設定ファイルとして用いることができる。

Debian の場合、nftables サービスが起動する際に /etc/nftables.conf があればそれをロードする。そのため、以下のようにすることで設定を永続化できる。

$ sudo nft list ruleset | sudo tee /etc/nftables.conf
$ sudo systemctl enable nftables
$ sudo systemctl restart nftables

なお、手動で設定ファイルをロードしたい場合は以下のようにする。

$ sudo nft -f <ファイル名>

まとめ

最後に、上記のような設定を一発で行うためのスクリプトを作ったので公開しておく。

ライセンスは、MITライセンスで。

github.com

所感

  • nftables で単純なステートフルファイアウォールを作るための最低限の設定を整理した
  • iptables と比べて、設定ファイルや構文も構造化されており、人間様にわかりやすい
  • なにより、ツールが nft 1本に統一されているのがよい

*1:10GbE を敷くのが精々である。

*2:全て許可でも、全て禁止でも、全て転送でも無い。nftables 以外の仕組みが動いていれば、その設定が有効になる。本文に「nftablesは」とあえて書いた理由はここにある。

*3:_start()じゃないのか、とか面倒なコメントは受け付けない。