Nginx配置实现访问封禁

知识分子没文化
2025-04-12 / 0 评论 / 2,426 阅读 / 2,082 字数 / 正在检测是否收录...

目录

一、allow/deny

allowdeny 是 Nginx 的 ngx_http_access_module 模块中用于访问控制(Access Control)的指令,主要用于限制客户端 IP 对某个资源、目录或接口的访问权限。可配置于 http、server、location、limit_except 等上下文中。

使用 deny 指令控制特定 IP 和 CIDR 网段不能访问:

location / {
    # 禁止指定 IP 地址访问
    deny 192.168.1.100;

    # 禁止 CIDR 网段
    deny 192.168.2.0/24;

    allow all;
}

使用 allow 指令只允许特定 IP 和 CIDR 网段可以访问:

location /admin {
    # 允许指定 IP 地址访问
    allow 192.168.2.1;

    # 允许 CIDR 网段访问
    allow 192.168.3.0/24;

    deny all;
}

allowdeny 指令的优点是简单方便,适合规则少、不复杂的场景,直接返回 403,逻辑单一,当需要配合 UA、请求路径等进行控制则无能为力,这时用 geomap 指令会更灵活。

CIDR 网段写法:

192.168.2.0/24 表示掩码为 255.255.255.0 的整个网段(即 192.168.2.0 ~ 192.168.2.255),/24 是前缀长度,数字越大范围越精确。常见对照:

  • /8 → 255.0.0.0 → 一个 A 类网,约 1677 万个 IP
  • /16 → 255.255.0.0 → 一个 B 类网,约 65536 个 IP
  • /24 → 255.255.255.0 → 一个 C 类网,256 个 IP

二、geo/map

geomap 的作用都是根据特定条件动态生成变量,从而替代性能较差的 if 语句,作用域为整个 http 块,当拿到请求时,Nginx 会用客户端的 IP 或其他变量查找这个映射,找到则赋予对应的值,找不到则使用默认值,二者区别在于应用场景不同:

  • geo专门针对 IP 的映射,支持 IP 地址和 CIDR 网段

  • map 则是通用的变量映射工具。它可以基于任意源变量(如 URI、User-Agent、Host 等)的值,按照设定的规则生成新的目标变量,如果基于 IP 作映射时只支持 IP 地址,不支持 CIDR 网段

geo/map 的好处在于把判断结果存为变量,可以和其他维度的判断做组合,灵活度远高于 allow/deny

2.1、使用 geo 实现IP黑名单

geo $remote_addr $is_ip_blocked {
    # 默认为0,不限制
    default             0;

    # 封禁整个 192.168.0.0/16 网段
    192.168.0.0/16      1;
    192.168.1.0/24     0;    # 但 192.168.1.0/24 例外(因为掩码更长,优先)
}

geo 指令根据客户端 IP 地址($remote_addr)建立映射,从上到下进行匹配,且 CIDR 掩码越长越优先,匹配到 IP 将 $is_ip_blocked 设置为 1,否则返回为 0

如果不指定第一个变量,默认用 $remote_addr

当 Nginx 部署在 CDN(如 Cloudflare)或反向代理之后时,$remote_addr 拿到的是代理服务器的 IP,而非真实客户端 IP。此时基于 $remote_addrgeo 封禁会失效——封的全是代理 IP。

解决方案是使用 http 块中 ngx_http_realip_module 模块:

http {
 real_ip_header X-Forwarded-For;
 set_real_ip_from 127.0.0.1;
 real_ip_recursive on;
 # 其他配置 ...
}

对应的 IP 映射中引用的变量修改为 $realip_remote_addr

