freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

GitLab CI 接入代码安全扫描技术实践
2023-06-05 12:29:42
所属地 北京

在诸多的互联网企业中,私有化部署GitLab平台是进行公司内部项目代码托管的最常用方式。

GitLab平台功能强大,除了用于进行Git项目的代码托管,还具备完善的CI/CD能力,能够帮助研发同学一站式的完成代码提交,项目编译,项目部署等工作,大大简化了DevOps流程中各种平台的对接工作。

这其中最重要的技术,就是GitLab平台提供的GitLab CI能力。它能够采用一个yaml格式的配置文件,完成整个项目的全流程建设,而不需要额外的平台配置(比如Jenkins)。

今天我们要探讨的,就是如何在采用GitLab CI的项目中,完成静态代码安全扫描,并具备安全卡点能力。

什么是GitLab CI

如题,我们首先来介绍一下强大的Gitlab CI 技术。

GitLab CI(Continuous Integration)是 GitLab 提供的一款持续集成/持续部署的解决方案,它能够帮助开发团队自动化构建、测试和部署应用程序。借助 GitLab CI,开发团队可以在代码发生变更时,自动构建、测试和部署应用程序,从而提高开发效率和软件质量。

GitLab CI 基于 .gitlab-ci.yml 文件来定义一系列的 Jobs(任务)。每个 Job 包含一个或多个具体的步骤,例如编译代码、运行测试、打包应用程序等。当一个 Job 完成后,可以根据其执行结果决定是否继续执行下一个 Job 或者终止整个流程。

GitLab CI 提供了许多有用的功能,例如并行构建、容器化构建、自定义环境变量、报告分析等。它还支持多种语言和框架,包括 Java、Python、Node.js、Ruby 等,以及容器化技术,如 Docker 和 Kubernetes。

使用 GitLab CI 可以提高开发效率,减少手动操作,提高代码质量和可靠性,并且便于管理和维护。同时,GitLab CI 与 GitLab 集成紧密,可以通过 GitLab 的界面来查看和管理 CI 流水线,更加方便。

我们来实践一下GitLab CI的使用。

什么是Gitlab CI Runner

Gitlab Runner是负责执行Gitlab CI任务的工作单元,我们需要为GitLab平台配置好GitLab CI Runner后,才可以使用GitLab CI,详细信息请查看https://docs.gitlab.com/runner/


使用案例演示

我们在GitLab 平台上有一个Java项目,叫做ProjectJava。我们需要使用GitLab CI技术来完整的实现项目测试,编译部署等工作。

首先我们需要在根目录下创建一个.gitlab-ci.yml配置文件,写入以下内容:

stages:                 # 定义多个阶段
  - build               # 构建
  - test                # 测试
  - deploy              # 部署

build_job:              # 定义一个构建任务
  stage: build          # 指定所属阶段
  script:
    - mvn package       # 执行命令:构建应用程序

test_job:               # 定义一个测试任务
  stage: test           # 指定所属阶段
  script:
    - mvn test          # 执行命令:运行单元测试

deploy_job:             # 定义一个部署任务
  stage: deploy         # 指定所属阶段
  script:
    - ./deploy.sh       # 执行命令:调用脚本部署应用程序
  only:
    - master            # 仅在 master 分支提交时执行

当我们在提交项目代码的时候,GitLab会自动运行根目录下的.gitlab-ci.yml配置文件,执行里面的指令。

GitLab CI最核心的是2个部分:stagejob

前面有提到GitLab CI 是由一系列的job构成,job就是执行任务单元。但是这个job在什么时间点执行,就是由stage决定的。

我们在.gitlab-ci.yml配置文件里看到的如下代码:

stages:                 # 定义多个阶段
  - build               # 构建
  - test                # 测试
  - deploy              # 部署

就是项目自定义了3个stage,分别表示项目执行的三个阶段。

然后后面_job结尾的任务,都会有一个stage标签,表示这个任务是在哪个stage进行执行。

所以以上配置的执行顺序是这样的:


这样我们通过自定义stage和job,就能实现我们想要实现的任意功能。当然GitLab CI语法不只是这些,详细可查看:

