天天看點

GitLab CI 接入代碼安全掃描技術實踐

作者:閃念基因

在諸多的網際網路企業中,私有化部署GitLab平台是進行公司内部項目代碼托管的最常用方式。

GitLab平台功能強大,除了用于進行Git項目的代碼托管,還具備完善的CI/CD能力,能夠幫助研發同學一站式的完成代碼送出,項目編譯,項目部署等工作,大大簡化了DevOps流程中各種平台的對接工作。

這其中最重要的技術,就是GitLab平台提供的GitLab CI能力。它能夠采用一個yaml格式的配置檔案,完成整個項目的全流程建設,而不需要額外的平台配置(比如Jenkins)。

今天我們要探讨的,就是如何在采用GitLab CI的項目中,完成靜态代碼安全掃描,并具備安全卡點能力。

什麼是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 CI 接入代碼安全掃描技術實踐

Gitlab Runner是負責執行Gitlab CI任務的工作單元,我們需要為GitLab平台配置好GitLab CI Runner後,才可以使用GitLab CI,詳細資訊請檢視https://docs.gitlab.com/runner/。

GitLab CI 接入代碼安全掃描技術實踐

我們所有的任務都會在GitLab Runner内執行(圖檔來源于網絡)

使用案例示範

GitLab CI 接入代碼安全掃描技術實踐

我們在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個部分:stage和job。

前面有提到GitLab CI 是由一系列的job構成,job就是執行任務單元。但是這個job在什麼時間點執行,就是由stage決定的。

我們在.gitlab-ci.yml配置檔案裡看到的如下代碼:

stages:                 # 定義多個階段
  - build               # 建構
  - test                # 測試
  - deploy              # 部署           

就是項目自定義了3個stage,分别表示項目執行的三個階段。

然後後面_job結尾的任務,都會有一個stage标簽,表示這個任務是在哪個stage進行執行。

是以以上配置的執行順序是這樣的:

GitLab CI 接入代碼安全掃描技術實踐

這樣我們通過自定義stage和job,就能實作我們想要實作的任意功能。當然GitLab CI文法不隻是這些,詳細可檢視:

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

配置好.gitlab-ci.yml,我們把送出項目代碼到gitlab平台,檢視Pipeline流水線,就能夠看到我們的各種任務被執行了。

GitLab CI 接入代碼安全掃描技術實踐

如果研發業務都是使用Gitlab CI 來進行編譯部署,我們該如何接入安全掃描呢?

換言之,我們現在具備了獨立的代碼安全掃描引擎,該如何接入到這些項目裡,幫助研發解決安全問題呢?

GitLab CI 接入代碼安全掃描技術實踐

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 接入代碼安全掃描技術實踐

設計好如上的.gitlab-ci.yml後,我們送出程式,安全掃描Job就會被觸發了。

GitLab CI 接入代碼安全掃描技術實踐

安全卡點

GitLab CI 接入代碼安全掃描技術實踐

一般來說,如果不需要因為安全問題對流程進行卡點的話,上面的配置就足夠了。掃描發送到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 CI 接入代碼安全掃描技術實踐
GitLab CI 接入代碼安全掃描技術實踐

那麼我們就可以在我們的gitlab_secscan.py腳本裡做判斷,如果掃描發現安全漏洞,通過exit傳回錯誤即可。

優化後的GitLab CI接入安全掃描

GitLab CI 接入代碼安全掃描技術實踐

我們将secscan-job寫到項目的.gitlab-ci.yml裡,看起來沒什麼問題,但是作為安全人員,我們面對成千上萬的項目都需要接入安全掃描,我們該怎麼辦?

GitLab CI 接入代碼安全掃描技術實踐

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

本質上講,增加安全掃描是給研發添麻煩,對方就是不加,你怎麼識别?

即使加上了,後續變更怎麼辦? 再讓所有研發修改一次?

變更需要所有研發配合,動靜太大,實作困難。

如果項目并不是太多,我們可以将基礎方案進行改進,使用gitlab ci的include文法完成優化工作,官方文檔:

https://docs.gitlab.com/ee/ci/yaml/includes.html

像PHP提供的include一樣,Gitlab CI允許使用include引入公共模闆,解決相同配置統一管控的方案。

GitLab CI 接入代碼安全掃描技術實踐

我們将我們基礎方案中的公共部分統一放入公共模闆:

http://gitlab.xxx.com/commom/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: 'commom/gitlab_ci_template'  # 項目名稱
    ref: master   # 分支
    file: 'commom/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 分支送出時執行           

