[MQTT] ドメイン名と異なるCNが設定されたサーバ証明書で、通信が可能かどうかを検証してみました

[MQTT] ドメイン名と異なるCNが設定されたサーバ証明書で、通信が可能かどうかを検証してみました

2025.08.10

1. はじめに

製造ビジネステクノロジー部の平内(SIN)です。

IoTシステムにおけるMQTT通信では、セキュリティの観点からTLS/SSL証明書を使用した暗号化通信が一般的になっています。しかし、実際の運用においては、証明書に設定されているCommon Name(CN)が、接続先ドメイン名と一致しないケースが発生することがあります。

今回は、このような状況でMQTT通信が可能かどうかを検証してみました。

最初に、結論から申し上げますと、ドメイン名と証明書のCommon Nameが異なる場合でも、クライアント側でホスト名検証を制御することで、通信が可能であることを確認できました。

なお、下記については、充分に考慮が必要であることを、予めご了承ください。

  • ホスト名検証の無効化は、すべてのMQTTライブラリで実装可能ではありません。(今回は、 paho.mqttでのみ確認しています)
  • ホスト名検証の無効化は、セキュリティリスクになることをご承知おきください

申し訳ございませんが、本記事は、あくまでも個人的なサーバ証明書検証・SNI・CNの動作確認が目的であり、本番運用等に利用可能な技術を解説するものではありません。

2. MQTTとTLS認証の基本

MQTTにおけるTLS/SSL通信

MQTT(Message Queuing Telemetry Transport)は、IoTデバイス間の軽量なメッセージング通信プロトコルです。デフォルトでは平文での通信となるため、セキュリティを確保するためにTLS/SSL暗号化が不可欠です。

MQTTでのTLS通信では、以下の要素が重要になります。

  • サーバー証明書: MQTTブローカーの身元を証明する
  • CA証明書: サーバー証明書を発行・署名する認証局
  • 証明書チェーンの検証: クライアントがサーバーの正当性を確認する仕組み

サーバー証明書の検証プロセス

MQTTクライアントがサーバーに接続する際、サーバー証明書の検証は以下の2段階で実施されます。

第1段階:CA署名による認証
クライアントは、サーバーから受信した証明書が信頼できるCA(認証局)によって署名されているかを確認します。この段階では、証明書の署名チェーンを辿り、最終的に信頼されたルートCAまで検証が行われます。

第2段階:CN(Common Name)による検証
証明書の署名が確認された後、証明書に記載されているCommon Name(CN)やSubject Alternative Name(SAN)が、実際の接続先ホスト名と一致するかを確認します。この検証により、中間者攻撃を防ぐことができます。(今回は、SANでの検証は、省略しています)

通常の運用では、この2段階の検証が両方とも成功した場合のみ、安全な通信が確立されます。

Server Name Indication(SNI)とは

SNIは、TLS通信における拡張機能で、クライアントが接続時にどのホスト名に対して通信を行いたいかをサーバーに伝える仕組みです。これにより、単一のIPアドレス・ポートで複数のドメインに対応した異なる証明書を提供することが可能になります。

Client → Server: ClientHello (SNI: mqtt.example.com)
Server → Client: Certificate for mqtt.example.com
Client → Server: ClientHello (SNI: mqtt.local)
Server → Client: Certificate for mqtt.local

従来のTLS通信では、一つのIPアドレスに対して一つの証明書しか提供できませんでしたが、SNIを使用することで、同一のサーバーで複数のドメイン名に対応した証明書を動的に選択・提供できるようになります。

3. 検証環境の構築

検証パターンの概要

今回の検証では、以下の2つのパターンを用意しました。

検証1: 単一CA環境での直接MQTT接続

  • Eclipse Mosquittoを使用したシンプルなTLS対応MQTTブローカー
  • 単一のCA(CA1)が発行する証明書(CN=mqtt.local)
  • クライアントは直接MQTTブローカーに接続

検証2: 複数CA環境でのSNIプロキシ接続

  • Nginxストリームプロキシを使用したTLS終端
  • 2つのCA(CA1、CA2)による異なる証明書
  • SNIベースの証明書選択機能

共通の前提条件

検証を行うための環境設定として、以下を準備しました。

# ホスト名解決の設定
echo "127.0.0.1 mqtt.example.com" | sudo tee -a /etc/hosts

# 必要なソフトウェア
- Docker & Docker Compose
- Python 3 & paho-mqtt ライブラリ
- OpenSSL(証明書操作用)

Python MQTTクライアントの設定

両方の検証で使用するPython MQTTクライアントは、以下の特徴を持っています。

import paho.mqtt.client as mqtt
import ssl

# SSL設定のカスタマイズ
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = CHECK_HOSTNAME  # ホスト名検証の制御
ssl_context.verify_mode = ssl.CERT_REQUIRED  # 証明書検証は有効
ssl_context.load_verify_locations(cafile=CA_FILE)  # CA証明書の指定

