Nginx 加固基线

一份可以直接抄走的 Nginx 安全基线配置——隐藏版本、默认服务器块拒绝未知 Host、敏感路径封堵、上传与超时限制、代理头处理,每一条都说明为什么。

作者 David更新于 第 10 篇(共 29 篇)

Nginx 加固基线

这一章给一份可以直接抄进配置的基线。每条都附上理由——照抄不理解,下次改动时就会把它删掉。

一、隐藏版本与指纹

http {
    server_tokens off;          # 响应头与错误页里不再出现具体版本号
    more_clear_headers 'Server';   # 需要 headers-more 模块;能完全去掉 Server 头
}

server_tokens off 之后,Server: nginx/1.24.0 变成 Server: nginx。

这不是真正的安全措施,是降噪。 有经验的攻击者能通过行为特征指纹识别版本。但它确实能让你从「自动化扫描器按版本号匹配 exp」的名单里掉出来——而那类流量占实际攻击的绝大多数。

同理要清理掉后端泄露的指纹头:X-Powered-By、X-AspNet-Version、X-Generator 等。

# 把后端泄露的指纹头统统抹掉
proxy_hide_header X-Powered-By;
proxy_hide_header X-AspNet-Version;
proxy_hide_header X-Runtime;

二、默认服务器块:拒绝未知 Host

这是最容易漏掉、也最有价值的一条。

如果没有 default_server,Nginx 会把「Host 头对不上任何 server_name」的请求交给第一个 server 块处理。后果是:攻击者直接用 IP 访问你的服务器,或者带一个伪造的 Host,照样能拿到你的站点内容——源站暴露那一章讲的直连问题在这里被放大。

# 放在所有 server 块之前:任何 Host 对不上的请求,直接断开
server {
    listen 80 default_server;
    listen 443 ssl default_server;
    server_name _;

    ssl_certificate     /etc/nginx/ssl/dummy.crt;   # 自签即可,反正要拒绝
    ssl_certificate_key /etc/nginx/ssl/dummy.key;

    return 444;    # Nginx 特有:直接关闭连接,不返回任何响应
}

444 比 403 更好:不给扫描器任何反馈,连「这里有个服务」都不告诉它。

验证:

# 用 IP 直连,Host 随便填 —— 应该连不上任何内容
curl -sI http://1.2.3.4/ -H "Host: whatever.invalid"

三、封堵敏感路径

# 隐藏文件与版本控制目录 —— 这类请求 100% 是扫描
location ~ /\. {
    deny all;
    access_log off;
    log_not_found off;
    return 404;
}

# 备份与源码残留
location ~* \.(bak|backup|old|orig|save|swp|sql|tar|tar\.gz|zip|log|ini|conf)$ {
    return 404;
}

# 常见的探测路径(就算你不是 PHP 站也要挡,纯粹为了降噪)
location ~* /(wp-admin|wp-login\.php|xmlrpc\.php|phpmyadmin|adminer) {
    return 404;
}

.git 泄露是最经典也最致命的一种。 如果 /.git/ 可访问,攻击者能用现成工具把整个仓库历史拉下来——包括你以为「已经删掉」的配置文件、密钥、内网地址。

自查:

curl -sI https://example.com/.git/config
curl -sI https://example.com/.env

返回 200 就是重大事故,而且要假设已经泄露:立刻轮换所有出现在仓库历史里的凭据。

注意上面第一条用了 location ~ /\.,它会连 .well-known 一起挡掉——而 ACME 证书申请需要它。所以要给个例外:

# 必须放在 location ~ /\. 之前(前缀匹配 ^~ 优先级高于正则)
location ^~ /.well-known/acme-challenge/ {
    root /var/www/certbot;
}

这是个真实的翻车点:加了隐藏文件封堵之后,证书自动续期静默失败,三个月后证书过期。

四、限制请求

client_max_body_size 10m;        # 按业务实际需要设,别用默认的 1m 也别无限
client_body_buffer_size 128k;
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;

client_body_timeout 10s;
client_header_timeout 10s;
send_timeout 10s;
keepalive_timeout 30s;
reset_timedout_connection on;

large_client_header_buffers 值得说一句:设太小,带很多 Cookie 的正常用户会收到 400;设太大,给了攻击者内存放大的空间。4 8k 是个稳妥起点。

五、只开需要的方法

# 静态站基本只需要这三个
if ($request_method !~ ^(GET|HEAD|POST)$) {
    return 405;
}

顺手挡掉 TRACE(跨站追踪)、PUT/DELETE(如果你没用到)。

六、代理头的正确处理

反向代理到后端时,这几个头决定了后端能不能拿到正确信息,以及攻击者能不能伪造它们:

location / {
    proxy_pass http://127.0.0.1:3000;

    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Host  $host;

    proxy_http_version 1.1;
    proxy_set_header Connection "";      # 保持后端长连接

    proxy_connect_timeout 5s;
    proxy_read_timeout 60s;
    proxy_send_timeout 60s;
}

proxy_set_header 是覆盖,不是追加——这一点救过很多人。

如果你不写 proxy_set_header X-Forwarded-For,客户端发来的 X-Forwarded-For 会原样透传给后端。攻击者随手加一个 X-Forwarded-For: 127.0.0.1,后端如果据此判断「这是内网请求,放行管理接口」——就直接被打穿了。

用 $proxy_add_x_forwarded_for 是在客户端值后面追加真实 IP;如果你的后端只信任最后一个值,这是对的。如果后端取第一个值,那必须改成 proxy_set_header X-Forwarded-For $remote_addr;(完全丢弃客户端传来的)。

搞清楚你的后端框架取哪一个,这是个真实的、被反复利用的分歧点。

七、TLS 与安全响应头

TLS 部分见证书那一章,响应头见安全响应头那一章。这里只强调一个 Nginx 特有的坑:

# 一定要加 always,否则 4xx/5xx 响应不会带上这些头
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;

add_header 在子块里会覆盖父块的全部 add_header,而不是合并。

server {
    add_header X-Frame-Options DENY always;

    location /api/ {
        add_header X-Custom "foo" always;   # ← server 块那条 X-Frame-Options 在这里失效了!
    }
}

这是 Nginx 最反直觉的行为之一,也是「我明明配了安全头,为什么某些路径没有」的头号原因。解决办法:把公共头抽成一个文件,在每个需要的 location 里 include 一次。

八、运行身份与文件权限

user www-data;      # 不要用 root 跑 worker
# 配置文件不该被 worker 用户写
chown -R root:root /etc/nginx
chmod -R 644 /etc/nginx/*.conf
chmod 600 /etc/nginx/ssl/*.key

# 站点目录:worker 只读即可
chown -R root:www-data /var/www/html
chmod -R 750 /var/www/html

为什么强调「只读」:如果 worker 对站点目录有写权限,一个文件上传漏洞就能变成 webshell。剥掉写权限,很多漏洞的利用链会直接断掉。

改完之后

nginx -t              # 语法检查,务必先跑
nginx -s reload       # 平滑重载,不断连接

验证清单:

D=example.com
echo "── 版本泄露 ──";      curl -sI https://$D/ | grep -i "^server\|x-powered-by"
echo "── 未知 Host ──";     curl -sI https://$D/ -H "Host: invalid.test" | head -1
echo "── .env ──";          curl -s -o /dev/null -w "%{http_code}\n" https://$D/.env
echo "── .git ──";          curl -s -o /dev/null -w "%{http_code}\n" https://$D/.git/config
echo "── 方法限制 ──";      curl -s -o /dev/null -w "%{http_code}\n" -X TRACE https://$D/
echo "── 安全头(404 页) ──"; curl -sI https://$D/不存在的路径 | grep -icE "strict-transport|x-frame|nosniff"

最后一条特意打 404 页——这是检验 always 有没有加对的最快方式。

小结

  • server_tokens off 是降噪不是安全,但值得做。
  • default_server 拒绝未知 Host 是最容易漏、价值最高的一条,用 444 直接断连。
  • 封堵 .git/.env 时别忘了给 .well-known/acme-challenge 开例外,否则证书续期会静默失败。
  • proxy_set_header 不写就是透传——X-Forwarded-For 伪造是真实攻击面,要搞清楚后端取第几个值。
  • add_header 在子块里是覆盖不是合并,这是安全头「时有时无」的头号原因。

下一章专讲那些「配置写错就出洞」的具体陷阱。👉 Nginx 配置陷阱

本页目录