所有人都用 git 登录,GitLab 为什么还能认出你?
第一次认真看 GitLab 的 SSH 地址时,很多人都会产生同一个疑问:
git@gitlab.example.com:team/project.git
奇怪,明明我的 GitLab 用户名是 alice,这里为什么写的是 git?如果所有人都以 git 登录,GitLab 又怎么知道这次推送究竟是谁发起的?更让人担心的是:GitLab 是不是接管了整台服务器的 SSH,连其他 Linux 用户也会受到影响?
答案比“GitLab 做了一套特殊 SSH”更有意思:它没有重新发明 SSH,而是把 OpenSSH 原本就提供的身份认证、强制命令和外部密钥查询机制组合了起来。
先拆开两个容易混淆的“用户”
这里其实有两个互不相同的身份系统:
| 身份 | 示例 | 谁负责管理 | 用来做什么 |
|---|---|---|---|
| Linux 系统用户 | git |
操作系统 | 建立 SSH 会话、启动服务入口 |
| GitLab 应用用户 | alice |
GitLab 数据库 | 判断项目、分支和操作权限 |
git 是服务器上真实存在的 Linux 服务账号,alice 则通常只存在于 GitLab 的应用数据库里。创建一个 GitLab 账号,并不意味着服务器也会执行一次 useradd alice。
所有人的 SSH 连接先进入同一个 git 账号,GitLab 再根据连接所使用的公钥,把这次会话映射到真正的应用用户:
flowchart LR
A[SSH 用户 git] --> B{本次使用的公钥}
B -->|Key A| C[GitLab 用户 alice]
B -->|Key B| D[GitLab 用户 bob]
B -->|Deploy Key| E[某个项目或自动化身份]
所以,git 回答的是“从哪个系统入口进来”,alice 回答的是“进来的人在 GitLab 里是谁”。
一次 git clone 究竟发生了什么
当我们运行:
git clone git@gitlab.example.com:team/project.git
它看上去像一次普通 SSH 登录,实际上并不会打开远端终端。Git 客户端会请求远端执行一个受限制的 Git 服务命令。以读取仓库为例,它最终对应的是 git-upload-pack;推送则对应 git-receive-pack。
完整链路可以简化成这样:
sequenceDiagram
participant Client as 本机 Git
participant SSH as SSH 服务
participant Shell as GitLab Shell
participant API as GitLab 内部 API
participant Repo as Gitaly / 仓库
Client->>SSH: 以 git 用户连接,并出示公钥
SSH->>Shell: 把已识别的密钥和受限命令交给 GitLab Shell
Shell->>API: 这把密钥是谁?能否读取 team/project?
API-->>Shell: 用户 alice,允许读取
Shell->>Repo: 发起受控的 upload-pack 请求
Repo-->>Client: 返回 Git 对象
这里最重要的分工是:
- SSH 服务负责加密连接,并证明客户端持有某把私钥。
- GitLab Shell 负责把公钥映射成 GitLab 身份,并接管 Git over SSH 请求。
- GitLab 应用负责项目成员、角色和授权判断。
- Gitaly 负责实际的 Git 仓库存储与数据传输。
GitLab Shell 名字里虽然有一个 Shell,但它并不是 Bash 或 Zsh,也不会给用户一个可以随意执行命令的终端。它更像一个只听得懂少数 Git 指令的门卫。
OpenSSH 留下的“接管点”
GitLab 常见的接入方式有两类。它们实现细节不同,但目标相同:认证公钥以后,不启动普通终端,而是启动受限制的 GitLab Shell。
方式一:带强制命令的 authorized_keys
普通 Linux 用户的公钥记录可能只是:
ssh-ed25519 AAAA... alice@example.com
GitLab 管理的记录在概念上更像下面这样:
command="gitlab-shell key-123",no-port-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAA...
这不是一段应该手工复制的生产配置,而是一张原理图。它表达了几个重要限制:
| 限制 | 作用 |
|---|---|
command="..." |
无论客户端请求什么,都强制交给 GitLab Shell |
no-pty |
不提供交互式终端 |
no-port-forwarding |
不允许拿 GitLab SSH 做端口隧道 |
no-agent-forwarding |
不允许转发本地 SSH Agent |
key-123 |
让 GitLab 知道当前命中了哪把公钥 |
客户端原本请求执行的 Git 命令不会凭空消失。OpenSSH 可以把它作为原始命令信息交给受限入口,由 GitLab Shell判断它是不是允许的 Git 操作。
方式二:AuthorizedKeysCommand 动态查库
当用户和密钥很多时,让 OpenSSH 线性扫描一个巨大的 authorized_keys 文件并不理想。GitLab 还支持通过 AuthorizedKeysCommand 按公钥指纹动态查询 GitLab 数据库。
典型配置只匹配 git 用户,逻辑类似:
Match User git
AuthorizedKeysCommand ...gitlab-shell-authorized-keys-check...
AuthorizedKeysCommandUser git
Match all
Match User git 是一个值得留意的细节:特殊行为被限制在 GitLab 的服务账号内,后面的 Match all 则结束这段条件配置。现代部署也可能使用 GitLab 自带的 gitlab-sshd。无论底层选择哪种方式,“公钥查身份,GitLab Shell 做授权”的核心模型没有变化。
为什么普通 Linux 用户不受影响
一台服务器上的 SSH 行为可以想象成下面这棵树:
sshd
├── root -> root 自己的密钥和 Shell
├── deploy -> deploy 自己的密钥和 Shell
├── operator -> operator 自己的密钥和 Shell
└── git -> GitLab Shell,只处理受控的 Git 请求
GitLab 不需要接管 root、deploy 或其他运维账号。它只需要管理专用的 git 用户,以及这个用户对应的密钥查询和命令入口。
正常情况下,下面两次连接会走完全不同的路径:
# 普通服务器运维登录
ssh operator@gitlab.example.com
# GitLab 的 Git over SSH 入口
ssh -T git@gitlab.example.com
第二条命令通常只会返回类似下面的欢迎信息,然后结束连接:
Welcome to GitLab, @alice!
这句话其实同时证明了三件事:SSH 已连接到 Linux 的 git 用户;当前客户端持有一把 GitLab 认可的私钥;这把密钥最终被映射到了 GitLab 用户 alice。
如果服务器真的存在一个普通 Linux 用户 alice,那只是恰好同名,与 GitLab 账号没有天然关系。运行 ssh alice@gitlab.example.com 走的是 Linux 账号认证,不会因为网页上的 GitLab 用户名也叫 alice 就自动获得访问权。
为什么不为每个 GitLab 用户创建一个 Linux 账号
想象一下,一家公司的 GitLab 有一万名用户和更多外部协作者。如果为每个人创建 Linux 账号,GitLab 就必须把下面这些应用概念翻译成 Unix 文件权限:
- 项目和子组成员关系;
- Owner、Maintainer、Developer、Reporter 等角色;
- 受保护分支和受保护标签;
- 临时封禁、离职和外部身份同步;
- Deploy Key、机器人和 CI/CD 身份。
这既笨重,又会让每个代码托管用户都更接近服务器登录面。统一的 git 服务账号把职责切得很干净:Linux 只负责可靠地建立受限连接,复杂的业务权限继续留在 GitLab 中。
SSH 配置里的 User git 不是你的 GitLab 用户名
因此,一份普通的客户端配置通常会写成:
Host gitlab.example.com
HostName gitlab.example.com
User git
Port 22
IdentityFile ~/.ssh/id_ed25519_gitlab
IdentitiesOnly yes
这里的 User git 指向服务器上的统一服务账号。真正的 GitLab 用户由 IdentityFile 对应的公钥决定。
如果远端 URL 已经显式包含用户名:
git@gitlab.example.com:team/project.git
其中的 git@ 会明确指定 SSH 用户。客户端配置里的 User 主要保证执行下面这种不带用户名的命令时,行为依然正确:
ssh -T gitlab.example.com
同一台 GitLab 使用多个账号怎么办
答案仍然不是把 SSH 用户改成 alice 或 bob,而是使用不同密钥和不同 Host alias:
Host gitlab-work
HostName gitlab.example.com
User git
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yes
Host gitlab-personal
HostName gitlab.example.com
User git
IdentityFile ~/.ssh/id_ed25519_personal
IdentitiesOnly yes
然后让不同仓库使用不同 alias:
git@gitlab-work:team/project.git
git@gitlab-personal:alice/side-project.git
两个连接的 Linux 用户仍然都是 git,但它们出示的公钥不同,因此会映射到不同的 GitLab 账号。
这套设计的安全边界
设计巧妙不等于可以忽略密钥安全。GitLab 把公钥视为身份凭证;谁拿到了对应私钥,谁就可能以那个 GitLab 身份访问代码。
实践中至少应注意:
- 给个人私钥设置口令,并配合可信的 SSH Agent 使用。
- 不要在多台机器之间随意复制同一把私钥。
- 离职、设备丢失或密钥疑似泄露时,及时从 GitLab 删除公钥。
- 自动化任务优先使用权限范围明确的 Deploy Key 或专用机器人身份。
- 不要手工向 GitLab 的
git系统用户添加一个不带强制命令限制的公钥,否则可能破坏原有安全边界。 - 区分用户密钥与服务器 Host Key:前者证明“你是谁”,后者帮助客户端确认“服务器是谁”。
最后:它没有绕过 Linux,而是借力 Linux
回到最初的问题:所有人都用 git 登录,GitLab 为什么还能认出你?
因为 git 只是共享的系统入口,公钥才是进入 GitLab 身份系统的索引。OpenSSH 完成加密和持钥证明,GitLab Shell 接管受限命令,GitLab 应用执行项目授权,Gitaly 完成仓库传输。
SSH 用户 git
+ 不同的公钥
+ GitLab Shell
+ GitLab 权限模型
= 同一个系统入口下的不同 GitLab 身份
最漂亮的地方也正在这里:GitLab 没有推翻成熟的 SSH 体系,只是在恰当的位置接入自己的身份和权限模型。知道这一点之后,再看到 URL 里的 git@,它就不再像一个奇怪的默认用户名,而更像一扇设计得相当克制的门。
延伸阅读
版权属于:一名宅。
本文链接:https://zhaiyiming.com/archives/81.html
转载时须注明出处及本声明