随着互联网越来越受重视,前端开发不再是简单的实现一个界面,使用 Javascript 让页面有一定的交互特效。在同一个时期的迭代里,我们可能需要同时开发浏览器应用、桌面端,甚至是 App、小程序等等。导致了我们迫切的需要考虑一种新的方式,优化我们前端的开发工作。而 CI/CD 是工程化的重要环节之一。
为什么需要 CI/CD ?
我们每次项目迭代过程中都会听到的各种抱怨:来自测试的抱怨、开发的抱怨,甚至是技术主管、运维的抱怨......
时间一长,很可能会导致同一个项目组的成员关系越来越差,项目的质量也不会好。项目迭代过程中常伴随着以下 5 大现状:
或许有人会说:项目发版一年只有那么几次,比起项目快速的迭代,搭建 CI/CD 系统只是一件必要但是不紧急的事情。
我们先来看看 GitLab 2020 DevSecOps 的调查数据统计:
频繁的发版,可能导致我们每天都得耗在发版里,根本没有时间做新的迭代。这尚且是 2020 年的数据统计,如今已是 2022 年,发版只可能更加频繁。
那么,搭建 CI/CD 系统还是一件不紧急的事情吗?
什么是 CI/CD ?
CI/CD 起源于 70 年代,软件工程的概念被提出,告诉我们不仅需要会开发软件,还需要系统的、规范的开发和维护软件,这标志着工程化意识的觉醒。直到几十年后,2015 年比尔团队的《凤凰项目: 一个 IT 运维的传奇故事》这本书才介绍了 CI/CD 的雏形。现今,CI/CD 已被广泛地提起以及应用。
从字面意思理解,CI/CD 是由两部分组成的。CI 指代持续集成,是指我们 Push 代码后对代码进行的一系列质保实践。通过持续集成,我们可以更早地识别和修复错误以及安全问题。CD 是由持续交付和持续部署组成。简单理解是上线过程的一组实践,减少人为误操作的风险。简单的理解就是,CI/CD 是持续集成和持续交付结合的一组实践。
传统上我们将新代码从提交到生产中所需的大部分或全部都是人工干预,例如构建、测试和部署,以及基础设施的配置等等。而 CI/CD,是将一切都自动化了。使用 CI/CD 管道,开发人员只需将更改后的代码 Push 上代码仓库,然后 CI/CD 管道会自动构建和测试,最后进行交付和部署。
深入了解 CI/CD
回顾完整的 CI/CD 过程图,我们可以发现版本控制和自动化测试是整个 CI/CD 管道中重要的两个环节。
版本控制在 CI/CD 中主要用于触发 CI/CD 流水线,它还有个分支管理策略,用于针对不同的环境的特殊场景做隔离处理。
自动化测试主要是使用自动化的手段去执行测试,包括单元、集成、性能和验收等等。它可确保假设某一环节测试失败了,则阻止将功能部署到生产中,并且提升了我们代码本身的质量等等作用。
以下是 CI/CD 搭建的基本原则:
搭建 PC 项目的 CI/CD 管道
怎么去搭建 PC 也就是 electron 项目的 CI/CD 管道呢?
以 Eolink Apikit 项目作为示例,以下是搭建 CI/CD 系统的步骤:
对 Web 应用来说,相对好一些,市面上很多现成的 CI/CD 方案能够参考。但是 PC 由于存在各种难点,例如需要绑定机器资源、代码签名等等,导致了它的 CI/CD 构建在一定程度上难以解决,并且市面上相关的资料也比较少,于是我们花了半个月时间,逐个去攻破难点,形成了现在的方案。
Eolink Apikit 项目的痛点,除了前面提到的缺乏可见性不存在之外,其他四个痛点:项目交付周期长、项目质量参差不齐、重复的执行测试、部署等操作以、发版等待时间过长,都一一具备。
难点
在 Eolink 的项目中,自动化测试是由三个环节组成,分别是:
因为单元测试、质量&安全检测由于都是对代码的扫描以及测试,所以不存在表现不一致的情况。而端到端测试 ,它跟操作系统是有一定关系的,例如 Windows 下,关闭按钮在右上角,而 Macos 是在左上角。
那么,我们是怎么解决的呢?当时我们是在端到端测试的入口配置中引入了 Os 这个库,通过配置 TestMatch 这个字段解决的,例如当前运行端到端测试的操作系统是 Windows 时,我们的 TestMatch 设置匹配文件名的规则为 *.windows.spec.ts。这样,对于依赖操作系统的用例场景,我们就可以快速的独立处理了。
为什么我们不建议直接在具体的用例脚本里面引入 Os 包呢?因为其实我们绝大部分的场景用例都是通用的。所以针对它们,我们应该直接在 CI/CD 管道机器上运行。
而针对特殊的场景用例,我们在构建完代码后,将它们和构建包一起分发到对应的操作系统,之后再在构建程序前跑一下就可以了。
针对第二个难点 “代码签名需要物理硬件”,我们先看一下不进行代码签名会怎么样?
上面两张图就是假设没有进行代码签名,Macos 和 Windows 各自的表现。虽说可以忽略,但是总归对公司形象不太友好。Macos 代码签名证书是电子凭证的,所以我们构建应用程序的时候只需要存在证书文件 ,使用 P12 & 密钥就可以了。
Windows 才是问题的所在,首先我们选用的 Windows 签名证书是 EV 代码签名,相较于标准代码签名,它不需要当应用程序下载量达到某一个值时才可以生效,并且可以直接对内核模式驱动签名。
不过坑就随之而来了,它的证书是存储在一个称为 Yubikey 的硬件里的。如果你要随时随地签名,那么你就需要时时刻刻把你的 Yubikey 带上。
这也有办法解决,我调研了很多 EV 签名的公司,发现 ssl.com 是可以进行云签名的。但是,也因为是独此一家,它的费用比较高,100$ 每个月。假设你不介意,这也是个不错的选择。
难点三 “构建应用和操作系统强绑定” ,主要由于我们的 PC 框架是 electron,假设我们需要打 Windows 应用程序,就得先拥有一台 Window 操作系统的物理机器 ,再在里面打包我们的 electron 项目。Macos、Linux 也是以此类推,得在对应的操作系统上打包,否则就会报错。
难点四的 “通信” 问题,主要是由于我们物理机器和 CI/CD 管道所在的服务器不是同一台服务器所导致的。最后是通过将打包的物理机器和 CI/CD 管道环境用 Open 形成内网解决。
难点五 “Web 应用和 PC 应用代码平滑同步” ,则是由于 Eolink apikit 项目是同时存在 Web 和 PC 两个应用的,它们的代码大部分相同,只有个别体验可能不太一样。因此,我们不能完全使用一套代码,这就不属于 CI/CD 的范畴了。
解决方案
主要问题在工具选型上,市面上的 CI/CD 工具很多,但是根据我们想要的形态,可直接归为两类,Github action 以及其他。
Github action 为什么可以独占一类呢?Github action 是 Microsoft 的一个较新的 CI/CD 平台,支持运行在 Linux、MacOS、Windows ,甚至是 ARM 运行器上。它之所以可以独占一类是因为它巧妙的运用了 Matrix ,也就是矩阵策略。前面我们提到 “应用程序构建和操作系统强绑定” 这一难点, Github action 让我们可以直接无视它。Github action 的 Job 是支持绑定一个叫 Os 的变量的,表达的意思是你可以是在这款操作系统上运行你的任务,可以设定为 Macos 、Windows 、Linux 等等。
最近一项新的 API 开源项目的 Eoapi ,它的 CI/CD 流水线就是使用 Github action 搭建的。
但是 Eolink Apikit 项目并不使用 Github action ,为什么呢?最主要的的原因就是 Windows 签名硬件这个坑。其他的 CI/CD 工具基本是大同小异,选择用哪个取决于开发团队的需求,毕竟适合自己的才是最好的。Eolink apikit 项目使用的代码仓库管理工具是 Gitlab ,秉着一路走到底的原则,选择的 CI/CD 工具也是 Gitlab。
上图是 Eolink apikit 最终设计的管道流程,当开发 Push 代码后,将会自动触发 CI/CD 流水线,根据我们设定好的分支管理策略将流拆成两条。针对临时分支,开发可能会经常 Push 。这种时候,我们就不在流水线上做任何测试覆盖率的要求。针对其他分支,我们将自动构造和单元测试、质量检测并行执行。在这个过程中,单元测试覆盖率是要求 80%,质检需要各个指标为 0。后面就是常规的端到端测试,覆盖率同为 80%。都没问题的话,就直接将编译好的代码和针对操作系统的用例分发给各个操作系统,以及最后上传代码。过程中假设出现任意错误,就会马上停止后面的流程。自动触发告警,并将相关的错误信息发送给提交者。
从以上「流水线执行后的预览效果」图我们可以看到详细的执行步骤,如果还没执行完,还可以看到当前哪些 Job 正在执行,哪些 Job 在 Pending。点击每个 Job ,可以看到具体的 Job 执行信息,发生错误还可以针对这个 Job 进行重试。
最后,我们告警系统接入的是飞书机器人。假设执行过程中发生任意错误,都会通过飞书机器人发送通知给相应人,让他们马上回来调整。
关于 electron CI/CD 管道的搭建,总结了几条建议,分享给大家:
CI/CD 管道安全的实践
我们为什么需要关注 CI/CD 管道安全呢?
从数据中,我们可以看到 2021 年世界上的软件供应链攻击增加了 6 倍多(650%)。同时 Gartner 也预测,到 2025 年全球会有将近 45% 的企业遭遇攻击。
CI/CD 管道攻击就属于供应链攻击。CI/CD 是我们软件开发周期的重要组成部分,假设我们忽略它的安全,就有可能被人为攻击管道漏洞,直接窃取我们的敏感信息,甚至是交付恶意代码。典型的案例有 2020 年 12 月的 Solar Winds 供应链攻击事件,以及去年的攻击者入侵了数千名开发人员使用的 Bash 上传器的 Codecov 供应链攻击事件等等。诸多案例告诉了我们,提高 CI/CD 管道的安全性迫在眉睫。
应该怎么做才能避免呢?关键在于我们需要知道具体有哪些可能性风险,才能去逐一攻破。在此,我们结合信息安全三要素进行初步的分析,可以了解到 CI/CD 管道涉及敏感数据泄露是造成风险的主要因素,如 IP 、密钥泄漏或者是漏洞的披露,而源码被植入后门、恶意挖矿或者是其他恶意行为是供应链攻击的主要一环。
我们总结出了十大 CI/CD 安全风险:
风险 1
不完善的流量控制机制导致的风险。我们搭建 CI/CD 关注的更多是怎么提效,往往会忽略它的安全。例如攻击者会利用 CI 中分支保护规则的漏洞,绕过审查去发布恶意代码。典型的案例是去年 4 月份在 PHP github 仓库中植入后门的事件,以及上年 10 月份攻击者使用 GitHub Actions 漏洞绕过审查机制的事件。
我们应该怎么去防范呢?老话说得好,吃哪补哪,所以针对这个风险最好的方式是建立完善的管道流量控制机制,以确保没有人或者软件能够在没有验证的情况下通过管道传送恶意的代码或者软件。例如,我们可以在受保护的分支上配置分支保护规则,所有用户提交的代码都得经过它去做审查才能去发布。
风险2
假设我们的 CI/CD 环境存在很多身份凭证,不管是授予机器的还是人的。一旦管理好,比如为了前期减少沟通成本,我们将所有账号都赋予管理员权限,就有可能被攻击者利用,想干什么就干什么。
典型案例是 2019 年 Stack Overflow TeamCity 发生了一起安全事件,当时出现了一个没有人认识的账号,获得了 Stack Exchange 网络中所有站点的审核者和开发人员级别的访问权限。官方追踪后,发现是因为新注册的账号在访问系统时会被自动分配管理员权限。
由此可见,我们需要避免创建本地账号。或者,我们可以使用像 Idap 这种集中式企业工具创建和管理账号。有人会说我不管,我就得创建本地账号。那么我们就需要确保能够定时清理账号,以及所有账号的安全策略需要与企业策略是一致的。
风险3
相信绝大部分团队都为项目引用过一些第三方开源组件,因为比起自己去实现,第三方开源组件有时候会考虑得更加全面一些,并且直接引用也更快。但是,我们千万不要去滥用它。
首先,我们没法保证引入的第三方开源组件是没有漏洞的。像上年的 Apache 日志控件被爆出远程代码执行漏洞,连这么稳定的包都可能有漏洞,其他的又怎么能确保完全安全呢。
其次,我们没法知道第三方开源资源的贡献策略是怎么样的,是否做了安全检测。这导致攻击者有可能拥有访问开源组件仓库的权限,可以直接为开源组件添加恶意后门程序,并对外发布。
这样,很容易引发大规模的供应链攻击。攻击者可以利用该后门对 CI/CD 环境进行探测,进而导致整个环境沦陷。
最后,在 CI/CD 管道中,我们通常会引入一些第三方工具对项目进行管理。例如 Nodejs 项目中,通常会引入 Npm 仓库,若项目直接从 Npm 中央仓库去拉组件,就无法确定是否会引入了含有漏洞的组件,进而可能导致组件漏洞被攻击者利用。
针对这种风险,我们建议首先梳理项目中所有依赖的开源组件,可以通过 SBOM 进行梳理,并采用软件成分分析工具对我们引入的开源资源进行漏洞扫描。当项目中引入了新的开源资源时,也能够具备针对性的安全管控措施。
风险4
我们称为 PPE,是“中毒”管道的执行所导致的,主要是中了攻击者恶意代码或者恶意命令的毒。
根据 “中毒” 的手段,PPE 分为以下三种类型:
第一种是攻击者直接修改有权限访问的管道配置文件,在管道运行中触发恶意命令来达到攻击我们的效果,我们又称之为直接 PPE (D-PPE) ;
第二种是攻击者通过向管道配置中所引用的文件注入恶意代码,来间接的毒害管道;
最后一种是攻击者假设需要通过获取身份凭证来访问管道配置文件,那么他可以通过破坏公共项目达到攻击私有项目的效果,从而挖掘更多的敏感信息。
典型案例是上年 GitHub Actions 通过包含恶意代码的拉取请求滥用来挖掘加密货币事件。具体的解决方案是需要从之前对代码的审查,改进为现在同时需要对管道配置文件也进行审查,甚至是定时监控管道的运行情况。
风险5
基于管道的访问控制不足所导致的风险。它的存在将导致攻击者直接将一段恶意代码注入到管道执行节点的上下文中,这样,恶意代码就可以具有运行管道阶段的完全权限,可以访问机密信息、访问底层主机,甚至访问连接到相关管道的任何系统。毋容置疑的将会导致我们机密数据泄露、CI 环境内的横向移动,以及被恶意软件部署到我们管道中,甚至是发布到生产环境中。
Codecov 事件中,就是因为疏于防范导致 Codecov 最终被破坏并用于从构建中窃取客户的环境变量。我们要怎么规避它呢?重点就是做好权限管理,例如 CI/CD 管道作业在控制器节点上的权限应该设置有限。
风险6
假设我们已经拥有了身份和访问认证机制了,但是凭证管理不妥当也是会导致风险的。例如,开发将包含凭证的代码推送到代码仓库中,不管是有意还是无意的,这都将会导致我们将凭证暴露给对代码仓库具有访问权限的人。即使后面我们发现不对,马上将它从被推送到的分支上删除,他们还是会在提交历史记录中出现。调试时将凭证打印到控制台中,可能会使凭证以明文方式在日志中公开,任何有权访问构建结果的人都可以查看。这些日志后面也可能会流入到日志管理系统中,从而扩大了凭证的暴露面。
回看Codecov 攻击事件,攻击者就是通过破坏 CI 中的凭证,去窃取了存储为环境变量的数千个凭证。解决方案中最重要的就是,不要在 CI/CD 环境中存储任何敏感的信息,至于其他都是次要的。
风险7
CI/CD 环境中不安全配置的系统导致的风险。攻击者利用有漏洞系统的安全漏洞来获得对系统未经授权的访问,或者更糟的情况是,破坏了系统并访问其他底层操作系统。
都有哪些不安全的操作系统呢?例如使用过时版本或缺少重要安全补丁的自我管理系统。或者是对底层操作系统具有管理权限的自托管系统。 SolarWinds 构建系统的入侵就是典型的案例。
除了以上的防范手段,还需要去定时为系统打补丁。
风险8
和第三方开源资源滥用一样,属于无监管使用导致的风险。缺乏对第三方服务的治理,会严重影响企业在其 CI/CD 系统维护管道中对于角色访问控制的操作,企业也会变得很被动,安全性得取决于它们实施的第三方。而第三方的最小特权,加上围绕第三方实施过程的最小治理和尽职调查,都会显著增加企业的攻击面。
鉴于 CI/CD 系统和环境的高度互联性,假设第三方的漏洞被利用,造成的危害是远远超出第三方所连接的系统范围的损害的。例如,具有写入权限的第三方存储库,攻击者可以将恶意代码推送到存储库,第三方存储库反过来又会触发构建,并在构建系统上运行攻击者的恶意代码。
这个风险防范手段很简单,主要是围绕第三方服务的治理控制,我们应在第三方使用生命周期的每个阶段去实施。
风险9
CI/CD 流程是由多个步骤组成的,最终负责将代码从仓库一路带到生产环境。每个步骤都可能有多个资源,最终软件包依赖于分布在不同步骤中的多个来源,它们是由多个贡献者提供的,从而让攻击者有可能通过这些入口点去篡改最终的软件。如果被篡改的软件成功地渗透到交付过程中,而不引起任何的怀疑或者没有遇到任何安全检测,它很可能就会直接以合法资源的名义继续流经我们的管道,一直发布到我们的生产环境中。这就是不正确的软件完整性验证导致的风险。
防止不正确的软件完整性验证风险需要一系列措施,跨越软件交付链中的不同系统和阶段。
风险10
强大的日志系统是能够帮助企业准备、检测以及调查相关安全事件的,但是如果它不够强大,那就会引来风险。随着攻击者逐渐将注意力转移到工程环境漏洞中,那些无法确保围绕这些环境进行适当的日志记录和可见性控制的企业,就有可能无法检测到违规的行为。
解决方案有哪些呢?虽然工作站、服务器以及业务应用程序通常在企业的日志记录和可见性程序中得到深入介绍,但工程环境中的系统和流程通常并非如此。
鉴于利用工程环境和流程的潜在攻击向量的数量,我们必须建立适当的能力以在这些攻击发生时立即检测到它们。其中许多载体涉及利用针对不同系统的程序化访问,面临这一挑战的关键就是围绕人工和机器访问创建强大的可见性。
鉴于 CI/CD 攻击向量的复杂性,系统的审计日志(用户访问、用户创建、权限修改)和应用日志(将事件推送到存储库、执行)需要具有同等的重要性构建以及上传软件。
总结:持续集成、自动化测试和持续部署
最后还需要注意的是,随着业务越发复杂,系统架构从单体走向微服务,CI/CD 管道的复杂性也会相应增多,每个阶段都可能会产生大量的敏感数据,这些敏感数据往往会成为巨大的攻击杠杆。试想一旦攻击者拿到了源码仓库的访问凭证,那么整个 CI/CD 环境都可能遭到沦陷。
在 Gitlab 的 CI/CD 起因统计报告的最后有这样一句话:
我们之所以这么频繁的发布程序,是因为采用了 DevOps 方法,并且主要归功于 CI/CD 中的持续集成、自动化测试和持续部署。
当然,安全也义不容辞。