Teleport Machine ID 主机证书支持(RFD 83)实践指南:用 tbot 自动化 OpenSSH 主机证书签发
2026/9/21 15:20:47 网站建设 项目流程
  • 网络安全
  • 认证鉴权
  • 运维
  • 后端

【免费下载链接】teleport

The easiest, and most secure way to access and protect all of your infrastructure.

项目地址:https://gitcode.com/gh_mirrors/tel/teleport
点击查看免费下载

本文围绕 Teleport 仓库中 rfd/0083-machine-id-host-certs.md 所描述的设计展开,介绍如何让 Machine ID bot 通过 RBACwhere子句安全地签发 OpenSSH 主机证书,并以ssh_host输出服务自动写入磁盘。读完本文,你将掌握host_cert权限的细粒度管控语法、tbot 配置文件写法、短期证书与sshd协同的注意事项,以及该方案与tctl auth sign手工签发方式的差异。

背景:为什么需要 Machine ID 签发主机证书

在引入 RFD 83 之前,为 OpenSSH 服务器签发主机证书需要具备管理员级别权限的人员通过tctl auth sign手工完成。这一做法存在两个痛点:

  1. 证书过期或 CA 轮换后需要人工刷新:主机证书一旦过期,或集群证书颁发机构(CA)发生轮换,就必须再次人工签发,运维负担重;
  2. 容易诱导用户分发长期凭证:为了减少反复签发,用户倾向于使用超长 TTL 甚至无过期时间的证书,这在引入 OpenSSH 节点时埋下安全隐患。

RFD 83(state: implemented,随 v10.3.0 落地)的目标是让 Machine ID bot 能够:

  • 通过 RBAC 规则安全地为特定主机签发主机证书;
  • 提供开箱即用的 bot UX,让证书的签发与落盘完全自动化,随 bot 的常规续期循环刷新。

其核心价值在于:将"谁可以为哪些主机签发证书"收敛为可审计、可限制的 RBAC 策略,并把"证书过期后怎么办"交给 bot 的自动续期机制解决,而不是依赖运维人员手工操作。

安全设计:为host_cert增加where子句支持

在 Teleport 中,主机证书并非真正持久化的资源,但系统暴露了一个虚拟的host_certRBAC 资源。任何被授予该资源create动词的用户,都可以调用GenerateHostCert()并携带任意参数获得一组主机证书。

对人类集群管理员而言,配合 MFA 的短期凭证是合理的;但对于生命周期很长的机器凭证,问题就出现了:即使凭证本身短期有效,被盗用的机器凭证仍可能被用来签发新的长期证书。虽然主机证书本身对 Teleport 集群的访问能力有限,但它足以让攻击者冒充真实节点

为此,RFD 83 提出为host_cert规则引入与其他真实 Teleport 资源一致的where子句支持。以下角色配置允许用户签发主机证书,但只允许 principal 以.foo.example.com结尾:

kind: role version: v5 metadata: name: example spec: allow: rules: - resources: [host_cert] verbs: [create] where: all_end_with(host_cert.principals, ".foo.example.com")

三个关键实现要点

RFD 83 明确了实现该能力的三项工作,在仓库源码中均可找到对应落地证据:

  1. 新增 DNS 匹配相关的比较函数:最少实现all_end_with(inputs, suffix)all_equal(inputs, value),未来按需扩展。正则表达式由于引号与转义问题复杂,暂不引入底层predicate库。

  2. 函数必须支持字符串列表:例如all_end_with(host_cert.principals, ".example.com")应对每个 principal 分别判断并做 AND 聚合。这一行为被限定在新函数内,不影响已有规则。

  3. GenerateHostCert()中评估where子句:由于host_cert不是真正的 Teleport 资源,predicate 上下文没有可比较的字段,因此需要仿照SSHSession的方式新增一个可选的host_cert字段,并在GenerateHostCert()中提供自定义上下文。

源码实现:predicate 函数与 HostCertContext

新增的 predicate 比较函数

三个核心函数实现在 lib/services/parser.go 中,并注册进默认的where解析器(见newDefaultWhereParserDef,第 171-218 行):

  • all_end_withpredicateAllEndWith(第 74-96 行):当第一个参数为string时调用strings.HasSuffix;当为[]string时遍历全部元素,任一元素不满足后缀即返回 false(AND 语义);
  • all_equalpredicateAllEqual(第 101-123 行):比较字符串或字符串列表中所有元素是否全部等于给定值,适合"该字段只应包含单个特定值"的场景;
  • is_subsetpredicateIsSubset(第 128-156 行):判断第一个参数(string[]string)是否全部包含在后续的可变参数集合中,可用于"允许的取值清单"这类约束。

此外还保留了equalscontainscontains_allcontains_anysethas_prefix等既有函数,以及system.catype()这类特殊用途函数。RFD 明确提到"未来可考虑集合运算以支持 'any of the following allowed values'"——而is_subset正是这一思路的早期形态,代码注释与测试均将其与host_cert规则配合使用。

