Cloudflare Keyless SSL を使ってみる

# Keyless SSL とは

顧客から外部（Cloudflare）にプライベートキー（秘密鍵）をアップロードすることなく、SSL 終端を行えるサービスです。

- [Keyless SSL | Cloudflare](https://www.cloudflare.com/ssl/keyless-ssl/)

> キーレスSSLは、CloudflareのSSLサービスを使用しながら、顧客がプライベートキーをオンプレミスで保持することができるようになりました。
>
> 標準的なCloudflare SSLサービスでは、顧客は自分のサイトのSSLキーをCloudflareと共有する必要があります。 Cloudflareは顧客のキー情報を保護するために広範囲な技術的措置を講じています。ただし、一部の顧客にはポリシーや技術上の障壁があるため、自分のサイトのSSLキーをCloudflareと共有することができません。このため、「Keyless SSL」を紹介することに興奮しています。
>
> キーレスSSLはエンタープライズプランのお客様のみ利用可能です。エンタープライズプランとキーレスSSLに関する詳細情報については、Cloudflare 営業チームまでお問い合わせください。

- [Protect your key server with Keyless SSL and Cloudflare Tunnel integration](https://blog.cloudflare.com/protect-your-key-server-with-keyless-ssl-and-cloudflare-tunnel-integration/)

> ![[LOGICAL DIAGRAM SHOWING PRIVATE KEYS STORED ON HSMs BEING USED FOR TLS HANDSHAKE]](https://blog.cloudflare.com/content/images/2023/03/image1-36.png)

プライベートキー（秘密鍵）に関して整理すると、以下の表のようになります。

プライベートキー（秘密鍵）を自社で持ちたくない・運用管理したくないという場合は、代わりに Cloudflare が保管・運用管理するモデルが良いでしょう。

逆にプライベートキー（秘密鍵）を外部に出したくない・出せない（業界セキュリティ標準の要請などの）場合には Keyless SSL が良いでしょう。

|                                                              | プライベートキーの生成 | プライベートキーの保管場所                                   | プライベートキーの管理主体<br />（鍵更新などの運用） |
| ------------------------------------------------------------ | ---------------------- | ------------------------------------------------------------ | ---------------------------------------------------- |
| Cloudflare 管理の証明書<br />([Universal](https://developers.cloudflare.com/ssl/edge-certificates/universal-ssl/), [Advanced](https://developers.cloudflare.com/ssl/edge-certificates/advanced-certificate-manager/)) | Cloudflare             | Cloudflare 内で完結                                          | Cloudflare                                           |
| [持ち込み証明書](https://developers.cloudflare.com/ssl/edge-certificates/custom-certificates/)<br />（[カスタムCSR](https://developers.cloudflare.com/ssl/edge-certificates/additional-options/certificate-signing-requests/)利用） | Cloudflare             | Cloudflare 内で完結                                          | Cloudflare                                           |
| [持ち込み証明書](https://developers.cloudflare.com/ssl/edge-certificates/custom-certificates/) | 顧客                   | 顧客側で生成したものを Cloudflare にアップロードし、顧客側でも保管 | 顧客                                                 |
| **[Keyless SSL](https://developers.cloudflare.com/ssl/keyless-ssl/)** | **顧客**               | **顧客側で完結**                                             | **顧客**                                             |

# Keyless SSL 通信フロー詳細

以下のブログで詳細が公開されています。

セッション開始時には以下のような通信となり、Cloudflare からキーサーバーへの通信が発生します。

- [Keyless SSL: The Nitty Gritty Technical Details](https://blog.cloudflare.com/keyless-ssl-the-nitty-gritty-technical-details/)

> ハンドシェイクシークエンス図から得られる教訓の1つは、プライベートキーが各ハンドシェイクで一度しか使用されないことです。これにより、TLSハンドシェイクを地理的に分割し、CloudFlareのエッジでほとんどのハンドシェイクを行いながら、プライベートキー操作をリモートキーサーバーに移動することができます。このキーサーバーは顧客のインフラストラクチャに配置されるため、彼らだけがプライベートキーに独占的アクセス権を持つことができます。
>
> ![cloudflare_keyless_ssl_handshake_rsa](https://blog.cloudflare.com/content/images/2022/10/cloudflare_keyless_ssl_handshake_rsa.jpeg)

続いて、TLS session resumption の仕組みを使ってセッションの状態を再開する場合の通信フローです。

- [TLSセッション再開 (session resumption) のしくみ - Qiita](https://qiita.com/n-i-e/items/bb0e582011a5b1f9c28d)

> 直近のNewSessionTicketでサーバからクライアントに送り出されたSession Ticketを、今度はClientHelloのSession Ticket拡張のパラメータに詰めてクライアントからサーバへと送ります。
> サーバ側ではこれを解読し、有効な内容と認められれば、それを使って「セッション再開」手順によるハンドシェイクを行います。

省略されたハンドシェイク処理によって、キーサーバーへの通信が発生しない形で処理が可能になります。

> ![session_resumption_with_session_ticket](https://blog.cloudflare.com/content/images/2022/10/session_resumption_with_session_ticket.jpeg)

Cloudflare の場合、セッションチケットの有効期限は 18 時間に設定されています。

- [TLS Session Resumption: Full-speed and Secure](https://blog.cloudflare.com/tls-session-resumption-full-speed-and-secure/)

> また、セッションチケットの有効期限ヒントを18時間に設定しました。これはSSLセッションタイムアウトと同じ値です。各サーバーは、過去18時間のチケット復号化のためにチケットキーを保持しています。

以下のコマンドで `TLS session ticket lifetime hint: 64800 (seconds)` となっていることが確認できます。

- [memo/SessionTicketTLS.md at master · tsuyopon/memo · GitHub](https://github.com/tsuyopon/memo/blob/master/TLS/extension/SessionTicketTLS.md)

```shell
% echo Q | openssl s_client -connect www.cloudflare.com:443 -tls1_2 -debug -msg
CONNECTED(00000005)

...

---
New, TLSv1.2, Cipher is ECDHE-ECDSA-CHACHA20-POLY1305
Server public key is 256 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-ECDSA-CHACHA20-POLY1305
    Session-ID: C58FE073B09D73C1D9B1369E8D5902C4B4D88CA0BF686EDFA5B94FCB2D207056
    Session-ID-ctx: 
    Master-Key: FE0C8A5884F1398EB15B1C5578B0373081B6077BD9292E54C7807523617498A0C5970EEF6921A28F729A80CF4DD8A4B4
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    TLS session ticket lifetime hint: 64800 (seconds)
    TLS session ticket:
    0000 - ec d7 3d 0f 69 7d 6d e8-04 69 59 89 61 97 42 28   ..=.i}m..iY.a.B(
    0010 - 8e 2a 11 59 0b 09 93 66-d9 e5 ce 9a 56 59 41 99   .*.Y...f....VYA.
    0020 - 5d 62 2c fc 63 2b 1a 06-f2 f8 04 5e 3a 35 c9 f4   ]b,.c+.....^:5..
    0030 - 21 ef 8e 3f 50 22 ef 44-6f 73 f4 1d e7 ad f3 ee   !..?P".Dos......
    0040 - 40 ae 96 fb d4 3d 31 92-46 15 aa e2 6f 31 b7 5e   @....=1.F...o1.^
    0050 - 64 e6 03 1b f3 d5 68 b6-4d b9 0c 5c 55 ff 41 90   d.....h.M..\U.A.
    0060 - c5 9f 30 3a d9 96 c4 9b-f9 cd f3 a4 24 98 2f 58   ..0:........$./X
    0070 - f8 38 80 25 82 4a 07 44-62 d7 6a 92 8b 80 64 c3   .8.%.J.Db.j...d.
    0080 - 6c be 61 41 52 ae cd ee-06 15 85 80 8a fc 91 bf   l.aAR...........
    0090 - 69 59 0d cf 23 c8 d8 1d-7b 3f 75 be cc e0 97 50   iY..#...{?u....P
    00a0 - 5e 9f ad ea 7d 65 2a 41-cd 3e 49 f6 11 a3 ee ce   ^...}e*A.>I.....
    00b0 - ff e8 96 c6 a4 a6 b3 ac-b8 d9 3c c7 88 aa 1a 5f   ..........<...._

    Start Time: 1682350487
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
    Extended master secret: yes
---

...
```

また、セッションチケットを利用した TLS session resumption の様子も `reconnect` オプションでの複数回接続試行の結果で `Reused ` により確認できます。

```shell
% echo Q |  openssl s_client -connect www.cloudflare.com:443 -reconnect -tls1_2  | grep -ie New -ie Reuse  
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 = www.cloudflare.com
verify return:1
New, TLSv1.2, Cipher is ECDHE-ECDSA-CHACHA20-POLY1305
Reused, TLSv1.2, Cipher is ECDHE-ECDSA-CHACHA20-POLY1305
Reused, TLSv1.2, Cipher is ECDHE-ECDSA-CHACHA20-POLY1305
Reused, TLSv1.2, Cipher is ECDHE-ECDSA-CHACHA20-POLY1305
Reused, TLSv1.2, Cipher is ECDHE-ECDSA-CHACHA20-POLY1305
DONE
Reused, TLSv1.2, Cipher is ECDHE-ECDSA-CHACHA20-POLY1305
```

# キーサーバーを立てる

こちらの実装を使ってキーサーバーを構築します。

- [cloudflare/gokeyless: Go implementation of the keyless protocol](https://github.com/cloudflare/gokeyless)

今回は Cloudflare Tunnel を使ったセットアップをします。

- [Cloudflare Tunnel setup - Keyless SSL · Cloudflare SSL/TLS docs](https://developers.cloudflare.com/ssl/keyless-ssl/configuration/cloudflare-tunnel/)
- [Protect your key server with Keyless SSL and Cloudflare Tunnel integration](https://blog.cloudflare.com/protect-your-key-server-with-keyless-ssl-and-cloudflare-tunnel-integration/)

> ![img](https://blog.cloudflare.com/content/images/2023/03/image4-11.png)

##  `gokeyless` 、`cloudflared` インストール

インストール手順は [https://pkg.cloudflare.com](https://pkg.cloudflare.com/) にあります。

```shell
$ lsb_release -a
No LSB modules are available.
Distributor ID:	Debian
Description:	Debian GNU/Linux 11 (bullseye)
Release:	11
Codename:	bullseye
```

以下のコマンドで `gokeyless` 、`cloudflared` がインストールできます。

```shell
# Add cloudflare gpg key
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null

# Add this repo to your apt repositories
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com bullseye main' | sudo tee /etc/apt/sources.list.d/cloudflared.list

# install gokeyless
git clone https://github.com/cloudflare/gokeyless.git
cd ~/gokeyless/pkg
sudo cp gokeyless.service /etc/systemd/system/
sudo mkdir -p /etc/keyless
sudo cp keyless_cacert.pem /etc/keyless/
cd ~/gokeyless/pkg/debian/
sudo chmod +x *.sh
sudo ./before-install.sh
sudo apt-get update && sudo apt-get install gokeyless
sudo ./after-install.sh 

# Add this repo to your apt repositories
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bullseye main' | sudo tee /etc/apt/sources.list.d/cloudflared.list

# install cloudflared
sudo apt-get update && sudo apt-get install cloudflared
```

## Cloudflare Tunnel 設定

次に以下の手順で Cloudflare Tunnel を設定します。

- [Via the dashboard · Cloudflare Zero Trust docs](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-guide/remote/)

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

以下の `"virtual_network_id"` 、`"network"` パラメータをメモします

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

% curl -sX GET "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/teamnet/routes" \
     -H "X-Auth-Email: $EMAIL" \
     -H "X-Auth-Key: $APIKEY" \
     -H "Content-Type: application/json" | jq '.result[] | select (.tunnel_name == "keyless")'
{
  "network": "10.174.0.7/32",
  "tunnel_id": "9b29c9d0-6d03-4e61-ae43-3b30a0cbc339",
  "comment": "",
  "created_at": "2023-04-24T16:13:08.166254Z",
  "deleted_at": null,
  "virtual_network_id": "604aa865-1642-405c-a745-3d448f69b8f1",
  "tunnel_name": "keyless"
}
```

## 証明書準備

検証に使う証明書を `acme.sh` を使って発行します。

- [acmesh-official/acme.sh: A pure Unix shell script implementing ACME client protocol](https://github.com/acmesh-official/acme.sh)

```shell
sudo apt-get install socat
curl https://get.acme.sh | sh
cd .acme.sh
./acme.sh --issue --dns -d keyless.example.com \
--yes-I-know-dns-manual-mode-enough-go-ahead-please \
--force --keylength ec-384 --server letsencrypt
# _acme-challenge TXT レコードを追加後
./acme.sh --renew -d keyless.example.com \
--yes-I-know-dns-manual-mode-enough-go-ahead-please \
--force --ecc --server letsencrypt
```

以下のディレクトリに証明書と鍵のファイルが配置されます。

```shell
[Mon Apr 24 16:29:00 UTC 2023] Your cert is in: /home/kyouhei/.acme.sh/keyless.example.com_ecc/keyless.example.com.cer
[Mon Apr 24 16:29:00 UTC 2023] Your cert key is in: /home/kyouhei/.acme.sh/keyless.example.com_ecc/keyless.example.com.key
```

鍵ファイルは同じものを `/etc/keyless/keys` に配置します。

```shell
sudo mkdir -p /etc/keyless/keys
sudo cp /home/kyouhei/.acme.sh/keyless.example.com_ecc/keyless.example.com.key /etc/keyless/keys/
```

## 証明書アップロード

先ほど発行した証明書ファイルを https://dash.cloudflare.com/?to=/:account/:zone/ssl-tls/edge-certificates からアップロードします。

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

先ほどの `"virtual_network_id"` 、`"network"` パラメータを使って以下のように指定します。

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

## キーサーバー設定

`gokeyless.yaml` を以下のように設定します。

- https://github.com/cloudflare/gokeyless/blob/master/pkg/gokeyless.yaml

```shell
sudo touch /etc/keyless/gokeyless.yaml
cat << EOS | sudo tee /etc/keyless/gokeyless.yaml
# Set the log level (0 = DEBUG, 5 = FATAL).
loglevel: 0

# Hostname must match the key server hostname that was configured in the
# Cloudflare dashboard during custom certificate upload.
hostname: 10.174.0.7

# Zone ID can be found on the Cloudflare dashboard 'Overview' tab.
zone_id: YOUR_ZONE_ID

# Origin CA API Key can be found on the Cloudflare dashboard under the 'My
# Profile' section.
origin_ca_api_key: YOUR_ORIGIN_CA_API_KEY

# Configure one or more private key directories.
private_key_stores:
- dir: /etc/keyless/keys

# Optionally customize the location of the certificates used for mutual
# authentication with Cloudflare keyless clients.
# auth_cert: /etc/keyless/server.pem
# auth_key: /etc/keyless/server-key.pem
# auth_csr: /etc/keyless/server.csr
# cloudflare_ca_cert: /etc/keyless/keyless_cacert.pem

# Optionally customize the listen ports.
port: 2407
metrics_port: 2406

# Optionally write the PID to a file (note that sysv-based systems will
# ignore this value and always use /var/run/gokeyless.pid).
pid_file:
EOS
```

## サービス起動

以下のコマンドでサービスを起動します。

```shell
sudo service gokeyless restart
```

正常に起動したことを確認します。

```shell
$ sudo service gokeyless status
● gokeyless.service - gokeyless daemon
     Loaded: loaded (/etc/systemd/system/gokeyless.service; disabled; vendor preset: enabled)
     Active: active (running) since Tue 2023-04-25 05:27:23 UTC; 8min ago
   Main PID: 188376 (gokeyless)
      Tasks: 7 (limit: 4691)
     Memory: 18.2M
        CPU: 88ms
     CGroup: /system.slice/gokeyless.service
             └─188376 /usr/local/bin/gokeyless

Apr 25 05:27:23 kyouhei-osaka-keyless systemd[1]: Started gokeyless daemon.
Apr 25 05:27:23 kyouhei-osaka-keyless gokeyless[188376]: 2023/04/25 05:27:23 [INFO] loading /etc/keyless/keys/keyless.example.com.key...
Apr 25 05:27:23 kyouhei-osaka-keyless gokeyless[188376]: 2023/04/25 05:27:23 [DEBUG] add signer with SKI: f39703813ebc2519d214d5731af56b8a95c7fa6b (https://crt.sh/?ski=f39703813ebc2519d214d5731af56>
Apr 25 05:27:23 kyouhei-osaka-keyless gokeyless[188376]: 2023/04/25 05:27:23 [INFO] Serving metrics endpoint at :2406/metrics
Apr 25 05:27:23 kyouhei-osaka-keyless gokeyless[188376]: 2023/04/25 05:27:23 [INFO] Listening at tcp://[::]:2407
```

# Keyless SSL 通信の確認

キーサーバーで以下のコマンドを打ち、ログを見ながら通信を確認します。

- [Troubleshooting Keyless SSL · Cloudflare SSL/TLS docs](https://developers.cloudflare.com/ssl/keyless-ssl/troubleshooting/)

```shell
sudo journalctl -f -u gokeyless
```

以下のように毎回セッションチケットを発行するような通信では、

```shell
% echo Q | openssl s_client -connect keyless.example.com:443 -tls1_2 -debug -msg
...
New, TLSv1.2, Cipher is ECDHE-ECDSA-CHACHA20-POLY1305
...
% echo Q | openssl s_client -connect keyless.example.com:443 -tls1_2 -debug -msg
...
New, TLSv1.2, Cipher is ECDHE-ECDSA-CHACHA20-POLY1305
...
% echo Q | openssl s_client -connect keyless.example.com:443 -tls1_2 -debug -msg
...
New, TLSv1.2, Cipher is ECDHE-ECDSA-CHACHA20-POLY1305
...
```

コマンドを実施するごとに `fetch key with SKI` でキーサーバーへの通信が発生します。

```shell
Apr 25 06:19:20 kyouhei-osaka-keyless gokeyless[188376]: 2023/04/25 06:19:20 [INFO] fetch key with SKI: f39703813ebc2519d214d5731af56b8a95c7fa6b
Apr 25 06:19:21 kyouhei-osaka-keyless gokeyless[188376]: 2023/04/25 06:19:21 [INFO] fetch key with SKI: f39703813ebc2519d214d5731af56b8a95c7fa6b
Apr 25 06:19:22 kyouhei-osaka-keyless gokeyless[188376]: 2023/04/25 06:19:22 [INFO] fetch key with SKI: f39703813ebc2519d214d5731af56b8a95c7fa6b
```

次にセッションチケットを利用してハンドシェイクが省略されているかどうかを確認するために `reconnect` オプションで何度か接続を試みます。

新規接続(New)からセッション再利用(Reuse)の出力を確認できますが、

```shell
% openssl s_client -reconnect -host keyless.example.com -port 443
...
New, TLSv1.2, Cipher is ECDHE-ECDSA-CHACHA20-POLY1305
...
Reused, TLSv1.2, Cipher is ECDHE-ECDSA-CHACHA20-POLY1305
...
Reused, TLSv1.2, Cipher is ECDHE-ECDSA-CHACHA20-POLY1305
...
Reused, TLSv1.2, Cipher is ECDHE-ECDSA-CHACHA20-POLY1305
...
Reused, TLSv1.2, Cipher is ECDHE-ECDSA-CHACHA20-POLY1305
...
Reused, TLSv1.2, Cipher is ECDHE-ECDSA-CHACHA20-POLY1305
...
closed
...
```

キーサーバーのログから確認できた  `fetch key with SKI` は1度だけでした。

これで省略されたハンドシェイク処理によって、キーサーバーへの通信が発生しない形で処理されていることを確認できました。

```
Apr 25 06:18:23 kyouhei-osaka-keyless gokeyless[188376]: 2023/04/25 06:18:23 [INFO] fetch key with SKI: f39703813ebc2519d214d5731af56b8a95c7fa6b
```

# 感想

Cloudflare のようなエッジサービスを使いながらも、プライベートキー（秘密鍵）は外部に出さない運用ができることがわかりました。

キーサーバーもステートレスに増やしていけるため、柔軟に構成が可能です。

- [High availability · Cloudflare SSL/TLS docs](https://developers.cloudflare.com/ssl/keyless-ssl/reference/high-availability/)

Hardware Security Module (HSM) でプライベートキー（秘密鍵）運用をされている場合は、その要件を満たすことができ、設定も容易です。

2023年3月にサポートされた、Keyless SSL と Cloudflare Tunnel の構成を確認でき、よく理解できました。

- [Protect your key server with Keyless SSL and Cloudflare Tunnel integration](https://blog.cloudflare.com/protect-your-key-server-with-keyless-ssl-and-cloudflare-tunnel-integration/)

プライベートキー（秘密鍵）をセキュアに利用させる構成としては、非常に簡単に始められると思います。