ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Caddy自动HTTPS实战:告别手动续期SSL证书的烦恼

Caddy自动HTTPS实战:告别手动续期SSL证书的烦恼 我记得很清楚上个月帮一个老同学迁移他的博客服务器。他打开手机日历给我看下周三SSL 证书到期这是他今年第三次设提醒。我说你不如直接把服务器换成 Caddy他先是愣住然后问我这跟证书有什么关系关系太大了——Caddy 这个在 GitHub 上 75K Star 的 Web 服务器最大的招牌就是让 HTTPS 变成默认行为证书的申请、部署、续期全部自动完成你几乎感觉不到它的存在。这篇文章不是 Caddy 的官方文档翻译是我自己从手动配置 Nginx Certbot、再逐步迁到 Caddy 后的一些真实体验。如果你也受够了手动处理 SSL 证书或者刚被某个ssl证书过期的线上事故折腾过又或者只是想知道http和https的区别之外还能不能有更省心的 Web 服务器方案那这篇应该对你有用。我会把 Caddy 的自动 HTTPS 原理、实际配置、内网开发场景以及我踩过的一些坑都讲清楚。1. 手动续证书到底烦在哪申请、验证、部署、续期四条链路拆开看1.1 传统的证书流程为什么总是让人焦虑先说个背景。在没有自动化工具之前一个 Web 服务器要上 HTTPS流程基本是这样生成私钥和证书签名请求CSR也就是那个.csr文件。去证书颁发机构CA提交申请比如 Lets Encrypt、阿里云 SSL 免费证书或者其他商业 CA。等待域名验证通过。验证方式要么是在域名解析里加一条 TXT 记录要么是在网站根目录放一个指定文件。下载证书文件常见是.crt、.pem、.key。这里最容易出问题因为同一张证书可能包含域名证书、中间证书、根证书三段链很多人搞不清到底该把哪个内容填进ssl_certificate。把文件传到服务器配置 Nginx 或 Apache 的证书路径然后重载服务。设置定时任务自动续期通常是certbot renew配合 cron。每过几个月手动检查一次过期时间命令一般是openssl x509 -enddate -noout -in your.crt或者到证书管理后台看。你有没有发现这里面每一步都有可能导致线上事故。生成 CSR 时私钥权限没设好部署后 Nginx 起不来验证域名时 DNS 解析没生效申请一直卡在 pending续期脚本的 cron 路径不对结果证书悄悄过期等到用户截图报错才发现。更隐蔽的问题是中间证书链有些 CA 给你的是两个 PEM 文件你得先cat拼接再填进配置不然安卓手机上会一片红。我自己就经历过一次SSL连接错误的故障查了半天最后发现是续期脚本更新了证书文件但 Nginx 的进程还缓存着旧的 OCSP 响应必须 reload 才生效。这种问题不难解决但它会消耗你本不该消耗的精力。1.2 自动化的意义不是省事而是减少人为失误这个变量很多人觉得证书自动化不就是少敲几条命令吗我用下来最大的感受是自动化的核心价值不是省事而是把人记性不好这个变量从系统里拿掉。证书过期是最典型的例子。Lets Encrypt 证书有效期是 90 天很多人设了 cron 后以为一劳永逸结果 cron 里 certbot 路径写错了或者 Python 版本升级后 certbot 崩溃证书就这么在你看不见的地方过期了。还有更常见的场景你申请证书的时候用的是一台服务器后来迁移到另一台只拷了.key和.crt忘了迁移续期用的账号配置和 challenge 目录新机器上证书照样过期。Caddy 的做法是把证书生命周期整个收进自己的进程里启动时检查现有证书不够 30 天有效期就自动续期续完自动重载重载失败还能回滚到旧证书。你不需要写 cron不需要关心 certbot 装在哪甚至不需要知道证书文件存在哪个目录。对多数中小站点来说这种默认值就是正确值的设计比任何文档都管用。1.3 说点大实话不是所有项目都需要自动化不过在推荐自动化之前我也得说句公道话如果你的场景是每年只需要配一次证书、网站访问量极小、也没有多域名需求那手动跑一次 certbot 也不是不行。自动化带来的复杂度在于你引入了一个更「黑盒」的系统一旦它出问题排查起来可能比手动流程更吃力。所以我的判断标准是只要你的站点数量超过两个或者域名有子域名扩展的可能或者你经常在本地和服务器之间切换环境那 Caddy 这类自动化 Web 服务器就值得上。它的配置成本几乎为零后面省下的时间却是指数级的。2. Caddy 的自动 HTTPS 为什么靠谱ACME 客户端与证书生命周期管理2.1 Caddy 的定位不是普通反代而是默认安全的 Web 服务器Caddy 的自我介绍通常是一句话Web 服务器自动 HTTPS。很多人第一反应是又一个 Nginx 替代品但这个定位其实说小了。Nginx 可以看作是什么都能干的高性能发动机配置自由度极高但也正因如此HTTPS 只是它众多配件里的一个需要你自己组装。Caddy 则相反它把安全做成出厂默认最常见的使用方式就是你只写清楚哪个域名对应哪个目录或后端剩下的 80 端口、443 端口、证书、跳转、协议版本它自己全搞定。举两个对比例子。Nginx 里你想要一个纯静态站点的 HTTPS至少要配 server 块、listen 443、ssl_certificate、ssl_certificate_key、ssl_protocols、ssl_ciphers、location 根目录、try_files还得单独处理 80 端口到 443 的跳转。而 Caddy 的配置长这样example.com { root * /var/www/example file_server }你甚至不用写tls指令。Caddy 看到example.com这个域名会自动向证书颁发机构申请证书并帮你把 80 端口的所有请求 301 到 443。这里面涉及http和https的区别中最核心的一点前者是明文传输后者在 TLS 握手后建立加密通道。绝大多数业务并不需要自己处理加密细节Caddy 的价值就是把这个通道变成默认项。2.2 ACME 协议与挑战方式Caddy 是怎么证明域名是你的要理解 Caddy 自动 HTTPS必须先理解它使用的 ACME 协议。ACME 是自动化证书管理环境的缩写Lets Encrypt 是它最著名的公共 CA。整个流程可以简化成三步Caddy 生成一对公私钥并把公钥、域名信息打包成 CSR。Caddy 向 CA 证明你拥有该域名这一步叫挑战。CA 验证通过后签发证书Caddy 自动保存并加载。Caddy 支持三种主流的挑战方式HTTP-01、TLS-ALPN-01、DNS-01。HTTP-01 是最常见的CA 会访问http://你的域名/.well-known/acme-challenge/xxxCaddy 需要在 80 端口响应这个请求。这意味着 Caddy 必须监听 80 端口防火墙不能挡。TLS-ALPN-01 是在 443 端口上通过 TLS 握手附带一个特殊的 ALPN 协议来验证适合只能用 443 端口的环境。DNS-01 是给你要验证的域名添加一条 TXT 记录。这种方式不需要开放 80 或 443 端口也支持通配符域名*.example.com。Caddy 里配置 DNS 挑战通常需要装对应的 DNS 插件因为证书申请服务需要通过 API 帮你自动添加解析记录。默认情况下Caddy 会依次尝试 HTTP-01 和 TLS-ALPN-01这两种都不需要你干预。只有当你的服务器没有公网 80/443 访问权限或者需要通配符证书时才需要去折腾 DNS 插件。2.3 生命周期管理证书续期、OCSP 与自动重载证书不是申请下来就完事了。Caddy 内部有一个证书管理器会定期检查已加载证书的有效期。按照我的实测它在证书剩余 30 天左右就会触发自动续期。续期成功后Caddy 在线更新证书不需要重启进程也不会中断现有的连接。还有一个很多人没注意的细节OCSP。OCSP 是用于实时校验证书吊销状态的一个协议。Caddy 会自动查询证书的 OCSP 响应并缓存到本地。如果你的证书 CA 的 OCSP 服务出问题Caddy 会保留上一次有效的 OCSP 响应而不是直接拒绝用户访问这个策略很务实。相比之下手动配置 Nginx Certbot 时ssl_stapling、ssl_stapling_verify这些参数经常被忽略。有些人甚至不知道 OCSP 是什么导致证书明明有效某些严格校验的客户端却报ssl错误排查起来非常玄学。Caddy 把这些都做了默认值所以会少很多这类疑难杂症。2.4 与 Nginx Certbot 的对比差距不在功能而在默认值我整理过一张对比表可以直观看出两者的差异项目Nginx CertbotCaddy 自动 HTTPS证书申请手动运行 certbot 或脚本首次加载域名时自动完成证书续期cron 定时调用 certbot内置调度器自动检查续期续期后生效需要 reload 或手动处理自动热加载HTTP 跳转 HTTPS手动配置 return 301默认开启HTTP/2、HTTP/3需单独配置默认支持中间证书链处理经常需要手动拼接自动处理本地自签证书手动 openssl 生成内置 CA 自动签发配置文件复杂度较长易出错几行即可这张表不是说 Nginx 不好。恰恰相反Nginx 允许你做非常精细的控制包括复杂的负载均衡策略、lua 扩展、权限控制等。但如果你只是想让 HTTPS 顺利跑起来Nginx 那套配置确实太重了。3. 实战 Caddyfile三行配置让 HTTPS 自动到起飞3.1 安装 Caddy 的几种姿势以及我推荐哪种Caddy 的安装有很多种覆盖 Linux、macOS、Windows、Docker。我个人比较推荐的方式是直接用官方静态二进制因为它依赖很少放到/usr/local/bin就能跑。以 Ubuntu/Debian 为例sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl curl -1sLf https://dl.cloudsmith.io/public/caddy/stable/gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg curl -1sLf https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt | sudo tee /etc/apt/sources.list.d/caddy-stable.list sudo apt update sudo apt install caddy装完以后systemd 会帮你注册caddy服务默认的配置目录在/etc/caddy/Caddyfile。如果是 Docker 环境官方镜像也很好用我经常这么起docker run -d \ -p 80:80 -p 443:443 \ -v $PWD/Caddyfile:/etc/caddy/Caddyfile \ -v caddy_data:/data \ -v caddy_config:/config \ caddy:latest注意一定要把/data目录持久化因为证书文件、ACME 账号私钥都存在这里。如果你忘了挂载这块容器每次重建都会重新申请证书容易触发 CA 的速率限制。3.2 最简静态站点域名解析好之后什么都不用管假设你在云服务商买了一个域名同时也买了一台服务器那么把这套配置放到 Caddyfile 里就能跑通 HTTPSblog.example.com { root * /home/user/www/blog file_server encode zstd gzip }把blog.example.com解析到服务器 IP 后执行caddy reloadCaddy 会立刻开始证书申请。第一次申请通常几秒钟就完成期间你可以看日志journalctl -u caddy -f正常情况下你会看到类似这样的日志成功获取证书、证书已保存、HTTP 端口自动跳转等。然后就可以直接用https://blog.example.com访问了。这里有个容易踩的坑如果域名解析还没生效或者服务器 80/443 端口被防火墙挡了Caddy 申请证书会失败日志里会提示 ACME challenge 无法完成。你要先确认dig blog.example.com返回的 IP 是这台服务器再检查云服务商的安全组规则。别问我为什么知道我第一台服务器就是这么折腾了半小时。3.3 反向代理场景给没有证书能力的应用自动套上 HTTPS更常见的场景是后端有一个自建服务比如 Node.js、Java、Gitea、GitLab 之类。这些应用本身能跑 HTTP但你要么不想在应用层折腾证书要么应用根本不支持。Caddy 可以一句话搞定git.example.com { reverse_proxy 127.0.0.1:3000 } api.example.com { reverse_proxy 127.0.0.1:8080 }Caddy 收到api.example.com的 HTTPS 请求后会把流量转发给本机8080端口。对于后端的 Node.js/Java 进程来说它永远只看到 HTTP 请求完全不知道 HTTPS 的存在所以不需要改任何代码。这就是反向代理的意义把 TLS 终止在最外层。这种方法对多应用共用一个服务器特别友好。你只需要维护 Caddy 一个入口按域名分流到不同端口每个域名都能自动获得独立的证书。3.4 验证是不是真的自动续期用命令确认证书状态很多人迁移过来后最大的疑问是它真的会自动续期吗你可以这么验证。第一步看证书文件caddy cert-info --domain blog.example.com这个命令会显示证书的签发时间、过期时间、发行人等信息。第二步用 openssl 直接从线上抓证书openssl s_client -connect blog.example.com:443 -servername blog.example.com /dev/null 2/dev/null | openssl x509 -noout -dates可以看到服务器正在使用的证书的notAfter时间。过两三个月再跑一次如果证书到期时间自动往后延了说明续期生效。我自己的站点跑了近一年证书只更新过两次完全没有人工干预。还有一个小技巧如果你想测试 Caddy 的续期逻辑可以把系统时间往后调几天最好在独立测试环境里或者在 Caddyfile 里临时设置tls指令的有效期阈值。但我不建议在生产环境做这种实验容易把自己吓一跳。4. 开发环境和内网 IP 的证书怎么搞Caddy 内置 CA 的完整玩法4.1 内网开发环境比生产环境更麻烦的原因很多人在本地开发时也会遇到SSL证书的困惑公网证书只能签域名不能签localhost更不能签192.168.1.100这种内网 IP。你要么在浏览器里点继续访问绕过证书警告要么自己生成自签证书并手动导入信任库整个过程非常繁琐。Caddy 对这类场景做了一个很聪明的处理当检测到localhost、内网 IP 或内网主机名时它不会去请求公共 CA而是使用自己内置的 CA 来签发证书。也就是说Caddy 内部会生成一个根证书然后基于这个根证书给本地服务签发证书。浏览器不信任这个根证书所以会报NET::ERR_CERT_AUTHORITY_INVALID或类似的错误这是正常现象因为根证书还没有加到系统信任库。4.2 把 Caddy 内置 CA 加入系统信任库Caddy 2.7 之后提供了一个非常好用的命令caddy trust这条命令会把 Caddy 的本地根证书安装到系统信任库。之后再用https://localhost访问浏览器就不会再有红色警告了。如果是开发团队多人协作还可以把你机器上生成的根证书导出给同事让大家统一信任同一个 CA这样整个团队本地环境都通用。具体操作如下先让 Caddy 启动一次确保它已经生成了内部 CA存储路径一般在/var/lib/caddy/.local/share/caddy/或~/.local/share/caddy/。找到root.crt或类似名称的 CA 证书。在开发机上执行caddy trust或者手动导入该证书到系统信任区。如果你用的是 macOS手动导入时记得设置为始终信任Windows 上则是导入到受信任的根证书颁发机构Linux 下需要执行update-ca-certificates。4.3 内网 IP 场景用 Caddy 服务局域网应用公司内网经常有这样的需求有一台开发服务器其他人要通过http://192.168.1.50:8080访问某个工具。如果你只想在这台服务器上统一暴露一个 HTTPS 端口Caddy 可以这么配192.168.1.50 { tls internal reverse_proxy 127.0.0.1:8080 }tls internal的意思是强制使用内置 CA 签发证书而不是尝试向公共 CA 申请。因为公共 CA 一般不会给内网 IP 签发证书。然后你把内网的 CA 根证书分发到每台客户端就能以https://192.168.1.50访问不会有警告。这里有个细节要注意Caddy 对纯 IP 地址的默认处理可能和我们以为的不一样。如果你不加tls internalCaddy 会尝试申请公共证书大概率失败。所以涉及 IP 或非公网域名时建议显式写上tls internal避免日志里刷一堆不明不白的报错。4.4 用本地域名代替 localhost 更顺手如果你开发时同时开了多个应用全是localhost:3000、localhost:8080会很难记。我习惯在本地系统 hosts 文件里加几条假的开发域名127.0.0.1 myapp.test 127.0.0.1 api.myapp.test然后在 Caddyfile 里写myapp.test, api.myapp.test { tls internal reverse_proxy 127.0.0.1:3000 }第一次用https://myapp.test访问并信任根证书后后面每次启动都是绿色锁。cookie 的 Secure 属性、Service Worker、CORS 这类对 HTTPS 有要求的浏览器特性也都能在本地正常调试。我之前用污染过的 http 协议调试一些接口经常遇到浏览器拦截 Cookie 的情况换成 HTTPS 后全部正常。5. 从 Nginx 迁到 Caddy 的排错手册5.1 端口冲突为什么 Caddy 起不来迁移最常遇到的第一个问题就是端口被占。如果你在 80 或 443 端口上还跑着 Nginx那 Caddy 启动会报address already in use。解决办法不是让两个服务同时监听 443而是先停掉旧的sudo systemctl stop nginx sudo systemctl disable nginx然后再启动 Caddy。有时候你明明停了 Nginx端口还是被占用可以用sudo lsof -i :443看到底是哪个进程占着端口。我遇到过云服务器上自带的一个 Web 管理面板占用了 443排查了很久才找到。如果生产环境不能立刻切换也可以让 Caddy 先监听其他端口测试example.com { reverse_proxy 127.0.0.1:8080 }然后在 Caddyfile 开头临时加上:8443来指定端口。但正式的自动 HTTPS 还是建议老老实实使用 443因为 ACME 挑战大概率还是要走这两个标准端口。5.2 no required ssl certificate was sent 到底是谁的问题迁移过程中很多人会在客户端连接时看到no required ssl certificate was sent或者SSL: certificate_verify_failed这类报错。这里要分两种情况。第一种是客户端要求服务端提供证书而服务端没给。这通常出现在设置了双向 TLS 的场景但如果你只是在做普通 HTTPS 访问那更可能是客户端和服务端的 TLS 配置不匹配。比如有些老客户端不支持 TLS 1.2 以上而 Caddy 默认启用了更严格的加密套件。这种情况下你可以在站点配置里临时放宽example.com { tls { protocols tls1.2 tls1.3 ciphers ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-RSA-AES128-GCM-SHA256 } }但我的建议是先用 openssl 探测一下到底是什么阶段失败的openssl s_client -connect example.com:443 -servername example.com /dev/null如果握手成功会输出证书链、session id、verify return code 等信息如果中断就看报错在哪一步。第二种情况是certificate_verify_failed意思是客户端在验证服务端证书链时失败。这通常是因为 Caddy 给出的证书链不完整或者客户端缺少对应的根证书。Caddy 默认会补全中间证书所以如果你用自签 CA 或者内部 CA那就要确保客户端信任对应的根证书。我在 Linux 服务器上遇到过好多次curl报证书验证失败而浏览器却正常原因就是系统里没有装根证书需要执行sudo apt install ca-certificates5.3 从 Nginx 迁到 Caddy 时证书文件和私钥怎么处理很多人担心一个问题我已经有 Nginx 用的证书了迁到 Caddy 是不是要重新申请其实不用。如果你之前在 Nginx 里已经配好了一个域名的.crt和.key文件Caddy 可以直接接管现有证书只要在 Caddyfile 里指定路径example.com { tls /etc/certs/example.crt /etc/certs/example.key root * /var/www/example file_server }这样 Caddy 会用这个现有证书完成 HTTPS 握手同时它可以继续用 ACME 自动续期吗这里要说明一下如果你显式指定了证书文件Caddy 会把这当成手动管理的证书不会再自动申请和续期。它只是单纯地加载这两个文件。所以更推荐的方式是直接让 Caddy 自动申请新证书不要纠结于旧证书。确认新证书申请成功后旧的.key和.crt文件就可以归档删除了。5.4 日志和 debug排错时最有用的三招Caddy 的默认日志比较克制但在排错时需要把日志级别调高。比较实用的做法是临时加上全局配置{ log { output stderr level DEBUG } }然后执行caddy reload --config /etc/caddy/Caddyfile journalctl -u caddy -f这样能看到每一步证书申请的细节包括 ACME 目录请求、挑战类型、CA 返回的错误。如果日志显示 ACCOUNT_THROTTLED 或 RATE_LIMITED说明证书申请太频繁需要等一段时间或者检查是不是/data目录没有持久化。第二招是直接用curl模拟请求curl -v https://example.com这个命令会打印 TLS 握手过程中的证书链以及最终请求响应。如果curl成功但浏览器不行常见原因是中间证书链顺序或完整性有问题可以考虑openssl s_client再次确认。第三招是检查/data/caddy/certificates目录。Caddy 会把申请的证书和账号信息都存在这里。如果你看到证书文件存在但站点配置不生效多半是配置文件里有其他tls配置覆盖了默认逻辑比如你写着tls off或者tls internal那自然不会有公共证书。5.5 迁移过程中的兼容性注意点最后说几个从 Nginx 迁过来时常被忽略的地方。第一是location规则。Nginx 的location /api、try_files这些写法在 Caddyfile 里不能直接照搬。Caddy 用的是路径匹配器和指令组合。比如example.com { handle /api/* { reverse_proxy 127.0.0.1:8080 } handle { root * /var/www/example file_server } }第二是 rewrite 规则。Nginx 里有大量rewrite ^/old/(.*)$ /new/$1 break;这种写法Caddy 使用命名匹配器加redir指令更直观但语法不同。第三是 WebSocket。如果你在反代一个带 WebSocket 的应用Caddy 不需要单独打开什么proxy_set_header Upgrade它默认就会处理必要的 Upgrade 头。这一点比 Nginx 还要省心因为 Nginx 需要你显式写proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade;Caddy 就不需要。第四是访问控制。Nginx 里你可能用allow/deny做 IP 白名单。Caddy 里可以用blocked匹配器blocked { remote_ip 192.168.1.2 } respond blocked 403语法不复杂但思路要跟着 Caddy 的匹配器模型走一遍。6. 我的最终建议什么项目值得切到 Caddy写了这么多最后说点个人经验。我现在的处理方式是新项目一律直接上 Caddy除非明确知道需要 Nginx 的某些高扩展性能力。老项目我一般建议按以下标准评估如果你的站点属于这几类那么 Caddy 可以无缝替代。一是博客、文档站、企业官网这类内容型站点Caddy 的file_server和自动 HTTPS 是绝配二是内部工具平台、API 网关、多应用反代反向代理一行搞定域名和 TLS三是个人 NAS、家庭宽带下的服务Caddy 在内网证书和端口复用上比 Nginx 省心得多。如果你的业务涉及非常复杂的负载均衡策略、基于 Lua 的运维脚本、或者需要和现有 Nginx 生态的模块深度集成那可以继续保留 Nginx但至少可以在边缘层加一个 Caddy 来处理证书和 HTTPS把 Nginx 专心用在业务逻辑上。还有一点Caddy 不止能当 Web 服务器它还有caddy file-server这样的一键静态文件服务器命令适合临时分享文件caddy file-server --domain files.example.com还有caddy reverse-proxy命令不想写配置文件的时候可以直接试caddy reverse-proxy --from example.com --to 127.0.0.1:8080这些命令对于快速验证一个想法非常方便。回到标题那句话HTTPS 自动起飞。我觉得 Caddy 真正做到的不只是把证书流程自动化而是把安全通信这件事从运维人员的负担清单里整个划掉了。我第一次在服务器上caddy start之后看着日志里自动生成的证书心里确实有点感慨原来 HTTPS 也可以这么安静。如果你现在还在为证书过期设日历提醒不妨抽一个下午把 Nginx 换成 Caddy 试试。你可能会发现以前那些关于证书的焦虑在换完配置后真的就消失了。
返回列表