news 2026/9/12 14:37:28

MQTT公网安全实战:从1883明文到8883 TLS加密,用Let‘s Encrypt保护设备通信

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MQTT公网安全实战:从1883明文到8883 TLS加密,用Let‘s Encrypt保护设备通信

一篇一篇写到第九篇,MQTT 这条线已经从“能连上”走到了“能在公网稳定跑”。前面几篇我们把 EMQX 服务端搭了起来,也把客户端的连接认证、Topic 设计捋了一遍,但一直留着一个隐患——服务端和客户端之间基本都在用 1883 端口直连,所有流量明文传输。内网里自己调试没问题,可一旦设备要跨公网通信,用户名、密码、Topic 里的业务数据全部裸奔。这篇就把这个洞补上:给自己的域名申请 SSL 证书,把 MQTT 服务迁到 TLS 加密链路上来。适合刚把 MQTT 服务器搭起来、准备接入公网设备或者正在做产品原型的开发者参考。

1. 公网环境下,明文 MQTT 的代价到底是什么

1.1 裸奔的 1883 端口:数据被完整暴露

很多新手觉得“我就传个温度湿度,谁稀罕看”。这个想法在内网成立,在公网完全不成立。公网链路上任何一个环节——交换机镜像、路由器抓包、ISP 的流量审计设备——都能把 1883 端口的数据包还原成可读的 MQTT 报文。用户名密码是明文的,设备上报的数据是明文的,你对设备下发的控制指令也是明文的。

尤其是做物联网的同学,设备端往往是 4G 模块直连服务器,走的是运营商网络,中间经过的节点比家用宽带的家庭路由器多得多。以常见场景为例:用 EC20 或者 A7670C 这类 4G 模块连接阿里云 MQTT 时,如果不做 TLS,模块发出的 AT 指令里的 MQTT CONNECT 报文里就带着 clientId、username、password,几乎等于把服务器登录凭据直接写在传输层。要抓到一个设备的凭据,只需要在链路上某个位置做流量镜像,然后用 Wireshark 过滤 MQTT 协议即可,成本极低。

1.2 TLS 在 MQTT 里保护了什么

MQTT over TLS 解决的核心问题有三个。

第一是机密性。握手之后所有 Application Message 都经过对称加密,链路上看到的只是一堆无法还原的密文。第二是完整性。TLS 的 MAC 校验能发现任何中间人篡改报文的尝试,设备上报的数据不会在中途被改掉。第三是身份认证。客户端校验证书链可以确认“我连的服务器确实是持有这个域名证书的那台服务器”,防止 DNS 劫持或者 IP 欺骗把你导到攻击者的伪服务器上。

这里要说明一点:TLS 加密的是传输层,MQTT 本身的 Topic、Payload 结构仍然是明文格式,只是外面裹了一层加密外壳。所以不要在 Topic 里带敏感信息这件事,和上 TLS 不冲突,两者是不同层面的防护。

1.3 从 1883 到 8883:端口背后的语义变化

MQTT 标准端口有两个:1883 是明文 MQTT,8883 是 MQTT over TLS。这不是随便约定的,IANA 确实把这两个端口都分配给了 MQTT。如果你的服务端同时监听两个端口,相当于同时提供明文和加密两套入口。实践中我强烈建议公网环境只开放 8883,把 1883 关掉或者只绑定在内网接口上。原因很简单:只要 1883 暴露在公网上,攻击者就有机会绕过 TLS 直接拿明文凭据尝试撞库。安全设计的基本原则是减少攻击面,明文的入口能不开就不开。

2. 证书从哪里来:免费证书与自签名证书的真实差异

2.1 Let's Encrypt、云厂商免费证书与自签名证书对比

要给域名做 TLS,首先得有一张证书。证书的作用是把“域名”和“公钥”绑定在一起,并且由一个客户端信任的第三方(CA)来背书。常见的三个选择如下。

Let's Encrypt:免费,有效期 90 天,支持通配符域名,完全自动化续期,所有主流客户端和设备操作系统都信任它的根证书。适合绝大多数个人服务器和中小项目。

