跳至内容
liuzhen932 的小窝
返回

安全地使用 SSH 进行持续集成和部署

在 GitHub Actions 等 CI/CD 服务中,如果需要将构建的二进制文件自动部署到远程服务器,我们通常会使用 SSH 连接到远程服务器,然后执行特定的命令。SSH 部署虽被广泛使用,但其安全隐患往往被忽视——长期有效的静态密钥一旦泄露,便足以令整片基础设施沦陷。

本文使用 AI 辅助收集资料和创作。

2026 年 3 月 Trivy 供应链攻击事件 表明,攻击者通过凭据泄露入侵 CI/CD 流水线,恶意发布植入后门的版本,影响范围波及整个开源生态。在「密钥已泄露」的假设下重建 SSH 部署的信任模型,已成为每一位 DevOps 工程师不可回避的课题。

传统 SSH 部署

沿用至今的传统部署模式,几乎在每个环节都留下了可被攻击者利用的缝隙。

当团队规模扩大、服务器数量增加时,公钥管理迅速脱轨。新成员入职需要手动将其公钥分发到所有服务器,离职后必须逐台删除对应公钥,密钥轮换更是需要遍历整个基础设施。更致命的是,公钥本身没有有效期——一旦泄露便长期有效,缺乏内置的撤销机制。

在 GitHub Actions 等 CI/CD 服务的语境下,传统方式是将 SSH 私钥直接存入 GitHub Secrets(或者类似的东西),然后在工作流中通过如下方式使用:

- name: Deploy via SSH
  env:
    SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
  run: |
    mkdir -p ~/.ssh
    echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
    chmod 600 ~/.ssh/id_rsa
    ssh deploy@server 'cd /app && ./deploy.sh'

显然,密钥以静态形式存储在 GitHub Secrets 中,永久有效,任何获得仓库 Secrets 访问权限的人(或入侵的第三方 Action)均可窃取并长期滥用。同时,这种方式无法在密钥粒度精确控制每位运维人员的访问范围;所有使用同一密钥的人都拥有同等权限,缺乏对具体身份、有效时段和可执行命令的精细约束。

让我们继续观察一种被广泛使用、但安全代价高昂的常见模式。许多团队为了方便,会在 GitHub Actions 中借助 webfactory/ssh-agent 这个 Action 来加载一个长期 SSH 密钥:

- name: Set Up SSH Agent
  uses: webfactory/[email protected]
  with:
    ssh-private-key: ${{ secrets.GIT_SSH_KEY }}

此做法的意图是良性的:它启动一个临时的 ssh-agent 进程,将私钥加载到内存中,避免直接写入磁盘。然而,它在根本上仍依赖一个静态的、长期有效的私钥,这个私钥必须被永久存储在 GitHub Secrets 里。一旦仓库的 Secrets 访问权限被突破——无论是内部人员恶意窃取、第三方 Action 被供应链攻击污染,还是日志意外泄露——攻击者就能获取这把「永不过期」的钥匙,从而自由出入服务器。

同时,引用 webfactory/[email protected] 这样的浮动版本标签,本质上是在信任一个外部维护者不会将标签重新指向恶意代码。通常更好的建议是 Pin SHA 而不是 Pin Tag。

破局

接下来我希望从我探究到的三个维度协同发力:消除静态凭据实现短周期认证引入集中化密钥治理

使用 OIDC 消除静态凭据

GitHub Actions 内置的 OpenID Connect(OIDC)支持,是消除静态凭据的关键武器——它让工作流在运行时按需获取短生命周期令牌,从源头上解决了「静态密钥泄露」的问题。

其核心原理是:当工作流启动时,GitHub 自动生成一个 JWT 令牌,其中包含该工作流的身份信息(哪个仓库、哪个分支、哪个触发者);云厂商或第三方服务验证该令牌的签名和声明后,返回一个有效期通常仅数分钟的临时访问凭证,凭证到期后自动失效。

相比之下,传统方式要求将长期 AK/SK 手动塞入 Secrets 中,而 OIDC 模式下这些静态凭证完全消失。每一步都建立在可验证的临时信任之上——即使令牌在传输过程中被截获,攻击者也无法长期利用。

诸如 AWS、阿里云、腾讯云和华为云等主流云厂商都已支持使用 OIDC 获取短暂的访问密钥,这是一种更安全的做法,值得推广。

引入集中化密钥治理

类似 Hashicorp Vault 和 Infisical 等开源工具都可以做到「集中化密钥治理」,这里使用 Infisical 举例:

Infisical 的 GitHub Actions 集成 专门针对 OIDC 做了原生优化。其核心概念是通过 Infisical 的「Machine Identity」(机器身份)机制,将 GitHub 仓库的 OIDC 声明映射为 Infisical 中的受信身份,无需配置任何 API 密钥或 Token。

机密在工作流执行期间动态获取并作为环境变量暴露,仅在该作业的生命周期内存在。这种方法将机密管理集中在 Infisical 中,同时通过基于身份的身份验证来维护安全性。

1. 在 Infisical 中配置机密

