git服務器pull和push 權限分開配

我們一般通過 Git 協議進行無授權訪問,通過 SSH 協議進行授權訪問,如果你的項目是內部項目,只針對部分授權用戶,那使用 SSH 協議就足夠了,但是如果既需要授權訪問也需要無授權訪問,可能需要 SSH 協議和 Git 協議搭配使用,這在維護上成本很高。這時就到了我們的壓軸戲 —— HTTP 協議出場的時候了,它同時支持上面兩種訪問方式。

通過 HTTP 協議訪問 Git 服務是目前使用最廣泛的方式,它支持兩種模式:舊版本的 Dumb HTTP 和 新版本的 Smart HTTP,Dumb HTTP 一般很少使用,下面除非特殊說明,所說的 HTTP 協議都是 Smart HTTP。使用 HTTP 協議的好處是可以使用各種 HTTP 認證機制,比如用戶名/密碼,這比配置 SSH 密鑰要簡單的多,對普通用戶來說也更能接受。如果擔心數據傳輸安全,也可以配置 HTTPS 協議,這和普通的 Web 服務是一樣的。

下面我們就來嘗試搭建一個基於 HTTP 協議的 Git 服務器。《Pro Git》上提供了一個基於 Apache 的配置示例,如果你是使用 Apache 作爲 Web 服務器,可以參考之,我們這裏使用 Nginx 來作爲 Web 服務器,其原理本質上是一樣的,都是通過 Web 服務器接受 HTTP 請求,並將請求轉發到 Git 自帶的一個名爲 git-http-backend 的 CGI 腳本。

首先我們安裝所需的軟件:

# apt-get install -y git-core nginx fcgiwrap apache2-utils

其中,Nginx 作爲 Web 服務器,本身是不能執行外部 CGI 腳本的,需要通過 FcgiWrap 來中轉,就像使用 php-fpm 來執行 PHP 腳本一樣。apache2-utils 是 Apache 提供的一個 Web 服務器的工具集,包含了一些有用的小工具,譬如下面我們會用到的 htpasswd 可以生成 Basic 認證文件。

啓動 Nginx 和 FcgiWrap,並訪問 http://myserver 測試 Web 服務器是否能正常訪問:

# service nginx start
# service fcgiwrap start


然後我們打開並編輯 Nginx 的配置文件(/etc/nginx/sites-available/default):

location / {
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend;
    fastcgi_param GIT_HTTP_EXPORT_ALL "";
    fastcgi_param GIT_PROJECT_ROOT /git/repo;
    fastcgi_param PATH_INFO $uri;
    fastcgi_param REMOTE_USER $remote_user;
    fastcgi_pass unix:/var/run/fcgiwrap.socket;
} 

這裏通過 fastcgi_param 設置了一堆的 FastCGI 參數,如下:

  • SCRIPT_FILENAME:指定 CGI 腳本 git-http-backend 的位置,表示每次 HTTP 請求會被轉發到該 CGI 腳本;
  • GIT_HTTP_EXPORT_ALL:git-http-backend 默認只能訪問目錄下有 git-daemon-export-ok 文件的 Git 倉庫,和上面介紹的 Git 協議是一樣的,如果指定了 GIT_HTTP_EXPORT_ALL,表示允許訪問所有倉庫;
  • GIT_PROJECT_ROOT:Git 倉庫的根目錄;
  • REMOTE_USER:如果有認證,將認證的用戶信息傳到 CGI 腳本;

改完之後我們重啓 Nginx,並通過 HTTP 協議 clone 倉庫:

aneasystone@little-stone:~/working$ git clone http://myserver/test.git

1.開啓身份認證

到這裏一切 OK,但是當我們 push 代碼的時候,卻會報下面的 403 錯誤:

aneasystone@little-stone:~/working/test$ git push origin master
fatal: unable to access 'http://myserver/test.git/': The requested URL returned error: 403

爲了解決這個錯誤,我們可以在 git-http-backend 的官網文檔上找到這樣的一段描述:
 

By default, only the upload-pack service is enabled, which serves git fetch-pack and git ls-remote clients, which are invoked from git fetch, git pull, and git clone. If the client is authenticated, the receive-pack service is enabled, which serves git send-pack clients, which is invoked from git push.

