先说一件我今年已经碰到好几次的事:同事搭了个内部系统,Nginx、域名、端口全配好了,结果浏览器一打开就是红色警告。检查了半天,问题要么出在证书链没配全,要么出在自签证书压根没被系统信任。SSL证书这个东西,说难不难,说简单也不简单,真正卡人的地方往往不在Nginx配置,而在证书本身怎么来、浏览器到底认不认。
这篇文章就把整条链路讲透:证书怎么生成、怎么签出来、怎么配置进Nginx,以及最后一步——怎么让浏览器从"拒绝访问"变成"小锁头"。无论你是要给公网网站上HTTPS,还是内网测试环境想消掉那个烦人的警告,都能从这里找到能直接抄走的方案。
1. SSL证书到底是什么,Nginx为什么需要它
1.1 证书解决的两个核心问题
网络传输本质上是一封封信件在驿站间传递,任何中间节点都可以拆开偷看。HTTP协议传的就是明文信件,密码、Cookie、订单数据全都赤裸裸地暴露在链路上。SSL/TLS协议解决的就是"信件加密"和"身份核验"两个问题,而SSL证书是这两个问题共同的基础设施。
- 身份核验:浏览器需要确认,当前连接的服务器确实是"example.com",而不是某个仿冒站点。证书里写着域名、颁发者、有效期、公钥等信息,数字签名保证了这些内容无法被篡改。
- 加密通信:证书里携带的公钥,让客户端和服务器能安全地协商出一把对称加密密钥,后续所有数据都用这把密钥加密传输。
没有证书,就算你在Nginx里强行开了SSL,浏览器也会因为"无法确认服务器身份"而拒绝信任。这也是为什么"配置SSL证书"这个动作,本质上不是写几行配置,而是解决"信任"的问题。
1.2 证书类型与适用场景
按颁发方和用途,常见的证书分三类:
| 证书类型 | 颁发方 | 浏览器是否默认信任 | 适用场景 |
|---|---|---|---|
| 公网CA证书 | Let's Encrypt、云厂商等商业CA | 是 | 线上正式环境、对外网站 |
| 自签名证书 | 自己用openssl签 | 否,需手动导入 | 测试环境、临时验证 |
| 内网自建CA证书 | 自己搭CA,再做下级签名 | 需把CA根证书导入客户端 | 内网多台服务器、长期使用的内部系统 |
这里有个常见误区:公司内网系统用自签证书,浏览器报错,很多人第一反应是"绕过警告继续访问"。短时间可以,但一旦有同事换了电脑、清了缓存,警告又会回来。正确做法是搭一个内网CA,把根证书分发到所有客户端,一劳永逸。后面的章节我会分别讲这三种方案的落地方式。
2. 三种获取SSL证书的方式,按需选择
2.1 自签名证书:openssl一步到位
测试环境最快的方式是直接用openssl生成自签名证书。一条命令搞定私钥和证书:
mkdir -p /etc/nginx/ssl && cd /etc/nginx/ssl openssl req -x509 -nodes -newkey rsa:2048 \ -keyout example.key -out example.crt \ -days 365 \ -subj "/C=CN/ST=Beijing/L=Beijing/O=Example Inc/OU=IT/CN=example.com"简单解释下几个参数:
-x509:直接输出自签名证书,而不是生成证书签名请求(CSR)。-nodes:私钥不加密,这样Nginx启动时不用输入密码。生产环境如果要更高安全等级,可以去掉这个参数,但Nginx每次reload都要手动输密码,实操中很少这么用。-days 365:有效期一年,测试环境够了。-subj:证书主体信息,其中CN=example.com是最关键的字段,浏览器会拿它和你访问的域名比对。这里填IP也可以,但要注意访问时用IP访问,且证书的CN或SAN里必须包含这个IP。
注意:现在主流浏览器已经不怎么认
CN字段了,更认SAN(Subject Alternative Name)。上面的命令只写了CN,Chrome新版可能还是会报错。稳妥的做法是额外加一个配置文件,或在命令里通过-addext添加SAN扩展:
openssl req -x509 -nodes -newkey rsa:2048 \ -keyout example.key -out example.crt \ -days 365 \ -subj "/C=CN/ST=Beijing/L=Beijing/O=Example Inc/OU=IT/CN=example.com" \ -addext "subjectAltName=DNS:example.com,DNS:www.example.com,IP:192.168.1.10"这个坑我踩过不止一次,早些年生成的证书连SAN都没有,结果Chrome升级后直接报"NET::ERR_CERT_COMMON_NAME_INVALID",排查了半天才想起来是证书缺扩展字段。
2.2 免费公网证书:ACME协议与Let's Encrypt
如果你的域名解析在公网,且网站对外提供服务,最省心的方式是申请Let's Encrypt免费证书。有效期90天,但配置自动续期后,基本可以做到"装上之后再也不管"。
目前主流工具是certbot,或者acme.sh。我更喜欢acme.sh,因为它是纯shell脚本,不依赖Python环境,在各类Linux发行版上都能跑。
以acme.sh为例,安装并签发证书:
curl https://get.acme.sh | sh alias acme.sh=~/.acme.sh/acme.sh acme.sh --issue -d example.com -d www.example.com --webroot /var/www/html签发成功后,把证书安装到Nginx目录:
acme.sh --install-cert -d example.com \ --key-file /etc/nginx/ssl/example.key \ --fullchain-file /etc/nginx/ssl/example.crt \ --reloadcmd "nginx -s reload"--fullchain-file是关键,它会把服务器证书和CA中间证书拼成一个文件,避免后面出现"证书链不完整"的问题。--reloadcmd会在证书自动续期成功后主动reload Nginx,让新证书生效。
2.3 云厂商提供的免费证书
阿里云、腾讯云等主流云平台都提供一年期的免费DV证书,在控制台搜索"SSL证书"就能找到。申请流程一般是:填写域名、选择验证方式(DNS验证或文件验证)、等待签发、下载证书文件。
这类证书的优势是:
- 证书链完整,签发后直接拿到
crt和key两个文件。 - 有的一年期到期后可以免费续期,操作路径和申请时一样。
- 支持泛域名(
*.example.com),一张证书覆盖所有子域名。
不足之处是要登录控制台手动操作,不像ACME协议那样全自动。如果你有精力折腾一次自动续期,我建议优先上Let's Encrypt;如果想省心、不想维护脚本,云厂商的免费证书也不差。我个人是混合使用:对外核心域名用云厂商证书,内部系统和测试域名用自建CA或Let's Encrypt。
3. Nginx配置HTTPS的完整流程
3.1 最基础的HTTPS server块
拿到证书文件后,Nginx配置其实很简单。核心只需要三个指令:
server { listen 443 ssl; server_name example.com; ssl_certificate /etc/nginx/ssl/example.crt; ssl_certificate_key /etc/nginx/ssl/example.key; root /var/www/html; index index.html; }关键点:
listen 443 ssl:让Nginx在443端口启用SSL。新版Nginx也可以写成listen 443 ssl default_server;。ssl_certificate:证书文件路径,如果用acme.sh安装,是fullchain文件。ssl_certificate_key:私钥文件路径。
写完配置后,先校验再生效:
nginx -t nginx -s reloadnginx -t这一步务必养成习惯。很多人改了配置不校验就直接reload,语法错了Nginx根本起不来,反而把线上服务搞挂了。
3.2 HTTP自动跳转HTTPS
配好443后,还要处理80端口的流量。最常用的做法是加一个server块,把所有HTTP请求301到HTTPS:
server { listen 80; server_name example.com; return 301 https://$host$request_uri; }这招的细节在于用$host而不是$server_name,这样用户用IP访问时也能正确跳转,不会因为server_name不匹配而跳到一个错误的域名上。
如果你是反向代理场景,比如Nginx在外面,后面挂了Tomcat或其他后端服务,443端口的server块就要加上proxy_pass:
server { listen 443 ssl; server_name example.com; ssl_certificate /etc/nginx/ssl/example.crt; ssl_certificate_key /etc/nginx/ssl/example.key; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $remote_addr; } }X-Forwarded-Proto要特别留意。后端的Tomcat或其他应用,只有拿到这个头才知道你用的是HTTPS,否则它会以为自己是纯HTTP环境,生成出来的重定向和回调地址全是http://,然后整个链路就乱了。
3.3 安全加固参数,一次配到位
证书能用之后,建议顺手把安全参数也加上。下面是经过实战验证的一组配置,兼容性和安全性比较平衡:
server { listen 443 ssl; server_name example.com; ssl_certificate /etc/nginx/ssl/example.crt; ssl_certificate_key /etc/nginx/ssl/example.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384'; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options SAMEORIGIN always; }几个参数的解释:
ssl_protocols TLSv1.2 TLSv1.3:禁掉老旧的TLSv1.0和TLSv1.1,这两个协议有多处已知漏洞,还在用的话安全检查肯定过不了。ssl_session_cache shared:SSL:10m:启用session cache,HTTPS握手太频繁时会大幅降低CPU开销。10m大约能存8万个session。Strict-Transport-Security:HSTS头,告浏览器"接下来一段时间只允许用HTTPS访问",防止中间人把HTTPS降级成HTTP。注意这个头一旦加上,浏览器会严格执行,如果证书配错了想临时切回HTTP,用户端会一直跳转失败。第一次配置建议先不加这个头,或者把max-age设小一点验证没问题再调大。
4. 让浏览器信任证书的三种方案
4.1 公网CA证书:默认信任,无需任何操作
如果你用的是Let's Encrypt或云厂商签发的公网证书,浏览器默认就会信任,不需要做任何额外工作。原因在于,你的证书是由一个"根CA"逐级签名下来的,而各大操作系统内置了一系列根CA的公钥列表,浏览器会顺着证书链一路验证上去,最终发现签名链的根在可信列表里,就判定为可信。
这里唯一可能翻车的情况是证书链不完整。服务器只发了自己的证书,没发中间CA证书,浏览器找不到中间环节,就会报"证书链不完整"(NET::ERR_CERT_AUTHORITY_INVALID)。这也是为什么我一直强调要用fullchain文件,而不是只拿证书文件本身。
4.2 自签证书:手动导入客户端信任库
自签名证书的信任问题是天生的——它没有上级CA,浏览器只认自身签名,而你的证书又不在系统信任列表里。解决思路只有一个:把你的自签证书(或者自建CA的根证书)导入到客户端的系统信任库。
以Windows为例,导入流程:
- 双击
.crt文件,点"安装证书"。 - 存储位置选择"本地计算机"。
- 选择"将所有的证书都放入下列存储",点"浏览",选择"受信任的根证书颁发机构"。
- 完成安装,重启浏览器。
macOS上操作类似:双击证书文件,在"钥匙串访问"里找到它,右键选择"显示简介",把"信任"选项改为"始终信任"。
Linux桌面环境下,不同发行版路径不同。Ubuntu可以用:
sudo cp example.crt /usr/local/share/ca-certificates/ sudo update-ca-certificates这种方式适合一台两台机器。如果你的内网环境有几十台电脑、手机、平板,再一台台手动导入就不现实了,得用下面的方案。
4.3 内网自建CA:一劳永逸的信任方案
内网多服务器场景,最佳实践是自己搭一个内部CA,用这个CA给所有内网服务器签证书,然后把CA根证书分发到所有客户端。这样每次新增服务器,只需要用CA签一张新证书,客户端的信任列表完全不用动。
自建CA的核心步骤分三步。
第一步,创建CA根证书:
mkdir -p /etc/pki/CA && cd /etc/pki/CA openssl genrsa -out ca.key 2048 openssl req -x509 -new -key ca.key -days 3650 \ -subj "/O=Internal CA/CN=Internal Root CA" \ -out ca.crt第二步,为服务器签发证书。这一步要先给服务器生成私钥和CSR,然后在CSR里带上SAN:
openssl genrsa -out server.key 2048 openssl req -new -key server.key \ -subj "/CN=gitlab.example.internal" \ -out server.csr openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \ -CAcreateserial -out server.crt -days 825 \ -extfile <(printf "subjectAltName=DNS:gitlab.example.internal,IP:192.168.1.100")这里有效期写825天而不是365天,是因为Apple和Google对超过825天的证书不予信任,虽然内网CA不受公网CA规则约束,但养成这个习惯没坏处。
第三步,把ca.crt分发到所有客户端,导入方式见4.2。之后无论哪台Nginx服务器用这个CA签证书,客户端都能正常信任。
提示:手机内网访问是自签/自建CA方案最容易遗漏的环节。iOS需要在"设置-通用-关于本机-证书信任设置"里额外打开"完全信任",Android也要手动安装CA证书。别等同事手机访问系统报错才想起来这一层。
5. 实战排错:常见问题速查表
5.1 证书链不完整导致的信任失败
浏览器报错"NET::ERR_CERT_AUTHORITY_INVALID"或"证书颁发者错误",十有八九是证书链问题。处理思路很简单,检查服务器返回的证书链是否包含中间证书。
用这条命令可以快速查看:
openssl s_client -connect example.com:443 -showcerts输出的证书列表中,应该能看到至少两段BEGIN CERTIFICATE。如果只有一段,说明你配置的证书文件里只有服务器证书,缺了中间CA。解决办法是把中间证书和服务器证书拼接成一个文件:
cat server.crt intermediate.crt > fullchain.crt然后ssl_certificate指向fullchain.crt,reload即可。这也是Let's Encrypt的--fullchain-file帮你做的事情。
5.2 nginx -t报错的几种典型情况
nginx -t报错主要集中在证书路径和私钥匹配上,我整理成一张速查表:
| 报错现象 | 常见原因 | 解决办法 |
|---|---|---|
cannot load certificate | 证书文件路径写错或文件不存在 | 检查ssl_certificate路径,确认文件权限可读 |
PEM_read_bio_PrivateKey | 私钥不是PEM格式,或私钥与证书不匹配 | 确认ssl_certificate_key指向正确的私钥文件 |
the certificate ... does not match the private key | 证书和私钥不是同一对 | 用自签或签发时配套的私钥,重新核对 |
[emerg] bind() to 0.0.0.0:443 failed | 80/443端口被占用 | 查ss -lntp,看是哪个进程占用了端口,常见的是其他Nginx实例或Apache |
证书和私钥是否匹配,也可以用一条命令验证:
openssl x509 -in server.crt -noout -pubkey | openssl md5 openssl rsa -in server.key -pubout 2>/dev/null | openssl md5两次输出的MD5一致,说明是同一对,否则肯定报错。
5.3 证书续期与自动更新
Let's Encrypt证书有效期90天,手动续期的话会经常忘。acme.sh默认装了cron任务,证书到期前会自动续期,并把新证书通过--reloadcmd触发Nginx reload,整体不需要人工干预。
验证自动续期是否正常:
acme.sh --list crontab -l | grep acme如果用的云厂商证书,需要在到期前在控制台重新申请并下载,替换Nginx里的证书文件后reload。这里我有个血泪教训:云厂商证书的有效期是按自然年计算的,到期那天正好是节假日,网站HTTPS挂了半天才发现。建议在手机上设个提前30天的提醒,或者干脆转Let's Encrypt让它全自动。
还有一个细节容易被忽略:证书续期后,如果Nginx进程长时间没有reload,会导致它一直握着旧证书,直到重启才换新。所以无论用哪种方式续期,都要记得reload一次Nginx。
这篇文章从证书原理讲到自签/免费/云厂商证书的获取,再到Nginx配置和浏览器信任,最后给了排查思路。最后再分享一个我个人的小建议:生产环境的证书文件,权限一定要收紧。私钥文件设成600或640,目录设成700,别让Nginx worker进程之外的账号能读。证书这种东西,表面上只是几行文本,但一旦私钥泄露,等于把加密的大门钥匙交了出去,这才是真正要命的隐患。能用自动化续期的就别手动,能用内网CA的别用一堆自签证书,把信任关系一次性理清楚,后面能省下大把时间。