Cloudflare Zerot Trust の Proxy Endpoint を PAC ファイルを使って確認する

# Proxy Endpoint とは

HTTP/S フィルタリング機能のみを使いたい場合に、Agentless で実現できます。

OS やブラウザの Proxy 設定を PAC ファイルで運用されている場合に対応できます。

# Proxy Endpoint の作成

API から作成できます。
ソース IP の設定は、例えばクライアントが出ていくグローバル IP を指定して制限できます。

> https://api.cloudflare.com/#zero-trust-gateway-proxy-endpoints-create-proxy-endpoint

```shell
  export EMAIL='YOUR_EMAIL'
  export APIKEY='YOUR_APIKEY'
  export ACCOUNT_ID='YOUR_ACCOUNT_ID'
```

```shell
curl -sX POST "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/gateway/proxy_endpoints" \
     -H "X-Auth-Email: $EMAIL" \
     -H "X-Auth-Key: $APIKEY" \
     -H "Content-Type: application/json" \
     --data '{"name":"test","ips":["192.0.2.1/32"]}' | jq
```

```json
{
  "result": {
    "id": "1aeca09e-56af-4923-b481-a6e20e66c1bf",
    "name": "test",
    "created_at": "2022-05-29T15:44:21.352832Z",
    "updated_at": "2022-05-29T15:44:21.352832Z",
    "ips": [
      "192.0.2.1/32"
    ],
    "subdomain": "oli3n9zkz5"
  },
  "success": true,
  "errors": [],
  "messages": []
}
```

# 通信確認（Cloudflare CA なし）

[Cloudflare のルート CA 証明書](https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/install-cloudflare-cert/) の指定がないと、エラーになります。

```shell
% curl -4 -p https://cloudflare.com/cdn-cgi/trace --proxy https://oli3n9zkz5.proxy.cloudflare-gateway.com -v 
*   Trying 162.159.193.21:443...
* Connected to oli3n9zkz5.proxy.cloudflare-gateway.com (162.159.193.21) port 443 (#0)
* ALPN, offering http/1.1
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-ECDSA-AES128-GCM-SHA256
* ALPN, server accepted to use http/1.1
* Proxy certificate:
*  subject: C=US; ST=California; L=San Francisco; O=Cloudflare, Inc.; CN=proxy.cloudflare-gateway.com
*  start date: Feb 15 00:00:00 2022 GMT
*  expire date: Feb 15 23:59:59 2023 GMT
*  subjectAltName: host "oli3n9zkz5.proxy.cloudflare-gateway.com" matched cert's "*.proxy.cloudflare-gateway.com"
*  issuer: C=US; O=DigiCert Inc; CN=DigiCert TLS Hybrid ECC SHA384 2020 CA1
*  SSL certificate verify ok.
* allocate connect buffer!
* Establish HTTP proxy tunnel to cloudflare.com:443
> CONNECT cloudflare.com:443 HTTP/1.1
> Host: cloudflare.com:443
> User-Agent: curl/7.82.0-DEV
> Proxy-Connection: Keep-Alive
> 
< HTTP/1.1 200 OK
< Cf-Team: 11b231faec0000f8bb1116a400000001
< Date: Sun, 29 May 2022 16:14:23 GMT
< 
* Proxy replied 200 to CONNECT request
* CONNECT phase completed!
* ALPN, offering h2
* ALPN, offering http/1.1
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (OUT), TLS alert, unknown CA (560):
* SSL certificate problem: unable to get local issuer certificate
* Closing connection 0
* TLSv1.2 (OUT), TLS alert, close notify (256):
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
```

# ルート CA ダウンロード

```shell
curl -O https://developers.cloudflare.com/cloudflare-one/static/documentation/connections/Cloudflare_CA.pem               
```

# 通信確認（Cloudflare CA あり）

