在本地 Docker 环境中信任自签名 CA 证书
![Cover Image 在本地 Docker 环境中信任自签名 CA 证书](https://george.betterde.com/covers/20240615-tls-ca-certificate.jpg)
前言⌗
我经常在内网部署一些服务,通过 Traefik 反向代理,并且使用自签名的 TLS 证书,如果只是本机浏览器访问是没有任何问题的,一切都很丝滑。但是如果部署在容器内的服务之间相互访问,则蛮烦的多,接下来介绍这其中可能遇到的坑!
域名解析⌗
我本地部署的服务主要是我本机访问,很少涉及内网访问,而我使用 Dnsmasq 作为 DNS 服务,将 .test
的 TLD 都解析为 127.0.0.1。
这在本机浏览器上访问时是没有什么问题的,但是如果容期间访问就会存在问题,例如 Outline 要访问 MinIO 服务,而 MinIO 是通过 Traefik 反向代理,域名是 minio.test,其流量如下所示:
`Outline` ---> `Traefik` ---> `MinIO`
这就导致一个问题 Outline 在请求 minio.test 这个域名是,从 Dnsmasq 哪里获取到的解析结果是 127.0.0.1,在容器内就等于是请求自己,无法将流量发送到 Traefik 容器。
为了解决这个问题可行的方案有两种:
- 将 Dnasmasq 的解析记录设置为 Traefik 的 IP;
- 在 Docker Compose 的配置文件中设置
extra_hosts
,等同于硬编码到容器的 /etc/hosts 文件中。
无论那种方式都需要为 Traefik 的容器设置一个固定 IP!
例如我本地使用 Valet 作为 PHP 的开发环境,在 ~/.config/valet/dnsmasq.d
目录中的配置如下:
tree
.
├── tld-infra.conf # 基础设施服务解析
├── tld-svc.dev.conf # Valet 相关项目解析
└── tld-test.conf # 其他测试服务解析
1 directory, 3 files
docker network inspect traefik
[
{
"Name": "traefik",
"Id": "1bab78892e73451c7ed7f73a9ac4415da15d690885d293edb6aeb287e35e156b",
"Created": "2023-10-22T18:07:03.173509102+08:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "10.8.10.0/24",
"Gateway": "10.8.10.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"b34f15b5a78146fbe5265e4653e843cb7201b8642747aa03dd4a11c9fc8122be": {
"Name": "traefik",
"EndpointID": "466ea1148e4ad1fe84757ee3c489e44b5c75b037d5988f43daaeb80b99f82012",
"MacAddress": "02:42:0a:08:0a:fe",
"IPv4Address": "10.8.10.254/24",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {}
}
]
例如我这里 *.test 的所有流量都走 traefik,Dnsmasq 的配置如下:
address=/.test/10.8.10.254
listen-address=127.0.0.1
这样无论是容器内还是宿主机都是直接请求的 Traefik 这个容器,就不存在容器内和宿主机因解析地址是 loopback 而产生未知问题了!
证书⌗
当 Outline 容器要请求 MinIO 的时候,走的是 HTTPS,因为使用了自签名证书,所以会导致建立 TLS 握手的时候失败,就是 X.509 证书不被信任!解决的方法也很简单,但如果基于不同的镜像可能存在不同的差异!
例如 Linux 系的都是将 CA 证书放到容器内的 /usr/local/share/ca-certificate
目录中,然后进入容器执行如下命令:
update-ca-certificates
如果容器内没有安装 update-ca-certificates
命令的话,那么可以直接将 CA 证书的内容追加到 /etc/ssl/certs/ca-certificates.crt
文件中:
cat /usr/local/share/ca-certificates/ca.crt >> /etc/ssl/certs/ca-certificates.crt
这种方法比较普适,但是存在的问题也很明显,你要么在创建容器收手动执行,要么重写 Dockerfile,将上述步骤在构建阶段就执行。这样不用每次 Recreate Container 的时候手动去执行上面的操作了!
切记更新完证书以后,需要重启容器(不是 Recreate 哦),否则不生效!
除了上述的方案以为,某些特定的服务也支持通过 ENV 来设置:
- Node.JS 的镜像,可以使用
NODE_EXTRA_CA_CERTS
环境变量来指定 CA 所在位置,这样容器内的 Node.JS 进程发起的 Request 就可以自动加载 CA; - Gitlab Runner 可以通过
CA_CERTIFICATES_PATH
和CI_SERVER_TLS_CA_FILE
环境变量来信任 CA 证书。 - Python 的镜像,可以使用
REQUESTS_CA_BUNDLE
环境变量来指定 CA 所在位置,这样容器内的 Python 进程发起的 Request 就可以自动加载 CA;
总结⌗
在本地开发,要模拟和生产一致的体验,坑还是挺多的,需要你有一定的耐心和 Debug 能力。但是只要坚持下来,你发现没有一根头发是白掉的……
I hope this is helpful, Happy hacking…