Kubernetes上のVerneMQクラスタでLet's Encryptの証明書を利用する

# はじめに
先日書いた[Kubernetes （Azure AKS）上にVerneMQでMQTT over TLSクラスタを構成する](https://qiita.com/nmatsui/items/0a46a2d2acd9ddfccaa4)で、「...Let’s Encrypt等のパブリックな認証局からサーバー証明書を発行してもらって利用することもできますが、...」という一言を書いていました。

が、実はLet's Encryptの証明書を用いるには、[IdenTrust](https://www.identrust.com/index.html)のルート証明書と[Let's Encrypt](https://letsencrypt.jp/)の中間証明書を結合して設定するという、一手間が必要です（2018/05/01時点）。

そこで今回は、Kubernetes上に構成したVerneMQクラスタへLet's Encryptのサーバ証明書を設定してみようと思います。

# VerneMQクラスタの構成
## ユーザー名とパスワードをKubernetes Secretに登録
[以前紹介した手順](https://qiita.com/nmatsui/items/0a46a2d2acd9ddfccaa4#%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E5%90%8D%E3%81%A8%E3%83%91%E3%82%B9%E3%83%AF%E3%83%BC%E3%83%89%E3%82%92kubernetes-secret%E3%81%AB%E7%99%BB%E9%8C%B2)を参考に、KubernetesのsecretsにVerneMQのパスワードを登録してください。

## Let's Encryptからサーバ証明書を取得
次にLet's Encryptから、DNS-01 challengeを用いて `mqtt.nmatsui.work` というFQDNのサーバ証明書を取得します。（ドメインレジストラへのドメイン登録やDNSサービスへのnameserver変更は、事前に実施しておいてください。）

### Dockerコンテナ経由で `certobot` コマンドを起動
1. certbotの公式Docker Imageを用いて、DNS-01 challengeを実行します。

    ```bash
    $ mkdir -p secrets
    $ docker run -it -v $(PWD)/secrets:/etc/letsencrypt certbot/certbot certonly --manual --domain mqtt.nmatsui.work --email nobuyuki.matsui@gmail.com --agree-tos --manual-public-ip-logging-ok --preferred-challenges dns
    ```
1. TXTレコードに登録すべきvalueを確認します。

    ```text
    -------------------------------------------------------------------------------
    Please deploy a DNS TXT record under the name
    _acme-challenge.mqtt.nmatsui.work with the following value:

    XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

    Before continuing, verify the record is deployed.
    -------------------------------------------------------------------------------
    ```
1. このDockerコンテナのターミナルはそのままにしておき、別のターミナルを開きます。

### DNSサービスへTXTレコードを登録
1. Dockerコンテナとは別のターミナルで、指定された `_acme-challenge.mqtt.nmatsui.work` のTXTレコードを作成します（今回はAzure DNSを用いています）。

    ```bash
    $ az network dns record-set txt add-record --resource-group nmatsui_dns --zone-name nmatsui.work --record-set-name "_acme-challenge.mqtt" --value "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
    ```

### `certbot`コマンドを継続してサーバ証明書を取得
1. TXTレコードを追加したら、Dockerコンテナを実行しているターミナルでEnterキーを押し、`certbot`に処理を継続させます。指定されたTXTレコードが正しく登録されていれば、 `./secrets`以下に `mqtt.nmatsui.work` に対するサーバ証明書が取得されます。

    ```text
    Press Enter to Continue
    Waiting for verification...
    Cleaning up challenges

    IMPORTANT NOTES:
     - Congratulations! Your certificate and chain have been saved at:
       /etc/letsencrypt/live/mqtt.nmatsui.work/fullchain.pem
       Your key file has been saved at:
       /etc/letsencrypt/live/mqtt.nmatsui.work/privkey.pem
    ...
    ```

### TXTレコードを削除
1. サーバ証明書が取得できたので、登録したTXT recordは削除しておきます。

    ```bash
    $ az network dns record-set txt remove-record --resource-group nmatsui_dns --zone-name nmatsui.work --record-set-name "_acme-challenge.mqtt" --value "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
    ```

## ルート証明書と中間証明書を統合
Let's Encryptが発行するサーバ証明書は、[IdenTrust](https://www.identrust.com/index.html)のルート証明書からチェーンされるクロスルート証明書です。そのため、VerneMQやMQTTクライアントに設定する認証局の証明書としては、IdenTrustのルート証明書（[DST Root CA X3](https://www.identrust.com/certificates/trustid/root-download-x3.html)）とLet's Encryptの中間証明書（[Let's Encrypt Authority X3](https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem.txt)）の双方が必要となります。

### IdenTrustのルート証明書（DST Root CA X3）の取得
IdenTrustの[解説](https://www.identrust.com/certificates/trustid/install-nes36.html)によれば、次のような手順が必要のようです。

* [公式ページ](https://www.identrust.com/certificates/trustid/root-download-x3.html)に記載されているルート証明を `-----BEGIN CERTIFICATE-----` と `-----END CERTIFICATE-----` の間にコピー&ペーストして証明書を作る

面倒ですし、ルート証明書は公開情報なので、DST_Root_CA_X3.pemというファイル名で[githubのリポジトリ](https://github.com/nmatsui/kubernetes-vernemq/blob/master/secrets/DST_Root_CA_X3.pem)にpushしてあります。

### Let's Encryptの中間証明書（Let's Encrypt Authority X3）と結合
Let's Encryptの中間証明書（Let's Encrypt Authority X3）は、Let's Encryptの[公式ページ](https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem.txt)から取得できます。ただしサーバ証明書取得時に同じファイルがダウンロードされていますので、それを用います。

```bash
$ cat secrets/DST_Root_CA_X3.pem secrets/archive/mqtt.nmatsui.work/chain1.pem > secrets/ca.crt
```

### サーバ証明書と鍵をコピー
[オレオレ証明書を生成した場合](https://qiita.com/nmatsui/items/0a46a2d2acd9ddfccaa4#%E3%82%AA%E3%83%AC%E3%82%AA%E3%83%AC%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E8%A8%BC%E6%98%8E%E6%9B%B8%E3%82%92kubernetes-secret%E3%81%AB%E7%99%BB%E9%8C%B2)と同じyamlファイルを利用できるように、Let's Encryptで取得したサーバ証明書と秘密鍵を/secrets以下にコピーします。

```bash
$ cp secrets/archive/mqtt.nmatsui.work/fullchain1.pem secrets/server.crt
$ cp secrets/archive/mqtt.nmatsui.work/privkey1.pem secrets/server.key
```

* 上記以外のファイル名を用いる場合は、vernemq-cluster.yamlの`DOCKER_VERNEMQ_LISTENER__SSL__CAFILE`や`DOCKER_VERNEMQ_LISTENER__SSL__CERTFILE`、`DOCKER_VERNEMQ_LISTENER__SSL__KEYFILE`をそのファイル名に変更してください。

## 証明書類をKubernetes Secretに登録
結合したルート証明書、及びサーバー証明書とその秘密鍵をKubernetes Secretに登録します。

```bash
$ kubectl create secret generic vernemq-certifications --from-file=./secrets/ca.crt --from-file=./secrets/server.crt --from-file=./secrets/server.key
```

### VerneMQクラスタの起動
1. [以前と同様](https://qiita.com/nmatsui/items/0a46a2d2acd9ddfccaa4#vernemq%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%BF%E3%81%AE%E8%B5%B7%E5%8B%95)に、StatefulSetとしてVerneMQを起動します。

    ```bash
    $ kubectl apply -f vernemq-cluster.yaml
    ```
1. VerneMQがクラスタを組んでいることを確認します。

    ```bash
    $ kubectl exec vernemq-0 -- vmq-admin cluster show
    +---------------------------------------------------+-------+
    |                       Node                        |Running|
    +---------------------------------------------------+-------+
    |VerneMQ@vernemq-0.vernemq.default.svc.cluster.local| true  |
    |VerneMQ@vernemq-1.vernemq.default.svc.cluster.local| true  |
    |VerneMQ@vernemq-2.vernemq.default.svc.cluster.local| true  |
    +---------------------------------------------------+-------+
    ```

### VerneMQクラスタのExternalIPをDNSサービスに登録
1. 起動したVerneMQのMQTTSサービスのExternalIPを確認します。

    ```bash
    $ kubectl get services -l app=mqtts
    NAME      TYPE           CLUSTER-IP    EXTERNAL-IP      PORT(S)          AGE
    mqtts     LoadBalancer   10.0.124.40   WW.XXX.YYY.ZZZ   8883:30934/TCP   12m
    ```
1. このExternal IPに対して、 `mqtt.nmatsui.work` をA recordとして登録します。

    ```bash
    $ az network dns record-set a add-record --resource-group nmatsui_dns --zone-name nmatsui.work --record-set-name "mqtt" --ipv4-address "WW.XXX.YYY.ZZZ"
    ```

# 接続確認
ここまでで、Let's Encryptから取得したサーバ証明書を用いて、VerneMQクラスタが起動できました。早速接続確認してみましょう。

## subscriberを起動
### Kubernetes内部
1. Kubernetes内部に検証用のpodを起動し、VerneMQクラスタをsubscribeします（経路の暗号化は行わない）。

    ```text
    $ kubectl run inner-sub --rm -it --image efrecon/mqtt-client /bin/ash
    # mosquitto_sub -h mqtt -p 1883 -t /foo/bar -d -u inner -P <password of 'inner'>
    ```

### Internet経由
1. 検証用端末からInternetを経由して、VerneMQクラスタをsubscribeします（TLSで経路を暗号化）。

    ```text
    $ mosquitto_sub -h mqtt.nmatsui.work -p 8883 --cafile ./secrets/ca.crt -t /foo/bar -d -u outer -P <password of 'outer'>
    ```
    * ルート証明書として、[ルート証明書と中間証明書を統合](https://qiita.com/nmatsui/items/8593e2bc70c1d7c62210#%E3%83%AB%E3%83%BC%E3%83%88%E8%A8%BC%E6%98%8E%E6%9B%B8%E3%81%A8%E4%B8%AD%E9%96%93%E8%A8%BC%E6%98%8E%E6%9B%B8%E3%82%92%E7%B5%B1%E5%90%88)で結合したファイルを指定します。

## メッセージをpublish
### Kubernetes内部
1. Kubernetes内部に新たに起動した検証用PODから、VerneMQクラスタへメッセージをpublishします。

    ```text
    $ kubectl run inner-pub --rm -it --image efrecon/mqtt-client /bin/ash
    # mosquitto_pub -h mqtt -p 1883 -t /foo/bar -d -u inner -P <password of 'inner'> -m "Message from inner"
    ```
1. Kubernetes内部のsubscriberと検証端末のsubscriberの双方にメッセージがpushされることが確認できます。

    ```text
    ...
    Client mosqsub|6-inner-sub-89b received PUBLISH (d0, q0, r0, m0, '/foo/bar', ... (6 bytes))
    Message from inner
    ...
    ```
    ```text
    ...
    Client mosqsub|35744-Nobuyukin received PUBLISH (d0, q0, r0, m0, '/foo/bar', ... (6 bytes))
    Message from inner
    ...
    ```

### Internet経由
1. 検証用端末からInternetを経由して、VerneMQクラスタへメッセージをpublishします。

    ```text
    $ mosquitto_pub -h mqtt.nmatsui.work -p 8883 --cafile ./secrets/ca.crt -t /foo/bar -d -u outer -P <password of 'outer'> -m "Message from outer"
    ```
    * subscribe時と同様、ルート証明書として[ルート証明書と中間証明書を統合](https://qiita.com/nmatsui/items/8593e2bc70c1d7c62210#%E3%83%AB%E3%83%BC%E3%83%88%E8%A8%BC%E6%98%8E%E6%9B%B8%E3%81%A8%E4%B8%AD%E9%96%93%E8%A8%BC%E6%98%8E%E6%9B%B8%E3%82%92%E7%B5%B1%E5%90%88)で結合したファイルを指定します。
1. Kubernetes内部のsubscriberと検証端末のsubscriberの双方にメッセージがpushされることが確認できます。

    ```text
    ...
    Client mosqsub|6-inner-sub-89b received PUBLISH (d0, q0, r0, m0, '/foo/bar', ... (6 bytes))
    Message from outer
    ...
    ```
    ```text
    ...
    Client mosqsub|35744-Nobuyukin received PUBLISH (d0, q0, r0, m0, '/foo/bar', ... (6 bytes))
    Message from outer
    ...
    ```

# さいごに
ということで、Let's Encryptのサーバ証明書を用いて経路をTLS暗号化するVerneMQクラスタをKubernetes上に起動してみました。ブラウザやcurlコマンドとは異なり、MQTTクライアントはwell knownなルート証明書を内蔵していないため、クライアント側の手間は減らないのが微妙ではあります。が、ドメインの所有者であることは確認できるため、Internet上にMQTT Brokerを公開する場合にはオレオレ証明書よりも安心感があるのではないでしょうか。