跳至正文
老丹的足迹 —— 代码写给机器,游记写给自己,感悟写给时间
老丹的足迹 老丹的足迹
老丹的足迹 老丹的足迹
  • 首页
  • 示例页面
  • 首页
  • 示例页面
老丹的足迹 老丹的足迹
老丹的足迹 老丹的足迹
  • 首页
  • 示例页面
  • 首页
  • 示例页面

应用级 MQTT 服务搭建完全指南:Mosquitto + 宿主机 Nginx

结论先行

这套方案的核心是:Mosquitto 跑在 Docker 里,只绑定宿主机 127.0.0.1:1883,公网入口完全交给宿主机 Nginx。设备和管理客户端走 mqtt.example.com:8883(Nginx stream 代理到 Mosquitto 的 1883),TLS 由 Nginx 终止,证书用 Let’s Encrypt 免费申请并自动续期。不配置 healthcheck,避免探测连接刷日志;真实客户端的连接日志正常记录。认证用密码文件插件,ACL 按业务前缀隔离 hytd 和 tang,管理账号 backend 只授予 $SYS/# 读权限用于监控。

一、整体架构

公网设备/管理端 ──TLS(8883)──► 宿主机 Nginx ──明文(127.0.0.1:1883)──► Docker Mosquitto
组件位置职责
MosquittoDocker,只绑 127.0.0.1:1883MQTT Broker
Nginx宿主机公网 8883 的 TLS 终止与四层代理

关键点:

  • Mosquitto 不暴露公网端口,只绑定宿主机本地回环。
  • Nginx 是唯一公网入口,8883 负责 MQTT over TLS。
  • 证书只存在于宿主机的 /etc/letsencrypt/,不挂载进容器。
  • 不配置 healthcheck,Docker 只靠 restart: unless-stopped 覆盖进程崩溃场景。

二、目录结构

/opt/mosquitto/
├── docker-compose.yml
├── mosquitto/
│   ├── config/
│   │   ├── mosquitto.conf
│   │   ├── passwd
│   │   └── acl
│   ├── data/
│   └── log/

创建命令:

sudo mkdir -p /opt/mosquitto/{mosquitto/config,mosquitto/data,mosquitto/log}
sudo chown -R 1883:1883 /opt/mosquitto/mosquitto/data /opt/mosquitto/mosquitto/log

三、Docker Compose 编排

/opt/mosquitto/docker-compose.yml:

services:
  mosquitto:
    image: eclipse-mosquitto:2.1.2-alpine
    container_name: mosquitto
    restart: unless-stopped
    ports:
      - "127.0.0.1:1883:1883"
    volumes:
      - ./mosquitto/config:/mosquitto/config:ro
      - ./mosquitto/data:/mosquitto/data
      - ./mosquitto/log:/mosquitto/log

关键说明:

127.0.0.1:1883:1883 让 Mosquitto 只绑定宿主机本地,公网无法直接访问。没有 healthcheck,避免每 30 秒一次的探测连接刷日志。restart: unless-stopped 仍然保留,进程崩溃时 Docker 会自动重启容器。

四、Mosquitto 配置

/opt/mosquitto/mosquitto/config/mosquitto.conf:

# ============================================================
# 认证(官方 2.1 推荐的插件方式)
# ============================================================
global_plugin /usr/lib/mosquitto_password_file.so
plugin_opt_password_file /mosquitto/config/passwd

global_plugin /usr/lib/mosquitto_acl_file.so
plugin_opt_acl_file /mosquitto/config/acl

allow_anonymous false

# ============================================================
# 持久化
# ============================================================
persistence true
persistence_location /mosquitto/data/
autosave_interval 300

# ============================================================
# 日志(保留真实客户端连接日志,没有探测噪音)
# ============================================================
log_dest file /mosquitto/log/mosquitto.log
log_dest stdout
log_type error
log_type warning
log_type notice
log_type information

# ============================================================
# MQTT 监听器
# ============================================================
listener 1883
protocol mqtt

# ============================================================
# $SYS 更新频率(默认 10 秒)
# ============================================================
sys_interval 10

# ============================================================
# 性能限制
# ============================================================
max_connections 5000
max_inflight_messages 100
message_size_limit 1048576

