内网GitLab自签名证书HTTPS配置实战:从SAN字段到客户端信任 1. 为什么内网环境下的 GitLab 必须认真对待 HTTPS 这件事很多团队在内部网络部署 GitLab 时第一反应是“内网嘛直接 HTTP 就行了省事”。我刚开始也这么想直到有一次在跨机房同步代码时发现某些 CI Runner 在拉取仓库时频繁报出“仓库地址不被信任”的警告部分命令行工具甚至直接拒绝连接。排查了一圈才意识到问题出在 GitLab 只开了 HTTP而新版的一些客户端组件默认要求安全传输层。从那时起我开始认真研究怎么用自签名证书给内网 GitLab 加上 HTTPS。自签名证书简单说就是自己给自己签发的数字证书不需要向公共证书颁发机构申请也不需要域名所有权验证。它的核心价值在于在内网或测试环境中用极低的成本实现传输层加密和身份标识。你不需要花钱买域名、不需要公网 IP、不需要等待审核一条命令就能生成一套可用的证书。当然代价是浏览器和命令行工具默认不信任它需要手动把根证书导入到各个客户端的信任库里。这篇文章适合谁看如果你正在维护一个内网 GitLab 实例团队规模在几人到几十人之间没有专职的安全运维又希望代码传输过程不被明文暴露那这套方案就是为你准备的。我会从证书生成的底层逻辑讲起一步步带你完成 GitLab 的 HTTPS 配置重点放在那些官方文档不会告诉你的坑上——比如证书的 SAN 字段到底该怎么写、Git 命令行报错时怎么快速定位、Docker Runner 怎么信任自签名证书。整个过程不需要你精通密码学但需要你耐心跟着操作因为每一步都有它的道理。注意自签名证书适用于内部可信网络环境。如果你的 GitLab 需要对外网提供服务建议使用受信任的公共证书否则外部用户访问时会遇到大量信任警告体验很差。2. 自签名证书的生成逻辑与 SAN 字段的隐藏陷阱2.1 证书到底在验证什么要理解自签名证书先得搞清楚证书在 HTTPS 握手时扮演什么角色。当你用浏览器或 Git 客户端访问https://gitlab.internal时客户端会向服务器索要证书然后做三件事第一检查证书是否由自己信任的根证书签发第二检查证书上的域名是否和你访问的地址匹配第三检查证书是否在有效期内。这三项任何一项不通过就会弹出警告或直接拒绝连接。自签名证书的问题出在第一项——它是由自己签发的不在系统默认的信任列表里。所以我们需要做两件事生成一套自签名的根证书和服务器证书然后把根证书手动导入到每个客户端的信任库中。这样客户端就会认为“这个根证书我认识它签发的服务器证书我也认”。2.2 SAN 字段90% 的人第一次都会写错早期生成证书时大家习惯用 Common Name 字段来写域名。但现代浏览器和 Git 客户端已经不再依赖 Common Name 了而是检查Subject Alternative Name字段。如果你只写了 Common Name 而没写 SAN访问时就会报NET::ERR_CERT_COMMON_NAME_INVALID错误。这个坑我踩过不止一次明明证书看起来没问题浏览器就是不认。SAN 字段可以包含多种类型的条目DNS 名称、IP 地址、邮箱等。对于内网 GitLab你至少需要把以下内容写进去GitLab 服务器的域名如gitlab.internalGitLab 服务器的 IP 地址如192.168.1.100如果用了不同的访问入口把常用的别名也加上下面是我常用的一套生成命令基于 OpenSSL在 Linux 和 macOS 上都能跑。先生成一个配置文件把 SAN 信息写清楚# 创建 openssl 配置文件 cat san.cnf EOF [req] default_bits 2048 prompt no default_md sha256 distinguished_name dn req_extensions v3_req [dn] C CN ST Internal L Internal O Internal IT OU Dev CN gitlab.internal [v3_req] basicConstraints CA:FALSE keyUsage digitalSignature, keyEncipherment extendedKeyUsage serverAuth subjectAltName alt_names [alt_names] DNS.1 gitlab.internal DNS.2 gitlab IP.1 192.168.1.100 EOF这个配置文件里[alt_names]段就是 SAN 的核心。DNS.1和DNS.2分别写了完整域名和短名称IP.1写了服务器 IP。这样无论用户是用域名访问还是用 IP 访问证书都能匹配上。2.3 生成根证书和服务器证书的完整流程有了配置文件接下来分两步走先签根证书再用根证书签服务器证书。为什么要分两层因为根证书的私钥应该离线保存只用来签发下级证书。如果直接用服务器证书自签以后换服务器就得重新导入信任很麻烦。用根证书签发的话只要根证书不变换服务器证书时客户端不需要重新配置。# 第一步生成根证书私钥和根证书有效期10年 openssl genrsa -out rootCA.key 4096 openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 3650 \ -subj /CCN/STInternal/LInternal/OInternal IT/OURoot CA/CNInternal Root CA \ -out rootCA.crt # 第二步生成服务器私钥和证书签名请求CSR openssl genrsa -out gitlab.key 2048 openssl req -new -key gitlab.key -out gitlab.csr -config san.cnf # 第三步用根证书签发服务器证书有效期2年 openssl x509 -req -in gitlab.csr -CA rootCA.crt -CAkey rootCA.key \ -CAcreateserial -out gitlab.crt -days 730 -sha256 \ -extensions v3_req -extfile san.cnf执行完这三步你会得到五个文件rootCA.key、rootCA.crt、gitlab.key、gitlab.csr、gitlab.crt。其中rootCA.key是根证书私钥一定要妥善保管不要放到 GitLab 服务器上。gitlab.key和gitlab.crt是服务器要用的证书对rootCA.crt是要分发给客户端导入的根证书。提示根证书私钥一旦泄露任何人都可以签发被你的客户端信任的证书。建议把rootCA.key放在离线存储或加密的 U 盘中只在需要签发新证书时才拿出来用。2.4 验证证书是否包含正确的 SAN生成完证书后别急着往 GitLab 上装先用命令检查一下 SAN 字段是否写对了openssl x509 -in gitlab.crt -text -noout | grep -A1 Subject Alternative Name如果输出里能看到你配置的 DNS 和 IP说明证书没问题。如果这一项是空的那说明签发时-extfile san.cnf没生效需要重新签发。这个检查步骤花不了几秒钟但能帮你省掉后面大量的排查时间。3. 把证书装进 GitLab配置文件改哪里、怎么改3.1 找到 GitLab 的 HTTPS 配置入口GitLab 的 HTTPS 配置集中在/etc/gitlab/gitlab.rb这个文件里。这个文件是 GitLab Omnibus 安装包的主配置文件所有对外服务的参数都在这里调整。你需要关注两个部分一是 GitLab 自身的 Nginx 配置二是 GitLab 各组件的内部通信地址。先备份原始配置文件这是个好习惯sudo cp /etc/gitlab/gitlab.rb /etc/gitlab/gitlab.rb.bak然后把证书文件放到一个 GitLab 能读取的位置。我习惯放在/etc/gitlab/ssl/目录下这个目录默认权限是 700只有 root 能访问比较安全sudo mkdir -p /etc/gitlab/ssl sudo cp gitlab.crt gitlab.key /etc/gitlab/ssl/ sudo chmod 600 /etc/gitlab/ssl/gitlab.key sudo chmod 644 /etc/gitlab/ssl/gitlab.crt注意私钥文件的权限必须是 600否则 GitLab 启动时会报权限错误。这个细节在官方文档里写得很隐蔽我第一次配置时就是因为私钥权限太开放导致 Nginx 起不来。3.2 修改 external_url 和 Nginx 配置打开/etc/gitlab/gitlab.rb找到external_url这一行。默认可能是http://gitlab.internal你需要把它改成https://gitlab.internalexternal_url https://gitlab.internal然后在文件末尾或 Nginx 配置段添加以下内容nginx[redirect_http_to_https] true nginx[ssl_certificate] /etc/gitlab/ssl/gitlab.crt nginx[ssl_certificate_key] /etc/gitlab/ssl/gitlab.key nginx[ssl_protocols] TLSv1.2 TLSv1.3 nginx[ssl_ciphers] ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384 nginx[ssl_prefer_server_ciphers] true这里有几个参数值得解释一下。redirect_http_to_https设为 true 后用户用 HTTP 访问时会自动跳转到 HTTPS避免有人误用明文连接。ssl_protocols我建议只保留 TLSv1.2 和 TLSv1.3这两个版本目前安全性足够兼容性也没问题。ssl_ciphers指定了加密套件的优先级把 ECDHE 开头的放在前面因为这类套件支持前向保密即使私钥泄露历史通信记录也无法被解密。3.3 重新配置并验证服务状态改完配置文件后执行重新配置命令sudo gitlab-ctl reconfigure这个过程会重新生成 Nginx 配置并重启相关服务通常需要一两分钟。完成后检查一下服务状态sudo gitlab-ctl status如果看到nginx、gitlab-workhorse、gitlab-rails等核心服务都是run状态说明启动正常。然后用 curl 在服务器本机测试一下 HTTPS 是否生效curl -k -v https://gitlab.internal-k参数表示跳过证书验证因为我们还没把根证书导入本机的信任库。如果能看到 GitLab 的登录页面 HTML说明 HTTPS 已经配置成功了。接下来要解决的就是客户端信任问题。注意gitlab-ctl reconfigure会覆盖手动修改的 Nginx 配置文件所以所有改动都必须写在gitlab.rb里不要直接去改/var/opt/gitlab/nginx/conf/下的文件否则下次 reconfigure 就丢了。4. 让各个客户端信任自签名证书的实操方法4.1 操作系统层面的根证书导入自签名证书配置好后第一个拦路虎就是浏览器和命令行工具的信任警告。解决办法是把rootCA.crt导入到操作系统的信任库中。不同系统的操作方式不同我整理了一个对照表操作系统导入方式生效范围Ubuntu/Debian复制到/usr/local/share/ca-certificates/后执行update-ca-certificates系统级curl、wget、Git 均生效CentOS/RHEL复制到/etc/pki/ca-trust/source/anchors/后执行update-ca-trust系统级macOS双击 crt 文件导入“钥匙串访问”设为“始终信任”系统级Safari 和 Chrome 均生效Windows双击 crt 文件安装到“受信任的根证书颁发机构”系统级Edge 和 Chrome 均生效以 Ubuntu 为例具体操作如下sudo cp rootCA.crt /usr/local/share/ca-certificates/gitlab-internal.crt sudo update-ca-certificates执行后会看到类似1 added, 0 removed的输出说明证书已经加入信任库。这时候再用 curl 访问 GitLab就不需要-k参数了curl -v https://gitlab.internal如果不再报证书错误说明系统级信任已经生效。Git 命令行工具在大多数情况下会使用系统的证书信任库所以这一步做完后git clone https://gitlab.internal/group/project.git应该也能正常工作了。4.2 Git 命令行单独指定证书路径有些场景下你可能没有系统管理员权限无法修改系统信任库。这时候可以通过 Git 的配置来单独指定根证书git config --global http.sslCAInfo /path/to/rootCA.crt这个配置告诉 Git 在发起 HTTPS 请求时使用指定的根证书文件来验证服务器证书。如果你只想对某个特定域名生效可以用更细粒度的配置git config --global http.https://gitlab.internal/.sslCAInfo /path/to/rootCA.crt这样只有访问gitlab.internal时才会使用这个根证书其他仓库不受影响。这个技巧在多环境切换时特别有用比如你同时维护内网和外部两个 GitLab 实例就可以分别配置不同的证书。4.3 Docker Runner 的证书信任配置如果你的 CI Runner 是用 Docker 跑的那证书信任问题会更复杂一些。Docker 容器有自己的文件系统宿主机的信任库不会自动传递进去。你需要把根证书挂载到容器里或者在构建 Runner 镜像时就加进去。对于已经运行的 Docker Runner可以在config.toml中指定证书路径[[runners]] name docker-runner url https://gitlab.internal tls-ca-file /etc/gitlab-runner/certs/rootCA.crt [runners.docker] volumes [/cache, /etc/gitlab-runner/certs:/etc/gitlab-runner/certs:ro]然后在宿主机上把rootCA.crt放到/etc/gitlab-runner/certs/目录下重启 Runner 服务。这样 Runner 在拉取代码时就会使用这个根证书来验证 GitLab 的服务器证书。如果 Runner 是在 Kubernetes 集群里跑的处理方式又不一样。你需要把根证书做成 ConfigMap挂载到 Runner Pod 的/etc/ssl/certs/目录下然后执行update-ca-certificates。这个操作稍微复杂一些但原理是一样的让容器内部的信任库包含你的根证书。5. 那些让我熬夜排查的典型报错与解决路径5.1 SSL certificate problem: unable to get local issuer certificate这个报错是我见过频率最高的。完整信息通常是fatal: unable to access https://gitlab.internal/group/project.git/: SSL certificate problem: unable to get local issuer certificate字面意思是“无法获取本地颁发者证书”翻译成人话就是Git 拿到了服务器证书但找不到对应的根证书来验证它。原因无非两个要么根证书没导入要么导入的位置不对。排查路径是这样的先用openssl s_client命令看看服务器实际返回的证书链openssl s_client -connect gitlab.internal:443 -showcerts如果输出中Certificate chain只有一张证书说明服务器没有发送中间证书。对于自签名证书来说这通常不是问题因为根证书就是签发者。但如果你的证书链有多级就需要确保服务器发送了完整的链。然后检查 Git 实际使用的证书路径git config --list | grep ssl如果http.sslCAInfo指向了一个不存在的文件或者指向了错误的证书就会报这个错。解决办法就是重新设置正确的路径或者把根证书导入系统信任库。5.2 NET::ERR_CERT_AUTHORITY_INVALID 在浏览器中的处理浏览器报这个错说明它不信任签发服务器证书的根证书。即使你把根证书导入了系统信任库Chrome 有时仍然会用自己的信任库需要额外配置。在 Linux 上Chrome 使用的是 NSS 数据库需要单独导入certutil -d sql:$HOME/.pki/nssdb -A -t C,, -n Internal Root CA -i rootCA.crt如果系统里没有certutil命令需要先安装libnss3-tools包。导入后重启 Chrome再访问 GitLab 就不会报错了。Firefox 则完全使用自己的证书管理器需要在“设置 - 隐私与安全 - 证书 - 查看证书 - 颁发机构”中手动导入。这个操作在每个用户的配置文件中都要做一遍比较繁琐但没办法Firefox 就是这么独立。5.3 Runner 注册时的 x509 证书错误注册 GitLab Runner 时如果证书没配好会看到这样的错误ERROR: Registering runner... failed x509: certificate signed by unknown authority这个错误说明 Runner 在向 GitLab 发起注册请求时无法验证服务器证书。解决办法取决于 Runner 的安装方式。如果是二进制安装直接在config.toml里加tls-ca-file配置如果是 Docker 安装需要把根证书挂载进去并更新容器内的信任库。我遇到过一次比较特殊的情况Runner 容器里的/etc/ssl/certs/ca-certificates.crt文件是只读的无法直接追加证书。后来发现可以通过环境变量SSL_CERT_FILE指定一个额外的证书文件路径Runner 会同时加载系统信任库和这个文件。这个技巧在容器环境中特别实用。5.4 证书过期后的续签流程自签名证书也有有效期到期后所有客户端都会报证书过期错误。续签的流程和初次签发类似但有几个注意事项。首先续签时最好保持 SAN 字段不变否则客户端需要重新验证域名匹配。其次如果根证书还没过期可以继续用它来签发新的服务器证书这样客户端不需要重新导入根证书。续签命令如下# 生成新的服务器私钥和 CSR openssl genrsa -out gitlab-new.key 2048 openssl req -new -key gitlab-new.key -out gitlab-new.csr -config san.cnf # 用现有的根证书签发新证书 openssl x509 -req -in gitlab-new.csr -CA rootCA.crt -CAkey rootCA.key \ -CAcreateserial -out gitlab-new.crt -days 730 -sha256 \ -extensions v3_req -extfile san.cnf # 替换 GitLab 证书文件并重新配置 sudo cp gitlab-new.crt /etc/gitlab/ssl/gitlab.crt sudo cp gitlab-new.key /etc/gitlab/ssl/gitlab.key sudo gitlab-ctl reconfigure替换完成后用openssl x509 -in /etc/gitlab/ssl/gitlab.crt -noout -dates确认新证书的有效期然后通知团队成员重启浏览器或清除 SSL 缓存。有些客户端会缓存证书验证结果不重启可能仍然报旧证书的错误。6. 关于这套方案的一些个人实践体会自签名证书这套方案我在多个内网环境中反复用过整体来说是稳定可靠的。它最大的优势是成本低、可控性强不需要依赖外部服务适合内部团队快速搭建安全传输通道。但有几个点是我踩过坑之后才真正重视起来的。第一根证书私钥的保管比什么都重要。我见过有团队把根证书私钥和服务器证书放在同一台机器上这等于把保险箱钥匙和保险箱放在一起。一旦服务器被入侵攻击者可以用根证书私钥签发任意证书伪装成任何内部服务。正确的做法是把根证书私钥离线保存只在签发新证书时才短暂使用。第二证书的 SAN 字段要一次性规划好。如果后期需要增加新的访问域名或 IP就得重新签发证书并重新分发根证书工作量不小。所以在生成证书之前把所有可能的访问方式都列出来包括短域名、完整域名、IP 地址甚至未来可能用的新域名。多写几个 SAN 条目不会有任何副作用但少写一个就可能让你在半夜被叫起来处理故障。第三客户端信任配置要有文档记录。新同事入职时第一件事就是帮他配置证书信任否则他连代码都拉不下来。我习惯把根证书文件、导入命令和常见问题整理成一个内部文档放在团队 Wiki 上。这样即使我不在其他人也能照着操作。第四定期检查证书有效期。自签名证书不像公共证书那样有自动续期机制过期了就是过期了。我建议在日历上设置一个提醒在证书到期前一个月开始准备续签。如果团队有监控系统也可以加一个证书有效期检查的脚本提前预警。这套方案不是银弹它解决的是内网环境下的传输加密和身份标识问题。如果你的团队规模扩大或者 GitLab 需要对外提供服务还是建议尽早切换到受信任的公共证书。但在内部开发、测试、预发布这些场景下自签名证书的性价比确实很高值得花时间把它配置好。