一种将秘密值从你的版本库传递到Travis CI系统的方法

114 阅读9分钟

几年前,Travis CI引入了一种将秘密值从你的版本库传递到Travis CI系统的方法。这种方法依靠加密来确保任何人都可以提供一个新的秘密,但只有CI系统本身可以读取这些秘密。我一直认为Travis处理秘密的方法是最好的方法之一,并对其他CI工具继续使用更标准的 "在网页界面中设置和更新秘密 "的方法感到失望。(我们稍后将讨论加密秘密方法的优势)。

快进到今年早些时候,对于运行Kube360部署作业,我们发现秘密在CI-web-interface的方法根本无法扩展。因此,我黑了一个快速脚本,使用GPG和对称密钥加密来加密一个secrets.sh 文件,其中包含CI的相关秘密(或者,在这种情况下,真的是CD)。这很有效,但也有一些缺点。

几周前,我终于咬牙重写了这个丑陋的脚本。我没有使用GPG和对称密钥加密,而是使用了 sodiumoxide和公钥加密。这基本上解决了我在CD设置中的所有痛点。然而,这个工具在很大程度上是为Kube360定制的。

上周末,我把这个工具的通用组件提取到一个新的开放源码库。这篇博文宣布了Amber的第一次公开发布,这是一个面向CI/CD系统的工具,用于更好地管理长期的秘密数据。在该版本中,有基本的信息来描述如何使用该工具。这篇博文旨在更详细地介绍为什么我认为加密的秘密是比网络秘密更好的方法。

痛点

标准的CI秘密管理方法有两个主要问题:

  1. 在web界面内管理大量的值是很乏味的。我个人曾犯过复制粘贴值的错误。如果你需要为测试目的在本地运行一个脚本,那么每次复制所有的值是一个更大的痛苦。(下面有更多关于这个问题的内容)。
  2. 秘密值随时间变化是完全合理的。然而,在进入CI系统的源码库中没有这方面的证据。相反,这些变化是不透明的,永远无法观察到它的变化,也无法用原始值忠实地再现旧的构建。(这与我们相信你的CI构建过程应该在你的代码库中的原因非常相似)。

在版本库中使用加密值,这两件事都会改变。添加新的加密值现在是一个命令行调用,对我们中的许多人来说,这没有那么繁琐,而且比网页界面更傻瓜化。加密的秘密存储在 Git 仓库本身,所以随着时间的推移,数值的变化,文件会提供这一事实的证据。从仓库中检查出一个旧的提交,你就可以用与提交时完全相同的秘密重新运行构建。

为什么是公钥

我对上面提到的GPG脚本所做的重要改变之一是公开密钥,而不是对称密钥的加密。在对称密钥加密中,你使用相同的密钥来加密和解密数据。这意味着,所有想把一个值加密到版本库的人都需要访问一段秘密数据。虽然加密新的秘密值并不是那么常见的活动,但要求访问该秘密数据是最好避免的。

相反,通过公钥加密,我们生成一个秘密密钥和公钥。公钥住在存储库中,与秘密本身在同一个文件中。有了它,任何能够访问版本库的人都可以加密新的值,而没有任何能力读取现有的值。

此外,由于公钥在存储库中可用,Amber能够执行理智检查,以确保其秘密密钥与存储库中的公钥相匹配。虽然我们使用的加密算法提供了确保消息完整性的能力,但这种自我检查提供了更好的诊断方法,明确区分 "消息被破坏 "和 "看起来你为这个存储库使用了错误的秘钥"。

尽量减少延迟

Amber 针对 Git 仓库的情况进行了优化。这包括希望在更新密匙时尽量减少延迟。这导致了三个设计决定:

  • 配置文件的格式是YAML。它对空格敏感的格式使它成为一个很好的选择,可以在更新秘密时尽量减少影响的行数。虽然其他格式(如TOML)也是很好的选择,但我坚持使用YAML,因为根据经验,它似乎对希望编写配套工具的人有更强的整体语言支持。

  • 除了存储秘密名称和加密值(密码文本)外,Amber还包括秘密的SHA256摘要。这意味着,如果你将相同的值加密两次,Amber可以检测到这一点,并避免生成新的密码文本。这有一个额外的好处,那就是让用户在无法解密文件的情况下检查他们是否知道秘密值。

  • 这种数据的最自然的表示方法是YAML映射,类似于:

    secrets:
      NAME1:
        sha256: deadbeef
        cipher: abc123
    

    然而,在大多数语言中,映射中的键的排序是任意的。这使得读取这些文件变得更加困难,并意味着任意的小改动可能会导致大的延迟。相反,Amber将秘密存储在一个数组中:

    secrets:
    - name: NAME1
      sha256: deadbeef
      cipher: abc123
    

