はじめに
Ubuntu の VPS で UFW を「受信は拒否、22/80/443 だけ許可」にしていても、Docker で -p や Compose の ports: で公開したポートには、UFW の受信ルールが効きません。Docker 公式ドキュメントにそう書かれています(Docker and ufw)。
そこで実際の VPS で試してみました。結果は少し意外なもので、UFWで許可していないポートを Docker で全アドレスに公開しても、手元の PC からは接続できませんでした。
この記事では、公式ドキュメントで確認できることと、実機で起きたことを分けて書きます。そのうえで、「UFWで閉じたから安心」でも「Dockerだから必ず外から見える」でもなく、サーバーの中と外の両方で確かめる手順を紹介します。
前回の記事「ポートの公開先を確認する」の続編です。https://qiita.com/supportapp3/items/f33e81e397be21d68dcd
公式ドキュメントで確認できること
-
Docker と UFW は一緒に使うと期待どおりに動かない。 公開ポート宛ての通信は
natテーブルで転送され、UFW が使う INPUT / OUTPUT チェーンより前に処理されます。そのため、UFW の通常の受信ルールを通りません(Docker: Packet filtering and firewalls)。 -
ホスト側のアドレスを省略すると、全アドレスで公開される。
-p 8080:80や"8080:80"は、IPv4 の0.0.0.0と IPv6 の::で公開されます。公式はこの既定を「insecure by default」と書いています。127.0.0.1:8080:80のように書けば、ホスト自身からだけ届きます(Docker: Port publishing and mapping)。 -
Docker 28.0.0 より前は注意が必要。
127.0.0.1に公開したポートに、同じL2ネットワークのホストから届くことがありました。Docker 28 で修正されています(Port publishing and mapping、Docker Engine 28 release notes)。 - Docker 29 でもこの前提は変わっていない。 ファイアウォールのバックエンドの既定は iptables で、nftables は Docker 29.0.0 で追加された試験的な機能です(Docker Engine 29 release notes、Docker with nftables)。
ここで大事なのは、「公開する」というのはホストに届いた通信をコンテナへ転送するという意味だということです。VPS の手前にある事業者のファイアウォールや経路を、通信が通り抜けられるとは限りません。
実機で試したこと
検証環境
- Ubuntu 22.04.5 LTS
- Docker 29.5.2 / Docker Compose v5.1.4
- ufw 0.36.1(有効、受信は既定で拒否、許可は 22/tcp と 80,443/tcp のみ。18999番の許可なし)
本番のサービスとかぶらない 18999 番を使い、nginx:alpine のコンテナを一時的に立てて比べました。終わったあとはコンテナ、イメージ、作業ファイルを削除しています。
① 全アドレスで公開
docker run -d --name test-web -p 18999:80 nginx:alpine
docker ps --filter name=test-web --format '{{.Names}}\t{{.Ports}}'
sudo ss -tlnp | grep 18999
- VPS の中で見ると、IPv4 と IPv6 の全アドレスで 18999 番を待ち受けていました。
- 手元の PC からは、ping は成功しましたが、18999 番への TCP 接続は失敗しました。
② 127.0.0.1 に限定して公開
docker rm -f test-web
docker run -d --name test-web -p 127.0.0.1:18999:80 nginx:alpine
sudo ss -tlnp | grep 18999
- 待ち受けは
127.0.0.1:18999だけになりました。 - 手元の PC からの TCP 接続は、こちらも失敗しました。
結果の読み方
②で届かないのは期待どおりです。問題は①です。公式ドキュメントのとおりなら UFW では止まらないはずなのに、外からは届きませんでした。
止まった原因は特定できていません。 ただ、以下が考えられます(推測です)。
- VPS 事業者のファイアウォール(パケットフィルタ、セキュリティグループなど)が、TCP/18999 を止めている
- 手元の PC が使った宛先(IPv4 / IPv6)と、VPS 側で公開した経路がずれている
- ほかの転送ルールや、コンテナの中の応答に問題がある
なお、ping が成功したことは ICMP が通ることを示すだけです。TCP の特定のポートが通るかどうかとは関係ありません。
つまり今回わかったのは、「UFW が守ってくれた」ではなく、**UFW 以外の何かで止まっていた(かもしれない)**ということです。その「何か」の設定が変われば、公式ドキュメントのとおり外から届くようになる可能性があります。
確かめる手順:中と外の両方を見る
Step1:サーバーの中で見る
docker ps --format 'table {{.Names}}\t{{.Ports}}'
sudo ss -tlnp
| 表示 | 意味 |
|---|---|
0.0.0.0:ポート / [::]:ポート
|
全アドレスで公開。UFW では止まらないので、外から届くかどうかは手前のファイアウォール次第 |
127.0.0.1:ポート |
ホスト自身からだけ(Docker 28.0.0 以上) |
5432/tcp のように -> がない |
ホストには公開していない |
DB(5432 / 3306 / 6379 / 27017)や管理画面のポートが 0.0.0.0 や [::] に出ていたら、Step2 に進みます。
Step2:外から実測する
手元の PC(VPS の外)から試します。自分が管理していないサーバーには実行しないでください。
# Mac / Linux
nc -vz <VPSのIP> <ポート>
nc -6 -vz <VPSのIPv6アドレス> <ポート> # IPv6 がある場合
# Windows(PowerShell)
Test-NetConnection <VPSのIP> -Port <ポート>
Test-NetConnection では TcpTestSucceeded が True なら届いています(Microsoft Learn: Test-NetConnection)。
- 届いた → Step3 で直します。
-
届かなかった → 今は届いていません。ただし Step1 で
0.0.0.0だったなら、止めているのは UFW 以外です。事業者側の設定を変えたり、別の事業者に移ったりした時点で届くようになる可能性があるので、Step3 で直しておくのがおすすめです。
届かなかったとき、どこで止まっているかを見る(tcpdump)
VPS の中で通信を待ち受けながら、手元の PC から Step2 を実行します。
# VPS で実行(20秒で自動終了)
sudo timeout 20 tcpdump -nni any 'tcp port <ポート>'
- 何も表示されない → 接続しようとしたパケットが VPS まで届いていません。事業者側のファイアウォールや経路で止まっている可能性が高いです。
- SYN は表示されるが応答がない、あるいは RST が返っている → VPS には届いています。Docker の転送ルールやコンテナ側を確認します。
tcpdump は見るだけのコマンドで、設定は変わりません。
Step3:直す
一番確実なのは、そもそも全アドレスで公開しないことです。
services:
db:
image: postgres:16
# アプリのコンテナからだけ使うなら ports: は書かない(サービス名 db:5432 で届く)
手元から使いたい場合は、ホスト自身だけに公開して SSH トンネルを使います。
ports:
- "127.0.0.1:5432:5432"
ssh -L 5432:127.0.0.1:5432 user@<VPSのIP>
Web アプリは 127.0.0.1 に公開して、nginx などのリバースプロキシの 80/443 から転送します。
特定の IP だけに公開したい場合は、UFW ではなく Docker の DOCKER-USER チェーンを使う方法が公式に紹介されています(Docker with iptables)。再起動後もルールを残す方法や、nftables バックエンドでの扱い(DOCKER-USER チェーンがない)も含めて、公式ドキュメントを確認してから設定してください。この記事では実機で試していないので、手順は載せません。
あわせて、VPS 事業者側のファイアウォールでも必要なポートだけを許可しておくと、サーバーの外側でも止められます。
直したら、Step1 と Step2 をもう一度実行して確かめます。
まとめ
- 公式ドキュメントでは、Docker で公開したポートは UFW の受信ルールを通りません。
- 実機では、全アドレスで公開しても外から届きませんでした。ただし原因は未特定で、UFW 以外(事業者のファイアウォールなど)で止まっていた可能性があります(推測)。
- だから「UFWで閉じたから安心」とは言えません。中(
ss/docker ps)と外(nc/Test-NetConnection)の両方で確かめ、届かないときは tcpdump でどこで止まっているかを見ます。
[ ] docker ps の PORTS に 0.0.0.0: / [::]: で DB や管理画面が出ていない
[ ] sudo ss -tlnp で、意図しないポートが 0.0.0.0 / [::] で待ち受けていない
[ ] 手元の PC から nc -vz / Test-NetConnection で、閉じたいポートに届かない(IPv6 も)
[ ] 届かない理由が「UFW」ではなく何なのかを把握している(必要なら tcpdump)
[ ] DB は ports を書かないか 127.0.0.1: に限定している
[ ] Docker は 28.0.0 以上
[ ] VPS 事業者側のファイアウォールも必要なポートだけ許可している
Step1 の確認をまとめて見たい場合は、読み取り専用の CLI vps-check の DKR-001(全アドレスに公開された Docker のポート)と NET-002(全アドレスで待ち受けている DB)が使えます。設定は変更しません。外から届くかどうかは判定しないので、Step2 は別に行ってください。
更新情報は X(@supportapp3_lab)で発信しています。