0x00 前言与威胁模型
在公网暴露的任何 Web 入口,都会在几分钟内被来自全球的自动化扫描器(如 Nuclei、Nikto、Sqlmap、Gobuster 等)常态化轮询。默认配置下的 Nginx 存在以下典型隐患:
-
指纹完全泄露:
Server: nginx/1.24.x响应头直接暴露出微版本号,攻击者可针对性检索 CVE 进行精准打靶。 -
敏感工程资产外溢:CI/CD 部署遗漏或误解压导致的
.git、.env、备份文件(.sql、.tar.gz、.bak)直接暴露,容易引发源码及数据库凭据泄露。 -
恶意扫描侵蚀后端:大并发扫描和暴力破解不仅耗尽 PHP-FPM / upstream 资源池,还会造成日志膨胀与带宽资源浪费。
虽然 Nginx 原生规则不能完全替代商用或开源 WAF(如 ModSecurity / Coraza / 商业云 WAF),但以不到 40 行的零侵入配置,封死最低成本的攻击面,是性价比极高的工程化实践。
0x01 变更前:30 秒现状巡检(Ops Healthcheck)
在部署变更前,建议先在终端执行以下检查,确认当前节点的暴露状态:
1. 检查版本指纹暴露
Bash
curl -sI https://your-domain.com | grep -i '^server:'
-
风险表现:返回
Server: nginx/1.24.0(包含小版本号)。 -
预期安全表现:返回
Server: nginx或自定义混淆字符。
2. 探针文件扫描探测
Bash
for p in /.git/config /.env /db.sql /backup.zip /dump.sql; do
printf "%-16s -> HTTP %s\n" "$p" "$(curl -s -o /dev/null -w '%{http_code}' https://your-domain.com$p)"
done
-
风险表现:返回
200(高危直接下载)或403(说明文件真实存在但被权限拦截,同样是信息泄露)。 -
安全表现:均返回统一的
404或规则主动阻断的403。
3. 查看近期访问日志中的恶意 UA 痕迹
Bash
grep -iE 'sqlmap|nikto|nmap|acunetix|nessus|nuclei|gobuster|dirbuster|wpscan|masscan' \
/var/log/nginx/access.log | awk '{print $1, $12}' | sort | uniq -c | sort -rn | head -10
0x02 推荐架构:模块化 Snippets 目录设计
为了保证多 vhost / server 配置的一致性与可维护性,强烈建议不要将所有规则全部硬编码在单文件里,推荐将其抽离为通用片段:
Plaintext
/etc/nginx/
├── nginx.conf
└── conf.d/
└── snippets/
├── security_core.conf # 注入 http {}
└── security_server.conf # 注入 server {}
0x03 核心加固配置实施
片段一:全局基础与上下文(注入 http {} 块)
新建 /etc/nginx/conf.d/snippets/security_core.conf:
Nginx
# 1. 隐藏 Nginx 详细版本信息与错误页版本号
server_tokens off;
# 2. 恶意 User-Agent 映射字典(利用高效哈希表匹配,O(1) 效率优于纯正则匹配)
map $http_user_agent $is_malicious_scanner {
default 0;
"~*(sqlmap|nikto|nmap|masscan|acunetix|nessus|nuclei|xray|wpscan|gobuster|dirbuster|zgrab|curl/.*python)" 1;
}
# 3. 流量控制 Zone(基于二进制远程 IP 内存表,降低内存占用)
# 全局请求速率:每秒 10 个请求,10MB 内存空间约可容纳 16 万 IP 状态
limit_req_zone $binary_remote_addr zone=global_req_limit:10m rate=10r/s;
# 核心认证接口更严格的限流区(如登录、发信接口)
limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=1r/s;
# IP 连接数上限限制
limit_conn_zone $binary_remote_addr zone=ip_conn_limit:10m;
片段二:虚拟主机级防御拦截(注入 server {} 块)
新建 /etc/nginx/conf.d/snippets/security_server.conf:
Nginx
# -------------------------------------------------------------
# 1. 关键例外放行:Let's Encrypt / ACME 挑战放行
# 必须使用 ^~ 前缀匹配,抢在后续正则 location 之前生效,避免续签被误伤阻断
# -------------------------------------------------------------
location ^~ /.well-known/acme-challenge/ {
allow all;
access_log off;
}
# -------------------------------------------------------------
# 2. 敏感隐藏目录与工程配置文件阻断 (.git, .env, .svn 等)
# -------------------------------------------------------------
location ~* /\.(git|svn|hg|env|DS_Store) {
deny all;
access_log off;
log_not_found off;
}
# -------------------------------------------------------------
# 3. 备份、脚本、数据库转储后缀阻断
# -------------------------------------------------------------
location ~* \.(bak|old|orig|sql|zip|rar|tar|gz|log|ini|sh|swp|dump)$ {
deny all;
access_log off;
log_not_found off;
}
# -------------------------------------------------------------
# 4. 扫描器 User-Agent 与异常 HTTP 请求方法阻断
# -------------------------------------------------------------
# 阻断恶意扫描器
if ($is_malicious_scanner) {
return 403;
}
# 仅放行生产标准 HTTP 请求方法(按需开放 PUT/DELETE)
if ($request_method !~ ^(GET|HEAD|POST)$) {
return 405;
}
# -------------------------------------------------------------
# 5. 限速与并发限制配置
# -------------------------------------------------------------
limit_req zone=global_req_limit burst=20 nodelay;
limit_conn ip_conn_limit 20;
limit_req_status 429;
limit_conn_status 429;
# -------------------------------------------------------------
# 6. HTTP 安全防护响应头 (Security Headers)
# -------------------------------------------------------------
# 点击劫持防御
add_header X-Frame-Options "SAMEORIGIN" always;
# 禁止 MIME 类型嗅探
add_header X-Content-Type-Options "nosniff" always;
# 规避敏感参数随 Referer 外泄
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# 仅 HTTPS 站点启用:强制 1 年 HSTS,包含子域名
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
0x04 生产环境“大坑”排错指南(SRE Gotchas)
在生产推广这套配置时,需要特别警惕以下三个运维踩坑点:
| 问题现象 | 根因剖析 | 运维解决对策 |
add_header 丢失失效 |
Nginx 原生继承机制缺陷:如果子 location(如 location /api 或 location ~ \.php$)内部声明了任何 add_header,父级 server 块的所有 add_header 将被彻底丢弃覆盖。 |
将全局安全头单独保存为 security_headers.conf,在包含额外 Header 的子 location 内执行 include snippets/security_headers.conf;。 |
| Let's Encrypt 证书续签失败 | Certbot 挑战路径为 /.well-known/acme-challenge/...,命中以 . 开头的正则黑名单直接返回 403。 |
确保 location ^~ /.well-known/acme-challenge/ 处于正则规则之前,^~ 修饰符会强制停止后续正则匹配。 |
| 静态资源或前端 SPA 报 429 | 并发资源较多时,单个页面请求 30+ 静态资源瞬间耗尽 burst=20 队列,导致页面图片或 CSS 破损。 |
对 `location ~* .(js |
0x05 发布上线与校验流程
上线前遵循严格的语法检查 -> 热重载 -> 外部黑盒回归流程:
Bash
# Step 1: 必须通过配置语法自检
sudo nginx -t
# Step 2: 平滑重载(不中断现有活跃连接)
sudo nginx -s reload
黑盒自动化验证脚本
复制并在控制台执行以下单行回归测试:
Bash
DOMAIN="your-domain.com"
echo "=== 1. 验证版本指纹 ==="
curl -sI "https://$DOMAIN" | grep -i '^server:'
echo "=== 2. 验证敏感路径拦截 (预期 403) ==="
curl -sI "https://$DOMAIN/.git/config" | head -n 1
curl -sI "https://$DOMAIN/database.sql" | head -n 1
echo "=== 3. 验证恶意扫描器拦截 (预期 403) ==="
curl -sI -A "sqlmap/1.7" "https://$DOMAIN" | head -n 1
echo "=== 4. 验证异常 HTTP 请求方法 (预期 405) ==="
curl -sI -X TRACE "https://$DOMAIN" | head -n 1
echo "=== 5. 验证安全响应头 ==="
curl -sI "https://$DOMAIN" | grep -iE 'x-frame|x-content-type|referrer|strict-transport'
0x06 生产发布检查清单(Release Checklist)
-
[ ]
nginx -t测试输出syntax is ok与test is successful。 -
[ ] 站点响应头中的
Server字段仅显示nginx,无版本号。 -
[ ] 探针路径
/.git/、.env、*.sql均能被有效阻断。 -
[ ] 伪装扫描器 UA(如
sqlmap)访问能准确返回403 Forbidden。 -
[ ] 非标准请求方法返回
405 Method Not Allowed。 -
[ ] 证书签发路径
/.well-known/acme-challenge/访问未被误拦。 -
[ ] 静态资源密集页面(包含多张图片与 CSS)加载顺畅,无误伤的
429 Too Many Requests。
文章评论