这一切共同实现了对我来说是版本库中秘密的目标:你可以在git diff ,很容易看到哪些秘密值被添加、删除或更新。

本地运行

理想情况下,生产部署只能从官方指定的CI/CD系统中运行然而:

  1. 在开发过程中,有时从你的本地系统进行非生产性部署,这对迭代来说要容易得多。
  2. 作为一个现实主义者,我不得不承认,即使是最好的DevOps团队,偶尔也需要为了权宜之计或更好地调试生产问题而弯曲规则。

对于Kube360来说,对于一个标准的部署来说,有十几个秘密值也不是没有道理的。每次你想调试一个问题时,复制/粘贴所有这些到你的本地机器是不可行的。这鼓励了一些最坏的做法,比如把秘密值放在本地的纯文本shell脚本文件中。对于一个开发集群来说,这并不是世界上最糟糕的事情。但开发中不严密的安全实践往往很容易渗入到开发中。

从CI秘密或团队密码管理器中复制一个单一的秘密值是一个完全不同的故事。在调试会话开始时,这需要30秒。我觉得这样做没有什么反对意见。

甚至这可能是我们可以用云秘密管理器绕过的东西,我将在下面提到这一点。

这个名字是怎么回事?

我们都知道,计算机科学中有两个难点:

  1. 缓存失效
  2. 命名问题
  3. 逐一错误

我把这个工具命名为 "琥珀",是基于《侏罗纪公园》,以及一些非常重要的数据(恐龙的DNA)被困在沉积物层下的琥珀中的想法。这与我想在Git仓库的提交中存储加密的秘密的想法非常吻合。但由于我刚刚玩完《塞尔达传说:天空之剑》,一个更合适的形象似乎是。

实施

我用Rust编写了这个工具。目前它是一个相当小的代码库,只有445 SLOC的Rust代码。这也是一个相当简单的整体实现,如果有人对第一个项目感兴趣的话,可以为之做出贡献。

未来的改进

未来的改进将由FP Complete的内部和客户需求,以及我们在问题跟踪器和拉动请求上收到的反馈来驱动。我有几个想法,从具体到模糊的增强功能:

  • 屏蔽值。目前,amber exec 将简单地运行子进程,完全不修改其输出。一个标准的CI系统功能是屏蔽输出中的秘密值。在Amber中实现这样的改变应该是很简单的。(问题#1)
  • 与云秘密管理系统的联系。目前,Amber的秘密密钥的唯一来源是通过环境变量。在许多用例中,从秘密管理器(如AWS Secrets Manager或Azure Key Vault)抓取数据将是一个更好的选择。特别是,在部署过程中,这可以允许将对秘密的访问委托给现有的云原生权限机制。有关更多信息,请参见问题2拉动请求4。一种可能的方法是遵循基于公钥命名秘密的模式,导致发现秘密密钥的零配置方法(因为公钥已经在存储库中)。
  • 额外的平台支持。目前,我们正在为Linux(通过musl静态)、Windows和Mac上的x86-64构建可执行文件。Rust的交叉编译支持非常好,这也是我喜欢用Rust编写CI工具的原因之一。然而,sodiumoxide 库依赖于libsodium ,所以需要额外的GitHub Actions设置来使这些构建工作。
  • 自动生成密码。在我们的Kube360工作中,一个常见的需求是生成一个临时密码,供系统中的不同组件使用(例如,身份提供者和服务提供商都使用的OpenID Connect客户端秘密)。一个简单的amber gen-password CLIENT_SECRET 子命令可能不错。
  • 我还没有把这段代码发布到crates,但如果有兴趣的话,我很乐意这样做。
  • 除了加密的环境变量外,还支持加密的文件。我还没有真正考虑过这个接口是什么样子的。

开始使用

在Repo里有关于开始使用Amber的说明基本步骤是:

  • 发布页上下载可执行文件或自己构建。
  • 使用amber init ,创建一个amber.yaml 文件和一个密匙
  • 将秘钥存放在安全的地方,比如你的密码管理器,另外,在你的CI系统的秘钥中也要存放。
    • 理论上说,这是你在那里存储的最后一个值!
  • 用添加你的秘密amber encrypt
  • amber.yaml 提交到你的版本库
  • 修改你的CI脚本以下载Amber可执行文件,并使用amber exec 来运行需要秘密的命令。