```shell
% curl -4 -p https://cloudflare.com/cdn-cgi/trace --proxy https://oli3n9zkz5.proxy.cloudflare-gateway.com -v --cacert Cloudflare_CA.pem
*   Trying 162.159.193.21:443...
* Connected to oli3n9zkz5.proxy.cloudflare-gateway.com (162.159.193.21) port 443 (#0)
* ALPN, offering http/1.1
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-ECDSA-AES128-GCM-SHA256
* ALPN, server accepted to use http/1.1
* Proxy certificate:
*  subject: C=US; ST=California; L=San Francisco; O=Cloudflare, Inc.; CN=proxy.cloudflare-gateway.com
*  start date: Feb 15 00:00:00 2022 GMT
*  expire date: Feb 15 23:59:59 2023 GMT
*  subjectAltName: host "oli3n9zkz5.proxy.cloudflare-gateway.com" matched cert's "*.proxy.cloudflare-gateway.com"
*  issuer: C=US; O=DigiCert Inc; CN=DigiCert TLS Hybrid ECC SHA384 2020 CA1
*  SSL certificate verify ok.
* allocate connect buffer!
* Establish HTTP proxy tunnel to cloudflare.com:443
> CONNECT cloudflare.com:443 HTTP/1.1
> Host: cloudflare.com:443
> User-Agent: curl/7.82.0-DEV
> Proxy-Connection: Keep-Alive
> 
< HTTP/1.1 200 OK
< Cf-Team: 11b23413fa0000f8bb11184400000001
< Date: Sun, 29 May 2022 16:16:41 GMT
< 
* Proxy replied 200 to CONNECT request
* CONNECT phase completed!
* ALPN, offering h2
* ALPN, offering http/1.1
*  CAfile: Cloudflare_CA.pem
*  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
* ALPN, server accepted to use h2
* Server certificate:
*  subject: CN=cloudflare.com
*  start date: May 27 03:00:00 2022 GMT
*  expire date: Jul 13 16:14:23 2022 GMT
*  subjectAltName: host "cloudflare.com" matched cert's "cloudflare.com"
*  issuer: C=US; ST=California; L=San Francisco; O=Cloudflare, Inc.; OU=Gateway Intermediate ECC Certificate Authority
*  SSL certificate verify ok.
* Using HTTP2, server supports multiplexing
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7fdd05814a00)
> GET /cdn-cgi/trace HTTP/2
> Host: cloudflare.com
> user-agent: curl/7.82.0-DEV
> accept: */*
> 
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
* Connection state changed (MAX_CONCURRENT_STREAMS == 4294967295)!
< HTTP/2 200 
< date: Sun, 29 May 2022 16:16:41 GMT
< content-type: text/plain
< access-control-allow-origin: *
< server: cloudflare
< cf-ray: 7130893359bbf8bb-NRT
< x-frame-options: DENY
< x-content-type-options: nosniff
< expires: Thu, 01 Jan 1970 00:00:01 GMT
< cache-control: no-cache
< cf-team: 11b234140e0000f8bb11185400000001
< 
fl=22f221
h=cloudflare.com
ip=x.x.x.x
ts=1653841001.494
visit_scheme=https
uag=curl/7.82.0-DEV
colo=NRT
http=http/2
loc=JP
tls=TLSv1.3
sni=plaintext
warp=on
gateway=on
* Connection #0 to host oli3n9zkz5.proxy.cloudflare-gateway.com left intact
```

# ダッシュボードでログ確認

レスポンスヘッダ `Cf-Team: 11b23413fa0000f8bb11184400000001` と Request ID が対応していることがわかります。

