别再手搓 ACME:把 Caddy 同款 CertMagic 嵌进 Go,一行接管自动 HTTPS

别再手搓 ACME:把 Caddy 同款 CertMagic 嵌进 Go,一行接管自动 HTTPS

别再手搓 ACME:把 Caddy 同款 CertMagic 嵌进 Go,一行接管自动 HTTPS

说明:源微信页常触发环境异常/验证码,无法稳定抓取全文。本文按题意「CertMagic / 一行代码自动 HTTPS」结合官方仓库 caddyserver/certmagic README、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 指到你的机器后,它就能把连接维持在加密态。

CertMagic 自动 HTTPS 全景


一、它到底解决什么:从“证书文件”到“证书生命周期”

传统做法把证书当成静态资产:磁盘上的 .pem + 定时任务。一旦服务水平扩展、多实例共享、或域名经常增减,静态资产很快崩盘——每台机器各续一份,很容易触发 CA 速率限制;证书吊销后还要人工替换。

CertMagic 的模型是:把“获得证书 / 续期 / 挑战应答 / OCSP 装订 / 集群协同”收成可嵌入的库。它是 Caddy 自动化 TLS 代码抽出来的独立包,底层 ACME 客户端是 ACMEz。换言之,你拿到的不是 demo 级 client,而是已经在大量站点上跑过多年的那一套。

别再手搓 ACME:把 Caddy 同款 CertMagic 嵌进 Go,一行接管自动 HTTPS

相对其他 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:只管理证书,服务器自己开。

Mermaid Diagram

上线前把 CA 切回生产端点(默认即生产 Let's Encrypt)。Staging 证书不被浏览器信任,只适合联调挑战链路。


三、三种 ACME 挑战:选错就等于续不上证

挑战是向 CA 证明“你控制这个名字”。CertMagic 默认启用 HTTP 与 TLS-ALPN;DNS 适合内网、防火墙挡低端口、或要签通配符的场景。

三种 ACME 挑战选型

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

💻 配套实训环境与动手练习

本文涉及的相关技术指令、开发环境与工具链已内置在边学边练在线实验室中,无需繁琐安装配置,随时在浏览器中实践体验:

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有

最新文章

热门文章

本栏目文章