HostCertContext:证书请求的谓词上下文

lib/services/parser.go 第 655-673 行定义了HostCertContext,用于在where子句中暴露证书请求的关键字段:

字段JSON 标识含义
HostIDhost_id证书请求中的主机 ID
NodeNamenode_name证书请求中的节点名
Principalsprincipals请求的证书 principal 列表
ClusterNamecluster_name签发证书所属集群名
Rolerole证书对应的 Teleport 角色(SystemRole)
TTLttl请求的证书 TTL

这些上下文字段用于进一步限制证书签发;其中除principals外,其余字段对常规 SSH 节点通常为空。RFD 同时指出:TTL 字段若需在谓词中比较时长,可能需要额外的自定义谓词函数。HostCertContext在解析器初始化时以emptyHostCert作为缺省值兜底(第 681-683 行)。

服务端签发路径

服务端入口位于 lib/auth/auth_with_roles.go 的GenerateHostCerts(第 698 行起):它校验调用者身份(ScopedBuiltinRole/BuiltinRole)、校验服务器 FQDN 与req.HostID匹配、校验角色不越权,最终调用a.authServer.GenerateHostCerts。这与 RFD 中"提供自定义上下文并在GenerateHostCert()中评估where子句"的设计意图一致。

测试印证

lib/auth/auth_with_roles_test.go 中的TestGenerateHostCert(第 9616 行起)以表驱动方式覆盖了完整场景矩阵,直接印证文档所述的规则语义:

  • 无规则时签发被拒(disallowed);
  • deny规则生效时被拒(denied);
  • 直接放行(allowed);
  • is_subset(host_cert.principals, "foo.example.com", "bar.example.com"):仅当请求的 principals 是清单子集时放行;
  • all_equal(host_cert.principals, "foo.example.com"):仅当全部 principal 等于给定值;
  • all_end_with(host_cert.principals, ".foo.example.com"):仅当全部 principal 以指定后缀结尾;
  • 复杂的where+denyWhere组合场景。

每一组用例都验证了放行或trace.IsAccessDenied,为"where 子句真正参与签发授权判断"提供了直接证据。

机器侧体验:tbot 的ssh_host输出服务

配置方式

RFD 83 的首选实现是Config Template(配置模板)方案:用户在 bot 配置中声明一个ssh_host类型的输出服务,bot 在每次续期循环中渲染该模板。当前仓库中该服务以type: ssh_host的形式配置(RFD 早期草案中的host_cert配置段已演化为独立的输出服务类型,见 lib/tbot/services/ssh/host_output_config.go 与 lib/tbot/config/config.go 的类型分发):

version: v2 proxy_server: example.teleport.sh:443 onboarding: token: my-token join_method: token services: - type: ssh_host destination: directory: /opt/machine-id principals: - host.example.com

HostOutputConfig支持的关键字段(见 lib/tbot/services/ssh/host_output_config.go):

字段说明
name服务名,用于日志与/readyz端点标识
destination凭证写入目标(目录、内存等)
principals请求的主机证书 principal 列表,至少指定一个,否则报错
ca_type导出的 CA 类型,支持user(默认)与openssh,用于生成 TrustedUserCAKeys
credential_ttl/renewal_interval凭证生命周期与续期频率(内联字段),二者必须同时指定或不指定

生成流程

HostOutputService(lib/tbot/services/ssh/host_output.go)的generate方法完整实现了 RFD 描述的动作链:

  1. 校验目标目录 ACL 与可写性;
  2. 用 bot 的默认生命周期生成 facade 身份,并构建模拟身份客户端impersonatedClient),确保后续取数操作受访问权限约束;
  3. 生成密钥对(cryptosuites.HostSSH,密钥套件取自 Auth 服务偏好);
  4. 调用TrustClient().GenerateHostCert,其中HostIdNodeName保持为空、RoleRoleNode、TTL 复用 bot 的常规 TTL;
  5. FormatOpenSSH格式写出ssh_host(私钥)与ssh_host-cert.pub(证书);
  6. 额外导出用户 CA 到ssh_host-user-ca.pub(供sshdTrustedUserCAKeys使用),导出的 CA 会剔除cert-authority前缀(见exportSSHUserCAs)。

续期循环与短期证书

RFD 指出:Config Template 会在 bot 常规续期循环中刷新,默认约 20 分钟一次。仓库中 lib/tbot/bot/credential_lifetime.go 的DefaultCredentialLifetime正是TTLRenewalInterval: 20 * time.MinuteHostOutputService.Run通过RunOnInterval以该间隔驱动generate,并支持 reload 信号与/readyz状态上报。证书在启动时写入一次,之后约每 20 分钟更新一次。

输出文件与sshd集成

