跳转到主要内容

NGINX 禁止 IP 访问(非标准端口)

出于安全和隐私需求,我们需要禁止直接 IP 访问。

在没有做处理的情况下,如果你直接使用 IP 地址访问一个 https 端口,那么你会发现即使无法连接(未携带 SNI 头,NGINX 不知道你要链接到哪一个域),但 NGINX 还是返回了默认服务器的 SSL 证书,这样你的 IP 地址和你的域名就暴露了(SSL 证书中)。

假如你的站点在 CDN 后,你又部署了同一个证书,那么攻击者很可能通过网络空间测绘引擎(如:https://censys.io/)找到你的源站地址。
因为网络空间测绘引擎在扫描全网所有 IPv4 地址并记录下这些信息,理论上扫描全网 IPv6 难度很大,但也不是不可能。

因此为了避免网络空间测绘引擎扫描、运营商扫描等,我们倾向于在直接 IP 访问时不返回任何内容。

注意:当你为子域名创建一个证书时,同样会暴露该子域名的存在,可以在 crt.sh 等站点上查看到几乎所有证书。

 

配置

在 http{} 内的第一个位置添加:

server {
    listen 80 default_server default reuseport fastopen=3;
    listen 443 default_server default reuseport fastopen=3 ssl;
    listen 443 default_server default reuseport quic;
    listen [::]:80 default_server default reuseport ipv6only=on fastopen=3;
    listen [::]:443 default_server default reuseport ipv6only=on fastopen=3 ssl;
    listen [::]:443 default_server default reuseport ipv6only=on quic;

   return 444;
   ssl_reject_handshake on;
   ssl_protocols TLSv1.2 TLSv1.3;
   server_name _ 
    "~^(?=(\b|\D))(((\d{1,2})|(1\d{1,2})|(2[0-4]\d)|(25[0-5]))\.){3}((\d{1,2})|(1\d{1,2})|(2[0-4]\d)|(25[0-5]))(?=(\b|\D))$" 
    "~^([\\da-fA-F]{1,4}:){7}([\\da-fA-F]{1,4})$"
    "";
}

注:listen 的选项根据你实际需求调整,后续再 listen 同一个端口时不可再带 reuseport 等选项,只可以带 ssl 或 quic。
具体选项请参考:https://nginx.org/en/docs/http/ngx_http_core_module.html#listen

default  选项(在 0.8.21 版本重命名为 default_server )实际上等同于 default_server ;

如果你在非标准端口上运行 https,你可能会发现,访问时会出现:

400 Bad Request
The plain HTTP request was sent to HTTPS port

这个页面的错误代码其实不是 400 而是 497,这是一个 NGINX 非标准代码。

我们并不想返回这个页面,所以要这样(添加其他的比如 80 443 随便什么端口都可以):
注意:ssl_reject_handshake 需要 NGINX 1.19.4 及以上版本。

server {
   listen 8443 default_server default reuseport fastopen=3 ssl;
   listen 8443 default_server default reuseport quic;
   listen 8995 default_server default reuseport fastopen=3 ssl;
   listen 8995 default_server default reuseport quic;
   listen [::]:8443 default_server default reuseport ipv6only=on fastopen=3 ssl;
   listen [::]:8443 default_server default reuseport ipv6only=on quic;
   listen [::]:8995 default_server default reuseport ipv6only=on fastopen=3 ssl;
   listen [::]:8995 default_server default reuseport ipv6only=on quic;

   return 444;
   ssl_reject_handshake on;
   ssl_protocols TLSv1.2 TLSv1.3;
   server_name _ 
    "~^(?=(\b|\D))(((\d{1,2})|(1\d{1,2})|(2[0-4]\d)|(25[0-5]))\.){3}((\d{1,2})|(1\d{1,2})|(2[0-4]\d)|(25[0-5]))(?=(\b|\D))$" 
    "~^([\\da-fA-F]{1,4}:){7}([\\da-fA-F]{1,4})$"
    "";
   error_page 497 @close;
   location @close {return 444;}
}

注:listen 的选项根据你实际需求调整,后续再 listen 同一个端口时不可再带 reuseport 等选项,只可以带 ssl 或 quic。

解释

  • return 444 成功请求就返回 444。444 是 NGINX 的非标准代码,直接关闭连接而不发送响应标头;但 444 仍然不能完全避免扫描,这是局限性;
  • ssl_reject_handshake on  server{} 中的 SSL 握手将被拒绝。默认情况下即使主机头不匹配,NGINX 仍然会发送证书,这样你的域名就暴露了(1.19.4 及以上版本支持);
  • ssl_protocols TLSv1.2 TLSv1.3 测试版本 1.23.2,如果不添加这个选项,那么无法启用 TLS v1.3;
  • server_name _  "_" 其实就是一个普通的字符串,达到不匹配任何主机头的效果。
  • server_name 正则表达式 这个正则表达式是多一层保险,匹配所有 IPv4 地址(不知道有没有用);
    server_name "" 根据官方文档,服务器名称被设置为一个空字符串 "",它将匹配没有 “Host” 标头字段的请求,从版本 0.8.48 开始可以省略;
  • error_page 497 @close; location @close {return 444;} 如果错误页面是 497,就使用 @close 页面,然后 @close 页面定向重定向返回 444;
BUG

注意,从某个版本开始(已测试 NGINX 1.23.2 + OpenSSL 3.0.1 和 NGINX 1.23.2 + OpenSSL 1.1.1o),如果不在此 Server 块中添加

ssl_protocols TLSv1.2 TLSv1.3;

有可能造成其他 Server 块中即使设置了 TLSv1.3,仍然无法启用的问题。

更进一步

如果攻击者已经确定要攻击某个域名,那么他就可以将带着该域名的正常握手信息遍历所有 IPv4。
这种情况下,攻击者的访问和正常访问没有区别,我们很难对其进行处理。
也许唯一的办法就是躲在 CDN 后,并仅允许 CDN 的 IP 段访问我们的服务器。

你可以使用防火墙或 NGINX自带功能来实现:

location / {
    allow   127.0.0.0/24;
    allow   169.254.1.1/32;
    deny    all;
    deny    192.168.1.0/24;
}

为什么?什么是 SNI?

我在这篇文章中进行了一些介绍 :D
https://wiki.pha.pub/link/376

主要参考