這樣就解決問題啦,我們可以讓研發統一按照這個模闆接入,如果後續安全掃描節點有變更,我們更改commom/gitlab_ci_template項目就可以啦!

不過你有沒有發現問題,我們的commom/gitlab_ci_template公共模闆裡,secscan-job的stage是啥?是secscan,如果業務的項目代碼裡沒有這個stage怎麼辦,那肯定是不能運作的!

Gitlab CI的預設Stage機制

GitLab CI 接入代碼安全掃描技術實踐

如果項目模版中定義了自己的Stage,那麼在include的公共模版中定義的Stage是無法生效的(會報錯,可自行嘗試)。要解決這個問題,我們需要研究一下Gitlab CI 的Stage機制。

我們來看一下官方文檔對Stages的描述(https://docs.gitlab.com/ee/ci/yaml/#stages):

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

If stages is not defined in the .gitlab-ci.yml file, the default pipeline stages are:

.pre

build

test

deploy

.post

如果項目并沒有在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是不需要額外定義的!

GitLab CI 接入代碼安全掃描技術實踐

回到我們掃描配置改進計劃中,這樣我們在公共模版中把我們的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}           

送出代碼執行一下看看,.pre Stage 被執行,我們的安全掃描Job被第一個觸發!

GitLab CI 接入代碼安全掃描技術實踐

到目前為止,真正實作了隻需要讓項目引入我們的公共模版即可,不需要項目的.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 CI 接入代碼安全掃描技術實踐

通過上面的優化,我們完美的實作了讓項目除了引入我們的模版外,不需要做任何變更的接入方式。

但是現在依然存在的問題是:如果項目沒有接入公共模版,或者因為安全問題被卡住了,使用者也完全可以先把公共掃描模版注釋掉,送出完成代碼後再恢複。

這樣我們的安全掃描覆寫度就形同虛設,很容易就繞過!

有沒有辦法實作強制卡點呢,研發同學無法跳過的那種!

有的,那就是通過GitLab Runner卡點的方式進行掃描。

GitLab CI 接入代碼安全掃描技術實踐

通過上圖我們發現,之前的接入方案都是在REPO端,這部分是由項目同僚控制的,我們沒有辦法做到強制卡點。

如果我們想不受項目的控制,就可以考慮把安全檢測卡點能力放到右側的Gitlab Runner 端。

這麼做有如下優勢:

  • 無需項目接入,調用Pipeline時自動進行安全檢測
  • 新增項目“零成本”,“無感覺”接入
  • 強制接入安全檢測,無法主動繞過

如何實作?

前面有提到,我們所有的Job都是在Gtilab Runner上被執行,無論是安全掃描Job還是其他業務Job。

如果業務Job在執行前,能夠給一個Hook事件,我們就可以利用這個Hook事件來執行前置的安全掃描工作。

GitLab CI 接入代碼安全掃描技術實踐

幸運的是,我們發現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 = "ASw-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 CI 接入代碼安全掃描技術實踐

各個任務都正确運作了,我們看一下任務的日志:

GitLab CI 接入代碼安全掃描技術實踐

我們在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}" --timeLimit "120" --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 CI 接入代碼安全掃描技術實踐

到目前為止,我們基本上完成了對于Gitlab 項目的強制檢測和卡點功能,我們最終使用的方式是使用Gitlab Runner的pre_clone_script配置。

但是這個配置存在一個問題,那就是每一個Job在執行前都會被調用。

GitLab CI 接入代碼安全掃描技術實踐

這種重複調用明顯不是必須的,我們預期的是在第一個Job進行完安全掃描後,後續的Job就不在進行安全掃描,該怎麼辦呢?

我們可以在Job與安全掃描見增加一個排程代理節點,實作功能是:先使用Gitlab Restful API擷取目前Pipeline的所有Job清單,判斷是不是第一個 Job (Job1),不是就不進行安全掃描。

GitLab CI 接入代碼安全掃描技術實踐

這樣我們就徹底解決了同一條流水線會進行多次安全掃描的問題。

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

寫在最後

GitLab CI 接入代碼安全掃描技術實踐

針對使用Gitlab CI的項目接入代碼安全掃描問題,以上循序漸進的提出了幾種處理方式。

其實以上幾種方式,本身都并無優劣之分,主要還是看具體業務場景,比如項目數量不多,最基礎的接入方式也沒問題;如果項目量非常大,又需要安全卡點,最後基于Gitlab Runner的方式肯定是最好的。

作者:l4yn3@小米安全

來源:微信公衆号:小米安全中心

出處:https://mp.weixin.qq.com/s/bSFkkGSxCBHZ22VAh3XgPA

繼續閱讀