惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - Franky
N
Netflix TechBlog - Medium
宝玉的分享
宝玉的分享
Google DeepMind News
Google DeepMind News
腾讯CDC
G
Google Developers Blog
Martin Fowler
Martin Fowler
Microsoft Security Blog
Microsoft Security Blog
Recent Announcements
Recent Announcements
爱范儿
爱范儿
Engineering at Meta
Engineering at Meta
Microsoft Azure Blog
Microsoft Azure Blog
A
About on SuperTechFans
aimingoo的专栏
aimingoo的专栏
有赞技术团队
有赞技术团队
Jina AI
Jina AI
人人都是产品经理
人人都是产品经理
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
M
MIT News - Artificial intelligence
罗磊的独立博客
博客园 - 三生石上(FineUI控件)
美团技术团队
WordPress大学
WordPress大学
阮一峰的网络日志
阮一峰的网络日志

WAYJAM

告别 NPM Token:迁移到 Trusted Publishing 全记录 Sing-box:IPv6 优先出站与 Netflix 强制 IPv6 分流 [All In One] HomeLab 虚拟化 iKuai + Mihomo - IPv6 支持 [All In One] HomeLab 虚拟化 iKuai + Mihomo LiteLLM 多模型 API 中转 MacOS Bootstrp with Nix Fluency Support Full Rss [All In One] PVE 中 OpenWrt LXC 重启问题 Tabby 终端配置以及配置全平台同步 Cloudflare Tunnel + Nginx Proxy Manager:服务穿透 PVE 7 升级 8 笔记 PVE 安装 Android x86 & 安装 Magisk Fly.io 部署 Artalk 评论系统 [All In One] HomeLab 2023 搭建总结 [All In One] 轻量的文件 & 媒体服务器配置 [All In One] R71s 主机散热改造 [All In One] PVE LXC 安装 OpenWrt 软路由以及网络规划 [All In One] 安装 Proxmox VE 7.4 从零开始配置Rime - 2023 Self Hosted Bitwarden + Cloudfare HTTPS Debian NFS Server 以及 MacOS Client AdguardHome 和 Clash 透明代理配合使用 使用 Kind 部署 Prow 手记 BackBlaze B2 + Cloudfare Worker 图床 PicGo Amazon S3 插件 自动给模板生成 Inline-Svg Hugo Theme WeUI 发布! Hugo Theme Fluency 发布! Docker Clash 和 透明代理 有限状态机与分布式熔断器
GitLab Shell如何通过SSH工作
2018-07-02 · via WAYJAM

GitLab访问Git仓库

首先回顾GitLab的Git仓库四种访问方式:

  1. git pull over http -> gitlab-rails (Authorization) -> accept or decline -> execute git command
  2. git push over http -> gitlab-rails (git command is not executed yet) -> execute git command -> gitlab-shell pre-receive hook -> API call to gitlab-rails (authorization) -> accept or decline push
  3. git pull over ssh -> gitlab-shell -> API call to gitlab-rails (Authorization) -> accept or decline -> execute git command
  4. git push over ssh -> gitlab-shell (git command is not executed yet) -> execute git command -> gitlab-shell pre-receive hook -> API call to gitlab-rails (authorization) -> accept or decline push

四种方式都有GitLab Shell的参与,但不同过程GitLab Shell发挥了不同的作用,并且它并不是一个整体的服务,而是由一些子命令组合而成。HTTP方式的Git操作,经gitlab workhorse直接交由Rails应用处理,然后通过HTTP协议交换数据,对于git的操作有三条路径:Gem包Rugged、Raw Git命令或者Gitaly,push/pull一般只跟后两种有关,GitLab Shell充当的作用仅仅是git hook的作用。

SSH方式的push/pull是GitLab Shell的主场景,而Rails在这其中充当了权限再验证的角色。

Git SSH 传输协议

首先,简要说明Git是如何通过SSH协议与服务端的Git交互数据。

ssh git@example.com "the-command"

在客户端执行这样的命令时候,服务端SSHD验证身份通过后,默认将启动一个Shell解析执行the-command的命令。普通使用中,大多都不加自定义命令,这将启动一个Shell交互式命令解析器。

要了解GitLab Shell的原理,不能不说它的“前任”——Gitolite。Gitolite是一个Git的授权层前端,同样提供了HTTP(httpd)和SSH的方式访问Git仓库。而Gitlab在v5.0完全用GitLab Shell替代了Gitolite,前者完全依赖Rails层的权限认证(项目、分支、用户等的权限),后者则由于需要完全保持一份冗余数据在自身的配置文件,主要由于速度和数据不同步被Gitlab官方放弃。但是,二者仍然采用了 authorized_keyscommand magic方案。

GitLab CE 10.4之后加入了`AuthorizedKeysCommand`的使用(_require_ OpenSSH 6.9+),使用自定义程序匹配Key而不是文件文本匹配。

SSH command magic

SSHD服务端收到客户端的连接请求后,会在authorized_keys进行匹配,认证失败则拒绝连接。查看~/.ssh/authorized_keys文件内容如下:

# Gitolite
command="[path]/gitolite-shell user-one",[more options] ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArXtCT...
# GitLab Shell
command="/home/git/gitlab-shell/bin/gitlab-shell key-1",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArXtCT...

可以发现保存的每条公钥前面都有command=,Gitolite和GitLab Shell后面的参数代表着用户的识别信息,Gitolite保存在配置文件,而gitlab保存在数据库。当用户的服务端校验成功后,则会执行command=后的命令,而不是shell access,同时将SSH命令所要执行的命令赋值给SSH_ORIGINAL_COMMAND。所以与客户端交互的真正命令是(以pull代码为例):