云厂商免费证书(阿里云、腾讯云等):免费,通常一年有效期,只签发单域名,不支持通配符(个别产品线有通配符但是付费的),申请需要登录控制台手动操作,续期也是手动点击。适合一次性部署、不想折腾自动化脚本的场景,但维护成本其实更高。

自签名证书:自己生成自己的 CA 并签发证书,完全免费,无有效期限制或自己控制。但是所有客户端都必须手动导入你的自签名 CA 证书,否则会直接拒绝连接。公网环境不推荐,内网测试或者公司内部设备的固件里预置了 CA 的场景可以用。

2.2 我的选择:Let's Encrypt + acme.sh

这个系列从第一篇开始我就强调工具选型要看“自动化程度”和“维护成本”。MQTT 服务搭完不是终点,后面还有设备接入、规则引擎、数据存储一堆事。证书如果每 90 天要手动换一次,那对运维来说是一种持续性的折磨。所以我选了 Let's Encrypt 配合 acme.sh 客户端,申请一次之后自动续期,cron 定时任务会帮你把证书在过期前自动换掉。

为什么不用云厂商的一年期免费证书?核心问题不是有效期长短,而是它不自动续期。一年之后你很可能已经忘了这件事,直到客户端突然全部离线、日志里刷出一片 certificate expired 才会想起来。而 Let's Encrypt 的 90 天有效期设计就是为了强制自动化,反而因为频繁轮换降低了“证书遗忘”的风险。

3. 申请证书之前的准备工作

3.1 域名解析:为什么 IP 直连不行

证书是绑在域名上的,不是绑在 IP 上的。你用 IP 直连 MQTT 服务也能连,但没法从 CA 申请到一张只包含 IP 地址的受信任证书(技术上存在 IP 证书,但免费渠道基本拿不到,而且物联网场景下 IP 会变,维护成本更高)。所以先把域名解析做好,添加一条 A 记录,把 MQTT 服务器所在的公网 IP 指到一个子域名上,比如 mqtt.example.com。

这里有个细节值得提醒:解析记录生效有延迟,DNS 的 TTL 如果设置太长,改完解析之后可能等很久才能生效。调试阶段建议把 TTL 设成 600 秒(10 分钟),确认一切稳定之后再改回 3600 以上,减少 DNS 查询压力。

3.2 端口规划与安全组放行

申请证书本身只需要 80 端口(HTTP 验证)或者 443 端口加 DNS API 验证。但 MQTT 服务要对外提供服务,需要提前规划好端口。如果你跟我一样用的是云服务器,登录云控制台在安全组里放行以下入方向规则。

端口用途建议
8883MQTT over TLS公网必须放行
1883明文 MQTT公网关闭,或仅内网放行
8083MQTT over WebSocket按需放行
8084MQTT over WSS如果做 Web 端/小程序接入,必须放行
18083EMQX Dashboard建议只允许你当前办公网 IP 访问

安全组是云服务器最外层的一道闸门,有时候你在服务器内部用 firewalld 或者 iptables 放行了端口,但安全组没放行,从公网依然连不上。排错的时候先把安全组规则过一遍,能省很多时间。

3.3 DNS 验证的工作机制

Let's Encrypt 在签发证书之前需要验证你对这个域名有控制权。ACME 协议提供了两种主流验证方式。

HTTP-01 验证:CA 会访问http://你的域名/.well-known/acme-challenge/xxxx,要求返回一个特定 token。这种方式的先决条件是 80 端口能从公网访问,且该域名的 Web 服务能响应这个路径。

DNS-01 验证:CA 会要求你在域名的 DNS 解析记录里添加一条 TXT 记录,内容是一段随机字符串。用 API 操作 DNS 服务商自动添加记录,验证完成后自动删除。

