自宅サーバーを安全に公開する ― VPS+frp+Caddy+Tailscale の「入口だけVPS」構成

IT
スポンサーリンク

自宅で動かしているサーバー(Webサイトやゲームサーバーなど)を外から見られるようにしたい。でも、

  • 自宅回線に固定IPがない(IPが変わる/CGNATでそもそも外から届かない)
  • ルーターのポート開放はセキュリティ的に怖い
  • 証明書(HTTPS)の管理が面倒

……という壁にぶつかりがちです。

この記事では、安いVPSを「入口」としてだけ使い、実体は自宅サーバーに置く構成を、実際に構築した手順そのままに紹介します。

担当役割
VPS(Linux)公開の入口。固定IPとポートだけを提供する
frpVPS⇔自宅を結ぶトンネル(接続は自宅側から張る)
Caddy(自宅側)HTTPS証明書の取得・更新と、サイトの振り分けを一手に担当。セルフホスティングツール用。
TailscaleSSHなどの管理専用ネットワーク。公開通信は通さない

この分担にすると、自宅ルーターのポート開放は一切不要で、回線のIPが変わっても平気です。証明書の管理も Caddy が全部自動でやってくれます。

本記事の環境例:VPS = Ubuntu 26.04 LTS、 自宅 = Windows 11 Pro、frp v0.71.1、Caddy v2.11.7。frp / Caddy は Linux 同士でも同じ考え方で組めます。frpはトンネルを作ることができれば良いので、ufwのbefore.ruleでIPマスカレードの設定をしたり、rathole等他の手段でも問題ないです。

スポンサーリンク

事前に用意するもの

  • VPS 1台(メモリ1GB程度の最安プランで十分です)
  • 独自ドメイン(サブドメインでも可。マルチゲームだけでCaddy要素要らないなら不要。)
  • 自宅サーバー(常時起動のPC)
  • Tailscale アカウント(無料プランで可)

費用感は VPS 代+ドメイン代のみで、月あたり数百円〜千円強です。

手順0:DNS の A レコードを VPS に向ける

ドメイン(例:example.com)の A レコードに、VPS の公開IPを設定します。

注意点がひとつ。Cloudflare を使う場合はプロキシ(オレンジ雲)はOFF、「DNS のみ」にしてください。プロキシを有効にすると証明書や接続元IPの扱いが変わり、この構成の前提が崩れます。

手順1:Tailscale で管理経路を先に作る

VPS・自宅サーバー(・普段使うスマホやPC)に Tailscale を入れて、同じ tailnet に参加させます。以後の SSH 作業は、基本的にこの Tailscale 経由で行います。

最初に管理経路を確保しておくと、後でファイアウォールで SSH の公開を閉じても締め出されません。

手順2:VPS に frps を入れる

frp のサーバー側(frps)を VPS にインストールし、systemd サービスとして常駐させます。

/etc/frp/frps.toml の例(値は自分の環境に置き換えてください):

bindPort = 7000
auth.method = "token"
auth.token = "<長いランダム文字列>"
transport.tls.force = true

webServer.addr = "<VPSのTailscale IP>"
webServer.port = 7500
webServer.user = "admin"
webServer.password = "<長いランダム文字列>"

allowPorts = [
{ start = 80, end = 80},
{ start = 443, end = 443},
]

tokenの長いランダム文字列は、サーバー側とクライアント側が一致していれば何でも良いです。また、userとpasswordは監視画面にログインするときに使用します。

  1. ダッシュボード(7500番)は Tailscale の IP でだけ待ち受ける。インターネットに管理画面を晒さない
  2. allowPorts は最小限に。ここに無いポートは、たとえ自宅側が要求しても frps が転送を拒否します(盾になります)
  3. TLS を必須(transport.tls.force = true)にして、トンネルを暗号化する

<VPSのTailscale IP>:7500にアクセスするとログインIDとパスワードが要求され、入力すると監視画面が開きます。

ufw の設定は、公開が必要なポートだけに絞ります。同じTailnet内からのみsshを許可するようにしていますが、不要であれば記載はいりません。

sudo ufw default deny incoming
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 7000/tcp
sudo ufw allow in on tailscale0 to any port 22 proto tcp # SSHはTailscale経由のみ
sudo ufw enable

手順3:自宅PCに Caddy を入れる

自宅サーバーに Caddy をインストールし、サービスとして常駐させます(Windows なら WinSW でサービス化するのが確実です)。

Caddyfile の例:

{
email you@example.com
servers {
listener_wrappers {
proxy_protocol {
allow 127.0.0.1/32
timeout 2s
fallback_policy ignore
}
tls
}
}
}

example.com {
reverse_proxy 127.0.0.1:8080
log {
output file C:/tools/caddy/logs/access.log
format json
}
}

http://example.com {
redir https://{host}{uri} permanent
}

ここがこの構成の肝です。

  • 証明書は Caddy が Let’s Encrypt から自動取得・自動更新します。VPS 側で証明書を扱う必要はありません。冒頭のメールアドレスは証明書の取得時に必要なので受信できるメールアドレスを指定します
  • 後述の PROXY protocol を受けるため、listener_wrappers に proxy_protocol を入れています。これでアクセスログに本当の接続元IPが記録されます
  • HTTP→HTTPS のリダイレクトは、ラッパー任せにせず明示的にサイトを定義するのが重要です(理由は「ハマりどころ」で)

