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 等等。

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