logo
0
0
WeChat Login
refactor: 重构按需证书策略接口

CertMagic

Go 语言 ACME 客户端库 — 自动化 TLS 证书管理

GoDoc Tests

概述

CertMagic 是 Go 语言中最成熟、最健壮的 ACME(自动化证书管理环境)客户端集成库。通过 CertMagic,只需一行代码即可为 Go 应用启用 HTTPS 服务,无需手动管理证书。

不像这样:

// 明文 HTTP http.ListenAndServe(":80", mux)

使用 CertMagic:

// 加密 HTTPS,自动 HTTP->HTTPS 重定向 certmagic.HTTPS([]string{"example.com"}, mux)

这一行代码即可通过 HTTPS 提供 HTTP 路由服务,自动获取和续期证书、staple OCSP 响应。只要域名指向服务器,CertMagic 就会保持连接安全。

功能特性

  • 全自动证书管理:包括签发和续期
  • ACME 协议完整支持:支持 RFC 8555
  • 三种 ACME 挑战方式
    • HTTP-01 挑战
    • TLS-ALPN-01 挑战
    • DNS-01 挑战
  • 多证书颁发机构支持:支持 Let's Encrypt、ZeroSSL、Google Trust Services 等
  • 通配符证书支持
  • On-Demand TLS:在 TLS 握手期间动态签发证书
  • OCSP Stapling:正确的 OCSP stapling 实现,自动续期和替换已撤销证书
  • 分布式挑战解决:支持负载均衡器后运行,集群环境共享证书
  • ACME Renewal Information (ARI):支持 RFC 9773
  • 事件钩子:支持观察和监控证书操作
  • 可插拔存储:默认使用文件系统,支持自定义存储后端
  • 可插拔密钥源:支持自定义密钥生成方式
  • 客户端证书支持:支持 mTLS 客户端证书管理
  • 多颁发机构策略:支持 UseFirstIssuerUseFirstRandomIssuer
  • 主题名转换:支持将域名转换为通配符证书

系统要求

  1. ACME 服务器(公开可信 CA 或自建)
  2. 你控制的公共 DNS 域名
  3. 服务器可从公共互联网访问(或使用 DNS 挑战)
  4. 控制端口 80 (HTTP) 和/或 443 (HTTPS)
    • 可转发到其他端口
    • 或使用 DNS 挑战来免除此要求
  5. 持久化存储(默认使用本地文件系统)
  6. Go 1.21 或更高版本

重要:使用本库前,你的域名必须已指向服务器(A/AAAA 记录),除非使用 DNS 挑战!

安装

go get github.com/caddyserver/certmagic

快速开始

最简用法