第一次讀這段話可能會有些不知所云,這是因爲我們對這裏提到的 upload-pack、fetch-pack、receive-pack、send-pack 這幾個概念還沒有什麼認識。但是我們大抵可以猜出來,默認情況下,只有認證的用戶纔可以 push 代碼,如果某個 Git 倉庫希望所有用戶都有權限 push 代碼,可以爲相應的倉庫設置 http.receivepack:

root@myserver:/# cd /git/repo/test.git/
root@myserver:/git/repo/test.git# git config http.receivepack true


當然最好的做法還是對 push 操作開啓認證,官網文檔上有一個 lighttpd 的配置我們可以借鑑:

$HTTP["querystring"] =~ "service=git-receive-pack" {
include "git-auth.conf"
}
$HTTP["url"] =~ "^/git/.*/git-receive-pack$" {
include "git-auth.conf"
} 


這個配置看上去非常簡單,但是想要理解爲什麼這樣配置,就必須去了解下 Git 的內部原理。正如上面 git-http-backend 文檔上的那段描述,當 Git 客戶端執行 git fetch,git pull,and git clone 時,會調用 upload-pack 服務,當執行 git push 時,會調用 receive-pack 服務,爲了更清楚的說明這一點,我們來看看 Nginx 的訪問日誌。

執行 git clone:

[27/Nov/2018:22:18:00] "GET /test.git/info/refs?service=git-upload-pack HTTP/1.1" 200 363 "-" "git/1.9.1"
[27/Nov/2018:22:18:00] "POST /test.git/git-upload-pack HTTP/1.1" 200 306 "-" "git/1.9.1"

執行 git pull:

[27/Nov/2018:22:20:25] "GET /test.git/info/refs?service=git-upload-pack HTTP/1.1" 200 363 "-" "git/1.9.1"
[27/Nov/2018:22:20:25] "POST /test.git/git-upload-pack HTTP/1.1" 200 551 "-" "git/1.9.1"

執行 git push:

[27/Nov/2018:22:19:33] "GET /test.git/info/refs?service=git-receive-pack HTTP/1.1" 401 204 "-" "git/1.9.1"
admin [27/Nov/2018:22:19:33] "GET /test.git/info/refs?service=git-receive-pack HTTP/1.1" 200 193 "-" "git/1.9.1"
admin [27/Nov/2018:22:19:33] "POST /test.git/git-receive-pack HTTP/1.1" 200 63 "-" "git/1.9.1"

可以看到執行 clone 和 pull 請求的接口是一樣的,先請求 /info/refs?service=git-upload-pack,然後再請求 /git-upload-pack;而 push 是先請求 /info/refs?service=git-receive-pack,然後再請求 /git-receive-pack,所以在上面的 lighttpd 的配置中我們看到了兩條記錄,如果要對 push 做訪問控制,那麼對這兩個請求都要限制。關於 Git 傳輸的原理可以參考 《Pro Git》的 Git 內部原理 - 傳輸協議這一節。

我們依葫蘆畫瓢,Nginx 配置文件如下:

location @auth {
    auth_basic "Git Server";
    auth_basic_user_file /etc/nginx/passwd;

    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend;
    fastcgi_param GIT_HTTP_EXPORT_ALL "";
    fastcgi_param GIT_PROJECT_ROOT /git/repo;
    fastcgi_param PATH_INFO $uri;
    fastcgi_param REMOTE_USER $remote_user;
    fastcgi_pass unix:/var/run/fcgiwrap.socket;
}

location / {
    error_page 418 = @auth;
    if ( $query_string = "service=git-receive-pack" ) {  return 418; }
    if ( $uri ~ "git-receive-pack$" ) { return 418; }

    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend;
    fastcgi_param GIT_HTTP_EXPORT_ALL "";
    fastcgi_param GIT_PROJECT_ROOT /git/repo;
    fastcgi_param PATH_INFO $uri;
    fastcgi_param REMOTE_USER $remote_user;
    fastcgi_pass unix:/var/run/fcgiwrap.socket;
} 

