Traefik ACME 多域名证书实战:用 tls.domains 的 main 与 sans 精确控制证书申请范围
本文围绕 Traefik 的 ACME(Let's Encrypt)多域名证书配置展开,完整讲解如何在 Docker & Swarm、Kubernetes、File(YAML/TOML)四种配置风格下,通过路由器的 tls.domains(main + sans)显式声明一个主域名加多个 SAN 域名,并深入源码说明 Traefik 是如何把这些域名转换为 ACME 证书申请、以及 tls.domains 与从路由规则自动解析域名之间的优先级关系。读完后你能够:正确为单张证书申请多域名(含通配符 SAN),理解域名的解析、去重与过滤逻辑,并知道配套的静态配置(证书解析器、挑战方式)应如何落地。
场景定位:单域名、规则解析与显式多域名三种取法
Traefik 的 HTTPS 配置中,路由器的 TLS 部分与 ACME 证书解析器(certificate resolver)配合工作。官方文档将「为路由启用自动证书」拆成了三个递进的示例:
- 单域名示例:仅配置
tls=true+certResolver,域名完全从路由规则中的Host(...)匹配器自动提取; - 从规则解析多域名示例:规则里出现多个 Host(如
(Host(example.com) && Path(/blog)) || Host(blog.example.org)),Traefik 会把规则中的多个域名自动解析出来; - 本文对应的 多域名显式示例:不依赖规则解析,而是在
tls.domains里显式声明main与sans,实现对证书申请范围的精确控制。
后文所有示例都来自这个显式多域名的官方示例文档。
完整配置示例:四种 provider 风格全覆盖
官方示例描述的是一条名叫 blog 的 HTTP 路由:规则为 Host(example.com) && Path(/blog),并为其申请一张主域名为 example.com、附带 SAN *.example.org 的证书。四种配置风格如下,可按所用 provider 直接复制。
Docker & Swarm(容器标签)
## Dynamic configuration
labels:
- traefik.http.routers.blog.rule=Host(`example.com`) && Path(`/blog`)
- traefik.http.routers.blog.tls=true
- traefik.http.routers.blog.tls.certresolver=myresolver
- traefik.http.routers.blog.tls.domains[0].main=example.com
- traefik.http.routers.blog.tls.domains[0].sans=*.example.org
要点:
tls=true表示该路由只处理 HTTPS 请求;certresolver=myresolver引用的解析器必须已在静态配置中定义;- 多个域名条目用数组下标区分,例如第二个域名块写成
tls.domains[1].main=...、tls.domains[1].sans=...,同一块内可继续追加多个sans(如tls.domains[0].sans=*.example.org,api.example.com的写法在标签语法中以逗号分隔)。
Kubernetes(IngressRoute CRD)
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: blogtls
spec:
entryPoints:
- websecure
routes:
- match: Host(`example.com`) && Path(`/blog`)
kind: Rule
services:
- name: blog
port: 8080
tls:
certResolver: myresolver
domains:
- main: example.com
sans:
- '*.example.org'
注意 Kubernetes CRD 中 spec.tls 一旦出现即等效于启用 TLS,无需单独的 tls: true 字段;域名条目是 domains 列表,每条含 main 与 sans 两个键。
File Provider(YAML 动态配置)
## Dynamic configuration
http:
routers:
blog:
rule: "Host(`example.com`) && Path(`/blog`)"
tls:
certResolver: myresolver
domains:
- main: "example.com"
sans:
- "*.example.org"
File Provider(TOML 动态配置)
## Dynamic configuration
[http.routers]
[http.routers.blog]
rule = "Host(`example.com`) && Path(`/blog`)"
[http.routers.blog.tls]
certResolver = "myresolver" # From static configuration
[[http.routers.blog.tls.domains]]
main = "example.com"
sans = ["*.example.org"]
TOML 中域名条目使用 [[...]] 数组表语法,sans 是字符串数组。
字段语义与优先级规则
tls.certResolver:指定用于自动签发证书的解析器名称(对应静态配置中的certificatesResolvers.<name>);tls.domains:显式列出证书要覆盖的域名及 SAN。文档明确写道——当使用 ACME 解析器时,域名默认会自动从路由规则中提取,而tls.domains提供对证书生成的细粒度控制,且优先级高于从路由规则自动提取的域名;- 每一条域名(包括 SAN)都必须有 A/AAAA 记录指向 Traefik,否则证书验证会失败;
- 示例中的 SAN 为通配符
*.example.org。参考 ACME 静态配置 flag 参考 中dnschallenge一节的注释:DNS-01 挑战是生成通配符证书的必备条件(Note: mandatory for wildcard certificate generation)。因此若sans里包含通配符,myresolver必须启用dnschallenge并配置 provider,而不能只依赖 HTTP-01/TLS-ALPN-01。
源码视角:tls.domains 如何驱动 ACME 申请
配置结构体
路由 TLS 配置的定义在 pkg/config/dynamic/http_config.go#L150-L156:
// RouterTLSConfig holds the TLS configuration for a router.
type RouterTLSConfig struct {
Options string `json:"options,omitempty" toml:"options,omitempty" yaml:"options,omitempty" export:"true"`
ResolvedOptions string `json:"-" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"false"`
CertResolver string `json:"certResolver,omitempty" toml:"certResolver,omitempty" yaml:"certResolver,omitempty" export:"true"`
Domains []types.Domain `json:"domains,omitempty" toml:"domains,omitempty" yaml:"domains,omitempty" export:"true"`
}
Domains 是 []types.Domain,其定义在 pkg/types/domains.go#L10-L15:
// Domain holds a domain name with SANs.
type Domain struct {
// Main defines the main domain name.
Main string `description:"Default subject name." json:"main,omitempty" toml:"main,omitempty" yaml:"main,omitempty"`
// SANs defines the subject alternative domain names.
SANs []string `description:"Subject alternative names." json:"sans,omitempty" toml:"sans,omitempty" yaml:"sans,omitempty"`
}
可见配置里的 main/sans 与 Go 结构体字段一一对应,ToStrArray() 方法会把一个 Domain 展平为「主域名 + SAN 列表」的字符串数组,供后续 ACME 流程使用。
核心流程:watchNewDomains 的分支判断
ACME provider 的入口在 pkg/provider/acme/provider.go 的 watchNewDomains(L515-L595)。它对每份动态配置中的 HTTP 路由做如下处理(节选逻辑):
- 跳过未开启 TLS 或
CertResolver不属于本解析器的路由:if route.TLS == nil || route.TLS.CertResolver != p.ResolverName { continue }; - 若
route.TLS.Domains非空(即本文示例的情况):调用deleteUnnecessaryDomains做去重/过滤后,逐个域名块调用resolveCertificate申请证书,再addCertificateForDomain注入证书存储; - 否则(未显式配置 domains):对 HTTP 路由调用
httpmuxer.ParseDomains(route.Rule)从规则里提取 Host,TCP 路由则用tcpmuxer.ParseHostSNI,再走resolveDomains统一申请。
这正是「tls.domains 优先于规则解析」这句话的源码依据:只有 domains 为空时才会回落到规则解析路径。
域名去重与通配符过滤
deleteUnnecessaryDomains(pkg/provider/acme/provider.go#L825-L828)在显式 domains 路径上起作用,其注释说明了职责:
// deleteUnnecessaryDomains deletes from the configuration :
// - Duplicated domains
// - Domains which are checked by wildcard domain.
也就是说,如果你在多个 domains 块里重复声明了同一域名,或者某个具体域名已被某条通配符覆盖,它们会被剔除,避免对同一张证书重复申请。另外 resolveDomains 内部还维护了 resolvingDomains 去重表(以排序后的域名列表拼接为 key),防止并发重复发起 ACME 订单。
申请后的落地
resolveCertificate 成功取到证书后,经 addCertificateForDomain 加入默认 TLS Store,TLS manager 随即可用于 SNI 匹配。示例中的通配符 SAN *.example.org 意味着这张证书同时覆盖 *.example.org 下的所有子域;由于 ACME(Let's Encrypt)会为同一主域名+SAN 组合签发单张证书,多个 domains 块之间相同的域名集合也会命中同一张已签发证书。
配套静态配置:证书解析器 myresolver
示例中所有 certResolver: myresolver 都指向名为 myresolver 的静态解析器。ACME 解析器文档 与 flag 参考 给出了完整参数面,最精简的生产配置至少需要:
--certificatesresolvers.myresolver.acme.email=test@example.com
--certificatesresolvers.myresolver.acme.storage=acme.json
常用可选项(含默认值,摘自 flag 参考):
| 参数 | 说明 | 默认值 |
|---|---|---|
acme.caserver |
CA 服务器;可指向 Let's Encrypt staging 环境调试 | https://acme-v02.api.letsencrypt.org/directory |
acme.certificatesDuration |
证书有效期(小时) | 2160(90 天) |
acme.clientTimeout |
与 ACME 服务器完成一次 HTTP 事务的超时 | 2m |
acme.clientResponseHeaderTimeout |
接收 ACME 响应头的超时 | 30s |
acme.preferredchain |
优先选择的证书链 Subject Common Name | "" |
acme.keytype |
密钥类型:EC256/EC384/RSA2048/RSA4096/RSA8192 |
RSA4096 |
acme.tlschallenge |
启用 TLS-ALPN-01 挑战 | 关 |
acme.httpchallenge + acme.httpchallenge.entrypoint |
启用 HTTP-01 挑战及对应入口 | 关(entrypoint 必填) |
acme.dnschallenge + acme.dnschallenge.provider |
启用 DNS-01 挑战(通配符证书必需) | 关(provider 必填) |
acme.dnschallenge.delaybeforecheck |
校验 TXT 记录前的延迟(秒),内网屏蔽外部 DNS 时有用 | 0 |
acme.dnschallenge.resolvers |
解析 FQDN 权威服务器所用的 DNS 服务器 | 空 |
acme.dnschallenge.disablepropagationcheck |
跳过 DNS 传播检查(官方标注不推荐,易触发限流) | false |
对应本文示例的推荐组合:myresolver 启用 dnschallenge(因含通配符 SAN *.example.org),并为 HTTP-01 兜底配置 httpchallenge.entrypoint(如 web)。
验证与回归:集成测试如何覆盖该场景
仓库的集成测试对「显式 domains」场景有专门的 fixture:integration/fixtures/acme/acme_domains.toml 以 Go template 渲染出带 certificatesResolvers(email=test@traefik.io、storage=/tmp/acme.json,以及可选的 HTTP/TLS 挑战块)和 File provider 的完整配置,并被 integration/acme_test.go 中多处用例引用(如 L140、L186、L385、L408 附近),用于验证不同挑战方式下 ACME 证书的签发行为。查看这些 fixture 与测试是理解「显式 domains + 挑战方式」组合行为的良好入口;本地演练时注意 storage 文件与 CA 地址应指向 staging 环境,避免消耗真实额度。
小结
- 多域名证书的核心字段是
tls.domains:每个条目含main(证书主题名)与sans(SAN 列表),对应源码结构 pkg/types/domains.go 的Domain; - 优先级上,显式
tls.domains高于从路由规则Host(...)自动提取的域名,这是 pkg/provider/acme/provider.go 中watchNewDomains的分支逻辑所保证的; - 通配符 SAN 必须配合 DNS-01 挑战;所有域名需有指向 Traefik 的 A/AAAA 记录;
- 解析器本身(
email、storage、挑战方式、keytype等)全部属于静态配置,动态侧的certResolver只做引用; - 四种 provider 的写法(Docker 标签、Kubernetes CRD、File YAML/TOML)字段语义一致,可依据 本文开头给出的四个示例 直接落地。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00