Stream 模块实现 mTLS 加密传输 TCP 流

2025 年 5 月 30 日 星期五
/ , ,
15

前段时间在考虑一个优雅的内网穿透方式,最好能利用已有的服务(因为本地已经在运行着太多的服务了)来实现,而且对隧道的安全性也需要一些要求。对于最常见的加密流量来说,采用 TLS 是最为普遍且经过数理验证的方法。

0x00 TLS / SSL 的奥秘

为什么 TLS 加密传输数据就可以确保不会被第三方解密、窃听或修改呢?这要从加密的传输方法开始探究讲起了。最原始的加密方式便是双方共同规定一段字符串来作密码。显然采用固定的一段密码是不行的,比如有一个公共的服务,若希望用户都能访问的同时也采用加密传输让服务商认定的服务范围外用户不可见数据,则需要给每一个提供服务的用户一段密码。在这种情况下,每个人都可以去使用这个密码去伪造服务商提供的服务来攻击其他的用户,此时便引出了 TLS。TLS 的前身是 SSL,在客户端-服务器连接中对服务器进行身份的验证,并提供了对客户端与服务器通信的加密。解决的问题是如何证明用户 Alice 是 Alice,而不是 Bob。

非对称加密

现在采用的一般为非对称加密传输技术。非对称加密(Asymmetric cryptography)需要两个密钥:公钥(Publickey)和私钥(Privatekey)。公钥用于加密,而私钥用于解密。将要传输的消息体 "Hello" 会被公钥加密,生成的加密内容用来传输,且只有相应的私钥才能解密出原本的明文,用于加密的公钥不能用作解密的用途。由于加密与解密需要两个不同的密钥,故称为非对称加密。公钥是可以公开分发到任何地方,每个人都可以轻易的获取到。而私钥是不可以公开的,其必须由拥有者自行秘密保管,不能透过任何途径向任何人提供,包括通信的另一方。

TLS

TLS 证书是一个数据文件,其文件中包含了用于验证的服务器信息、设备信息、公钥及其生效时间段。通常在 TLS 中,服务器有一个 TLS 证书和一个公钥/私钥对,而客户端没有。典型的 TLS 流程如下:

  1. 客户端发送请求到服务器
  2. 服务器回应请求,并返回其拥有的 TLS 证书
  3. 客户端获取到服务器返回的 TLS 证书,验证其有效性
  4. 客户端与服务器建立了有效的 TLS 连接,通过 TLS 传输加密的信息

mTLS

mTLS全名是 Mutual TLS,是一种相互验证身份的方法。与普通 TLS 不同的是,mTLS中客户端与服务器都有一个证书,且双方都会使用它们的公钥/私钥进行身份验证。与上述TLS步骤中相比,mTLS会有一些额外的步骤来验证对方

  1. 客户端发送请求到服务器
  2. 服务器回应请求,并返回其拥有的 TLS 证书
  3. 客户端获取到服务器返回的 TLS 证书,验证其有效性
  4. 客户端发送自己的 TLS 证书给服务器
  5. 服务器收到客户端发送来的 TLS 证书,验证其有效性
  6. 服务器接受此客户端的连接
  7. 客户端与服务器建立了有效的 TLS 连接,通过 TLS 传输加密的信息\ 在 TLS 中,通常使用 CA 颁发的有效证书,其主要负责检查证书所有者是否拥有合法的关联域。而在 mTLS 验证时,是需要自签名证书,授权的客户端与服务器使用的证书必须由此自签名的根证书生成的。

0x01 加密隧道方案

其实生活中便有着双向验证的例子。如在使用 ssh 远程连接服务器时,需要先将公钥上传到 ssh 服务器上。在第一次建立连接时,本地的 ssh 客户端展示服务器的公钥,提示是否信任。在信任后便会将服务器信息与公钥保存到~/.ssh/known_hosts

公钥需要确定

公钥需要确定

\ 如果key发生了变化,再次建立 ssh 连接时客户端会提示公钥不匹配\

公钥不匹配时

公钥不匹配时
\ 而 git 一般也是通过 ssh 协议完成的,故其也是一种双向的验证。如在使用 GitHub时,需要建立彼此的信任。GitHub 通过已经上传的公钥来验证,而客户端是通过验证 GitHub 分发在互联网上的公钥

0x02 自签证书

为了实现 mTLS,先进行自签名证书操作。共需要两套证书,一套分配给客户端,另一套给服务器使用。\ 使用 openssl 生成 CA key 与证书:

openssl genrsa -des3 -out ca.key 4096
openssl req -new -x509 -days 36500 -key ca.key -out ca.crt

