- 人工智能
- AI Agent
- 多模态
- 语音
- AI 应用
【免费下载链接】ten-framework
Open-source framework for conversational voice AI agents
本篇文章以 third_party/libwebsockets 仓库中的 lws-acme-client 协议插件为核心,系统讲解如何让基于 libwebsockets 的服务(如 lwsws)在启动阶段"无中生有"地自动向 Let's Encrypt 等 ACME 证书服务商申请、安装并续期浏览器可信任的 TLS 证书。读完本文,你将掌握 lwsws 配置文件中的完整 PVO(per-vhost option)参数含义、tls-sni-01质询的自动化流程、root 权限与低权限运行模式下的证书存储设计,以及多 vhost 共享证书的更新机制,可直接上手部署一个免人工干预的 HTTPS 服务。
引言:什么是 lws-acme-client
lws-acme-client是 libwebsockets 的一个协议插件(protocol plugin),它实现了一个完整的 ACME(Automatic Certificate Management Environment)客户端,可以与 Let's Encrypt 以及其它遵循 ACME 协议的证书签发服务商通信,自动获取 TLS 证书。
它实现了tls-sni-01质询(challenge),能够"凭空"(from thin air)配置出被所有主流浏览器接受的 TLS 证书——也就是说,你不需要预先购买或手工放置证书文件,只要服务器满足基本条件,插件会在运行时自动完成证书的申请与安装。同时,它还会在证书剩余有效期不足两周时自动重新申请证书(自动续期),无需人工干预。
该插件同时支持 OpenSSL 与 mbedTLS 两种 TLS 后端,因此无论构建时选择了哪种后端,都可以正常使用。
从源码看,插件的实现位于 plugins/acme-client/protocol_lws_acme_client.c,文件头注释明确说明:"This implementation follows draft 7 of the IETF standard, and falls back to whatever differences exist for Boulder's tls-sni-01 challenge. tls-sni-02 is also supported.",即遵循 IETF ACME 标准草案第 7 版,并兼容 Boulder(Let's Encrypt 的服务端实现)的tls-sni-01差异,同时支持tls-sni-02。
使用前准备:三个前提条件
要让 lws-acme-client 正常工作,你需要满足以下三个前提:
- 域名解析:为你的服务器 IP 配置域名解析。例如
myserver.com必须能够解析到承载你的服务器的那个 IP 地址——因为 ACME 服务器需要根据证书里的域名找到你的服务器来完成 SNI 质询。 - 网络可达:开启端口转发或外部防火墙放行规则,通常放行
443端口,使外部网络能够访问到你的服务。 - 启用插件:在希望由该插件管理证书的 vhost 上启用
lws-acme-client插件。 - 配置 PVO:为每个 vhost 添加描述"证书中应该包含什么内容"的每 vhost 选项(per-vhost options,PVO)。
完成以上配置后,其余工作(申请、签发、安装、续期)全部由插件自动完成。
完整 lwsws 配置示例
下面是在 lwsws(libwebsockets 的 WebSocket + HTTP 服务器守护进程)配置文件中启用该插件的完整示例。该配置声明了一个名为home.warmcat.com的 vhost,监听443端口,并把lws-acme-client协议插件挂到该 vhost 上:
"vhosts": [ { "name": "home.warmcat.com", "port": "443", "host-ssl-cert": "/etc/lwsws/acme/home.warmcat.com.crt.pem", "host-ssl-key": "/etc/lwsws/acme/home.warmcat.com.key.pem", "ignore-missing-cert": "1", "access-log": "/var/log/lwsws/test-access-log", "ws-protocols": [{ "lws-acme-client": { "auth-path": "/etc/lwsws/acme/auth.jwk", "cert-path": "/etc/lwsws/acme/home.warmcat.com.crt.pem", "key-path": "/etc/lwsws/acme/home.warmcat.com.key.pem", "directory-url": "https://acme-staging.api.letsencrypt.org/directory", "country": "TW", "state": "Taipei", "locality": "Xiaobitan", "organization": "Crash Barrier Ltd", "common-name": "home.warmcat.com", "email": "andy@warmcat.com" }, ...配置要点说明:
- vhost 级别的
"host-ssl-cert"与"host-ssl-key"含义与平时完全一致,分别指向证书与私钥文件;但因为 ACME 插件可以自动生成这些文件,所以必须同时给 vhost 打上"ignore-missing-cert" : "1"标记。 ws-protocols下的lws-acme-client对象,其内部字段就是插件的 PVO(per-vhost options),用于描述证书内容与 ACME 服务端地址等信息。- 需要特别强调的是,配置里提到的所有目录都必须预先手工创建——lws 不会替你创建目录。这些目录建议设置为
0700且属主为root:root,即使之后 lws 以降权后的身份运行也没有关系(原因见下文"安全与密钥存储设计"一节)。
必需 PVO(Required PVOs)
关于ignore-missing-cert与host-ssl-cert/host-ssl-key
在 lwsws 配置中,"host-ssl-cert"和"host-ssl-key"的含义与常规配置完全一致,它们指向 vhost 实际使用的证书与私钥文件。区别在于:这些文件最初并不存在,需要 ACME 插件在运行期把它们生成出来。
因此必须给 vhost 设置"ignore-missing-cert" : "1",这样 lwsws 在启动时对"缺失的证书/密钥"不会报错退出,而是会启动 ACME 流程去创建所需证书与密钥。
如果是在代码层面实现(不使用 lwsws 配置),等价的做法是:创建 vhost 时,确保info.options设置了LWS_SERVER_OPTION_IGNORE_MISSING_CERT位。也就是说,"ignore-missing-cert" : "1"在底层对应的就是 vhost options 中的这个标志位。
同样,在代码中,上面展示的每个 per-vhost 选项都可以在创建 vhost 时通过一个struct lws_protocol_vhost_options链表提供。原文档建议参考./test-apps/test-server-v2.0.c(注:该文件在当前仓库快照中未收录,相关 PVO 链表的查询与绑定实现可参见 lib/core-net/vhost.c 中的lws_vhost_protocol_options()与 lib/core-net/wsi.c 中的lws_pvo_search())。
auth-path
auth-path是插件存放**自己生成的认证密钥(auth keys)**的位置。插件启动时会检查该路径下是否已有 JWK 格式的注册密钥,如果没有就生成新的 RSA 密钥对并保存。
源码 protocol_lws_acme_client.c 中的lws_acme_load_create_auth_keys()函数(L663-L686 附近)展示了这一逻辑:先用lws_jwk_load()尝试加载已有密钥,若加载失败(即密钥尚不存在),则通过lws_genrsa_new_keypair()生成新的 RSA 密钥对,再用lws_jwk_save()保存到auth-path。在非 ESP32 平台上默认使用4096 位RSA 密钥(见lws_acme_load_create_auth_keys(vhd, 4096)的调用处,L852)。
cert-path
cert-path是插件存放证书文件的位置。它应当与 vhost 使用的host-ssl-cert指向同一个文件,这样证书签发完成后 vhost 才能直接使用。
路径中至少要包含一个0700 root:root权限的目录,原因同样是"root-only 存储"设计(见下)。
key-path
key-path是插件存放证书私钥的位置,同样应当与 vhost 使用的host-ssl-key一致。
路径中同样至少要包含一个0700 root:root权限的目录。
directory-url
directory-url定义你要从中获取证书的ACME 服务端目录 URL。以 Let's Encrypt 为例,它有两个:
- 练习(staging)地址:
https://acme-staging.api.letsencrypt.org/directory - 正式(real)地址:
https://acme-v01.api.letsencrypt.org/directory
两者的主要区别在于:正式地址的 CA 证书已经预置在绝大多数浏览器中,而 staging 地址的 CA 证书不在浏览器内置列表中。同时 staging 服务器对反复测试的限制更宽松("让你更随意地滥用它做重复测试")。
官方强烈建议:先用 staging 的 directory-url 确认整个流程按预期工作,然后再切换到正式 URL。这样可以避免测试过程中反复触发 Let's Encrypt 的签发频率限制。
common-name
common-name是你的服务器 DNS 名称,例如libwebsockets.org。远程 ACME 服务器会用这个名称去找到你的服务器,然后执行 SNI 质询——这就是整个自动签发的关键环节:ACME 服务器必须能通过该域名访问到你的 443 端口。
源码在lws_acme_start_acquisition()(L689-L697 附近)中会首先检查是否配置了common-name(LWS_TLS_REQ_ELEMENT_COMMON_NAME),如果没有则直接返回失败——它是证书申请的必要信息之一。
email是证书的联系邮箱地址。源码在 ACME 注册(new account)阶段使用它(对应枚举ACME_STATE_NEW_ACCOUNT,见 protocol_lws_acme_client.c 中ACME_STATE_*状态机定义,L45-L57),即"注册一个新的 RSA 密钥 + email 组合"。
可选 PVO(Optional PVOs)
以下 PVO 是证书主体(subject)中可选的填充项。文档特别注明:"These are not included in the cert by letsencrypt"(这些字段 Let's Encrypt 不会包含进证书里),即它们会参与证书请求的构造,但 Let's Encrypt 签发的证书不会携带这些可辨识身份信息。它们分别是:
- country:证书的两字母国家代码(Two-letter country code),示例中为
"TW"。 - state:证书的州/省(State "or province"),示例中为
"Taipei"。 - locality:证书的所在地(Locality),示例中为
"Xiaobitan"。 - organization:你的公司名称(Your company name),示例中为
"Crash Barrier Ltd"。
从源码角度,这些字段对应的 PVO 名称全部定义在pvo_names[]数组中(protocol_lws_acme_client.c L643-L655):country、state、locality、organization、common-name、subject-alt-name、email、directory-url、auth-path、cert-path、key-path,共 11 个。插件在 vhost 初始化(LWS_CALLBACK_PROTOCOL_INIT)时会遍历 PVO 链表并逐一比对名字完成绑定;其中common-name及之后的必填项(除subject-alt-name外)缺失时,初始化会直接失败(L826-L845 附近的校验逻辑)。
安全与密钥存储设计:root-only 存储 + 动态热更新
lws-acme-client插件最精巧的设计在于:即使 lws 进程以非 root 的 uid/gid 运行、且对存储目录没有任何访问权限,它也能在一个完全"仅 root 可访问"(root-only)的环境中完成证书和密钥的签发与更新。
其实现机制如下(源码证据见 protocol_lws_acme_client.c):
- 启动阶段以 root 权限打开更新文件描述符:在 vhost 初始化(
LWS_CALLBACK_PROTOCOL_INIT)期间,插件还拥有 root 权限,它会为每个证书和私钥在"更新路径"上打开并持有两个只写(WRONLY)文件描述符。这些更新路径就是正常的 cert/key 路径加上.upd后缀,即cert-path.upd与key-path.upd(源码 L859-L885:对"%s.upd"以LWS_O_WRONLY | LWS_O_CREAT | LWS_O_TRUNC模式打开,权限 0600)。文件描述符保存在vhd->fd_updated_cert与vhd->fd_updated_key(结构体定义见 L128-L129,注释写明 "these are opened while we have root...")。 - 运行期低权限写入:之后 lws 即便降权运行,这两个 fd 依然有效,插件可以随时向其中写入新证书/新私钥。
- 到期前两周自动续期:当证书剩余有效期进入两周内时,插件会走完整的 ACME 协商流程申请新证书,并通过这两个 fd 写入。对应的回调是
LWS_CALLBACK_VHOST_CERT_AGING(L898-L929):源码首先通过(int)(ssize_t)len > 14判断"证书是否已接近到期(剩余天数不超过 14 天)",然后确认该 vhost 是否是自己被配置的 vhost,接着从caa->element_overrides合并证书元素覆盖项,最后调用lws_acme_start_acquisition()开始申请。 - 新证书下载与写入:在
ACME_STATE_DOWNLOAD_CERT状态(L1507-L1581)中,插件校验响应码为 200 后,把证书与私钥分别写入两个.updfd(lws_plat_write_cert(),且故意"don't close it... we may update the certs again"),随后调用lws_tls_cert_updated()通知 libwebsockets 发生了证书更新。 - 下次启动时落盘:下一次服务器启动时,如果发现
.upd证书与密钥存在,它会在降权之前把旧文件备份、将.upd内容拷贝到位作为新证书。这样持久化的证书/密钥始终只存在于 root-only 目录里。 - 长时间运行场景的热更新:为了应对服务器长时间不重启的情况,lws 在更新证书后,还会用证书和密钥的内存临时副本即时更新 vhost 正在使用的 TLS 证书——vhost 无需重启即可使用新证书。
通过这套"root-only 落盘 + 内存热更新"的双轨机制,证书与私钥始终被保护在仅 root 可读的目录中,同时 vhost 能动态跟上证书的任何变化。这也解释了为什么前文要求存储目录必须手工创建为0700 root:root——插件在降权前就已打开 fd,运行期不再需要目录写权限。
多个 vhost 共享同一证书
在多个 vhost 使用同一份证书的场景下,只需把lws-acme-client插件挂载到其中一个 vhost 实例上即可,不要重复挂载。
当证书更新时,所有使用该证书的 vhost 都会被通知,而那些通过相同文件路径访问证书的 vhost 也能同步更新自己的证书。这依赖于前文提到的lws_tls_cert_updated()通知机制以及 vhost 的证书老化(cert aging)回调。
实现注意事项:切换 TLS 后端时清除认证密钥
一个需要特别注意的实现细节:当从 OpenSSL 后端切换到 mbedTLS 后端(或反之)时,必须删除auth-path指向的认证密钥文件(示例路径为/etc/lwsws/acme/auth.jwk)。
原因是认证密钥(JWK 注册密钥)由旧的加密后端生成,切换后端后格式可能不兼容。删除后插件会在下次运行时自动重新生成(见lws_acme_load_create_auth_keys()中"加载失败则重新生成"的逻辑),无需手工干预。
底层 ACME 状态机一览
从源码可以清晰地看到插件实现的 ACME 客户端完整状态机(protocol_lws_acme_client.c L45-L57):
ACME_STATE_DIRECTORY /* GET 目录 JSON 并解析 */ ACME_STATE_NEW_NONCE /* 获取 replay nonce */ ACME_STATE_NEW_ACCOUNT /* 注册新的 RSA 密钥 + email 组合 */ ACME_STATE_NEW_ORDER /* 开始请求证书的流程 */ ACME_STATE_AUTHZ /* 授权 */ ACME_STATE_START_CHALL /* 通知服务器准备接收一个质询 */ ACME_STATE_POLLING /* 服务器应正在验证我们的质询 */ ACME_STATE_POLLING_CSR /* 已发送 CSR,检查结果 */ ACME_STATE_DOWNLOAD_CERT /* 下载签发的证书 */ ACME_STATE_FINISHED整个流程的起点是lws_acme_start_acquisition()(L689):若尚未取得目录信息,则先从directory-urlGET 目录 JSON(ACME_STATE_DIRECTORY);否则直接进入新账户注册(ACME_STATE_NEW_ACCOUNT)。即使是非首次运行,重复注册也只是收到一个合法的、非致命的 409 JSON 响应(源码注释中有明确示例:"Registration key is already in use"),并不会导致失败。
此外,源码中ACME_STATE_NEW_NONCE对应获取 replay nonce,这是 ACME 协议防重放(anti-replay)的要求;在证书下载阶段(L1518-L1534),插件还会处理 ACME 2.0 可能返回的证书链(最多 3 张证书),通过查找"END CERTIFICATE-----"标记只保存第一张叶子证书。
部署检查清单
最后,整理一份可操作的部署清单:
- 确保域名(如
home.warmcat.com)已解析到服务器 IP,且 443 端口对外可达(端口转发 / 防火墙放行)。 - 手工创建证书存储目录(如
/etc/lwsws/acme/),权限设为0700 root:root。 - 在 lwsws 配置中为 vhost 声明
host-ssl-cert/host-ssl-key,并设置"ignore-missing-cert": "1"。 - 在
ws-protocols中挂载lws-acme-client,至少配置 5 个必需 PVO:auth-path、cert-path、key-path、directory-url、common-name、email。 - 先用
directory-url指向 Let's Encryptstaging地址验证完整流程,确认无误后再切换到正式地址。 - 若从 OpenSSL 后端切换到 mbedTLS 后端,记得删除
auth-path下的 JWK 文件让其重新生成。 - 多 vhost 共享同一证书时,只在其中一个 vhost 上挂载插件。
完成上述步骤后,libwebsockets 的 lwsws 即具备"证书自动申请、自动安装、到期前两周自动续期"的全生命周期管理能力,HTTPS 服务从此无需人工维护证书。
- 人工智能
- AI Agent
- 多模态
- 语音
- AI 应用
【免费下载链接】ten-framework
Open-source framework for conversational voice AI agents
相关推荐
EMQX ACME 插件实战:为 MQTT TLS 与 Dashboard HTTPS 自动签发和续期证书
EMQX ACME 插件实战:为 MQTT TLS 与 Dashboard HTTPS 自动签发和续期证书 EMQX 从 6.1.x 起通过 emqx_acme
后端物联网消息队列通信CAS 与 ACME 集成指南:基于 Let's Encrypt 的自动化证书签发与续期
CAS 与 ACME 集成指南:基于 Let's Encrypt 的自动化证书签发与续期 CAS 服务器内置了对 ACME(Automatic Certific
后端认证鉴权单点登录acme-companion 的 Let's Encrypt / ACME 证书自动化指南:ACME_HOST 驱动签发、DNS-01 挑战与智能续期全解
acme companion 的 Let's Encrypt / ACME 证书自动化指南:ACME_HOST 驱动签发、DNS 01 挑战与智能续期全解 本指
云原生运维
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考