GitLab系列4 GitLab Shell

看了前三期 GitLab 相關解析的讀者(僞裝有讀者)
nginx

都會發現我總會貼 GitLab 的架構圖。至此咱們將 GitLab 如何處理 HTTP/HTTPS 請求的過程解釋了「一半」:用戶請求從 HTTP/HTTPS 入口進來,經由 nginx 到達 gitlab-workhorse ,若是能處理的請求本身優先處理,不能處理的請求再交給 unicorn ,unicorn 能夠認爲是一層外殼,保障了請求能高效地調度與處理,而真正處理請求的內核主要仍是看 GitLab Rails ,即 gitlab-ce ,請求處理的「另外一半」就交給 gitlab-ce 去處理了git


本期我並不打算介紹 GitLab Rails ,由於這個項目實在太龐大和複雜了(固然也能夠簡單地當作是一個須要持久化數據庫 PostgreSQL 和分佈式緩存 Redis 支撐的 web 應用),徹底能夠另起一個專題進行介紹。並且本系列打算儘量介紹 GitLab 每個組件的功能(而不是介紹開源項目的代碼結構等),剛纔也說了 GitLab Rails 至關於 GitLab 的真內核,大部分用戶請求的處理邏輯都在這裏實現了(有個打算是,將來將結合某些業務場景對 gitlab-ce 作源碼跟蹤及分析)web

那本期介紹什麼呢?別忘了前幾期都是講 http/https 的路由狀況,那 ssh 的呢?從架構圖上看,ssh 入口的第一站便是 GitLab Shell。本節主要講解理解 GitLab-Shell 運做原理的預備知識算法


一談到 Shell,咱們可能會聯想到相似 bash 或 zsh 這樣的命令行終端,某種程度上 GitLab Shell 也能夠被當作是一系列預約義命令的集大成者,但也不止是這樣shell

GitLab Shell 是 shell ?

還記得以前第一期的時候貼的這張解釋 SSH 利用對稱加密算法登陸登陸服務器的流程嗎?回顧一下:其實整個過程主要使用了公鑰加密、私鑰解密的對稱加密算法:用戶將本身本機的 SSH 公鑰上傳至遠程服務器上(遠程服務器的 $HOME/.ssh/authorized_keys 文件將保存用戶上傳的公鑰,通常狀況直接追加到文件末尾便可)登陸的時候遠程服務器向用戶發送一段隨機序列,用戶用本身本機的 SSH 私鑰加密後發回遠程服務器,遠程服務器用事先儲存的公鑰進行解密,若是成功就證實用戶是可信的,容許你登陸而且再也不要求密碼數據庫



