☰
OpenSSL 自签证书实战:内网 HTTPS 配置与避坑指南
2026/10/15 1:43:27 网站建设 项目流程

1. 为什么内网服务也需要一张正经的证书

很多人第一次接触自签证书,是因为浏览器地址栏那个刺眼的"不安全"提示。尤其是在内网搭建测试环境、部署内部管理系统、跑一个本地 HTTPS 服务时,明明数据只在局域网里流转,却还是被浏览器反复警告,甚至某些接口调用直接因为证书校验失败而中断。这时候最省事的做法就是自己签一张证书,让链路先跑通。

OpenSSL 是 Linux 下最通用的密码学工具箱,几乎所有的发行版都预装了它,或者一条包管理命令就能装上。用 OpenSSL 自签证书,本质上就是自己扮演一次"证书颁发机构"的角色:先生成一个根证书,再用这个根证书去签发服务器证书。整个过程不需要联网、不需要付费、不需要第三方审核,几分钟就能拿到一套可用的证书文件。

这套流程适合谁?适合需要在开发、测试、内网演示环境中启用 HTTPS 的后端工程师、运维人员、嵌入式开发者,以及任何想搞清楚 TLS 证书到底是怎么一回事的技术爱好者。它不适合直接用于对公网开放的生产环境,原因后面会详细说。但作为理解证书体系、快速搭建加密通道的手段,自签证书的性价比极高。

我见过太多人在这件事上走弯路:有人直接拿网上一段命令复制粘贴,结果生成的证书缺少 SAN 扩展,Chrome 直接拒绝;有人签完证书忘了把根证书导入系统信任库,排查半天以为是服务端配置问题;还有人把私钥权限设成 777,埋下了严重的安全隐患。这些坑我都会在下面逐一拆开讲清楚。

2. 动手之前先把这几个概念理顺

2.1 根证书、服务器证书和私钥到底是什么关系

用生活化的类比来说,根证书就像是你自己刻的一枚公章,服务器证书则是用这枚公章盖过章的身份证明。私钥是盖章时用的那个印泥盒,绝对不能给别人看。公钥则附在证书里,任何人都可以拿到,用来验证"这个章是不是你盖的"。

具体到文件层面,一次完整的自签流程会产生这些东西:

  • 根私钥(ca.key):根证书的私钥,用来给服务器证书签名。这个文件一旦泄露,别人就能以你的名义签发任意证书。
  • 根证书(ca.crt):根证书本身,需要导入到客户端或系统的信任库中。
  • 服务器私钥(server.key):服务器端使用的私钥,用于 TLS 握手时的密钥交换。
  • 服务器证书(server.crt):由根证书签发的服务器身份证明,包含域名、有效期、公钥等信息。

理解了这个层级关系,后面所有命令的参数含义就都顺了。根证书是"信任的源头",服务器证书是"被信任的实体",私钥是"证明身份的能力"。

2.2 为什么不能跳过根证书直接自签服务器证书

有人会问:我直接用一条命令生成一个自签名的服务器证书不行吗?技术上当然可以,openssl req -x509一条命令就能搞定。但这样做有个问题:每次生成新证书,客户端都得重新信任一次,而且无法用同一个根去签发多张证书。

正确的做法是建立一套两级结构:根证书签一次,导入信任库一次,之后所有服务器证书都用这个根来签。这样内网里十台机器、二十个服务,只需要维护一个根证书就够了。这也是为什么我强烈建议哪怕只是测试环境,也走完整的 CA 流程,而不是图省事直接自签。

2.3 SAN 扩展:现代浏览器的硬性门槛

这是最容易踩的坑。早期证书靠 Common Name(CN)字段来标识域名,但现代浏览器和 TLS 库早就改用 Subject Alternative Name(SAN)扩展了。如果你的证书里只有 CN 没有 SAN,Chrome、Firefox、curl 新版本都会直接报错,提示"证书不匹配"或"缺少主题备用名称"。

所以生成证书时,必须通过配置文件或命令行参数显式加入 SAN。这一点在后面每个步骤里我都会强调。很多人复制了老教程里的命令,生成的证书在旧环境能用,一到新浏览器就废,根源就在这里。

3. 从零开始生成一套可用的自签证书

3.1 准备工作目录与 OpenSSL 配置文件

先建一个干净的工作目录,把所有证书相关文件都放进去,避免和系统里其他证书混淆:

mkdir -p ~/myca && cd ~/myca

接下来创建一个 OpenSSL 配置文件openssl.cnf。虽然可以直接用命令行参数,但配置文件方式更清晰、可复用、便于维护。下面这份配置是我实际用下来最顺手的版本:

[ req ] default_bits = 2048 distinguished_name = req_distinguished_name req_extensions = req_ext prompt = no [ req_distinguished_name ] C = CN ST = Beijing L = Beijing O = MyInternalOrg OU = DevTeam CN = myinternal.local [ req_ext ] subjectAltName = @alt_names [ alt_names ] DNS.1 = myinternal.local DNS.2 = *.myinternal.local IP.1 = 127.0.0.1 IP.2 = 192.168.1.100 [ v3_ca ] subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always,issuer basicConstraints = critical, CA:true keyUsage = critical, digitalSignature, cRLSign, keyCertSign

