读零信任网络:在不可信网络中构建安全系统12源代码和构建系统
1. 建立应用信任
1.1. 软件正在吞噬整个世界
1.2. 零信任网络需要关注应用程序的安全性,这似乎违反直觉,毕竟网络是不可信的,因此可以预见网络上存在不可信的应用
1.3. 运行在数据中心的软件堪称一切魔法之源,因此,毋庸置疑,任何人都期望软件能按照预期运行
1.4. 在可信设备上运行的代码能够准确地执行,设备可信是代码可信的前提
1.5. 实现设备可信只完成了一半,同时还必须信任代码本身和编写它们的程序员
1.6. 建立代码信任的要求
-
1.6.1. 确保创建代码的人可信
-
1.6.2. 确保从代码生成应用的过程可信
-
1.6.3. 确保应用被部署到基础设施的过程可信
-
1.6.4. 持续监控可信应用,防止应用程序被恶意程序所操纵
2. 应用流水线
2.1. 采用流水线可将可信开发人员编写的软件转换成应用并最终部署到基础设施中
2.2. 在计算机系统中,代码的创建、交付和执行构成了一条非常敏感的事件链
-
2.2.1. 每个步骤中都存在攻击向量,潜在的破坏难以察觉
-
2.2.2. 必须确保事件链中每个环节的潜在破坏都能够被监测
-
2.2.3. 为支撑软件交付链的安全,整个过程的每个环节都需要完全审计并且在关键节点进行加码验证措施
2.3. 4个不同阶段
-
2.3.1. 源代码
-
2.3.2. 构建/编译
-
2.3.3. 分发
-
2.3.4. 执行
3. 源代码
3.1. 编写源代码是软件运行的第一步
-
3.1.1. 如果开发人员不可信,那么其编写的源代码就很难得到信任
-
3.1.2. 即使经过了仔细的代码审查,恶意开发人员仍然有可能在众目睽睽之下故意植入恶意代码(并隐藏!)
-
3.1.3. 非恶意的开发人员也可能在无意中造成应用缺陷
-
3.1.4. 零信任网络其实更专注于识别恶意使用,而不是去除这部分用户的信任
3.2. 源代码存储在一个集中的代码库中,许多开发人员在上面交互并提交代码
- 3.2.1. 这些代码库也必须严格控制,尤其是在允许构建/编译系统直接访问代码库的情况下,更不可掉以轻心
3.3. 保护代码库
-
3.3.1. 对于代码库的防护,传统的安全措施依然有效,并且还有更多的高级安全措施可以考虑,其中包括一些基本安全原则,比如最小权限原则,用户只能获得完成任务所必需的代码库访问权限
-
3.3.2. 对于集中代码库而言,传统的安全防护机制依然有效
-
3.3.3. 虽然传统安全机制依然有效并值得推荐,但随着分布式源代码控制的引入,情况会有所不同
-
3.3.3.1. 由于代码可以通过多种方式进入分布式代码库,因此安全问题变得更加难以解决
3.4. 验证代码和审计跟踪
-
3.4.1. 许多版本控制系统(Version Control System,VCS),特别是分布式VCS,使用加密技术存储源代码历史记录
-
3.4.1.1. 这种技术被称为内容寻址存储,在数据库中使用内容的加密散列作为该对象的标识,而不是位置或坐标
-
3.4.1.2. 通过这种方式计算源文件散列,并将其作为源文件在数据库中存放的标识,可以确保源文件中的任何更改都会有新的散列对应
-
3.4.1.3. 这种机制意味着文件的存储是不可变的:一旦存储,就不可更改
-
3.4.2. 一些VCS系统对内容寻址存储机制进行了完善,将历史数据本身存储为对象
-
3.4.2.1. Git
> 3.4.2.1.1. 将历史提交记录存储为一个有向无环图(Directed Acyclic Graph, DAG)
> 3.4.2.1.2. 将“提交”本身作为数据库中的对象,存储提交时间、作者和前序提交标识等细节
> 3.4.2.1.3. 将前序提交的加密散列存储在每个提交记录上,可以形成一棵Merkle可信树,通过加密方式验证整个提交链是否未经修改
> 3.4.2.1.4. 随着源代码历史记录被分发给贡献者,系统会获得一项好处:不可能在其他贡献者不注意的情况下改变历史记录
> 3.4.2.1.5. 以这种方式存储DAG能够确保历史数据不被篡改——要彻底改变历史是不可能的
> 3.4.2.1.6. 这种存储并不能确保新的提交是经过授权且真实的
> 3.4.2.1.6.1. 基于受信任开发人员的推送访问权限,这个恶意提交就出现在存储库中了
> 3.4.2.1.6.2. 恶意的提交者可以在该字段中放置他们想要的任何细节
> 3.4.2.1.7. 为防范这种攻击向量,Git允许使用受信任开发人员的GPG密钥对提交和标签进行签名
> 3.4.2.1.7.1. 标签指向特定历史中起始提交的位置,可以使用GPG密钥来签名以确保某次发布的真实性
> 3.4.2.1.7.2. 对提交进行签名能够进一步验证整个Git的历史记录,这使得攻击者除非先窃取提交者的GPG密钥,否则无法假冒其他提交者
> 3.4.2.1.8. 签名的源代码提供了显著的好处,应该尽可能地使用它
> 3.4.2.1.8.1. 不仅为人类提供了健壮的代码验证,也为机器的代码验证提供了鲁棒性
> 3.4.2.1.8.2. 一个完整签名的历史数据允许构建系统在编译部署之前对代码进行身份验证
> 3.4.2.1.8.3. 第一个签名的提交实际上默认信任之前的所有提交
3.5. 代码审查
-
3.5.1. 为单个用户赋予过大权限非常危险
-
3.5.2. 签名能够使我们对提交代码的开发人员进行身份验证,但不能确保其提交的代码是正确或安全的
-
3.5.3. 对开发人员赋予信任并不意味着开发人员就可以单方面将代码提交给敏感项目
-
3.5.4. 在代码审查过程中,所有的贡献都必须经过一个或多个额外的开发人员的批准
-
3.5.4.1. 这个简单的过程不仅极大地改善了软件的质量,还降低了有意或无意的漏洞引入率
4. 构建系统的信任
4.1. 构建服务器经常受到持续威胁的攻击
- 4.1.1. 构建服务器具有更高的访问权限,并能够生成直接在产品中执行的代码
4.2. 在构建阶段难以发现代码的篡改和恶意代码的植入,因此,对构建服务实施强有力的保护非常重要
4.3. 风险
-
4.3.1. 评估
-
4.3.1.1. 构建所用的源代码是符合预期的
-
4.3.1.2. 构建流程/配置是符合预期的
-
4.3.1.3. 构建的执行本身安全可靠,未被操纵
-
4.3.2. 构建系统可以输入签名代码,编译结果也可以签名输出,但输入输出之间的过程(构建本身)却缺乏可靠的加密保护
-
4.3.2.1. 这是构建系统的重要攻击向量
-
4.3.2.2. 如果缺乏正确的处理和验证,那么这类攻击和破坏就很难被发现,甚至根本不可能被发现
-
4.3.2.3. 构建配置及其执行缺乏密码技术保护,这个环节的断裂蕴含了巨大的威胁,是一个强大的攻击向
-
4.3.3. 考虑到构建过程的敏感性,如果要将这项工作外包,则务必谨慎评估
-
4.3.3.1. 准确评估你成为高价值目标的可能性,从而决策是否构建外包
-
4.3.4. 可重现构建这样的技术可以帮助识别这方面的攻击和破坏,但却无法防止其分发
-
4.3.5. 构建服务器本身的安全性很重要
-
4.3.5.1. 如果构建服务器被破坏,那么它就不能再被信任来忠实地履行职责
-
4.3.5.2. 可重现构建、不可变的主机和零信任模型本身可以在这方面有所帮助
4.4. 对输入输出建立信任
-
4.4.1. 如果将构建系统看作是一个可信操作,那么为了产生可信输出显然需要首先信任构建系统的输入
-
4.4.2. 作为版本控制系统的使用者,构建系统负责验证源代码的信任度
-
4.4.2.1. 应通过一个经过身份验证的通道(如TLS通道)来访问版本控制系统
-
4.4.2.2. 为了获得额外的安全保障,标签和/或提交应被签名,构建系统在启动构建之前应该首先验证这些签名或者签名链
-
4.4.3. 构建系统的另一个重要输入是构建配置
-
4.4.3.1. 对构建配置的攻击可能会导致构建系统链接到恶意库
-
4.4.3.2. 即使是看似安全的优化选项,在关键的安全相关代码中也可能是恶意的,比如定时攻击缓解代码可能会因为代码优化而意外失去作用
-
4.4.3.3. 将构建配置置于源代码管理之下,可以通过提交签名来对其进行版本控制和验证,从而确保构建配置也是可信输入
-
4.4.4. 构建系统需要为生成的目标程序进行签名,以便下游系统能够验证其真实性
-
4.4.4.1. 保护构建生成的目标程序和散列,将其分发给下游使用者,即可完成构建系统的可信输出
4.5. 构建的可重现性
-
4.5.1. 可重现构建是防止构建流水线被破坏的一个较好的工具
-
4.5.2. 支持可重现构建的软件采用确定的方式进行编译,以保证无论给定的源代码由谁构建,其生成的二进制文件都完全相同
-
4.5.3. 允许多方检查源代码并生成相同的构建,从而确信用于生成某个二进制文件的构建过程没有被篡改
-
4.5.4. 可以很容易地检测到构建过程中的恶意干扰或代码注入,将其与源代码签名结合,能够实现一个健壮的流程,对源代码和由它产生的目标二进制文件进行验证
-
4.5.5. 可重现构建听起来很容易,但重现每个字节都相同的目标二进制文件非常困难
-
4.5.5.1. 发布系统在虚拟文件系统(Chroot Jail, Chroot“监牢”)中保存了所有的历史构建包,并涵盖构建配置中所需的所有依赖项
-
4.5.5.2. 虚拟机或容器可以是确保构建环境与运行构建的主机完全隔离的有用工具
4.6. 解耦发布版本和工件(Artifact)版本
-
4.6.1. 不变性(Immutable)构建对于确保构建和发布系统的安全性至关重要
-
4.6.2. 如果缺少不可变构建机制,那么可能导致一个已知的“好”版本被替换掉,从而为针对底层构建工件的攻击敞开大门,这会使得攻击者将“坏”版本伪装成一个“好”版本
-
4.6.3. 构建系统生成的工件应该遵循“单写多读”(Write Once Read Many)的原则
-
4.6.4. 鉴于工件的版本不变性需求为其版本管理带来了不小的压力,许多项目倾向于使用有意义的版本号(如语义化版本号),以便告知使用者升级软件可能导致的潜在影响
-
4.6.5. 使用一个补丁级别的版本重新发布,或者修改规则并使用一个新的构建工件以相同的版本号重新发布
-
4.6.5.1. 在许多项目中会选择第二种方式,宁愿确保市场版本发布的版本号和当初宣称的一致,也不重新变更版本号
> 4.6.5.1.1. 显然不是一个好的选择
-
4.6.6. 在创建一个构建系统时,最好让其能独立产生一个不可变的版本(号),而不要受到发布版本号的影响
-
4.6.6.1. 可通过下游系统(如版本分发系统)管理发布版本和工件版本之间的映射关系,这样可以确保在不牺牲可用性或引入不安全实践的前提下维护工件版本的不变性