所有人都用 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 不需要接管 rootdeploy 或其他运维账号。它只需要管理专用的 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 用户改成 alicebob,而是使用不同密钥和不同 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 身份访问代码。

实践中至少应注意:

  1. 给个人私钥设置口令,并配合可信的 SSH Agent 使用。
  2. 不要在多台机器之间随意复制同一把私钥。
  3. 离职、设备丢失或密钥疑似泄露时,及时从 GitLab 删除公钥。
  4. 自动化任务优先使用权限范围明确的 Deploy Key 或专用机器人身份。
  5. 不要手工向 GitLab 的 git 系统用户添加一个不带强制命令限制的公钥,否则可能破坏原有安全边界。
  6. 区分用户密钥与服务器 Host Key:前者证明“你是谁”,后者帮助客户端确认“服务器是谁”。

最后:它没有绕过 Linux,而是借力 Linux

回到最初的问题:所有人都用 git 登录,GitLab 为什么还能认出你?

因为 git 只是共享的系统入口,公钥才是进入 GitLab 身份系统的索引。OpenSSH 完成加密和持钥证明,GitLab Shell 接管受限命令,GitLab 应用执行项目授权,Gitaly 完成仓库传输。

SSH 用户 git
  + 不同的公钥
  + GitLab Shell
  + GitLab 权限模型
  = 同一个系统入口下的不同 GitLab 身份

最漂亮的地方也正在这里:GitLab 没有推翻成熟的 SSH 体系,只是在恰当的位置接入自己的身份和权限模型。知道这一点之后,再看到 URL 里的 git@,它就不再像一个奇怪的默认用户名,而更像一扇设计得相当克制的门。

延伸阅读