SSH_ORIGINAL_COMMAND=git-upload-pack git-pack/home/git/gitlab-shell/bin/gitlab-shell key-1

“Shell”

GitLab Shell由ruby脚本和go程序组成,首先看GitLab Shell入口代码:bin/gitlab-shell

#!/usr/bin/env ruby
# ...
key_id = /key-[0-9]+/.match(ARGV.join).to_s
original_cmd = ENV.delete('SSH_ORIGINAL_COMMAND')
# ...
require File.join(ROOT_PATH, 'lib', 'gitlab_shell')
if GitlabShell.new(key_id).exec(original_cmd)
# ...

这段代码里面的两个变量:

  • key_id -> sshd调用GitLab Shell时传入的参数。
  • original_cmd -> 即上文提到的SSH_ORIGINAL_COMMAND环境变量,并且获取完即移除。

举个例子,如果用户通过git客户端调用git clone git@server的时候,实际上git客户端启动的是receive-pack(git由许许多多子命令组成)并且在内部执行的是ssh git@git@server git-upload-pack git@server,那么此时,服务端给GitLab Shell设定的环境变量则是git-upload-pack git@server,。

然后看lib/gitlab-shell.rb代码,初始化一个GitlabShell,并且执行exec,而exec里面有几个关键步骤:

def exec(origin_cmd)
  unless origin_cmd
    puts "Welcome to GitLab, #{username}!"
    return true
  end

  args = Shellwords.shellwords(origin_cmd)
  args = parse_cmd(args)

  if GIT_COMMANDS.include?(args.first)
    GitlabMetrics.measure('verify-access') { verify_access }
  end

  process_cmd(args)

  true
rescue something #异常
end
1. 解析命令 parse_cmd

主要是处理Windows/Linux命令差异、屏蔽非法命令、LFS命令。

2. 验证权限 verify_access
/api/v4/internal/allowed

通过Rails的HTTP API :/api/v4/internal/allowed发送查询参数到Rails,接口返回此用户对这个仓库是否有此操作的权限。

参数:

{
  command => git命令
  repo => 仓库信息
  key_id => SSH key id(在数据库能找查找到对应的用户)
  protocol => ssh/env
}
3. 处理命令 process_cmd

处理命令,检测gitaly此特性是否有开启,如果开启则调用gitaly处理,否则则调用git原生命令。

设定环境变量,然后使用Kernel.exec调用目标进程替代当前进程。

env = {
    'HOME' => ENV['HOME'],
    'PATH' => ENV['PATH'],
    'LD_LIBRARY_PATH' => ENV['LD_LIBRARY_PATH'],
    'LANG' => ENV['LANG'],
    'GL_ID' => @key_id,
    'GL_PROTOCOL' => GL_PROTOCOL,
    'GL_REPOSITORY' => @gl_repository,
    'GL_USERNAME' => @username
}
# ...
Kernel.exec(env, *args, unsetenv_others: true, chdir: ROOT_PATH)

处理Git命令

以Pull,且调用Gitaly为例

// bin/gitaly-upload-pack
code, err := handler.UploadPack(os.Args[1], &request)
// go/internal/handler/upload_pack.go
func UploadPack(gitalyAddress string, request *pb.SSHUploadPackRequest) (int32, error) {
    # ...

	conn, err := client.Dial(gitalyAddress, dialOpts())
	if err != nil {
		return 0, err
	}
	defer conn.Close()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	return client.UploadPack(ctx, conn, os.Stdin, os.Stdout, os.Stderr, request)
}
// go/vendor/gitlab.com/gitlab-org/gitaly/client/upload_pack.go
func UploadPack(ctx context.Context, conn *grpc.ClientConn, stdin io.Reader, stdout, stderr io.Writer, req *pb.SSHUploadPackRequest) (int32, error) {
	ctx2, cancel := context.WithCancel(ctx)
	defer cancel()

	ssh := pb.NewSSHServiceClient(conn)
	stream, err := ssh.SSHUploadPack(ctx2)
	if err != nil {
		return 0, err
	}

	if err = stream.Send(req); err != nil {
		return 0, err
	}

	inWriter := streamio.NewWriter(func(p []byte) error {
		return stream.Send(&pb.SSHUploadPackRequest{Stdin: p})
	})

	return streamHandler(func() (stdoutStderrResponse, error) {
		return smHandler(func() (stdoutStderrResponse, error) {
		return stream.Recv()
	}, func(errC chan error) {
		_, errRecv := io.Copy(inWriter, stdin)
		stream.CloseSend()
		errC <- errRecv
	}, stdout, stderr)
}

可以看到通过grpc跟gitaly server通信,获得响应之后:

# go/vendor/gitlab.com/gitlab-org/gitaly/client/std_stream.go
exited code => resp.GetExitStatus().GetValue()
stderr => stderr.Write(resp.GetStderr())
stdout => stdout.Write(resp.GetStdout())

git客户端将标准错误打印到控制台,解析标准输出作为git数据:通过远程ssh调用命令将数据打印到标准输出传输到客户端解析

Cloning into 'gitlab-shell'...
remote: Counting objects: 5558, done.
remote: Compressing objects: 100% (2548/2548), done.
remote: Total 5558 (delta 3051), reused 5117 (delta 2754)
Receiving objects: 100% (5558/5558), 2.83 MiB | 2.72 MiB/s, done.
Resolving deltas: 100% (3051/3051), done.

Refs

  • https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols
  • https://gitlab.com/gitlab-org/gitlab-shell/
  • https://docs.gitlab.com/ce/administration/operations/fast_ssh_key_lookup.html