https://docs.gitlab.com/ee/ci/quick_start/

配置好.gitlab-ci.yml,我们把提交项目代码到gitlab平台,查看Pipeline流水线,就能够看到我们的各种任务被执行了。


如果研发业务都是使用Gitlab CI来进行编译部署,我们该如何接入安全扫描呢?

换言之,我们现在具备了独立的代码安全扫描引擎,该如何接入到这些项目里,帮助研发解决安全问题呢?


GitLab CI接入安全扫描的一般配置

一般来说,我们是通过添加安全扫描Job的方式来做这件事。

我们上面说过GitLab CI通过添加Stage和Job的方式进行管理,那我们可以添加一个名字叫做secscan的stage,作为我们的安全扫描节点。

stages:                 # 定义多个阶段
  - build               # 构建
  - secscan             # 安全扫描
  - test                # 测试
  - deploy              # 部署

在这个扫描节点里,我们实现把相关信息传递给代码扫描引擎,完成扫描工作。

我们的Job可以叫做secscan-job,可以这么写:

secscan-job:
  stage: secscan
  script:
    - export MULT_COMMIT_BRANCH=${CI_COMMIT_BRANCH}
    - if [ ! "$MULT_COMMIT_BRANCH" ]; then export MULT_COMMIT_BRANCH=${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}; fi
    - if [ ! "$MULT_COMMIT_BRANCH" ]; then export MULT_COMMIT_BRANCH=${CI_COMMIT_TAG}; fi
    - python3 /home/agent/gitlab_secscan.py
      --gitUrl "${CI_PROJECT_URL}.git"
      --gitCommitId ${CI_COMMIT_SHA}
      --gitBranch $MULT_COMMIT_BRANCH
      --gitProjectPath ${CI_PROJECT_PATH}
      --url ${CI_PIPELINE_URL}
      --users ${GITLAB_USER_LOGIN}
      --pipelineId ${CI_PIPELINE_ID}

Gitlab CI提供了非常多的环境变量,具体可查看https://docs.gitlab.com/ee/ci/variables/predefined_variables.html

我们通过script获取了当前本次提交的项目信息后,执行了/home/agent/gitlab_secscan.py这个脚本来处理这些信息。

这个脚本在哪里?

前面我有提到,Gitlab CI的任务执行,都是通过Gitlab Runner来负责执行的,Gitlab Runner可以是物理机,docker镜像,甚至是K8S环境。

所以这个脚本应该放到Gitlab Runner环境里!这样在执行的时候就会自动执行这个脚本!

当然这个脚本的内容不是本文的重点,无非是实现获取这些参数,再传递给扫描引擎进行安全扫描,如图:


设计好如上的.gitlab-ci.yml后,我们提交程序,安全扫描Job就会被触发了。


安全卡点

一般来说,如果不需要因为安全问题对流程进行卡点的话,上面的配置就足够了。扫描发送到SAST扫描引擎,不影响Pipeline流水线的执行流程,不影响业务开发。安全方通过人工、自动化分析扫描结果,创建Jira,然后跟进漏洞修复。

但是安全不卡点还叫DevSecOps吗?又何谈安全左移呢?

当然你可以说,安全卡点导致误报率,业务影响什么的,这不在本文的讨论范围,以后有机会讨论。

如果我们现在需要做的,就是发现了严重的安全问题,比如log4j2组件调用,我们就是需要停止掉整个流水线操作,让业务修复漏洞后才可以继续,我们该怎么办?

利用Gitlab CI实现卡点,还是比较简单的,实现原理很简单:如果某一个Job运行过程中,返回非0错误码,当前Job会自动停止,并阻断后续Job的运行。

我们来试一下:

secscan-job:
  stage: secscan
  script:
    - I am done!
    - exit 255

我们直接模拟返回255错误,运行流水线,发现secscan-job运行失败的同时,后续流水线也被阻断了。


那么我们就可以在我们的gitlab_secscan.py脚本里做判断,如果扫描发现安全漏洞,通过exit返回错误即可。

优化后的GitLab CI接入安全扫描

我们将secscan-job写到项目的.gitlab-ci.yml里,看起来没什么问题,但是作为安全人员,我们面对成千上万的项目都需要接入安全扫描,我们该怎么办?


