2026年11月8日19:12〜19:15 JST、さくらVPSでIPv4の3項目を実測。以下はPR #9の検証記録に基づきます。記事編集時に再実行した結果ではありません。
SSHのrootログインを禁止し、鍵認証にして、ufwで不要な受信を拒否する。それでもDockerで開発用サービスを公開すると、SSHとは別の入口ができます。今回は専用の使い捨てVPSで、外部のWindows PCから確認しました。
結論:今回の環境ではufwの受信拒否だけでは防げなかった
| 条件 | VPS内 | Windows PCからのIPv4接続 |
|---|---|---|
-p 8080:80、UFW active/default deny incoming |
応答あり | 3回ともHTTP 200・本文一致 |
-p 127.0.0.1:8080:80 |
応答あり | 3回とも約5秒でtimeout |
| 全アドレス公開に戻す | 応答あり | 3回ともHTTP 200・本文一致 |
| 検証元に限定したDOCKER-USER conntrack DROP追加 | 応答あり | 3回とも約5秒でtimeout |
| 同じDROPルールを削除 | — | 3回ともHTTP 200・本文一致 |
今回試したのはdefault deny incomingであり、ufw deny 8080を明示追加した試験ではありません。 UFWの許可項目はOpenSSHだけでした。結果はこの構成・この検証元のIPv4接続についての確認であり、他の環境まで保証しません。
検証環境と事前準備
| 項目 | 値 |
|---|---|
| VPS | さくらのVPS・1GBプラン(契約情報に基づく) |
| OS | Ubuntu 24.04 LTS |
| カーネル | 6.8.0-36-generic |
| Docker Engine/CLI |
29.9.3/29.9.3
|
| Docker Firewall backend | iptables |
| iptables実装 | iptables v1.8.10 (nf_tables) |
| UFW | 0.36.2 |
| コンテナ |
nginx:stable-alpine、nginx 1.30.6
|
| 外部クライアント | Windows PC上のPython。HTTPプロキシなしでIPv4へ直接接続 |
nf_tablesはiptablesコマンドの実装であり、Dockerのnftables backendを使ったという意味ではありません。各比較は同じイメージdigestと確認用本文で行いました。
開始時はDocker未導入、8080のlistenerなし、UFW inactiveでした。SSHホスト鍵を照合し、agent forwardingを無効にして接続。変更前の設定を保存し、接続断に備えたrollback timerを用意しました。Dockerは公式aptリポジトリから導入しました。
UFWはSSH許可を先に追加してから有効化しています。以下は検証記録の抜粋であり、既存環境へそのまま適用する手順ではありません。
ufw allow OpenSSH
ufw default deny incoming
ufw --force enable
昼・夕方のboxでの導入はaptのHTTP 500・署名エラーで失敗し、そこでの通信試験は未実施でした。設定の回避変更はせず、今回のVPS実測へ切り替えています。旧試行の出力は qiita_vps_ufw_docker_test.txt に残しています。
1. 全アドレス公開は外部から到達した
検証用コンテナは -p 8080:80 で起動し、本文を vps-check-issue8 に固定しました。UFWと公開先の実出力抜粋です。
80/tcp -> 0.0.0.0:8080
80/tcp -> [::]:8080
Status: active
Default: deny (incoming), allow (outgoing), deny (routed)
22/tcp (OpenSSH) ALLOW IN Anywhere
22/tcp (OpenSSH (v6)) ALLOW IN Anywhere (v6)
初回の18:32〜18:37 JSTは外部3回ともtimeoutでした。ただしVPS内はHTTP 200、Dockerの転送カウンターは0であり、これを「UFWで防げた」とは判定していません。利用者が事業者側パケットフィルターのTCP 8080未許可を確認し、許可設定を保存した後に再測定しました。
外部フィルター許可後の実出力:
VPS内: vps-check-issue8
Windows外部1: HTTP 200 / marker一致 / 0.076秒
Windows外部2: HTTP 200 / marker一致 / 0.054秒
Windows外部3: HTTP 200 / marker一致 / 0.076秒
今回のIPv4では、UFWの受信拒否だけではDockerの公開ポートへの到達を防げませんでした。インターネットからの到達可否には、事業者側フィルターや経路も関係します。
なぜSSHやufwの設定だけでは足りないのか
Docker公式は、公開したコンテナへの通信が、ufwが利用するINPUT・OUTPUTチェーンへ到達する前にnatテーブルで転送されると説明しています。iptables backendでの代表的な経路を簡略化すると、次のとおりです。
外部からホストの8080番へ
→ DNAT(宛先変換)
→ FORWARDとDocker関連チェーン
→ コンテナの80番へ
ホスト自身へのINPUTではなく、コンテナ向けの転送になる点が重要です。この経路の説明は公式資料に基づくもので、今回全パケットを追跡したという意味ではありません。SSHの鍵認証も、別サービスへの接続を防ぐ設定ではありません。
2. 外部公開しないなら127.0.0.1へ限定する
同じイメージ・本文の専用コンテナを作り直し、公開先だけを -p 127.0.0.1:8080:80 に変更しました。
80/tcp -> 127.0.0.1:8080
vps-check-issue8
Windows外部1: HTTPなし / URLError / 5.008秒
Windows外部2: HTTPなし / URLError / 5.010秒
Windows外部3: HTTPなし / URLError / 5.016秒
VPS内の応答を維持したまま、同じWindows PCからの外部到達を防げました。全アドレス公開に戻すと外部HTTP 200が回復しています。
開発用画面を自分だけで使うなら、まず公開範囲を狭くします。次は一般的な設定例です(このコマンドそのものの実行記録ではありません)。
docker run --rm -p 127.0.0.1:8080:80 nginx
自分のPCからはSSHトンネルを利用できます。次のSSHコマンドも未検証の利用例です。
# <SSH_ALIAS>は自分のSSH設定の接続先へ置き換える
ssh -N -L 18080:127.0.0.1:8080 <SSH_ALIAS>
PCで http://127.0.0.1:18080 を開きます。localhost限定はVPS上のプロセスを認証する仕組みではありません。Docker公式には28.0.0未満の版で同じL2セグメントからlocalhost公開ポートへ到達できる問題の注意書きがありますが、その旧版の挙動は今回未検証です。direct routingなど別構成も個別確認が必要です。
3. DOCKER-USERとconntrackで対象接続を遮断する
外部公開が必要な場合、iptables backendではDOCKER-USERで制御できます。このチェーンへの到達時点ではDNAT済みなので、-p 8080:80に対し単に宛先ポート8080を指定するだけでは不適切です。元の宛先を照合するため、今回はconntrackの --ctorigdstport 8080 を使いました。
実測では検証PCの送信元・VPSの元の宛先・入口interface・IPv4 TCPのNEW接続に限定しています。次はIPを伏せた実行コマンドの抜粋です。変数設定、存在確認のguard、復元準備は省略しているため、そのまま貼り付けて実行する手順ではありません。
rule=(-i "$iface" -s "$client" -p tcp -m conntrack --ctdir ORIGINAL --ctstate NEW --ctorigdst "$server" --ctorigdstport 8080 -m comment --comment vps-check-issue8 -j DROP)
iptables -w 5 -I DOCKER-USER 1 "${rule[@]}"
実出力抜粋:
DROP前の外部1: HTTP 200 / marker一致 / 0.054秒
DROP前の外部2: HTTP 200 / marker一致 / 0.049秒
DROP前の外部3: HTTP 200 / marker一致 / 0.050秒
VPS内: vps-check-issue8
DROP後の外部1: HTTPなし / URLError / 5.005秒
DROP後の外部2: HTTPなし / URLError / 5.014秒
DROP後の外部3: HTTPなし / URLError / 5.014秒
対象DROPルールのカウンターは 9 packets/468 bytes でした。HTTPリクエスト9回ではなく、3回の接続試行とTCP再送を含むパケット数です。同じ引数でexact ruleを削除した後は次の結果になりました。
TEST_DROP_RULE_REMOVED=yes
削除後の外部1: HTTP 200 / marker一致 / 0.055秒
削除後の外部2: HTTP 200 / marker一致 / 0.050秒
削除後の外部3: HTTP 200 / marker一致 / 0.050秒
到達成功→DROPカウンター増加とtimeout→削除後の回復を確認しました。ただし今回のルールは特定の検証元だけを遮断する試験であり、全送信元を保護する完成済み設定ではありません。
広すぎるDROPは他の転送通信も止めます。実運用では対象interface・通信を特定し、既存接続、許可元、IPv6、永続化を設計してください。公式はconntrackによる性能低下の可能性も注意しています。Dockerのiptables設定を無効にする方法はネットワークを壊す可能性があり、公式も多くの利用者には推奨していません。nftables backendには別の手順が必要です。
未確認の範囲と後片付け
今回未確認なのは、IPv6からの外部到達、再起動・Docker再起動後のルール永続性、別送信元、別network namespace、別backend・rootless・Docker Desktop・hostネットワーク等です。今回の一構成から一般化しません。原因などを推測として追記する場合は(推測)と明記します。
検証後は専用コンテナと取得イメージを削除し、Docker・containerdを停止・無効化。開始前のUFW設定ファイルのhash、IPv4/IPv6ルール、forwarding値を照合して復元したと記録されています。
IPTABLES_RESTORED=yes
IP6TABLES_RESTORED=yes
UFW_CONFIG_RESTORED=yes
Status: inactive
DOCKER_SERVICE=inactive
CONTAINERD_SERVICE=inactive
PORT_8080_LISTENER=absent
CLEANUP_ERRORS=0
TEMPORARY_PASSWORDLESS_SUDO_REMOVED=yes
SSH_AUTHENTICATION=ok
PASSWORDLESS_SUDO_UNAVAILABLE=yes
Docker関連パッケージ・公式apt source・root専用復元データは残存しています。事業者側のTCP 8080許可を戻すこととVPS削除は利用者の管理です。VPSを残す場合も検証用許可の戻しを忘れないでください。
PRのCI verify は2件とも成功(2026年11月8日19:19 JST)。Goのtest・vet・build等の回帰チェックと、今回の外部到達性の実測は別の確認です。
まとめ
SSHを固めても、Dockerの -p は別の入口になります。外部公開しないサービスはlocalhostへ限定し、必要な公開には適切な転送通信の制御を設計する。そしてホスト内だけでなく、認可された別端末から閉じるはずの入口を確認することが重要です。
出典
- 実測記録:vps-check PR #9(2026年11月8日 JST確認)
- Docker and ufw
- Docker with iptables
- Port publishing and mapping
公式資料は2026年11月8日 JSTに確認した既存下書きの参照先です。
必要な方へ:VPSでAIエージェントを動かす手順を通しでまとめた有料記事:https://note.com/fresh_murre7145/n/n6bfeffb63e16
タグ案:Docker / ufw / VPS / セキュリティ / ClaudeCode
編集用画像:img/qiita_vps_ufw_docker.png は旧box試行の画像であり、今回のVPS実測結果の証拠としては使用しません。公開前に差し替え、または掲載を取りやめます。