「sudo ufw status」がactiveだから安全——そう思い込んでいると、Dockerを1つ動かしただけでその前提が崩れることがあります。UbuntuやDebianでUFW(Uncomplicated Firewall、iptablesを簡単に操作するためのラッパー)を有効にしていても、Dockerはコンテナのポート公開(-pオプション)のたびに、UFWの管理下にない場所へ独自のiptablesルールを挿入します。その結果、UFWの一覧には出てこないポートが外部から丸見えになるケースがあります。この記事では、ufw statusだけで安心せず、iptables -Siptables -L -nで実際の許可ルールを突き合わせて確認する手順を、Ubuntu 24.04・Debian 12を基準にコマンド単位で説明します。

UFWが「enabled」でもDockerが穴を開けてしまう理由

UFWは主に、ホスト自身宛ての通信(INPUTチェーン)を制御します。ところがDockerがコンテナのポートを公開すると、通信は「ホスト宛て」ではなく「コンテナへ転送される通信」(FORWARDチェーン)として扱われ、Docker自身が作るDOCKERDOCKER-USERという別のチェーンで許可されてしまいます。UFWはこの部分を関知しないため、ufw statusの一覧に載っていないポートでも、Docker経由なら外部から到達できてしまうことがあります。

これは新しい脆弱性ではなく、Docker側で長年知られている挙動です。ただし見落とされやすく、2026年9月にCISA(米サイバーセキュリティ・インフラセキュリティ庁)が積極的に悪用されているLinuxカーネルの脆弱性3件(CVE-2025-39682、CVE-2026-53266、CVE-2025-39964)をKEVカタログに追加し、連邦機関に9月21日までの対応を求めた例のように、「外部から届く通信そのものを減らしておく」ことの重要性は上がっています。届く通信が多いほど、こうしたカーネル脆弱性を突かれる入口も増えるためです。

影響を受けやすい典型パターン

  • 検証用にdocker run -p 5432:5432 postgresのように、本来は社内からだけ使うつもりのDBコンテナをポート公開したまま放置している
  • UFWを先に設定してから、後でDockerをインストール・利用し始めた(順番的にUFWの想定にDockerが無い)
  • 「UFWで443と22だけ許可しているから安全」と思い込み、iptablesの実際のルールを一度も見ていない

ufw status verbose と iptables -S / -L -n を突き合わせる

まずUFW側の認識を確認します。設定変更は行わないコマンドなので、実行しても環境に影響はありません。

sudo ufw status verbose

何を見ているか:UFWが自分の設定としてどのポートを許可・拒否しているかの一覧です。

出力例(架空の値):

Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW IN    Anywhere
80,443/tcp                 ALLOW IN    Anywhere

ここだけを見ると「22・80・443しか開いていない」と読めます。次に、実際にカーネルへ渡っているルールをiptablesで直接見ます。

sudo iptables -S

何を見ているか:現在有効な全ルールを、再現可能なコマンド形式(-Aで始まる行)でそのまま表示します。UFWが作ったチェーンだけでなく、Dockerなど他のソフトが挿入したチェーンも含めて全部出ます。

sudo iptables -L -n -v

何を見ているか:チェーンごとの許可・拒否ルールを、通過したパケット数(カウンタ)付きで見やすく表示します。-nを付けるとホスト名の逆引きをしないため、実際に来た通信元IPが素早く分かります。

正常/異常の見分け方:iptables -Sの出力に、ufw statusには無かったはずのポート番号を許可する行(ACCEPT)が混ざっていないかを確認します。特にDOCKER・DOCKER-USER・DOCKER-ISOLATION-STAGE-1/2という名前のチェーンがあれば、それはDockerが自動生成したものです。UFWの一覧とここに矛盾があれば、それが「UFWが把握していない穴」です。

確認箇所

正常な状態

要調査な状態

ufw status verbose

公開してよいポートのみ ALLOW

公開した覚えのないポートがある

iptables -S内のDOCKER系チェーン

意図して公開したコンテナのポートだけACCEPT

検証用・停止したはずのコンテナのポートがACCEPT

両者の一致

UFWの一覧=実際に到達可能なポートの一覧

UFWでdenyのはずが実際はACCEPTされている

Dockerが空けたポートを特定する

矛盾を見つけたら、どのコンテナがどのポートを公開しているかを特定します。

docker ps --format "table {{.Names}}\t{{.Ports}}"

何を見ているか:稼働中コンテナの公開ポート一覧です。

出力例(架空の値):

NAMES          PORTS
webapp         0.0.0.0:80->80/tcp
test-db        0.0.0.0:5432->5432/tcp

0.0.0.0:5432は「サーバーの全てのネットワークインターフェースから5432番ポートで受け付ける」という意味です。検証用のDBコンテナがこの状態だと、UFWの一覧に5432が無くても外部から接続できてしまいます。

sudo iptables -t nat -L DOCKER -n -v

