Cloudflare API Shield の mTLS を確認する

# Cloudflare API Shield とは

こちらのブログが参考になります。

> [Introducing API Shield](https://blog.cloudflare.com/introducing-api-shield/)

公式ドキュメントはこちらです。全プランで使える機能になります。

> [API Shield™ · Cloudflare Firewall Rules docs](https://developers.cloudflare.com/firewall/cf-firewall-rules/api-shield#mutual-tls-mtls)

# API Shield (mTLS) 有効化前

TLS ハンドシェイクの詳細です。

```shell
HOSTNAME=mtls.example.com
CFIP=$(dig +short @1.1.1.1 $HOSTNAME | head -n 1)

% openssl s_client -verify 5 -connect $CFIP:443 -servername $HOSTNAME -quiet -state
verify depth is 5
SSL_connect:before SSL initialization
SSL_connect:SSLv3/TLS write client hello
SSL_connect:SSLv3/TLS write client hello
SSL_connect:SSLv3/TLS read server hello
SSL_connect:TLSv1.3 read encrypted extensions
depth=2 C = IE, O = Baltimore, OU = CyberTrust, CN = Baltimore CyberTrust Root
verify return:1
depth=1 C = US, O = "Cloudflare, Inc.", CN = Cloudflare Inc ECC CA-3
verify return:1
depth=0 C = US, ST = California, L = San Francisco, O = "Cloudflare, Inc.", CN = example.com
verify return:1
SSL_connect:SSLv3/TLS read server certificate
SSL_connect:TLSv1.3 read server certificate verify
SSL_connect:SSLv3/TLS read finished
SSL_connect:SSLv3/TLS write change cipher spec
SSL_connect:SSLv3/TLS write finished
SSL_connect:SSL negotiation finished successfully
SSL_connect:SSL negotiation finished successfully
SSL_connect:SSLv3/TLS read server session ticket
SSL_connect:SSL negotiation finished successfully
SSL_connect:SSL negotiation finished successfully
SSL_connect:SSLv3/TLS read server session ticket
SSL3 alert read:warning:close notify
SSL3 alert write:warning:close notify
```

# API Shield (mTLS) 有効化後

以下のダッシュボードでホスト名を追加すると、

> [Enable mTLS · Cloudflare SSL docs](https://developers.cloudflare.com/ssl/client-certificates/enable-mtls)

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

以下のようにクライアント証明書を要求するように TLS ハンドシェイクが変わります。

```shell
% openssl s_client -verify 5 -connect $CFIP:443 -servername $HOSTNAME -quiet -state
verify depth is 5
SSL_connect:before SSL initialization
SSL_connect:SSLv3/TLS write client hello
SSL_connect:SSLv3/TLS write client hello
SSL_connect:SSLv3/TLS read server hello
SSL_connect:TLSv1.3 read encrypted extensions
SSL_connect:SSLv3/TLS read server certificate request   <-- クライアント証明書を要求
depth=2 C = IE, O = Baltimore, OU = CyberTrust, CN = Baltimore CyberTrust Root
verify return:1
depth=1 C = US, O = "Cloudflare, Inc.", CN = Cloudflare Inc ECC CA-3
verify return:1
depth=0 C = US, ST = California, L = San Francisco, O = "Cloudflare, Inc.", CN = example.com
verify return:1
SSL_connect:SSLv3/TLS read server certificate
SSL_connect:TLSv1.3 read server certificate verify
SSL_connect:SSLv3/TLS read finished
SSL_connect:SSLv3/TLS write change cipher spec
SSL_connect:SSLv3/TLS write client certificate
SSL_connect:SSLv3/TLS write finished
SSL_connect:SSL negotiation finished successfully
SSL_connect:SSL negotiation finished successfully
SSL_connect:SSLv3/TLS read server session ticket
SSL_connect:SSL negotiation finished successfully
SSL_connect:SSL negotiation finished successfully
SSL_connect:SSLv3/TLS read server session ticket
SSL3 alert read:warning:close notify
SSL3 alert write:warning:close notify
```

# ファイアウォールルールの作成

こちらの手順に従って、作成できます。

> [Create a mTLS rule · Cloudflare Firewall Rules docs](https://developers.cloudflare.com/firewall/cf-dashboard/create-mtls-rule)

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

# クライアント証明書なしでcurlでアクセス

以下のようなエラーになります。

```shell
% curl -svo /dev/null  https://mtls.example.com
*   Trying 104.18.14.139...
* TCP_NODELAY set
* Connected to mtls.example.com (104.18.14.139) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/cert.pem
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
} [234 bytes data]
* TLSv1.2 (IN), TLS handshake, Server hello (2):
{ [100 bytes data]
* TLSv1.2 (IN), TLS handshake, Certificate (11):
{ [2327 bytes data]
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
{ [115 bytes data]
* TLSv1.2 (IN), TLS handshake, Request CERT (13):
{ [374 bytes data]
* TLSv1.2 (IN), TLS handshake, Server finished (14):
{ [4 bytes data]
* TLSv1.2 (OUT), TLS handshake, Certificate (11):
} [7 bytes data]
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
} [37 bytes data]
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
} [1 bytes data]
* TLSv1.2 (OUT), TLS handshake, Finished (20):
} [16 bytes data]
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
{ [1 bytes data]
* TLSv1.2 (IN), TLS handshake, Finished (20):
{ [16 bytes data]
* SSL connection using TLSv1.2 / ECDHE-ECDSA-AES128-GCM-SHA256
* ALPN, server accepted to use h2
* Server certificate:
*  subject: C=US; ST=California; L=San Francisco; O=Cloudflare, Inc.; CN=example.com
*  start date: Aug  4 00:00:00 2021 GMT
*  expire date: Aug  3 23:59:59 2022 GMT
*  subjectAltName: host "mtls.example.com" matched cert's "*.example.com"
*  issuer: C=US; O=Cloudflare, Inc.; CN=Cloudflare Inc ECC CA-3
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* 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 0x7fa3ff80d600)
> GET / HTTP/2
> Host: mtls.example.com
> User-Agent: curl/7.64.1
> Accept: */*
> 
* Connection state changed (MAX_CONCURRENT_STREAMS == 256)!
< HTTP/2 403 
< date: Wed, 13 Oct 2021 17:15:12 GMT
< content-type: text/plain; charset=UTF-8
< content-length: 16
< x-frame-options: SAMEORIGIN
< referrer-policy: same-origin
< cache-control: private, max-age=0, no-store, no-cache, must-revalidate, post-check=0, pre-check=0
< expires: Thu, 01 Jan 1970 00:00:01 GMT
< expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
< set-cookie: __cf_bm=OQ5zBTND0KtNG6simpoFUFbb52zxMhyll5Dnj2OHmBA-1634145312-0-AUtQQfUj4/luQhwAaUZEbEz/u4V0zMvsXqNYWqbIHf+DiLyRJVU5ya9kCpyWRzWkyRVmfJl+chiylIswSRk4PPQ=; path=/; expires=Wed, 13-Oct-21 17:45:12 GMT; domain=.example.com; HttpOnly; Secure; SameSite=None
< server: cloudflare
< cf-ray: 69da356c6ca00ad0-NRT
< 
{ [16 bytes data]
* Connection #0 to host mtls.example.com left intact
* Closing connection 0
```

# クライアント証明書なしでブラウザでアクセス

以下のようにブロックされます。

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

# クライアント証明書の作成

ダッシュボードからクライアント証明書を作成します。

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

「Cloudflare Managed CA」が発行する証明書となります。

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

発行されたクライアント証明書と秘密鍵をローカルファイル `cf_managed_client.pem`, `cf_managed_client.key` に保存します。

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

# クライアント証明書の内容確認
以下のコマンドで確認できます。

> [Cloudflare の PKI ツール cfssl で証明書を作成する - Qiita](https://qiita.com/khayama/items/a8f7bc16e5554c9ccd51)

```
% cfssl certinfo -cert cf_managed_client.pem 
{
  "subject": {
    "common_name": "Cloudflare",
    "country": "US",
    "names": [
      "US",
      "Cloudflare"
    ]
  },
  "issuer": {
    "common_name": "Managed CA b5341236aea624bd7e2a0770ee3fb5e5",
    "country": "US",
    "organization": "Cloudflare, Inc.",
    "organizational_unit": "www.cloudflare.com",
    "locality": "San Francisco",
    "province": "California",
    "names": [
      "US",
      "California",
      "San Francisco",
      "Cloudflare, Inc.",
      "www.cloudflare.com",
      "Managed CA b5341236aea624bd7e2a0770ee3fb5e5"
    ]
  },
  "serial_number": "593413451556153623200318414325279173937034532728",
  "not_before": "2021-10-13T06:05:00Z",
  "not_after": "2031-10-11T06:05:00Z",
  "sigalg": "SHA256WithRSA",
  "authority_key_id": "B1:D9:A6:61:71:04:AB:A9:1C:1B:88:51:CC:20:1F:01:B9:88:98:50",
  "subject_key_id": "D7:6F:64:16:92:F6:29:29:14:F3:FB:91:3F:BF:7A:C3:FB:20:90:78",
  "pem": "-----BEGIN CERTIFICATE-----\nxxx\n-----END CERTIFICATE-----\n"
}
```

# クライアント証明書ありでcurlでアクセス

先ほど保存したファイルを使って検証できます。

> [Troubleshooting · Cloudflare SSL docs](https://developers.cloudflare.com/ssl/client-certificates/troubleshooting)

```shell
% curl -svo /dev/null --cert cf_managed_client.pem --key cf_managed_client.key https://mtls.example.com
*   Trying 104.18.14.139...
* TCP_NODELAY set
* Connected to mtls.example.com (104.18.14.139) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/cert.pem
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
} [234 bytes data]
* TLSv1.2 (IN), TLS handshake, Server hello (2):
{ [100 bytes data]
* TLSv1.2 (IN), TLS handshake, Certificate (11):
{ [2327 bytes data]
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
{ [116 bytes data]
* TLSv1.2 (IN), TLS handshake, Request CERT (13):
{ [374 bytes data]
* TLSv1.2 (IN), TLS handshake, Server finished (14):
{ [4 bytes data]
* TLSv1.2 (OUT), TLS handshake, Certificate (11):
} [1059 bytes data]
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
} [37 bytes data]
* TLSv1.2 (OUT), TLS handshake, CERT verify (15):
} [264 bytes data]
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
} [1 bytes data]
* TLSv1.2 (OUT), TLS handshake, Finished (20):
} [16 bytes data]
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
{ [1 bytes data]
* TLSv1.2 (IN), TLS handshake, Finished (20):
{ [16 bytes data]
* SSL connection using TLSv1.2 / ECDHE-ECDSA-AES128-GCM-SHA256
* ALPN, server accepted to use h2
* Server certificate:
*  subject: C=US; ST=California; L=San Francisco; O=Cloudflare, Inc.; CN=example.com
*  start date: Aug  4 00:00:00 2021 GMT
*  expire date: Aug  3 23:59:59 2022 GMT
*  subjectAltName: host "mtls.example.com" matched cert's "*.example.com"
*  issuer: C=US; O=Cloudflare, Inc.; CN=Cloudflare Inc ECC CA-3
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* 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 0x7f9e3680d600)
> GET / HTTP/2
> Host: mtls.example.com
> User-Agent: curl/7.64.1
> Accept: */*
> 
* Connection state changed (MAX_CONCURRENT_STREAMS == 256)!
< HTTP/2 200 
< date: Wed, 13 Oct 2021 17:26:28 GMT
< content-type: text/html; charset=UTF-8
< x-powered-by: PHP/7.2.24
< x-apache-logname: -
< x-apache-username: -
< cf-cache-status: DYNAMIC
< expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
< set-cookie: __cf_bm=sG5jNcimJrNvRrCI8QCJwN9hX1kvZ_VMEBbBhtM9REg-1634145988-0-AV1Z6zj22fs71zMX16Ej+lPfqo2JmBiWSlAmmTMun2yQXIfhUtfCYCRNybz2oZEKATtxDLTi4RZa8czZztjJaF4=; path=/; expires=Wed, 13-Oct-21 17:56:28 GMT; domain=.example.com; HttpOnly; Secure; SameSite=None
< server: cloudflare
< cf-ray: 69da45ec0cd234f3-NRT
< 
{ [665 bytes data]
* Connection #0 to host mtls.example.com left intact
* Closing connection 0
```



# クライアント証明書ありでブラウザでアクセス

macOS の場合、クライアント証明書をキーチェーンアクセスにインポートすることでブラウザから利用できるようになります。

まず、先ほど保存したクライアント証明書と秘密鍵を `p12` 形式にします。

```shell
openssl pkcs12 -export -nodes \
-in cf_managed_client.pem \
-inkey cf_managed_client.key \
-out cf_managed_client.p12  \
-passout pass:password \
-name "Cloudflare Client Certificate for API Shield"
```

その後、キーチェーンアクセスにインポートします。

```shell
% security -v import cf_managed_client.p12 -k ~/Library/Keychains/login.keychain-db -P "password"
import "cf_managed_client.p12" "-k" "/Users/kyouhei/Library/Keychains/login.keychain-db" "-P" "password"
1 identity imported.

% security -v find-identity ~/Library/Keychains/login.keychain-db
find-identity "/Users/kyouhei/Library/Keychains/login.keychain-db"

Policy: X.509 Basic
  Matching identities
  1) 90CABE912B699A9FC863B609366181022B647568 "Cloudflare" (CSSMERR_TP_NOT_TRUSTED)
     1 identities found

  Valid identities only
     0 valid identities found

# 消したいとき
# security -v delete-identity -c Cloudflare ~/Library/Keychains/login.keychain-db
```

Chrome でアクセスしてみます。

```
open -a "Google Chrome" --args --incognito "https://mtls.example.com"  
```

このポップアップ画面でクライアント証明書を選択すれば、アクセスできます。

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

# 参考：Safari 11.0 より古い場合

こちらにある対応が必要になります。

> [Why is Safari not detecting my Trusted Endpoints certificate?](https://help.duo.com/s/article/3724?language=en_US)

```shell
security set-identity-preference -c Cloudflare -s mtls.example.com
security get-identity-preference -s mtls.example.com
```

# 参考リンク

- [CIS(IBM Cloud Internet Service)でmTLSを構成する(1) - 基本設定編 - Qiita](https://qiita.com/testnin2/items/f83b09c3404810b9bb27#5-3-%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%E8%A8%BC%E6%98%8E%E6%9B%B8%E3%81%82%E3%82%8A%E3%81%A7curl%E3%81%A7%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9)
- [CIS(IBM Cloud Internet Service)でmTLSを構成する(2) - クライアント証明書をブラウザに設定 - Qiita](https://qiita.com/testnin2/items/07b0966485b4622d4140)