4.1 关于连接日志

因为去掉了 healthcheck,日志里不会再出现每 30 秒一次的探测连接。log_type 保留了 notice,所以真实设备的连接和断开仍然会被记录:

New client connected from ... as RT00001 (p4, c1, k60, u'RT00001').
Client RT00001 ... disconnected.

末尾的 u'RT00001' 表示认证通过。如果看到没有 u'...' 的连接,说明有匿名连接被允许了,需要检查 allow_anonymous 是否被误改。

4.2 关于 sys_interval

$SYS 主题的更新频率,默认 10 秒。设为 0 可完全关闭 $SYS 发布。对于你的监控需求,保持 10 秒即可。

4.3 关于 connection_messages

这个选项保持默认(true)或不写。因为 healthcheck 已经去掉,不存在探测噪音,不需要关闭它。真实客户端的连接日志对排查问题有价值,保留。

五、创建密码文件

# tang 管理者
docker run --rm -it \
  -v /opt/mosquitto/mosquitto/config:/mosquitto/config \
  eclipse-mosquitto:2.1.2-alpine \
  mosquitto_passwd -c /mosquitto/config/passwd tang

# hytd 管理者
docker run --rm -it \
  -v /opt/mosquitto/mosquitto/config:/mosquitto/config \
  eclipse-mosquitto:2.1.2-alpine \
  mosquitto_passwd /mosquitto/config/passwd hytd

# 管理端/监控账号 backend
docker run --rm -it \
  -v /opt/mosquitto/mosquitto/config:/mosquitto/config \
  eclipse-mosquitto:2.1.2-alpine \
  mosquitto_passwd /mosquitto/config/passwd backend

hytd 设备账号(共享密码,批量):

for dev in $(cat /opt/mosquitto/hytd-devices.txt); do
  docker run --rm -v /opt/mosquitto/mosquitto/config:/mosquitto/config \
    eclipse-mosquitto:2.1.2-alpine \
    mosquitto_passwd -b /mosquitto/config/passwd "$dev" "hytd-shared-pwd"
done

tang 设备账号(独立密码):

docker run --rm -it -v /opt/mosquitto/mosquitto/config:/mosquitto/config \
  eclipse-mosquitto:2.1.2-alpine \
  mosquitto_passwd /mosquitto/config/passwd TD00001

重载:

docker exec mosquitto killall -HUP mosquitto

密码文件权限:保持属主 1883:1883,权限 600。不要 chown root,否则容器内的 mosquitto 用户读不到文件,容器起不来。

六、ACL 配置

/opt/mosquitto/mosquitto/config/acl:

# ============================================================
# 默认拒绝一切未明确授权的操作
# 这行必须放在最前面,形成"白名单"模型
# ============================================================
topic deny #

# ============================================================
# hytd 设备:动态绑定到自己的用户名
# 只写自己的 status,只读自己的 commands
# ============================================================
pattern write hytd/devices/%u/status
pattern read  hytd/devices/%u/commands

# ============================================================
# tang 设备:动态绑定到自己的用户名
# ============================================================
pattern write tang/devices/%u/status
pattern read  tang/devices/%u/commands

# ============================================================
# hytd 管理者:只能管 hytd/ 下的设备
# ============================================================
user hytd
topic read  hytd/devices/+/status
topic write hytd/devices/+/commands

# ============================================================
# tang 管理者:只能管 tang/ 下的设备
# ============================================================
user tang
topic read  tang/devices/+/status
topic write tang/devices/+/commands

# ============================================================
# 管理端/监控账号:只读 $SYS
# 不碰任何设备业务主题,降低凭证泄露影响范围
# ============================================================
user backend
topic read $SYS/#

6.1 ACL 语法精要

topic read/write/readwrite/deny <主题模式> 定义主题级权限。read 对应订阅,write 对应发布,readwrite 两者皆可,deny 用于显式拒绝(优先级最高,会覆盖其他允许规则)。如果省略访问类型,默认为 readwrite。

user <用户名> 开启一个用户专属的权限块,其后所有 topic 规则只对该用户生效。用户名必须与密码文件中的一致。