几个关键点解释一下。default_bits = 2048是当前安全性和性能的平衡点,4096 位更安全但握手更慢,内网场景 2048 足够。prompt = no让命令不交互,适合脚本化。alt_names里把域名和 IP 都列上,这样无论用域名还是 IP 访问,证书都能匹配。v3_ca段是给根证书用的,basicConstraints = CA:true明确声明这是一张 CA 证书。

注意:IP.2那个地址要换成你服务器实际的局域网 IP。如果服务监听多个地址,全部列进去。

3.2 生成根私钥和根证书

第一步,生成根私钥。这里用 RSA 2048 位,并且用 AES-256 加密保护私钥:

openssl genrsa -aes256 -out ca.key 2048

执行后会提示输入一个密码。这个密码是保护私钥文件的,每次用这个私钥签名时都要输入。如果你在自动化脚本里用,可以去掉-aes256参数生成无密码私钥,但那样私钥文件的安全性就完全依赖文件权限了。我的建议是:手动操作时加密,自动化场景下用无密码私钥但严格限制文件权限。

第二步,用根私钥生成自签名的根证书:

openssl req -new -x509 -days 3650 -key ca.key -out ca.crt -config openssl.cnf -extensions v3_ca

-days 3650表示根证书有效期十年。根证书有效期可以设长一点,因为更换根证书意味着所有客户端都要重新导入信任,成本很高。-extensions v3_ca确保根证书带有 CA 相关的扩展属性。

执行完这两步,你就有了ca.key和ca.crt。可以用下面的命令验证根证书内容:

openssl x509 -in ca.crt -noout -text

输出里应该能看到CA:TRUE、Key Usage包含Certificate Sign,以及 SAN 列表。

3.3 签发服务器证书的完整链路

服务器证书不能直接用根私钥生成,正确流程是:先生成服务器私钥和证书签名请求(CSR),再用根证书对 CSR 进行签名。

生成服务器私钥:

openssl genrsa -out server.key 2048

生成 CSR:

openssl req -new -key server.key -out server.csr -config openssl.cnf

注意这里用的还是同一份配置文件,所以 SAN 信息会自动带进 CSR。用根证书签名:

openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 825 -extfile openssl.cnf -extensions req_ext

这里有几个参数值得说明。-CAcreateserial会生成一个序列号文件,用于记录已签发的证书序号。-days 825是服务器证书的有效期,825 天这个数字不是随便定的——主流浏览器对证书有效期有上限要求,超过会被拒绝,825 天是安全的选择。-extfile和-extensions确保 SAN 扩展被正确写入最终证书。

签完之后验证一下服务器证书:

openssl x509 -in server.crt -noout -text | grep -A1 "Subject Alternative Name"

如果能看到你配置的域名和 IP,说明 SAN 写入成功。

3.4 文件权限与安全加固

证书生成完,权限设置是很多人忽略的一步。私钥文件如果被其他用户读到,等于把身份凭证拱手让人。建议这样设置:

chmod 600 ca.key server.key chmod 644 ca.crt server.crt

600意味着只有文件所有者能读写,其他用户无任何权限。644让证书文件可被读取,因为证书本身是公开信息,需要被服务进程读取。

另外,根私钥ca.key在签发完所有服务器证书后,最好离线保存或者干脆从服务器上移走。日常运行的服务只需要server.key和server.crt,根私钥不应该长期放在对外提供服务的机器上。

4. 把证书用起来:服务端配置与客户端信任

4.1 Nginx 与 Apache 的配置差异

证书生成好了,得让服务用起来。以 Nginx 为例,在 server 块里加上:

server { listen 443 ssl; server_name myinternal.local; ssl_certificate /path/to/server.crt; ssl_certificate_key /path/to/server.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; }

Apache 的配置逻辑类似,在 VirtualHost 里指定:

SSLEngine on SSLCertificateFile /path/to/server.crt SSLCertificateKeyFile /path/to/server.key

两者都需要注意:证书路径要写绝对路径,相对路径在不同工作目录下会出问题。另外如果服务器证书和中间证书是分开的,Nginx 需要用ssl_certificate指向包含完整链的文件,Apache 则用SSLCertificateChainFile单独指定。自签场景下通常只有根证书和服务器证书两级,把服务器证书配好即可。

4.2 让系统信任你的根证书

服务端配好了,客户端访问还是会警告,因为客户端不认识你的根证书。解决办法是把ca.crt导入到客户端的信任库。

在 Linux 上,不同发行版路径不同。Debian/Ubuntu 系:

sudo cp ca.crt /usr/local/share/ca-certificates/myinternal.crt sudo update-ca-certificates

RHEL/CentOS 系:

sudo cp ca.crt /etc/pki/ca-trust/source/anchors/myinternal.crt sudo update-ca-trust