この設定により、証明書検証とホスト名検証を独立して制御することが可能になります。

4. 検証1: 単一CA環境での動作確認

アーキテクチャ構成

検証1では、最もシンプルな構成でMQTT通信を行います。

001

証明書の生成

まず、CA証明書とサーバー証明書を生成します。

cd verification_1
chmod +x ./generate_ca_and_cert.sh
./generate_ca_and_cert.sh

これにより、以下の証明書ファイルが生成されます。

certs/ca1/
├── ca.crt          # CA1の証明書
├── ca.key          # CA1の秘密鍵
├── server.crt      # サーバー証明書(CN=mqtt.local)
└── server.key      # サーバーの秘密鍵

MQTTブローカーの起動

Dockerを使用してMQTTブローカーを起動します。

docker build -t mqtt-server .
docker run --rm --name mqtt-server -p 8883:8883 mqtt-server

Mosquittoの設定ファイル(mosquitto.conf)では、以下のTLS設定を行っています。

port 8883
cafile /mosquitto/config/ca.crt
certfile /mosquitto/config/server.crt
keyfile /mosquitto/config/server.key
tls_version tlsv1.2

通信テストの実行

2つのターミナルを使用して、Publisher/Subscriberの動作を確認します。

# ターミナル1: Subscriber
python3 subscribe.py
# 出力: Connecting to mqtt.example.com:8883
#       Connected successfully
#       Subscribed to test/topic

# ターミナル2: Publisher
python3 publish.py
# 出力: Connected successfully
#       Message published (mid: 1)

実装上重要になるのは、ホスト名検証の無効化です。

# ホスト名検証を無効化する場合は、Falseである必要がある
CHECK_HOSTNAME = False
ssl_context.check_hostname = CHECK_HOSTNAME  

テストに使用したコードは、以下です。

  • publish.py

https://github.com/furuya02/work-server-authentication-sample/blob/main/verification_1/publish.py

  • subscribe.py

https://github.com/furuya02/work-server-authentication-sample/blob/main/verification_1/subscribe.py

接続先が、 mqtt.example.com:8883となっているため、SNIは、 mqtt.example.comが入っていますが、mosquittoは、SNIに対応しておらず、設定されている、mqtt.localの証明書を返しています。

003

004

検証結果の分析

この検証により、以下のことが分かります。

  1. ホスト名不一致でも通信可能: 接続先が「mqtt.example.com」で、証明書のCNが「mqtt.local」
  2. クライアント設定の重要性: CHECK_HOSTNAME = False の設定により、ホスト名検証を無効化される
段階 処理内容 可否 備考
第1 署名検証 ✅️ mqtt.localは、CA1で署名されているのでOK
第2 ホスト名検証 ✅️ 無効化されているため

5. 検証2: 複数CA環境でのSNI証明書選択

アーキテクチャ構成

検証2では、SNIに指定された証明書が返される環境です。Mosquittoは複数のサーバ証明書を配置することができない(SNI未対応)ため、手前にNginxを置いて、TLSを終端しました。Mosquittoは、TLSなし(TCP:1883)で接続されます。

002

複数CA証明書の生成

2つの異なるCAと、それぞれが発行するサーバー証明書を生成します。

cd verification_2
chmod +x ./generate_ca_and_cert.sh
./generate_ca_and_cert.sh

生成される証明書構造は以下の通りです。

certs/
├── ca1/
│   ├── ca.crt          # CA1の証明書
│   ├── server.crt      # CN=mqtt.local
│   └── server.key
└── ca2/
    ├── ca.crt          # CA2の証明書
    ├── server.crt      # CN=mqtt.example.com
    └── server.key

Nginxストリームプロキシの設定

nginx.confでは、SNIベースの証明書選択を以下のように設定しています。

stream {
    upstream mqtt_backend {
        server mosquitto:1883;
    }

    # mqtt.local用のサーバーブロック
    server {
        listen 8883 ssl;
        server_name mqtt.local;

        ssl_certificate /etc/nginx/ssl/ca1/server.crt;
        ssl_certificate_key /etc/nginx/ssl/ca1/server.key;

        proxy_pass mqtt_backend;
    }

    # mqtt.example.com用のサーバーブロック
    server {
        listen 8883 ssl;
        server_name mqtt.example.com;

        ssl_certificate /etc/nginx/ssl/ca2/server.crt;
        ssl_certificate_key /etc/nginx/ssl/ca2/server.key;

        proxy_pass mqtt_backend;
    }
}

サービスの起動と通信テスト

Docker Composeを使用して複数のサービスを起動します。

docker compose up -d

% docker compose up -d
[+] Running 0/1
 ⠋ Network verification_2_mqtt-network  Creating   0.1s
[+] Running 3/3d orphan containers ([haproxy-sni]) for this project. If you removed or renamed this service in your comp ✔ Network verification_2_mqtt-network  Created  0.1s
 ✔ Container mosquitto                  Started   0.2s
 ✔ Container nginx-sni                  Started   0.4s