何を見ているか:Dockerが公開ポートを実際のコンテナ内部IPへ転送するためのNAT(アドレス変換)ルールです。ここに載っている行が、上のdocker psの結果と対応します。

sudo iptables -L DOCKER-USER -n -v

何を見ているか:Docker公式が「利用者が独自の制限を書き足すための場所」として用意しているチェーンです。ここが空(Chain DOCKER-USER (1 references)の下に何も無い)なら、Docker関連の通信に対する追加の制限は一切かかっていない、ということです。

本当に外から届くかどうかは、サーバー内部だけでなく別のネットワークにある端末から次のように確認するのが確実です(サーバーのグローバルIPが203.0.113.10、対象ポートが5432の場合の例)。

nc -vz -w 3 203.0.113.10 5432

「succeeded」と返れば到達可能、「timed out」「refused」なら到達できていません。到達できてしまい、かつ社外・検証以外で使う予定が無いポートなら、次の対処に進みます。

見つかった穴をふさぐ2つの方法と元に戻し方

どちらも設定変更を伴うため、変更前に現在の状態を控えておくことをおすすめします(sudo iptables-save > ~/iptables_backup_$(date +%Y%m%d).txtでルール一式をファイルに退避できます)。

方法1:公開先を127.0.0.1に限定する(コンテナ側の設定変更・最も簡単)

社内・自分専用の検証用途で、外部公開が不要なコンテナはこちらが手軽です。docker rundocker-compose.ymlのポート指定を書き換えます。

# 変更前(全世界に公開)
docker run -p 5432:5432 postgres

# 変更後(サーバー自身からしか繋がらない)
docker run -p 127.0.0.1:5432:5432 postgres

元に戻す方法:コンテナを止めて、元の-p 5432:5432で作り直すだけです。iptablesを直接触らないため後戻りは簡単ですが、コンテナの再作成が必要(既存のコンテナを起動したまま設定だけ変えることはできません)。

方法2:DOCKER-USERチェーンで許可元IPを絞る(要sudo・iptablesの設定変更)

特定の相手(例えば自社オフィスの固定IP)からだけアクセスを許したい場合は、Docker公式が推奨する場所であるDOCKER-USERチェーンに制限を追加します(許可するIPが203.0.113.50、対象がeth0の場合の例)。

sudo iptables -I DOCKER-USER -i eth0 ! -s 203.0.113.50 -j DROP

何をする変更か:eth0から入ってくる通信のうち、203.0.113.50以外の送信元をすべて破棄する、というルールをDOCKER-USERチェーンの先頭に挿入します。

元に戻す方法:-I(挿入)を-D(削除)に変えて同じ内容を実行します。

sudo iptables -D DOCKER-USER -i eth0 ! -s 203.0.113.50 -j DROP

注意点:iptablesで直接追加したルールは、サーバー再起動で消えます(それ自体は事故時の逃げ道にもなります)。恒久的に効かせたい場合はiptables-persistentパッケージの導入が必要ですが、初めて触る場合はまず再起動で消える状態のまま数日運用し、狙い通りに遮断できているかを確認してからにしてください。

Ubuntu/Debian以外との違いと定期点検の習慣化

この記事の手順はUbuntu 24.04・Debian 12(いずれもiptablesベースでUFWを使う構成)を基準にしています。他のディストリビューションでは前提が変わります。

  • RHEL/CentOS/Fedora系:標準はUFWではなくfirewalldです。Dockerとの同様の競合はfirewall-cmd --list-alliptables -Sの突き合わせで確認しますが、firewalldはゾーンの概念があるため見方が異なります
  • nftables移行環境:Ubuntu 24.04・Debian 12はデフォルトでiptables-nft(iptablesコマンドの見た目のままnftablesを裏で使う互換レイヤー)です。sudo iptables --versionで末尾に(nf_tables)と出ていればこの構成です。素のnftablesに完全移行した環境ではsudo nft list rulesetで確認します

UFWとDockerの矛盾は、Dockerでコンテナを1つ増減させるたびに再発しうるものです。設定ファイルそのものが意図せず書き換えられていないかは、findとstatで設定ファイル改ざん点検の手順とあわせて定期的に見ておくと、UFWのルールファイル自体が知らないうちに変更されていた場合にも気づけます。また、ポートを絞ってもSSH自体が空いていれば侵入経路として残るため、今誰がSSHに入っているか確認もあわせて習慣化することをおすすめします。

まとめ

UFWの「Status: active」は、あくまでUFWが把握している範囲での安全宣言にすぎません。Dockerはポート公開のたびにFORWARDチェーン側へ独自ルールを追加するため、UFWの一覧に無いポートが外部から到達可能になっていることがあります。月に一度でよいので、ufw status verboseiptables -Siptables -L -n -vを突き合わせ、docker psの公開ポートと矛盾がないかを確認してください。不要な公開は127.0.0.1限定に変更するか、DOCKER-USERチェーンで送信元を絞るだけで、多くの穴はふさげます。