使用 Nginx 穿透代理HTTPS的碎碎念

有的时候,我们希望使用Nginx来分发多个站点的HTTPS请求,但又希望HTTPS证书只部署在源站上。这个时候,我们会考虑使用 Nginx 的穿透代理

stream {
  server{
    listen 443;
    proxy_pass 10.0.1.5:443;
    proxy_connect_timeout 15s;
    proxy_timeout 30s;
    proxy_next_upstream_timeout 120s;
  }
}

但大多数时候,我们需要分发就意味着不止是1个后端服务,这个时候,我们就需要通过解析SNI获取域名来判断需要转发的目标服务,因此我们开启了 ssl_preread 。

stream {
  map_hash_bucket_size 64;

  map $ssl_preread_server_name $backend_pool {
    service_a.com backend_a;
    service_b.com backend_b;
    default backend_a;
  }

  upstream backend_a {
    server 10.0.1.5:443;
  }
  upstream backend_b {
    server 10.0.1.6:443;
  }

  server{
    listen 443;
    ssl_preread on;
    proxy_pass $backend_pool;
    proxy_connect_timeout 15s;
    proxy_timeout 30s;
    proxy_next_upstream_timeout 120s;
  }
}

当然,想要物尽其用的我们,在这一台用于转发服务的Nginx上,也打算启动 HTTPS 服务

如果我们这么配置

http {
  server {
    listen 443 ssl;
    server_name service_local.com;
    ssl_certificate /etc/nginx/certs/ssl.cert;
    ssl_certificate_key /etc/nginx/certs/ssl.key;

    ....
  }
}

这个时候就会发现,我们刚才的 stream server 已经占用了 443 端口 会产生报错

nginx: [emerg] bind() to 0.0.0.0:443 failed (98: Address already in use)

因此我们需要做一些调整,将本地的 https 绑定到未被占用的端口,再由前面的转发服务根据域名进行转发

http {
  server {
    listen 127.0.0.1:8443 ssl;
    ...
  }
}

stream {
  map $ssl_preread_server_name $backend_pool {
    service_a.com backend_a;
    service_b.com backend_b;
    service_local.com backend_local;
    default backend_a;
  }

  upstream backend_local {
    server 127.0.0.1:8443;
  }
  ...
}

是的 看起来 一切都很正常 但这个时候 如果我们通过上游服务器日志观察请求的话 会发现所有的请求客户端都是中转服务的IP 无法获得实际用户的IP

同时,由于我们是4层代理,并不能像http代理一样,通过修改HTTP头的方式来传递信息。

此时存在两个方案:

1. 通过 IP Transparent 修改3层中的源IP地址
L4(传输层)IP透明反向代理的实现(传递客户端真实IP)

在 nginx 中可以通过 proxy_bind 来实现

proxy_bind $remote_addr transparent;

这种方式对于上游来说,是没有感知正常处理的,但是因为对3层包进行了修改,为了能够正确处理数据流向,则需要配合进行路由配置

2. 使用 proxy protocol 协议

这是随着负载均衡类服务大量运用而产生的专门为了交换代理的信息的协议,最初由HAProxy提出。

要使用 proxy protocol 协议,需要上下游同时支持该协议。

目前主流的云服务商提供的 负载均衡 服务大多都支持该协议

如果你的上游是自行基于 Nginx 部署的话,那么可以通过 proxy_protocol 来开启

http {
    #...
    server {
        listen 80   proxy_protocol;
        listen 443  ssl proxy_protocol;
        #...
    }
}
   
stream {
    #...
    server {
        listen 4433 proxy_protocol;
        #...
    }
}

同时 proxy_protocol 可以配合 Real‑IP modules 来使用

server {
  ...
  set_real_ip_from 10.0.1.0/24;
  ...
}
http {
  server {
    ...
    real_ip_header proxy_protocol;
  }
}

在上游开启了 proxy_protocol 支持后 在进行分发的服务上配上 proxy_protocol on; 就可以了

stream {
  map_hash_bucket_size 64;

  map $ssl_preread_server_name $backend_pool {
    service_a.com backend_a;
    service_b.com backend_b;
    default backend_a;
  }

  upstream backend_a {
    server 10.0.1.5:443;
  }
  upstream backend_b {
    server 10.0.1.6:443;
  }

  server{
    listen 443;
    ssl_preread on;
    proxy_pass $backend_pool;
    proxy_connect_timeout 15s;
    proxy_timeout 30s;
    proxy_next_upstream_timeout 120s;
    proxy_protocol on;
  }
}

使用 proxy protocol 之所以是一个协议 那要求的就是请求和接受的双方都认识这个协议

对于转发 HTTP 来说,如果只有一方开启,一方未开启,我们能够直观的通过请求异常来发现问题

对于4层转发 HTTPS 来说,这个问题则会显得隐蔽。当我们在代理服务上开启了 proxy_protocol ,但上游不支持该协议时:

我们会发现,使用对应的SNI来访问上游不支持的业务时,由显得很隐蔽

Chrome 会提示 该网站不能提供安全的连接 ERR_SSL_PROTOCOL_ERROR

Firefox 会提示 SSL_ERROR_RX_RECORD_TOO_LONG

如果直接使用 openssl 检查则会看到 error:1408F10B:SSL routines:ssl3_get_record:wrong version number

而这些信息充满了迷惑性,只能得出 Nginx 的SSL配置不正确的结论,同时,如果你直接使用 openssl 检查上游时,openssl 是能够正常连接的,毕竟上游服务器的证书配置的并没有问题,只是没有开启 proxy_protocol 支持而已。

因此,在转发需要配合 proxy_protocol 使用的时候,一定需要先确认上游是否开启了 proxy_protocol 支持。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注