Nginx 安全加固清单:隐藏指纹、请求过滤与恶意流量防护的完整实践

系统梳理 Nginx 的安全加固清单,覆盖版本与指纹隐藏、请求过滤与参数校验、CC 防护与连接限制、安全响应头、防爬与恶意流量拦截,结合真实配置给出从接入层到纵深防御的完整加固实践。

Web 服务器暴露在公网之后,每天都会收到大量扫描器、爬虫与攻击流量。很多入侵的第一步并非复杂的漏洞利用,而是先通过响应指纹判断服务器的版本与组件,再挑选对应的已知漏洞发起攻击。Nginx 作为接入层的第一道关口,天然具备隐藏指纹、过滤请求、限制连接、下发安全响应头的能力。本文按「隐藏指纹 → 请求过滤 → 连接限制 → 响应头加固 → 恶意流量拦截」的顺序,给出可逐条落地的 Nginx 安全加固清单。

一句话总结: Nginx 安全加固的实质是在接入层收敛攻击面,把版本指纹、非法请求、超量连接与恶意流量拦截在业务代码之前。

1. 隐藏版本与指纹消除

一句话总结: 关闭 server_tokens、定制错误页并清除组件标识,让扫描器无法通过响应头与错误页判断 Nginx 版本与后端技术栈。

Nginx 默认会在响应头与默认错误页中携带版本号,例如 Server: nginx/1.24.0。攻击者据此可以直接查 CVE 表,针对该版本发起定向攻击。第一步加固是关闭版本号输出:server_tokens off 后响应头只保留 Server: nginx,去掉了点号后的具体版本号。

# 关闭版本号,只暴露 "nginx" 字样
server_tokens off;

# 需要 ngx_headers_more 模块时,可彻底清除或改写 Server 头
more_clear_headers Server;
more_set_headers "Server: ws-edge";

server_tokens off 已经足够应对绝大多数扫描器,但部分安全要求严格的环境还需要彻底移除 Server 头或改写为自定义值,这需要 ngx_headers_more 模块(OpenResty 默认自带,Nginx 官方版需单独编译)。more_set_headers 可以改写任意响应头,把真实组件标识替换成迷惑性的值,进一步提高指纹收集成本。

除了响应头,默认错误页也会泄露版本信息与文件路径:Nginx 自带的 404 页面底部通常会显示 nginx/1.24.0,后端应用(如 Tomcat、PHP)的默认错误页则可能泄露绝对路径与框架版本。加固时应统一替换为自定义错误页:

# 统一错误页,避免泄露默认页中的版本与路径
error_page 400 /err/400.html;
error_page 403 /err/403.html;
error_page 404 /err/404.html;
error_page 500 502 503 504 /err/5xx.html;

location = /err/400.html { internal; root /srv/error_pages; }
location = /err/403.html { internal; root /srv/error_pages; }
location = /err/404.html { internal; root /srv/error_pages; }
location = /err/5xx.html  { internal; root /srv/error_pages; }

internal; 是关键:它保证错误页只能被 Nginx 内部跳转访问,外部请求直接访问 /err/404.html 也会被拒绝。错误页内容建议保持极简,只提供状态码与提示文字,不携带任何版本、时间戳或路径信息。

指纹隐藏不只是 Nginx 自身。反向代理后端的应用如果输出 X-Powered-By: PHP/7.4、X-AspNet-Version 之类的头,也会把技术栈暴露给攻击者。加固时要在 Nginx 层统一清除这些标识,同时把后端 5xx 错误页统一替换为定制页,避免后端默认页泄露内部信息。指纹隐藏是「纵深防御」的第一层,它不能阻止攻击,但能让攻击者无法低成本地挑选攻击路径,从而显著提高自动化扫描的无效命中率。

2. 请求过滤与参数校验

一句话总结: 在 location 上用 limit_except 收紧方法、校验关键请求头、规范化 URI,把明显非法的请求挡在业务层之外。