号召研发都在自己的.gitlab-ci.yml中增加secscan-job任务?

本质上讲,增加安全扫描是给研发添麻烦,对方就是不加,你怎么识别?

即使加上了,后续变更怎么办? 再让所有研发修改一次?

变更需要所有研发配合,动静太大,实现困难。

如果项目并不是太多,我们可以将基础方案进行改进,使用gitlab ci的include语法完成优化工作,官方文档:https://docs.gitlab.com/ee/ci/yaml/includes.html。

像PHP提供的include一样,Gitlab CI允许使用include引入公共模板,解决相同配置统一管控的方案。


我们将我们基础方案中的公共部分统一放入公共模板:

http://gitlab.xxx.com/common/gitlab_ci_template/.base_gitlab_ci.yml

secscan-job:
  stage: secscan
  script:
    - export MULT_COMMIT_BRANCH=${CI_COMMIT_BRANCH}
    - if [ ! "$MULT_COMMIT_BRANCH" ]; then export MULT_COMMIT_BRANCH=${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}; fi
    - if [ ! "$MULT_COMMIT_BRANCH" ]; then export MULT_COMMIT_BRANCH=${CI_COMMIT_TAG}; fi
    - python3 /home/agent/gitlab_secscan.py
      --gitUrl "${CI_PROJECT_URL}.git"
      --gitCommitId ${CI_COMMIT_SHA}
      --gitBranch $MULT_COMMIT_BRANCH
      --gitProjectPath ${CI_PROJECT_PATH}
      --url ${CI_PIPELINE_URL}
      --users ${GITLAB_USER_LOGIN}
      --pipelineId ${CI_PIPELINE_ID}

然后再在各个子项目中使用include引入这个模板:

include:
  - project: 'common/gitlab_ci_template'  # 项目名称
    ref: master   # 分支
    file: 'common/gitlab_ci_template/.base_gitlab_ci.yml'  # 公共配置文件
    
stages:                 # 定义多个阶段
  - build               # 构建
  - test                # 测试
  - secscan             # 安全扫描节点
  - deploy              # 部署

build_job:              # 定义一个构建任务
  stage: build          # 指定所属阶段
  script:
    - mvn package       # 执行命令:构建应用程序

test_job:               # 定义一个测试任务
  stage: test           # 指定所属阶段
  script:
    - mvn test          # 执行命令:运行单元测试

deploy_job:             # 定义一个部署任务
  stage: deploy         # 指定所属阶段
  script:
    - ./deploy.sh       # 执行命令:调用脚本部署应用程序
  only:
    - master            # 仅在 master 分支提交时执行

这样就解决问题啦,我们可以让研发统一按照这个模板接入,如果后续安全扫描节点有变更,我们更改common/gitlab_ci_template项目就可以啦!

不过你有没有发现问题,我们的common/gitlab_ci_template公共模板里,secscan-job的stage是啥?是secscan,如果业务的项目代码里没有这个stage怎么办,那肯定是不能运行的!

Gitlab CI的默认Stage机制

如果项目模版中定义了自己的Stage,那么在include的公共模版中定义的Stage是无法生效的(会报错,可自行尝试)。要解决这个问题,我们需要研究一下Gitlab CI 的Stage机制。

