YouShallNotPass! 强化关键环境中的CI/CD管道
引言
在Kudelski Security(KS),我们广泛使用自托管的GitLab实例来管理所有代码库,包括应用程序、云环境配置和用户管理。通过GitLab CI/CD,我们运行测试、构建和部署到各种环境,并通过API管理安全设备。
然而,由于KS对GitLab实例没有完全控制权(由第三方管理),我们在配置符合特定用例的安全控制方面面临限制。这引发了一个问题:如何在托管代码并从GitLab实例执行代码的同时保护我们的安全环境?
我们提出的解决方案是构建一个名为YouShallNotPass的自定义运行器,它充当守门人。其主要角色是决定是否允许GitLab CI/CD作业在我们的安全网络环境中的GitLab运行器上运行。
示意图如下:
[此处为示意图描述]
在这篇博客文章中,我们将介绍并展示我们的开源实现YouShallNotPass,旨在增强GitLab和GitHub管道执行的安全性。
威胁模型
在增强CI/CD管道安全性的过程中,定义威胁模型以识别潜在风险和恶意行为者至关重要。
我们定义了以下威胁模型:
[此处为威胁模型描述]
我们的威胁模型考虑了两个角色:有权访问git项目的恶意用户和代码协作平台(如GitLab或GitHub)上的恶意管理员。
被识别为高风险的情景包括:
- 身份泄露:威胁行为者获得对代码协作平台的访问权限,修改其配置以在安全环境中执行代码。
- 错误分配运行器:允许运行器被错误地分配给不应访问运行器所在安全环境的存储库,可能导致未授权访问。
- 受损的Docker镜像:使用受损的Docker镜像(无论是通过新版本还是覆盖)构成重大风险。这种泄露可能影响运行器本身或授予对敏感资源的访问权限。
- 管道配置中毒:恶意行为者可能修改管道配置以在运行器上执行未授权代码,可能损害其完整性。
- 代码协作平台中的未知漏洞:影响代码协作平台的漏洞,如尚未发现的身份验证绕过问题(0day)。
这些风险导致我们定义了以下四个安全控制,我们需要能够通过YouShallNotPass进行配置:
- 存储库检查:实施控制以确保只有特定存储库被允许使用托管在我们安全环境中的运行器。
- 镜像检查:强制执行限制,只允许批准的Docker镜像由我们的运行器执行。
- 脚本检查:设置机制以只允许预先批准的作业在我们的管道中运行。
- 用户检查:建立访问控制以只允许授权用户在系统中启动作业。
YouShallNotPass
我们的CI/CD作业验证解决方案,称为YouShallNotPass(YSNP),可在GitHub上作为概念验证使用。YSNP旨在增强CI/CD管道的安全性,并确保只有预先批准的作业在受信任和受控制的环境中运行。
从架构角度来看,我们有以下三个关键部分:
- CI/CD平台:GitLab或GitHub——处于“较少”安全的环境,因为由第三方管理。
- CI/CD自定义运行器:这个专业运行器配备了YSNP软件和自定义执行器脚本。它充当守护者,在继续之前验证作业执行。
- HashiCorp Vault:YSNP的配置存储解决方案。虽然可以使用其他具有适当API的密码存储系统,但我们对Vault功能的熟悉使其成为我们的自然选择。
我们认为自定义运行器和Vault都处于安全环境中,因为我们可以放置我们想要的安全控制。详细组件图如下:
[此处为组件图描述]
与正常运行器执行的作业相比,我们的自定义运行器在执行作业(4)+(5)之前添加了使用YSNP和Vault的验证(3),只有在Vault上定义的检查成功通过时才会执行。
重要的是要注意,Vault、GitLab运行器和CI/CD平台(GitLab)彼此独立运行。例如,GitLab运行器的配置直接在主机上管理,GitLab无法访问其配置。
我们的自定义运行器由两部分组成:
- 自定义执行器Bash脚本:这些脚本是平台特定的,在作业之前、期间或之后执行。我们将在以下部分深入探讨这些脚本。
- YouShallNotPass Go应用程序:这个组件是我们验证过程的核心,确保作业在执行前满足预定义标准。
自定义执行器只是用户提供的脚本,可以在作业之前、期间或之后执行,我们将在下一节中描述它们。
GitLab自定义执行器
对于GitLab,这是通过自定义执行器完成的,它使用四个脚本,每个阶段一个:config_exec、prepare_exec、run_exec、cleanup_exec。
感兴趣的读者可以在我们的存储库中找到这些脚本,但简而言之:
- config_exec (config.sh):生成JWT令牌,我们将使用它来验证运行器到Vault——稍后更多。
- prepare_exec (prepare.sh):与YouShallNotPass(YSNP)交互以验证计划启动的Docker镜像。它检查镜像是否在Vault配置中预先批准,有效验证存储库本身是否被允许。
- run_exec (run.sh):根据Vault上的YSNP配置,脚本检查和用户检查也在此阶段完成,然后运行GitLab CI/CD作业的任何脚本。运行阶段比这更复杂,因为它被执行多次,因为每个子阶段都被调用(有11个子阶段!)。对于run_exec的每个子阶段调用,我们决定是否应该调用YSNP执行检查。然后,如果没有YSNP检查失败,作业的不同子阶段被执行。
- cleanup_exec (cleanup.sh):这个最终脚本停止在prepare.sh中启动的Docker容器。
GitLab自定义执行器开发的关键见解:
- 使用环境变量:为了利用GitLab CI/CD可用的环境变量,我们发现有必要用CUSTOM_ENV_前缀它们。
- 脚本内容解析:缺少包含脚本内容的变量要求我们解析运行器的日志以从.gitlab-ci.yml文件中提取关键字(例如,script、before_script、after_script)在run.sh阶段。
- 环境变量的可用性:在某些情况下,并非所有GitLab提供的预期环境变量在自定义执行器中都可用,我们必须进一步调查。然而,这些变量在YSNP运行时是可访问的。
GitHub自定义执行器
虽然我们的主要CI/CD平台是GitLab,但我们探索了使用GitHub的自托管运行器功能,经过一些适应,证明了对我们的需求功能。GitHub的自托管运行器缺乏GitLab的高级自定义执行器概念,但允许我们在作业之前和之后运行脚本。
before_script.sh(可在此处获得)执行以下任务:
- 最初且仅一次,生成公钥/私钥对,将用于生成JWT令牌以验证到Vault。
- 这些密钥将需要配置Vault以允许运行器进行身份验证。
- 基于GitLab的命名约定设置必要的变量,这些变量由YouShallNotPass要求。
- Git克隆目录:由于GitHub仅在作业本身运行时执行存储库的git克隆,我们在before_script中执行git克隆操作,使其能够拉取公共存储库。对于私有存储库,用户必须在profile.sh中指定GITHUB_USER和GITHUB_TOKEN变量。
- 从GitHub工作流中提取作业的Docker镜像和脚本,因为存储库内容变得可用。
- 生成用于Vault身份验证的JWT令牌并使用YSNP应用程序验证作业。
请注意,在GitHub Actions上的作业执行期间,作业日志直到作业结束才可见。这带来了挑战,当before_script等待用户交互以删除临时代码时,因为日志不包含用户所需的链接。
YouShallNotPass应用程序
YSNP是一个Golang应用程序,由上述自定义执行器调用,以针对存储在Vault上的配置执行验证。
自定义执行器使用适当的环境变量调用YSNP。以下是一些最重要的:
- CI_JOB_IMAGE – 包含作业中指定的要验证的Docker镜像
- CI_PROJECT_PATH – 包含带有存储库名称的存储库路径
- CI_JOB_SCRIPT – 包含作业中指定的要验证的脚本
- CI_USER_EMAIL – 包含启动GitLab作业/GitHub Action的用户的电子邮件地址/用户名。
- CI_JOB_JWT – 包含将用于登录到Vault的JWT令牌
- VAULT_* – 与如何访问Vault相关的变量,这些变量在自定义执行器的profile.sh脚本中定义。
有关YSNP使用的完整变量列表,请参见此处。
高级算法如下:
- 使用JWT令牌,YSNP验证到Vault并检索与启动作业的存储库相关的配置文件。有两个配置文件:白名单(包含允许的镜像和脚本)和youshallnotpass_config(包含配置检查)。
这些配置文件可以在存储库级别或命名空间级别。 这些配置文件的示例将在下一节关于Vault中看到。
- YSNP然后执行配置中指定的每个检查
- 如果任何这些检查失败(取决于youshallnotpass_config),YSNP以失败代码退出,防止作业继续。
- 如果所有检查通过,YSNP正常退出,允许运行器继续作业执行。
YouShallNotPass配置
YSNP验证CI/CD作业执行的能力由存储在Vault中的配置文件驱动。这些文件定义了作业验证的标准,包括镜像和脚本白名单,以及哪些检查是必需的。
YSNP依赖于存储在Vault中的两个基本配置文件:
- 白名单配置:这个基于JSON的文件包含批准在特定Git存储库中执行的镜像和脚本哈希。它确保只允许经过验证的镜像和脚本运行。示例白名单配置可能如下所示:
[此处为白名单配置示例]
- youshallnotpass_config:这个配置文件允许配置YSNP本身以及每个作业或全局需要哪些检查。
例如,上面的配置文件只是提到名为“user_mfa_job”的作业只有一个检查,即验证执行作业的用户。
默认情况下,YSNP进行以下验证检查:
- 存储库验证,
- Docker镜像哈希的验证,和
- 要么作业脚本的验证,要么通过删除Vault中的临时代码来授权启动作业的用户。这些检查依赖于正确配置的Vault访问控制列表(ACL),这些在下一节中详细说明。
有关配置此文件的可用选项的更多信息,请参见项目配置选项部分。
为了防止恶意用户删除作业日志以隐藏其活动,我们实现了一个功能,将运行器活动记录到Mattermost频道。此功能在命名空间级别操作,并在此处更详细地描述。
HashiCorp Vault
Vault是一个伟大的键值存储工具,因为它提供了使用访问控制列表(ACL)进行细粒度访问和使用OIDC进行透明身份验证的功能。此外,它具有我们从YSNP调用的所有所需API端点。
需要理解的三个重要点是:
- 运行器需要使用JWT验证到Vault。
- 必须在Vault上配置适当的ACL。
- 必须在Vault上创建YSNP配置文件。
集成测试
为了维护GitLab自定义执行器与YSNP结合的可靠性和功能性,我们在testing/scripts下实现了集成测试。这些测试可用于提供关于我们的Git存储库和Vault设置的见解。
这些docker-compose文件的架构在testing/integration下可用,看起来像这样:
[此处为集成测试架构描述]
集成测试目录由两个docker-compose文件组成:
-
vault-compose.yml:这个Docker设置文件定义了HashiCorp Vault的配置。它伴随着脚本vault-init.sh,该脚本配置Vault:
- 用于生成JWT令牌的公钥/私钥(存储在Docker卷中,供gitlab_runner容器访问)。
- Vault配置允许运行器通过JWT令牌进行身份验证。
- 设置键值存储和配置文件,包括白名单和youshallnotpass_config。
- Vault ACL策略授予YSNP对存储在Vault中的YSNP配置的读取访问权限。
-
runner-compose.yml Docker文件配置自定义运行器(在上图中名为gitlab_runner),包含自定义执行器和YSNP应用程序。
自定义运行器使用git-init.sh脚本设置,该脚本配置自定义运行器。此脚本执行以下操作:
- 设置本地git存储库。
- 推送包含一些示例作业的演示.gitlab-ci.yml文件。
此设置需要模拟git克隆,就像它来自GitLab当作业启动时。
现在我们可以简单地使用exec命令启动自定义运行器,该命令允许直接本地运行作业,而不需要从GitLab拉取。请注意,此exec命令在此问题中已弃用,因为此命令不支持正常运行器在运行作业时需要的所有功能。然而,由于此功能的流行,GitLab现在正在调查如何本地运行管道。
为简单起见,我们添加了youshallnotpass_builder_daemon,允许重新构建go应用程序而不必重新启动完整的docker compose。
用例
在本节中,我们探索三个关键用例,说明YouShallNotPass(YSNP)如何有效解决和减轻潜在威胁,保护CI/CD管道免受未授权访问和恶意活动:
- 运行器劫持。
- 恶意修改存储库。
- 用户冒充。
运行器劫持
此情景模拟了一个案例,其中具有访问敏感内部机器的GitLab运行器被分配给整个命名空间(group_with_sensitive_repos)。此配置将允许命名空间下的所有存储库使用运行器进行作业执行。
然而,在Vault上,我们只白名单了该命名空间中的特定存储库(repo_name)(而其他两个条目是命名空间级别的配置文件):
[此处为Vault配置示例]
当恶意用户尝试从未授权的存储库(repo_unauthorized)启动作业(例如,malicious_job)使用YSNP自定义运行器时,会触发失败消息,阻止未授权的作业执行。我们可以在下面的屏幕截图中看到失败消息:
[此处为屏幕截图描述]
恶意修改存储库
此情景模拟了一个有权访问批准存储库及其关联运行器的用户,试图恶意修改CI/CD配置。目的是将环境变量(包括秘密)重定向到攻击者控制的服务器。
[此处为恶意脚本示例]
当YSNP运行时,它将检查上述脚本是否已在Vault上的配置中预先批准。由于此脚本与先前允许的脚本的哈希不匹配,当作业运行时,作业将失败,如我们在下面的屏幕截图中所见:
[此处为屏幕截图描述]
其中allowed_script在该存储库的Vault上的白名单配置中找到。
用户冒充
我们想要呈现的最终用例与最近为GitLab发布的CVE相关:CVE-2023-5207,其中经过身份验证的攻击者可以在启动管道时冒充另一个用户。这可能允许攻击者启动只有特定用户应该被允许的作业。当攻击者冒充用户(带有电子邮件地址[email protected])启动作业时,他们会看到此消息(最多到第66行):
[此处为消息示例]
现在,攻击者还需要能够以冒充的user.name登录到Vault,以便能够删除由YSNP生成的临时代码。
如果攻击者不删除临时代码,一段时间后,YSNP将使作业失败(第67-68行)。 Vault ACL需要以这样的方式配置,只有user.name有权访问秘密的路径以删除它。
结论
当我们结束这篇博客文章时,我们想重申保护CI/CD管道的重要性以及我们的开源自定义运行器解决方案YouShallNotPass(YSNP)在此努力中扮演的重要角色。以下关键要点 encapsulate 了我们讨论的精髓。
CI/CD平台被威胁行为者视为高价值目标,因为它们对现代组织的重要性。由于我们将CI/CD平台视为比代码本身执行的环境(即运行器和可从它们访问的机器)更不受信任的环境,这意味着必须应用安全检查以防止未授权使用。
安全检查作为YSNP配置添加,存储在受信任环境中的HashiCorp Vault上,该环境与CI/CD平台独立管理。因此,使其超出CI/CD平台泄露的范围。
YouShallNotPass允许您:
- 验证存储库是否被允许在运行器上,
- 验证作业使用的Docker镜像的哈希,
- 验证作业脚本的哈希,和
- 验证启动作业的用户是否被允许。
所有这一切都在任何作业在运行器上执行之前发生。
我们目前每天使用此解决方案来保护我们最敏感的运行器和CI/CD作业。
我们欢迎任何关于我们GitHub repo的反馈,并在Black Alps 2023见面,我们将展示我们的解决方案!
链接和进一步阅读
- https://github.com/kudelskisecurity/youshallnotpass
- 另一个我们发现的改善CI/CD安全的工具是https://github.com/step-security/harden-runner,它也使用自定义脚本通过允许或不允许作业脚本执行的各种操作来提供运行时安全。
- https://owasp.org/www-project-top-10-ci-cd-security-risks/
- https://github.com/rung/threat-matrix-cicd 提供MITRE ATT&CK矩阵风格的CI/CD相关技术。