很多攻击在 URI、请求方法或请求头上就有明显特征:GET 以外的危险方法、超长参数、畸形请求头、目录穿越路径。Nginx 可以在接入层做第一道过滤,语法简单、开销极低。方法过滤用 limit_except 比 if 更干净,它直接声明「该 location 只允许哪些方法,其余一律拒绝」:

# 只读接口:只允许 GET/HEAD,其余方法一律拒绝
location /api/read/ {
    limit_except GET HEAD {
        deny all;
    }
    proxy_pass http://api_cluster;
}

# 显式拦截带请求体的 GET(常见于扫描器探测)
if ($request_method = GET) {
    if ($content_length) {
        return 400;
    }
}

请求头校验适合用 map 把「非法特征」映射为拒绝。例如很多扫描器会携带伪造的 Content-Length: 0、异常的 Transfer-Encoding 或超大的 Accept-Encoding 头,接入层可针对这些已知畸形特征做拦截。更常见的是校验 Host 头:把 Host 头白名单化,能有效防止 Host 头注入与 DNS 重绑定攻击:

# 只允许白名单 Host,其余一律断开连接(444)
map $http_host $is_allowed_host {
    hostnames;
    default 0;
    .example.com      1;
    api.example.com   1;
    admin.example.com 1;
}

server {
    listen 80 default_server;
    server_name _;
    if ($is_allowed_host = 0) {
        return 444;   # 444 表示直接断开连接,不给任何响应
    }
}

return 444 是 Nginx 特有的非标准状态码,直接关闭连接,扫描器得不到任何 HTTP 响应,减少了探测反馈。对于业务服务所在的 443 端口,同样应配置 Host 白名单,避免攻击者用 IP 直连绕过虚拟主机的鉴权逻辑。

URI 规范化同样重要。../、%2e%2e 编码穿越、双斜杠等路径变体会绕过部分业务层的鉴权判断,接入层应拒绝这类畸形路径。Nginx 的 merge_slashes 能把 // 合并为 /,缓解一部分问题,更彻底的做法是把含编码穿越特征的请求直接拒绝:

# 拦截编码穿越与畸形路径
location ~ (\.\./|\.\.%2f|%2e%2e|//) {
    return 444;
}

# 拦截超长 URI 与参数(防哈希碰撞与解析耗尽)
large_client_header_buffers 4 8k;
client_header_buffer_size 4k;

请求过滤的原则是「默认拒绝,显式放行」,把过滤规则沉淀成配置资产,与业务路由规则一起评审、一起发布。过滤规则要避免「一刀切」:每个规则上线前先评估对正常流量的影响,命中日志单独收集,定期复盘误杀比例。

3. CC 防护与连接限制

一句话总结: limit_req 与 limit_conn 分别控制请求速率与并发连接,配合超时与请求体大小限制,可有效压制 CC 攻击与慢速攻击。

CC 攻击的本质是用大量低频请求或大量并发连接耗尽服务器资源。Nginx 的 limit_req(漏桶)与 limit_conn(并发连接)是两道核心防线,上一篇文章《Nginx 限流与安全防护》已详细讲过漏桶参数,这里给出安全视角的组合配置:

limit_req_zone $binary_remote_addr zone=cc_req:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=cc_conn:10m;

server {
    location / {
        # 每 IP 每秒 10 次,允许 20 个突发后开始丢弃
        limit_req zone=cc_req burst=20 nodelay;
        # 每 IP 同时最多 30 个连接
        limit_conn cc_conn 30;
        # 触发限流时返回 429
        limit_req_status 429;
        limit_conn_status 429;

        proxy_pass http://web_cluster;
    }
}

limit_req_status 429 与 limit_conn_status 429 让被限流的请求以标准的 429 状态码返回,客户端可以据此做退避重试,而不是收到 503 或连接被直接切断。429 响应建议再带上 Retry-After 头,配合 add_header 下发,客户端就能合理安排重试时机。

CC 防护还要防慢速攻击(Slowloris)。Slowloris 通过「连接建立后不发送完整请求体」的方式长期占用 worker 连接,导致连接池耗尽。Nginx 默认的 client_header_timeout、client_body_timeout、send_timeout 已经提供了基础防护,但需要显式收紧并限制请求体大小:

