1、背景
研发gitlab CI/CD时,需要编辑一个整个代码仓库统一的CI/CD流程,用于流程与权限的控制。众所周知,Gitlab的CI/CD流程是通过.gitlab-ci.yml文件配置的。通常,如果用户拉出自己的开发分支,那么该yaml文件也会被用户修改,也就是说用户可以完全不用当前的CI/CD流程而重新自定义自己的流程,越权部署代码,存在极大的安全风险。
2、适用场景
需要利用Gitlab的CI/CD功能,保护.gitlab-ci.yml不被普通用户修改;保护其他CI/CD调用到的相关文件不被用户修改。或推广到保护代码仓库中任意文件。
3、方法论
Gitlab目前并没有直接提供锁定或者保护文件相关的功能,经过充分思考与研究后,采用通过配置服务端钩子(Hook onServer),来检查特定文件是否被修改,如有发生,就拒绝接受用户推到远端的代码以保护文件。
4、开发与实践
- 代码仓库位置
可能位置1:/home/git/repositories//.git
可能位置2:/var/opt/gitlab/git-data/repositories//.git
注意: 代码仓库名称与路径可能被哈希处理了,例如当前测试的gitlab机器上就是这种情况,需要进入@hashed目录,
sh-4.2$ pwd
/var/opt/gitlab/git-data/repositories
sh-4.2$ ls
+gitaly @hashed
sh-4.2$ cd \@hashed/
sh-4.2$ tree
`-- d4
`-- 73
|-- d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35.git
| |-- branches
| |-- config
| |-- custom_hooks
| | `-- pre-receive
| |-- description
| |-- HEAD
| |-- hooks
| | |-- applypatch-msg.sample
| | |-- commit-msg.sample
| | |-- fsmonitor-watchman.sample
| | |-- post-update.sample
| | |-- pre-applypatch.sample
| | |-- pre-commit.sample
| | |-- pre-merge-commit.sample
| | |-- prepare-commit-msg.sample
| | |-- pre-push.sample
| | |-- pre-rebase.sample
| | |-- pre-receive.sample
| | `-- update.sample
| |-- info
| | |-- exclude
| | `-- refs
| |-- language-stats.cache
| |-- objects
| | |-- 47
| | | `-- 81462ff2e83f3130effecea404514a8ef7382
| | |-- info
| | | `-- packs
| | `-- pack
| | |-- pack-00634d576da38a59c654603dbb72a9e12d647124.bitmap
| | |-- pack-00634d576da38a59c654603dbb72a9e12d647124.idx
| | |-- pack-00634d576da38a59c654603dbb72a9e12d647124.pac
| |-- packed-refs
| `-- refs
| |-- heads
| | `-- daliang_test_branch
| |-- keep-around
| | `-- f37f13e889088502c18715e974299e3aafe0ff7a
| |-- merge-requests
| `-- pipelines
找到要配置的代码仓库,上图同时也展示仓库的目录与文件结构。
- 配置服务端钩子
服务器端的钩子主要为:
pre-receive:
在有人用git push向仓库推送代码时被执行
post-receive:
post-receive钩子在成功推送后被调用,适合用于发送通知。对很多工作流来说,这是一个比post-commit更好的发送通知的地方,因为这些更改在公共的服务器而不是用户的本地机器上。给其他开发者发送邮件或者触发一个持续集成系统都是post-receive常用的操作。这个脚本没有参数,但和pre-receive一样通过标准输入读取
update:
update钩子在pre-receive之后被调用,用法也差不多。它也是在实际更新前被调用的,但它可以分别被每个推送上来的引用分别调用。也就是说如果用户尝试推送到4个分支,update会被执行4次。和pre-receive不一样,这个钩子不需要读取标准输入。
事实上,它接受三个参数:
更新的引用名称
引用中存放的旧的对象名称
引用中存放的新的对象名称
因此,我们这里用pre-receive。
sh-4.2$ cd \@hashed/d4/73/d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35.git/
sh-4.2$ mkdir custom_hooks
vim pre-receive
该文件为:
#!/bin/bash
echo "I'm a hooked file on server"
z40=0000000000000000000000000000000000000000
echo $GL_USERNAME
while read oldrev newrev refname; do
if [ $oldrev == $z40 ]; then
# Commit being pushed is for a new branch
oldrev=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi
yaml_file_name=".gitlab-ci.yml"
approval_user="root"
file_name=$(git diff --name-only $oldrev $newrev)
if [ "$yaml_file_name" == "$file_name" ] && [ $GL_USERNAME != "$approval_user" ]; then
echo "No permission to modify ${yaml_file_name}"
exit 1
fi
done
echo "I'm a hooked file on server"
- 测试结果
当时用户修改.gitlab-ci.yml之后push,会被服务端拒绝,从而保护了该文件及CI/CD流程不被篡改。
17:41:54.577: [b2c-airflow-poc] git -c credential.helper= -c core.quotepath=false -c log.showSignature=false push --progress --porcelain origin refs/heads/daliang_test_branch:daliang_test_branch
Authenticated to 3.86.51.69 ([3.86.51.69]:22).
Enumerating objects: 5, done.
Delta compression using up to 12 threads
Total 3 (delta 2), reused 0 (delta 0)
remote: I'm a hooked file on server
remote: No permission to modify .gitlab-ci.yml
Transferred: sent 3368, received 3948 bytes, in 1.2 seconds
Bytes per second: sent 2789.3, received 3269.7
error: failed to push some refs to 'git@3.86.51.69:root/b2c-airflow-poc.git'
To 3.86.51.69:root/b2c-airflow-poc.git
! refs/heads/daliang_test_branch:refs/heads/daliang_test_branch [remote rejected] (pre-receive hook declined)