确保工作流需要的机密存储在 Infisical 项目和环境中(例如 dev / prod

例如,一个构建并推送 Docker 镜像的流水线可能需要:

密钥的作用范围限定在某个环境中。请确保它们存在于你的工作流将要访问的环境中。

2. 使用 OIDC 身份验证创建机器身份

一个机器身份代表一个非人类工作负载(例如 CI/CD 流水线、服务器或自动化任务),并定义了该工作负载在没有绑定用户账户的情况下被授权访问的内容。使用 OIDC 身份验证创建机器身份:

  1. 在 Infisical 中导航到您的项目
  2. 前往 项目 > 访问控制 > 机器身份
  3. 点击 将机器身份添加到项目
  4. 为身份提供一个名称
  5. 选择一个组织级别的角色,该角色定义了身份可以访问的内容,例如 Viewer
  6. 点击 创建

默认情况下,身份将配置为使用 通用认证。对于 CI/CD 工作流,我们希望避免长期存在的凭证,因此将切换到 OIDC 认证。

3. 配置 OIDC 身份验证

  1. 点击您的机器身份
  2. 移除默认的 Universal Auth 配置
  3. 点击 Add auth method,然后选择 OIDC Auth

配置以下字段:

repo:<owner>/<repo>:<context>

通过仔细配置主题、受众和声明字段来限制访问。主题、受众和声明字段支持通配符模式匹配。

4. 复制身份 ID

配置 OIDC 身份验证后,Infisical 会提供一个 身份 ID。您将在 GitHub Actions 工作流中引用此 ID。

身份 ID 不是机密信息。它是一个公共标识符,可以直接提交到您的工作流 YAML 文件中,这是安全的。

5. 配置工作流

name: Build and Push Docker Image

on:
  workflow_dispatch: # Manual trigger for testing

permissions:
  id-token: write # Required for OIDC authentication
  contents: read # Required for checking out code

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Fetch secrets from Infisical
        uses: Infisical/[email protected]
        with:
          method: "oidc"
          identity-id: "your-identity-id-here" # From Step 4
          project-slug: "your-project-slug" # Your Infisical project slug
          env-slug: "dev" # Your environment slug

      - name: Login to Docker Registry
        run: |
          echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin

      - name: Build and push Docker image
        run: |
          docker build -t my-image:latest .
          docker push my-image:latest

切勿在工作流日志中打印完整的密钥值。GitHub Actions 会自动屏蔽密钥,但应避免使用 echo $SECRET 或类似会暴露值的命令。

SSH 证书体系替代传统公钥

即使 OIDC 和 Infisical 解决了静态密钥的存储与分发问题,实际连接服务器时仍需要一个「身份凭证」向 SSH 服务端发起认证。

传统做法是部署长期 SSH 密钥对,该模式本身就埋下了安全隐患。SSH 证书(Certificate)机制正是为此而生——它通过在团队或组织内部引入证书颁发机构(Certificate Authority,CA)的架构,将分散的公钥分发问题转化为集中的证书签发与信任传播问题。

SSH 证书并非取代密钥对本身,而是在用户密钥对之上叠加一层 CA 签名:用户的公钥由内部 CA 进行签名,生成一张包含身份信息和有效期限的证书;服务器端只需配置信任该 CA 的公钥,即可自动验证所有由该 CA 签发的用户证书,不再需要在每台服务器上手动维护 authorized_keys 文件。

Infisical 的 PAM(Privileged Access Management) 模块原生支持 SSH 证书管理,其定位并非仅仅作为一个密钥存储工具,而是充当一个内置的证书颁发机构。这意味着工程师不需要手动签发证书,不需要管理 CA 私钥的轮换,也不需要为每台服务器编写证书部署脚本。Infisical PAM 会自动为每次授权请求生成一个全新的、短期有效的 SSH 证书,会话结束后立即失效,从根源上消除了长期凭据泄露的风险。

由于我不想复制官方文档了,建议自行查看。

强化重点

  1. 所有第三方 Action 必须 Pin 到不可变引用。Trivy 事件最根本的教训是:标签(tags)是可变的——攻击者一旦控制 Action 仓库,可以将已有标签重新指向恶意代码,所有下游流水线在不知情的情况下继续「信任」同名标签。因此,引用第三方 Action 时必须使用完整 commit SHA 而非浮动标签,这是阻断供应链攻击的第一道硬性防线。

  2. 密钥残留是证书方案也无法完全规避的问题。即使证书有效期只有 15 分钟,CI/CD runner 在退出前仍会将私钥明文写入磁盘(~/.ssh/id_ed25519);如果未采取额外措施,该文件在离职或被入侵的 runner 节点上可能被事后恢复。解决方案包括:将敏感文件存放在 tmpfs 内存文件系统中,并在任务结束时显式 shred 删除;对于自托管 runner,必须确保 runner 实例是彻底毁灭的(不可重用),杜绝其他任务继承前次任务残留的文件句柄。

  3. Infisical 的证书审批机制可作为人工安全卡点。为高危生产环境的 SSH 证书请求配置审批工作流,只有获得指定审批者(如值班 SRE 或安全 Owner)的明确批准后,CA 才会签发证书。这在「零信任架构」的落地中尤为关键——零信任的核心原则是「默认不信任,持续验证,最小权限」,而审批工作流正是这种持续验证在 CI/CD 自动化中的具体体现。

从信任持久化到信任自动化

SSH 部署安全从来不是单个工具的事,而是信任模式的根本转变。GitHub Actions 提供了 OIDC 消除静态凭据的能力,SSH 证书体系将认证从“永久信任一把钥匙”扭转为“每次签发、自动过期、集中可控”,而 Infisical PAM 则将二者工程化整合,让复杂的 PKI 基础设施变得可触及,并引入审批工作流作为安全合规的最后一道人工卡点。

GitHub Actions 与 Infisical 都提供了立即可用的安全原语——OIDC 认证只需在 workflow 中声明权限即可启用,SSH 证书机制在 OpenSSH 5.4+ 版本中已原生支持无需额外软件——它们需要的不是更多的技术复杂度,而是安全心智的转变:把「信任」从一把永不过期的钥匙,重构为每一次短时效的、可验证的、有边界的临时授权。


分享这篇文章:

上一篇
命令行必备的小工具和小技巧
下一篇
在 Debian 13 上部署 OpenGrok 源代码浏览工具

人机验证:请刷新页面以加载评论区