http {
    # 慢速攻击防护:收紧各类超时
    client_header_timeout 10s;
    client_body_timeout   10s;
    send_timeout          10s;
    keepalive_timeout     15s;

    # 限制请求体大小,拒绝超大上传与畸形分块
    client_max_body_size 10m;

    # 丢弃分块请求的空分块(防 chunked 攻击)
    chunked_transfer_encoding on;
}

连接限制要同时考虑 Nginx 与后端的容量。limit_conn 只是 Nginx 侧的准入控制,后端连接池同样有限,因此限流参数要与后端承载能力对齐,而不是拍脑袋定值。CC 防护上线后要监控限流触发量:如果大量正常用户被误杀,说明阈值过低;如果限流几乎没有触发而资源仍在耗尽,则问题可能在应用层而非接入层。限流本身会消耗 CPU 与共享内存,压测时要包含限流触发路径,确认限流模块不会成为新的瓶颈。

4. 安全响应头

一句话总结: 通过 add_header 统一下发 CSP、HSTS、X-Frame-Options 等安全响应头,在浏览器侧建立基础防护边界。

安全响应头是浏览器侧的第一道防线,它们不消耗后端资源,却能把 XSS、点击劫持、MIME 嗅探等风险挡在渲染层。Nginx 在 server 块统一下发这些头是最省事、最不易遗漏的做法:

server {
    # 点击劫持防护:禁止页面被 iframe 嵌入
    add_header X-Frame-Options "SAMEORIGIN" always;
    # MIME 嗅探防护:禁止浏览器猜测响应类型
    add_header X-Content-Type-Options "nosniff" always;
    # 强制 HTTPS:一年内所有子域都走 HTTPS
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    # 控制 Referer 泄露范围
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    # 关闭浏览器高级能力,收窄攻击面
    add_header Permissions-Policy "geolocation=(), camera=(), microphone=(), payment=()" always;
}

add_header 的 always 参数很重要:不带 always 时,4xx/5xx 响应不会携带这些头,错误页也就失去了保护。另一个坑是 add_header 的继承规则:一旦在 location 里新增了任何 add_header,该 location 就不会再继承 server 块里定义的其它 add_header。因此要么把安全头统一放在 http 块,要么每个 location 都补齐全套,避免某条路由悄悄丢失安全头。

内容安全策略(CSP)是其中最有分量也最需要调校的一项,它通过白名单控制页面可以加载的脚本与样式来源。一份保守的 CSP 如下:

# 严格 CSP:只允许同源脚本与样式
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'" always;

CSP 上线前务必先在 report-only 模式下观察一段时间,确认没有破坏正常业务后再切换为强制模式:

# report-only 模式:只上报违规不拦截
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; report-uri /csp-report" always;

frame-ancestors 'none' 与 X-Frame-Options 的职责重叠,现代浏览器优先支持 CSP 的 frame-ancestors,两者同时下发不会冲突。安全响应头的维护与业务耦合度低,可以作为站点的统一基线配置,随加固清单一起定期复查。如果站点引用了第三方 CDN 脚本或内联脚本,CSP 白名单要相应放开,但每放开一项都意味着攻击面的扩大,需要权衡。

5. 防爬与恶意流量

一句话总结: 用 map 按 UA、Referer、来源 IP 等特征分流恶意流量,配合动态封禁脚本把已知爬虫与扫描器挡在接入层。

搜索引擎爬虫是良性流量,但暴力采集、撞库、扫描器、数据抓取工具则是恶意流量。接入层过滤恶意流量的核心是「特征识别 + 分流拒绝」。UA(User-Agent)是最常见的识别维度,用 map 维护一份可扩展的黑名单:

# 按 UA 特征识别恶意爬虫
map $http_user_agent $bad_bot {
    default 0;
    "~*curl"                    1;
    "~*(python|scrapy|libwww)"  1;
    "~*(sqlmap|nikto|nmap|masscan)" 1;
    "~*(ahrefs|semrush|mj12bot)" 1;   # 激进采集的 SEO 爬虫可按需拦截
    "~*(wget|java|okhttp)"      0;    # 部分合法下载工具,谨慎处理
}

