Cloudflare を経由し、固定の IP から Internet にアクセスをする

Web アプリケーションが**送信元 IP アドレス指定でアクセスコントロール**している場合、リモートワークの環境ではそこがネックになることがあります。

この記事では Cloudflare のプロダクトを利用して、そんな状況に対応するためのいくつかの方法をメモしておきます。（他にもありそうですが、思いつくものを）

- 固定 IP を持つのは「データセンター」、遠隔から繋ぐ人は「リモートユーザー」と呼ぶことにします。
- Free プランでできるものから、Enterprise プランまで載せてます。

## 1. データセンターの HTTP Proxy 経由で外部に接続

リモートユーザーはデータセンターに配備されている明示 HTTP Proxy を踏んで、そこの固定 IP で Internet にアクセスします。
<img width="600" alt="Screenshot" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/e0289535-bf35-841d-ca72-a248a76687b3.png">

データセンター、リモートユーザーの双方に [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) を準備します。
ただ、これだとオープンに接続できてしまうので、[Cloudflare Access](https://www.cloudflare.com/ja-jp/zero-trust/products/access/) を付けて認証・認可を噛ませます。
<img width="600" alt="Screenshot" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/63d198c0-4126-78c5-aa72-22f2369fa88b.png">

cloudflared と Access を介してアプリケーションを公開する方法は本家以外にも情報がありますのでここでは割愛しますが、基本的な準備としては

- Cloudflare に[ゾーン](https://developers.cloudflare.com/dns/zone-setups/)（ドメイン）をつくる
    - 上の図では example.com か proxy.example.com
- そのゾーンに Access アプリケーション（[Self-Hosted](https://developers.cloudflare.com/cloudflare-one/applications/configure-apps/self-hosted-apps/)）をつくる
<img width="600" alt="Screenshot" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/cc3ea2a8-6c5b-81dc-d3d0-3b06d95c76aa.png">

- cloudflared トンネル（[Public Hostnames](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/routing-to-tunnel/)）を作り、Access アプリケーションと紐つける
    - 今回はデータセンターの HTTP Proxy サーバーを TCP の Service として指定
    - localhost なので cloudflared と HTTP Proxy を同一ホストで動かすケース
<img width="600" alt="Screenshot" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/95245ebb-2689-046c-213a-fc1646b75a20.png">

を完了しておきます。

### 操作
リモートユーザー側は `cloudfalred access` コマンドでブラウザーからの接続を待ち受けておきます。
```shell:リモートユーザー側：cloudflared
~ $ cloudflared access tcp --url localhost:12345 --hostname proxy.example.com
2023-08-29T06:44:54Z INF Start Websocket listener host=localhost:12345
```
ブラウザーからデータセンターの HTTP Proxy 経由で外部にアクセスを試みます。
```shell:リモートユーザー側：ブラウザ
~ $ curl -x localhost:12345 https://httpbin.org/ip
```

Access を噛ませているので認証が走ります。

認証画面がブラウザに自動表示されます。
されなければターミナルに表示された URL をブラウザで叩きます。

```shell:リモートユーザー側：cloudflared
:
A browser window should have opened at the following URL:

https://proxy.example.com/cdn-cgi/access/cli?edge_token_transfer=true&redirect_url=https%3A%2F%2F
...
If the browser failed to open, please visit the URL above directly in your browser.
```

認証を進めます。
<img width="600" alt="Screenshot" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/92ff9d37-7f6a-de67-5175-41ea471ea9e0.png">

外部 IDP での認証と Access での認可が成功すると、HTTP Proxy サーバーの固定 IP で接続することができます。

```shell:リモートユーザー側：ブラウザ
~ $  curl -x localhost:12345 https://httpbin.org/ip
{
  "origin": "データーセンターの IP アドレス"
}
```
HTTP Proxy サーバー側で受信するリクエストは下記のとおりでした。
```shell:データセンター側：パケットキャプチャ
Internet Protocol Version 4, Src: 127.0.0.1, Dst: 127.0.0.1
Transmission Control Protocol, Src Port: 59368, Dst Port: 3128, Seq: 1, Ack: 1, Len: 113
Hypertext Transfer Protocol
    CONNECT httpbin.org:443 HTTP/1.1\r\n
    Host: httpbin.org:443\r\n
    User-Agent: curl/8.2.1\r\n
    Proxy-Connection: Keep-Alive\r\n
    \r\n
```
```shell:データセンター側：明示 HTTP Proxy ログ
~$ tail /var/log/squid/access.log
127.0.0.1 TCP_TUNNEL/200 5841 CONNECT httpbin.org:443 - HIER_DIRECT/3.94.140.209 -
```
接続元は 127.0.0.1 となっていますが、これは cloudflared と HTTP Proxy が同じサーバーで動いている環境のためです。

### メモ
Access の認証については認証期間なども設定できます。
<img width="600" alt="Screenshot" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/3f39f96d-6309-5c18-9db8-5f741a4fa48d.png">

受領した Token の状況は `cloudflared access` でも確認できます。
設定通り24時間で Token が発行されているようです。
```shell:リモートユーザー側：JWT
~ $ cloudflared access token -app proxy.example.com | sh jwtdecode
{
  "alg": "RS256",
  "kid": "a0ed..."
}
{
  "aud": [
    "b00..."
  ],
  "email": "foo@bar",
  "exp": 1693377955,
  "iat": 1693291555,
  "nbf": 1693291555,
  "iss": "https://<YOUR_ORG>.cloudflareaccess.com",
  "type": "app",
  "identity_nonce": "JFk...",
  "sub": "a23...",
  "country": "JP"
}

>>> (1693377955-1693291555)/60/60
24.0
```

## 2\. cloudflared の socks-service で外部に接続

1 と違い、データセンターに HTTP Proxy は不要で、シンプルな構成になります。
cloudflared が [socks5 サーバで](https://github.com/cloudflare/cloudflared/commit/206523344f290dcf787156348e55a17348f9569f)動きます。

<img width="600" alt="Screenshot" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/88e65f31-eebb-557a-c42d-cfa9e2b8262a.png">

### 操作
この場合リモートの cloudflared は [CLI](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/install-and-setup/tunnel-guide/local/) で設定します。
（[ダッシュボード](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/install-and-setup/tunnel-guide/remote/)は socks-service 未対応）

```shell:データセンター側：cloudflared 設定抜粋
ingress:
 - hostname: socks.example.com
   service: socks-proxy
   originRequest:
     ipRules:
       - prefix: 0.0.0.0/0
         allow: true
```

リモートユーザー側は `cloudflared access` で接続を待ち受けておきます。
```shell:リモートユーザー側：cloudflared
~ $ cloudflared access tcp --hostname socks.example.com --url 127.0.0.1:23456
2023-08-31T04:52:19Z INF Start Websocket listener host=127.0.0.1:23456
```

ブラウザーから socks で接続すると、1 のように Access での認証が走ったあと、接続されます。（有効な認証情報がキャッシュされいれば、認証プロセスはバイパスされます）

```shell:リモートユーザー側：ブラウザー
~ $ curl httpbin.org/ip --socks5-hostname localhost:23456
{
  "origin":  "データーセンターの IP アドレス"
}
```
## 3\. WARP クライアントからデータセンターの HTTP Proxy 経由で外部に接続

<img width="600" alt="Screenshot" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/1c41f059-f98b-2cdb-6689-cb6b95df525c.png">

ローカル側は cloudflared でなく WARP Client にします。
1、2 と違い 、リモートユーザー側は **HTTP Proxy のプライベート IP に直接つなぐ**、VPN的な接続になります。

### 操作

cloudflared の設定で [Private Network](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/private-net/)（[WARP Routing](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/install-and-setup/tunnel-guide/local/local-management/configuration-file/#top-level-configurations)）をデータセンターのサブネットで有効にしておきます。
<img width="600" alt="Screenshot" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/dd49d350-6cd8-3883-37e6-0f195e8a79dd.png">

WARP On の状態のブラウザーから HTTP Proxy 172.16.0.252:3128 経由で接続することができました。

```shell:リモートユーザー側：WARP Client & ブラウザー
~$ warp-cli status
Status update: Connected
Success

~$ curl -x 172.16.0.252:3128 httpbin.org/ip
{
  "origin":  "データーセンターの IP アドレス"
}
```
HTTP Proxy の接続ログは下記の通りです。

```shell:データセンター側：明示 HTTP Proxy ログ
~$ sudo tail -f /var/log/squid/access.log
172.16.0.147 TCP_MISS/200 352 GET http://httpbin.org/ip - HIER_DIRECT/54.175.144.50 application/json
```
接続元の 172.16.0.147 というのはデータセンター側の cloudflared の IPアドレスになります。つまり、**cloudflared はソース NAT をして HTTP Proxy にリクエストを転送**します。
cloudflared と HTTP Proxy は別サーバーで動いていることがわかります。


### メモ
内部のホストに IP アドレスではなく、名前でアクセスしたい場合は DNS Policy の Override でホスト名を設定することもできます。

その場合、まず [Fallback domains](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/private-net/private-hostnames-ips/#create-a-local-domain-fallback-entry) を確認します。
ここにあるドメインは Gateway の DNS でなくローカルのリゾルバに問い合わせます。（リスト中のドメインの変更はユーザでできますし、内部ホストの名前解決なら Gateway で Override するのでなく、そっちにやらせてもいいですね）
```shell:リモートユーザー：WARP 設定
~$ warp-cli settings
:
Fallback domains:
  home.arpa
  intranet
  internal
  private
  localdomain
  domain
  lan
  home
  host
  corp
  local
  localhost
  invalid
  test
:
```

Gateway に DNS 解決を依頼したいので、ここに入ってない名前をつけます。
my.proxy という名前にし 172.16.0.252 で A を返すようにします。（テスト的に使ってます。いずれ Cloudflare の権威DNS で内部のゾーンもサービスできることを期待）

<img width="600" alt="Screenshot" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/55aa0706-7850-806b-f179-c35f5c26b815.png">

名前で接続してみます。

```shell:リモートユーザー側：内部名で接続
~$ dig my.proxy +short
172.16.0.252

~$ curl -x my.proxy:3128 httpbin.org/ip
{
  "origin":  "データーセンターの IP アドレス”
}
```

## 4\. Magic を使う
[Magic WAN](https://www.cloudflare.com/ja-jp/network-services/products/magic-wan/) というプロダクトもあります。Enterprise プランのみです。

ざっくり言うと、プライベートネットワークをまるごと Cloudflare に載せ、相互通信や Internet との接続を行うというものです。
従来の IP-VPN 網などを置き換えます、地球規模で一枚に。

これにリモートユーザーが [WARP で乗り入れる](https://developers.cloudflare.com/magic-wan/zero-trust/warp/)ことができます。

<img width="600" alt="Screenshot" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/6b797f87-945d-1503-e48c-55b0be910db2.png">

### 操作
まず、プライベートネットワークのつなぎですが、さまざまなルーターとの[接続例](https://developers.cloudflare.com/magic-wan/third-party/)があります。
今回は strongswan でつながってます。
```shell:データセンター側：IPsec ルータの状況
~$ sudo ipsec status
Security Associations (1 up, 0 connecting):
   oci_to_cf[43]: ESTABLISHED 8 minutes ago, 172.16.1.223[...]...<Cloudflare の Anycast IP>
   oci_to_cf{268}:  INSTALLED, TUNNEL, reqid 1, ESP SPIs: c5_i 06_o
   oci_to_cf{268}:   0.0.0.0/0 === 0.0.0.0/0
```

WARP Client から接続すると HTTP Proxy で Block されました… 
ログを見ると WARP Client の IP が [100.96.0.0/12](https://developers.cloudflare.com/magic-wan/zero-trust/warp/#1-route-packets-back-to-warp-devices) で [ISP Shared Address](https://datatracker.ietf.org/doc/html/rfc6598#section-7) となってます。
```shell:データセンター側：明示 HTTP Proxy ログ
$ sudo tail -f /var/log/squid/access.log
100.96.0.62 TCP_DENIED/403 3858 GET http://httpbin.org/ip - HIER_NONE/- text/html
```
そうでした。WARP クライアントが Cloudflare の中をさまようときはそのレンジから IP を割り振られるのでした。

HTTP Proxy の ACL に追加していなかったので、こいつを通すようにします。

```shell:データセンター側：squid.conf
acl my_net src 100.96.0.0/12
```
接続できました。
```shell:リモートユーザー側：ブラウザー
~$ curl -x 172.16.1.243:3128 httpbin.org/ip
{
  "origin": "データーセンターの IP アドレス"
}
```
プロキシーを噛ます場合は、送信元の IP アドレス確認も大事ですね。

## 5\. データセンターの固定 IP の代わりに、Gateway の Dedicated Egress IP オプションに切り替える

[Enterprise plan のみ](https://developers.cloudflare.com/cloudflare-one/policies/gateway/egress-policies/dedicated-egress-ips/#dedicated-egress-ips)

これまでのような、**データセンターの配備が不要**です（cloudflared や HTTP proxy など）。
**Cloudflare Gateway だけで**、お客様専有の固定された IP アドレスでインターネットに[出れます](https://blog.cloudflare.com/ja-jp/gateway-dedicated-egress-policies-ja-jp/)。

**目的をシンプルに実現**できます。

リモートユーザーからの接続の方法としては下記あたりです。

- WARP クライアント
- ブラウザで明示 HTTP Proxy 指定
    - 上の例と違うのは Cloudflare Gateway 自身が[明示 Proxy として](https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/agentless/pac-files/)も動くところ → こちらはEnterprise のみ


<img width="600" alt="Screenshot" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/51c8cd8e-b60c-79ad-71c2-fb42e8648ff1.png">

### 操作
まず、WARP Client から直接接続してみます。
```shell:リモートユーザー側：WARP 経由インターネットへの直接接続
~$ warp-cli status
Status update: Connected
Success

~$ curl httpbin.org/ip
{
  "origin": "ユーザー専用の Gateway 出口 IP アドレス"
}
```

次に Gateway の 明示 HTTP Proxy 経由でアクセスします。
これは送信元の IP アドレスで利用をみてアクセスコントロールをしますので、一旦 WARP を切りリモートユーザー自身の IP アドレスを確認します。
```shell:リモートユーザー側：WARP Off & IP 確認
~$ warp-cli disconnect
Success

~$ curl httpbin.org/ip
{
  "origin": "リモートユーザーのIPアドレス"
}
```
この IP を　Gateway の明示 HTTP Proxy で受け入れるように設定します。
すると、下記のような接続が可能となります。（”サブドメイン”の部分は自動で割り振られます）

```shell:リモートユーザー側：Gateway Explicit Proxy 経由での接続
~$ curl -x https://サブドメイン.proxy.cloudflare-gateway.com:443 httpbin.org/ip

{
  "origin": "ユーザー専用の Gateway 出口 IP アドレス"
}
```
Gateway 側の関連設定を確認する方法は下記のとおりです。（APIでの例）

```shell:Cloudflare Gateway：Proxy ユーザーに割り当てる出口 IP
~ $ curl https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/gateway/rules \
  --header 'Content-Type: application/json' \
  --header 'X-Auth-Email: '$EMAIL'' \
  --header 'X-Auth-Key: '$API_KEY'' -s | jq '.result[]|select (.action=="egress")' | jq '.|select (.name =="設定名")|.rule_settings.egress'


{
  "ipv6": "ユーザー専用の出口 IPv6/64",
  "ipv4": "ユーザー専用の出口 IPv4/32"
}
```

```shell:Cloudlare Gateway：Proxy に接続許可する IP
~ $ curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/gateway/proxy_endpoints" \
  --header 'Content-Type: application/json' \
  --header 'X-Auth-Email: '$EMAIL'' \
  --header 'X-Auth-Key: '$API_KEY'' -s| jq '.result[]|select (.subdomain=="サブドメイン")|.ips'

[
  "Proxy の利用を許可するリモートユーザー側のサブネット",
  :
]
```



## 5\. 【番外】1〜4 は併用が可能
いくつかの方法を示しましたが、これらは併用することができます。

## 6\. 【番外】WARPクライアント利用時にローカルに流す
WARP を利用しているときに、そこの環境の出口 IP で接続するのでよければ、[Split Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/configure-warp/route-traffic/split-tunnels/) が使えます。
トラフィックを WARP のトンネルに流さずに、物理インタフェースに流すため、ローカルのIPで出ることができます。
<img width="600" alt="Screenshot" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/8f79887a-6e08-09f3-3818-ef27efb769ea.png">

**ルーターでローカルブレイクアウトするのではなく、端末の時点でブレイクアウトする**という形です。

## 7\. 【番外】WARP から WARP へ
[WARP-to−WARP](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/private-net/create-private-networks/) ってのも使えそうです。執筆時点では closed beta とありますが、**本家のドキュメントはどんどん更新**されていきますので、期待です。
逆にこの様な記事がすぐ古くなります…

## 8\.【番外】勝手トンネル対策

便利さの裏返しで、組織内から勝手にトンネル張られないようにするには、[cloudflared の利用ポート](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/install-and-setup/ports-and-ips/) および [WARP の利用ポート](https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/deployment/firewall/) を参考に、対象のサーバーからの接続のみ許可するなど、対策をしておきましょう。
**他の VPN や SSH トンネルなどと考え方は同じ** ですね。

## まとめ
準備する事項、使われる IP アドレス、対応プランをまとめます。
||リモートユーザー側|データセンター側|公開ドメイン|Webアプリが受ける送信元 IP アドレス|対応プラン|
|-|-|-|-|-|-|
|1|cloudflared access<br>+<br>ブラウザー|cloudflared tunnel<br>+<br>HTTP proxy|要|データセンターのIP|すべて|
|2|cloudflared access<br>+<br>ブラウザー|cloudflared tunnel|要|データセンターのIP|すべて|
|3|WARP<br>+<br>ブラウザー|cloudflared tunnel<br>+<br>HTTP proxy|不要|データセンターのIP|すべて|
|4|WARP<br>+<br>ブラウザー|Magic WAN<br>+<br>HTTP Proxy|不要|データセンターのIP|Enterprise|
|5|WARP<br>+<br>ブラウザー<br>or<br>ブラウザーのみ|なし|不要|Cloudflare Gateway の Dedicated Egress IP|Enterprise|

これも追加
https://zenn.dev/oymk/articles/98177fc9636c26

## 執筆BGM

ニッポン放送ショーアップナイター
巨人 対 広島 24回戦