通配符与 MQTT 订阅语法一致:+ 匹配单层,# 匹配多层。例如 hytd/devices/+/status 匹配 hytd/devices/RT00001/status,hytd/devices/# 匹配 hytd/devices/ 下的所有层级。

6.2 动态模式(最实用的特性)

pattern 关键字支持 %u(用户名)和 %c(客户端 ID)的替换。pattern write hytd/devices/%u/status 意味着用户 RT00001 自动获得对 hytd/devices/RT00001/status 的写权限,用户 RT00002 获得对 hytd/devices/RT00002/status 的写权限。这比逐条列举用户规则更简洁,也避免了遗漏。新增设备时无需修改 ACL 文件,只要在密码文件里加个用户就行。

6.3 ACL 规则顺序与 deny 优先级

deny 规则的优先级高于所有 read/write/readwrite 规则。即使后面有 topic readwrite # 允许一切,只要前面有 topic deny somespecific/#,该主题仍会被拒绝。这个特性可以用来从宽泛的允许中抠掉敏感主题。

6.4 backend 只读 $SYS 的设计意图

当前 ACL 中 backend 只授予 $SYS/# 读权限,不碰任何设备业务主题。这样监控凭证泄露的影响范围极小——攻击者拿到 backend 密码,只能看到 Broker 的运行指标,看不到设备数据,也不能下发命令。

如果你以后需要 Dashboard 或管理工具读取设备状态、下发命令,应该单独创建一个账号(比如 manager),授予业务主题的读写权限,而不是扩大 backend 的权限。

6.5 需要留意的坑

ACL 不报错,只静默失败:订阅一个未被 ACL 允许的主题时,Broker 在 SUBACK 中返回拒绝码,但很多客户端库不会报错,只是收不到消息。发布时更是完全静默丢弃。调试 ACL 时,建议临时把 log_type 设为 all,观察日志中是否有 denied 记录。

修改 ACL 后必须重启或发送 SIGHUP:Mosquitto 不会自动重载 ACL 文件。修改后执行 docker exec mosquitto killall -HUP mosquitto 即可热加载,已连接的客户端不受影响。

用户名 ≠ Client ID:user 块和 %u 占位符使用的是用户名(连接时提供的),而 %c 使用的是 Client ID。两者可以相同,但概念上要区分清楚。

业务前缀必须匹配:设备端代码里硬编码的业务前缀必须和 ACL 里的前缀一致。hytd 设备用 hytd/ 前缀和 RT 开头的 ID,tang 设备用 tang/ 前缀和 TD 开头的 ID。前缀写错会导致 ACL 拒绝,且错误是静默的。

设备 ID 必须全局唯一:RT + 5 位数字支持 99999 台 hytd 设备,TD + 5 位数字支持 99999 台 tang 设备。如果超过,扩展到 6 位。设备 ID 同时是 MQTT 用户名和主题里的设备标识,重复会导致权限串号和主题冲突。

6.6 如何验证 ACL 生效

用 mosquitto_pub 和 mosquitto_sub 做反向测试:

# 用 RT00001 尝试向 tang 的主题发布(应该被静默拒绝)
mosquitto_pub -h 127.0.0.1 -p 1883 -u RT00001 -P hytd-shared-pwd \
  -t "tang/devices/TD00001/status" -m "hack"

# 用 RT00001 向自己的主题发布(应该成功)
mosquitto_pub -h 127.0.0.1 -p 1883 -u RT00001 -P hytd-shared-pwd \
  -t "hytd/devices/RT00001/status" -m "ok"

# 检查 broker 日志确认拒绝记录
docker logs mosquitto | grep denied

如果日志中出现 denied PUBLISH 或 denied SUBSCRIBE,说明 ACL 正在起作用。

6.7 hytd 共享密码在代码里的风险边界

攻击者逆向一台 hytd 设备,可以拿到共享密码,从而冒充所有 hytd 设备。但攻击者仍然无法:

  • 冒充 tang 设备(tang 密码独立)
  • 冒充管理者(管理者密码独立)
  • 越权访问其他主题(ACL 隔离)

对于“设备不重要、挂了也无所谓”的场景,这个风险可以接受。如果要降低风险,可以改用 HMAC 派生(共享密钥 + 用户名算出每台设备的独立密码),但共享密钥仍在固件里,逆向一台设备仍可算出所有密码,只是提高了网络层面密码泄露的门槛。

