群晖 NAS 支持 HTTPS 访问

2023-05-24 ⏳7.3分钟(2.9千字) 🕸️

朋友在我这托管了一个群晖设备,正好我也有中国电信的公网IP地址,就想着做一个公网可用的网络存储。公网访问肯定要上 HTTPS,不然密码可能会被别人截获。虽然群晖系统默认支持通过 Let’s Encrypt 申请免费 SSL 证书,却无法在国内网络使用。因为运营商会屏蔽443端口,导致无法验证域名控制权。今天分享基于 DNS API 的方案,绕过运营商的端口限制,为群晖设备开启 HTTPS 支持。

有两个问题要解决。首先是要验证域名控制权并获得签发的 DV SSL 证书。其次是自动将证书文件导入到群晖的 Synology 系统。

ACME 认证

基于 ACME 签发免费 SSL 证书需要验证域名控制权。验证控制权行业术语叫 challenge,有多种类型1

HTTP-01

这是最常用的一种,基于 HTTP 协议。首先给域名设置合适的 A/AAAA 记录,指向自己的服务器的 IP 地址。然后调用签发服务商 API 生成验证令牌,也叫 TOKEN。最后服务商通过访问如下链接验证域名所有权。

http://<YOUR_DOMAIN>/.well-known/acme-challenge/<TOKEN>

我们只需要修改 DNS,其他过程都有 ACME 客户端自动完成。

该方案简单易用。但缺点也很明显,依赖 80 端口。如果 ISP 运营商封掉这个端口就无法完成验证,也就不能签发证书😂

TLS-ALPN-01

考虑到 80 端口可能被封,IETF 又标准化了基于 TLS ALPN 的验证方式。它跟 HTTP-01 很像。但验证过程不再依赖请求某特定的 URL,而是利用 TLS 会话过程中的 ALPN 机制返回 TOKEN。

简单来说就是 ACME 客户端调用 API 生成 TOKEN,并监听 443 端口。然后服务端尝试建立 TLS 会话并通过 ALPN 机制发送 acme-tls/1,客户端收到后回复 TOKEN 完成验证。

TLS-ALPN-01 跟 HTTP-01 的本质区别是依赖的端口不一样,它使用标准 443 端口。

但 443 端口也会被封。而且有些设备需要部署 HTTPS,但不想让外部网络访问。于是就诞生了第三种方式。

DNS-01

DNS-01 说白了就是让客户端自动修改 DNS 记录,然后服务查询对应的记录来验证所有权。这需要 DNS 服务商开发接口。ACME 客户端调用证书服务商接口生成 TOKEN,然后将其写入如下 TXT 记录

TXT _acme-challenge.<YOUR_DOMAIN>. TOKEN...

然后证书服务会通过 DNS 查询该记录来完成验证。

这种方式最灵活,不依赖 80/443 等端口,甚至可以实现离线部署,非常方便。但缺点需要域名解析厂商提供 API 支持。好在主流的 DNS 解析服务商都支持,国内的也不例外,但不同的服务商调用方式略有不同,需要按需配置。

本文接下来要用的就是 DNS-01 这种方式。

签发证书

ACME 是开放标准,有很多实现。基本常见的编程语言都有对应的实现。但是在 Linux 下最方便的还是 Shell 脚本。所以我选用 acme.sh,这是纯 Shell 脚本实现的 ACME 客户端。最早支持通过 Let’s Encrypt 自动签发证书,现在默认使用 ZeroSSL 证书。

Let’s Encrypt 最出名,使用最广泛;ZeroSSL 可以签发完整的 ECC 证书,更紧凑。大家可以按需选用。

安装非常简便,可以直接执行下面命令:

curl https://get.acme.sh | sh -s email=my@example.com

注意把最后面的邮箱换成自己的地址,用于接收证书过期等信息。更多安装相关信息请参考官方文档2

然后就可以运行 acme.sh 命令了。我用的是 Cloudflare。打开 API Tokens3页面,

点击 Create Token,选择 Edit zone DNS 这个模版,在 Zone Resources 一栏中选择对应的域名 Zone。完成后会得到一串类似随机字符串的 API Token,请务必妥善保管。

acme.sh 通过环境变量 CF_Token 读取 API Token,所以需要事先设置。完整命令如下。

export CF_Token=XXXX
acme.sh --issue \
    --dns dns_cf \
    --keylength ec-384 \
    --ocsp-must-staple \
    -d example.com \
    -d '*.example.com'

这里使用--dns dns_cf表示使用 Cloudfare 的 API 进行 DNS-01 验证。 --keylength参数指定证书的类型长密钥的长度。这里使用长度为384位的椭圆曲线证书。

当前有 ECC 和 RSA 两种非对称加密体系。跟 RSA 相比,加密强度相同的情况下 ECC 的密钥更短,计算量更小。所以如果不是遗留系统,就尽量选择 ECC 证书。这里结出两种体验的密钥长度对比4

Security RSA Key Length Required ECC Key Length Required
80 1024 160-223
112 2048 224-255
128 3072 256-383
192 7680 384-511
256 15360 512+

单位:比特。

现在行业要求加密强度至少为 128 位,对应 RSA 密钥至少为 3072(通常是 4096),对应 ECC 密钥为 256 位。上例中指定长度为 384 位,对应 RSA 密钥长度为 7680,等效加密强度为 192 位,已经相当不错了。

ECC 虽然密钥比较短,有利于网络传输并减少 TLS 会话延迟。但也不是没有的缺点,有极少数旧设备或者软件可能不支持。acme.sh 支持的密钥类型和长度有:

