公網搭建 GitLab 安全拾遺
- 2019 年 10 月 10 日
- 筆記
本文使用「署名 4.0 國際 (CC BY 4.0)」許可協議,歡迎轉載、或重新修改使用,但需要註明來源。 署名 4.0 國際 (CC BY 4.0)
本文作者: 蘇洋
創建時間: 2019年08月05日 統計字數: 7024字 閱讀時間: 15分鐘閱讀 本文鏈接: https://soulteary.com/2019/08/05/public-network-to-build-gitlab-security.html
公網搭建 GitLab 安全拾遺
在公網搭建的 GitLab 頻頻遇到安全挑戰,然而其實只需要做一兩個簡單的動作,維護成本就能夠大大降低,並且還能避免未被許可的內容,被搜索引擎爬蟲暴露的到處都是。
本篇文章,我們就來聊聊公網搭建的 GitLab 程式碼倉庫的安全小細節。
寫在前面
公網搭建 GitLab ,常見的攻擊面主要有:
- 運行宿主機系統部分
- 運行宿主機網路部分
- 應用 Web 程式漏洞
- 應用 SSH 漏洞
前兩點可以通過 SLB + VPC 進行網路隔離,來降低被攻擊風險。後兩點除了保證最快跟進系統安全修補程式,升級應用版本外,其實還有更好的解決方案,畢竟存放著數據的程式,每次升級都有未知的風險:
- 解決 Web 漏洞可以通過加一層
BasicAuth來解決。 - 解決 SSH 攻擊風險,可以通過加一個簡單的日誌監控程式來解決:參考之前文章中 監控 GitLab SSH 埠 小節。
但是加一層 BasicAuth 其實會對 GitLab 使用造成一些麻煩。
為 GitLab 添加請求驗證
GitLab 程式本身並不支援 BasicAuth,這裡需要使用一個 Web 前端軟體來完成這部分的工作,比如:Nginx、Traefik。
我這裡選擇使用 Traefik,因為配置更簡單,具體配置可以參考之前文章的「 添加網路請求驗證 」小節。
在配置聲明裡添加一句話就夠了,比如這樣:
- "traefik.gitlab.frontend.auth.basic=soulteary:$apr1$E86fARwM$tXmggGAtCEDKqsBCSvDA3/
這樣處理之後,所有的 HTTP 請求就都會被驗證是否是合法訪問啦。而其他的埠和協議則不受影響,比如開在22埠的 SSH 服務等。
Basic Auth 到底是什麼
當訪問頁面的時候,會展示類似下面的對話框,要求用戶登陸,否則會提示 401Unauthorized。

當爬蟲/安全檢測工具請求頁面的時候,如果沒有提交用戶名和密碼,獲得的結果也是一樣。
curl -I https://gitlab.domain/ HTTP/2 401 content-type: text/plain vary: Accept-Encoding www-authenticate: Basic realm="traefik" content-length: 17 date: Sat, 03 Aug 2019 18:22:17 GMT
如果你輸入了正確的用戶名以及用戶密碼,你會發現在你的請求參數中會出現一個額外的請求參數 authorization。

參數數值一般由兩部分構成,第一部分表示加密方式(加密協議名稱),第二部分則是你的身份資訊(更多資訊詳見 RFC 7617)。
現代瀏覽器一般會很智慧的在你第一次正確輸入之後,將身份資訊記錄下來,攜帶在後續的每一次請求中,如果是使用程式或者工具的話,則需要手動將 authorization 資訊加入到每一個 HTTP 的請求頭中。
然而 Basic Auth 擋住了外來者隨意訪問的同時,還擋住了 GitLab CI Runner。

