pre-receive钩子必须用go编译的静态二进制文件,因其在git协议层毫秒级拦截,需满足elf格式、无解释器依赖、属git用户且可执行;shell脚本会被忽略或报exec format error。

pre-receive 钩子能拦明文密钥,但必须用 Go 二进制而非 shell 脚本
GitLab 社区版(含 Docker 部署)的 pre-receive 钩子不支持直接执行 shell 脚本——它只认可具有可执行权限、且能从标准输入读取 <old-sha><new-sha><ref-name></ref-name></new-sha></old-sha> 格式数据的二进制程序。你写个 grep -r "password =" 的 bash 脚本放进去,大概率静默失败或直接被 Git 进程忽略。官方明确要求:钩子文件必须是 ELF 可执行体,且不依赖外部解释器路径(比如不能硬编码 /usr/bin/python3)。Go 编译出的静态二进制刚好满足这点:无运行时依赖、跨平台、启动快,适合在 Git 协议层做毫秒级拦截。
常见错误现象:
– 推送后没报错,但钩子逻辑完全没触发
– 报错 fatal: could not execute hook 'pre-receive'
– 日志里看到 exec format error
- 必须用
go build -o pre-receive -ldflags="-s -w"编译,禁用调试符号减小体积 - 不要用
CGO_ENABLED=0 go build,除非你确认所有依赖都不调 C 库(比如某些正则或 crypto 包会悄悄依赖) - 编译目标平台要和 GitLab 服务器一致(通常是
linux/amd64或linux/arm64)
怎么从推送的 commit 中提取 patch 并扫描密钥
钩子收不到完整文件内容,只能拿到 Git 对象的 SHA 和 ref 变更列表。想查明文密钥,得自己调用 git 命令解析 diff:对每个 <new-sha></new-sha>,执行 git diff $old_sha $new_sha --no-commit-id --unified=0 | grep -E "(password|secret|api_key|token).*=.*["'"]"。但注意:这不是在工作区跑命令,而是在 bare repo 环境下,所以必须显式设置 GIT_DIR=/var/opt/gitlab/git-data/repositories/@hashed/xxx.git,否则 git diff 找不到对象库。
性能影响很实际:一次 push 含 10 个 commit,每个 commit 平均 diff 出 200 行,正则扫一遍约 80ms;如果并发推送多,CPU 就容易打满。建议加限制:
- 只检查
refs/heads/(main|master|develop)分支,跳过refs/tags/和 CI 临时分支 - 对每个 commit,只 diff 修改的 .js/.py/.env/.yml 文件,跳过
.min.js、node_modules/、dist/ - 用
git show --name-only $sha先过滤路径,再对目标文件做git show $sha:xxx提取内容,避免全量 diff
为什么 grep 正则很容易漏掉真实密钥
硬编码密钥的写法千奇百怪:const API_KEY = "sk-abc123"、os.environ['DB_PASS'] = os.getenv("DB_PASS", "root")、甚至 base64 编码的字符串。单纯匹配 password = 或引号内长字符串,会漏掉变量拼接、环境 fallback、注释伪装等场景。真实生产环境用的规则更接近:
password\s*[:=]\s*["'][a-zA-Z0-9+/]{20,}["']
api[_-]?key\s*[:=]\s*["'][a-zA-Z0-9]{32,}["']
(?i)(secret|token)\s*[:=]\s*["'][^\n"']{16,}["']
但即便这样,也会误杀 token = "test" 或漏掉 process.env.PASSWD || "default"。所以真正可靠的方案不是靠正则穷举,而是结合两层:
- 第一层:用正则快速筛出高风险行(命中即拒)
- 第二层:对疑似行做简单语法解析(比如检查右侧是否为纯字符串字面量,而非函数调用或变量名)
Go 里可用 go/ast 解析 .py/.js 源码,但要注意:不是所有语言都需深度解析,.env 文件直接逐行 scan 更稳。
部署位置和权限最容易被忽略的三个点
GitLab 容器化部署后,pre-receive 文件不能随便扔进 /var/opt/gitlab/git-data/repositories/ 下任意路径——它必须放在对应仓库的 hooks/ 子目录里,且路径由 GitLab 的 @hashed 机制决定。你得先查出仓库真实路径:
sudo gitlab-rails runner "p Project.find_by_full_path('group/name').repository.full_path"
输出类似 /var/opt/gitlab/git-data/repositories/@hashed/ab/cd/ef...git,然后把编译好的 pre-receive 放进该目录下的 hooks/ 里,并设权限:
-
chmod +x hooks/pre-receive—— 必须可执行 -
chown git:git hooks/pre-receive—— 必须属git用户,否则 Git 进程拒绝调用 -
ls -l hooks/pre-receive确保没有 setuid/setgid 位(Git 会因安全策略拒绝执行)
改完别忘了 sudo gitlab-ctl restart gitlab-workhorse,否则新钩子不会加载。很多团队卡在这步,以为钩子没生效,其实是服务没重载。











