多个 SSH 公钥共用 git 账户时,如何在 Git hook 中识别推送者?
可以把每把 SSH 公钥映射到服务器指定的身份,再通过环境变量传给 PHP hook。不同 SSH 会话的进程环境相互独立,即使共用 git 账户,变量也不会互相覆盖。
如果账户的登录 shell 是 git-shell,原来的 command="export ... && git-shell ..." 写法通常会被拒绝,因为它包含普通 shell 语法。生产部署权限还必须由服务器上的可信逻辑检查,不能仅凭变量存在就允许部署。
为什么 whoami 无法区分推送者?
whoami 返回的是进程所属的系统账户。所有人都通过 git 账户连接,结果自然都是 git。
要区分推送者,需要获取本次认证公钥对应的身份。Git 提交中的 author、committer 和客户端配置的 user.name 都不能代替 SSH 认证身份。
保留 git-shell 时,如何传递身份?
如果服务器的 OpenSSH 支持按变量名限制 PermitUserEnvironment,可以用 authorized_keys 的 environment= 选项传递身份,无需使用 export。
先由管理员在 sshd_config 的全局配置中仅允许所需变量:
PermitUserEnvironment CURR_USER
这项配置会影响服务器对环境变量的处理,需要结合现有设置确认。不要为了传递一个身份变量而直接开放所有用户环境变量。OpenSSH 文档提醒,某些环境变量可能绕过访问限制。该选项默认关闭,具体语法以服务器安装版本为准。配置说明
然后给每把公钥添加固定身份。下面的姓名和公钥内容均为示例,<PUBLIC_KEY_...> 必须替换为真实公钥的 Base64 字段:
restrict,environment="CURR_USER=JohnDoe" ssh-ed25519 <PUBLIC_KEY_JOHN> john
restrict,environment="CURR_USER=JaneDoe" ssh-ed25519 <PUBLIC_KEY_JANE> jane
使用对应公钥认证时,environment= 会设置变量。行末注释只便于管理员辨认,不会自动成为会话身份。restrict 会关闭端口转发、agent 转发、X11 转发、PTY 和用户 SSH rc 执行,但仍需依靠 git-shell 限制可执行命令。公钥选项说明
保留账户原有的 git-shell 登录 shell。修改 SSH 配置后,先执行配置检查,再按系统的服务管理方式重新加载:
sshd -t
PHP hook 如何读取并使用这个身份?
由本次 Git 接收进程直接启动的 PHP hook 通常会继承该环境变量。如果中间经过 sudo、任务队列或其他会清理环境的程序,则需要另行设计可信的身份传递方式。
<?php
$user = getenv('CURR_USER');
if ($user === false || $user === '') {
fwrite(STDERR, "Missing authenticated identity; deployment denied.\n");
exit(1);
}
// 示例策略:实际名单应由服务器管理员维护。
$productionUsers = ['JohnDoe'];
$canDeployProduction = in_array($user, $productionUsers, true);
// 在现有逻辑中结合本次更新的 ref 判断部署目标。
// 只有 $canDeployProduction 为 true 时才允许生产部署。
身份缺失或不在授权名单时,应拒绝相应部署。生产部署逻辑还要检查本次更新的完整 ref 名称,例如 refs/heads/production,避免只凭模糊的分支名称匹配就执行发布。这里的 ref 名称仅为示例。
原来的 command 写法哪里有问题?
OpenSSH 会通过账户的登录 shell 执行命令,强制命令也不会自动获得 /bin/sh 的语法能力。SSH 命令执行说明
git-shell 只接受受支持的 Git 服务端命令,以及配置在 ~/git-shell-commands 中的自定义命令,不能把 export ... && ... 当作普通 shell 脚本执行。git-shell 文档
如果不采用 environment=,可以设计一个由管理员控制的入口程序,接收服务器固定配置的身份、设置环境,再启动受限制的 Git 服务。保留 git-shell 时,需要通过其自定义命令机制接入。入口程序必须验证允许的命令和仓库路径,不要用 eval 或 sh -c "$SSH_ORIGINAL_COMMAND" 直接执行客户端提供的内容。
有没有现成的公钥信息可以读取?
在支持该功能的 OpenSSH 上,可以启用:
ExposeAuthInfo yes
启用后,SSH_USER_AUTH 环境变量会指向一个临时文件,其中包含本次认证使用的方法和公开凭据,包括公钥。这个变量保存的是文件路径,并不是用户名或公钥指纹。仍需解析文件,并将凭据映射到服务器维护的身份和权限。该功能默认关闭,因此“任何情况下都无法获得认证公钥”的说法过于绝对。ExposeAuthInfo 说明
手动维护少量公钥时,逐把公钥配置固定身份通常更直接。
这种方案的安全边界是什么?
环境变量可以承载服务器确认的身份,安全性取决于谁能设置变量,以及谁能修改授权逻辑:
- 不要通过
AcceptEnv接收客户端声明的身份,也不要用提交信息或 push option 作为授权依据。 authorized_keys、~/.ssh/environment、hook、仓库配置和权限名单必须受到保护,普通推送者不能修改这些文件或替换其父目录。- 同一个账户的其他登录或执行入口不能绕过身份绑定与权限检查。
- 公钥识别的是私钥持有者。多人共用同一把私钥时,无法据此区分具体人员。
共享 git 账户意味着共享系统权限。如果推送的代码能在服务器上以该账户执行任意命令,环境变量就不足以隔离这些人。生产发布应由权限受限的部署入口执行,并保护好部署凭据和授权代码。
并发连接是否会互相覆盖?
不会。环境变量属于进程及其继承链,不是系统账户上的全局字典。JohnDoe 和 JaneDoe 的会话各有自己的 CURR_USER,各自启动的 Git 进程和 hook 会继承对应值。
但如果把“当前用户”写入同一个文件或全局缓存,就会产生覆盖问题。并发部署同一个目录也可能冲突,需要另外通过锁或部署队列串行处理。
生产分支权限应放在哪个 hook?
如果目标是拒绝未授权用户更新生产分支或标签,应在 pre-receive 或 update 中检查身份与 ref。pre-receive 返回非零状态可以拒绝本次引用更新,update 可以拒绝单个 ref 的更新。
post-receive 执行时引用已经更新完成,退出失败只能表示后续处理失败,无法撤销已经接受的 push。可以用它触发已获授权的部署,但不能把它作为阻止生产分支更新的唯一检查点。Git hooks 文档
备注:内容仅供参考。
