物联网设备安全认证:TLS加密与MQTT ACL权限控制落地

物联网设备安全认证:TLS 加密 与MQTT ACL权限控制落地

物联网安全这事,行业里说得多做得少。很多项目MQTT跑明文1883端口,设备ID和传感器数据裸奔在网络里,觉得内网环境无所谓。但只要有人接个抓包工具,你整个设备通信协议就透明了——设备ID、Topic结构、数据格式,全暴露。

这篇把TLS加密和MQTT ACL权限控制两块落地讲清楚,从证书生成到ESP32连接EMQX双向认证,再到Topic权限控制,都是实际部署验证过的方案。

明文MQTT的实际风险

用Wireshark抓MQTT 1883端口的数据包,CONNECT包里设备名和密码是明文,PUBLISH包里Topic和payload也是明文。在共享网络环境(比如工厂车间同一网段)下,任何一台设备装个抓包工具就能看到其他设备的通信内容。

更危险的是有人可以伪造设备身份,往你的MQTT Broker发假数据。没有TLS,设备身份无法验证;没有ACL,任何设备都能订阅或发布任何Topic。这不是理论风险,是实际发生过的事故。

自签CA证书与设备证书签发

IoT
场景下用商业CA证书成本太高(每台设备一张证书),自签CA是主流方案。你自己当CA,给每台设备签发独立的客户端证书。

# 1. 生成CA私钥(4096位,安全性优先)
openssl genrsa -out ca.key 4096

# 2. 用CA私钥生成自签CA证书,有效期10年
openssl req -x509 -new -key ca.key -sha256 -days 3650 \
    -subj "/C=CN/O=IoT-CA/CN=IoT-Root-CA" -out ca.crt

# 3. 生成设备私钥
openssl genrsa -out device001.key 2048

# 4. 生成设备证书签名请求(CSR)
openssl req -new -key device001.key \
    -subj "/C=CN/O=IoT/CN=device001" -out device001.csr

# 5. 用CA签发设备证书,有效期1年
openssl x509 -req -in device001.csr \
    -CA ca.crt -CAkey ca.key -CAcreateserial \
    -out device001.crt -days 365 -sha256

几个设计决策值得说明。CA证书用4096位,因为它要长期使用(10年),安全性优先。设备证书用2048位,因为ESP32的mbedTLS做RSA运算要占
内存
和CPU,4096位的握手耗时会明显增加。设备证书CN字段填设备ID,后面ACL可以用CN来匹配设备身份。

每台设备拿到三个文件:自己的私钥device001.key、自己的证书device001.crt、CA证书ca.crt。CA私钥绝对不能下发到设备上,泄露了等于整个PKI体系崩了。

ESP32使用mbedTLS实现MQTT over TLS

ESP32的Arduino环境自带mbedTLS库,WiFiClientSecure封装了TLS连接。配合PubSubClient库做MQTT客户端:

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>

const char* ssid = "YourSSID";
const char* password = "YourPassword";
const char* mqtt_server = "broker.example.com";
const int mqtt_port = 8883;

// CA证书,用于验证Broker身份
const char* ca_cert = \
"-----BEGIN CERTIFICATE-----\n"
"MIIF..."  // 这里粘贴ca.crt的内容
"-----END CERTIFICATE-----\n";

// 设备证书和私钥
const char* client_cert = \
"-----BEGIN CERTIFICATE-----\n"
"MIID..."  // device001.crt内容
"-----END CERTIFICATE-----\n";

const char* client_key = \
"-----BEGIN PRIVATE KEY-----\n"
"MIIE..."  // device001.key内容
"-----END PRIVATE KEY-----\n";

WiFiClientSecure espClient;
PubSubClient client(espClient);

void setup() {
    Serial.begin(115200);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
    }

    espClient.setCACert(ca_cert);
    espClient.setCertificate(client_cert);
    espClient.setPrivateKey(client_key);

    client.setServer(mqtt_server, mqtt_port);
    client.setBufferSize(512);
}

