场景:接到一句「把某某站点的证书换一下」。手里只有一个域名,其余一无所知。
本文记录从「域名指向哪台机器」到「配置文件到底在哪个目录」的完整定位过程,以及证书更换、校验、回滚的实操细节。文中域名、IP、容器名均已脱敏。
一、起点:只知道一个域名
任务描述只有一句话:coc.example.com 的 HTTPS 证书要换。
第一件事不是登录服务器,而是确认这个域名到底落在哪台机器上:
bash
ping coc.example.com
返回 10.10.13.101。这是个内网地址,说明解析是在内网完成的(内网 DNS 或 hosts),公网查不到。顺手确认一下解析来源,能避免后面误判:
bash
cat /etc/hosts | grep example
cat /etc/resolv.conf
拿到 IP 后通过 SSH 登录,先用一条命令确认系统环境:
bash
cat /etc/os-release
结果是 CentOS Linux 7 (Core) 。这里插一句题外话:CentOS 7 已在 2024 年 6 月 EOL,官方源早已下线,yum install 会直接 404,需要换到 vault.centos.org 或国内镜像的 vault 源。如果这台机器还对外提供服务,建议排进迁移计划(Rocky Linux 9 / AlmaLinux 9)。不过对本次任务而言,它不影响操作,继续。
二、第一个弯路:按传统思路找 nginx 配置
按照老习惯,直接去默认路径翻:
bash
grep -rIl "coc.example.com" /etc/nginx /etc/httpd /usr/local/nginx 2>/dev/null
一无所获。
这不是配置写得隐蔽,而是思路错了。再补两条命令看进程和端口:
bash
ss -lntp | grep -E ':80|:443'
ps -ef | grep -Ei 'nginx|httpd|java' | grep -v grep
输出很有意思:
LISTEN 0 4096 *:8082 *:* users:(("docker-proxy",pid=104798,fd=4))
LISTEN 0 4096 *:8083 *:* users:(("docker-proxy",pid=97595,fd=4))
root 97633 ... java -Xms512m -Xmx1024m -jar /app.jar --spring.config.location=/conf/application.yml
root 104836 ... nginx: master process nginx -g daemon off;
101 104891 ... nginx: worker process
三个关键信号:
- 端口监听者是
docker-proxy,不是 nginx 本身; - nginx 的启动命令是
nginx -g daemon off;------这是容器里跑前台进程的标准写法; - Java 进程的工作目录是
/app.jar,配置从/conf/application.yml读------典型的容器挂载路径。
结论:所有服务都跑在 Docker 里,宿主机上当然搜不到配置。 这大概是容器化环境排障时最容易踩的第一个坑:/etc/nginx 目录可能压根不存在,或者存在但只是个空壳。
三、顺着容器找到真正的挂载目录
先看全貌:
bash
docker ps --format 'table {{.ID}}\t{{.Image}}\t{{.Names}}\t{{.Ports}}'
CONTAINER ID IMAGE NAMES PORTS
087d3b64c789 httpapi:v1.4 api-service 0.0.0.0:8083->8083/tcp
70e3de68ba9a vue:latest web-front 80/tcp, 0.0.0.0:8082->8082/tcp
ac04623a603f contract:v1 contract 0.0.0.0:5000->5000/tcp
71a8b01c767d redis redis 6379/tcp
对应关系一下清楚了:
web-front:nginx + 前端静态资源,宿主 8082 → 容器 8082;api-service:Spring Boot 后端,8083;contract:另一个业务应用,5000;redis:缓存。
接下来是最关键的一步------看容器挂了哪些宿主机目录:
bash
docker inspect web-front -f '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'
/root/config/nginx -> /etc/nginx
答案出来了。宿主机 /root/config/nginx 整个目录挂到容器的 /etc/nginx,所以真正的配置文件在宿主机上,容器重启也不会丢。
这一步值得单独强调:判断配置是「挂载」还是「打进镜像」,决定了后续所有操作的姿势 。挂载的直接改宿主机文件;打进镜像的,改完容器一重建就白改,必须
docker commit或改 Dockerfile 重建。docker inspect的Mounts字段是最快的判断依据。
四、在容器内确认配置生效情况
找到挂载点之后,再验证一下域名到底写在哪个文件里:
bash
docker exec web-front sh -c 'grep -rl "coc.example.com" /etc/nginx /usr/share/nginx 2>/dev/null'
/etc/nginx/conf.d/default.conf # 配置本体
/usr/share/nginx/html/static/js/*.js # 前端产物里硬编码的接口地址
配置文件锁定为 conf.d/default.conf,宿主机路径是 /root/config/nginx/conf.d/default.conf。
比起 cat,更推荐用 nginx -T------它会把主配置和所有 include 的文件全部展开打印,避免漏看:
bash
docker exec web-front nginx -T | grep -n -B10 -A40 "coc.example.com"
五、读懂这份配置:它比看起来更「朴素」
配置核心部分如下(省略注释):
nginx
server {
listen 8082 ssl;
server_name coc.example.com;
ssl_certificate /etc/nginx/example.com_cert_chain.pem;
ssl_certificate_key /etc/nginx/example.com_key.key;
ssl_session_timeout 5m;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/nginx/dhparams.pem;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
}
几个值得记录的观察点:
1. 只有 8082 ssl,没有 80 端口。
也就是说站点必须用 https://coc.example.com:8082 访问,直接敲域名是打不开的。这是个非标准端口,用户每次都要带端口号,体验一般。要改成 443,不只要改配置里的 listen,还得改容器的端口映射并重建容器------reload 是不够的。
2. TLSv1.1 已被主流浏览器废弃。
等保测评或安全扫描大概率会报这一条,建议至少收紧到 TLSv1.2 TLSv1.3。
3. 全篇没有 proxy_pass。
这说明 nginx 只做静态托管,不代理后端。那么前端怎么调接口?答案在第四步 grep 到的 JS 文件里------接口地址是编译时写死在前端产物里的。这带来两个隐患:换 IP 要重新打包前端;浏览器直连后端端口会涉及跨域。
如果哪天要统一入口,可以加一段反代:
nginx
location /api/ {
proxy_pass http://10.10.13.101:8083/;
proxy_set_header Host $host;
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 $scheme;
}
但注意:前端打包时的 baseURL 必须同步改成 /api,否则改了 nginx 也没用。这种「配置改一半」的情况,排查起来比不改还费劲。
4. 静态资源没有缓存策略。
js/css 没配 expires,每次刷新都回源。可以补一段,但要记得 index.html 不能缓存,否则发版后用户看不到新版本:
nginx
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
root /usr/share/nginx/html;
expires 30d;
add_header Cache-Control "public";
}
六、换证书:四步,不重启容器
目标很明确:替换两个挂载文件,然后 reload。不需要重启容器,业务零中断。
1. 备份
bash
cd /root/config/nginx
mkdir -p bak_$(date +%Y%m%d)
cp example.com_cert_chain.pem example.com_key.key bak_$(date +%Y%m%d)/
备份这一步别省。证书更换失败会导致整站 HTTPS 不可用,有备份就能在 30 秒内回滚。
2. 校验新证书
新证书必须是 PEM 格式 ,且证书在前、中间证书在后。顺序错了,部分客户端会报证书链不完整。
bash
# 看有效期、域名、签发者
openssl x509 -in new.pem -noout -subject -issuer -dates
# 证书链里应包含 2~3 张证书
grep -c "BEGIN CERTIFICATE" new.pem
# 私钥与证书是否匹配:两条命令输出的 MD5 必须一致
openssl x509 -in new.pem -noout -modulus | md5sum
openssl rsa -in new.key -noout -modulus | md5sum
如果拿到的文件是 .pfx / .p12,需要先转换:
bash
openssl pkcs12 -in cert.pfx -clcerts -nokeys -out new.pem
openssl pkcs12 -in cert.pfx -nocerts -nodes -out new.key
注意这样只导出了叶子证书,还得把中间证书 cat 追加到 new.pem 末尾。
3. 覆盖文件并设置权限
bash
cp new.pem example.com_cert_chain.pem
cp new.key example.com_key.key
chmod 600 example.com_key.key
chmod 644 example.com_cert_chain.pem
chown root:root example.com_*
私钥权限给 600 是底线,避免被同机其他账号读取。
4. 校验 + 热加载
bash
docker exec web-front nginx -t
docker exec web-front nginx -s reload
nginx -t 会同时验证证书和私钥是否匹配,如果报 SSL_CTX_use_PrivateKey,就是私钥不配套,直接回滚。
七、验证:别只看浏览器锁标
最可靠的验证是直接问 TLS 握手要证书:
bash
echo | openssl s_client -connect 127.0.0.1:8082 -servername coc.example.com 2>/dev/null \
| openssl x509 -noout -dates -subject -issuer
本次输出:
notBefore=Oct 9 00:00:00 2026 GMT
notAfter =Apr 25 23:59:59 2027 GMT
subject = ... O = 某建筑企业, CN = *.example.com
issuer = C = US, O = "DigiCert, Inc.", CN = DigiCert Basic OV G2 TLS CN RSA4096 SHA256 2022 CA1
三个信息同时确认了结果:
notBefore是新的起始日期,说明确实换成了新证书;- 是通配符证书
*.example.com,以后新增子域不用重新签发; - 能拿到
issuer,说明中间证书随服务下发正常。
再补一条命令确认链完整:
bash
echo | openssl s_client -connect 127.0.0.1:8082 -servername coc.example.com -showcerts 2>/dev/null \
| grep -c "BEGIN CERTIFICATE"
返回 2 或 3 才是对的。返回 1 说明只下发了叶子证书,PC 浏览器可能没事,但部分安卓 WebView 和老客户端会报错------这种问题上线后才暴露,很难查。
回滚
bash
cp /root/config/nginx/bak_20261019/* /root/config/nginx/
docker exec web-front nginx -s reload
八、复盘:这次踩到的坑和沉淀下来的清单
三个认知纠正
其一,容器环境里,宿主机上的「默认路径」大概率是空的。
/etc/nginx 存在不代表配置在里面。看到 docker-proxy 占端口、进程带 daemon off、路径是 /app.jar 这类特征,就该立刻切到 docker 视角。
其二,docker inspect 的 Mounts 是排障的入口,不是可选项。
它同时回答了两个问题:配置文件在哪、改了会不会丢。
其三,nginx -T 比 cat 可靠。
配置分散在 nginx.conf、conf.d/*.conf、sites-enabled/* 多个文件里时,只有 -T 能保证你看到的是「实际生效的组合」。
一份可复用的定位流程
bash
# 1. 确认系统
cat /etc/os-release
# 2. 谁在占端口
ss -lntp | grep -E ':80|:443'
ps -ef | grep -Ei 'nginx|httpd|java' | grep -v grep
# 3. 列出容器与端口映射
docker ps --format 'table {{.ID}}\t{{.Image}}\t{{.Names}}\t{{.Ports}}'
# 4. 看挂载
docker inspect <容器名> -f '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'
# 5. 容器内搜域名
docker exec <容器名> sh -c 'grep -rl "域名" /etc/nginx'
# 6. 打印生效配置
docker exec <容器名> nginx -T | grep -n -B10 -A40 "域名"
六条命令,从「只有一个域名」到「精确锁定一行 server_name」,通常五分钟内能走完。
顺手发现的待办
- CentOS 7 已 EOL,需排迁移计划;
TLSv1.1建议移除;- 站点跑在 8082 非标端口,可评估是否收敛到 443;
- 前端接口地址硬编码,建议后续改为相对路径 + nginx 反代;
- 静态资源缺少缓存策略;
- 证书 2027-04-25 到期,提前 30 天续期。
一次换证书的任务,最后变成了一次小规模架构梳理。运维工作里这种情况挺常见的------问题表面是「改个文件」,实际考验的是「你能不能先找到那个文件」 。而找到它的关键,往往不是更熟练的 grep,而是先问一句:这个进程,到底跑在哪儿?