この構成により、以下のサービスが起動します。

  • Nginx プロキシ(ポート8883、TLS終端)
  • Eclipse Mosquitto(ポート1883、平文TCP)

通信テストでは、検証1と同様の手順でPublisher/Subscriberの動作を確認できます。

SNI証明書選択の動作確認

OpenSSLコマンドを使用して、SNIで指定した証明書が正しく返却されることを確認します。

# mqtt.example.com用の証明書取得(CA2証明書が返される)
openssl s_client -connect mqtt.example.com:8883 -servername mqtt.example.com < /dev/null 2>/dev/null | openssl x509 -text
# Subject: C=JP, ST=Tokyo, L=Tokyo, O=Test, OU=MQTT, CN=mqtt.example.com

# mqtt.local用の証明書取得(CA1証明書が返される)
openssl s_client -connect mqtt.example.com:8883 -servername mqtt.local < /dev/null 2>/dev/null | openssl x509 -text  
# Subject: C=JP, ST=Tokyo, L=Tokyo, O=Test, OU=MQTT, CN=mqtt.local

証明書チェーンの検証

取得した証明書が正しいCAによって署名されていることを確認します。

# SNIでmqtt.localの証明書をダウンロード
echo | openssl s_client -connect mqtt.example.com:8883 -servername mqtt.local 2>/dev/null | openssl x509 -out tmp.crt

# CA1による署名検証(成功する)
openssl verify -CAfile certs/ca1/ca.crt tmp.crt
# tmp.crt: OK

# CA2による署名検証(失敗する)
openssl verify -CAfile certs/ca2/ca.crt tmp.crt
# error 20 at 0 depth lookup: unable to get local issuer certificate

通信テストの実行

2つのターミナルを使用して、Publisher/Subscriberの動作を確認します。

# ターミナル1: Subscriber
python3 subscribe.py
# 出力: Connecting to mqtt.example.com:8883
#       Connected successfully
#       Subscribed to test/topic

# ターミナル2: Publisher
python3 publish.py
# 出力: Connected successfully
#       Message published (mid: 1)

実装上重要になるのは、SNIの設定です。ホスト名検証は有効でも問題ありません。

# ホスト名検証を無効化する場合は、Falseである必要がある
CHECK_HOSTNAME = True
ssl_context.check_hostname = CHECK_HOSTNAME  

SNI_HOSTNAME = "mqtt.local"
# SSLソケットをカスタマイズ
original_wrap_socket = ssl_context.wrap_socket
ssl_context.wrap_socket = lambda sock, **kwargs: original_wrap_socket(
    sock, server_hostname=SNI_HOSTNAME  # SNIホスト名を指定
)

テストに使用したコードは、以下です。

  • publish.py

https://github.com/furuya02/work-server-authentication-sample/blob/main/verification_2/publish.py

  • subscribe.py

https://github.com/furuya02/work-server-authentication-sample/blob/main/verification_2/subscribe.py

接続先が mqtt.example.com:8883 となっていますが、SNIには指定された mqtt.local が入っており、Nginxは指定された mqtt.local の証明書を返しています。

005

006

検証結果の分析

この検証により、以下のことが分かります。

  1. SNIベースの証明書選択: 接続先ホスト名に関係なく、指定した証明書を選択可能
  2. ホスト名検証: SNIで指定したホスト名との一致を検証する(paho.mqtt)
段階 処理内容 可否 備考
第1 署名検証 ✅️ mqtt.localは、CA1で署名されているのでOK
第2 ホスト名検証 ✅️ SNIと一致しているためOK

なお、SNIを指定しないと、接続先ドメイン名が優先され、CA2が署名した証明書が返されるため、以下のように第1段階の署名検証でNGとなります。

段階 処理内容 可否 備考
第1 署名検証 ❌️ mqtt.example.comは、CA2で署名されているのでNG
第2 ホスト名検証 第1段階でNGのため、評価されず

6. 最後に

今回の検証を通じて、実装によっては接続先ドメイン名とサーバ証明書のCNの一致は必須ではないことが分かりました。また、NginxプロキシによるTLSの終端と、SNIによる証明書の選択の実装についても確認できました。

最初に記載した通り、「ホスト名検証の無効化」はセキュリティ上のリスクが伴うため、適用するネットワークには注意が必要ですが、様々な要求に答える一つの知識として理解しておいても損はないと思いました。

検証に使用したコードは下記に置きました。
https://github.com/furuya02/work-server-authentication-sample

特に検証の詳しい手順は下記をご参照ください。
https://github.com/furuya02/work-server-authentication-sample/blob/main/Verification Procedure.md

7. 参考リンク

この記事をシェアする

facebookのロゴhatenaのロゴtwitterのロゴ

© Classmethod, Inc. All rights reserved.