void loop() {
    if (!client.connected()) {
        reconnect();
    }
    client.loop();

    static unsigned long lastReport = 0;
    if (millis() - lastReport > 10000) {
        lastReport = millis();
        String payload = String(millis() / 1000);
        client.publish("devices/device001/telemetry", payload.c_str());
    }
}

void reconnect() {
    while (!client.connected()) {
        Serial.println("Connecting to MQTT over TLS...");
        if (client.connect("device001")) {
            Serial.println("MQTT connected");
            client.subscribe("devices/device001/cmd");
        } else {
            Serial.print("failed, rc=");
            Serial.print(client.state());
            Serial.println(" retry in 5s");
            delay(5000);
        }
    }
}

setCACert验证Broker证书是否由信任的CA签发,setCertificate和setPrivateKey提供设备自己的证书和私钥给Broker验证。这三个一起构成了TLS双向认证:设备验证Broker,Broker也验证设备。

证书内容直接嵌在代码里是简单做法,生产环境建议用SPIFFS/LittleFS存到Flash里,方便证书轮换时不用重新编译固件。调试阶段可以先嵌代码里,上线前迁移到文件系统。

EMQX Broker配置TLS双向认证

EMQX的
TLS配置
在emqx.conf或Dashboard的Listener设置里:

listeners.ssl.default {
    bind = "0.0.0.0:8883"
    ssl_options {
        keyfile = "/etc/emqx/certs/server.key"
        certfile = "/etc/emqx/certs/server.crt"
        cacertfile = "/etc/emqx/certs/ca.crt"
        verify = verify_peer
        fail_if_no_peer_cert = true
    }
}

verify = verify_peer开启客户端证书验证,fail_if_no_peer_cert = true表示没有客户端证书的连接直接拒绝。这两个参数是双向认证的关键,少了任何一个都等于单边认证。

配置后用mosquitto_pub测试:

mosquitto_pub -h broker.example.com -p 8883 \
    --cafile ca.crt \
    --cert device001.crt \
    --key device001.key \
    -t devices/device001/telemetry \
    -m "test"

能发成功说明TLS双向认证通了。不带证书直接连8883端口会被拒绝,这就是你想要的效果——没有有效证书的设备根本连不上来。

MQTT ACL权限规则设计

TLS解决了"你是谁"的问题,ACL解决"你能干什么"的问题。EMQX支持基于文件、数据库和内置规则的ACL。

文件ACL最直接,在acl.conf里写规则:

{allow, {clientid, "device001"}, publish, ["devices/device001/#"]}.
{allow, {clientid, "device001"}, subscribe, ["devices/device001/cmd/#"]}.
{deny, {clientid, "device001"}, subscribe, ["devices/+/+"]}.
{allow, all, subscribe, ["$SYS/#"]}.

设计思路是按设备ID做Topic前缀隔离。device001只能发布和订阅自己前缀下的Topic,不能订阅其他设备的数据。管理端(后端服务)用单独的证书连接,ACL规则放开订阅所有devices/#。

设备量上去后文件ACL不好维护,改用EMQX的HTTP认证插件,ACL规则存数据库,按设备ID动态查询。每台设备认证时查一次数据库拿权限,灵活但多了一层DB依赖。

ACL规则写完后要测。用mosquitto_sub和mosquitto_pub带证书测试每条规则:device001的证书能不能发devices/device001/telemetry(应该可以),能不能发devices/device002/telemetry(应该被拒),能不能订阅devices/device002/data(应该被拒)。EMQX的Dashboard里有连接日志和ACL鉴权日志,能看到每条pub/sub的鉴权结果。

实际项目里还有个常见需求:多个后端服务需要订阅所有设备数据。给后端服务签发单独的证书,CN设为backend-service,ACL规则放开:

{allow, {clientid, "backend-service"}, subscribe, ["devices/#"]}.
{allow, {clientid, "backend-service"}, publish, ["devices/+/cmd/#"]}.

后端服务能订阅所有设备数据,也能往任意设备下发指令。设备端只能管自己的Topic,权限边界清晰。

