首页
/ Traefik ACME 多域名证书实战:用 tls.domains 的 main 与 sans 精确控制证书申请范围

Traefik ACME 多域名证书实战:用 tls.domains 的 main 与 sans 精确控制证书申请范围

2026-09-04 13:32:23作者:宣聪麟

本文围绕 Traefik 的 ACME(Let's Encrypt)多域名证书配置展开,完整讲解如何在 Docker & Swarm、Kubernetes、File(YAML/TOML)四种配置风格下,通过路由器的 tls.domainsmain + 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 里显式声明 mainsans,实现对证书申请范围的精确控制。

后文所有示例都来自这个显式多域名的官方示例文档。

完整配置示例:四种 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 列表,每条含 mainsans 两个键。

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 概览文档路由配置文档 的说明:

  • 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.gowatchNewDomainsL515-L595)。它对每份动态配置中的 HTTP 路由做如下处理(节选逻辑):

  1. 跳过未开启 TLS 或 CertResolver 不属于本解析器的路由:if route.TLS == nil || route.TLS.CertResolver != p.ResolverName { continue }
  2. route.TLS.Domains 非空(即本文示例的情况):调用 deleteUnnecessaryDomains 做去重/过滤后,逐个域名块调用 resolveCertificate 申请证书,再 addCertificateForDomain 注入证书存储;
  3. 否则(未显式配置 domains):对 HTTP 路由调用 httpmuxer.ParseDomains(route.Rule) 从规则里提取 Host,TCP 路由则用 tcpmuxer.ParseHostSNI,再走 resolveDomains 统一申请。

这正是「tls.domains 优先于规则解析」这句话的源码依据:只有 domains 为空时才会回落到规则解析路径。

域名去重与通配符过滤

deleteUnnecessaryDomainspkg/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 渲染出带 certificatesResolversemail=test@traefik.iostorage=/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.goDomain
  • 优先级上,显式 tls.domains 高于从路由规则 Host(...) 自动提取的域名,这是 pkg/provider/acme/provider.gowatchNewDomains 的分支逻辑所保证的;
  • 通配符 SAN 必须配合 DNS-01 挑战;所有域名需有指向 Traefik 的 A/AAAA 记录;
  • 解析器本身(emailstorage、挑战方式、keytype 等)全部属于静态配置,动态侧的 certResolver 只做引用;
  • 四种 provider 的写法(Docker 标签、Kubernetes CRD、File YAML/TOML)字段语义一致,可依据 本文开头给出的四个示例 直接落地。
登录后查看全文
热门项目推荐
相关项目推荐