TT Lab
はじめる
学ぶ 学習パス コース

ネットワーク基礎 — Linux VM で手を動かす

塞ぐと書き換える — netfilter フック・状態追跡・NAT

TT Labで続きを見る

一言でいうと

nftablesのルールは、パケットが通る5つのフック(prerouting・input・forward・output・postrouting)のどれかに掛かります。接続追跡(conntrack)が「すでに開いている接続のパケット」を見分けるので、レスポンスは1行で許可でき、NATは同じconntrackの上でアドレスを書き換え、元に戻します。

なぜ必要なのか

ファイアウォールはルールの一覧ではなく、デフォルトと順序です。ポリシーがdropなら書き漏らしたものが遮断され、acceptなら書き漏らしたものが開きます。そしてルールには方向があります。サーバーへ入ってくるSYNを開けたからといって、サーバーが出していくレスポンスが自動で開くわけではありません。昔はそのためにルールを2倍書いており、それが接続追跡ができた理由です。ct state established,related acceptの1行が「すでに開いている接続は通す」という意味なので、最初に開くSYNだけを選べば済みます。

プライベートアドレスは、インターネット上ではルーティングされません。ところが、家・会社・クラウド・KubernetesのPodは、すべてプライベートアドレスです。これらが外と通信するには、境界でアドレスを書き換え、レスポンスが来たら元に戻す必要があります。元に戻すには「このレスポンスはもともと誰のものか」を覚えておく必要があり、その記憶がconntrackです。NAT装置が再起動するとすべての接続が切れるのは、このためです。

どう動くのか

フックは位置です。自分宛てのパケットはprerouting → input、自分が送るパケットはoutput → postrouting、他のホストのパケットを転送するときはprerouting → forward → postroutingを通ります。サーバーを守るルールはinputに、ルーターのルールはforwardに置きます。ルーター自身のinputを閉じると、管理用の接続までブロックされます。

netfilterの5つのフック。入ってきたパケットはpreroutingを通り、ルーティング判定で分かれ、自分宛てならinputを経てローカルプロセスへ向かい、他のホスト宛てならforwardを経てpostroutingから出ていく。自分が送るパケットはoutputから始まってpostroutingから出ていく。DNATはルーティングの前でなければならないのでpreroutingに、SNATとmasqueradeは出ていくインターフェースが決まったあとでなければならないのでpostroutingに置く

チェーンはtype filter hook input priority 0; policy dropのように作ります。ルールは上から順に見て、最初にマッチしたものの判定(accept/drop)で終わります。counterを付けると何個掛かったかを数え、logを付けるとカーネルログに残します(ネームスペースの中ならnf_log_all_netns=1が必要です)。

マスカレードはpostroutingに置きます。ルーティングが終わり、どのインターフェースから出ていくのかがわかる場所だからです。ip saddr 10.30.0.0/24 oifname "enp1s0" masqueradeは、そのアドレス帯がuplinkへ出ていくとき、送信元をuplinkのアドレスに書き換えます。DNATはpreroutingに置きます。ルーティングの前に宛先を書き換えないと、書き換えた宛先で経路を探せないからです。Dockerの-p 8080:80が作るのが、このルールです。NATチェーンを通るのは接続の最初のパケットだけで、残りはconntrackが処理します。

現場での姿

自己ロックアウト。SSHで入ってpolicy dropを有効にしました。セッションが固まります。establishedの許可を先に入れなかったため、自分のセッションのレスポンスが止まったのです。順序がそのまま安全性です。許可を先に、ポリシーを後に入れます。

NATを設定したのに外へ出ていかない。マスカレードのルールを入れたのに、プライベートネットワークがインターネットに出られません。forwardチェーンのポリシーがdropだからです。NATはアドレスを書き換えるだけで、通過を許可しません。2つのチェーンは別の仕事です。ip_forwardが0の場合も、同じ症状になります。

ファイアウォールのルールを扱う規律

ルールそのものよりも、ルールをどう変えるかが、事故の分かれ目になります。先ほど見た自己ロックアウトは、手順1つでほぼなくせます。

元に戻す仕掛けを先に掛ける。リモートでルールを変えるときは、数分後に自動で古いルールへ戻る予約を先に掛けてから始めます。うまくいけば取り消し、ロックアウトされたら待ってから入り直します。この習慣1つで、「サーバーに入れなくなって人を派遣した」がなくなります。

ルールはファイルで管理し、丸ごと適用する。手で1行ずつ入れると、今の状態が何なのか誰にもわからなくなり、再起動すると消えます。全ルールを書いたファイルを置いて、アトミックに入れ替えれば、動いているものとファイルが常に同じになります。

ログを先に有効にしてから遮断する。新しいルールで何が遮断されるか確信がないときは、遮断する代わりにcounterとlogだけを付けて、数日間観察します。掛かるものがなければ、そのときに判定をdropへ変えます。先ほど見たPod Security Admissionのwarn → enforceと同じ順序です。

そして、ファイアウォールがどこにあるかを常に意識する必要があります。最近のシステムでは、パケットは何層ものフィルターを通ります。クラウドのセキュリティグループ、ホストのnftables、コンテナランタイムが作ったルール、そしてサービスメッシュのポリシー。どの層で遮断されたのかわからないまま1つの層だけを直しても何も変わらず、そのうち「とりあえず全部開けてみよう」になってしまいます。先ほど見たように、refusedなのかタイムアウトなのか、そして各層のカウンターが増えているかを見れば、どの層かを絞り込めます。

最後に、ルールにコメントを残します。nftablesはルールにcommentを付けられます。なぜ開けたのかが書かれていないルールは、誰も消せず、そうして積み上がったルールの中から本当に必要なものを探すのが、だんだん難しくなります。

次のラボですること

ネームスペース内のサーバーにinputチェーンを作り、デフォルトのdropから始めて、lo・established・icmp・80・送信元を制限した2222を1つずつ開け、counterで捨てられたものを数えます。そのあとVMをルーターにして、プライベートネームスペースをマスカレードでインターネットに出し、両側のインターフェースで送信元が書き換わる様子を捕まえ、DNATでポートを内側のサーバーに転送します。