输出目录中将出现三个文件:

  • ssh_host:主机私钥;
  • ssh_host-cert.pub:由 Teleport 主机 CA 签发的证书;
  • ssh_host-user-ca.pub:导出的用户 CA 公钥,供客户端信任校验使用。

将私钥与证书路径分别配置到sshd_configHostKey指令即可。RFD 特别说明了两点测试结论:

  • OpenSSH 按需重读证书文件sshd无需 reload(除非sshd_config本身发生变化,例如修改了证书路径);
  • sshd不关心文件权限(除非文件属主为root),因此与tbot init生成的 ACL 实现可以良好共存。

该方案的一个已知缺点:权限类错误(例如where谓词不匹配)只会作为错误上报,不会导致 bot 崩溃,运维需要关注日志与就绪状态。

短期证书与sshd的已知注意点

RFD 的核心取舍是"放弃tctl auth sign签发的不含过期时间的主机证书,改为与其他 bot 凭证一致的短期证书"。当sshd的主机证书过期(例如tbot进程崩溃)时,客户端会看到如下错误:

Certificate invalid: expired The authenticity of host '192.168.122.6.foo.example.com (<no hostip for proxy command>)' can't be established. RSA key fingerprint is SHA256:CWqUJ7q3uPGX9gMoD7R76Hi8pJsoSL8SA0J1FIMmOc8. This key is not known by any other names Are you sure you want to continue connecting (yes/no/[fingerprint])?

这几乎与常见的 SSH TOFU(首次信任)提示一模一样,唯一的区别是容易被忽略的Certificate invalid: expired一行。风险在于:用户很可能习惯性地输入yes接受,导致过期或无效的主机密钥被永久写入known_hosts,此后"expired"提示不再出现——这会让用户连接到潜在不可信的主机。

官方给出的规避方法是:一旦确认证书过期或主机不可信,使用ssh-keygen -R <host>移除known_hosts中的旧条目。在实际部署中,建议同时配合监控 bot 的/readyz状态与续期错误日志,尽量缩短证书过期但 bot 未恢复的空窗期。

备选方案对比

RFD 83 同时评估了另外两条路径,了解它们有助于理解最终设计的选择理由:

备选一:No-op(现状延续)

用户本就可以通过host_cert权限,将 Machine ID 的identity文件传给tctl手工签发:

tctl -i path/to/identity auth sign \ --host=foo.example.com \ --format=openssh \ --out=myhost

这种方式与安全的证书签发可以配合使用,但证书在 CA 轮换或凭证泄露后仍需手动重签。用户可以用 cron / systemd timer 等外部机制做自动化(当前仓库中 tool/tctl/common/auth_command.go 即tctl auth sign的实现所在),但相比 bot 内建服务,属于"自己造轮子"。

备选二:Out of band(独立续期循环)

如果认为主机证书应以不同于其他 bot 资源的间隔续期,可以为这些证书单独新增一条续期循环,并在 CA 轮换时像常规续期一样额外触发。该方案遗留了开放问题:bot 启动时仍会续期全部证书,是否维持这一行为?若不维持,又该如何判断下次续期时机?——这些未决问题也是最终选择 Config Template 方案的原因之一。

未来展望

RFD 83 在文末列出了三个演进方向,可供阅读源码与测试时对照:

  • 改进 IP 地址支持:自动将客户端 IP 地址纳入允许的证书 principal;
  • 进一步强化谓词:引入集合运算,更好地表达"以下允许值中的任意一个";
  • 审计日志:考虑为失败的GenerateHostCert()调用写入审计事件,甚至支持审计事件谓词动作。

结合 lib/services/parser.go 中已落地的is_subset函数,可以推断"集合运算"方向已经部分实现,后续能力可在此基础上继续扩展。

小结

RFD 83 为 Teleport Machine ID 补齐了 OpenSSH 主机证书的自动化签发能力:通过host_cert资源上的where子句与新增的all_end_withall_equalis_subset谓词函数,把"哪些 bot 能为哪些主机签发证书"收敛为可审计的 RBAC 策略;通过 tbot 的ssh_host输出服务,以约 20 分钟的续期循环自动生成短期证书并写入磁盘,配合sshd按需重读证书的特性实现零停机轮换。理解Certificate invalid: expired的 TOFU 陷阱并配合ssh-keygen -R清理,是落地这套方案时最值得注意的运维细节。

  • 网络安全
  • 认证鉴权
  • 运维
  • 后端

【免费下载链接】teleport

The easiest, and most secure way to access and protect all of your infrastructure.

项目地址:https://gitcode.com/gh_mirrors/tel/teleport
点击查看免费下载
上一篇:基于 Model Context Protocol 构建 YouTube MCP Server:从零配置到视频、字幕、频道与播放列表的 AI 工具接入实践
下一篇:ops-tensor测试框架深度解析:自动化测试与超时控制实现

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询