那當咱們在本機執行 git-over-ssh 操做的時候,難道就能登陸到服務器的 bash 終端搞事情嗎?顯然是不可能的:服務器不可能讓咱們用戶登陸終端執行一切命令,除非服務器管理人員想讓用戶體驗 rm -rf /* 刪庫跑路的感受api


所以 GitLab 使用了 SSH 的一個特性:經過 authorized_keys 指定登陸後要執行的命令。以前登陸的時候遠程服務器是直接將用戶公鑰追加到 $HOME/.ssh/authorized_keys 文件末尾緩存


而如今做爲服務器管理人員,你確定是不但願用戶登陸到你服務器的終端亂搞的,因此思路就是僅容許用戶執行管理員所指定的 shell 命令,達到安全控制的做用,以下圖所示安全


上圖主要是說,當咱們在 $HOME/.ssh/authorized_keys 末尾追加以下格式的內容:bash

# command="./cmd ssh-rsa <my-rsa-key>"
command="/home/git/gitlab-shell/bin/gitlab-shell key-10",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa <my-rsa-key>
複製代碼

用戶就沒辦法用 ssh 登陸到服務器終端搞事情了,用戶登陸後只能執行這條命令,執行完畢就退出

/home/git/gitlab-shell/bin/gitlab-shell key-10
複製代碼

這就是爲何 GitLab Shell 如此命名,其實仍是很形象的:僅容許你在服務器執行 GitLab Shell ,別的 Shell 命令,好比 rm -rf /* 一把梭的用戶少給我亂來,其中這個 key-10就是 /home/git/gitlab-shell/bin/gitlab-shell 命令的執行參數 ARGV,表示的固然是用戶的密鑰標識

接下來咱們來看看 /home/git/gitlab-shell/bin/gitlab-shell 的代碼


注意,咱們還能用 $SSH_ORIGINAL_COMMAND 變量取到客戶端發來的命令,如今作個實驗:打印出 /home/git/gitlab-shell/bin/gitlab-shellwhooriginal 變量值

# 打印變量值
File.write("/tmp/git_original_cmd", original_cmd)
File.write("/tmp/git_who", who)
複製代碼

 當我在本地分別執行 git push | git fetch | git pull | git clone 的時候,打印的值分別是:

// action => original_cmd who
git push => git-receive-pack 'BradeHyj/ToyProject.git' key-11
git fetch => git-upload-pack 'BradeHyj/ToyProject.git' key-11
git pull => git-upload-pack 'BradeHyj/ToyProject.git' key-11
git clone => git-upload-pack 'BradeHyj/ToyProject.git' key-11
複製代碼

git-receive-pack - Receive what is pushed into the repository

git-upload-pack - Send objects packed back to git-fetch-pack

上述說明了什麼?當咱們在本地執行 git push 操做的時候,包含了登陸服務器執行命令的操做(並且這個登陸操做後面是帶 gitlab-shell 所能識別的命令),寫具體點就是:

# git push
ssh user@host:port git-receive-pack 'BradeHyj/ToyProject.git'
複製代碼

以上,針對 GitLab Shell 是如何接收到客戶端 ssh 請求,咱們作了相應的解釋,咱們再回頭看變量 whooriginal_cmd

  • who : sshd 調用 GitLab Shell 時傳入的參數
  • original_cmd : 前面提到的 $SSH_ORIGINAL_COMMAND 變量,取到後即移除

感興趣的能夠深刻研究 Pro git 2 關於 ssh 智能傳輸協議的介紹,具體以下:git-scm.com/book/zh/v1/…

凡是通過 gitlab-shell 執行的 git 命令,在真正執行前都會進行校驗權限的操做,具體以下代碼所示

# lib/gitlab-shell.rb
...
  def verify_access
    status = api.check_access(@git_access, nil, @repo_name, @who || @gl_id, '_any', GL_PROTOCOL)

    raise AccessDeniedError, status.message unless status.allowed?

    status
  end
複製代碼

Git 鉤子爲什麼物

GitLab Shell 的處理邏輯是依賴 git 鉤子腳本的。GitLab 服務器端存儲的全部代碼倉庫的 hooks 文件夾都是連接到 /home/git/gitlab-shell/hooks 中的,因此理解 gitlab-shell 鉤子腳本的執行邏輯很是重要


此處引用 Pro Git 2 對服務端鉤子的內容介紹:git-scm.com/book/zh/v2/…

鉤子都被存儲在 Git 目錄下的 hooks 子目錄中。 也即絕大部分項目中的 .git/hooks 。 當你用 git init 初始化一個新版本庫時,Git 默認會在這個目錄中放置一些示例腳本。這些腳本除了自己能夠被調用外,它們還透露了被觸發時所傳入的參數。 全部的示例都是 shell 腳本,其中一些還混雜了 Perl 代碼,不過,任何正確命名的可執行腳本均可以正常使用 —— 你能夠用 Ruby 或 Python,或其它語言編寫它們。 這些示例的名字都是以 .sample 結尾,若是你想啓用它們,得先移除這個後綴。

把一個正確命名且可執行的文件放入 Git 目錄下的 hooks 子目錄中,便可激活該鉤子腳本。 這樣一來,它就能被 Git 調用

做爲系統管理員,你可使用若干服務器端的鉤子對項目強制執行各類類型的策略。這些鉤子腳本在推送到服務器以前和以後運行。推送到服務器前運行的鉤子能夠在任什麼時候候以非零值退出,拒絕推送並給客戶端返回錯誤消息,還能夠依你所想設置足夠複雜的推送策略。

  • pre-receive : 處理來自客戶端的推送操做時,最早被調用的腳本是 pre-receive。 它從標準輸入獲取一系列被推送的引用。若是它以非零值退出,全部的推送內容都不會被接受。 你能夠用這個鉤子阻止對引用進行非快進(non-fast-forward)的更新,或者對該推送所修改的全部引用和文件進行訪問控制。
  • update : update 腳本和 pre-receive 腳本十分相似,不一樣之處在於它會爲每個準備更新的分支各運行一次。 假如推送者同時向多個分支推送內容,pre-receive 只運行一次,相比之下 update 則會爲每個被推送的分支各運行一次。 它不會從標準輸入讀取內容,而是接受三個參數:引用的名字(分支),推送前的引用指向的內容的 SHA-1 值,以及用戶準備推送的內容的 SHA-1 值。 若是 update 腳本以非零值退出,只有相應的那一個引用會被拒絕;其他的依然會被更新。
  • post-receive : post-receive 掛鉤在整個過程完結之後運行,能夠用來更新其餘系統服務或者通知用戶。 它接受與 pre-receive 相同的標準輸入數據。 它的用途包括給某個郵件列表發信,通知持續集成(continous integration)的服務器,或者更新問題追蹤系統(ticket-tracking system) —— 甚至能夠經過分析提交信息來決定某個問題(ticket)是否應該被開啓,修改或者關閉。 該腳本沒法終止推送進程,不過客戶端在它結束運行以前將保持鏈接狀態,因此若是你想作其餘操做需謹慎使用它,由於它將耗費你很長的一段時間。

我們先看看 gitlab-shell 的pre-receive 的邏輯(如下示例代碼均在 $gitlab-shell/hooks文件夾裏)


能夠看到在對服務器倉庫進行推送(git-receive-pack)以前得先執行用戶權限認證及受權等準備工做


post-receive 同理,與 pre-receive 的區別主要是執行邏輯不一樣


對於服務器端倉庫操做的任何 git 命令執行完成後,都要調用 GitLab Rails 的 post_receive 接口處理後續邏輯


GitLab Shell 組件簡介

GitLab Shell 組件用於處理 GitLab 全部的 git SSH 會話。當用戶以 SSH 的方式訪問 GitLab 時(例如 git pull/push over ssh),GitLab Shell 組件會作下列事情:

  1. 限制用戶使用預約義的 git 命令(git push, git pull 等)
  2. 調用 GitLab Rails 的 API 接口以檢查用戶是否已受權,以及判斷用戶經過哪臺 Gitaly 服務器訪問代碼倉庫(Gitaly 組件的主要功能是進行與代碼倉庫相關的操做)
  3. 在 SSH 客戶端和 Gitaly 服務器之間來回拷貝數據

當咱們執行 git pull/push over ssh 時,分別發生如下的事情:

  1. git pull over ssh -> gitlab-shell -> 調用 gitlab-rails 的 api 接口以認證用戶信息並受權 -> 受權成功或失敗 -> 創建 Gitaly 會話
  2. git push over ssh -> gitlab-shell(此時服務器的 git 命令還未被執行) -> 創建 Gitaly 會話 -> 在 Gitaly 服務器執行 gitlab-shell 的 pre-receive 鉤子腳本 -> 調用 gitlab-rails 的 api 接口以認證用戶信息並受權 -> 受權成功或失敗

因爲歷史緣由 gitlab-shell 組件也包含了容許 GitLab 校驗用戶 git push 命令的鉤子腳本(例如,判斷當前用戶是否有權限將本地代碼變更 push 到某保護分支),這些鉤子腳本同時也能觸發 GitLab 的事件(好比當用戶成功推送代碼後觸發 CI 流水線啓動等)。從目前的架構來看,gitlab-shell 的 git 鉤子腳本是屬於 Gitaly 組件的,鉤子腳本只會運行在 Gitaly 服務器。在 Gitaly 服務器安裝 gitlab-shell 組件實屬不必,具體看 gitlab.com/gitlab-org/…

附錄

參考連接

gitlab-shell 官方倉庫

Pro git 2

GitLab系列1 基礎功能及架構簡介
GitLab系列2 GitLab Workhorse
GitLab系列3 Unicorn

相關文章
相關標籤/搜索