我们来看一下官方文档对Stages的描述(https://docs.gitlab.com/ee/ci/yaml/#stages):


Usestages to define stages that contain groups of jobs. Usestagein a job to configure the job to run in a specific stage.

Ifstages is not defined in the.gitlab-ci.ymlfile, the default pipeline stages are:

如果项目并没有在gitlab-ci.yml中配置Stages,那么默认是以上的Stages,可以直接使用,不需要定义。

但是如果用户项目自定义了Stages,那么就不能直接默认的Stages了。

我们注意到第一个(.pre)和最后一个(.post)两个stage跟其他不太一样,看一下文档描述。

If a pipeline contains only jobs in the .pre or .post stages, it does not run. There must be at least one other job in a different stage. .pre and .post stages can be used in required pipeline configuration to define compliance jobs that must run before or after project pipeline jobs.

意思为.pre.post两个stage为默认执行的stage,如果在项目里有其他stage被执行,那么再执行以前,会先执行.pre stage,执行完成之后,会执行.post stage!

并且这两个stage是不需要额外定义的!


回到我们扫描配置改进计划中,这样我们在公共模版中把我们的secscan-job放入.pre Stage 就可以了。

.pre stage 会在第一个具体定义的stage执行前被执行,完全符合我们进行安全卡点的需求,我们需要对触发Pipeline编译、部署任务的流水线进行安全检测和卡点,对那些不触发流水线的一般提交不作处理。

具体公共模版如下:

secscan-job:
  stage: .pre
  script:
    - export MULT_COMMIT_BRANCH=${CI_COMMIT_BRANCH}
    - if [ ! "$MULT_COMMIT_BRANCH" ]; then export MULT_COMMIT_BRANCH=${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}; fi
    - if [ ! "$MULT_COMMIT_BRANCH" ]; then export MULT_COMMIT_BRANCH=${CI_COMMIT_TAG}; fi
    - python3 /home/agent/gitlab_secscan.py
      --gitUrl "${CI_PROJECT_URL}.git"
      --gitCommitId ${CI_COMMIT_SHA}
      --gitBranch $MULT_COMMIT_BRANCH
      --gitProjectPath ${CI_PROJECT_PATH}
      --url ${CI_PIPELINE_URL}
      --users ${GITLAB_USER_LOGIN}
      --pipelineId ${CI_PIPELINE_ID}

提交代码执行一下看看,.preStage 被执行,我们的安全扫描Job被第一个触发!


到目前为止,真正实现了只需要让项目引入我们的公共模版即可,不需要项目的.gitlab-ci.yml做任何改动!

include:
  - project: 'commom/gitlab_ci_template'  # 项目名称
    ref: master   # 分支
    file: 'commom/gitlab_ci_template/.base_gitlab_ci.yml'  # 公共配置文件

如果测试发现,push操作可以正常触发secscan-job,但是Merge Request事件并没有触发,那么可以使用解决方案:https://gitlab.com/gitlab-org/gitlab-runner/-/issues/5970

解决方案是Job配置添加:

rules:
    - when: on_success

望知晓。

具备完善卡点能力的GitLab CI接入安全扫描

通过上面的优化,我们完美的实现了让项目除了引入我们的模版外,不需要做任何变更的接入方式。

但是现在依然存在的问题是:如果项目没有接入公共模版,或者因为安全问题被卡住了,用户也完全可以先把公共扫描模版注释掉,提交完成代码后再恢复。

这样我们的安全扫描卡点就形同虚设,很容易就绕过!

有没有办法实现强制卡点呢,研发同学无法跳过的那种!

有的,那就是通过GitLab Runner卡点的方式进行扫描。


通过上图我们发现,之前的接入方案都是在REPO端,这部分是由项目同事控制的,我们没有办法做到强制卡点。

如果我们想不受项目的控制,就可以考虑把安全检测卡点能力放到右侧的Gitlab Runner 端。

这么做有如下优势:

  • 无需项目接入,调用Pipeline时自动进行安全检测
  • 新增项目“零成本”,“无感知”接入
  • 强制接入安全检测,无法主动绕过

如何实现?

前面有提到,我们所有的Job都是在Gtilab Runner上被执行,无论是安全扫描Job还是其他业务Job。

如果业务Job在执行前,能够给一个Hook事件,我们就可以利用这个Hook事件来执行前置的安全扫描工作。


幸运的是,我们发现Gitlab CI Runner配置中提供了这样一个事件:pre_clone_script

pre_clone_script

此配置允许Gitlab Runner在执行代码下载操作之前,执行一段用户自定义的shell脚本。一般可以用此参数设置一些环境变量等执行前置信息,详情请参照https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section。

如果此shell脚本返回exit -1,则当前job会被自动停止,并被在pipeline里标识为failed。

如果我们的Gitlab Runner 是用的shell模式,那么我们只需要在我们的Gitlab Runner Server的配置文件(/etc/gitlab-runner/config.toml)里,调整如下内容:

[[runners]]
  name = "ubuntu"
  url = "https://gitlab.xxx.com/"
  token = "AUt-sfU1xxxxxx"
  executor = "shell"
  pre_clone_script="echo pre_clone_script && pwd"
  pre_build_script="echo pre_build_script_test && pwd"
  [runners.custom_build_dir]
  [runners.cache]
    [runners.cache.s3]
    [runners.cache.gcs]
    [runners.cache.azure]

我们实际上增加的内容是:

pre_clone_script="echo pre_clone_script && pwd"
pre_build_script="echo pre_build_script_test && pwd"

这样Gitlab Runner执行任务前,会先执行echo pre_clone_script && pwd脚本,再执行Job内容。

我们配置好后,提交代码试一下。


各个任务都正确运行了,我们看一下任务的日志:


我们在Gitlab Runner 配置文件中增加的shell脚本被执行了,但是项目本身并没有做任何配置。

到此,我们完成了在Gitlab Runner端控制项目代码的方案,将测试的shell脚本换成代码安全扫描的shell脚本即可。

比如我们编写脚本seccheck.sh

echo "Start security scan"

target_agent_path="/tmp/sec_agent"
agent_api="https://xxx.com/gitlab/sec_agent" # 远程的安全agent地址

# 如果Runner是docker、k8s模式,可以采用这种远程下载agent再执行的方式,如果是shell模式则不需要,直接上传agent即可
{ download_error=$(wget --tries=2 --timeout=10 --quiet -O $target_agent_path $agent_api 2>&1 >&3 3>&-); } 3>&1 || {
  exit 0
}

chmod +x /tmp/sec_agent

# 发送git项目数据给agent,agent再使用sast引擎的api进行检测,并返回结果,判断是否卡点,如果errcode==255,流程会被卡点
{ security_agent_errors=$(/tmp/sec_agent --gitUrl "${CI_PROJECT_URL}.git" --gitCommitId "$CI_COMMIT_SHA" --gitBranch "${MULT_COMMIT_BRANCH}" --url "${CI_PIPELINE_URL}" --users "${GITLAB_USER_LOGIN}" --gitProjectPath "${CI_PROJECT_PATH}" --pipelineId "${CI_PIPELINE_ID}" --pipelineName "${CI_PROJECT_PATH}" --ciJobName "${CI_JOB_NAME}" 2>&1 >&3 3>&-); } 3>&1 || {
  if [[ $? == 255 ]]; then
    exit -1 # 阻断
  else
    echo "failed security scan"
  fi
}
echo "Finish security scan"

然后在Gitlab Runner的配置中增加:

pre_clone_script="path/seccheck.sh"

这样就实现了我们的终极目的。

剩余问题解决

到目前为止,我们基本上完成了对于Gitlab 项目的强制检测和卡点功能,我们最终使用的方式是使用Gitlab Runner的pre_clone_script配置。

但是这个配置存在一个问题,那就是每一个Job在执行前都会被调用。


这种重复调用明显不是必须的,我们预期的是在第一个Job进行完安全扫描后,后续的Job就不在进行安全扫描,该怎么办呢?

我们可以在Job与安全扫描前增加一个调度代理节点,实现功能是:先使用Gitlab Restful API获取当前Pipeline的所有Job列表,判断是不是第一个 Job (Job1),不是就不进行安全扫描。


这样我们就彻底解决了同一条流水线会进行多次安全扫描的问题。

如果您使用的Gitlab Runner模式是k8s,而不是shell,那么可以使用RUNNER_PRE_CLONE_SCRIPT代替pre_clone_script配置。

写在最后

针对使用Gitlab CI的项目接入代码安全扫描问题,以上循序渐进的提出了几种处理方式。

其实以上几种方式,本身都并无优劣之分,主要还是看具体业务场景,比如项目数量不多,最基础的接入方式也没问题;如果项目量非常大,又需要安全卡点,最后基于Gitlab Runner的方式肯定是最好的。

本文作者: l4yn3@小米安全

笔者专注于应用安全测试,代码审计,以及devsecops工具链设计,研发与业务整合工作。

# 企业安全 # Gitlab # DevSecOps
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录