--ocsp-must-staple参数比较有意思,展开说能写一篇文章。这里简单介绍一下。所有证书都有过期时间。但如果在有效期内出现密钥泄漏,那就得立刻吊销。浏览器在验证的时候就需要检查当前证书吊销状态,行业术语叫 Online Certificate Status Protocol,简称为 OCSP。OCSP 状态通过证书里携带的专用 HTTP 链接发布。因为是 HTTP 明文传输,所以状态信息会使用 CA 根密钥签名。浏览器可以根据 CA 的根证书(公钥)验证签名。

但是每次打开 HTTPS 网站都检查 OCSP 状态,这会严重拖慢网页的加载速度;不检查又不能及时排除已经吊销的证书。业界争议比较大,像 Google Chrome 根本不会检查,它有自己的状态检查机制;但像 Firefox 就会检查。

iOS 好像也会检查。之前 Let’s Encrypt 的 OCSP 域名的 CDN 节点被异常屏蔽,导致国内好多 App 启动时会卡住一段时间。也是因为这个 OCSP 检查。

有没有两全其美的办法呢?是有的,这就是 staple 技术。简单来说就是服务器在 TLS 握手时主动把 OCSP 信息发给浏览器。这样浏览器就不用单独查询了,一举两得。开启上面的参数就是为了让浏览器使用 staple 加载 OCSP。但这也需要在服务端做专门的配置,不然在某些浏览器中可能会出现无法打开网页的情况。

所以不管用什么 CA 签发证书,建议大家都配置好 stapling 功能。

最后的-d example.com参数则为指定域名。如果是泛域名可以使用星号。但不建议使用泛域名证书,如果有需要可以在一张证书里指定多个域名,也可以针对不同的域名签发单独的证书。尽量不把鸡蛋放在同一个篮子里。

执行成功后,证书相关文件会保存到~/.acme.sh/example.com目录:

.acme.sh/example.com_ecc/
├── ca.cer
├── fullchain.cer
├── example.com.cer
├── example.com.conf
├── example.com.csr
├── example.com.csr.conf
└── example.com.key

有了证书相关文件,接下来就要想办法导入到群晖系统。

导入证书

群晖并没有标准的 API 来上传或者更新 SSL 证书。Synology 系统内部使用 Nginx 做 HTTP 服务器。Nginx 配置 HTTPS 的资料有很多。但 Synology 系统中 Nginx 是通过工具自动生成的,支持修改下次就会被覆盖。所以我们要找到在哪里保存证书文件,以及要怎样才能重新构建配置文件。

这里我主要参考了这个 Gist5

保存证书的目录是/usr/syno/etc/certificate/_archive/。我的设备上有如下文件:

9Hvqxg  DEFAULT  INFO  SERVICES

这里的 9Hvqxg 目录保存默认的证书文件,也就是群晖自签名证书。DEFAULT 保存字符串,就是 9Hvqxg。SERVICES 保存当前有哪些服务使用 HTTPS 证书。INFO 里是个 JSON 对象,外层 key 是 9Hvqxg。

我们可以在该目录下创建新目录,比如 example.com,然后把证书文件复制到这里:

ca.pem  cert.pem  fullchain.pem  is_default_cert  privkey.pem

这里 cert.pem 域名证书,privkey.pem 保存证书私钥,名字不能错。

然后需要将 DEFAULT 和 INFO 中的 9Hvqxg 修改为 example.com,也就是跟新建的目录名对应。

最后重新生成配置并重启 Nginx 服务:

synow3tool --gen-all
synow3tool --nginx=reload

然后新证书就生效了。因为 ACME 自动签发的证书一般都是三个月有效,需要定时签发新的证书。在 Linux 下用 crontab 最合适。但 Synology 系统登录后无法执行 crontab -e 命令。所以我就是图形界面创建定时任务,好在它支持定时运行 bash 脚本,比较方便。

问题排查

我更新之后 File Station 不能用了,点击重新初始化就可以恢复。

另外,新的 HTTPS 网站在 Firefox 下无法正常展示,报如下错误:

Error code: MOZILLA_PKIX_ERROR_REQUIRED_TLS_FEATURE_MISSING

搜了一下发下是跟 staple 有关。Firefox 严格实现了 ocsp_must_staple 特性,如果证书里有要求而服务端在 TLS 握手时没有返回 OCSP 信息话就会报错。

网上大片的资料都是说要 Firefox 关闭 security.ssl.enable_ocsp_must_staple,但我建议大家还是修改 Nginx 的配置正常开启 ocsp_staple 特性6

我们需要在/etc/nginx/conf.d/创建新配置文件,比如ssl.stapling.conf,内容为:

ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /usr/syno/etc/certificate/_archive/lehu/ca.pem;

最后的ssl_trusted_certificate指定 CA 根证书的路径。完整说明请参考 Nginx 官方网站7

更新加载配置后通过 openssl 验证 stapling 特性:

openssl s_client -connect [yoursite.com]:yourport -status

如果系统输出中饮食 OCSP Response Status: successful (0x0) 则证明已经正常开启 stapling 特性。

这就解决了 Firefox 无法加载的问题。

总结

以上就是本文的全部内容,希望大家都能在家庭网络中吃上标准 HTTPS 特性。


  1. https://letsencrypt.org/docs/challenge-types/↩︎

  2. https://github.com/acmesh-official/acme.sh↩︎

  3. https://dash.cloudflare.com/profile/api-tokens↩︎

  4. https://sectigostore.com/blog/ecdsa-vs-rsa-everything-you-need-to-know/↩︎

  5. https://gist.github.com/catchdave/69854624a21ac75194706ec20ca61327↩︎

  6. 试验发现,无论如何配置,Firefox 下都有概率报错,大家可以考虑关闭 stapling 😂↩︎

  7. http://nginx.org/patches/attic/ocsp-stapling/README.txt↩︎