這是怎麼回事呢,我們繼續往下看。
解救被攔住的 CI Runner
在解釋為什麼 CI Runner 會被 BasicAuth 攔住時,我們需要先了解另外一個協議規範 RFC1738 中對於 HTTP 協議的定義:
//<user>:<password>@<host>:<port>/<url-path>
當我們使用客戶端(瀏覽器、curl等工具)請求這類格式的地址時,部分客戶端會將 user:password 部分轉換為標準的 HTTP Authorization 請求頭。
默認 CI Runner 行為
在不做任何修改時,CI 直接執行會報錯,日誌輸出類似下面:
Running with gitlab-runner 12.0.2 (d0b76032) on ci-runner sGNUJnq6 Using Shell executor... Running on ci-runner... Fetching changes with git depth set to 50... Initialized empty Git repository in /data/runner/builds/sGNUJnq6/0/config/project/.git/ Created fresh repository. remote: 401 Unauthorized fatal: Authentication failed for 'https://gitlab-ci-token:[MASKED]@gitlab.domain/repo.git/' ERROR: Job failed: exit status 1
結合文章前面的內容,我們知道這裡是缺少了 Authorization 請求頭,那麼我們嘗試給請求補上這個請求頭,在 CI 和 GitLab 中間搭建一台 Proxy ,讓 CI 請求 GitLab 數據的時候,自動完成「認證」。
請求自動添加驗證資訊
如果使用 Nginx 搭建一個支援 GET/POST 請求的代理,核心配置如下:
location / { forward-http-post-requests-via-rewrite proxy_pass https://192.168.0.123; proxy_redirect https://192.168.0.123/ /; proxy_read_timeout 10s; proxy_set_header Host 'gitlab.domain'; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Authorization 'Basic YmFhaABCD'; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto 'https'; proxy_http_version 1.1; }再次使用 curl 對 GitLab 進行請求,發現沒有出現之前的 401 非驗證提示:
curl -I https://gitlab.domain/ HTTP/2 302 cache-control: no-cache content-type: text/html; charset=utf-8 date: Sat, 03 Aug 2019 18:45:39 GMT location: https://gitlab.domain/users/sign_in referrer-policy: strict-origin-when-cross-origin referrer-policy: strict-origin-when-cross-origin server: nginx strict-transport-security: max-age=315360000 vary: Accept-Encoding x-content-type-options: nosniff x-download-options: noopen x-frame-options: DENY x-permitted-cross-domain-policies: none x-request-id: Z8uahE5gN39 x-runtime: 0.012164 x-ua-compatible: IE=edge x-xss-protection: 1; mode=block
打開項目配置,會發現 Runner 已經上線。

CI 構建依舊是失敗的
繼續在 GitLab Runner 運行 CI 流水線,會看到還是報錯無法通過構建。
Running with gitlab-runner 12.2.0~beta.1803.g41d5c6ad (41d5c6ad) on ci-runner S8sY8o6A Using Shell executor... Running on ci-runner... Fetching changes with git depth set to 50... Reinitialized existing Git repository in /data/runner/builds/S8sY8o6A/0/project/.git/ > GitLab: The project you were looking for could not be found. fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists. ERROR: Job failed: exit status 1
還記得前面提到過的 Authorization 請求頭和 HTTP RFC規範嗎?GitLab Runner 在處理 CI 任務的時候,使用的是 https://gitlab-ci-token:[MASKED]@gitlab.domain/repo.git/ 這樣的 HTTP 協議,請求中的用戶名和密碼和 Nginx ProxyPass 中的欄位「八字不合」。
那麼不使用 HTTP 協議,使用 SSH 協議或許能解決問題。
嘗試使用 SSH 協議
可惜的是,官方並不支援 GitLab Runner 使用 SSH 協議進行倉庫下載,有類似需求的用戶還真不少,如果你願意找,類似下面的 issue 還有不少:
- Supports git clone via ssh
- Allow Configuration of how cloning is performed by runner/pipeline
雖然官方沒有直接提供這個功能,但是從官方文檔中看到有一個 clone_url,查找程式碼發現實現很簡單,只需要稍微改造就能夠滿足我們的需求:
// GetRemoteURL checks if the default clone URL is overwritten by the runner // configuration option: 'CloneURL'. If it is, we use that to create the clone // URL. func (b *Build) GetRemoteURL() string { cloneURL := strings.TrimRight(b.Runner.CloneURL, "/") if !strings.HasPrefix(cloneURL, "http") { return b.GitInfo.RepoURL } variables := b.GetAllVariables() ciJobToken := variables.Get("CI_JOB_TOKEN") ciProjectPath := variables.Get("CI_PROJECT_PATH") splits := strings.SplitAfterN(cloneURL, "://", 2) return fmt.Sprintf("%sgitlab-ci-token:%s@%s/%s.git", splits[0], ciJobToken, splits[1], ciProjectPath) }改動後的程式碼如下:
// GetRemoteURL checks if the default clone URL is overwritten by the runner // configuration option: 'CloneURL'. If it is, we use that to create the clone // URL. func (b *Build) GetRemoteURL() string { cloneURL := strings.TrimRight(b.Runner.CloneURL, "/") if !strings.HasPrefix(cloneURL, "http") && !strings.HasPrefix(cloneURL, "ssh") { return b.GitInfo.RepoURL } variables := b.GetAllVariables() ciJobToken := variables.Get("CI_JOB_TOKEN") ciProjectPath := variables.Get("CI_PROJECT_PATH") splits := strings.SplitAfterN(cloneURL, "://", 2) if strings.HasPrefix(cloneURL, "ssh") { ciProjectPath = strings.TrimLeft(ciProjectPath, "/") return fmt.Sprintf("git@%s:/%s.git", splits[1], ciProjectPath) } return fmt.Sprintf("%sgitlab-ci-token:%s@%s/%s.git", splits[0], ciJobToken, splits[1], ciProjectPath) }如果你不想在浪費時間在折騰構建環境上,可以參考我之前寫的一篇文章: 源碼編譯 GitLab Runner 。
執行 CI 成功
再次執行 CI 任務,會發現已經能夠順利的進行啦。
Running on ci-runner... Fetching changes with git depth set to 50... Reinitialized existing Git repository in /data/runner/builds/S8sY8o6A/0/project.git/ Checking out 860587f6 as master... Skipping Git submodules setup Warning: Permanently added '192.168.x.x' (ECDSA) to the list of known hosts. From gitlab.domain:project 3957006..860587f master -> origin/master Updating 3957006..860587f Fast-forward .gitlab-ci.yml | 6 +++--- README.md | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) Stopping service ... Stopping service ... done Removing service ... Removing service ... done Network service is external, skipping Creating service ... Creating service ... done Job succeeded
最後
GitLab 先折騰到這裡,或許後面有時間的會寫一篇更加全面的折騰攻略。如果你對 GitLab 感興趣,可以瀏覽我之前寫的相關文章。
—EOF