深夜にサーバーがSSHブルートフォース攻撃を受けてから、セキュリティを後回しにするという選択肢はなくなった。Gitea、Vaultwarden、Uptime Kumaをセルフホストし始めたとき、問いは「どうやって公開するか?」ではなく「マシンを標的にさらさずにどうやって公開するか?」だった。標準的なポートフォワーディングは機能する——設定ミスのルールや新たに発見された脆弱性が見事に機能しなくさせるまでは。
Cloudflare Tunnelがその問題を解決してくれた。以降では両アプローチを直接比較し、Tunnelが苦手とする点、そしてsystemdサービス・DNSルーティング・Zero Trust Accessポリシーを含む完全なLinux構築手順を紹介する。
アプローチ比較:リバースプロキシ vs. Cloudflare Tunnel
従来型:リバースプロキシ+ポートフォワーディング
定番の構成:ルーターがポート80/443をローカルのNginxまたはCaddyインスタンスに転送し、そこからアプリへプロキシする。Let’s Encryptの更新、fail2banのルール、ファイアウォールポリシー、WAFの設定はすべて自分で管理する。機能はする。しかし実IPがパブリックDNSに載り、ポートは稼働から1時間以内にShodanに登録され、公開したサービスはすべて自分が管理すべき攻撃対象となる。
家庭用ISPにはさらなる問題がある。多くはインバウンドポート80/443を完全にブロックしており、非標準ポートを使うか、TLS終端のためだけに別のVPSを用意するしかない。
Cloudflare Tunnel:アウトバウンド専用モデル
Cloudflare Tunnelは接続方向を逆転させる。サーバー上でデーモンとして動作するcloudflaredを起動すると、Cloudflareのエッジに対して持続的なアウトバウンド接続を確立する。訪問者がドメインにアクセスすると、トラフィックはCloudflareに入り、そのトンネルを経由してルーティングされる。サーバーはパブリックポートでリッスンする必要が一切ない。
ファイアウォールでインバウンドトラフィックをすべてブロックしても、アプリは正常に動作する。実IPがパブリックDNSに登録されることもない。TLS終端、DDoS緩和、そしてオプションでリクエストがアプリに届く前にZero Trust認証をCloudflareが処理する。
メリットとデメリット
従来型リバースプロキシ
- メリット:完全なコントロール、サードパーティ依存なし、任意のプロトコル対応(TCP/UDP/rawソケット)、地理的に近いユーザーへの低レイテンシ
- デメリット:実IPが公開されてスキャン可能、すべてのセキュリティレイヤーを自己管理、家庭用ISPのポートブロックが一般的、プロキシブロックの設定ミスで内部が露出するリスク
Cloudflare Tunnel
- メリット:インバウンドポート不要、実IPがパブリックDNSに非公開、無料のDDoS保護とCDN、Zero Trust Accessでトラフィックがアプリに届く前に認証レイヤーを追加、無料プランで最大50ユーザーに対応
- デメリット:すべてのトラフィックがCloudflareインフラを経由する(プライバシーの考慮が必要)、Cloudflare PoPから遠いユーザーにはわずかなレイテンシ増加、ドメインのDNSはCloudflare管理が必須、無料プランではraw TCP/UDPトンネリングにCloudflare WARPが必要
推奨構成
自宅サーバーまたはVPS上のWebアプリやダッシュボードには?Zero Trust Accessポリシーを組み合わせたCloudflare Tunnelを使おう。インバウンドポートを一切開放せず、CloudflareをWAFとして活用し、リクエストがアプリケーションコードに届く前に強固な認証ゲートを設ける。たとえアプリに未パッチのCVEがあっても、攻撃者はCloudflareの認証レイヤーを突破しない限りアクセスできない。
レイテンシに敏感なサービス、ゲームサーバー、直接SSH接続やデータベース接続などのraw TCPプロトコルには、WireGuardまたは厳格なファイアウォールルールを設けた従来型リバースプロキシを使う。実践的な使い分け:WebアプリはCloudflare Tunnel経由、サーバー管理はIPホワイトリストを設定した非標準ポートのWireGuard経由。
構築手順
前提条件
- CloudflareでDNSを管理しているドメイン(無料アカウントで可)
- アプリがローカルで動作しているLinuxサーバー(本ガイドはDebian/Ubuntuを対象)
- Cloudflare Zero Trustアカウント(無料プラン)
ステップ1:cloudflaredをインストール
curl -L --output cloudflared.deb \
https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.deb
cloudflared --version
RPM系ディストリビューションやmacOSの場合は、GitHubリリースページから適切なバイナリを取得する。バイナリは静的リンクのため、ランタイム依存関係を管理する必要はない。
ステップ2:Cloudflareで認証
cloudflared tunnel login
ブラウザが開くので、ログインして使用したいドメインを選択する。cloudflaredが証明書を~/.cloudflared/cert.pemに書き込む。このファイルはアカウント全体のトンネル作成を認可するものなので、安全に管理すること。
ステップ3:名前付きトンネルを作成
cloudflared tunnel create my-homelab
出力にトンネルのUUIDが表示される。認証情報ファイルは~/.cloudflared/<UUID>.jsonに保存される。このUUIDは次のステップで必要になるのでメモしておくこと。
ステップ4:設定ファイルを作成
~/.cloudflared/config.yamlを作成する:
tunnel: <YOUR-TUNNEL-UUID>
credentials-file: /home/youruser/.cloudflared/<UUID>.json
ingress:
- hostname: gitea.yourdomain.com
service: http://localhost:3000
- hostname: uptime.yourdomain.com
service: http://localhost:3001
- service: http_status:404
最後のキャッチオールルールは、どのingressエントリにも一致しないホスト名に対して404を返す。これを省略すると、マッチしないリクエストでcloudflaredがエラーを返し、デーモンがクラッシュする可能性がある。
ステップ5:DNSをトンネルにルーティング
cloudflared tunnel route dns my-homelab gitea.yourdomain.com
cloudflared tunnel route dns my-homelab uptime.yourdomain.com
これにより、Cloudflare DNSにトンネルを指すCNAMEレコードが自動的に作成される。DNSパネルを手動で操作する必要はない。
ステップ6:systemdサービスとして実行
sudo cloudflared service install
sudo systemctl enable cloudflared
sudo systemctl start cloudflared
sudo systemctl status cloudflared
サービスが起動しない場合はログを確認する:
sudo journalctl -u cloudflared -f
9割のケースで原因はcredentials-fileのパスが間違っていることだ。設定ファイルのUUIDがディスク上の実際のファイル名と一致しているか確認すること。
ステップ7:Zero Trust Accessによる認証を追加
多くのガイドはこのステップを省略する。それは間違いだ。Zero Trust Accessこそが、cloudflaredを「賢いルーティングの仕組み」から「実際のセキュリティ境界」に変えるものだ。Cloudflare Zero Trustダッシュボードで:
- Access → Applications → Add an applicationに移動
- Self-hostedを選択
- ホスト名を入力(例:
gitea.yourdomain.com) - Policiesでルールを追加——
@yourdomain.comのメールアドレスを許可するか、特定のアドレスをリスト
有効化すると、Cloudflareがすべての訪問者に対してアプリへの転送前にログイン画面を表示する。個人利用であればメールOTPオプションが便利で、OAuthのセットアップが不要——Cloudflareが許可済みのアドレスにワンタイムコードを送信する。パスワードなし、認証サービスの管理も不要。
動作確認
# アクティブなトンネルとその状態を一覧表示
cloudflared tunnel list
# パブリックポート80/443でリッスンしていないことを確認
ss -tlnp | grep ':80\|:443'
# 期待される結果:空の出力
# トンネル経由でアプリをテスト
curl -I https://gitea.yourdomain.com
構築後のセキュリティ強化
次はネットワーク境界を閉じる。cloudflaredはアウトバウンド接続のみ必要とするため、インバウンドをすべてブロックしても何も変わらない——ポートスキャナーの攻撃対象面が丸ごとなくなるだけだ:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh # WireGuardまたはWARPを設定したら削除する
sudo ufw enable
トンネルの認証情報を定期的にローテーションする:
cloudflared tunnel rotate-secret my-homelab
ダッシュボードのZero Trust → Networks → Tunnelsでトンネルの状態を監視できる。コネクターのアップタイム、アクティブな接続数、トンネルごとのトラフィック量が確認可能だ。
Dockerユーザーへの重要な注意点:コンテナ内でcloudflaredを実行する場合、設定ディレクトリをボリュームとしてマウントし、--config /etc/cloudflared/config.yamlで明示的にパスを指定すること。このフラグがないと、コンテナ内でバイナリのデフォルトパス探索が無音で失敗し、空の設定でエラーメッセージなしに起動してしまう。深夜2時にこれをデバッグするのは非常に不快な体験だ。
Tunnelが苦手とする場面
HTTPとHTTPSは問題なく動作する。raw TCPは別の話だ。直接SSHセッション、データベース接続、カスタムプロトコルには、Cloudflare WARP-to-Tunnel(無料Zero Trustプランで利用可能だが追加設定が必要)か、別途VPNレイヤーが必要だ。WARPを使う場合、すべてのクライアントにWARPアプリのインストールが必要になる。個人利用なら問題ないが、他者とアクセスを共有する場合はすぐに摩擦が生じる。
深夜のブルートフォース攻撃を受けた後の私の構成:WebアプリはZero Trust AccessのCloudflare Tunnel経由、サーバー管理はIPホワイトリストを設定した高番号ポートのWireGuard経由。ブルートフォースボットは自動化されており、プローブを止めることはない。実際に効果があるのは、プローブする対象を与えないことだけだ。

