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

推荐订阅源

博客园 - 三生石上(FineUI控件)
Martin Fowler
Martin Fowler
月光博客
月光博客
AI
AI
B
Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
C
CXSECURITY Database RSS Feed - CXSecurity.com
WordPress大学
WordPress大学
GbyAI
GbyAI
L
Lohrmann on Cybersecurity
O
OpenAI News
Schneier on Security
Schneier on Security
P
Palo Alto Networks Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
T
Troy Hunt's Blog
V2EX - 技术
V2EX - 技术
W
WeLiveSecurity
L
LINUX DO - 最新话题
人人都是产品经理
人人都是产品经理
S
Security Affairs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
A
Arctic Wolf
Recorded Future
Recorded Future
Microsoft Security Blog
Microsoft Security Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
G
GRAHAM CLULEY
N
Netflix TechBlog - Medium
TaoSecurity Blog
TaoSecurity Blog
C
Check Point Blog
Scott Helme
Scott Helme
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Apple Machine Learning Research
Apple Machine Learning Research
PCI Perspectives
PCI Perspectives
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Vercel News
Vercel News
The Hacker News
The Hacker News
Y
Y Combinator Blog
Latest news
Latest news
SecWiki News
SecWiki News
Hugging Face - Blog
Hugging Face - Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Google Online Security Blog
Google Online Security Blog
Webroot Blog
Webroot Blog
Google DeepMind News
Google DeepMind News
Recent Commits to openclaw:main
Recent Commits to openclaw:main
C
Cisco Blogs
博客园_首页
H
Hackread – Cybersecurity News, Data Breaches, AI and More
宝玉的分享
宝玉的分享
L
LINUX DO - 热门话题

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 和 透明代理 有限状态机与分布式熔断器 「家庭娱乐」N1刷 EmuELEC 做游戏盒子 「家庭娱乐」N1刷 Armbian 做家庭服务器 分布式限流器之令牌桶实现 Kustomize 管理 Kubernetes 集群 Mixedpaper 主题发布到 Hugo 官方仓库 About 优化 Docker 构建时间 解决 etcd 与 grpc 不兼容问题 Kubernetes 部署 ElasticSearch7 集群 从零开始配置Rime - 2019 转移域名,更换评论系统,更换图床 搭建 NPM Proxy 之预编译包 搭建 NPM Proxy 美区Apple ID终于绑定PayPal成功 无可奈何的页面增强方案 LogStash的调试 Python打开类 荒原 [Theme] Bitcron主题Mixedpaper发布! Ngrok端口转发 故事之五 - 柠檬茶 故事之四 - 我是天才 故事之一 Arch滚挂了怎么办? OpenWrt内网IPv6之二 某次更新后Primusrun不能启动Dota2 OpenWrt的DNS智能解析方案 替换Google CDN库 使用Privoxy将socks代理转化为http代理 ArchLinux之软件篇 再次安装ArchLinux手记 博客再次重生 搞了个Minecraft服务器 【LnV工作室荣誉出品】小苹果MV华农大版 雨梦 醉臥石中笑人間 路由器内网IPV6地址分配 OPENWRT安装软件的两个问题 路由器上进行锐捷认证 改了BIOS的LOGO Windows下搭建我的C/C++的开发环境 Linux下用Mentohust认证校园网 Archlinux+Windows 双系统安装手记(UEFI+GPT) Google-code-prettify代码高亮 《末世迷踪》 Hello Net World!
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