Azure
kubernetes
vernemq
letsencrypt

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

はじめに

先日書いたKubernetes (Azure AKS)上にVerneMQでMQTT over TLSクラスタを構成するで、「...Let’s Encrypt等のパブリックな認証局からサーバー証明書を発行してもらって利用することもできますが、...」という一言を書いていました。

が、実はLet's Encryptの証明書を用いるには、IdenTrustのルート証明書とLet's Encryptの中間証明書を結合して設定するという、一手間が必要です(2018/05/01時点)。

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

VerneMQクラスタの構成

ユーザー名とパスワードをKubernetes Secretに登録

以前紹介した手順を参考に、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を実行します。

    $ 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
    
  2. TXTレコードに登録すべきvalueを確認します。

    -------------------------------------------------------------------------------
    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.
    -------------------------------------------------------------------------------
    
  3. このDockerコンテナのターミナルはそのままにしておき、別のターミナルを開きます。

DNSサービスへTXTレコードを登録

  1. Dockerコンテナとは別のターミナルで、指定された _acme-challenge.mqtt.nmatsui.work のTXTレコードを作成します(今回はAzure DNSを用いています)。

    $ 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 に対するサーバ証明書が取得されます。

    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は削除しておきます。

    $ 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のルート証明書からチェーンされるクロスルート証明書です。そのため、VerneMQやMQTTクライアントに設定する認証局の証明書としては、IdenTrustのルート証明書(DST Root CA X3)とLet's Encryptの中間証明書(Let's Encrypt Authority X3)の双方が必要となります。

IdenTrustのルート証明書(DST Root CA X3)の取得

IdenTrustの解説によれば、次のような手順が必要のようです。

  • 公式ページに記載されているルート証明を -----BEGIN CERTIFICATE----------END CERTIFICATE----- の間にコピー&ペーストして証明書を作る

面倒ですし、ルート証明書は公開情報なので、DST_Root_CA_X3.pemというファイル名でgithubのリポジトリにpushしてあります。

Let's Encryptの中間証明書(Let's Encrypt Authority X3)と結合

Let's Encryptの中間証明書(Let's Encrypt Authority X3)は、Let's Encryptの公式ページから取得できます。ただしサーバ証明書取得時に同じファイルがダウンロードされていますので、それを用います。

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

サーバ証明書と鍵をコピー

オレオレ証明書を生成した場合と同じyamlファイルを利用できるように、Let's Encryptで取得したサーバ証明書と秘密鍵を/secrets以下にコピーします。

$ 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__CAFILEDOCKER_VERNEMQ_LISTENER__SSL__CERTFILEDOCKER_VERNEMQ_LISTENER__SSL__KEYFILEをそのファイル名に変更してください。

証明書類をKubernetes Secretに登録

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

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

VerneMQクラスタの起動

  1. 以前と同様に、StatefulSetとしてVerneMQを起動します。

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

    $ 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を確認します。

    $ 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
    
  2. このExternal IPに対して、 mqtt.nmatsui.work をA recordとして登録します。

    $ 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します(経路の暗号化は行わない)。

    $ 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で経路を暗号化)。

    $ mosquitto_sub -h mqtt.nmatsui.work -p 8883 --cafile ./secrets/ca.crt -t /foo/bar -d -u outer -P <password of 'outer'>
    

メッセージをpublish

Kubernetes内部

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

    $ 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"
    
  2. Kubernetes内部のsubscriberと検証端末のsubscriberの双方にメッセージがpushされることが確認できます。

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

Internet経由

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

    $ 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"
    
  2. Kubernetes内部のsubscriberと検証端末のsubscriberの双方にメッセージがpushされることが確認できます。

    ...
    Client mosqsub|6-inner-sub-89b received PUBLISH (d0, q0, r0, m0, '/foo/bar', ... (6 bytes))
    Message from outer
    ...
    
    ...
    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を公開する場合にはオレオレ証明書よりも安心感があるのではないでしょうか。