iptables でパケットフィルタリングを管理する

LinuxBeginner
オンラインで実践に進む

はじめに

iptablesは、Linux のパケットフィルタリングルールを管理するためのコマンドラインインターフェースです。現在、多くのディストリビューションでは、これらのルールを nftables バックエンド経由で実装していますが、管理者は依然としてiptablesコマンド構文を使用する作業や既存システムに遭遇します。

初心者向けには、ファイアウォールを順序付きのチェックリストとして捉えると分かりやすいでしょう。パケットがチェーンに入り、各ルールがインターフェース、プロトコル、ポートなどの条件にパケットが一致するかを確認し、ACCEPTやREJECTなどのターゲットがその後の処理を決定します。INPUTチェーンは、ローカルホスト宛てのパケットを処理します。ルールは上から下へ評価されるため、ルールの内容だけでなく配置も重要です。

この実験では、1 つのローカル Web サービスを対象に、その TCP ポートに対して範囲を限定した変更だけを行います。現在のファイアウォールを確認し、リクエストを拒否するルールを追加して実際の通信で効果を確認します。その後、明示的な許可ルールに置き換え、ファイアウォール全体をフラッシュしたり無関係な状態を書き換えたりすることなく、実験で追加したルールだけを削除します。

ファイアウォールとテストサービスを確認する

このステップでは、インストールされているiptablesの実装を確認し、ファイアウォールルールを変更する前にローカルのテストサービスが正常に動作していることを確認します。

後続のコマンドをすべてコースの作業ディレクトリから実行できるよう、プロジェクトディレクトリへ移動します。

cd /home/labex/project

command -vを使用して、シェルが実行する実行ファイルの場所を確認します。

command -v iptables

この確認は、コマンドがインストールされていると決めつけるよりも有用です。通常は/usr/sbin/iptablesのような解決済みの実行ファイルパスが表示され、シェルがコマンドを見つけられない場合はゼロ以外の終了ステータスを返します。

バージョンとバックエンドの情報を表示します。この Ubuntu ホストでは、通常、出力にnf_tablesが含まれます。これは、使い慣れたiptables構文が、最新のカーネルルールセットバックエンドに変換されていることを示します。

sudo iptables --version

次のような出力が表示されます。

iptables v1.8... (nf_tables)

バックエンド名によって、この実験で使用するコマンドが変わることはありません。引き続きiptablesでルールを作成・確認できます。実装がカーネル内でのルール表現を処理します。

ポートに対するフィルタリングを行う前に、ヘルスエンドポイントへリクエストを送信します。レスポンスが正常に返れば、アプリケーションの準備が整っていることが分かるため、その後の失敗をファイアウォールルールの影響だと判断できます。

curl --noproxy '*' --fail --silent http://127.0.0.1:18086/health

ここで127.0.0.1は IPv4 のループバックアドレスです。そのため、通信は外部ネットワークへ出ることなく、ローカルのloインターフェースを経由して送受信されます。--noproxy '*'はプロキシ設定によるリクエストの迂回を防ぎ、--failは HTTP エラー時に失敗ステータスを返し、--silentは進捗メーターを非表示にしてサービスのレスポンスを読みやすくします。

サービスは次の内容を返します。

iptables filter service ready

INPUT チェーンを確認して拒否ルールを追加する

このステップでは、現在のINPUTチェーンを確認し、テストポート宛てのループバック TCP 通信を拒否する、範囲を限定したルールを 1 つ挿入します。

-Sオプションを使用すると、ルールがコマンド形式で表示されます。INPUTを指定することで、このホスト宛てのパケットだけに出力を限定できます。

sudo iptables -S INPUT

通常、最初の行にはチェーンポリシーが表示されます。たとえば、-P INPUT ACCEPTのようになります。ポリシーは、それ以前のルールで最終的な判断が下されなかった場合にのみ使用されるデフォルトの処理です。-A INPUTで始まる後続の行には、そのチェーンに現在追加されているルールが表示されます。

運用時の確認には、-Lでチェーンを一覧表示し、-nで時間のかかる名前解決を避け、--line-numbersで各ルールの位置を表示します。ルールは上から順に評価されるため、最初に一致した終了ターゲットがパケットの処理結果を決定します。

sudo iptables -L INPUT -n --line-numbers

この 2 つの表示方法は目的が異なります。再利用可能なルール構文が必要な場合は-Sが便利で、ルールの位置、パケットカウンター、運用上の概要を確認する場合は-Lが便利です。

-I INPUT 1を使用して、実験用ルールを 1 番目の位置に挿入します。このルールは、ループバックインターフェースlo、TCP、宛先ポート18086に一致します。tcp-resetを指定したREJECTターゲットは、破棄されたパケットのタイムアウトを待たせるのではなく、直ちに接続を拒否します。

sudo iptables -I INPUT 1 -i lo -p tcp --dport 18086 -j REJECT --reject-with tcp-reset

コマンドを左から右へ読み解くと、-i loは受信インターフェースを限定し、-p tcpはプロトコルを選択し、--dport 18086はサービスを選択し、-j REJECTは拒否ターゲットへ処理を移します。-Iは指定した位置にルールを挿入します。一方、-Aを使用するとルールは末尾に追加されるため、それより前に一致するルールがある場合、ルールに到達しない可能性があります。この限定的な条件の組み合わせにより、無関係なポートや外部インターフェースをブロックせずに済みます。

REJECTとDROPは同じではありません。REJECTはエラーレスポンスを返しますが、DROPはパケットを通知なしに破棄します。TCP リセットを使用すると、このローカルテストは迅速かつ予測可能に失敗するため、タイムアウトを待つよりも原因を診断しやすくなります。