-geo $remote_addr $is_ip_blocked {
+geo $realip_remote_addr $is_ip_blocked {
 # 默认为0,不限制
 default             0;
}

2.2、使用map实现UA/路径黑名单

map $http_user_agent $is_ua_blocked {
    # “~*”:不区分大小写的正则匹配
    ~*Firefox/[1-5][0-9]     1;
    ~*Chrome/[5-8][0-9]      1;
    default                  0;
}

map 指令根据源变量 $http_user_agent(Nginx 内置变量,表示请求头中的 User-Agent 字段)的值,通过正则匹配或精确匹配,映射出目标变量 $is_ua_blocked 的值。map 中正则表达式的匹配顺序为从上到下,首次匹配即返回。

UA 中的正则匹配:

  • Firefox/[1-5][0-9]:匹配 Firefox 10 ~ Firefox 59([1-5] 匹配十位 1 ~ 5,[0-9] 匹配个位 0 ~ 9)
  • Chrome/[5-8][0-9]:匹配 Chrome 50 ~ Chrome 89

这些规则封禁的都是非常老旧的浏览器版本,正常浏览器早已不在这些版本范围内。现实中碰到这些 UA 的请求,几乎可以断定是爬虫或扫描器伪装的。

*补充——`$http_变量规则:** Nginx 中请求头字段会自动转为$http前缀变量,字段名中的-转为,全小写。例如User-Agent$http_user_agentX-Forwarded-For$http_x_forwarded_for`。

与此类似的就可以基于请求路径 $request_uri 的变量映射:

map $request_uri $is_path_blocked {
    default                       0;
    ~*^/(admin123|webadmin|pma|login|phpeditor|dashboard)/(.*)                    1;
    ~*^/((wp.*)|test(.*)|(php(.*)info)|server(.*)|xmlrpc|cmd|php|install)\.php    1;
    ~*\.(sql|bak|zip|tar|gz|rar|7z|bak|old|conf|config|env|yml|phpeditor|git)$    1;
    "~^.{301,}$"                  1;
}

$request_uri 也是 Nginx 的内置变量,为完整的请求 URI,包含查询参数,不含域名,通过对这部分信息做正则匹配,命中则返回 1。这三条规则分别针对不同类型的恶意请求:

规则 1——恶意目录扫描:

~*^/(admin123|webadmin|pma|login|phpeditor|dashboard)/(.*)

这些路径(phpMyAdmin、后台登录、编辑器等)是最常被试探的目标。(.*) 匹配目录下的任意子路径。

规则 2——恶意 PHP 文件扫描:

~*^/((wp.*)|test(.*)|(php(.*)info)|server(.*)|xmlrpc|cmd|php|install)\.php

WordPress 相关文件(wp-configwp-login)、phpinfo、xmlrpc 等都是自动化攻击脚本的高频目标。

规则 3——敏感后缀文件:

~*\.(sql|bak|zip|tar|gz|rar|7z|old|conf|config|env|yml|phpeditor|git)$

.sql 数据库备份、.env 环境变量、.git 仓库信息、.conf 配置文件,这些文件一旦泄露,后果严重。$ 锚定 URI 结尾,确保匹配的是文件扩展名。

规则 4——超长 URI:

"~^.{301,}$"

{301,} 表示匹配 301 个字符及以上。正常请求的 URI 极少超过 300 字符,超长 URI 通常来自缓冲区溢出攻击或恶意注入尝试。

注意:正则表达式中如果包含大括号 {}, 则必须用双引号包裹,否则 Nginx 配置解析会报错。

2.3、组合逻辑

将三个维度的判断结果用 : 拼接成一个字符串(如 "0:0:1"),再对这个字符串做正则匹配,实现 OR 逻辑,任意一个为 1,最终结果就是 1

map "$is_ip_blocked:$is_ua_blocked:$is_path_blocked" $is_blocked {
    ~1:[01]:[01]      1;
    ~[01]:1:[01]      1;
    ~[01]:[01]:1      1;
    default           0;
}

如果要实现 AND 逻辑,三个维度全部命中才封禁,只需一条规则:

map "$is_ip_blocked:$is_ua_blocked:$is_path_blocked" $is_blocked {
    ~1:1:1      1;
    default     0;
}

三、完整示例

3.1、黑名单配置

将上面四段 geo/map 单独写成一个配置文件 blacklist(或写在 http 块中):

# ==================== IP 封禁映射 ====================
geo $remote_addr $is_ip_blocked {
    # 默认不封禁
    default             0;

    # ---------- 封禁的 IP 列表 ----------
#    192.168.1.1         1;

    #
    # ---------- 封禁的网段(CIDR) ----------
    # 封禁整个 192.168.0.0/16 网段
    192.168.0.0/16      1;
    192.168.1.0/24     0;    # 但 192.168.1.0/24 例外(因为掩码更长,优先)
}

# ==================== UA 封禁映射 ====================
map $http_user_agent $is_ua_blocked {
    ~*Firefox/[1-5][0-9]     1;
    ~*Chrome/[5-8][0-9]      1;
    default                  0;
}

# ==================== 路径封禁映射 ====================
map $request_uri $is_path_blocked {
    default                  0;
    # 常见恶意扫描路径
    # 规则:“~*”:不区分大小写,“^/”:必须以 / 开头
    ~*^/(admin123|webadmin|pma|login|phpeditor|dashboard)/(.*)    1;

    # 特定名称的 PHP 文件
    ~*^/((wp.*)|test(.*)|(php(.*)info)|server(.*)|xmlrpc|cmd|php|install)\.php    1;

    # 特定后缀
    ~*\.(sql|bak|zip|tar|gz|rar|7z|bak|old|conf|config|env|yml|phpeditor|git)$    1;

    # URI 长度超过 300
    "~^.{301,}$"            1;
}

# ==================== 组合逻辑:任意一个为 1 则封禁 ====================
map "$is_ip_blocked:$is_ua_blocked:$is_path_blocked" $is_blocked {
    # 当任意一个判定为 "1" 时,结果为 "1"
    ~1:[01]:[01]      1;
    ~[01]:1:[01]      1;
    ~[01]:[01]:1      1;

    # 其他情况默认为0,不封禁
    default             0;
}

3.2、引入配置

由于 geo/map 块的作用域为 http 块,所以在网站的配置文件 www.confserver 块外使用 include 指令引入配置文件,在 server 配置中引用 $is_blocked 使之生效:

# 引入黑名单配置
include ./blacklist;
server {
    listen 80;
    server_name example.com;
    root /var/www/html;

    # 记录黑名单访客的访问日志,表示仅当变量值非 0 时记录日志,正常请求不会写入该日志文件。
    access_log /xxx/path/www_blocked.log combined if=$is_blocked;
    # 引用$is_blocked,对黑名单访客返回指定状态码
    if ($is_blocked) {
        return 444;
    }

    location / {
        try_files $uri $uri/ =404;
    }
}

if ($is_blocked) 的判断逻辑: Nginx 的 if 会将变量值视为布尔值,0 为假,非零为真。当 $is_blocked = 1 时条件成立,执行 return 444

444 是 Nginx 私有状态码,表示直接关闭连接而不返回响应,如果爬虫请求密集时可以使用 444 返回码节省流量。

1

评论 (0)

取消