导入后,用 curl 测试:

curl -v https://myinternal.local

如果不再报证书错误,说明信任链打通了。浏览器方面,Chrome 和 Firefox 有各自的证书管理界面,需要手动导入ca.crt并勾选"信任此证书用于识别网站"。

提示:导入根证书时,务必确认勾选的是"信任用于网站识别",而不是仅仅"信任用于邮件"之类的选项,否则浏览器依然会拦截。

4.3 用 curl 和 openssl s_client 做链路验证

配置完成后,别急着打开浏览器,先用命令行工具验证,能快速定位问题出在哪一层。

用openssl s_client检查服务端证书链:

openssl s_client -connect myinternal.local:443 -CAfile ca.crt -servername myinternal.local

输出里重点看Verify return code,如果是0 (ok),说明证书链验证通过。如果报unable to verify the first certificate,通常是根证书没被正确识别。如果报certificate verify failed,可能是 SAN 不匹配或证书过期。

用 curl 测试完整请求:

curl --cacert ca.crt https://myinternal.local/api/health

--cacert参数指定用哪个根证书来验证,适合在还没导入系统信任库时做临时测试。如果这条命令能通,说明证书本身没问题,剩下的就是客户端信任库的事了。

5. 那些年我踩过的证书坑

5.1 SAN 缺失导致的"证书不匹配"

这是我遇到最多的坑。早期教程里大量使用openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365这种一条命令生成自签证书的方式,完全不涉及 SAN。在旧版浏览器上能用,但新版 Chrome 直接报NET::ERR_CERT_COMMON_NAME_INVALID。

排查方法很简单,用这条命令看证书里有没有 SAN:

openssl x509 -in server.crt -noout -text | grep -i "alternative"

如果没有输出,就是 SAN 缺失。解决办法就是回到第 3 节的流程,用配置文件方式重新生成,确保subjectAltName被写入。

5.2 时间不同步引发的"证书尚未生效"

有一次在内网测试环境,证书怎么都验证不过,报错说"certificate is not yet valid"。查了半天发现是那台测试机的系统时间比实际时间慢了两天,而证书的生效时间是从签发那一刻算起的。系统时间落后于证书生效时间,自然就报"尚未生效"。

这个坑的教训是:内网机器如果没配 NTP 时间同步,系统时间漂移是常有的事。签发证书前先date看一下时间,确保服务器和客户端时间大致一致。如果时间差得离谱,先修时间再排查证书。

5.3 私钥权限过宽被安全扫描告警

有次做完内网部署,安全扫描工具报了一堆"私钥文件权限过于宽松"的告警。原因是部署脚本里用了cp复制文件,而cp默认会保留源文件的权限,源文件当时是 644,复制过去还是 644。私钥文件 644 意味着同组用户和其他用户都能读,这在多用户系统上是实打实的风险。

修复很简单,部署脚本里加一行chmod 600就行。但更重要的是养成习惯:任何涉及私钥的操作,结束后都检查一下权限。可以用ls -l确认,也可以用stat -c "%a %n" *.key批量查看。

5.4 证书过期后的连锁反应

自签证书同样会过期。服务器证书设了 825 天,根证书设了 3650 天,看起来很久,但时间过得比想象中快。证书一过期,所有依赖它的服务全部报错,而且报错信息往往很隐晦,比如"SSL handshake failed"或者"connection reset",不一定会直接告诉你证书过期了。

我的做法是在日历上设一个提醒,提前一个月续签。续签流程和首次签发一样,只是不需要重新生成根证书,直接用现有的ca.key和ca.crt签新的服务器证书即可。签完后替换服务端证书文件,重启服务,客户端不需要做任何改动,因为根证书没变。

6. 自签证书的边界与替代思路

自签证书最大的价值在于"可控"和"零成本",但它的局限也很明显。首先,任何客户端要访问你的服务,都必须手动导入根证书,这在内部小范围使用没问题,一旦面向大量外部用户就完全不现实。其次,自签证书没有吊销机制,如果私钥泄露,你无法像商业 CA 那样发布吊销列表,只能重新签发并通知所有客户端更新。再者,部分移动端应用和 IoT 设备对自签证书的支持很差,可能根本不给你导入根证书的入口。

如果你的场景是内网服务、开发测试、设备间通信,自签证书完全够用。如果是对公网提供服务的生产环境,还是应该使用受信任的 CA 签发的证书。现在也有一些自动化工具可以简化证书申请和续期流程,但对于内网场景来说,OpenSSL 自签依然是最直接、最不依赖外部条件的方案。

我个人在实际操作中的体会是:把根证书的生成和服务器证书的签发分成两个独立脚本,根证书只生成一次并妥善保管,服务器证书按需签发。这样既保证了根证书的稳定性,又让日常的证书签发变成一条命令的事。另外,把 OpenSSL 配置文件纳入版本管理,每次签发都用同一份配置,能避免很多"这次生成的证书和上次不一样"的困惑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询