做内网项目的HTTPS改造时,最让人纠结的往往不是Nginx配置,也不是Tomcat那堆参数,而是“证书到底从哪来”。外网服务器一条certbot就能签下Let's Encrypt证书,内网机器没有公网域名、没有入站流量,部门内部系统要加密访问,却总是卡在申请SSL证书这一步。这篇文章我打算把内网环境下常见的申请与落地方式从头梳理一遍,涵盖自签名、私有CA、公共CA免费证书三条路线,并给出Nginx、Tomcat等场景的直接配置参考。如果你正在为内网系统“上HTTPS”发愁,或者想搞清楚为什么浏览器总提示不安全,这篇内容应该能帮你把思路捋顺。
1. 内网HTTPS为什么难,难在哪
1.1 内网系统上HTTPS的真实价值
很多人觉得内网系统不暴露在公网,何必多此一举上HTTPS,这个想法在过去还算成立,现在越来越站不住脚。内网不等于安全,办公Wi-Fi可以被伪造,交换机端口可能被接入不明设备,内网抓包工具随手就能用。明文HTTP下,账号密码、接口参数、业务数据在网络里都是裸奔的,只要有人在内网链路做一次抓包,隐私和敏感信息就全部暴露了。
另一个理由是移动端强制要求。现在很多内网系统要配企业微信、钉钉、小程序或者自研App,这些平台在Android和iOS上对HTTP接口限制非常严格,明文流量容易直接被系统拦截或弹安全警告。如果你不想被这些平台的安全策略反复折腾,尽早把内网服务接入HTTPS是更省事的选择。
再加上现在很多企业做等保、做内部安全审计,网络流量加密是明文写进要求的。内网服务不加密,审计的时候很难解释。所以内网环境下把HTTPS落实好,不是“锦上添花”,而是规范化管理里绕不开的一步。
1.2 证书在HTTPS握手中的作用,以及两大拦路虎
要搞定内网HTTPS,得先理解证书在HTTPS里干什么。简单说,浏览器和服务器建立加密连接之前,服务器需要出示一张数字证书,里面包含站点身份信息、公钥、有效期,以及签发该证书的CA(证书颁发机构)。浏览器收到证书后,会依次检查三件事:证书链是否符合系统信任的CA、域名是否和证书中的SAN(Subject Alternative Name)匹配、证书是否在有效期内。全部通过才会继续建立加密会话。
这也是内网HTTPS最常见的两大拦路虎。第一个是“证书不受信任”,自签名证书或者私有CA签发的证书不在浏览器内置信任列表里,浏览器就直接报NET::ERR_CERT_AUTHORITY_INVALID。第二个是“域名和证书不匹配”,很多人给IP做了证书,用户却通过主机名访问,或者证书里只写了Common Name没写SAN,浏览器照样不认。
搞明白这两点之后,内网SSL证书的申请思路就清晰了:要么想办法让客户端信任我们自己的CA,要么申请一张证书,让证书里的信息能覆盖用户实际访问的域名或IP。
1.3 内网申请SSL证书的三条路线选择
针对不同场景,内网环境下申请SSL证书主要有三条路线:
| 路线 | 信任效果 | 部署复杂度 | 典型场景 | 维护成本 |
|---|---|---|---|---|
| 自签名证书 | 单机可信,其他机器需手动导入 | 低 | 临时调试、个人测试 | 每台机器都要导入 |
| 私有CA统一签发 | 导入根证书后全网可信 | 中等 | 公司内部正式系统、多台服务器 | 需维护CA根证书 |
| 公共CA免费证书 | 浏览器原生信任 | 中等 | 有公网可解析域名的内网服务 | 证书有效期短,需定期续期 |
公共CA证书在浏览器里最“权威”,但正因为权威,申请门槛也高。CA需要验证域名所有权,而内网域名比如gitlab.internal.local在公网DNS无法解析,CA验证根本过不去。纯IP地址的证书很多公共CA也不再签发。所以很多内网环境最后都选择了私有CA或自签名路线,既能完全掌控证书签发,又不受公网域名限制。
2. 不同证书申请方式的完整实操
2.1 自签名证书:一条命令生成,但要带SAN
如果你只是临时给某个内部服务开HTTPS,自签名是最快的方式。直接用openssl生成一张带SAN的证书:
openssl req -x509 -newkey rsa:2048 -sha256 -nodes \ -days 825 \ -keyout server.key \ -out server.crt \ -subj "/CN=192.168.1.50" \ -addext "subjectAltName=IP:192.168.1.50,DNS:dev.internal.example.com"解释一下关键参数:-x509表示直接生成自签名证书而不是CSR;-nodes表示私钥不加密,这样Nginx启动时不用频繁输密码;-days 825约等于两年多一点,这是考虑到部分系统对超过825天的新证书会有兼容性提示,内网环境也建议别把有效期拉太长;-addext "subjectAltName=..."把IP和域名都写进SAN,这一步非常关键,很多人生成证书后浏览器报域名不匹配,多半就是漏了SAN。
生成之后,服务端配置用server.key和server.crt,但其他机器访问时浏览器仍会提示不安全。要让浏览器信任,需要把server.crt导入到访问端系统的“受信任的根证书颁发机构”。Windows下双击证书文件,选择“安装证书”,存储位置选“本地计算机”,然后手动放到“受信任的根证书颁发机构”;Linux(CentOS/RHEL系)执行:
cp server.crt /etc/pki/ca-trust/source/anchors/ update-ca-trustUbuntu/Debian系则是放到/usr/local/share/ca-certificates/后执行update-ca-certificates。
自签名的坑在于证书分发。测试环境只有一两台机器还好,一旦客户端数量上来,每台机器手动导入一遍很崩溃,而且换证书时又得重来。所以自签名只建议应急或测试用。
2.2 mkcert:内网开发神器,三分钟让本机信任
如果你主要是在本机或少量开发环境调试,mkcert比手动openssl省心很多。它本质上是帮你自动生成一个本地CA根证书并安装到系统信任库,然后用这个CA去签发目标域名/IP的证书,整个过程命令化操作:
# 安装mkcert后先初始化本地CA mkcert -install # 为指定域名和IP签发证书 mkcert 192.168.1.50 dev.internal.example.com执行完当前目录会生成192.168.1.50+1.pem和192.168.1.50+1-key.pem,直接丢给Nginx用即可。mkcert -install把本地CA根证书装进了系统信任区,所以本机浏览器访问时不会再报错。如果其他同事也要访问,只需要把mkcert生成的根证书(默认路径在~/Library/Application Support/mkcert/或~/.local/share/mkcert/下可以找到)分发给他们导入系统信任区即可。
mkcert非常轻量,适合开发联调场景,但它生成的CA严格说只在你的团队内部有效,公司正式系统不建议长期依赖。
2.3 自建私有CA:给整个内网统一签发证书
当内网机器多起来,自签名就熬不住了。此时应该自建一个私有CA,用它统一给所有服务器签发证书。大家只要信任一次根证书,之后由这个根CA签发的任何服务器证书都会被浏览器认可。这个思路和公共CA完全一致,只不过信任的根换成了你自己。
第一步,生成CA根证书:
openssl genrsa -out ca.key 4096 openssl req -x509 -new -key ca.key -days 3650 -out ca.crt -subj "/CN=Company Internal CA"ca.key是CA私钥,必须妥善保管,最好离线备份加密存储。ca.crt是根证书,将来要分发到所有客户端。
第二步,为某台服务器生成私钥和证书签名请求(CSR):
openssl genrsa -out gitlab.key 2048 openssl req -new -key gitlab.key -out gitlab.csr \ -subj "/CN=gitlab.internal.local" \ -addext "subjectAltName=DNS:gitlab.internal.local,IP:10.0.0.5"第三步,用CA签发服务器证书:
openssl x509 -req -in gitlab.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -out gitlab.crt -days 825 -sha256 \ -extfile <(printf "subjectAltName=DNS:gitlab.internal.local,IP:10.0.0.5")签发后,gitlab.crt就是服务器证书,gitlab.key是私钥,ca.crt用于给客户端做信任验证。注意签发时同样要写SAN,否则部署后域名/IP校验依然会失败。
第四步,把ca.crt分发到所有访问端。这一步最费人工,企业环境如果有AD域,可以通过组策略批量下发;没有域控的小团队,最少也要写一份文档告诉大家如何导入。分发范围越广,内网后续上HTTPS就越顺畅。
2.4 申请公共CA免费证书:阿里云/Let's Encrypt可行但有限制
再来说说公共CA免费证书。很多人盯着阿里云、腾讯云的免费SSL证书,或者在考虑Let's Encrypt,这些方案在内网环境能不能用?能,但有前置条件:证书里的域名必须能在公网被解析验证,否则CA核验域名所有权时根本找不到目标。
以阿里云免费证书为例,操作路径大致是:控制台搜索“数字证书管理服务”,进入SSL证书申请页面,选择一个免费证书规格,填写你要保护的主域名(比如gitlab.example.com),验证方式选择DNS。如果域名托管在阿里云DNS,通常可以一键自动添加TXT解析记录,审核几分钟到几小时不等,签发后按需下载Nginx、Tomcat格式的证书文件。
这里有个容易误解的点:证书绑定的域名在公网需要能解析,并不代表流量必须真的走公网。你完全可以把example.com的A记录解析到一条测试公网IP或者不对外提供业务,然后企业内部DNS把gitlab.example.com解析到内网服务器IP 10.0.0.5。客户端在内网访问gitlab.example.com时,实际流量完全走内网,但浏览器校验证书时,证书里写的域名和访问域名一致,就能通过。这种方式既拿到了浏览器原生信任的证书,又保住了内网低延迟访问。
Let's Encrypt也同理。用acme.sh配合DNS API,比如阿里云DNS:
curl https://get.acme.sh | sh -s email=you@example.com acme.sh --issue --dns dns_ali -d gitlab.example.com前提依然是这个域名在公网DNS有权威解析,并且你持有云解析API密钥。纯离线内网、完全不能访问外网的环境,这条路线走不通,只能回头用私有CA。
还要强调一点:公共CA现在基本不签纯IP证书,免费证书尤其如此。所以如果你只有一个内网IP,没有域名,别指望阿里云免费证书能帮你,老老实实走自签名或私有CA。
2.5 证书续期与到期检查
公共CA证书有效期普遍短,Let's Encrypt是90天,阿里云个人免费证书现在也缩短到了3个月左右,必须把续期当成周期任务。常见的检查命令:
# 查看证书过期时间 openssl x509 -enddate -noout -in server.crt # 查看证书内容 openssl x509 -in server.crt -text -noout如果用了acme.sh自动签发的Let's Encrypt证书,可以开启自动续期:
acme.sh --install-cert -d gitlab.example.com \ --key-file /etc/nginx/ssl/gitlab.key \ --fullchain-file /etc/nginx/ssl/gitlab.crt \ --reloadcmd "nginx -s reload"脚本会自动在到期前续期并重载Nginx。私有CA签发的证书有效期通常可以设得更长,但依然要有到期检查机制,建议写个简单脚本扫描证书目录,配合crontab每周执行,到期前30天邮件或企业微信告警。
3. 在内网常见服务上部署HTTPS
3.1 Nginx部署HTTPS:证书链别拼错
拿到证书之后,配置Nginx其实是最简单的。这里给一个可以直接用的server块:
server { listen 443 ssl; http2 on; server_name gitlab.internal.local; ssl_certificate /etc/nginx/ssl/gitlab.crt; ssl_certificate_key /etc/nginx/ssl/gitlab.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:...'; }如果你是申请公共CA证书,下载的压缩包里面往往有server.crt和ca.crt,需要把两个文件拼接成fullchain:
cat server.crt ca.crt > fullchain.crt配置里就用fullchain.crt。证书链不完整会导致部分客户端校验失败,这是Nginx部署里非常容易踩的坑。
还有一点提醒:内网环境下不要把HTTP到HTTPS的跳转做得太绝对。如果有服务暂时还没上证书,强制跳转会直接断掉。稳妥做法是先让所有域名都覆盖证书,再考虑统一跳转。
3.2 Tomcat部署HTTPS:cer转pfx再上
Tomcat部署HTTPS比Nginx绕一些,因为Tomcat默认偏好PKCS12/JKS格式的密钥库,而很多渠道下发的证书是PEM/CRT格式。热搜词里“cer 转 tomcat ssl 证书 pfx”就是典型的现实需求。
假设你手里有server.crt、server.key和ca-chain.pem,转成PFX的完整命令:
# 如果cer是DER格式,先转成PEM openssl x509 -inform DER -in server.cer -out server.pem # 把PEM证书和私钥打包为PFX,certfile可以附加中间证书 openssl pkcs12 -export \ -in server.pem \ -inkey server.key \ -out server.pfx \ -name tomcat \ -certfile ca-chain.pem转换时会要求设置PFX的导出密码,这个密码要记住,一会配置里要用。然后把server.pfx丢到Tomcat的conf目录,修改conf/server.xml:
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" maxThreads="150" SSLEnabled="true"> <SSLHostConfig> <Certificate certificateKeystoreFile="conf/server.pfx" certificateKeystorePassword="你的密码" type="RSA"/> </SSLHostConfig> </Connector>注意Tomcat 8.5+使用<SSLHostConfig>嵌套结构,老教程里直接用keystoreFile属性的写法在新版本已经逐渐弃用。PFX的好处是私钥和证书在一个文件里,Tomcat读起来省事,你要真想用JKS也行,但PFX通用性更好,跨平台迁移也方便。
3.3 网关统一终止HTTPS与Spring Boot内置配置
如果内网服务是微服务架构,不建议每个业务服务都自己配证书。更好的做法是在Nginx网关或负载入口统一终止HTTPS,后面到业务服务走内网HTTP,降低证书维护面和私钥暴露风险。这也是我实际项目里推荐的做法,证书统一由网关管理,业务方无感知。
当然,单机应用不需要网关分层时,Spring Boot直接内置HTTPS也很方便。把证书和私钥转成PKCS12格式后配置:
server: port: 8443 ssl: enabled: true key-store: classpath:server.pfx key-store-type: PKCS12 key-store-password: yourpassword这种方式适合内部小工具或管理系统,部署简单,不需要额外装Nginx。但微服务和前后端分离项目,统一网关始终是更可控的方案。
4. 内网HTTPS的常见问题排查
4.1 浏览器报“不安全”的排查顺序
浏览器提示不安全,原因千奇百怪,但绝大多数逃不开下面几类。建议按照顺序排查:
- 证书链是否完整。用命令查:
openssl s_client -connect gitlab.internal.local:443 -showcerts openssl s_client -connect gitlab.internal.local:443 -servername gitlab.internal.local看输出里是否有完整的“Certificate chain”,如果只有一层或缺少中间证书,就是fullchain没拼对。
根证书是否已经导入访问端。私有CA签发的证书,客户端必须信任对应的根证书;公共CA一般没这个问题,但如果你是从某台内网机器复制了不全的链,也会报authority invalid。
证书域名和用户访问的地址是否匹配。证书SAN里写了gitlab.internal.local,用户却用https://10.0.0.5访问,必然报错。要么给IP补一张带IP SAN的证书,要么让客户端走域名访问。
证书是否在有效期内,以及系统时间是否准确。内网机器时间漂移非常常见,机器时间比证书有效期差了几天甚至几年,浏览器直接判证书无效。先
date看时间,再openssl x509 -dates看证书有效期,对比一下。私钥和证书是否匹配。如果部署时搞混了多套证书,Nginx可能启动失败,Tomcat可能报错,用这条命令核对:
openssl x509 -noout -modulus -in server.crt | openssl md5 openssl rsa -noout -modulus -in server.key | openssl md5两个md5一致才说明匹配。
4.2 访问IP和证书域名不一致的处理
内网环境很多用户习惯直接输IP访问,但证书签发时域名是主体。处理方式有两条路:一是给证书SAN里同时加上IP地址,像前面openssl命令里-addext "subjectAltName=IP:10.0.0.5,DNS:gitlab.internal.local"那样;二是统一约定域名访问,通过内网DNS解析或客户端hosts映射把域名指到对应IP。
从长期维护看,我更推荐统一用域名。因为IP会变,证书里IP多了以后迁移麻烦;而域名只是一个解析记录,改映射比换证书成本低太多。如果公司有内网DNS,优先把内部服务域名规划起来。
4.3 老客户端打不开HTTPS页面的兼容问题
内网环境经常藏着Windows 7、老版本浏览器、旧版JDK客户端。这些老客户端对TLS 1.3甚至TLS 1.2的支持都不好,而你如果把ssl_protocols只配了TLSv1.2和TLSv1.3,老客户端可能直接握手失败。
这里有个取舍问题:安全要求上,TLS 1.0和1.1早该停用;但内网存量设备太多时,直接停用会影响业务。我的做法是默认配置TLSv1.2和TLSv1.3,特殊老设备单独开一个兼容端口跑TLS 1.0/1.1,并限定访问来源。这样既保证绝大多数流量走强加密,又不至于把老设备一棍子打死。
4.4 内网证书管理最容易忽略的五个坑
我总结一下这几年在内网摸爬滚打遇到的高频问题,每条都踩过不止一次:
- 系统时间不同步。内网机器不做NTP校时,证书明明是刚签的,浏览器却提示过期或“证书无效”。给内网所有服务器和客户端统一配置好NTP同步,这个坑能避开一大半。
- 根证书只装服务器不装客户端。私有CA方案里,根证书必须下发到访问端,不是只放在签发服务器上就行。
- 私钥权限不设防。server.key一旦泄露,等于HTTPS裸奔。务必把私钥设为仅root/管理员可读,不要放进代码仓库。
- 证书链缺中间证书。证书链不完整在PC浏览器上有时不报错,但App、Java、curl、微信小程序里经常校验失败。多端测试很重要。
- 只图方便不写SAN。新证书签发时忘记加SAN,最后部署完发现浏览器依然提示域名不匹配,只能重新签发。
另外,如果内网有Java程序调用HTTPS接口,遇到PKIX path building failed也很常见,本质是JVM没有信任你的私有CA或证书链不完整。要么把根证书导入JAVA_HOME/lib/security/cacerts,要么在代码里自定义信任库。这个点很多测试同学第一次遇到时特别懵。
最后再分享一个我自己的习惯
折腾完这些,我现在的流程基本固定下来了:开发环境用mkcert,几十秒出证书,本机全信任;测试和正式内网环境直接搭一个私有CA,根证书通过公司文档库统一分发,之后所有内网服务都用它签发;只有遇到“必须让公网生态比如小程序回调地址用HTTPS”的需求,才去申请公共CA免费证书,并且用acme.sh这类工具做好自动续期。
每次签完证书,我都会立刻把证书文件备份到一个固定目录,同时记录域名、IP、到期时间、私钥路径。内网证书一旦多了,光靠脑子记住每张证书什么时候到期,早晚要出问题。如果你也在做内网HTTPS改造,我建议先别急着部署,花十分钟想清楚“信任链”怎么建立,再动手操作,后面能省下大量排查时间。