如何让Python在Docker容器中找到CA证书文件解决SSL验证失败问题
问题背景
我部署了两个Docker容器app_1和app_2,app_1通过FastAPI向app_2发起REST请求,连接需通过TLS协议。我为每个应用创建了自签名证书,并使用自建CA进行签名,通过以下命令启动:
docker-compose --env-file my.env up --build
错误信息
发起请求时出现以下错误:
File "/usr/local/lib/python3.9/ssl.py", line 1310, in do_handshake
app_1 | self._sslobj.do_handshake() app_1 |
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED]
certificate verify failed: self signed certificate in certificate
chain (_ssl.c:1129)
我知道需要告知requests库CA证书包的位置,但不清楚具体操作方法。相关配置与代码如下:
环境文件内容
REQUESTS_CA_BUNDLE=/app/certs/chain.pem
chain.pem包含CA.crt、CA.key、app_2.crt和app_2.key文件。
app_1代码
from fastapi import FastAPI import requests import time import sched app = FastAPI() ca_cert_file = '/app/certs/ca.crt' ca_key_file = '/app/certs/ca.key' @app.get('/') async def read_root(): return {"message": "Hello from app_1"} def make_request(): url = "https://app_2" response = requests.get(url, timeout=10, cert=('/app/certs/ca.crt', '/app/certs/ca.key')) if response.status_code == 200: print(response.json()) else: print(f"Request failed status code: {response.status_code}") scheduler = sched.scheduler(time.time, time.sleep) def make_rest_call_periodically(): make_request() scheduler.enter(10, 10, make_rest_call_periodically) scheduler.enter(1, 1, make_rest_call_periodically) scheduler.run() if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=443, ssl_ca_certs='/app/certs/ca.crt', ssl_keyfile="/app/certs/app_1.key", ssl_certfile="/app/certs/app_1.crt")
app_2代码
from fastapi import FastAPI import ssl import uvicorn import requests import time import urllib3 app = FastAPI() @app.get('/') async def read_root(): return {"message": "Hello from app_2"} if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=443, ssl_ca_certs='/app/certs/ca.crt', ssl_keyfile="/app/certs/app_2.key", ssl_certfile="/app/certs/app_2.crt")
解决方案
问题根源
你在requests.get中错误使用了cert参数:这个参数是用来指定客户端证书(app_1向app_2证明自身身份的凭证),而非信任的CA证书。当前报错是因为requests默认不信任自建CA签名的app_2证书链,因此需要明确告知requests信任你的自建CA。
修复步骤
修正requests请求参数
将make_request函数中的cert参数替换为verify,指定自建CA的证书路径:def make_request(): url = "https://app_2" # 使用verify指定信任的CA证书,验证app_2的服务器证书 response = requests.get(url, timeout=10, verify='/app/certs/ca.crt') if response.status_code == 200: print(response.json()) else: print(f"Request failed status code: {response.status_code}")如果你想通过环境变量
REQUESTS_CA_BUNDLE生效,只需确保chain.pem仅包含CA公钥证书(CA.crt),然后去掉verify参数,requests会自动读取该环境变量。清理并修复chain.pem
当前chain.pem混入了密钥文件(CA.key、app_2.key),这既不安全也不符合证书包规范。重新生成仅包含CA公钥的证书包:cat /app/certs/CA.crt > /app/certs/chain.pem密钥文件需单独存储,禁止放入证书包中。
调整app_2的客户端验证配置(可选)
app_2启动时的ssl_ca_certs参数用于开启客户端证书验证(要求app_1提供身份证书):- 若不需要客户端验证,直接移除该参数;
- 若需要客户端验证,app_1的请求需用
cert参数指定自身的客户端证书(/app/certs/app_1.crt和/app/certs/app_1.key),同时保留verify参数指定CA证书,示例:response = requests.get(url, timeout=10, verify='/app/certs/ca.crt', cert=('/app/certs/app_1.crt', '/app/certs/app_1.key'))
验证修复效果
重启容器后,app_1的周期性请求应能正常建立TLS连接并获取app_2的响应。
内容的提问来源于stack exchange,提问作者Jim Archer