server {
    if ($bad_bot = 1) {
        return 403;
    }
    location / { proxy_pass http://web_cluster; }
}

注意 map 的值与 if 的配合:if ($bad_bot = 1) 是 Nginx 中少数被官方支持的合法用法(与 return 搭配),不应在 if 里做复杂逻辑。UA 黑名单要避免误伤:很多采集工具会用浏览器 UA 伪装,因此 UA 过滤只能拦截「诚实暴露特征」的爬虫,真正的防御还需要结合行为分析。

Referer 与来源 IP 是另外两个维度。盗链与采集往往带有固定的 Referer 特征,可用 valid_referers 校验;扫描器往往集中在特定网段,可结合 geo 模块按 IP 归属地或网段封锁:

# geo 模块:按网段封禁
geo $blocked_region {
    default 0;
    10.20.0.0/16    1;   # 内部异常网段
    203.0.113.0/24  1;   # 示例恶意网段
}

server {
    if ($blocked_region = 1) {
        return 403;
    }
}

# 防盗链:只允许本站与空 Referer(直接输入 URL)访问
location ~* \.(gif|jpg|png|webp)$ {
    valid_referers none blocked server_names *.example.com;
    if ($invalid_referer) {
        return 403;
    }
}

静态特征只能拦截已知流量,动态封禁才能应对变化中的恶意来源。生产实践中常见的做法是把 Nginx 访问日志喂给分析程序,识别出高频恶意 UA 或高频 403 来源 IP,再通过管理接口动态更新 Nginx 的封禁配置并 reload。如果使用 OpenResty,可以直接用 lua_shared_dict 在内存中维护动态封禁名单,无需 reload 即可生效(后续《Lua 扩展与 OpenResty》一文会展开)。恶意流量过滤要避免误伤:把 UA 过滤做成 map 白名单式管理,每个新特征都先观察命中率再决定是否启用。

6. 目录与敏感资源防护

一句话总结: 禁止目录列举、屏蔽隐藏文件与敏感路径(.git/.env/备份文件),并把敏感文件访问重定向到 404 以迷惑攻击者。

很多数据泄露源于「敏感文件被直接访问」:.git 目录泄露源码、.env 泄露密钥、备份文件泄露数据库。接入层应当把这些路径一律挡在门外。最稳妥的做法是返回 404 而非 403,因为 403 会告诉攻击者「路径存在但不允许访问」,等于给了探路的回应。

server {
    # 禁止目录列举
    autoindex off;

    # 隐藏文件与敏感目录:一律 404
    location ~ /\. {
        deny all;
        return 404;
    }

    # 敏感后缀:备份、SQL 导出、压缩包、编辑器临时文件
    location ~* \.(env|bak|sql|conf|log|tar|gz|zip|swp|old|~)$ {
        deny all;
        return 404;
    }

    # 版本库与密钥目录
    location ~ ^/(\.git|\.svn|\.hg|\.env) {
        deny all;
        return 404;
    }
}

正则 location 的优先级需要留意:location ~ 的优先级高于普通前缀 location,但低于 location =。上述规则应当放在 location / 之前定义,确保命中敏感路径时先走正则拒绝。对于静态文件目录,还应在 root 层面保证 Web 根目录之外的文件不可被请求,例如备份文件放在 root 之外,仅通过专用入口下载。

另一个常被忽略的点是 location 的精确匹配与 alias 的目录穿越。使用 alias 时若路径末尾的斜杠处理不当,可能造成目录穿越读取任意文件。下面的写法是安全基线:

# alias 与 location 的斜杠必须一致,防止目录穿越
location /static/ {
    alias /srv/web/static/;   # 两端都以 / 结尾
}

# 使用 root 时无需担心该问题,root 会把 location 路径拼到末尾
location /static/ {
    root /srv/web;
}

敏感资源防护要定期巡检。加固清单本身也会过时:新框架会引入新的敏感路径(如 Laravel 的 .env、Spring Boot 的 actuator、配置中心的 /config 端点),因此建议把「敏感路径黑名单」做成可扩展列表,配合扫描器定期验证,确保新增的敏感路径及时进入拒绝名单。Git 提交前用 git-secrets 或 pre-commit 钩子扫描,从源头杜绝敏感文件进入版本库。

7. 传输安全与纵深防御

一句话总结: TLS 基线、与 WAF/防火墙联动、日志审计构成纵深防御,接入层加固只是整体安全的其中一环。

前面的加固都发生在 HTTP 应用层,传输层安全同样不能缺席。TLS 基线建议:只启用 TLS 1.2 与 TLS 1.3、禁用弱密码套件、开启 HSTS 与 OCSP Stapling。这些内容在《Nginx HTTPS 与 TLS 加固》与《Nginx TLS 进阶与 mTLS》中有完整实践,这里给出一份可直接套用的基线:

server {
    listen 443 ssl;
    http2 on;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;
    add_header Strict-Transport-Security "max-age=31536000" always;
}

ssl_session_tickets off 会在每次会话结束后禁用 TLS 会话票据,避免会话票据被截获后冒充会话。对于需要会话复用的场景,使用共享内存的 ssl_session_cache 即可,无需会话票据。TLS 基线的完整参数(证书链、OCSP、mTLS)建议直接参考专题内的 TLS 专项文章。

接入层加固不能替代纵深防御。Nginx 之外的防线包括:云防火墙与安全组限定暴露端口;WAF(如 ModSecurity,或 OpenResty 生态的 lua-resty-waf)识别应用层攻击签名;日志审计发现异常访问模式。Nginx 在其中承担「流量闸门」的角色,它做粗粒度过滤,WAF 做应用层签名检测,安全组做网络层隔离,三层互补。

# 日志审计:把被拦截请求单独记录,便于分析误杀与绕过
log_format blocked '$remote_addr - $request_method $request_uri '
                   '$status $http_user_agent';

server {
    set $blocked_log off;
    if ($bad_bot = 1) { set $blocked_log on; }

    access_log /var/log/nginx/blocked.log blocked if=$blocked_log;
}

纵深防御还要求「可观测」。加固之后要能回答三个问题:哪些请求被拦截了?拦得对不对?有没有绕过?这需要把 Nginx 拒绝日志单独落盘,定期分析拒绝样本与误杀比例。安全加固不是一次性动作,而是「加固 → 观测 → 调整 → 再加固」的持续循环。每次调整都先在小流量上验证,确认无误后再全量生效。

8. 总结

加固项关键配置作用
指纹隐藏server_tokens off、定制错误页、清除组件头提高攻击者信息收集成本
请求过滤limit_except、Host 白名单、URI 规范化拦截非法方法与畸形请求
CC 防护limit_req、limit_conn、收紧超时与请求体压制 CC 与慢速攻击
安全响应头CSP、HSTS、X-Frame-Options 等浏览器侧基础防护
防爬过滤UA/Referer/IP 特征 map + geo拦截已知爬虫与扫描器
敏感资源目录列举关闭、敏感路径 404防止源码与密钥泄露
传输安全TLS 1.2/1.3 基线 + HSTS保障传输机密性
纵深防御与 WAF、安全组、日志审计联动多层互补降低整体风险

Nginx 安全加固的价值在于把大量低成本的攻击拦截在业务代码之前,让后端的每一分计算都服务于真实业务。加固要遵循「默认拒绝、显式放行」的原则,同时警惕过度加固导致的误伤——安全配置上线前要结合真实流量验证,上线后要持续观测拦截样本。安全不是一堆配置的堆砌,而是「收敛攻击面、持续观测、快速响应」的循环过程。接下来可以把限流、TLS、mTLS 等安全主题串起来,形成完整的网关安全体系,再结合日志与监控把安全事件的证据链补全。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「nginx」更多文章

  1. Nginx 在服务网格中的角色:边车代理、mTLS 与 Envoy 取舍
  2. Nginx 大文件上传与请求体处理:缓冲、临时文件与断点续传
  3. Nginx 证书自动化与 ACME:certbot、DNS-01 通配符与自动续期