文章目录
环境: GitLab CE 16.2.9 · Docker Compose · Nginx TCP 透传
背景
在执行 gitlab-ctl reconfigure 时,GitLab 内置的 Let's Encrypt 自动申请证书功能报错,导致 reconfigure 失败,GitLab 无法正常启动 HTTPS。
错误现象
There was an error running gitlab-ctl reconfigure:
letsencrypt_certificate[gitlab.example.com] (letsencrypt::http_authorization line 6) had an error:
KeyError: acme_certificate[staging] (letsencrypt::http_authorization line 43) had an error:
KeyError: key not found: "token"
完整堆栈指向:
/opt/gitlab/embedded/lib/ruby/gems/3.0.0/gems/acme-client-2.0.14/
lib/acme/client/resources/authorization.rb:59 `initialize_challenge'
→ KeyError: key not found: "token"
架构说明
外网用户
│
├─ :80/:443 → Nginx (stream 模块 TCP 透传)
│ └─→ 10.41.0.7:4080 / 4443
│ └─→ GitLab Docker 容器
└─ :22 → Nginx (stream 模块 TCP 透传)
└─→ 10.41.0.7:4022
Nginx 使用四层 TCP 透传(stream 模块),80/443/22 端口直接转发到内网 GitLab 容器,不做任何 HTTP 层处理。
排查过程
Step 1:确认 HTTP-01 Challenge 链路是否畅通
Let's Encrypt HTTP-01 验证需要从公网访问:
http://<your-domain>/.well-known/acme-challenge/<token>
在容器内手动放置一个测试文件:
bash
docker compose exec gitlab bash -c \
"mkdir -p /var/opt/gitlab/nginx/www/.well-known/acme-challenge && \
echo 'challenge-test-ok' > /var/opt/gitlab/nginx/www/.well-known/acme-challenge/testtoken"
从外网验证:
bash
curl http://gitlab.example.com/.well-known/acme-challenge/testtoken
# 返回:challenge-test-ok ✅
链路完全正常,排除网络/防火墙问题。
Step 2:阅读堆栈,定位根本原因
查看出错的 gem 源码:
bash
docker compose exec gitlab sed -n '50,65p' \
/opt/gitlab/embedded/lib/ruby/gems/3.0.0/gems/acme-client-2.0.14/\
lib/acme/client/resources/authorization.rb
关键代码:
ruby
def initialize_challenge(attributes)
arguments = {
type: attributes.fetch('type'),
status: attributes.fetch('status'),
url: attributes.fetch('url'),
token: attributes.fetch('token'), # ← 问题所在
error: attributes['error']
}
Acme::Client::Resources::Challenges.new(@client, **arguments)
end
根本原因:
Let's Encrypt 在返回 authorization 时,会包含多种 challenge 类型(http-01、dns-01 等)。其中 dns-01 类型的 challenge 对象不含 token 字段 ,但 acme-client 2.0.14 使用了 Hash#fetch(无默认值),直接抛出 KeyError。
这是 acme-client gem 的一个已知 bug,在更高版本中已修复,但 GitLab 16.2.9 打包的版本过旧,未包含该修复。
Step 3:尝试配置规避(无效)
尝试在 gitlab.rb 中显式设置:
ruby
letsencrypt['staging'] = false
letsencrypt['contact_emails'] = ['admin@example.com']
结论: 无效。问题在 gem 底层代码,配置层无法绕过。
修复方案
直接 patch 容器内的 gem 源码,为 token 的 fetch 调用添加默认值 nil:
bash
# 打补丁
docker compose exec gitlab sed -i \
"s/token: attributes.fetch('token'),/token: attributes.fetch('token', nil),/" \
/opt/gitlab/embedded/lib/ruby/gems/3.0.0/gems/acme-client-2.0.14/\
lib/acme/client/resources/authorization.rb
# 验证修改生效
docker compose exec gitlab grep 'token' \
/opt/gitlab/embedded/lib/ruby/gems/3.0.0/gems/acme-client-2.0.14/\
lib/acme/client/resources/authorization.rb
# 期望输出:token: attributes.fetch('token', nil),
# 重新执行 reconfigure
docker compose exec gitlab gitlab-ctl reconfigure
执行后证书申请成功,问题解决。
原理说明
fetch('token', nil) 让没有 token 字段的 challenge 类型(如 dns-01)返回 nil 而不是抛出异常。后续 acme cookbook 在筛选 http-01 challenge 时会忽略这些类型,整个流程正常推进。
注意事项
| 事项 | 说明 |
|---|---|
| Patch 持久化 | 此 patch 直接修改容器内文件,重新拉取镜像后会丢失。但证书一旦申请成功,后续自动续签(renew)不会再触发这个 bug,因为证书文件已经存在于 volume 中 |
| 升级建议 | GitLab 16.2.9 已停止维护,存在多个高危安全漏洞。建议按官方升级路径逐步升级至受支持版本 |
| 受影响版本 | 使用 acme-client 2.0.14 的 GitLab 版本均可能受此影响 |