七、宿主机 Nginx 配置

7.1 安装 nginx-full

sudo apt update
sudo apt install -y nginx-full
nginx -V 2>&1 | grep -o with-stream

输出 with-stream 说明 stream 模块可用。

7.2 stream 配置(MQTT 四层代理)

编辑 /etc/nginx/nginx.conf,在 http 块之外添加:

stream {
    upstream mosquitto_backend {
        server 127.0.0.1:1883;
    }

    server {
        listen 8883 ssl;
        listen [::]:8883 ssl;
        ssl_certificate     /etc/letsencrypt/live/mqtt.example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/mqtt.example.com/privkey.pem;
        proxy_pass mosquitto_backend;
        proxy_timeout 1h;
    }
}

关键点:

  • 127.0.0.1:1883 指向宿主机上 Docker 映射出来的 Mosquitto 端口。
  • proxy_timeout 1h 设置长连接超时,MQTT 是长连接,超时要设大。
  • 证书路径是宿主机的 /etc/letsencrypt/live/mqtt.example.com/,必须用 fullchain.pem。

7.3 重载 Nginx

sudo nginx -t && sudo systemctl reload nginx

八、申请证书(宿主机 Certbot)

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d mqtt.example.com

Certbot 会自动通过 80 端口验证并修改 Nginx 的 http 配置。stream 块里的证书路径需要手动写,指向 Certbot 生成的 /etc/letsencrypt/live/mqtt.example.com/。

自动续期 hook:

sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh << 'EOF'
#!/bin/bash
systemctl reload nginx
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

验证自动续期:

sudo certbot renew --dry-run

九、启动与验证

cd /opt/mosquitto
docker compose up -d
docker compose logs -f mosquitto

验证认证生效:

# 匿名应该被拒绝
docker run --rm eclipse-mosquitto:2.1.2-alpine \
  mosquitto_pub -h 127.0.0.1 -p 1883 -t test -m hello

# backend 应该成功
docker run --rm eclipse-mosquitto:2.1.2-alpine \
  mosquitto_pub -h 127.0.0.1 -p 1883 -u backend -P '密码' -t test -m hello

验证外网 8883:

mosquitto_sub -h mqtt.example.com -p 8883 \
  --cafile /etc/ssl/certs/isrgrootx1.pem \
  -u backend -P '密码' -t '$SYS/broker/uptime' -C 1 -W 5

如果收到 9 seconds 或类似数字,说明外网 TLS 代理、认证、ACL 全通了。

验证日志:启动后观察 docker logs mosquitto,应该只有真实客户端的连接/断开日志,没有每 30 秒一次的探测噪音。

十、设备端连接

设备通过 ssl://mqtt.example.com:8883 连接,Nginx 转发到 Mosquitto 的 127.0.0.1:1883。

const std::string BROKER_ADDRESS = "ssl://mqtt.example.com:8883";
const std::string CLIENT_ID = "RT00001";
const std::string USERNAME = "RT00001";
const std::string PASSWORD = "hytd-shared-pwd";
const std::string CA_CERT = "/etc/ssl/certs/ca-certificates.crt";

mqtt::connect_options conn_opts;
conn_opts.set_user_name(USERNAME);
conn_opts.set_password(PASSWORD);
conn_opts.set_keep_alive_interval(60);
conn_opts.set_clean_session(false);
conn_opts.set_automatic_reconnect(true);

mqtt::ssl_options ssl_opts;
ssl_opts.set_trust_store(CA_CERT);
ssl_opts.set_enable_server_cert_auth(true);
conn_opts.set_ssl(ssl_opts);

设备发布状态到 hytd/devices/RT00001/status(QoS 0),订阅 hytd/devices/RT00001/commands(QoS 1)。消息格式和命令处理按你之前的设计文档执行。

十一、MQTTX 客户端简要使用

11.1 内网测试(1883 明文)

新建连接:

字段值
Protocolmqtt://
Host127.0.0.1
Port1883
Usernamebackend
Password对应密码

连接成功后,新建订阅:Topic 填 $SYS/#,QoS 选 0。消息列表会持续刷新 Broker 的 uptime、clients/connected、messages/received 等指标。

