homelab notes ポート開放できない家の自宅サーバー

ufw を有効にしても Docker の公開ポートは素通りする

DEFAULT_INPUT_POLICY=DROP にしてあるのに、 docker run -p 3000:3000 したサービスは LAN の誰からでも見えている。 これは設定ミスではなく、そういう仕組みになっている。

何が起きているか

ufw が書くルールは基本的に INPUT チェインに入る。 一方 Docker の公開ポートは DNAT でコンテナ宛に書き換えられるので、 パケットは INPUT ではなく FORWARD を通る

LAN のパケット
  → nat/PREROUTING (DOCKER チェインで DNAT)
  → FORWARD          ← Docker が ACCEPT を挿入済み
  → コンテナ

  ※ INPUT は通らない = ufw の DROP が効かない

しかも Docker は自分のルールを ufw より前に挿入するので、 ufw deny 3000 と書いても勝てない。

確認方法

待受アドレスを見れば一目でわかる。0.0.0.0 になっているものは全部 LAN から見える。

$ ss -tln
LISTEN 0 4096 0.0.0.0:3000  0.0.0.0:*   ← Grafana、LAN に丸見え
LISTEN 0 4096 0.0.0.0:9090  0.0.0.0:*   ← Prometheus
LISTEN 0 4096 0.0.0.0:11434 0.0.0.0:*   ← Ollama(認証なし)

対処

ファイアウォールで塞ごうとせず、そもそも 0.0.0.0 で待ち受けないのが正しい。 compose の ports にバインドアドレスを書く。

ports:
  - "3000:3000"              # ✗ 0.0.0.0:3000
  - "127.0.0.1:3000:3000"    # ✓ ループバックのみ

これで LAN からは消える。アクセスは Tailscale 経由のリバースプロキシに一本化する。

塞いだら別のものが壊れた

全サービスを 127.0.0.1 に寄せたら、リバースプロキシから host ネットワークで動いているサービス(Jellyfin)への応答が 7.6 秒かかるようになった。 ヘルスチェックも通らず、名前解決をやり直し続けていた。

理由はこうだった。

経路通るチェイン結果
bridge のコンテナ → ホストの公開ポートFORWARD(Docker が ACCEPT)通る
bridge のコンテナ → ホストの非公開ポートINPUT(ufw が DROP)通らない

Jellyfin は host ネットワークで直接 8096 を持っていて、Docker の公開ポートではない。 つまりブリッジ網のプロキシコンテナから見ると「ufw に守られたホストのポート」であり、 DROP されていた。公開ポートを閉じたことで、それ自体が到達不能になったという筋。

解決

プロキシ側を network_mode: host にした。 ホスト上で動けばループバック経由になるので ufw の INPUT を通らない。

services:
  proxy:
    # ufw が INPUT DROP なので、bridge 網からホストの非公開ポートに届かない。
    # host 網ならループバック経由になり ufw の影響を受けない。
    network_mode: host

応答は 7.6 秒から 0.053 秒になった。

まとめ

Docker を使っているホストでは、ufw は「Docker が公開していないポート」しか守らない。 守られている範囲と守られていない範囲が直感と逆になるので、 ss -tln0.0.0.0 を数えるのを習慣にしたほうがいい。 ufw の status を見ても何もわからない。