对 MQTT 场景来说,如果你的服务器 80 端口已经被 Nginx 占用,或者服务器本身不跑 Web 服务,用 DNS-01 验证更省心。acme.sh 对阿里云、腾讯云、Cloudflare 等主流 DNS 服务商都提供了 API 自动接管。我自己的服务器上 Nginx 和 EMQX 同时跑,80 端口被占着,所以直接走 DNS-01,完全不需要动 Web 服务。

4. 实操:用 acme.sh 申请 Let's Encrypt 证书

4.1 安装 acme.sh

acme.sh 是一个用 Shell 写的 ACME 客户端,部署起来非常简单,一条命令搞定。

curl https://get.acme.sh | sh -s email=你的邮箱@example.com

安装完成之后,它会自动配置好 shell 别名和 cron 定时任务。注意这里填的邮箱主要用来接收证书过期提醒,填一个你常用的邮箱即可。

安装完成之后验证一下:

acme.sh --version

如果提示命令找不到,检查当前用户的.bashrc.zshrc里有没有 acme.sh 的 alias,或者直接使用~/.acme.sh/acme.sh全路径执行。

4.2 签发证书

第一次使用先注册账号,然后直接发起签发请求。这里以 DNS-01 验证为例,使用 Cloudflare 的 API Token。

export CF_Token="你的Cloudflare_API_Token" export CF_Zone_ID="你的Zone_ID" acme.sh --issue --dns dns_cf -d mqtt.example.com

如果你用的是阿里云 DNS,对应设置Ali_KeyAli_Secret两个环境变量,然后--dns dns_ali。腾讯云则是dnspod_iddnspod_key

签发成功之后,证书文件会存放在~/.acme.sh/mqtt.example.com/目录下,我们需要的是这样几个文件:

~/.acme.sh/mqtt.example.com/fullchain.cer # 完整证书链 ~/.acme.sh/mqtt.example.com/mqtt.example.com.key # 私钥

fullchain.cer里包含了你的服务器证书和中间证书,EMQX 和 Mosquitto 配置时用的就是完整证书链。不要只把服务器证书单独拷出来用,那样在某些客户端上会因为缺少中间证书导致证书链验证失败。

签发过程可能有延迟,DNS 记录创建之后需要等待几十秒到几分钟传播。acme.sh 默认会等待一段时间并自动重试验证,正常情况下一两分钟内能完成。

4.3 把证书安装到 MQTT 服务目录

acme.sh 的证书文件在~/.acme.sh目录下,这个目录被设计为 acme.sh 的内部工作目录,不建议直接让 EMQX 读取。推荐做法是用--install-cert命令把证书复制到服务目录,并指定 reload 命令。

acme.sh --install-cert -d mqtt.example.com \ --key-file /etc/emqx/certs/mqtt.example.com.key \ --fullchain-file /etc/emqx/certs/fullchain.cer \ --reloadcmd "systemctl reload emqx"

重点是--reloadcmd参数。每次 acme.sh 自动续期成功之后,会执行你指定的 reload 命令,让 EMQX 重新加载新证书,不需要人工介入。如果你忘了配这个参数,续期之后证书文件虽然更新了,但运行中的 EMQX 还在用旧证书,过期之后客户端照样连不上。

5. 把证书配置到 MQTT 服务端

5.1 EMQX 5.x 的 TLS 监听器配置

EMQX 支持通过 Dashboard 可视化上传证书,也可以直接改emqx.conf配置文件。我倾向直接改配置,因为方便用版本管理工具跟踪。

EMQX 默认配置里已经有一个 8883 的 SSL 监听器,只是证书路径被指向了默认的certs/目录下的自带证书。修改/etc/emqx/emqx.conf,找到 SSL 监听器部分:

listeners.ssl.external { bind = "0.0.0.0:8883" ssl_options { keyfile = "/etc/emqx/certs/mqtt.example.com.key" certfile = "/etc/emqx/certs/fullchain.cer" cacertfile = "/etc/emqx/certs/fullchain.cer" } }

保存后执行:

emqx reload

或者重启服务:

systemctl restart emqx