11.2 外网测试(8883 TLS)

字段值
Protocolmqtts://
Hostmqtt.example.com
Port8883
Usernamebackend
Password对应密码
SSL/TLS开启
CA Fileisrgrootx1.pem(从 https://letsencrypt.org/certs/isrgrootx1.pem 下载)

11.3 常用的 $SYS 主题

主题含义
$SYS/broker/uptime运行秒数
$SYS/broker/clients/connected当前连接数
$SYS/broker/messages/received累计接收消息数
$SYS/broker/messages/sent累计发送消息数
$SYS/broker/publish/messages/dropped被丢弃的消息数,非 0 需关注
$SYS/broker/load/messages/received/1min最近 1 分钟接收速率

注意:# 通配符不匹配 $SYS 开头的主题,必须显式订阅 $SYS/#。命令行里 $ 要用单引号包住,避免 shell 展开。

十二、运维要点

12.1 数据备份

mosquitto/data/ 目录下的 mosquitto.db 是持久化文件,包含会话、订阅关系和保留消息。定期备份该目录和 mosquitto/config/ 目录即可保留所有配置和状态。恢复时停止容器、替换文件、重启即可。

12.2 日志轮转

长时间运行后 mosquitto/log/mosquitto.log 会持续增长。在宿主机上配置 logrotate:

/opt/mosquitto/mosquitto/log/mosquitto.log {
    daily
    rotate 7
    compress
    missingok
    notifempty
    postrotate
        docker exec mosquitto killall -USR1 mosquitto
    endscript
}

SIGUSR1 让 Mosquitto 重新打开日志文件,轮转后不需要重启容器。

12.3 监控:$SYS 主题树

Mosquitto 的 $SYS/# 主题树包含 Broker 自身的运行指标。当前配置下,backend 账号只有 $SYS/# 读权限,适合做专用监控账号。可以订阅该主题接入 Prometheus、Grafana 或自定义脚本。

日常最值得盯的几个指标:

主题异常信号
$SYS/broker/clients/connected突然暴跌 = 大量设备掉线
$SYS/broker/publish/messages/dropped非 0 = 有消息被丢弃,检查队列限制
$SYS/broker/load/messages/received/1min突增 = 可能有设备在异常高频上报
$SYS/broker/uptime归零 = Broker 刚重启过

12.4 证书续期

Certbot 的 systemd timer 会自动检查续期。用 --nginx 插件时,续期后 Certbot 自动重载 Nginx。stream 块引用的证书文件路径由 Certbot 生成,续期后文件更新,Nginx 重载时自动加载新证书。

12.5 版本升级

修改 Compose 文件中的镜像标签(如从 2.1.2-alpine 到未来的 2.1.x-alpine),执行 docker compose up -d 即可。升级前务必备份 mosquitto.db,因为持久化格式可能随版本改变。

十三、需要留意的关键问题

13.1 stream 块的证书路径

Certbot 的 --nginx 插件只管理 http 块里的证书配置,不会自动修改 stream 块。stream 块里引用的证书路径需要手动写,指向 /etc/letsencrypt/live/mqtt.example.com/。续期后文件更新,Nginx 重载时自动加载新证书。

13.2 Mosquitto 看到的客户端 IP 是 127.0.0.1

Nginx 的 stream 模块代理 MQTT 后,Mosquitto 日志里的客户端 IP 变成 Nginx 的 IP(127.0.0.1),不是设备的真实 IP。

如果你需要记录真实 IP(比如为了 fail2ban 封禁暴力破解的 IP),需要启用 PROXY protocol。Mosquitto 2.1 支持 PROXY protocol,Nginx 的 stream 模块也支持。在 Nginx 的 server 块里加 proxy_protocol on;,Mosquitto 的 listener 加 proxy_protocol true。

PROXY protocol v1 不支持传递 TLS 信息,对你现在的单向 TLS + 密码认证场景够用。如果你不需要 fail2ban,可以忽略这一点。

13.3 加用户时千万别带 -c

-c 是“创建新文件”,会覆盖整个密码文件,之前加的所有账号都会消失。只有第一次建文件时才用 -c,之后追加用户一律不加。

13.4 密码文件修改后必须重载

mosquitto_passwd 改完密码文件后,Broker 不会自动重载,必须发信号:

docker exec mosquitto killall -HUP mosquitto

13.5 ACL 静默失败

订阅未被允许的主题时,Broker 不会返回明确的“拒绝”错误,只是不发送消息。发布时更是完全静默丢弃。调试 ACL 时,查看日志中的 denied 记录:

docker logs mosquitto 2>&1 | grep denied

13.6 密码字符限制

密码本身几乎没有字符限制,MQTT 密码最高支持 65535 字节。唯一的硬性限制是不能为空,且用户名里不能包含冒号 :(因为密码文件用冒号分隔用户名和哈希)。

用 mosquitto_passwd -b 时,如果密码里有特殊符号(如 #、$、!),Shell 可能会把它当成控制字符。建议:

  • hytd 共享密码:用大小写字母和数字的组合,避免符号转义。
  • tang 独立密码:用交互式输入(不带 -b),不经过 Shell 解析,可以放心用复杂密码。

13.7 密码文件属主与权限

密码文件属主保持 1883:1883,权限 600。不要 chown root——官方 Docker 镜像以 UID 1883 运行 Mosquitto,chown root 后容器读不到文件,会无限重启。那条 Warning: File ... owner is not root 是 Mosquitto 2.1 对非 Docker 场景的提示,在 Docker 环境下只能忽略。

总结

这套方案的完整配置链是:Docker Compose 定义 Mosquitto,端口只绑定 127.0.0.1 → 宿主机 Nginx 的 stream 块负责公网 8883 的 TLS 终止 → Certbot 申请和续期证书 → ACL 按业务前缀隔离,backend 只读 $SYS → 密码认证 + 单向 TLS 保证安全。

不配置 healthcheck,彻底消除探测连接产生的日志噪音;真实客户端的连接日志通过 log_type notice 正常记录,排查问题时仍然可用。监控账号 backend 只读 $SYS/#,最小化权限。MQTTX 可以分别用内网 1883 和外网 8883 两种方式连接,订阅 $SYS/# 即可实时观察 Broker 运行状态。

作者

老丹

关注我
其他文章
上一个

OpenWrt 固件签名三剑客:usign、ucert 与 fwtool 深度解析

下一个

Mosquitto $SYS 主题详解:Broker 自监控指南

关于博主

    老丹是一名C/C++后台开发工程师,信奉“无抽象不设计,无性能不生产”。

  • 技术栈:Modern C++、Linux环境编程、多线程/并发、网络编程等。
  • 信条:能用constexpr解决的问题绝不拖到运行时,能靠RAII避免的泄漏绝不写析构。
  • 正在填坑:从解封装到渲染的C++全链路实现,正在驯服FFmpeg与H.264/H.265。
  • 输出原则:这里的每一段代码都经过-Wall -Wextra -Werror -O2的洗礼。

近期文章

  • 在 Windows 里养一只 Linux:WSL 完整指南 2026年10月24日
  • Paho MQTT C 与 C++ 客户端库完全指南:从概念到 Ubuntu 26.04 下的编译、安装与使用 2026年10月23日
  • Mosquitto $SYS 主题详解:Broker 自监控指南 2026年10月23日
  • 应用级 MQTT 服务搭建完全指南:Mosquitto + 宿主机 Nginx 2026年10月22日
  • OpenWrt 固件签名三剑客:usign、ucert 与 fwtool 深度解析 2026年10月21日

文章分类

  • C/C++开发 (22)
  • Docker容器 (5)
  • Linux工具包 (21)
  • Linux服务配置 (53)
  • Linux系统 (16)
  • MQTT技术 (6)
  • OpenWrt路由 (5)
  • Shell脚本 (3)
  • 代码管理 (1)
  • 安防技术 (4)
  • 数据安全 (36)
  • 网络协议 (25)
  • 计算机理论 (23)
  • 问题解决 (3)
  • 防火墙 (6)
  • 音视频技术 (6)
联系我们:📍 地址:中国·广东省深圳市   |   ✉️ 邮箱:support@tanglinux.com   |   💬 QQ:870866607
版权所有:老丹的足迹粤ICP备2026061170号-1       公安备案图标 粤公网安备44030002013274号