登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go TLS 客户端 SAN 与 CommonName 怎样选择

来源:17golang原创

时间:2026-10-31 20:32:41 427浏览 收藏

Go TLS 客户端选择证书名称时,优先把真实连接名写入 SAN:域名放进 DNS SAN,IP 地址放进 IP SAN。CommonName(简称 CN)不要再当作主机名校验的主要位置;Go 的 x509.VerifyHostname 会忽略旧式 CommonName。客户端的 tls.Config.ServerName 应填写要校验的域名,通常也就是发送给服务端的 SNI 名称。

官方资料:https://pkg.go.dev/crypto/tls

一句话判断:证书服务谁,就把谁写入 SAN;客户端连谁,就把谁填到 ServerName。只写 CommonName 或为了通过握手设置 InsecureSkipVerify,都不是正确的生产方案。

先把“连接地址”和“证书名称”对齐

假设客户端连接的是 api.internal.example,证书至少应在 DNS SAN 中出现这个名字。如果服务通过多个稳定别名访问,就把这些别名一起放入同一张证书的 DNS SAN;如果客户端直接拨号 10.20.0.8,则证书需要在 IP SAN 中写入这个 IP。不要把 IP 当成普通字符串塞进 DNS SAN,也不要只依赖 CN。

Go TLS 客户端连接地址与 DNS SAN、IP SAN、CommonName 的静态关系说明图
图1:结构说明图,展示连接地址、SAN 名称和 CommonName 的边界关系;这是原创说明图,不是运行截图。

可以先用一张清单拆开名称来源:

客户端实际连接证书应填写常见误区
api.internal.exampleDNS SAN只写 CN=api.internal.example
10.20.0.8IP SAN把 IP 写成 DNS SAN
*.internal.example 覆盖的单层子域左侧完整通配符 DNS SAN期待它覆盖更深层级

CommonName 该怎么处理

CommonName 可以继续作为证书主题中的可读字段,方便人工查看旧系统资料,但不要把它当作 Go 新客户端的主机名匹配依据。当前 Go 的主机名校验会检查 DNS SAN 或 IP SAN;当证书没有 SAN、只有旧 CN 时,常见结果是类似“certificate relies on legacy Common Name field, use SANs instead”的错误。

因此,签发证书时应把名称放在 SAN 扩展中。下面的配置片段只演示客户端侧的对应关系,证书签发工具或 CA 的具体命令可以按组织现有流程完成:

package main

import (
    "crypto/tls"
    "crypto/x509"
    "fmt"
)

func tlsConfig(caPEM []byte) (*tls.Config, error) {
    roots := x509.NewCertPool()
    // 把内部 CA 加入信任池,避免用跳过校验来掩盖信任链问题。
    if ok := roots.AppendCertsFromPEM(caPEM); !ok {
        return nil, fmt.Errorf("无法解析内部 CA 证书")
    }

    return &tls.Config{
        // 这里填证书 DNS SAN 中的名称,同时作为客户端 SNI。
        ServerName: "api.internal.example",
        RootCAs:    roots,
        MinVersion: tls.VersionTLS12,
    }, nil
}

这个例子没有设置 InsecureSkipVerify。如果 CA 不受系统信任,应修正 RootCAs 或部署信任链,而不是关闭主机名和证书链校验。

ServerName 不是“服务器 IP”的简单替代

ServerName 的值决定客户端校验哪个主机名,也会用于 SNI。通过负载均衡器或代理访问时,TCP 目标地址可以是 IP,但证书服务名通常仍是域名;此时应使用证书中的 DNS SAN 作为 ServerName,并由自定义拨号器把连接落到实际 IP。反过来,如果协议明确以 IP 作为访问身份,就应使用 IP SAN,并将 IP 作为 ServerName。

package main

import (
    "context"
    "crypto/tls"
    "net"
    "time"
)

func dialService(ctx context.Context, caPEM []byte) (net.Conn, error) {
    cfg, err := tlsConfig(caPEM)
    if err != nil {
        return nil, err
    }

    dialer := &net.Dialer{Timeout: 5 * time.Second}
    // 实际 TCP 目标可以是固定 IP,但 TLS 身份仍使用证书里的 DNS 名称。
    raw, err := dialer.DialContext(ctx, "tcp", "10.20.0.8:443")
    if err != nil {
        return nil, err
    }
    conn := tls.Client(raw, cfg)
    // 显式握手,把证书链和主机名错误尽早返回给调用方。
    if err := conn.HandshakeContext(ctx); err != nil {
        _ = raw.Close()
        return nil, err
    }
    return conn, nil
}

这里的关键不是“IP 能不能连通”,而是“TLS 身份要验证谁”。如果没有代理、SNI 或多租户需求,直接让 tls.Dial 使用目标域名通常更简单。

Go TLS ServerName、SNI、主机名校验与 IP 拨号边界的静态关系说明图
图2:结构说明图,展示 TCP 目标、ServerName、SNI 与 SAN 校验的静态关系;这是原创说明图,不是运行截图。

通配符、IP 和旧证书的边界

Go 对域名通配符按左侧完整标签匹配,例如 *.internal.example 可以匹配 api.internal.example,但不能自然覆盖 v1.api.internal.example。IP 地址则按 IP SAN 比较,不应依赖通配符或 CN。证书里有多个 DNS SAN 时,客户端只需把本次连接使用的名称填入 ServerName。

如果对端只能提供旧式 CN 证书,正确的处理顺序是重新签发包含 SAN 的证书、更新服务端证书链,再保留旧 CN 作为过渡信息。测试环境可以通过自定义验证逻辑明确写出兼容边界,但不要把 InsecureSkipVerify: true 作为生产修复;它会同时放弃证书链和主机名验证。

用 VerifyHostname 复核名称选择

在证书对象已经解析后,可以用 VerifyHostname 检查一个候选名称。这个调用本身不替代完整的证书链验证,但很适合核对“名称究竟放在 SAN 还是 CN”这一层:

func checkCertificateName(cert *x509.Certificate, host string) error {
    // Go 只按 DNS SAN 或 IP SAN 匹配 host,旧 CommonName 不参与匹配。
    if err := cert.VerifyHostname(host); err != nil {
        return fmt.Errorf("证书名称不匹配 %q: %w", host, err)
    }
    return nil
}

发布前可以按下面的三项做人工复核:证书 SAN 是否覆盖真实连接名;ServerName 是否与要验证的身份一致;内部 CA 是否进入正确的信任池。三项都满足后,再处理超时、协议版本或连接复用等其他 TLS 问题。

相关问题

  • 客户端连 IP 时 ServerName 应该填什么? 如果证书身份就是该 IP,填 IP 并配置 IP SAN;如果只是通过 IP 找到服务但身份是域名,填 DNS SAN 对应的域名。
  • CN 和 SAN 同时存在但内容不一致怎么办? 以 SAN 为准,统一证书签发清单和客户端 ServerName,避免让 CN 造成错误预期。
  • 为什么设置 InsecureSkipVerify 后能连通? 因为它关闭了证书链和主机名校验;这只说明网络和握手路径可达,不说明连接身份可信。
声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>