其中相同的配置我們也可以用 include 指令放在一個共用的配置文件裏,這樣我們就實現了在 push 的時候需要填寫用戶名和密碼了。我們通過 Nginx 的 auth_basic_user_file 指令來做身份認證,用戶名和密碼保存在 /etc/nginx/passwd 文件中,這個文件可以使用上面提到的 apache2-utils 包裏的 htpasswd 來生成:

root@myserver:/# htpasswd -cb /etc/nginx/passwd admin 123456

另外,在 push 的時候,有時候可能會遇到 unpack failed: unable to create temporary object directory 這樣的提示錯誤:

aneasystone@little-stone:~/working/test$ git push origin master
Counting objects: 3, done.
Writing objects: 100% (3/3), 193 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
error: unpack failed: unable to create temporary object directory
To http://myserver/test.git
! [remote rejected] master -> master (unpacker error)
error: failed to push some refs to 'http://myserver/test.git'

這一般情況下都是由於 Git 倉庫目錄的權限問題導致的,在這裏 Git 倉庫的根目錄 /git/repo 是 root 創建的,而運行 nginx 和 fcgiwrap 的用戶都是 www-data,我們可以把 Git 倉庫目錄設置成對所有人可讀可寫,也可以像下面這樣將它的擁有者設置成 www-data 用戶:

root@myserver:/# chown -R www-data:www-data /git/repo

2.憑證管理

上面我們站在管理員的角度解決了用戶身份認證的問題,但是站在用戶的角度,每次提交代碼都要輸入用戶名和密碼是一件很痛苦的事情。在上面介紹 SSH 協議時,我們可以使用 SSH 協議自帶的公鑰認證機制來省去輸入密碼的麻煩,那麼在 HTTP 協議中是否存在類似的方法呢?答案是肯定的,那就是 Git 的憑證存儲工具:credential.helper。

譬如像下面這樣,將用戶名和密碼信息保存在緩存中:

$ git config --global credential.helper cache

這種方式默認只保留 15 分鐘,如果要改變保留的時間,可以通過 --timeout 參數設置,或者像下面這樣,將密碼保存在文件中:

$ git config --global credential.helper store

這種方式雖然可以保證密碼不過期,但是要記住的是,這種方式密碼是以明文的方式保存在你的 home 目錄下的。可以借鑑操作系統自帶的憑證管理工具來解決這個問題, 比如 OSX Keychain 或者 Git Credential Manager for Windows。更多的內容可以參考《Pro Git》憑證存儲這一節。

除此之外,還有一種更簡單粗暴的方式:

aneasystone@little-stone:~/working$ git clone http://admin:123456@myserver/test.git

綜合對比

這一節對 Git 的四大協議做一個綜合對比。

本地協議

  • 優點:架設簡單,不依賴外部服務,直接使用現有文件和網絡權限,常用於共享文件系統
  • 缺點:共享文件系統的配置和使用不方便,且無法保護倉庫被意外損壞,傳輸性能較低

SSH 協議

  • 優點:架設簡單,所有數據經過授權加密,數據傳輸很安全,傳輸性能很高
  • 缺點:不支持匿名訪問,配置 SSH 的密鑰對小白用戶有一定的門檻

Git 協議

  • 優點:對開放的項目很適用,無需授權,傳輸性能最高
  • 缺點:缺乏授權機制,架設較麻煩,企業一般不會默認開放 9418 端口需要另行添加

HTTP/S 協議

  • 優點:同時支持授權訪問和無授權訪問,傳輸性能較高,配合 HTTPS 也可以實現數據安全
  • 缺點:架設 HTTP 服務較麻煩,認證憑證不好管理

更高級的工具

上面介紹的是搭建 Git 服務器最基本的方法,如果你只是希望能找一個版本控制系統來替代現有的 SVN,這也許就足夠了。但如果你希望你的版本控制系統能擁有一個更友好的 UI 界面,能更好的管理你的用戶和權限,能支持更現代的 Pull Request 功能以及能和 CI/CD 系統更緊密的聯繫起來,你就需要一個更高級的工具,你可以試試 GitWebGitoliteGitLabGogsGitea,當然,如果你願意,你也可以把代碼放在那些流行的代碼託管平臺上,比如 GitHubBitbucket 等等。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章