前言

早在 2020 年我刚会玩 Linux 那会,就想写这个主题了,那时候是准备作为 Debian 系统安全指南来写一篇的,可惜不停的搁置。事到如今,这种大而全的文章我已无力再写,不过如本标题而言,针对性某一方面还是可以写写的。刚会建站就发现这个问题了,直接使用 HTTPS 协议访问源站 IP 的 443 端口,也就是 https://IP:443 会暴露你源站的 TLS 证书,这点不多赘述。解决方案随着软件版本迭代和需求的不同,也逐渐完善,下面依次介绍

签发空白假证书

2020 年上半年及之前,统一的解决方案是随便签个空白假证书,很简单的一行命令 openssl req -new -x509 -nodes -out fake.crt -keyout fake.key,然后写到默认配置里,这样访问 https://IP:443 会得到这个空白假证书,达到保护源站的目的。示例配置如下

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# /etc/nginx/sites-enabled/default
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 444;
}
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
ssl_certificate /etc/nginx/ssl/fake.crt;
ssl_certificate_key /etc/nginx/ssl/fake.key;
}
# 我习惯把证书放在 /etc/nginx/ssl

使用 nginx 新特性

而在 2020/10/27 发布的 nginx 1.19.4 带来了 ssl_reject_handshake 特性后,情况发生了变化。话说我能用上这个功能足足等了三年,Debian 12 发布才附带了 nginx 1.22(Debian 11 是1.18)。简单来说开启本功能后会拒绝握手(并且不会在 /var/log/nginx/access.log 留下日志),相比于上个方案的优点是简单方便通用性强,不需要自己签多余的假证书。并且还有额外的好处,通过 Wireshark 抓包可以发现,发送无效的握手信息后还会发送 Connection reset (RST),在一定程度上可以对访问者隐藏自己是 HTTP/HTTPS server 的事实(虽然没什么大用)。示例配置如下

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# /etc/nginx/sites-enabled/default
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 444;
}
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
ssl_reject_handshake on;
}
# 当然,这并不是最终的配置,有几个漏洞,后面会讲

状态码重定向

上面的配置仍有两个问题

  1. 访问 http://IP:443 会返回 400 Bad Request The plain HTTP request was sent to HTTPS port
  2. 通过查看日志,有一些访问者使用非 HTTP 协议进行扫描,也会返回 400 Bad Request,并在日志留下记录,比如 curl scp://localhost:80curl scp://localhost:443

这倒不是什么大问题,只是在日志看到 400 响应而非 444 响应心里有点膈应。但在前几天刷到的帖子里的一个回复,经过我尝试并完善后,完美解决了这两个问题,参考
V2EX - Nginx 的 444 命令(强制切断 TCP 连接),用于对抗运营商检测家宽开服务是否有帮助?
十楼提到了 error_page 400 =444 /;,但其实经我测试,单纯这样写没用,根据 Stack Overflow - Return 444 Instead of 400 上的回答补足后,示例配置如下

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# /etc/nginx/sites-enabled/default
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 444;
error_page 400 =444 /;
location = / {
return 444;
}
}
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
ssl_reject_handshake on;
error_page 400 497 =444 /;
location = / {
return 444;
}
}
# 使用了拓展响应码 497 HTTP Request Sent to HTTPS Port

配置文件简化

上面的配置有太多重复了,于是我对其进行了简化,经测试效果是完全一样的(甚至可能更好)
于是这就是最终配置了,可以直接复制过去用,感兴趣的话也可以继续看下面的测试

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# /etc/nginx/sites-enabled/default
server {
listen 80 default_server;
listen [::]:80 default_server;

listen 443 ssl default_server;
listen [::]:443 ssl default_server;

server_name _;
ssl_reject_handshake on;
return 444;

error_page 400 497 =444 /;
location = / {
return 444;
}
}

测试用例

顺便把测试结果展示一下,这里还是挺费劲的,我是 tmux 上下开弓,又结合 Wireshark 抓包,使用了 curl、nc、nmap 进行了大量测试

基础

bash
1
2
3
4
5
6
7
8
9
10
11
curl http://localhost:80 # Return 444

curl http://localhost:443 # Return 444

curl https://localhost:80 # Return 444

curl https://localhost:443 # 日志无记录,抓包显示为 TLSv1.2 Record Layer: Alert (Level: Fatal, Description: Unrecognized Name)

curl scp://localhost:80 # Return 444

curl scp://localhost:443 # Return 444

进阶

bash
1
2
3
echo test | nc localhost 80 # Return 444

echo test | nc localhost 443 # Return 444

专业

这里为了排除已知常用端口的干扰,修改了 nginx 监听端口

bash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
nmap -sS -p 20080,20443 localhost
# PORT STATE SERVICE
# 20080/tcp open unknown
# 20443/tcp open unknown

nmap -sT -p 20080,20443 localhost
# PORT STATE SERVICE
# 20080/tcp open unknown
# 20443/tcp open unknown

nmap -sA -p 20080,20443 localhost
# PORT STATE SERVICE
# 20080/tcp unfiltered unknown
# 20443/tcp unfiltered unknown

nmap -sN -p 20080,20443 localhost
# PORT STATE SERVICE
# 20080/tcp open|filtered unknown
# 20443/tcp open|filtered unknown

nmap -sV -p 20080,20443 localhost
# PORT STATE SERVICE VERSION
# 20080/tcp open unknown
# 20443/tcp open ssl/unknown

nmap -A -p 20080,20443 localhost
# PORT STATE SERVICE VERSION
# 20080/tcp open unknown
# 20443/tcp open ssl/unknown
# OS details: Linux 2.6.32

最后总结

  1. 抓包可以发现,nginx 拒绝握手是通过发送无效握手信息而不是直接关闭连接,所以在 nmap 扫描结果中还是能看到疑似 ssl 服务,这算是最后一点小遗憾吧
  2. 完成所有测试后,查看写入日志的信息,全部都是返回 444 响应而没有 400 响应了,这样就最大程度屏蔽了来自互联网的扫描