EMQX 会去读取你指定的证书和私钥文件。这里有一个容易踩的坑:EMQX 进程是以emqx用户身份运行的,如果私钥文件权限是 600 且 owner 是 root,EMQX 会直接报 permission denied 导致启动失败。处理方式是把证书目录 owner 改为 emqx,或者把私钥文件权限调整为 640,确保 emqx 用户能读。

5.2 Mosquitto 的 TLS 配置

如果你用的是 Mosquitto 而不是 EMQX,配置逻辑一样,只是文件格式不同。编辑/etc/mosquitto/mosquitto.conf

listener 8883 cafile /etc/mosquitto/certs/fullchain.cer certfile /etc/mosquitto/certs/fullchain.cer keyfile /etc/mosquitto/certs/mqtt.example.com.key

然后重启:

systemctl restart mosquitto

Mosquitto 和 EMQX 一个重要的区别:Mosquitto 的cafile参数是用于告诉客户端应该信任哪个 CA 来验证客户端证书的,如果你的目标是只验证服务端身份,把cafile指向我们自己的完整证书链即可;如果还要做客户端证书认证,需要额外配置require_certificate trueuse_identity_as_username true。这个系列先不做双向认证,大家知道有这回事就行。

5.3 证书文件权限管理

证书文件本身是公开的,但私钥文件必须严格控制权限。我把经验总结为三条:

第一,私钥文件权限设为 600,owner 设为运行 MQTT 服务的用户。第二,不要用 root 身份跑 MQTT 服务。第三,每次续期替换私钥后,检查一下文件权限是否被重置。acme.sh 默认复制的文件权限是当前用户可读写,如果你用 root 跑的 acme.sh,复制出来的私钥可能只有 root 能读,这个时候 MQTT 服务就可能启动失败。

更稳妥的做法是在--install-cert命令之后加一条chown

chown emqx:emqx /etc/emqx/certs/mqtt.example.com.key chmod 600 /etc/emqx/certs/mqtt.example.com.key

把这个命令也写进续期后的 hook 脚本里,或者用 systemd service 文件里的 ExecStartPost 统一处理。

6. 客户端连接验证与常见排错

6.1 用 MQTTX 做首次连接验证

配置完成后,用 MQTTX 这类图形化客户端做一次连接测试最直观。新建连接时填写以下信息。

配置项填写内容
Hostmqtt.example.com
Port8883
Username / Password你在 EMQX 里创建的用户名和密码
SSL/TLS开启
CA Certificate选择fullchain.cer文件

MQTTX 会通过系统信任库自动验证服务器证书,正常情况下不需要手动指定 CA,除非你用了自签名证书。如果你的操作系统信任了 Let's Encrypt 的根证书(现代操作系统基本都信任),只需要点击连接即可。

如果连接失败,最需要关注的是日志里的 TLS 层报错。MQTTX 这类客户端会把具体错误原因显示出来,这是排错的第一手信息。

6.2 用 openssl 命令快速验证 TLS 握手

图形化客户端能验证能不能连,但不能直观地看到证书链的完整状态。用 openssl 的s_client命令可以更底层地检查服务端 TLS 配置。

openssl s_client -connect mqtt.example.com:8883 -servername mqtt.example.com

这个命令会输出完整的 TLS 握手信息,包括证书链、加密套件、协议版本。执行后重点关注这几项:

  • verify return code: 0 (ok),表示证书链验证通过。
  • subject里 CN 或 SAN 要包含mqtt.example.com
  • issuer应该是 R 或 Let's Encrypt 的中间证书。

如果 verify 返回的代码非 0,说明证书链或者域名匹配有问题。常见错误是unable to get local issuer certificate,这说明服务端没有配置完整证书链,只发了服务器证书没发中间证书。

另外可以用下面这条命令直接查看证书文件和过期时间:

openssl x509 -in /etc/emqx/certs/fullchain.cer -noout -subject -dates

6.3 嵌入式设备连接 TLS 的注意事项