Topic命名规范直接影响ACL规则的写法。一个好的IoT Topic结构应该是devices/{device_id}/{message_type},device_id和证书CN对应,ACL规则就能用通配符精确匹配。如果Topic结构混乱(比如混用大小写、层级不统一),ACL的deny规则容易漏掉边界情况,安全模型就有缝。

EMQX还支持共享订阅(Shared Subscription),语法是$share/{group}/{topic}。后端多个实例消费同一组设备数据时用共享订阅,消息只被组内一个实例收到,避免重复处理。但共享订阅的ACL规则要单独配——$share前缀也算在Topic匹配里,漏了这条后端服务会连上但收不到消息,排查时不容易想到是ACL问题。

证书过期管理与轮换

设备证书设了1年有效期,到期后设备连不上Broker。轮换方案有两种。

简单方案是证书有效期设长(比如5年),到期前通过OTA更新固件顺便换证书。适合设备数量不多、固件本身有OTA能力的场景。

正式方案是建一个证书管理服务,记录每张证书的CN、签发时间、过期时间。到期前30天自动签发新证书,通过MQTT下发cmd/cert_update通知设备更新,设备从指定URL下载新证书写入Flash。复杂度不低,但设备量大了必须做。

不管哪种方案,CA证书的有效期要设得足够长(10年以上)。CA证书过期意味着所有设备证书全部失效,这个更换成本极高。

证书私钥的保护同样关键。设备私钥存在Flash里,如果设备被物理获取,攻击者可以提取私钥伪造身份。对安全要求高的场景,ESP32可以启用Flash加密和Secure Boot,让Flash内容不可直接读出。ESP32-C3以上型号还有硬件加密加速器,TLS性能更好。

设备证书的CN字段就是设备身份标识,签发时确保CN和设备ID一一对应。如果一台设备的证书泄露了,在EMQX的ACL规则或证书吊销列表(CRL)里把它拉黑,这台设备的连接会被拒绝。EMQX 5.x支持在线管理客户端证书状态,不需要重启Broker。

内存占用与握手耗时

ESP32开启TLS后的内存开销不小。mbedTLS握手过程峰值占约30-40KB堆内存,加上证书解析和RSA运算的缓冲区,ESP32至少需要50KB可用堆内存才稳定。如果同时跑WiFi、MQTT、传感器采集,内存可能不够。

几个优化手段:espClient.setBufferSizes(512, 512)减小TLS缓冲区;证书用2048位RSA而不是4096;client.setBufferSize(512)控制MQTT缓冲。优化后ESP32能稳定在40KB可用堆内存下跑TLS+MQTT。

握手耗时的体感数据:局域网内RSA 2048位的TLS握手大概800ms-1.5s,RSA 4096位会到3-5s。这个延迟只在连接时发生,连上之后后续通信是加密通道不额外耗时。断线重连时会有这个延迟,对实时性要求高的场景要注意。

还有个容易忽略的性能点:TLS握手时RSA签名运算会把ESP32的CPU拉满几百毫秒。如果这段时间有高优先级的定时任务(比如步进电机控制),可能会被打断。实时控制场景下考虑用ECDSA证书替代RSA,椭圆曲线的运算量小一个量级,ESP32的mbedTLS支持secp256r1曲线。不过ECDSA证书的客户端兼容性不如RSA,老版本MQTT客户端可能不支持,部署前要测一遍。

设备多了之后需要一个集中管理入口,把EMQX的Dashboard、证书管理后台、各服务面板统一到一个入口。虎王科技开源的anime_nav_pro_plus导航站系统(Gitee: gitee.com/zesso)可以搭一个内网设备管理导航面板,把这些Web UI聚合到一起,深色风格放运维场景下也不违和。

物联网安全这块很多人嫌麻烦就跳过了,但出事的时候代价更大。如果你正在做设备安全设计,先收藏这篇,关注我后续更新更多IoT安全实践。

posted @ 2026-09-25 10:46  虎王科技  阅读(4)  评论(0)    收藏  举报