在公网搭建的 GitLab 频频遇到安全挑战,然而其实只需要做一两个简单的动作,维护成本就能够大大降低,并且还能避免未被许可的内容,被搜索引擎爬虫暴露的到处都是。
本篇文章,我们就来聊聊公网搭建的 GitLab 代码仓库的安全小细节。
写在前面
公网搭建 GitLab ,常见的攻击面主要有:
- 运行宿主机系统部分
- 运行宿主机网络部分
- 应用 Web 程序漏洞
- 应用 SSH 漏洞
前两点可以通过 SLB + VPC 进行网络隔离,来降低被攻击风险。后两点除了保证最快跟进系统安全补丁,升级应用版本外,其实还有更好的解决方案,毕竟存放着数据的程序,每次升级都有未知的风险:
解决 Web 漏洞可以通过加一层 Basic Auth 来解决。
解决 SSH 攻击风险,可以通过加一个简单的日志监控程序来解决:参考之前文章中 监控 GitLab SSH 端口 小节。
但是加一层 Basic Auth 其实会对 GitLab 使用造成一些麻烦。
为 GitLab 添加请求验证
GitLab 程序本身并不支持 Basic Auth,这里需要使用一个 Web 前端软件来完成这部分的工作,比如:Nginx、Traefik。
我这里选择使用 Traefik,因为配置更简单,具体配置可以参考之前文章的“ 添加网络请求验证 ”小节。
在配置声明里添加一句话就够了,比如这样:
- "traefik.gitlab.frontend.auth.basic=soulteary:$apr1$E86fARwM$tXmggGAtCEDKqsBCSvDA3/
这样处理之后,所有的 HTTP 请求就都会被验证是否是合法访问啦。而其他的端口和协议则不受影响,比如开在22端口的 SSH 服务等。
Basic Auth 到底是什么
当访问页面的时候,会展示类似下面的对话框,要求用户登陆,否则会提示 401 Unauthorized。
当爬虫/安全检测工具请求页面的时候,如果没有提交用户名和密码,获得的结果也是一样。
curl -I https://gitlab.domain/
HTTP/2 401
content-type: text/plain
vary: Accept-Encoding
www-authenticate: Basic realm="traefik"
content-length: 17
date: Sat, 03 Aug 2019 18:22:17 GMT
如果你输入了正确的用户名以及用户密码,你会发现在你的请求参数中会出现一个额外的请求参数 authorization。
参数数值一般由两部分构成,第一部分表示加密方式(加密协议名称),第二部分则是你的身份信息(更多信息详见 RFC 7617)。
现代浏览器一般会很智能的在你第一次正确输入之后,将身份信息记录下来,携带在后续的每一次请求中,如果是使用程序或者工具的话,则需要手动将 authorization 信息加入到每一个 HTTP 的请求头中。
然而 Basic Auth 挡住了外来者随意访问的同时,还挡住了 GitLab CI Runner。
这是怎么回事呢,我们继续往下看。
解救被拦住的 CI Runner
在解释为什么 CI Runner 会被 Basic Auth 拦住时,我们需要先了解另外一个协议规范 RFC1738 中对于 HTTP 协议的定义:
//<user>:<password>@<host>:<port>/<url-path>
当我们使用客户端(浏览器、curl等工具)请求这类格式的地址时,部分客户端会将 user:password 部分转换为标准的 HTTP Authorization 请求头。
默认 CI Runner 行为
在不做任何修改时,CI 直接执行会报错,日志输出类似下面:
Running with gitlab-runner 12.0.2 (d0b76032)
on ci-runner sGNUJnq6
Using Shell executor...
Running on ci-runner...
Fetching changes with git depth set to 50...
Initialized empty Git repository in /data/runner/builds/sGNUJnq6/0/config/project/.git/
Created fresh repository.
remote: 401 Unauthorized
fatal: Authentication failed for 'https://gitlab-ci-token:[MASKED]@gitlab.domain/repo.git/'
ERROR: Job failed: exit status 1
结合文章前面的内容,我们知道这里是缺少了 Authorization 请求头,那么我们尝试给请求补上这个请求头,在 CI 和 GitLab 中间搭建一台 Proxy ,让 CI 请求 GitLab 数据的时候,自动完成“认证”。
请求自动添加验证信息
如果使用 Nginx 搭建一个支持 GET/POST 请求的代理,核心配置如下:
location / {
forward-http-post-requests-via-rewrite
proxy_pass https://192.168.0.123;
proxy_redirect https://192.168.0.123/ /;
proxy_read_timeout 10s;
proxy_set_header Host 'gitlab.domain';
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Authorization 'Basic YmFhaABCD';
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto 'https';
proxy_http_version 1.1;
}
再次使用 curl 对 GitLab 进行请求,发现没有出现之前的 401 非验证提示:
curl -I https://gitlab.domain/
HTTP/2 302
cache-control: no-cache
content-type: text/html; charset=utf-8
date: Sat, 03 Aug 2019 18:45:39 GMT
location: https://gitlab.domain/users/sign_in
referrer-policy: strict-origin-when-cross-origin
referrer-policy: strict-origin-when-cross-origin
server: nginx
strict-transport-security: max-age=315360000
vary: Accept-Encoding
x-content-type-options: nosniff
x-download-options: noopen
x-frame-options: DENY
x-permitted-cross-domain-policies: none
x-request-id: Z8uahE5gN39
x-runtime: 0.012164
x-ua-compatible: IE=edge
x-xss-protection: 1; mode=block
打开项目配置,会发现 Runner 已经上线。
###CI 构建依旧是失败的
继续在 GitLab Runner 运行 CI 流水线,会看到还是报错无法通过构建。
Running with gitlab-runner 12.2.0~beta.1803.g41d5c6ad (41d5c6ad)
on ci-runner S8sY8o6A
Using Shell executor...
Running on ci-runner...
Fetching changes with git depth set to 50...
Reinitialized existing Git repository in /data/runner/builds/S8sY8o6A/0/project/.git/
> GitLab: The project you were looking for could not be found.
fatal: Could not read from remote repository.
Please make sure you have the correct access rights
and the repository exists.
ERROR: Job failed: exit status 1
还记得前面提到过的 Authorization 请求头和 HTTP RFC规范吗?GitLab Runner 在处理 CI 任务的时候,使用的是https://gitlab-ci-token:[MASKED]@gitlab.domain/repo.git/ 这样的 HTTP 协议,请求中的用户名和密码和 Nginx ProxyPass 中的字段“八字不合”。
那么不使用 HTTP 协议,使用 SSH 协议或许能解决问题。
尝试使用 SSH 协议
虽然官方没有直接提供这个功能,但是从官方文档中看到有一个 clone_url,查找代码发现实现很简单,只需要稍微改造就能够满足我们的需求:
// GetRemoteURL checks if the default clone URL is overwritten by the runner
// configuration option: 'CloneURL'. If it is, we use that to create the clone
// URL.
func (b *Build) GetRemoteURL() string {
cloneURL := strings.TrimRight(b.Runner.CloneURL, "/")
if !strings.HasPrefix(cloneURL, "http") {
return b.GitInfo.RepoURL
}
variables := b.GetAllVariables()
ciJobToken := variables.Get("CI_JOB_TOKEN")
ciProjectPath := variables.Get("CI_PROJECT_PATH")
splits := strings.SplitAfterN(cloneURL, "://", 2)
return fmt.Sprintf("%sgitlab-ci-token:%s@%s/%s.git", splits[0], ciJobToken, splits[1], ciProjectPath)
}
改动后的代码如下:
// GetRemoteURL checks if the default clone URL is overwritten by the runner
// configuration option: 'CloneURL'. If it is, we use that to create the clone
// URL.
func (b *Build) GetRemoteURL() string {
cloneURL := strings.TrimRight(b.Runner.CloneURL, "/")
if !strings.HasPrefix(cloneURL, "http") && !strings.HasPrefix(cloneURL, "ssh") {
return b.GitInfo.RepoURL
}
variables := b.GetAllVariables()
ciJobToken := variables.Get("CI_JOB_TOKEN")
ciProjectPath := variables.Get("CI_PROJECT_PATH")
splits := strings.SplitAfterN(cloneURL, "://", 2)
if strings.HasPrefix(cloneURL, "ssh") {
ciProjectPath = strings.TrimLeft(ciProjectPath, "/")
return fmt.Sprintf("git@%s:/%s.git", splits[1], ciProjectPath)
}
return fmt.Sprintf("%sgitlab-ci-token:%s@%s/%s.git", splits[0], ciJobToken, splits[1], ciProjectPath)
}
###执行 CI 成功
再次执行 CI 任务,会发现已经能够顺利的进行啦。
Running on ci-runner...
Fetching changes with git depth set to 50...
Reinitialized existing Git repository in /data/runner/builds/S8sY8o6A/0/project.git/
Checking out 860587f6 as master...
Skipping Git submodules setup
Warning: Permanently added '192.168.x.x' (ECDSA) to the list of known hosts.
From gitlab.domain:project
3957006..860587f master -> origin/master
Updating 3957006..860587f
Fast-forward
.gitlab-ci.yml | 6 +++---
README.md | 1 +
2 files changed, 4 insertions(+), 3 deletions(-)
Stopping service ...
Stopping service ... done
Removing service ...
Removing service ... done
Network service is external, skipping
Creating service ...
Creating service ... done
Job succeeded