Cloudflare Zero TrustでSSHを保護する
背景
サーバにSSHでアクセスするには、通常22番ポートをインターネットに公開する必要がある。しかし公開されたSSHポートは常に攻撃の標的になる。
Cloudflare Zero Trustを使えば、SSHポートを閉じたまま、認証済みのユーザーだけがSSH接続できる環境を構築できる。
Zero Trustとは
従来のセキュリティモデルでは「社内ネットワーク内は信頼する」という前提があった。Zero Trustはこの前提を捨て、全てのアクセスを検証するモデル。
【従来】
インターネット → ファイアウォール → 社内(信頼済み)
※ 一度入れば自由にアクセス可能
【Zero Trust】
インターネット → Cloudflare(認証・認可) → サーバ
※ 毎回アクセスを検証する
※ 認証されていなければ接続不可
仕組み
SSHの場合、Cloudflare TunnelとAccess機能を組み合わせる。
開発者のPC
└── ssh コマンド
└── cloudflared(ProxyCommand)
└── Cloudflare(Access認証)
└── Tunnel
└── サーバのSSHデーモン
- SSHコマンドを実行すると、
cloudflaredがプロキシとして動作 - Cloudflare Accessが認証を要求(ブラウザでメール認証)
- 認証成功後、Tunnel経由でサーバのSSHに接続
- サーバ側のSSHポートは閉じたまま
前提条件
- Cloudflare Tunnelが設定済みであること(Cloudflare Tunnelの設定方法を参照)
- ローカルPCに
cloudflaredがインストール済みであること
手順
1. Tunnel IngressにSSHを追加
Cloudflare APIでSSHのルーティングを追加する:
curl -X PUT "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/cfd_tunnel/<TUNNEL_ID>/configurations" \
-H "X-Auth-Email: <EMAIL>" \
-H "X-Auth-Key: <API_KEY>" \
-H "Content-Type: application/json" \
--data '{
"config": {
"ingress": [
{"hostname": "ssh-cf.example.jp", "service": "ssh://host.docker.internal:22"},
{"service": "http_status:404"}
]
}
}'
cloudflaredがDockerコンテナで動作している場合、host.docker.internalでホストのSSHデーモンにアクセスする。docker-compose.ymlにextra_hostsの設定が必要:
services:
cloudflared:
image: cloudflare/cloudflared:latest
extra_hosts:
- host.docker.internal:host-gateway
2. DNSレコードの追加
cloudflared tunnel route dns my-tunnel ssh-cf.example.jp
3. Access Applicationの作成
Cloudflare APIでSSH用のAccessアプリケーションを作成する:
curl -X POST "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/access/apps" \
-H "X-Auth-Email: <EMAIL>" \
-H "X-Auth-Key: <API_KEY>" \
-H "Content-Type: application/json" \
--data '{
"name": "My Server SSH",
"domain": "ssh-cf.example.jp",
"type": "ssh",
"session_duration": "24h"
}'
type: "ssh"が重要。これによりCloudflareがSSHプロキシとして動作する。
4. アクセスポリシーの設定
誰がSSHできるかを制御する。メールアドレスで制限する例:
curl -X POST "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/access/apps/<APP_ID>/policies" \
-H "X-Auth-Email: <EMAIL>" \
-H "X-Auth-Key: <API_KEY>" \
-H "Content-Type: application/json" \
--data '{
"name": "Allow admin",
"decision": "allow",
"include": [
{"email": {"email": "admin@example.com"}}
]
}'
他にも以下のような条件で制限可能:
- メールドメイン(
@example.ac.jpの全員) - IPアドレス範囲
- 国
- IdP(Google Workspace、GitHub等)のグループ
5. ローカルPCのSSH設定
~/.ssh/configに以下を追加:
Host my-server-cf
HostName ssh-cf.example.jp
User myuser
IdentityFile ~/.ssh/my_key
ProxyCommand cloudflared access ssh --hostname %h
6. 接続
ssh my-server-cf
初回はブラウザが開き、メールアドレスの認証が求められる。認証後、トークンがローカルにキャッシュされ、session_duration(24時間)の間は再認証なしで接続できる。
7. SSHポートの閉鎖
Zero Trust SSH経由で接続できることを確認したら、サーバのファイアウォールまたはセキュリティグループで22番ポートを閉じる。
従来のSSHとの比較
| 項目 | 通常のSSH | Zero Trust SSH |
|---|---|---|
| ポート開放 | 22番が必要 | 不要 |
| 認証 | 鍵認証のみ | 鍵認証 + Cloudflare Access |
| ブルートフォース | リスクあり | 攻撃経路なし |
| アクセス制御 | IP制限程度 | メール・IdP・国など柔軟 |
| 監査ログ | サーバのみ | Cloudflare側にも記録 |
| VPN | 場合により必要 | 不要 |
トラブルシューティング
Empty app domainエラー
古いトークンキャッシュが原因。以下で解消:
rm -f ~/.cloudflared/*.lock ~/.cloudflared/*-token*
再度sshコマンドを実行してブラウザ認証をやり直す。
Connection timed out during banner exchange
cloudflaredコンテナからホストのSSHに到達できていない可能性がある。
extra_hostsが設定されているか確認- docker-compose.ymlで
--protocol http2を指定(UDPが制限されている環境ではQUIC接続が失敗するため) - コンテナからホストへの接続テスト:
docker run --rm --add-host host.docker.internal:host-gateway alpine sh -c "nc -zv host.docker.internal 22"(alpineにはncがデフォルトで含まれている)
QUICエラー
Failed to dial a quic connection
サーバのネットワークがUDPを制限している場合に発生する。cloudflaredの起動コマンドに--protocol http2を追加:
command: tunnel --protocol http2 run
まとめ
Cloudflare Zero Trust SSHを使うことで、SSHポートを完全に閉じた状態でサーバに安全に接続できる。鍵認証に加えてCloudflare Accessの認証が加わるため、多層防御が実現する。万が一SSHの鍵が漏洩しても、Cloudflare Accessの認証を通過しなければ接続できない。