-Cを使用して、完全に一致するルールが存在するか確認します。確認に成功すると、終了ステータスとしてゼロが返ります。このホストの nftables 対応実装では、正規化された一致ルールも表示されますが、実装によっては何も表示せずに完了する場合があります。

sudo iptables -C INPUT -i lo -p tcp --dport 18086 -j REJECT --reject-with tcp-reset

-Cで確認する際に重要なのは画面に文字が表示されたかどうかではなく、終了ステータスです。そのため、同じコマンドをシェルスクリプトや検証処理でも利用できます。

もう一度チェーンを一覧表示します。新しいルールが 1 番目のルールとして表示され、tcp dpt:18086が含まれていることを確認します。

sudo iptables -L INPUT -n --line-numbers

ブロックされた通信を検証する

このステップでは、先ほどと同じリクエストを再実行し、ルールカウンターを確認します。これにより、通信を拒否したのがアプリケーションではなくファイアウォールであることを証明します。

リクエストをif文の中で実行します。リクエストは失敗することが想定されているため、else分岐でその失敗を明確なメッセージに変換し、手順を続行できるようにします。

if curl --noproxy '*' --fail --silent http://127.0.0.1:18086/health; then
  echo "Unexpected: request succeeded"
else
  echo "Expected: iptables rejected the request"
fi

シェルの条件分岐は、コマンドの終了ステータスに基づいて判断します。ゼロの場合はthenが選択され、ゼロ以外の場合はelseが選択されます。ファイアウォールが送信するリセットによりcurlはゼロ以外を返すため、ここではelse分岐に進むのが想定される結果です。

想定されるメッセージは次のとおりです。

Expected: iptables rejected the request

次に、詳細なカウンターを表示する-vを付けてチェーンを一覧表示します。リクエスト後には、ポート18086のルールに表示されるパケット数とバイト数が 0 より大きくなっているはずです。

sudo iptables -L INPUT -n -v --line-numbers

出力のpkts列は一致したパケット数を、bytes列はそれらの合計サイズを示します。カウンターは有力な証拠になります。-Cで確認すればルールの存在を証明でき、カウンターの増加を確認すれば、実際の通信がそのルールに到達して一致したことを証明できます。カウンターが 0 のままの場合は、アプリケーションを疑う前に、インターフェース、プロトコル、ポート、ルールの位置を確認してください。

拒否ルールを許可ルールに置き換える

このステップでは、拒否ルールを正確に削除し、同じ通信を許可する明示的なルールを追加して、アクセスが復旧することを確認します。

次の-D形式では、完全な一致条件とターゲットによってルールを特定します。チェーンをフラッシュするのではなく、実験で追加した変更だけを削除するため、より安全です。

sudo iptables -D INPUT -i lo -p tcp --dport 18086 -j REJECT --reject-with tcp-reset

ルールは行番号で削除することもできますが、ルールの挿入や削除が行われるたびに行番号は変わります。完全な仕様を指定して削除する方法は、再現可能な手順の中でも意図が明確で、隣接する別のルールを削除する可能性を減らせます。ここではiptables -Fを使用しないでください。フラッシュすると、選択したチェーン内のすべてのルールが削除され、この実験とは無関係のルールまで失われます。

同じインターフェース、プロトコル、ポートに対するACCEPTルールを挿入します。

sudo iptables -I INPUT 1 -i lo -p tcp --dport 18086 -j ACCEPT

ACCEPTは、一致したパケットがネットワークスタックを通過することを許可します。1 番目の位置に挿入することで、前にある終了ルールによって処理結果が上書きされないようにします。条件が先ほどと同じく範囲限定されており、変更されたのはターゲットだけである点に注目してください。

完全に一致する許可ルールを確認します。

sudo iptables -C INPUT -i lo -p tcp --dport 18086 -j ACCEPT

先ほどと同じヘルスリクエストを再実行します。

curl --noproxy '*' --fail --silent http://127.0.0.1:18086/health

再び次のレスポンスが返ります。

iptables filter service ready

実験で追加したルールを削除する

このステップでは、一時的な許可ルールを削除し、この演習で追加したルールが残っていないことを確認します。

完全に一致するACCEPTルールを削除します。これまでと同様に、チェーンをフラッシュしたり、プラットフォームや別の管理者が所有するルールに触れたりすることはありません。

sudo iptables -D INPUT -i lo -p tcp --dport 18086 -j ACCEPT

明示的な許可ルールを削除しても、サービスが自動的にブロックされるとは限りません。ルールがなくなると、残りのルール、そして最終的にはチェーンポリシーがパケットの処理結果を決定します。この準備済み環境では、元のファイアウォール設定によってローカルリクエストが許可されます。

もう一度ルールの存在を確認します。今回は失敗が想定されるため、iptables -Cがゼロ以外のステータスを返した場合にのみ、||の右側にあるメッセージが実行されます。

sudo iptables -C INPUT -i lo -p tcp --dport 18086 -j ACCEPT 2>/dev/null || echo "Lab allow rule removed"

次の内容が表示されます。

Lab allow rule removed

最後に、明示的なルールを削除してもアプリケーションが停止していないことを確認します。再び、チェーンの元のポリシーとルールによってアクセスが決まります。

curl --noproxy '*' --fail --silent http://127.0.0.1:18086/health

まとめ

iptablesの実装とINPUTチェーンを確認し、範囲を限定した拒否ルールを挿入・検証しました。また、実際の通信とカウンターによって効果を証明し、そのルールを許可ルールに置き換えた後、実験で追加した変更だけを削除しました。これらの操作を通じて、無関係なファイアウォール状態をフラッシュせず、安全にパケットフィルタリングを行うために必要なコマンド操作を実践できます。