Proxmox 上で動かしている自分用のサービス群を、これまで Cloudflare Tunnel 経由でほぼすべて外に出していた。最近は AI を使った攻撃で既知の脆弱性が突かれるまでの時間が短くなっているので、「自分用は Tailscale からだけ、外に出すのは本当に公開したいものだけ」という構成に組み替えることにした。その設計をまとめておく。
インターネット
└─ Cloudflare ─ Tunnel ─ cloudflared ─ Nginx Proxy Manager ─ 各サービス(LXC)
(ingress は *.example.com のワイルドカード 1 本)
*.example.com の 1 本だけなので、NPM にホストを足すと、それだけで外に出る。外に出すかどうかを判断する場所がなかった/wp-admin・/.env・/.git を探しに来るリクエストがすでに届いていたアクセスログを 1 か月分見直すと、外から本当に使われていたのは、公開しているアプリと、スマホのアプリから自分が使うものくらいだった。後者は Tailscale があれば外に出す必要がない。
[インターネット]
└─ Cloudflare ─ Tunnel ─ cloudflared ─ Caddy(http, localhost のみ) ─ 公開アプリ
[自分の端末(Tailscale を常時接続)]
① app.example.com → 公開 DNS が 192.168.1.201 を返す(DNS only)
② 192.168.1.0/24 はサブネットルート → OPNsense → 内部セグメント
③ Caddy :443(OPNsense 経由の送信元だけ許可。証明書は DNS-01)
④ 各サービス
OPNsense に Tailscale のプラグインを入れて、内部のセグメントをサブネットルートとして広告している。もともと SSH 用に使っていたので、Web もここにまとめた。
Proxmox のホストだけは別に tailnet に入れる。管理画面がサブネットルートの範囲外にあることと、OPNsense 自体がこのホストの上の VM なので、OPNsense が落ちたときの入口が要るためだ。
*.example.com を、DNS only(Cloudflare のプロキシなし)でプロキシのプライベートアドレスに向ける。公開するアプリだけは、個別のレコードで Tunnel を指す。個別のレコードはワイルドカードより優先される。
一部のルーターや DNS は、DNS rebinding 対策でプライベートアドレスの答えを捨てることがある。使う回線と端末で引けることを、先にテスト用のレコードで確かめておく。
Tailscale に移すと、全ホストに HTTPS と送信元の制限を付け直すことになる。手間が同じなら設定がファイルとして残るほうがいいので、Caddy に置き換えることにした。
{
acme_dns cloudflare {env.CF_API_TOKEN}
}
(private) {
@outside not remote_ip 192.168.1.1
respond @outside 403
}
# 公開: Tunnel(同じホストの cloudflared)からだけ受ける
http://public-app.example.com {
@notunnel not remote_ip 127.0.0.1
respond @notunnel 403
reverse_proxy 192.168.1.202:80
}
# 自分用: OPNsense 経由(= tailnet)からだけ受ける
grafana.example.com {
import private
reverse_proxy 192.168.1.204:3000
}
caddy-dns/cloudflare を入れてビルドする)。外からの到達は要らない。API トークンは対象ゾーンの DNS 編集だけに絞るnetwork_mode: host で動かすhttp:// の site として書く。TLS は Cloudflare のエッジで終端していて、Caddy の HTTPS 自動リダイレクトとぶつかってループするのを避けるためhttp_status:404 にするどちらか片方を設定ミスしても、自分用のサービスは外に出ない。
ingress:
- hostname: public-app.example.com
service: http://127.0.0.1:80
- service: http_status:404
--accept-routes が要る