手順4:自宅に frpc を入れる

frp のクライアント側(frpc)もサービスとして常駐させます。

Windowsの場合、Windows Defenderにより検閲されますので適宜除外を行ってください。

frpc.toml の例:

serverAddr = "<VPSのグローバルIP>"
serverPort = 7000
auth.method = "token"
auth.token = "<frpsと同じトークン>"
transport.tls.enable = true

[[proxies]]
name = "web-http"
type = "tcp"
localIP = "127.0.0.1"
localPort = 80
remotePort = 80
transport.proxyProtocolVersion = "v2"

[[proxies]]
name = "web-https"
type = "tcp"
localIP = "127.0.0.1"
localPort = 443
remotePort = 443
transport.proxyProtocolVersion = "v2"

ポイント:

  • 80/443 は frp の HTTP 機能(vhost)を使わず、生のTCPとして素通しします。振り分けと証明書は全部 Caddy の仕事にするためです
  • proxyProtocolVersion = "v2" が実IP引き継ぎの要です。frpc が接続元IPを PROXY protocol で Caddy に伝え、Caddy がそれをログと X-Forwarded-For に使います

手順5:動作確認

  1. スマホを Wi-Fi から切り離し(モバイル回線で)、https://example.com にアクセスできるか
  2. Caddy のアクセスログに、スマホのキャリアのIP(=実IP)が記録されているか
  3. VPS と自宅サーバーの両方を再起動し、数分後に自動で復旧するか
  4. SSH の公開ポートに外から繋がらないこと、frps ダッシュボードが公開IPで開かないことを確認

注意点

実際に構築して踏んだ罠を、正直に書いておきます。

1. Caddy の:80 リダイレクトと PROXY protocol が競合する

Caddy には HTTP→HTTPS リダイレクト専用の listener wrapper がありますが、あれを使うと:80 のリスナーを wrapper が占有し、PROXY protocol の設定が効かなくなります。結果、frp 経由の HTTP が 400 で死に、HTTPS のハンドシェイクも失敗します。

対策は手順3の通り、リダイレクト用に http://example.com のサイトを明示的に定義することです(その場合はリダイレクトが 308 ではなく 301 になります)。

2. Windows Defender が frp をマルウェアと誤検知する

frpc.exe は PUA:Win32/FRProxy、frps.exe はトロイの木馬として検知されることがあります(機械学習判定)。frp はリモートアクセス系ツールなので、検知ロジックに引っかかりやすいようです。

公式リリースのチェックサムを必ず照合したうえで、インストールフォルダだけを Defender の除外に設定してください。むやみに除外範囲を広げないこと。

3. SSH セッションから起動したプロセスのサービス化

Windows の OpenSSH 経由でフォアグラウンド起動したプロセスは、ログアウト時に巻き添えで終了します(ジョブオブジェクトの仕様)。常駐させたいものは必ずサービス化(WinSW等)するか、タスクスケジューラを使ってください。

新しいWebサービスを追加する

たとえばdocker desktopで別のセルフホスティングツールを立ち上げたときはCaddyfileに追記します。

  1. DNSのAレコードにサブドメインを追加する
  2. Caddyfileに追記する(ドメインとリバースプロキシ先。例えばaaa.example.comと3000)
  3. powershellでRestart-Service caddyをする。frpの変更は不要
  4. aaa.example.comを開いて3000番ポートで待ち受けているサービスが開くか確認する

ゲームサーバーを公開する

TCP/UDP のゲームサーバー(Minecraftなど)も同じ仕組みで公開できます。

  1. VPS の frps.toml の allowPorts にゲームのポートを追加し、frps を再起動
  2. VPS の ufw で同じポートを許可(TCP/UDP 両方なら sudo ufw allow 25565 のようにプロトコル無記入で1ルール)
  3. 自宅の frpc.toml に tcp / udp のプロキシを追加(PROXY protocol は付けません。ゲームのプロトコルには不要です)
[[proxies]]
name = "game-tcp"
type = "tcp"
localIP = "127.0.0.1"
localPort = 25565
remotePort = 25565

[[proxies]]
name = "game-udp"
type = "udp"
localIP = "127.0.0.1"
localPort = 25565
remotePort = 25565

なお UDP は Tailscale 経由の疎通確認がしにくい(環境によってはTCPのみ)ので、検証は実機+外部回線で行うのが確実です。

まとめ

  • VPS は「入口だけ」、実体と証明書は自宅側で自分で管理
  • ポート開放なし・固定IPなしでも、自宅サーバーを HTTPS で公開できる
  • 管理系(SSH・ダッシュボード)を Tailscale に寄せることで、攻撃対象を最小化できる

一度組んでしまえば、あとは証明書の更新も再起動後の復旧も全部自動です。自宅サーバーの公開で悩んでいる方の参考になれば幸いです。

タイトルとURLをコピーしました