err := certmagic.HTTPS([]string{"example.com", "www.example.com"}, mux) if err != nil { // 处理错误 }

启动 TLS 监听器

ln, err := certmagic.Listen([]string{"example.com"}) if err != nil { // 处理错误 }

获取 tls.Config

tlsConfig, err := certmagic.TLS([]string{"example.com"}) if err != nil { // 处理错误 } // 如需自定义 NextProtos,例如支持 h2: tlsConfig.NextProtos = append([]string{"h2", "http/1.1"}, tlsConfig.NextProtos...)

配置 ACME 证书颁发机构

// 推荐:设置默认 ACME 配置 certmagic.DefaultACME.Agreed = true // 同意 CA 服务条款 certmagic.DefaultACME.Email = "you@yours.com" // 提供邮箱 certmagic.DefaultACME.CA = certmagic.LetsEncryptStagingCA // 使用 staging 环境(开发时) // 生产环境使用: // certmagic.DefaultACME.CA = certmagic.LetsEncryptProductionCA

核心概念

Config

certmagic.Config 是配置证书管理器的核心结构。空配置无效,需使用 New()NewDefault() 获取有效配置。

// 使用默认配置 cfg := certmagic.NewDefault() // 自定义配置 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

Issuer 是证书颁发机构的抽象,支持多种实现:

  • ACMEIssuer:使用 ACME 协议(Let's Encrypt 等)
  • ZeroSSLIssuer:使用 ZeroSSL HTTP API
// 创建 ACME Issuer myACME := certmagic.NewACMEIssuer(magic, certmagic.ACMEIssuer{ CA: certmagic.LetsEncryptStagingCA, Email: "you@yours.com", Agreed: true, }) magic.Issuers = []certmagic.Issuer{myACME}

Cache

Cache 是内存中的证书缓存,索引证书以快速访问。

cache := certmagic.NewCache(certmagic.CacheOptions{ GetConfigForCert: myConfigGetter, })

Storage

Storage 是持久化存储接口,默认使用文件系统($HOME/.local/share/certmagic)。

certmagic.Default.Storage = &myCustomStorage{} // 实现 certmagic.Storage 接口

ACME 挑战

HTTP-01 挑战

需要 80 端口。通过在特殊端点提供特定 HTTP 响应来验证域名控制权。

// 使用标准库 mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "我的 HTTPS 网站!") }) http.ListenAndServe(":80", myACME.HTTPChallengeHandler(mux))

TLS-ALPN-01 挑战

需要 443 端口。使用 TLS 扩展(ALPN)提供特殊值进行验证。

// 使用 magic.TLSConfig() 配置 TLS 监听器 tlsConfig := magic.TLSConfig()

DNS-01 挑战

最灵活的挑战方式,无需服务器公网访问,也是 Let's Encrypt 签发通配符证书的唯一方式。

import "github.com/libdns/cloudflare" certmagic.DefaultACME.DNS01Solver = &certmagic.DNS01Solver{ DNSManager: certmagic.DNSManager{ DNSProvider: &cloudflare.Provider{ APIToken: "topsecret", }, }, }

On-Demand TLS

不知道所有域名或不想预先管理所有证书时,On-Demand TLS 非常有用。 CertMagic 会在 TLS 握手时读取 SNI,按策略:

  1. 尝试从内存缓存命中证书
  2. 尝试从持久化存储加载 exact / wildcard 证书
  3. 在策略允许时现场申请新证书

生产环境建议始终显式配置策略,避免任意 SNI 触发证书申请。

最简启用

certmagic.Default.OnDemand = certmagic.OnDemandPolicy(nil)

如需同时接入外部 Manager,可直接作为变参传入:

certmagic.Default.OnDemand = certmagic.OnDemandPolicy(nil, myManager)

自定义策略函数

certmagic.Default.OnDemand = certmagic.OnDemandPolicy( func(ctx context.Context, serverName string) (certmagic.DomainPolicy, error) { if serverName != "example.com" { return certmagic.DomainPolicy{}, fmt.Errorf("not allowed") } return certmagic.DomainPolicy{}, nil }, )

默认 DomainPolicy{} 的行为是:

  • 先尝试 exact:api.example.com
  • 再尝试单层 wildcard:*.example.com
  • 若仍未命中,则申请 exact:api.example.com

生产推荐:租户域名动态绑定

certmagic.Default.OnDemand = certmagic.OnDemandPolicy( func(ctx context.Context, serverName string) (certmagic.DomainPolicy, error) { tenant, err := domainStore.LookupEnabledTenant(ctx, serverName) if err != nil { return certmagic.DomainPolicy{}, err } if tenant == nil { return certmagic.DomainPolicy{}, fmt.Errorf("domain not allowed") } return certmagic.DomainPolicy{ BaseDomain: tenant.BaseDomain, PreferWildcard: tenant.WildcardEnabled, ObtainWildcard: tenant.WildcardEnabled && tenant.DNS01Ready, DisableExactObtain: tenant.WildcardEnabled && tenant.DNS01Ready, }, nil }, )

这样上层只需返回一份 DomainPolicy,库内会自动完成:

  • exact / wildcard 加载顺序
  • wildcard 主题名推导
  • exact / wildcard 申请目标选择
  • load-only 与 obtain 模式切换

DomainPolicy 字段说明

DomainPolicy 支持以下字段:

字段类型说明
ExactSubjectstring覆盖默认 exact 主题名;留空时使用归一化后的请求 SNI
BaseDomainstring指定 wildcard 的基础域名,例如 "tenant.example.com";留空时自动推导单层 wildcard
WildcardSubjectstring显式指定 wildcard 主题名;优先级高于 BaseDomain
PreferWildcardbool控制加载顺序是否优先尝试 wildcard
DisableWildcardLoadbool禁止在加载阶段尝试 wildcard 证书
ObtainWildcardbool未命中现有证书时申请 wildcard 证书;启用前应确保 issuer 已具备 DNS-01 能力
DisableExactObtainbool禁止申请 exact 证书;若同时未启用 ObtainWildcard,则该策略仅允许加载已有证书

零值策略等价于:

  1. 允许加载 exact 证书
  2. 允许加载与请求名匹配的单层 wildcard 证书
  3. 未命中时申请 exact 证书

Wildcard 判定规则

默认策略只会尝试单层 wildcard

  • api.example.com*.example.com
  • foo.bar.example.com*.bar.example.com

不会默认尝试:

  • *.*.example.com
  • *.example.com 覆盖 example.com
  • *.example.com 覆盖 foo.bar.example.com

如果你要现场申请 wildcard,必须同时满足:

  • 业务策略明确允许
  • 域名归属已验证
  • DNS-01 挑战能力已就绪

否则建议仅复用现有 wildcard,申请时仍走 exact 证书。

客户端证书 (mTLS)

CertMagic 支持管理客户端证书用于 mTLS:

chains, err := cfg.ClientCredentials(ctx, []string{"example.com"}) if err != nil { // 处理错误 }

事件系统

CertMagic 在重要事件发生时发出事件。设置 Config.OnEvent 来订阅:

cfg.OnEvent = func(ctx context.Context, event string, data map[string]any) error { switch event { case "cert_obtained": // 证书获取成功 case "cert_failed": // 证书获取失败 case "tls_get_certificate": // TLS 握手获取证书阶段 } return nil }

事件类型:

  • cached_unmanaged_cert:缓存了非托管证书
  • cert_obtaining:即将获取证书
  • cert_obtained:成功获取证书
  • cert_failed:获取证书失败
  • tls_get_certificate:TLS 握手 GetCertificate 阶段
  • cert_ocsp_revoked:证书 OCSP 状态为已撤销

日志系统

CertMagic 内置 Logger 接口,支持自定义日志实现:

// 使用标准库 slog logger := certmagic.NewLogger(slog.New(slog.NewJSONHandler(os.Stdout, nil))) certmagic.SetLogger(logger) // 使用默认文本日志 certmagic.SetLogger(certmagic.NewTextLogger(os.Stdout, slog.LevelInfo)) // 丢弃所有日志 certmagic.SetLogger(certmagic.NopLogger())

存储后端

文件存储(默认)

默认存储到 $HOME/.local/share/certmagic

自定义存储

实现 certmagic.Storage 接口:

type Storage interface { Locker Store(ctx context.Context, key string, value []byte) error Load(ctx context.Context, key string) ([]byte, error) Delete(ctx context.Context, key string) error Exists(ctx context.Context, key string) bool List(ctx context.Context, prefix string, recursive bool) ([]string, error) Stat(ctx context.Context, key string) (KeyInfo, error) }

集群部署

CertMagic 非常适合在负载均衡器后或集群环境中运行。确保所有实例使用相同的 Storage。

// 所有实例使用相同存储配置 certmagic.Default.Storage = mySharedStorage

开发与测试

Let's Encrypt 生产环境有严格的速率限制。开发时使用 staging 环境

certmagic.DefaultACME.CA = certmagic.LetsEncryptStagingCA

常见问题

可以同时使用自己的证书吗?

可以。调用以下方法将证书添加到缓存:

  • CacheUnmanagedCertificatePEMBytes()
  • CacheUnmanagedCertificatePEMFile()
  • CacheUnmanagedTLSCertificate()

注意:非托管证书不会自动续期。

如何监听 80 和 443 端口?需要 root 吗?

Linux 上可使用 setcap 授予绑定低位端口的权限:

sudo setcap cap_net_bind_service=+ep /path/to/your/binary

支持 IP 证书吗?

支持。CertMagic 支持 ACME IP 证书(RFC 8738),但需 CA 支持。当前 Let's Encrypt 和 Google Trust Services 支持。

项目历史

CertMagic 是 Caddy 核心 TLS 自动化代码提取出的库。底层 ACME 客户端实现基于 ACMEz

CertMagic 代码最初是 Caddy 的一部分,早在 2015 年 Let's Encrypt 公开测试前就已存在。

多年来,Caddy 的 TLS 自动化技术已被广泛采用,在生产环境中经过测试,保护了数百万网站和数万亿连接。

贡献

欢迎贡献!请参阅贡献指南

许可证

CertMagic 由 Matthew Holt 开发,采用 Apache 2.0 许可证。