这里给证书设置了十年的有效期,避免证书过期后再生成的问题。至此已经有了 CA 的私钥与证书文件,现在已经可以签发给客户端与服务器证书了。\ 继续生成两套证书:

# 客户端证书
openssl genrsa -out client.key 4096
openssl req -new -key client.key -out client.csr
openssl x509 -req -days 36500 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt

# 服务器证书
openssl genrsa -out server.key 4096
openssl req -new -key server.key -out server.csr -subj "/CN=*.private_tunnel.net" -addext "subjectAltName=DNS:*.private_tunnel.net"
openssl x509 -req -days 36500 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt

这样就为客户端与服务器分别生成了证书。需要注意的是,在为服务器生成证书时额外添加了证书对应的域名,这可以在一些验证域名的场景中使用。\ 此时我们获得了以下文件:\

所有已生成的文件

所有已生成的文件
\ 准备工作就已经完成了。接下来开始部署给 nginx 使用

Nginx 作为服务端提供服务端口

启用一个测试用 HTTP 服务

这里以一个简陋的 HTTP 服务作为测试端口是否转发成功:

python3 -m http.server -b 127.0.0.1 8888
启用的临时 Web 服务

启用的临时 Web 服务
启用 Nginx 服务

这里推荐使用 Docker 启用便于管理。新建一个文件夹用于存放 nginx 相关文件,并将以下内容保存到文件docker-compose.yml

services:
  nginx:
    image: nginx:latest
    restart: unless-stopped
    volumes:
      - ./conf/nginx.conf:/etc/nginx/nginx.conf
      - ./conf/conf.d:/etc/nginx/conf.d
      - ./conf/stream.d:/etc/nginx/stream.d
      - ./certs:/etc/certs
      - ./html:/usr/share/nginx/html
      - /var/log/nginx:/var/log/nginx
    network_mode: host

而后建立一些文件夹:

mkdir -p ./conf/conf.d ./conf/stream.d certs html /var/log/nginx
touch ./conf/nginx.conf

把生成的证书放入目录./certs里\ 修改 Nginx 的配置,将以下内容保存到nginx.conf中:

user  nginx;
worker_processes  auto;
error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;
events {
    worker_connections  1024;
}
stream {
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
        ssl_prefer_server_ciphers on;
        include /etc/nginx/stream.d/*;
}
http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  /var/log/nginx/access.log  main;
    sendfile        on;
    #tcp_nopush     on;
    keepalive_timeout  65;
    gzip  on;
    include /etc/nginx/conf.d/*.conf;
}

新建一个 stream 监听,将以下内容保存到./conf/stream.d/myproxy.conf中:

server {
        listen 8889 ssl;
        proxy_pass 127.0.0.1:8888;
        ssl_certificate /etc/certs/tunnel/server.crt;
        ssl_certificate_key /etc/certs/tunnel/server.key;
        ssl_verify_client on;
        ssl_client_certificate /etc/certs/tunnel/ca.crt;
}

其中

  • listen 字段代表服务器监听的端口,即将来客户端请求的接口
  • proxy_pass 字段与常规的 http 代理中 proxy_pass 一致,是请求到 listen 监听端口的请求转发的目的。
  • ssl_certificate 与 ssl_certificate_key 分别为已经生成的证书私钥与证书位置
  • ssl_verify_client 来决定是否启用客户端身份验证
  • ssl_client_certificate 来指定 CA 的证书\ 由上述各字段内容可知,此配置是 nginx 在本机 8889 端口启用监听,将 8889 端口接受的请求进行客户端身份的验证。若验证通过后将请求转发到本机的 8888 端口,即之前已经启用的 HTTP 服务上。\ 此时文件已准备完成,执行命令docker compose up -d即可启用 nginx 服务
客户端 Nginx 服务配置

大部分内容与服务器的配置相同,不同的地方在于目录stream.d下的配置文件\ 将以下内容保存到./conf/my_proxy_client.conf

server {
        listen 4008;
        proxy_pass remote;
        proxy_ssl_trusted_certificate /etc/certs/tunnel/ca.crt;
        proxy_ssl_verify on;
        proxy_ssl_server_name on;
        proxy_ssl_name test.private_tunnel.net;
        proxy_ssl on;
        proxy_ssl_certificate /etc/certs/tunnel/client.crt;
        proxy_ssl_certificate_key /etc/certs/tunnel/client.key;
}

需要注意的是,proxy_ssl_name这里要填写在服务器生成证书时指定的域名。

连接验证

在正确的配置了上述的客户端与服务器上的 nginx 服务并启用后,TCP 数据流在公网中由服务器的 8889 端口加密传出,客户端便可以请求本地的 4008 端口来访问到服务器 8888 端口上的服务了。该数据流是高度加密的,常见的用途如本地连接到服务器上的数据库。

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...