如果你在 STM32 这类嵌入式设备上做 MQTT TLS 通信,情况比 PC 客户端复杂很多。嵌入式设备的内存小,很多设备使用的 mbedTLS 或者 WolfSSL 默认只加载了精简的受信任 CA 列表,甚至为了省空间根本不包含任何 CA。

在这种场景下,最可靠的做法是在设备固件里烧录完整的 CA 证书链,或者直接把 Let's Encrypt 的根证书(ISRG Root X1)内嵌到设备中。这里有一个容易踩的坑:Let's Encrypt 的证书链会定期轮换中间证书,如果设备固件里只硬编码了旧的中间证书,续期之后新证书由新的中间证书签发,设备就验证不过了。稳妥的方案是设备端只信任根证书,不要在固件里锁定中间证书,因为根证书几十年不变,中间证书却会轮换。

调试嵌入式设备时建议先用 PC 端 MQTTX 验证一次服务器证书没毛病,再怀疑设备端的 CA 配置问题。这样能把问题域切成“服务端问题”和“设备端问题”两部分。

6.4 续期之后客户端突然连不上

Let's Encrypt 证书每 90 天自动续期,正常情况下客户端无感知。但如果发生客户端突然在证书到期节点附近全部断开的情况,大概率是以下原因之一。

第一,EMQX 没有加载新证书。确认--install-cert--reloadcmd是否配置正确,手动执行一次systemctl reload emqx看日志有没有报错。

第二,证书链不完整。acme.sh 续期时重新生成的fullchain.cer应该没问题,但如果你用的是云厂商的一年期证书,手动下载时一定要选“完整证书链”而不是“服务器证书”单独下载。

第三,设备端硬编码了旧证书指纹。有些安全要求高的设备会在固件里固定服务端证书的 SHA256 指纹做 pinning,证书一换就失联。这种设计对物联网设备来说是把双刃剑,换证书的操作成本极高。我的建议是:能用 CA 验证就不要 pin 指纹,确实需要 pin 的,一定要预留远程更新 pin 的通道。

最后再分享一个我实际运维过程中的小技巧:证书续期后不要直接覆盖原文件然后 reload,用软链接来管理证书路径。在证书目录里创建一个current软链接,指向当前正在使用的版本目录或者直接指向 acme.sh 生成的文件,EMQX 的配置只写死这个current路径。这样每次续期只需要把软链接重新指到新文件,服务无感切换,即使 reload 失败也能快速回滚到旧证书目录。这个思路在服务多了之后会非常省心,遇到 TLS 相关问题也能很快定位是不是证书文件被其他操作覆盖了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 14:36:24

A*算法与非线性优化融合的智能路径规划技术

1. 项目概述:A*与非线性优化的融合路径规划 在机器人导航、游戏AI和物流调度等领域,路径规划始终是核心挑战。传统A 算法虽然能保证找到最短路径,但在复杂环境中存在计算效率低、路径不够平滑等问题。而单纯的非线性优化方法又难以处理大规模…

作者头像 李华
网站建设 2026/9/12 14:30:34

龙珠超109集战斗艺术与角色成长解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 14:30:20

Kafka Consumer 如何从 classic 协议在线迁移到 group.protocol=consumer

Kafka Consumer 如何从 classic 协议在线迁移到 group.protocolconsumer 【免费下载链接】Kafka Apache Kafka - A distributed event streaming platform 项目地址: https://gitcode.com/GitHub_Trending/kafka4/kafka 如果你的消费组目前运行在 Kafka 4.0 集群上&…

作者头像 李华
网站建设 2026/9/12 14:25:00

基于YOLOv8的高速公路团雾预警系统:从模型训练到可视化部署全解析

简介:面向计算机视觉与智慧交通方向的毕业设计、课程设计开发者,这套基于YOLOv8的团雾预警系统融合目标检测与可视化界面,覆盖高速公路团雾场景的数据处理、模型训练、视频检测和界面演示,适合有一定深度学习基础的学生快速上手。…

作者头像 李华
网站建设 2026/9/12 14:24:57

Linux设备驱动开发:硬件交互、设备树与国产化实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华