TLS 与证书
中继自行终止 TLS,并且只使用 TLS 1.3:没有 TLS 1.2,没有 0-RTT,普通连接上没有 ALPN,版本不可配置。
TLS 必须在中继处终止。 协议第 2 版把每个客户端的密钥证明以及中继的身份签名,都绑定到客户端与中继之间的 TLS 会话上。在中继前终止 TLS 的反向代理、负载均衡器或 CDN 会让所有连接失败。支持以下三种部署方式:
| 部署方式 | 证书 | 说明位置 |
|---|---|---|
| A. 直连,自动证书(推荐) | 内置 Let’s Encrypt | 本页、快速入门 |
| B. 直连,静态证书 | 您自己的文件,收到 SIGHUP 时重新加载 | 本页 |
| C. 位于带 PROXY 协议 v2 的 TCP(第 4 层)透传之后 | 中继自己的(A 或 B) | TCP 前端部署 |
您需要配置且只配置一个证书来源:
| 来源 | 配置节 | 80 端口 | 续期 |
|---|---|---|---|
| 内置 Let’s Encrypt(推荐) | [server.acme] | 不使用 | 自动,无需重启 |
| 磁盘上的静态证书 | [server.tls] | 仅当您的证书工具需要时 | 由您续期,然后执行 systemctl reload |
服务器安全失败
Section titled “服务器安全失败”以下情况下服务器拒绝启动:
- 配置文件缺失、不可读或无效;
- 既没有配置
[server.acme](且enabled = true)也没有配置[server.tls],并且没有使用--no-tls; - 两者都配置了;
- 无法加载静态证书或私钥,或私钥与证书不匹配;
- ACME 设置无效:没有域名、通配符或格式错误的域名、无效的联系人、相对路径的
cache_dir、cache_dir是符号链接,或无法创建缓存目录或设置其权限; tls_handshake_timeout_secs不在 1–120 之间;- 使用
--no-tls但没有设置[server] dev_origin; --no-tls与非回环地址同时使用,且没有--insecure-public-plaintext。
绝不会静默回退到明文。
内置 Let’s Encrypt(ACME)
Section titled “内置 Let’s Encrypt(ACME)”中继自行申请和续期证书,在正常监听端口上应答 TLS-ALPN-01 质询。
- 每个域名都有指向本主机的公网 DNS
A或AAAA记录。 - 公网 TCP 443 端口直达中继。Let’s Encrypt 只在 443 端口验证 TLS-ALPN-01。普通端口转发(iptables
DNAT、pfrdr、TCP 模式的负载均衡器)可以;任何在中继前终止 TLS 的东西都会导致验证失败。 - 一个持久、可写的缓存目录。
如果 bind_addr 不在 443 端口,中继仍会启动,但会警告:
ACME: listening on port 8443, but Let's Encrypt validates TLS-ALPN-01 on port 443 only. Make sure public :443 is forwarded to this listener without TLS termination in between.[server]bind_addr = "0.0.0.0:443"
[server.acme]enabled = truedomains = ["chat.example.com"]cache_dir = "/var/lib/rchat/acme"staging = false| 配置项 | 类型 | 默认值 | 含义 |
|---|---|---|---|
enabled | 布尔 | false | 启用 ACME。为 false 时忽略整个配置节。 |
domains | 字符串列表 | [] | 证书上的主机名,启用时必填。必须是小写的完整域名(字母、数字、连字符,最长 253 个字符),不能是通配符或 IP 地址,也不能带协议、端口或路径。 |
contacts | 字符串列表 | [] | 可选,ACME 账户的电子邮件地址。缺少 mailto: 时会自动添加,其他协议会被拒绝。 |
cache_dir | 路径 | /var/lib/rchat/acme | 存放 ACME 账户密钥和证书私钥。必须是绝对路径,不能是符号链接,并且必须在重启后保留。 |
staging | 布尔 | false | 使用 Let’s Encrypt 测试环境:证书不受信任,但速率限制宽松。首次部署时使用。 |
[server.acme] 中的未知配置项会在启动时被拒绝,因此像 email = ... 这样的拼写错误会让服务器停止,而不是被忽略。该配置节也可以写成顶层的 [acme]。
缓存目录。 中继以 0700 权限创建 cache_dir,并通过临时文件加原子重命名,以 0600 权限写入每个文件。如果目录或文件已经存在且权限较宽,会将其收紧,并记录 ACME: tightening permissions on ... to 0700。文件包括 account-<hash>.der(账户密钥)和 cert-<hash>.pem(证书私钥与证书链)。测试环境与生产环境的文件名不同,切换时不会互相覆盖。丢失缓存意味着要重新申请,会计入速率限制。
首张证书签发之前
Section titled “首张证书签发之前”缓存为空时,中继会记录:
WARN ACME: no certificate cached yet. TLS handshakes will FAIL until Let's Encrypt issues one (usually under a minute). Issuance needs public DNS for ["chat.example.com"] pointing here and port 443 reaching this listener directly.签发完成之前,所有 TLS 握手都会失败。 不存在临时证书或自签名证书。证书到达时:
INFO ACME: certificate issued and deployed; TLS is ready之后启动时,会从 cache_dir 加载已缓存的证书:
INFO ACME: cached certificate loaded; TLS is ready测试环境与生产环境
Section titled “测试环境与生产环境”- 测试环境(
staging = true)签发的证书不受客户端信任,但基本不用担心速率限制。中继会警告ACME: staging certificates are NOT trusted by clients; switch staging off once issuance works。失败后按 ACME 库自带的快速指数退避重试。 - 生产环境(
staging = false)。缓存为空的首次生产运行时,中继会提醒 Let’s Encrypt 每个主机名每小时只允许 5 次验证失败。失败重试的间隔至少 5 分钟,一次配置错误不会在几秒内耗尽这个额度。
请先使用测试环境。证书签发成功(颁发者包含 STAGING)后,设置 staging = false 并重启。
失败永远不会让服务器停止。每次失败记录一行日志,然后重试:
WARN ACME: certificate issuance failed, will retry with backoff (not before 300 s): <error from Let's Encrypt>续期失败时这一行写的是 renewal 而不是 issuance。测试环境下没有 (not before …) 部分。
第一次遇到速率限制时记录一次,重复的速率限制错误在下次成功之前都记录在 debug 级别:
WARN ACME: rate-limited by Let's Encrypt (retry after <time>). Retrying automatically with backoff; further rate-limit errors are logged at debug. See https://letsencrypt.org/docs/rate-limits/证书有效期过去三分之二后,ACME 客户端会自动续期。新证书在内存中替换:新的握手立即使用它,已有连接不受影响,不需要重启或重新加载。
INFO ACME: certificate renewed and deployed (no restart needed)在 ACME 模式下执行 systemctl reload 是无害的,中继会记录 SIGHUP: nothing to reload (no static certificate in use)。
正在使用的证书的到期时间会在加载时、每次部署或重新加载时以及每 6 小时检查一次,ACME 证书和静态证书都适用。
- 距到期不足 7 天:
WARN TLS certificate expires in N day(s). ...。对 ACME 来说,这意味着续期已经失败了数周。 - 已过期:
ERROR TLS certificate being served has EXPIRED; clients will reject it. ...
这个值也以 Prometheus 指标 rchat_tls_cert_not_after_seconds 导出。可以对 rchat_tls_cert_not_after_seconds - time() < 7 * 86400 设置告警(参见指标)。
缓存损坏时的恢复
Section titled “缓存损坏时的恢复”如果缓存文件损坏,失败日志后面会跟一条提示:
ACME: the cached ACME account key looks corrupt. Stop the server, delete account-*.der in cache_dir and start again; a new account is registered automatically.ACME: the cached certificate looks corrupt. Stop the server, delete cert-*.pem in cache_dir and start again; a new certificate is ordered (mind production rate limits).
质询如何共享 443 端口
Section titled “质询如何共享 443 端口”中继会先读取每个 TLS ClientHello,再决定如何应答。只提供一个 ALPN 协议 acme-tls/1(RFC 8737)的握手来自 Let’s Encrypt 的验证,中继用质询证书应答后关闭连接。其他所有握手(来自应用或浏览器)都使用正常的 TLS 1.3 配置。两条路径都只使用 TLS 1.3。
如果您已经在管理证书(企业 CA、certbot,或用于测试的私有 CA),使用这种方式。
[server]bind_addr = "0.0.0.0:443"
[server.tls]cert_path = "/etc/rchat/tls/cert.pem" # PEM,完整证书链,叶证书在前key_path = "/etc/rchat/tls/key.pem" # PEM 私钥:PKCS#8、PKCS#1 或 SEC1[server.tls] 只接受 cert_path 和 key_path。旧模板中的 min_version 和 tls13_only 从未被读取,现在会在启动时被拒绝:
[server.tls] `tls13_only` is not a setting: TLS 1.3 is mandatory and cannot be changed. Remove `tls13_only` from the config file.这两个文件必须能被 rchat 用户读取。启动时中继会记录 TLS 1.3 enabled with static certificate (SIGHUP reloads it)。
重新加载续期后的证书
Section titled “重新加载续期后的证书”中继收到 SIGHUP 时重新读取这两个文件,不会断开连接。附带的单元定义了 ExecReload=/bin/kill -HUP $MAINPID:
sudo systemctl reload rchat-server成功时记录 SIGHUP: TLS certificate reloaded。如果新文件不可读、格式错误或私钥不匹配,会继续使用旧证书,并记录:
ERROR SIGHUP: TLS certificate reload failed, keeping the old one: ...certbot
Section titled “certbot”如果更想用 certbot 而不是内置 ACME 客户端,可用 standalone 插件签发证书(签发期间需要 80 端口空闲),再复制到 rchat 用户可读的位置。请复制而不是建立符号链接,因为 /etc/letsencrypt/live 只有 root 能读:
sudo install -d -m 0750 -o root -g rchat /etc/rchat/tlssudo install -m 0644 /etc/letsencrypt/live/chat.example.com/fullchain.pem /etc/rchat/tls/fullchain.pemsudo install -m 0640 -g rchat /etc/letsencrypt/live/chat.example.com/privkey.pem /etc/rchat/tls/privkey.pem将 [server.tls] 指向 /etc/rchat/tls/fullchain.pem 和 /etc/rchat/tls/privkey.pem。续期时,添加部署钩子 /etc/letsencrypt/renewal-hooks/deploy/rchat-reload.sh(权限 0755),把新证书复制到位并重新加载中继:
#!/bin/shset -eumask 077install -m 0644 "$RENEWED_LINEAGE/fullchain.pem" /etc/rchat/tls/.fullchain.pem.newinstall -m 0640 -g rchat "$RENEWED_LINEAGE/privkey.pem" /etc/rchat/tls/.privkey.pem.newmv -f /etc/rchat/tls/.fullchain.pem.new /etc/rchat/tls/fullchain.pemmv -f /etc/rchat/tls/.privkey.pem.new /etc/rchat/tls/privkey.pemsystemctl reload rchat-server用 sudo certbot renew --dry-run 检查。
对于独占 443 端口的中继,内置 ACME 客户端更简单:不需要 80 端口、certbot 或钩子。
用于测试的私有 CA
Section titled “用于测试的私有 CA”在测试和内部网络中,可以使用自建私有 CA 签发的证书。不要把 CA 私钥放在中继上,并让每台客户端设备信任该 CA 证书。
本地开发用的明文模式(--no-tls)
Section titled “本地开发用的明文模式(--no-tls)”[server]bind_addr = "127.0.0.1:18443"dev_origin = "ws://127.0.0.1:18443"rchat-server --config dev.toml --init-relay-identity # 只需一次rchat-server --config dev.toml --no-tls-
它会忽略
[server.tls]和[server.acme]并给出警告,同时记录醒目的TLS DISABLED横幅。 -
它要求设置
[server] dev_origin,即客户端拨号使用的小写scheme://host:port。没有 TLS 就没有通道绑定,因此客户端的证明改为绑定到该源的哈希,这只能防止证明被重放到源不同的中继。 -
使用
--no-tls且没有--config时,如果/etc/rchat/server.toml不存在,本可使用内置默认值,但默认值中没有dev_origin,因此中继会拒绝启动。显式指定的--config文件不存在时总是报错。 -
它在非回环地址(
127.0.0.0/8和::1之外的任何地址)上拒绝启动:Error: --no-tls with non-loopback bind address 0.0.0.0:8443: refusing to serve plaintext (including circuit tokens) on the network. ...--insecure-public-plaintext可以覆盖这一点,但会把所有流量暴露在网络上,并在启动时记录错误。它没有任何正当的生产用途。
--no-tls 没有生产用途。位于终止 TLS 的代理之后的中继无法使用协议第 2 版;位于 TCP 透传之后时,中继保留自己的 TLS(参见 TCP 前端部署)。iOS 应用的发布版本拒绝 ws://,同一设备上的中继除外。
TLS 握手超时
Section titled “TLS 握手超时”tls_handshake_timeout_secs(默认 10,允许 1–120)限制客户端完成 TLS 握手的时间,超时的握手会被断开。
# TLS 1.3 成功openssl s_client -connect chat.example.com:443 -tls1_3 </dev/null 2>&1 | grep -E '^ *Protocol|New, TLS'
# TLS 1.2 被拒绝,提示 "alert protocol version"openssl s_client -connect chat.example.com:443 -tls1_2 </dev/null 2>&1 | grep -i alert
# 颁发者与有效期openssl s_client -connect chat.example.com:443 -servername chat.example.com </dev/null 2>/dev/null \ | openssl x509 -noout -issuer -dates| 日志消息 | 处理方法 |
|---|---|
no TLS configured. Add [server.acme] ... or [server.tls] ... | 添加一个证书来源,或在本地开发时使用 --no-tls。 |
both [server.tls] (static certificate) and [server.acme] (automatic certificates) are configured | 只保留一个。在 [server.acme] 中设置 enabled = false 也可以。 |
[server.acme] is enabled but `domains` is empty | 至少列出一个主机名。 |
[server.acme] domain "..." is invalid: ... | 使用小写 DNS 名称,不带通配符、协议、端口或路径。 |
[server.acme] contact "..." is not an email address | 使用 [email protected] 或 mailto:[email protected]。 |
[server.acme] cache_dir ... must be an absolute path | 例如使用 /var/lib/rchat/acme。 |
ACME cache_dir ... is a symlink; refusing to follow it | 将 cache_dir 指向真实目录。 |
cannot create ACME cache directory / cannot set 0700 on ACME cache directory | 目录属于其他用户,或不在单元允许写入的路径(/var/lib/rchat)内。 |
[server.tls] `tls13_only` is not a setting | 删除 min_version 和 tls13_only。 |
failed to load TLS certificate chain / failed to load TLS private key / TLS key ... does not match certificate ... | 检查路径、PEM 内容,以及 rchat 用户能否读取这两个文件。 |
--no-tls with non-loopback bind address | --no-tls 仅用于本地开发:绑定 127.0.0.1,或配置 TLS。 |
--no-tls requires [server] dev_origin | 在 [server] 下添加 dev_origin = "ws://127.0.0.1:<port>"(仅限开发)。 |
| 应用提示 “This server couldn’t prove its identity” | 中继前面有东西在终止 TLS。请使用 TCP 透传,或让中继自己占用 443 端口。 |
绑定 443 端口时 Permission denied | 使用附带的单元运行,它授予 CAP_NET_BIND_SERVICE。 |
握手失败,日志显示 no certificate cached yet | 首次签发尚未完成,请关注 ACME: 日志。 |
ACME: certificate issuance failed, will retry ... | 通常是 DNS 未指向本主机,或 443 端口没有直达中继(防火墙、代理、转发错误)。冒号后面是 Let’s Encrypt 返回的错误。 |
ACME: rate-limited by Let's Encrypt | 等待重试时间。用 staging = true 测试,并保持 cache_dir 持久。 |
TLS certificate expires in N day(s) / ... has EXPIRED | ACME:查看更早的 ACME: 警告。静态证书:续期后执行 systemctl reload。 |