开了公网端口之后第一件事就是把Nginx反向代理和SSL证书配上。这俩东西看着是两个独立名词实际上在线上就是一对固定搭档Nginx负责把443端口的流量按规则分给你后面的各种服务SSL证书负责让这条链路在用户眼里是“小锁头https”而不是满屏橙色警告。这篇文章就是围绕这组组合展开的实操笔记包含证书怎么选、怎么签发、Nginx怎么配、证书到期怎么自动续期以及我踩过的一些坑。适合正在从HTTP裸奔过渡到HTTPS的运维同行也适合自己做独立站、想在一台服务器上塞多个服务的开发者。1. 为什么Nginx要站在前面SSL证书要给谁看1.1 反向代理不是“反向”而是一层总入口很多人第一次听到“反向代理”这个词容易被“反向”两个字绕晕。换个说法就简单了正向代理是代访问者出去反向代理是代服务器收客。你打开浏览器访问某个域名Nginx先收到这个请求再看这个域名该去哪个后端进程然后把请求转发过去拿到响应再返回给你。对外部来说你只暴露了443和80端口至于后面是Java、Node还是Python用户都看不见也不应该看见。这层“总入口”带来的好处是实打实的一台服务器上可以同时跑多个服务用不同域名或者不同路径区分各自监听内网端口不用抢占公网端口静态资源可以直接放在Nginx层做缓存减轻后端压力后端配置变更、灰度发布时Nginx可以做简单的流量切换更重要的是所有流量统一经过TLS加密证书只要管这一个入口而不是每个后端服务各自维护一套HTTPS。很多项目一开始只用一台开发机裸跑Spring Boot自己监听8080就直接对外开放等真正要上线时才会意识到8080端口裸奔、没有HTTPS、后端报错堆栈直接通过HTTP返回给调用方这些东西在安全评审那里全都是问题。我对反向代理的定位一直没变过它不是功能特性是上线前的基础设施。哪个服务该暴露、哪个服务只该内网访问、如何统一鉴权、如何记录所有请求日志这些都可以在Nginx这一层解决而不去打扰业务代码。1.2 TLS握手SSL证书在这个环节干了两件事SSL证书的作用不能只简单理解为“传输加密”。它其实在TLS握手里承担两件事证明服务端身份以及帮助双方协商出对称加密密钥。握手过程简化下来是这样的客户端发起ClientHello带上自己支持的TLS版本和加密套件列表服务端回复ServerHello选好版本和套件同时把自己的证书链发给客户端客户端拿到证书后首先验证这个证书是谁签发的、是否在有效期内、域名是否匹配验证通过后双方再通过密钥交换算法常见的是ECDHE在公开的信道里协商出一个只有双方知道的会话密钥之后所有内容都用这个会话密钥做对称加密。可以把证书理解成“带身份信息的保险柜”。保险柜本身负责把数据锁起来但你在没有见过对方的前提下怎么确定这个保险柜不是中间人塞给你的证书里的CA签名就是那层信任基础。所以就算你只在内网测试只要用上了TLS证书信任链路就必然存在——要么是公共CA构建的信任链要么是你在客户端手动安装的自建CA信任链绕不开。1.3 先想清楚是公网还是内网再决定证书路线我建议拿到任何项目先问一句这个服务需要被公网访问吗如果是公网服务——独立站、SaaS、接口平台——直接用公共CA签发的证书浏览器和手机端天然信任不用在每台设备上折腾证书导入。如果只是内网服务比如公司内部的GitLab、运维后台、NAS管理页用公共CA也能签但你需要暴露域名到公网做验证或者走DNS验证很多团队会因为不想让内网域名出网而选择自签证书。这时我建议不要直接生成一张自签名证书到处导而是搭一个私有CA用这个CA给内网域名批量签发证书然后把CA根证书分发到需要访问的设备上。以后员工电脑、手机一次性信任根CA换证书不用再导一遍风险也小得多。2. 证书类型与申请准备从自签到达公共CA2.1 证书类型怎么选DV、OV、EV别拍脑袋证书市场上叫得最响的几个缩写是DV、OV、EV它们表示CA验证主体身份的程度不同。类型验证内容签发速度典型费用适用场景DV验证域名控制权几分钟免费或极低个人站点、API、内部系统、自动化运维OV验证域名控制权企业身份信息数小时到数天中高企业官网、电商、对信任度要求较高的站点EV验证最严格含法律文件核验数天高金融、银行、高价值交易平台我实际项目里90%的场景用DV证书就够了。原因很直接DV证书同样提供完整的TLS加密能力浏览器也能正常显示小锁头只是不显示企业名称罢了。前几年EV证书还能让浏览器地址栏变绿现在主流浏览器为了统一的UI体验把企业名显示从地址栏挪走了EV的投入产出比进一步下降。除非你有明确的合规要求或客户要求否则DV公共证书是性价比最高的选择。自签名证书和公共CA证书之间还有一个折中方案私有CA。私有CA的信任树不会自动被浏览器认可需要你在客户端安装根证书。但它胜在灵活、免费、支持任意内网域名和IP。我服务过的几个项目内网环境里有几十个服务域名全部由一套私有CA统一签发运维手里只有一套根证书设备侧装一次后面所有服务都能用比每台服务器生成一张自签名证书再分别信任要整洁太多。2.2 OpenSSL生成自签名证书的完整命令自签名证书不是给公网正式用的但在测试环境、内网联调、快速拉起HTTPS时非常有用。用OpenSSL生成一张带SAN扩展的证书命令如下mkdir -p /etc/ssl/example openssl req -newkey rsa:2048 -nodes \ -keyout /etc/ssl/example/example.com.key \ -x509 -days 365 \ -out /etc/ssl/example/example.com.pem \ -subj /CNexample.com \ -addext subjectAltNameDNS:example.com,DNS:www.example.com,IP:192.0.2.1参数拆开看req是证书请求和自签工具-newkey rsa:2048生成2048位RSA私钥2048位现在还是底线别再退回去用1024位-nodes表示私钥不加密这样Nginx启动时不需要额外输入密码生产环境不建议用加密私钥让运维敲密码自动化也走不下去-x509表示直接输出自签名证书而不是证书签名请求-days 365是有效期-subj里写CNCN必须和证书使用时的主域名一致-addext很关键必须加SAN也就是主题备用名称。为什么强调SAN浏览器从几年前开始只认证书里的subjectAltNameCN字段已经被忽略。如果你生成的证书只有CNexample.com而没有SANChrome和Firefox大概率报“证书名称不匹配”。我见过有人在Nginx配置里折腾半天最后发现是证书里缺了SAN。IP:192.0.2.1表示这张证书也认这个IP适合直接用IP访问内网服务的场景。2.3 公共证书申请ACME协议把流程自动化公网服务上我更推荐用ACME协议申请公共CA证书。ACME最大的贡献是把证书申请、校验、续期全部协议化和自动化让“有效期只有90天”这件事变得没那么可怕。ACME验证域名控制权有两种主流方式。HTTP-01验证CA给一个随机token放临时文件到你域名根目录下的指定路径CA通过HTTP访问到这个文件就算通过。这个方式要求你的站点80端口可达配置文件里要留好location /.well-known/acme-challenge/的放行策略。DNS-01验证CA要求你把一串TXT记录加到域名的DNS解析里适合没有80端口、服务器在NAT后面、或者一个证书包含几十个域名的场景。DNS-01不需要开放任何入站端口但需要你通过DNS服务商的API去增删记录ACME客户端一般都有对应插件。申请完成后证书目录里通常会区分几个文件cert.pem服务器证书本身chain.pemCA中间证书链fullchain.pem服务器证书和中间证书拼在一起privkey.pem私钥。在Nginx配置里ssl_certificate用的应该是fullchain.pem而不是cert.pem否则很多严格校验的客户端会报“证书链不完整”ssl_certificate_key则对应privkey.pem。2.4 证书文件管理的三个底线证书文件管理看似简单实际踩坑的不少。我给自己定了三个底线第一私钥权限绝不能宽松。建议私钥文件设为600或640owner设置成rootNginx master进程以root启动时能正常读取普通用户和nginx工作进程无法直接查看。如果权限太开Nginx会直接拒绝启动日志里报permission denied。第二证书目录结构要统一。我习惯用/etc/ssl/域名/作为一级目录里面放以域名命名的fullchain和key这样写续期脚本、找问题、对接监控都不会混乱。第三证书和私钥文件要做只读处理。避免有人手滑chmod改了权限、或者误编辑把内容弄坏。真正更新证书时由ACME客户端负责覆盖文件和Nginx reload之间做好顺序协调。3. Nginx反向代理与SSL配置实操一切以可维护为准则3.1 先验证普通的HTTP反向代理直接上HTTPS之前我习惯先把HTTP反向代理跑通确认域名、路径、后端转发都没问题再套SSL层。否则一旦加了证书排查问题的变量又多了一个很烦。下面是一个最基础的server块server { listen 80; server_name example.com; location / { proxy_pass http://127.0.0.1:8080; 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; } }proxy_pass要写完整的http://协议头Nginx才会按HTTP协议转发不要写成proxy_pass 127.0.0.1:8080这种缺协议的形式。三个proxy_set_header很重要后端程序需要知道用户真实IP、原始协议和原始Host才能生成正确的链接、记录访问日志、做逻辑判断。如果不传X-Forwarded-For后端的日志里所有来源IP都会显示成Nginx所在的内网IP将来做安全分析时等于瞎了。3.2 一套规范到位的443配置反向代理确认可用后把HTTPS配置加上去。下面是我常用的一整套433配置骨架server { listen 443 ssl; http2 on; server_name example.com; ssl_certificate /etc/ssl/example/fullchain.pem; ssl_certificate_key /etc/ssl/example/example.com.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; ssl_session_tickets off; ssl_stapling on; ssl_stapling_verify on; resolver 127.0.0.53 valid300s; resolver_timeout 5s; add_header Strict-Transport-Security max-age31536000; includeSubDomains always; location / { proxy_pass http://127.0.0.1:8080; 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; } }这里展开讲几个点。listen 443 ssl;和http2 on;的写法是Nginx新版本推荐的。Nginx 1.25.1之前HTTP/2开关写在listen里形如listen 443 ssl http2;。新版本如果继续写会输出一段废弃警告部分发行版甚至直接报参数错误。所以现在统一写成listen 443 ssl;加独立指令http2 on;两边都兼容。ssl_protocols只启TLSv1.2和TLSv1.3是比较合理的默认策略。TLSv1.0和TLSv1.1早该从配置里拿掉了它们身上的已知漏洞太多浏览器客户端兼容性也早就不是问题。如果用户群里还有老旧的Windows系统、旧款手机浏览器才需要额外评估兼容性否则就不要让旧协议继续活着。ssl_ciphers这行控制的是TLS1.2及以前版本的密码套件优先级在前把GCM和CHACHA20这类认证加密套件排在前面。ssl_prefer_server_ciphers off;表示允许客户端套件优先级介入而不是强制服务端顺序。对绝大多数现代客户端来说off带来的协商兼容性更好一些。TLS1.3的套件不归这行管OpenSSL里有它自己的一套安全默认值不用在这里手工写。ssl_session_cache和ssl_session_timeout解决的是握手性能问题。TLS握手最耗费资源的部分是密钥交换和证书验证如果把会话票据或缓存开起来一定时间内再次连接就能复用之前的协商结果。共享缓存shared:SSL:10m大约能容纳几十万个session凭证对大部分站点足够。ssl_stapling开启OCSP封套作用是让Nginx在握手时主动附带证书的有效性状态省去客户端自己再向CA的OCSP服务器发起查询。好处是握手更快、更隐私还减少了因为OCSP服务器临时不可用导致的验证失败。但要配置resolver让Nginx能解析OCSP服务地址。我在示例里写了127.0.0.53这是systemd-resolved的本地DNS解析器如果你没开systemd-resolved可以改成自己网络环境的DNS地址或公共DNS地址。add_header Strict-Transport-Security ... always;开启HSTS。它的作用是告诉浏览器以后只能通过HTTPS访问这个域名。这是很强大的安全策略但也意味着它是“有伤害”的——如果你还没完全搞定HTTPS、或者以后想临时切回HTTP调试HSTS会强制浏览器跳转HTTPS导致你“无论如何都访问不到HTTP”。所以这个头建议在正式启用HTTPS稳定运行后再加测试阶段要么不加要么把max-age设成几分钟。3.3 HTTP强制跳转HTTPS的几种写法最容易理解的写法就是单独监听80端口直接返回301server { listen 80; server_name example.com www.example.com; return 301 https://$host$request_uri; }用$host而不是$server_name是为了保留用户原始请求里的域名。如果用户访问的是www.example.com跳转后还是www.example.com不会被吞掉前缀。这里也不建议用rewrite那一套来做跳转return 301语义清晰、性能更好一条指令搞定。需要留意的是如果你同一套Nginx还要承接ACME的HTTP-01验证请求必须在80端口server块里额外放行/.well-known/acme-challenge/路径否则后续自动续期会失败。3.4 一个入口反代多个后端按路径路由与WebSocket单台服务器多服务复用443最常见的方案是按URL路径切。比如所有/api/开头的请求转到后端A的8081其他请求转到后端B的8080location /api/ { proxy_pass http://127.0.0.1:8081; 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; } location / { proxy_pass http://127.0.0.1:8080; 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; }WebSocket服务反代是另一个容易踩坑的地方。Nginx默认在HTTP/1.0转发时不带Upgrade和Connection头导致WebSocket握手失败。加这段location /ws/ { proxy_pass http://127.0.0.1:9000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; }proxy_http_version 1.1是必须的因为WebSocket握手依赖HTTP/1.1的Upgrade机制。Connection upgrade则让后端知道这次连接要升级成WebSocket协议。如果你发现客户端连上了WebSocket但很快断开、或者干脆握手被拒先查这里。3.5 配置校验与平滑重载的仪式感每次改完Nginx配置都要先做语法检查再重载这个流程一步都不能省nginx -t nginx -s reloadnginx -t会检查所有配置文件语法和include关系有问题会精确报出文件路径和行号。-s reload是平滑重载不中断现有连接worker进程会以新配置重新拉起。如果用的是systemd也有systemctl reload nginx效果相同。很多老手即便只改了一个括号也会先nginx -t再reload这是因为线上环境一个逗号错误就能让整个站点挂掉而这种错误不做语法检查根本发现不了。4. 证书自动续期与过期监控让服务“忘掉”证书4.1 为什么公共证书有效期越缩越短前几年证书能签一年甚至更久。这几年主流公共CA都在缩短有效期现在普遍是90天。原因是证书有效期越长一旦私钥泄露或者CA误签发风险暴露窗口就越大。把有效期缩短再加上ACME自动化等于把证书管理的成本从“一年操心一次”变成“三个月自动换一次”。这件事对运维是个提醒不能再靠“手工算好到期日再续签”的老思路了。我见过不止一次事故续期脚本没配、或配置了但Nginx没reload结果凌晨证书过期用户访问时浏览器直接给红屏。把续期做成自动化流程是这个时代运维的基本功。4.2 一条龙续期脚本下面这个脚本的思路是先检查证书剩余时间不足30天才触发续期每次只续一个指定域名的证书续完后立刻reload Nginx。#!/usr/bin/env bash CERT/etc/ssl/example/fullchain.pem if ! openssl x509 -in $CERT -noout -checkend 2592000; then echo $(date): certificate expires within 30 days, renew now certbot renew --cert-name example.com --deploy-hook systemctl reload nginx else echo $(date): certificate still valid for more than 30 days, skip fiopenssl x509 -checkend 2592000是一个很实用的参数它检查证书剩余有效时间是否大于2592000秒也就是30天。如果剩余时间不足30天openssl会返回非零退出码被!取反后满足条件执行续期命令。certbot renew只会在证书确实需要续期时才真正执行续期--deploy-hook systemctl reload nginx则保证只有新证书签发成功后才会重载Nginx。这样脚本不会在每次运行时无意义地重载服务也不会在续期失败时把老的Nginx配置给刷一遍。4.3 定时任务与告警脚本写好后用crontab每天跑一次15 3 * * * /usr/local/bin/renew-cert.sh /var/log/cert_renew.log 21选在凌晨3点15分避开了整点高峰也不影响白天业务。日志一定要落到文件里方便第二天排查。如果连续续期失败脚本没有发告警只能说等到证书过期时你才会知道那就晚了。我建议在日志方案之外再叠加一个独立监控每天用openssl检查证书到期天数剩余天数小于7天时发个告警。告警渠道不限于邮件钉钉、企业微信、短信都可以前提是和你团队的响应机制打通。另外一个容易被忽略的细节是证书到期检查的服务器要从外部探测而不是只在本机看文件。本机证书更新成功、但公网到服务器的443链路断了或者本地DNS缓存了旧证书都会让线上用户看到过期证书。所以至少要有“本机文件检查”和“外部探测”两条线才能真的放心。5. 常见问题与排查技巧实录5.1 先看这张问题速查表我把部署SSL证书和Nginx反向代理过程中最常遇到的一批问题整理成了表基本能覆盖日常80%的报错场景现象可能原因排查/解决方向浏览器提示证书不受信证书链不完整、私密证书未导入信任库检查fullchain是否包含中间证书客户端是否信任签发CA浏览器提示证书名称不匹配证书缺少SAN扩展或server_name与证书不一致用openssl x509 -in cert.pem -text查看SAN修改Nginx的server_name或重新签发带正确SAN的证书Nginx启动失败报cannot load certificate证书或私钥路径错误、文件权限不对检查路径是否存在、是否为PEM格式、私钥权限是否为600/640SSL握手失败报NO_CYPHER_OVERLAPssl_protocols或ssl_ciphers配置过严对比客户端支持的协议套件临时放开ssl_protocols和ssl_ciphers测试代理后后端日志里IP全是内网地址没设置X-Real-IP和X-Forwarded-For在location里补上proxy_set_header转发头https页面里部分资源报错、锁头变成灰色页面中仍有资源走http浏览器控制台找到http资源地址统一改成https或协议相对路径WebSocket连接建立失败缺少Upgrade和Connection头或HTTP/1.1未启用给对应location加上WebSocket反向代理头开启HSTS后无法切回HTTP调试HSTS策略仍在生效临时把max-age调小或使用其他浏览器/无痕窗口测试并检测生产环境勿开启过大值5.2 实战里最容易踩的五个细节第一个坑是证书链不完整。公共CA签发的证书通常不只是一张证书而是“服务器证书中间证书”的多层链。Nginx要求ssl_certificate使用fullchain如果你只填了服务器证书那一张电脑上的浏览器可能没事但安卓App或一些协议栈严格的客户端会拒绝连接。判断方法用openssl s_client -connect example.com:443 -showcerts看返回的证书数量正常应该能看到至少两层只看到一层就是漏了中间证书。第二个坑是私钥权限过于开放。Nginx加载私钥时如果发现其他用户可读会在error.log里报权限警告甚至直接拒绝启动。规范做法私钥文件chmod 600证书文件chmod 644目录本身建议700。不管是在手动签发还是ACME自动签发后都要顺手把权限固定下来。第三个坑是续期成功但没有reload。ACME客户端只负责把新证书写到磁盘Nginx不会自动去读新文件。如果续期脚本没有执行systemctl reload nginxNginx内存里持有的还是旧证书。等到旧证书过期那一刻线上立即失败。所以deploy-hook和reload动作必须和续期脚本绑在一起。第四个坑是HSTS开早了。我在测试环境见过同事把max-age31536000开起来过了几天想改成HTTP方便调试浏览器就是不配合怎么访问都强制跳HTTPS。HSTS的初衷是好的但它是一个“单向门”开了以后想回头很麻烦。线上站点在切换完成后稳定运行几天再加HSTS并把“测试服务器”和“正式用户”分开对待。第五个坑是旧客户端兼容性。如果你在配置里只留了TLS1.3和极窄的加密套件那么旧版Android WebView、老Java程序、一些嵌入式收银设备可能压根连不上。设备环境比较杂的项目建议计算一下公式TLS1.2保底GCM套件优先再留一个CHACHA20给不支持AES硬件加速的移动端设备。5.3 快速定位问题的一套组合拳遇到套路问题怀疑是证书或代理配置时我通常按这个顺序敲命令nginx -t # 先确认配置语法没问题 nginx -T | grep ssl_certificate # 看当前生效的证书路径 curl -vI https://example.com/ --resolve example.com:443:127.0.0.1 # 本机直连443绕过DNS看握手 openssl s_client -connect 127.0.0.1:443 -servername example.com -brief # 查看服务端证书信息 ls -l /etc/ssl/example/ # 确认证书文件和权限curl加--resolve可以把域名临时解析到本地IP适合在没改DNS的情况下测试本机Nginx的HTTPS效果。openssl s_client输出里重点看两处证书是否匹配、证书链是否完整。如果本地访问正常但外部访问超时再去查云安全组的443端口是否放行、Nginx是否真的监听在0.0.0.0:443而不是127.0.0.1:443。Nginx的error.log也是排查的重中之重。很多证书问题都会在error.log里留下明确线索比如“cannot load certificate”“SSL_CTX_use_PrivateKey_file failed”“permission denied”。日志不会骗你只是很多人在浏览器报错之后忘了看一眼最直接的证据。我个人这几年做下来最大的体会是SSL证书与Nginx反向代理这一套东西难点从来不在某一个配置项而在于把它当成一个完整的生命周期来管理——从证书签发、文件权限、Nginx语法到续期自动化和监控告警每一环都要在第一天就打通。最后分享一个小技巧把openssl x509 -checkend这个命令写进每个证书目录的README里配合crontab和告警证书过期问题基本可以绝迹。