别再手搓 ACME:把 Caddy 同款 CertMagic 嵌进 Go,一行接管自动 HTTPS
说明:源微信页常触发环境异常/验证码,无法稳定抓取全文。本文按题意「CertMagic / 一行代码自动 HTTPS」结合官方仓库
caddyserver/certmagicREADME、pkg.go.dev 文档与 ACME 工程实践深度改写,代码与配置均可对照官方接口验证。
很多 Go 小服务上线时,HTTPS 仍是一堆手工活:申请证书、拷到机器、改 Nginx、到期前再续一次。漏一次就把站点打回明文。Caddy 之所以“开箱即 HTTPS”,核心不是魔法配置文件,而是它真正在用的库——CertMagic。把同一套能力嵌进你自己的 http.Handler,常见场景里只需一行:
certmagic.HTTPS([]string{"example.com"}, mux)
这一行会帮你:向 ACME CA(默认 Let's Encrypt)签发/续期证书、做 HTTP→HTTPS 跳转、钉 OCSP、在证书生命周期内持续照看。域名 A/AAAA 指到你的机器后,它就能把连接维持在加密态。

一、它到底解决什么:从“证书文件”到“证书生命周期”
传统做法把证书当成静态资产:磁盘上的 .pem + 定时任务。一旦服务水平扩展、多实例共享、或域名经常增减,静态资产很快崩盘——每台机器各续一份,很容易触发 CA 速率限制;证书吊销后还要人工替换。
CertMagic 的模型是:把“获得证书 / 续期 / 挑战应答 / OCSP 装订 / 集群协同”收成可嵌入的库。它是 Caddy 自动化 TLS 代码抽出来的独立包,底层 ACME 客户端是 ACMEz。换言之,你拿到的不是 demo 级 client,而是已经在大量站点上跑过多年的那一套。

相对其他 Go ACME 库,官方 README 强调的差异点包括:
| 能力 | CertMagic 的做法 | 工程含义 |
|---|---|---|
| 挑战类型 | HTTP-01 / TLS-ALPN-01 / DNS-01 全覆盖,可随机轮换 | 单一挑战被 CA 下线时不易整站挂掉 |
| 错误与重试 | 指数退避,最长可持续重试约 30 天 | 短暂网络/CA 抖动不立刻放弃 |
| OCSP | 正确装订,并落盘应对响应方故障;吊销时可尝试换证 | 隐私与在线率更稳 |
| 集群 | 同 Storage 即同集群,主动锁 + 队列 | 负载均衡后多实例不重复刷证 |
| On-Demand TLS | 握手期按 SNI 按需签发(需决策函数限流) | 多租户 / 未知域名场景可落地 |
| 多 Issuer | 可挂多个 CA 提升韧性 | 单 CA 故障可降级 |
使用前硬前提:你控制的公网域名;机器可从公网触达(或改用 DNS-01);持久化存储可写。域名未指向服务器就开生产 CA,几乎必踩速率限制。
二、五分钟接入:从明文 Listen 到自动 HTTPS
安装:
go get github.com/caddyserver/certmagic
要求 Go 1.21+。先按最佳实践改默认配置,再调用高层 API:
package main
import (
"fmt"
"net/http"
"github.com/caddyserver/certmagic"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello over managed HTTPS")
})
// 阅读并同意 CA 条款;务必填邮箱(到期与异常可联系)
certmagic.DefaultACME.Agreed = true
certmagic.DefaultACME.Email = "you@example.com"
// 开发期用 Staging,避免锁死生产额度
certmagic.DefaultACME.CA = certmagic.LetsEncryptStagingCA
err := certmagic.HTTPS([]string{"example.com", "www.example.com"}, mux)
if err != nil {
panic(err)
}
}
HTTPS() 会同时起 HTTP/HTTPS 监听并把明文请求跳到 HTTPS。同级还有:
certmagic.Listen(names):只要 TLS listener;certmagic.TLS(names):返回可嵌入的*tls.Config;ManageSync/ManageAsync:只管理证书,服务器自己开。

