用 Nginx 在公网上搭建加密数据通道
推荐关注
以下文章来源Java后端栈,回复”面试“获面试宝典
哈喽,各位新来的小伙伴们,大家好!由于公众号做了改版,为了保证公众号的资源能准时推送到你手里,大家记得将后端君的公众号 加星标置顶 ,在此真诚的表示感谢~
编辑:后端妹 | 来源:laixintao
链接:kawabangga.com/posts/4649
正文
CA 的全称是 Certification Authority, 是一个第三方机构,在上述加密的流程中,扮演的角色同时被访客和网站所信任。
网站需要去 CA 申请证书,而 CA 要对自己颁发(签名)的证书负责,即确保证书颁发给了对方,颁发证书之前要验证你是你。申请证书的时候,CA 一般会要求你完成一个 Challenge 来证明身份,比如,要求你将某个 URL 返回特定内容,或者要求你将 DNS 的某个 text
record 返回特定内容来证明你的确拥有此域名(详见 validation standards[3])。只有你证明了你是你,CA 才会签证书给你。
访客是怎么验证证书的呢?这就用到了上文提到的 功能 2:“私钥可以对数据进行签名,公钥拿到数据之后可以验证数据是否由私钥的所有者签名的。” CA 也有自己的一套私钥公钥,CA 使用私钥对网站的证书进行签名(担保),访客拿到网站的证书之后,使用 CA 的公钥校验签名即可验证这个“担保”的有效性。
那么 CA 的公钥是怎么来的呢?答案是直接存储在客户端的。Linux 一般存储在 /etc/ssl/certs
。由此可见,CA 列表更新通常意味着要升级系统,一个新的 CA 被广泛接受是一个漫长的过程。新 CA 签发的证书可能有一些老旧的系统依然不信任。比如 letsencrypt 的 CA[4],之前就是使用交叉签名的方式工作,即已有的 CA 为我做担保,我可以给其他的网站签发证书。这也是中级证书的工作方式。每天有这么多网站要申请证书,CA 怎么签发的过来呢?于是 CA 就给很多中级证书签名,中级证书给网站签名。这就是“信任链”。访客既然信任 CA,也就信任 CA 签发的中级,也就信任中级签发的证书。
被信任很漫长,被不信任很简单。
CA (以及中级证书机构)有着非常大的权利。举例,CA 假如给图谋不轨的人签发了 Google 的证书,那么攻击者就可以冒充 Google。即使 Google 和这个 CA 并没有任何业务往来,但是自己的用户还是被这个 CA 伤害了。所以 CA 必须做好自己的义务:
保护自己的私钥不被泄漏; 做好验证证书申请者身份的义务; 如果 (2) 有了疏忽,对于错误签发的证书要及时吊销[5];
案例:赛门铁克证书占了活跃证书的 30% – 45%(当时[6]),但是被 Google 发现其错误颁发了 3 万个证书,发现后却不作为。因此逐步在后续的 Chrome 版本中吊销了赛门铁克的证书[7]。
案例 2:let’sencrypt 今年 1 月份发现自己的 TLS-ALPN-01 chanllege 有问题,于是按照规定,在 5 天后吊销了这期间通过 TLS-ALPN-01 颁发的所有证书[8]。
说道这里我想继续跑一个题。我以前给博客部署证书的时候(2017 年[9])就想:CA 给我发一个证书居然要收我的钱?这个不是零成本的东西吗?他们想发多少就发多少。看到现在读者应该明白了,这并不是一个零成本的事情:签发证书的验证服务需要花钱,而 CA Root key 的保护要花更多的钱。整个 CA 公司(组织)的核心资产就是一个 key,如果这个 key 暴露了,后果不堪设想。所以,一个无比重要却要一直使用的 key 在一个上千万人的组织里怎么被使用而不暴露给任何一个人呢?这是要花很多钱的。Root key 的生成会有一个仪式(Key ceremony[10]),全程录像,有 20 多个不同组织的代表会现场参加并监督,会有 3000 多个人观看实时录像,确保 key 的生成是标准流程。在 Root key 的保存和使用上,Root key 只会签中级 CA,以减少使用次数以及 Root key 需要被 revoke(代价太大)的风险。Root Key 保存在一个特殊的硬件中(HSM[11], Hardware security module),完全离线保存,HSM 也放在特殊的机房中,7×24 有人看守,并离线录像,机房有 Class 5 Alarm System[12],有多把锁,没有一个人可以单独进入。使用这个 Root Key 必须物理上进入这个机房,使用过程全程录像,并且记录使用过程,如果有问题可以很快地将 Root Key 签的内容 revoke。这里有一个视频介绍 Key Signing Ceremony[13],非常有趣。所以说 CA 机构并不是一个摇钱树,Let’s Encrypt 这种组织简直就是慈善机构。
以上就是 TLS,证书,CA 大致的工作原理,稍稍有些跑题,有了这些知识我们就可以利用 TLS 来建立一个加密的数据通道了。后续几乎都是实际的操作。笔者对这部分也不是精通,如果有错误,欢迎指出。
对应用透明的加密通道的方案
背景
上文是通过网站部署 HTTPS 来讲的 TLS 的工作原理。其实网站部署 HTTPS 还算是比较简单:你只需要找一个 CA,申请证书,完成 CA 的验证,部署证书,就可以了。
现在要解决的问题更加复杂一些:我们的两个组件之间是通过自己研发的协议通讯(基于 TCP),现在要分别部署在两个机房,通过公网进行通讯。另外,搜索公众号顶级算法后台回复“算法”,获取一份惊喜礼包。
我们的方案要对通讯的两边做好安全防护:
数据要进行加密传输; 要对两边做身份验证,比如 A 向 B 发起连接,A 要验证 B 的身份,B 也要验证 A 的身份; 最好对于应用来说透明,即应用完全不修改代码,依然按照原来的方式工作,但是我们将中间的流量进行加密;
mTLS
mTLS 的全称是 Mutual TLS. 即双向的 TLS 验证。HTTPS 只是访客验证了网站的身份,网站并没有验证访客的身份。其实要验证也是可以的,网站发送证书之后可以跟访客说:“现在该轮到你出示你的证书了”。如果访客不能提供有效的证书,网站可以拒绝服务。
其实,ssh 方式就是一个双向验证的过程。我们都知道通过 ssh key 登录 server 的时候,需要让 server 信任你的 key(即将你的 pubkey 放到 server 上去)。但是还有一个过程容易被忽略掉,在第一次通过 ssh 连接服务器的时候,ssh 客户端会给你展示 server 的 pubkey,问你是否信任。如果之后这个 key 变了,说明有可能你连接到的并不是目的服务器。另外,搜索公众号顶级科技后台回复“API”,获取一份惊喜礼包。
如果之后这个 key 变了,ssh 客户端就会拒绝连接。
Git 也是通过走 ssh 协议的,所以也是一个双向认证。你在使用 Github 的时候要互相信任对方:
Github 信任你的方式是:你将自己的 pubkey 上传到 Github (设置,profile,keys) 你信任 Github 的方式是:Github 将自己的 pubkey 公布在网上[14]。
解决方案
为了实现对应用透明的加密通讯,我们在两个机房各搭建一个 Nginx,这里两个 Nginx 之间通过 mTLS 相互认证对方。应用将请求明文发给同机房的 Nginx,然后 Nginx 负责加密发给对方。对于应用来说,对方机房的组件就如同和自己工作在相同机房一样。最终搭建起来如下图所示。
搭建过程
因为用 HTTP 流量来搭建,相关的工具和日志会更友好一些。所以我们会先用 HTTP 将这个通道搭建起来,然后换成 tcp steam。
准备证书
我们一共需要两套证书,一套给 Client,一套给 Server. 因为我们这里主要要解决的问题内部互相信任的问题,不需要开给外面的用户,所以这里我们采用 self signed certificate. 即,我们自己做 CA,给自己签发证书。自签发证书的好处是很灵活,方便,坏处是有一些安全隐患[15](毕竟不像权威机构那样专业)。所以我把这个过程写在博客上,请大家帮忙看看流程有没有问题。
首先我们创建一个 CA 的 key,即私钥。CA 的 key 最好给一个密码保护,每次使用这个 CA 签发证书的时候,都需要输入密码。
生成 key 的命令:
$ openssl genrsa -des3 -out ca.key 4096
输出(其中按照提示输入密码):
Generating RSA private key, 4096 bit long modulus (2 primes)
.............................................................++++
....................................................................................................................................................................................++++
e is 65537 (0x010001)
Enter pass phrase for ca.key:<passphrase>
Verifying - Enter pass phrase for ca.key:<passphrase>
命令的解释:
openssl
: cert 和 key 相关的操作我们都用 openssl 来完成;genrsa
: 生成 RSA 私钥;-des3
: 生成的 key,使用des3
进行加密,如果不加这个参数,就不会提示让你输入密码;4096
: 生成 key 的长度;
这里我们假设所使用的密码是 hello
.
然后我们来生成 CA 的公钥部分,即证书。
$ openssl req -new -x509 -days 365 -key ca.key -out ca.crt
这时会询问你一些信息,比如地区,组织名字之类的。其中,Organization Name 和 Common Name 需要留意。CA 的这一步填什么都可以。Common Name 又简称 CN,就是证书签发给哪一个域名(也可以是 IP)的意思。
输出会是如下所示:
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:CertAuth
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:
命令的解释:
req
: 创建证书请求;-new
: 产生新的证书;-x509
: 直接使用 x509 产生新的自签名证书,如果不加这个参数,会产生一个“证书签名请求”而不是一个证书。-days 365
: 证书 1 年之后过期,也可以省略这个参数,设置为永不过期;key
: 创建公共证书的私钥,会被提示输入私钥的密码;-out
: 生成的证书。
到这里,我们有了一对 CA 证书,ca.key
和 ca.crt
两个文件。接下来申请 server 端的证书。
Server 端证书依然是先生成一个 key,这里就不需要密码保护了:
$ openssl genrsa -out server.key 4096
然后这里下一步不是直接生成证书,而是生成一个证书请求。但是那些问题依然是要回答一遍的。
$ openssl req -new -key server.key -out server.csr
回答问题的时候要注意两个地方:
Organization Name: 不能和 CA 的一样; Common Name: 必须要写一个,可以写一个不存在的域名,比如 proxy.example.com
。否则,会有错误:“* SSL: unable to obtain common name from peer certificate”。
否则证书无法使用。
到这里其实也可以看出,CA 的证书和其他的证书没有什么不同,也是一个普通的证书而已。
这个 .csr
文件是 Ceritifcate Signing Request,即请求签名。接下来我们使用我们的 CA 给这个 Server 证书签名(作担保!)。
$ openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt
这个命令需要输入 CA key 的密码,就是刚刚说的 hello
。
命令的解释:
x509
: 公有证书的标准格式;-CA
: 使用 CA 对其签名;-CAkey
: CA key(没有这个岂不是人人可以用 CA 证书签名了?);-set_serial 01
: 签发的序列号,如果证书有过期时间的话,过期之后,可以直接用这个.csr
修改序列号重新签一个,不需要重新生成.csr
文件;
如此,就得到了 server.crt
文件。扩展:远程控制系统
我们可以使用这条命令验证生成的证书是 ok 的:
$ openssl verify -verbose -CAfile ca.crt server.crt
server.crt: OK
重复此流程再签发一个 client 端的证书。
结束后,我们有以下内容:
ca.key
ca.crt
CA 的密码,需要保存 server.key
server.crt
server.csr
: 部署不需要用到,可以只保存在安全的地方即可;Server 证书签发序列:只保存即可; client.key
client.crt
client.csr
: 部署不需要用到,可以只保存在安全的地方即可;Client 证书签发序列:只保存即可;
然后接下来就可以部署起来了。
搭建远程 Server 端的 Nginx
为了模拟转发到后端应用的场景,这里的 Nginx 不使用静态文件,而是用一个 fastapi 写的样例程序来做后端:
from typing import Optional
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
启动的命令是:
$ uvicorn app:app
程序默认会运行在 8000 端口。
然后修改 Nginx 的配置,nginx.conf
不变,我们只修改 default
的配置,将 default
rename 成 remote_server
,然后修改成成如下配置:
server {
listen 443 default_server ssl;
listen [::]:443 default_server ssl;
server_name _;
ssl_certificate /home/vagrant/cert/server.crt;
ssl_certificate_key /home/vagrant/cert/server.key;
location / {
proxy_pass http://127.0.0.1:8000;
}
}
这就是一个很简单的 Nginx HTTPS 配置,证书配置上了我们刚刚自己签发的证书:
ssl_certificate
: 告诉 Nginx 使用哪一个公有证书;ssl_certificate_key
: 此证书对用的私钥是什么,服务器需要有私钥才能工作。
证书已经配置好了。这时候我们去 cURL 443 端口会出现错误:“curl: (60) SSL: unable to obtain common name from peer certificate”,cURL 不信任这个服务器的证书。这是当然了,因为这个证书是我们自己作为 CA 签的。
要正常访问,必须使用 cURL --ca ./ca.cert
来告诉 cURL 我们信任这个 CA (所签发的所有证书)。
另外还要注意的是,记得我们之前的 Server 证书是签发给 proxy.example.com
的吗?我们这里必须要访问这个域名才行。需要这样使用:
$ curl -v https://proxy.example.com --cacert ./ca.crt --connect-to proxy.example.com:443:127.0.0.1:443
--connect-to
的意思是,所有发往这个域名的请求,都直接发给这个 IP。
Client 对 Server 的验证就配置好了,接下来再配置 Server 对 Client 的验证。
我们只需要将上面的配置文件改成如下即可:
server {
listen 443 default_server ssl;
listen [::]:443 default_server ssl;
server_name _;
ssl_certificate /home/vagrant/cert/server.crt;
ssl_certificate_key /home/vagrant/cert/server.key;
ssl_verify_client on;
ssl_client_certificate /home/vagrant/cert/ca.crt;
location / {
proxy_pass http://127.0.0.1:8000;
}
}
添加的内容的含义:
ssl_verify_client
: 需要验证客户端的证书;ssl_client_certificate
: 我们信任这个 CA 所签发的所有证书。
这里有一个小插曲:Nginx 的文档上说,ssl_trusted_certificate 和 ssl_client_certificate 这两个配置效果都是一样的,唯一的区别是 ssl_client_certificate 会将信任的 CA 列表发送给客户端,但是 ssl_trusted_certificate 不会发。发送是合理的[16],因为客户端如果有很多证书,让客户端一个一个去尝试哪一个能建连是没有意义并且很浪费的。ssl_trusted_certificate 的作用是验证 OCSP[17] Response。但是我尝试了 ssl_trusted_certificate,Nginx 会直接 fail 掉语法检查:
The server fails to start with error: nginx: [emerg] no ssl_client_certificate for ssl_verify_clientb
这里发现一个 ticket 询问和我一样的问题:https://trac.nginx.org/nginx/ticket/1902,不过至今没有回复。我以为是 Nginx 版本的 Bug,然后尝试了最新的版本依然是一样的结果。如果读者知道可以指点一下,谢谢。
这样配置之后 reload Nginx,就开启了对客户端的证书验证了。这时候我们继续使用上面那个 cURL,就无法得到响应。另外,搜索公众号前端技术精选后台回复“大礼包”,获取一份惊喜礼包。
<head><title>400 No required SSL certificate was sent</title></head>
Nginx 会要求你提供证书。
如下的 cURL,带上证书,就可以正常拿到响应。
$ curl -v https://proxy.example.com --cacert ./ca.crt --connect-to proxy.example.com:443:127.0.0.1:443 --cert client.crt --key client.key
这样,远端的 Nginx 就配置好了,它会提供证书证明自己的身份,也会要求客户端提供证书进行验证。另外,搜索公众号技术社区后台回复“算法”,获取一份惊喜礼包。
接下来搭建本地的 Nginx,将明文请求加密对接到远端的 Nginx。
搭建本地 Client 端的 Nginx
本地机房开启一个 Nginx,监听 80 端口,转发到远程的 443 端口。
配置如下:
upstream remote{
server 127.0.0.1:443;
}
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
location / {
proxy_pass https://remote;
proxy_ssl_trusted_certificate /home/vagrant/cert/ca.crt;
proxy_ssl_verify on;
proxy_ssl_server_name on;
proxy_ssl_name proxy.example.com;
proxy_ssl_certificate /home/vagrant/crt/client.crt;
proxy_ssl_certificate_key /home/vagrant/cert/client.key;
}
}
这个配置可以分成两部分看,第一部分,是要验证对方的证书:
proxy_ssl_verify: 需要对方提供证书; proxy_ssl_trusted_certificate: 我们只信任这个 CA 签发的所有证书; proxy_ssl_server_name: 不像 cURL 的 --connect-to
选项,这里我们直接指定目标 IP 转发,但是我们使用 SNI[18] 功能来告诉对方我们要连接哪一个 domain,来验证相关 domain 的证书;proxy_ssl_name: 我们需要哪一个 domain 的证书。
然后第二部分是提供自己的证书:
proxy_ssl_certificate: 我的证书; proxy_ssl_certificate_key: 我的私钥,不会发送给对方,只是本地 Nginx 自己使用。
然后就可以 cURL 本地的 80 端口了:
$ curl http://127.0.0.1 -v
* Trying 127.0.0.1:80...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
> GET / HTTP/1.1
> Host: 127.0.0.1
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: nginx/1.18.0 (Ubuntu)
< Date: Wed, 16 Mar 2022 03:49:05 GMT
< Content-Type: application/json
< Content-Length: 17
< Connection: keep-alive
<
* Connection #0 to host 127.0.0.1 left intact
{"Hello":"World"}
可以看到我们从客户端(cURL)发出明文 HTTP 请求,到服务端(fastapi)收到明文 HTTP 请求,两边都不知道中间流量加密过程,但是走公网的部分已经被加密了。就实现了本文开头的需求。
代理 TCP steam
以上是 HTTP 的配置,将其换成 TCP Steam 的代理也很简单,相应的配置修改一下就可以。这里我们以 Redis 服务为例来展示一下配置。扩展:接私活儿
/etc/nginx/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
}
stream {
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Remote Server 的配置:/etc/nginx/sites-enabled/remote_server
server {
listen 443 ssl;
proxy_pass 127.0.0.1:6379;
ssl_certificate /home/vagrant/cert/server.crt;
ssl_certificate_key /home/vagrant/cert/server.key;
ssl_verify_client on;
ssl_client_certificate /home/vagrant/cert/ca.crt;
}
local_client 的配置:/etc/nginx/sites-enabled/client_server
upstream remote{
server 127.0.0.1:443;
}
server {
listen 80;
listen [::]:80;
proxy_pass remote;
proxy_ssl_trusted_certificate /home/vagrant/cert/ca.crt;
proxy_ssl_verify on;
proxy_ssl_server_name on;
proxy_ssl_name config.example.com;
proxy_ssl on;
proxy_ssl_certificate /home/vagrant/cert/client.crt;
proxy_ssl_certificate_key /home/vagrant/cert/client.key;
}
基本上就是把 HTTP 代理换成了 TCP 代理指令。
这样配置好之后,我们就可以用 redis-cli
去连接本地的 80 端口了。
redis-cli -p 80
127.0.0.1:80> get foo
"bar"
一些参考资料:
Nginx 反向代理相关的文档。http://nginx.org/en/docs/http/ngx_http_proxy_module.html[19] CA 如何保存 key?https://security.stackexchange.com/questions/24896/how-do-certification-authorities-store-their-private-root-keys 自签名证书的风险:https://www.preveil.com/blog/public-and-private-key/ TLS 建联的展示,每一个字段都有详细的展示,非常好看:https://tls.ulfheim.net/ 什么是 mTLS? https://www.cloudflare.com/zh-cn/learning/access-management/what-is-mutual-tls/
在 GitHub猿 还有更多优质项目系统学习资源,欢迎分享给其他同学吧!
最后给读者整理了一份BAT大厂面试真题,需要的可扫码加微信备注:“面试”获取。
◆ ◆ ◆ ◆ ◆
(放到你圈子里,朋友们会感激您)
字节跳动面试经验总结,已顺利拿到offer! IntelliJ IDEA这样配置,代码效率嗖嗖的~ Intellij IDEA 2021.2.3 最新版免费激活教程(可激活至 2099 年,亲测有效) 分享一个牛逼的 Java 开源后台管理系统,不要造轮子了! 一款 IDEA 插件帮你优雅转化 DTO、VO、BO、PO、DO MyBatis 中使用流式查询避免数据量过大导致OOM 一套全部代码开源的快速开发平台,毫无保留给个人及企业免费使用 接口性能优化技巧,干掉慢代码! JWT 登录认证及 token 自动续期方案解读 Spring Boot + Redis 搞定搜索栏热搜、不雅文字过滤功能 Java接口开发,不用写Controller、Service、Dao、Mapper、XML、VO,自动生成!
欢迎添加栈哥个人微信 ysle007 进粉丝群或围观朋友圈
扫码关注带你吊爆Java后端技术
引用链接
将 TLS 与 SSL 作为同义词: https://www.globalsign.com/en/blog/ssl-vs-tls-difference
[2]这里: https://www.cloudflare.com/zh-cn/learning/ssl/what-happens-in-a-tls-handshake/
[3]validation standards: https://en.wikipedia.org/wiki/Certificate_authority#Validation_standards
[4]letsencrypt 的 CA: https://letsencrypt.org/docs/dst-root-ca-x3-expiration-september-2021/
[5]及时吊销: https://en.wikipedia.org/wiki/OCSP_stapling
[6]当时: https://cloud.tencent.com/developer/article/1151919
[7]逐步在后续的 Chrome 版本中吊销了赛门铁克的证书: https://security.googleblog.com/2017/09/chromes-plan-to-distrust-symantec.html
[8]在 5 天后吊销了这期间通过 TLS-ALPN-01 颁发的所有证书: https://community.letsencrypt.org/t/2022-01-25-issue-with-tls-alpn-01-validation-method/170450
[9]2017 年: https://www.kawabangga.com/posts/2353
[10]Key ceremony: https://en.wikipedia.org/wiki/Key_ceremony
[11]HSM: https://en.wikipedia.org/wiki/Hardware_security_module
[12]Class 5 Alarm System: https://security.gallagher.com/en/Class-5-certified
[13]一个视频介绍 Key Signing Ceremony: https://www.youtube.com/watch?v=b9j-sfP9GUU
[14]公布在网上: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints
[15]一些安全隐患: https://www.keyfactor.com/blog/self-signed-certificate-risks/
[16]发送是合理的: https://serverfault.com/questions/938269/nginx-client-cert-verification-ssl-client-certificate-vs-ssl-trusted-certificat
[17]OCSP: https://en.wikipedia.org/wiki/Online_Certificate_Status_Protocol
[18]SNI: https://en.wikipedia.org/wiki/Server_Name_Indication
[19]http://nginx.org/en/docs/http/ngx_http_proxy_module.html: https://nginx.org/en/docs/http/ngx_http_proxy_module.html