![image-20220530012246335](https://cdn.jsdelivr.net/gh/kyouheicf/image@master/uPic/image-20220530012246335.png)

また、Network タブのログでは特定の Proxy PAC Endpoint への通信と認識されたことがわかります。

![image-20220530012732582](https://cdn.jsdelivr.net/gh/kyouheicf/image@master/uPic/image-20220530012732582.png)

# PAC ファイル作成

RFC 1918 のアドレス以外では、すべて Proxy Endpoint を経由して Proxy 適用される設定です。

```javascript:proxy.pac
function FindProxyForURL(url,host)
{
    // No proxy for private (RFC 1918) IP addresses (intranet sites)
    if (isInNet(dnsResolve(host), "10.0.0.0", "255.0.0.0") ||
        isInNet(dnsResolve(host), "172.16.0.0", "255.240.0.0") ||
        isInNet(dnsResolve(host), "192.168.0.0", "255.255.0.0")) {
         return "DIRECT";
    }
 
    // No proxy for localhost
    if (isInNet(dnsResolve(host), "127.0.0.0", "255.0.0.0")) {
        return "DIRECT";
    }
 
    // Proxy all
    return "HTTPS oli3n9zkz5.proxy.cloudflare-gateway.com:443";
}
```

# Cloudflare R2 にアップロード

R2 の Open Beta にサインアップし、バケット作成後に `proxy.pac` ファイルをアップロードします。

> [Get started guide · Cloudflare R2 docs](https://developers.cloudflare.com/r2/get-started/)

![image-20220530102327016](https://cdn.jsdelivr.net/gh/kyouheicf/image@master/uPic/image-20220530102327016.png)

# Workers 経由で公開する準備

以下のディレクトリを `wrangler` コマンドで準備します。

> [Get started guide · Cloudflare R2 docs](https://developers.cloudflare.com/r2/get-started/)

```shell
wrangler login
wrangler init r2_public_pac
tree r2_public_pac
```

```shell
r2_public_pac
├── src
│   └── index.js
└── wrangler.toml
```

`index.js` は以下のコードを使います。

```javascript:index.js
const ALLOW_LIST = ['proxy.pac'];

// Check requests for a pre-shared secret
const hasValidHeader = (request, env) => {
  return request.headers.get('X-Custom-Auth-Key') === env.AUTH_KEY_SECRET;
};

function authorizeRequest(request, env, key) {
  switch (request.method) {
    case 'GET':
      return ALLOW_LIST.includes(key);
    default:
      return false;
  }
}

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    const key = url.pathname.slice(1);

    if (!authorizeRequest(request, env, key)) {
      return new Response('Forbidden', { status: 403 });
    }

    switch (request.method) {
      case "GET":
        const object = await env.MY_BUCKET.get(key);

        if (!object) {
          return new Response("Object Not Found", { status: 404 });
        }

        return new Response(object.body);
      default:
        return new Response("Method Not Allowed", { status: 405 });
    }
  }
};
```

`wrangler.toml` は以下のファイルを使います。

```toml:wrangler.toml
name = "r2_public_pac"
main = "src/index.js"
compatibility_date = "2022-05-30"

account_id = "YOUR_ACCOUNT_ID"
workers_dev = true

[[r2_buckets]]
binding = 'MY_BUCKET' # <~ valid JavaScript variable name
bucket_name = 'my-bucket'
```

# PAC ファイルを公開

任意の `AUTH_KEY_SECRET` を設定し、公開します。

```shell
cd r2_public_pac
wrangler secret put AUTH_KEY_SECRET
wrangler publish
```

```shell
 ⛅️ wrangler 2.0.7 
-------------------
Your worker has access to the following bindings:
- R2 Buckets:
  - MY_BUCKET: my-bucket
Uploaded r2_public_pac (1.50 sec)
Published r2_public_pac (3.85 sec)
  r2_public_pac.<YOUR_WORKER>.workers.dev
```

`curl` でアクセスしてみると、公開されたことが確認できます。

```
% curl https://r2_public_pac.<YOUR_WORKER>.workers.dev/proxy.pac
function FindProxyForURL(url,host)
{
    // No proxy for private (RFC 1918) IP addresses (intranet sites)
    if (isInNet(dnsResolve(host), "10.0.0.0", "255.0.0.0") ||
        isInNet(dnsResolve(host), "172.16.0.0", "255.240.0.0") ||
        isInNet(dnsResolve(host), "192.168.0.0", "255.255.0.0")) {
         return "DIRECT";
    }
 
    // No proxy for localhost
    if (isInNet(dnsResolve(host), "127.0.0.0", "255.0.0.0")) {
        return "DIRECT";
    }
 
    // Proxy all
    return "HTTPS oli3n9zkz5.proxy.cloudflare-gateway.com:443";
}
```

# Firefox で Proxy 設定

Network Settings に、公開した `https://r2_public_pac.<YOUR_WORKER>.workers.dev/proxy.pac` を指定します。

![image-20220530124229631](https://cdn.jsdelivr.net/gh/kyouheicf/image@master/uPic/image-20220530124229631.png)

作成しておいた HTTP のブロックポリシーが適用されたことが確認できます。

> HTTPS 通信の TLS 復号化には、Cloudflare ルート証明書のインストールが必要です。
> - [Install the Cloudflare certificate · Cloudflare Zero Trust docs](https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/install-cloudflare-cert/)
    - `curl` や Firefox 等、使うツールによっては OS で管理する証明書を見ない場合があるので、OS にインストールする以外に個別にルート証明書のインストールが必要な場合があります。

![image-20220530124348479](https://cdn.jsdelivr.net/gh/kyouheicf/image@master/uPic/image-20220530124348479.png)

# 参考

- [Online proxy PAC file tester](https://thorsen.pm/proxyforurl)