上线前把 CA 切回生产端点(默认即生产 Let's Encrypt)。Staging 证书不被浏览器信任,只适合联调挑战链路。
三、三种 ACME 挑战:选错就等于续不上证
挑战是向 CA 证明“你控制这个名字”。CertMagic 默认启用 HTTP 与 TLS-ALPN;DNS 适合内网、防火墙挡低端口、或要签通配符的场景。

1. HTTP-01(80 端口)
CA 访问 http://你的域名/.well-known/acme-challenge/...,你的服务必须返回约定内容。若你已经有 HTTP Server,包一层即可:
http.ListenAndServe(":80", myACME.HTTPChallengeHandler(mux))
或在 ServeHTTP 里:
if myACME.HandleHTTPChallenge(w, r) {
return
}
没有 HTTP 服务时,要么单独起一个只解挑战的 80,要么关掉 HTTP 挑战改用 DNS。
2. TLS-ALPN-01(443 端口)
利用 TLS ALPN 扩展在握手期应答。最省事的方式是直接用库给出的配置:
tlsConfig := magic.TLSConfig()
ln, err := tls.Listen("tcp", ":443", tlsConfig)
或给已有 tls.Config 挂上:
myTLSConfig.GetCertificate = magic.GetCertificate
myTLSConfig.NextProtos = append(myTLSConfig.NextProtos, acmez.ACMETLS1Protocol)
3. DNS-01(通配符刚需)
不要求机器对公网开放 80/443,但需要 DNS 供应商 API。CertMagic 对接 libdns 生态。以 Cloudflare 为例(切勿把真实 Token 写进仓库):
import "github.com/libdns/cloudflare"
certmagic.DefaultACME.DNS01Solver = &certmagic.DNS01Solver{
DNSManager: certmagic.DNSManager{
DNSProvider: &cloudflare.Provider{
APIToken: os.Getenv("CF_API_TOKEN"),
},
},
}
启用 DNS-01 后,该 ACMEIssuer 实例会以 DNS 为主路径。Let's Encrypt 签发通配符(*.example.com)目前仍依赖 DNS 挑战。
排坑:多挑战同时开时,库会随机选择并在失败后轮换——这是有意设计,避免“只依赖一种挑战”的历史翻车(如当年 TLS-SNI 被下线)。
四、进阶:自定义 Config、集群共享与 On-Demand
高层函数全部走 certmagic.Default。需要按名字差异化策略时,自己建 Cache + Config:
var cache *certmagic.Cache
cache = certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {
return certmagic.New(cache, certmagic.Config{}), nil
},
})
magic := certmagic.New(cache, certmagic.Config{})
issuer := certmagic.NewACMEIssuer(magic, certmagic.ACMEIssuer{
CA: certmagic.LetsEncryptStagingCA,
Email: "you@example.com",
Agreed: true,
})
magic.Issuers = []certmagic.Issuer{issuer}
if err := magic.ManageSync(context.Background(), []string{"example.com"}); err != nil {
return err
}
tlsConfig := magic.TLSConfig()
集群:同 Storage = 同舰队
默认把资产写到 $HOME/.local/share/certmagic(尊重 $XDG_DATA_HOME)。多实例只要挂同一块共享存储(NFS、对象存储适配、或自实现 Storage 接口),就会共享证书与锁,避免 100 个 Pod 各自向 CA 申请同一张证。
On-Demand TLS:握手时再签发
域名事先不全知道时:
certmagic.Default.OnDemand = &certmagic.OnDemandConfig{
DecisionFunc: func(name string) error {
if name != "app.example.com" && name != "api.example.com" {
return fmt.Errorf("name not allowed: %s", name)
}
return nil
},
}
务必用决策函数白名单/限流,否则任意 SNI 都能把你的存储与 CA 配额打爆。高层 API 若传入固定域名列表,会自动把这些名字放进隐式白名单。
速率与开发纪律
- 库内默认约 每分钟 10 次获得/续期事件(可调,但别为了图快关掉伦理边界)。
- CA 还有自己的硬限制;开发期死磕生产端点,可能锁号数天。
- 邮箱强烈建议填写:到期提醒、CA 异常联系都靠它。
五、实战清单与边界警告
推荐落地顺序
1. Staging CA 跑通挑战(先 HTTP 或 TLS-ALPN 其一);
2. 确认 Storage 可写、进程重启后证书仍在;
3. 再切生产 CA;
4. 多副本时统一 Storage;
5. 需要 *.domain 再上 DNS-01。
Linux 绑 80/443 不必 root:
sudo setcap cap_net_bind_service=+ep /path/to/your/binary
可以混用自管证书:CacheUnmanagedCertificatePEMFile 等 API 可把已有 PEM 塞进缓存;它们不会被自动续期,但符合条件时仍会做 OCSP 装订。
不要做的事
- 在无 DecisionFunc 的情况下对公网开放 On-Demand;
- 把 DNS API Token / ZeroSSL API Key 写进 Git;
- 用 Staging 证书当生产信任锚;
- 在域名未解析时反复打生产 CA。
配套最小可编译示例见同目录 practice/main.go(默认 Staging,占位域名,仅演示 API 形状)。
六、小结
CertMagic 把“自动 HTTPS”从运维剧本变成了 Go 程序内的库调用:简单场景一行 HTTPS(),复杂场景用 Config + 多挑战 + 共享 Storage + On-Demand 决策函数。它不是又一个玩具 ACME client,而是 Caddy 正在使用的那套成熟实现。下次给你的内部工具或边缘小服务上 TLS 时,优先考虑嵌库,而不是再抄一份 cron + certbot 脚本。
官方入口:
- GitHub:
https://github.com/caddyserver/certmagic - 文档:
https://pkg.go.dev/github.com/caddyserver/certmagic
💻 配套实训环境与动手练习
本文涉及的相关技术指令、开发环境与工具链已内置在边学边练在线实验室中,无需繁琐安装配置,随时在浏览器中实践体验: