Ubuntu和Debian上的Nginx安全加固

3 分钟阅读·Matthieu·debiannginxubuntutlssecurityhardening|

在默认配置之上加固Nginx:安全头、TLS 1.3、HSTS、HTTP方法限制和访问控制。每条指令都关联到它所防御的具体攻击。

默认的Nginx安装能提供流量服务,但不会保护流量。默认配置会泄漏版本信息、接受任何HTTP方法、不发送安全头,并使用OpenSSL提供的TLS设置。

本指南在Debian 12和Ubuntu 24.04上加固Nginx。每个章节先说明威胁,然后展示阻止它的指令。你可以在运行中的服务器上应用这些更改,无需停机。

前提条件:

我们将所有加固指令存储在一个include文件中。这样可以保持server块整洁,便于审计:

sudo touch /etc/nginx/snippets/security-hardening.conf

每个需要加固的server块只需添加一行:

include /etc/nginx/snippets/security-hardening.conf;

本指南中每次修改后,测试并重新加载:

sudo nginx -t && sudo systemctl reload nginx

如何隐藏Nginx服务器版本?

/etc/nginx/nginx.confhttp块中添加server_tokens off;。这会从Server响应头和默认错误页面中移除版本号。攻击者会扫描具有已知CVE的特定版本。隐藏版本不能修复漏洞,但会增加定向攻击的成本。

/etc/nginx/nginx.confhttp块内添加:

server_tokens off;

这放在主配置中,不放在snippet里,因为它是全局生效的。

重新加载后,检查响应头:

curl -sI https://your-domain.com | grep -i server
Server: nginx

没有版本号。修改前显示的是类似Server: nginx/1.24.0的内容。

如何阻止发往未知主机名的请求?

当有人直接访问你服务器的IP地址或使用未识别的主机名时,Nginx会提供它找到的第一个server块。这为DNS重绑定攻击打开了大门,并让扫描器能够识别你的服务。

一个拒绝所有不匹配请求的默认server块可以解决这个问题。将它添加为Nginx加载的第一个server块:

sudo nano /etc/nginx/sites-available/00-default-deny.conf
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;

    server_name _;

    # Self-signed or snakeoil cert just to complete the TLS handshake
    ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
    ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;

    return 444;
}

状态码444是Nginx特有的。它会立即关闭连接而不发送响应。

启用它:

sudo ln -s /etc/nginx/sites-available/00-default-deny.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

在Debian 12和Ubuntu 24.04上,如果snakeoil证书缺失,安装ssl-cert包:

sudo apt install ssl-cert

通过直接访问IP来测试:

curl -sI -k https://YOUR_SERVER_IP

连接会以空响应或curl错误(退出码52)关闭。没有内容被提供。

2026年Nginx应该使用什么TLS配置?

使用Mozilla的Intermediate配置文件:TLS 1.2和1.3,配合前向保密的密码套件。这覆盖了Firefox 27和Android 4.4.2以来的客户端,同时淘汰了不安全的协议。TLS 1.0和1.1容易受到POODLE和BEAST攻击,自RFC 8996(2021年)起已被弃用。如果你的所有客户端都支持TLS 1.3,则使用Modern配置文件(仅ssl_protocols TLSv1.3;)。

TLS加固在2026年初变得更加紧迫。CVE-2026-1642(CVSS 5.9)表明Nginx的TLS upstream处理存在竞态条件,允许在握手完成前进行中间人注入。修复在Nginx 1.28.2和1.29.5中发布。使用nginx -v检查你的版本,必要时更新。

配置文件 协议 最老客户端 使用场景
Modern 仅TLS 1.3 Firefox 63、Chrome 70 API、现代Web应用
Intermediate TLS 1.2 + 1.3 Firefox 27、Android 4.4 通用服务器
Old TLS 1.0 + 1.1 + 1.2 + 1.3 IE 8(XP上) 仅限遗留兼容

为Intermediate配置文件中的DHE密码套件生成DH参数。这需要几分钟:

sudo openssl dhparam -out /etc/nginx/dhparam.pem 2048
sudo chmod 644 /etc/nginx/dhparam.pem

/etc/nginx/snippets/security-hardening.conf中添加TLS加固:

# TLS protocols - Mozilla Intermediate profile
ssl_protocols TLSv1.2 TLSv1.3;

# Cipher suites - Mozilla Intermediate profile (version 5.7)
# TLS 1.3 suites are configured automatically by OpenSSL
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305';

ssl_prefer_server_ciphers off;
ssl_dhparam /etc/nginx/dhparam.pem;

# Session settings
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;

为什么设置ssl_prefer_server_ciphers off 当列表中只有强密码时,客户端偏好能提供更好的性能。客户端会选择其硬件加速最好的密码。

为什么设置ssl_session_tickets off 会话票据使用服务器范围的密钥。如果该密钥泄漏,攻击者可以解密所有记录的会话。没有票据时,共享会话缓存仅在同一服务器上提供恢复,这对单服务器部署来说足够了。

OCSP stapling说明: Let's Encrypt于2025年8月关闭了其OCSP服务。如果你使用Let's Encrypt证书,请移除所有ssl_stapling指令。它们会在日志中产生警告。Let's Encrypt现在仅通过CRL发布吊销信息。

重新加载并从本地机器测试:

openssl s_client -connect your-domain.com:443 -tls1_2 </dev/null 2>/dev/null | grep "Protocol\|Cipher"
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-AES256-GCM-SHA384

确认TLS 1.1被拒绝:

openssl s_client -connect your-domain.com:443 -tls1_1 </dev/null 2>&1 | head -5

握手失败。完整审计可将域名提交到SSL Labs,目标是A或A+评级。

如何在Nginx中启用HSTS?

HSTS(HTTP Strict Transport Security)告诉浏览器在设定的时间内只通过HTTPS连接。没有它,中间人攻击者可以拦截初始HTTP请求并降级连接。浏览器一旦看到HSTS头,就会在max-age到期前拒绝向你的域发起明文HTTP连接。

/etc/nginx/snippets/security-hardening.conf中添加:

# HSTS - 2 years, include subdomains
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;

always标志确保Nginx在错误响应(4xx、5xx)时也发送此头。没有always,403或500响应将不包含该头,为降级攻击留下窗口。

添加includeSubDomains之前,确认你的所有子域都支持HTTPS。此指令适用于每个子域。没有有效证书的子域将无法访问。

HSTS预加载:preload添加到头中并将域名提交到hstspreload.org会在浏览器中硬编码HTTPS强制。这是永久的。移除需要数月。只有在确定每个子域都将始终提供HTTPS时才添加preload

重新加载后:

curl -sI https://your-domain.com | grep -i strict
strict-transport-security: max-age=63072000; includeSubDomains

每个Nginx服务器应该有哪些安全头?

五个响应头可以防御常见的浏览器端攻击:MIME嗅探、点击劫持、跨站脚本、referrer泄漏和浏览器API滥用。添加所有五个并使用always标志,使其也适用于错误响应。已弃用的X-XSS-Protection头不应包含。现代浏览器已移除其XSS审计器,该头本身在某些情况下反而引入了漏洞。

/etc/nginx/snippets/security-hardening.conf中添加:

# Prevent MIME type sniffing - stops browsers from interpreting files as a
# different content type than declared, blocking drive-by download attacks
add_header X-Content-Type-Options "nosniff" always;

# Clickjacking protection - prevents your pages from being embedded in
# iframes on other sites
add_header X-Frame-Options "SAMEORIGIN" always;

# Content Security Policy - controls which sources can load scripts, styles,
# images, and other resources. Start restrictive, loosen as needed.
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;

# Referrer Policy - controls how much URL information the browser sends
# when navigating away from your site
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# Permissions Policy - disables browser features you don't use, preventing
# compromised scripts from accessing camera, microphone, etc.
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;

interest-cohort=()出现在旧指南中用于阻止Google FLoC。FLoC已在2023年被放弃。现代浏览器不识别此指令,如果包含它会在控制台显示警告。

缓解的威胁
X-Content-Type-Options nosniff MIME混淆攻击、偷渡式下载
X-Frame-Options SAMEORIGIN 通过iframe嵌入的点击劫持
Content-Security-Policy default-src 'self' XSS、数据注入、未授权资源加载
Referrer-Policy strict-origin-when-cross-origin URL泄漏给第三方
Permissions-Policy camera=(), microphone=()... 注入脚本滥用浏览器API

CSP调整: 上面的示例很严格。大多数应用需要调整。如果你的站点从CDN加载脚本,添加它:script-src 'self' https://cdn.example.com;。使用浏览器开发者控制台查找CSP违规,逐步放宽策略。除非你理解风险,否则不要使用unsafe-eval

add_header继承陷阱

当子location块定义自己的add_header时,Nginx会丢弃父块的所有add_header指令。如果你在location块中添加头,serverhttp级别的所有安全头将从该location的响应中消失。

两种处理方式:

  1. Nginx 1.29.3+: 在子块中使用add_header_inherit merge;以保留父级头。
  2. 旧版本: 在任何添加自己头的location块中重复include /etc/nginx/snippets/security-hardening.conf;行。

重新加载并检查:

sudo nginx -t && sudo systemctl reload nginx
curl -sI https://your-domain.com | grep -iE "x-content-type|x-frame|content-security|referrer-policy|permissions-policy"

所有五个头都出现在输出中。还要检查特定的location路径,不仅仅是根路径,以发现继承问题。

如何在Nginx中禁用不安全的HTTP方法?

大多数站点只需要GET、HEAD和POST。DELETE、PUT、TRACE和OPTIONS(未使用时)等方法会扩大你的攻击面。TRACE尤其可以通过跨站追踪(XST)攻击泄漏cookie和认证令牌。

标准Nginx中最干净的方法是limit_except,专门为方法限制而设计。将它放在location块中:

location / {
    limit_except GET POST {
        deny all;
    }

    # ... your existing config
}

limit_except GET隐式允许HEAD(根据HTTP规范,HEAD是没有正文的GET)。任何未列出的方法返回403 Forbidden。

使用map的全局方法(在nginx.confhttp块中添加):

map $request_method $method_not_allowed {
    default  1;
    GET      0;
    HEAD     0;
    POST     0;
}

然后在server块中:

if ($method_not_allowed) {
    return 405;
}

当需要按location控制时,limit_except方法更好。当需要整个服务器的统一策略时,map方法更合适。

用被阻止的方法测试:

curl -sI -X DELETE https://your-domain.com

返回HTTP/1.1 403 ForbiddenHTTP/1.1 405 Not Allowed

curl -sI -X GET https://your-domain.com

返回HTTP/1.1 200 OK(或你的正常响应代码)。

如何按IP地址限制管理路径的访问?

管理面板、监控仪表板和状态端点不应从公共互联网访问。Nginx的allowdeny指令在location级别按IP地址限制访问。

location /admin {
    allow 203.0.113.10;      # Your office IP
    allow 198.51.100.0/24;   # Your VPN range
    deny all;

    # ... proxy_pass or other directives
}

location /nginx-status {
    stub_status;
    allow 127.0.0.1;
    allow ::1;
    deny all;
}

顺序很重要。Nginx从上到下评估allowdeny,在第一个匹配处停止。将deny all放在最后。

如果你的IP经常变化,考虑通过防火墙限制访问,或使用VPN。Nginx中基于IP的ACL是第二层防御,不是认证的替代品。

从允许的IP:

curl -sI https://your-domain.com/admin

返回正常响应(200、302等)。

从其他IP(或通过代理),同一请求返回HTTP/1.1 403 Forbidden

检查访问日志中被拒绝的请求:

sudo tail -5 /var/log/nginx/access.log | grep admin

哪些缓冲区和超时限制能防止Nginx中的DoS攻击?

默认的缓冲区大小和超时设置很宽松。攻击者可以利用这一点,发送大头部、慢请求或超大正文来占用worker连接。收紧这些值可以限制单个连接造成的损害。

/etc/nginx/snippets/security-hardening.conf中添加:

# Maximum allowed request body size - reject uploads larger than 10MB
client_max_body_size 10m;

# Buffer for reading client request body
client_body_buffer_size 16k;

# Buffer for reading large client headers
large_client_header_buffers 4 16k;

# Timeouts - how long Nginx waits for client data
client_body_timeout 30s;
client_header_timeout 30s;
send_timeout 30s;
keepalive_timeout 65s;
指令 过高 过低
client_max_body_size 10m 允许填满磁盘的大量上传 破坏文件上传表单
client_body_buffer_size 16k 每连接浪费内存 小POST请求强制使用临时文件
large_client_header_buffers 4 16k 头部攻击下浪费内存 破坏带大cookie/URL的应用
client_body_timeout 30s 慢速连接保持打开 断开慢速网络用户
client_header_timeout 30s 同上 同上
send_timeout 30s 为慢客户端占用连接 中断大文件下载
keepalive_timeout 65s 连接池耗尽 额外的TLS握手

根据你的应用调整client_max_body_size。如果处理文件上传,按应用期望设置。10 MB的默认值对大多数Web应用来说是合理的。

重新加载并测试正文大小限制:

dd if=/dev/zero bs=1M count=11 2>/dev/null | curl -s -X POST --data-binary @- -o /dev/null -w "%{http_code}" https://your-domain.com/

预期:413(Request Entity Too Large)。

关于速率限制和更深入的DDoS防护,参见Nginx限流与DDoS防护

如何测试完整的加固配置?

所有更改到位后,重新加载并检查关键点。从配置测试和重新加载开始:

sudo nginx -t && sudo systemctl reload nginx

用一个请求检查头和版本信息:

curl -sI https://your-domain.com

Server头应显示nginx而没有版本号。所有六个安全头(HSTS、X-Content-Type-Options、X-Frame-Options、Content-Security-Policy、Referrer-Policy、Permissions-Policy)都应出现在响应中。

对于TLS,从本地机器确认协议和密码:

openssl s_client -connect your-domain.com:443 </dev/null 2>/dev/null | grep -E "Protocol|Cipher"

你需要TLSv1.2或TLSv1.3,配合GCM或CHACHA20密码。

直接访问服务器IP以确认默认拒绝块正常工作:

curl -sk https://YOUR_SERVER_IP -o /dev/null -w "%{http_code}"

响应代码000表示连接在没有响应的情况下关闭,这是正确的。

尝试一个被阻止的HTTP方法:

curl -sI -X TRACE https://your-domain.com -o /dev/null -w "%{http_code}"

应返回403405

外部审计可将域名提交到SSL Labs(目标A或A+评级)和securityheaders.com获取头部专项报告。

完整的加固snippet

包含本指南所有内容的完整/etc/nginx/snippets/security-hardening.conf

# /etc/nginx/snippets/security-hardening.conf
# Nginx security hardening - include in each server block

# --- TLS Hardening (Mozilla Intermediate profile v5.7) ---
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305';
ssl_prefer_server_ciphers off;
ssl_dhparam /etc/nginx/dhparam.pem;

# TLS session settings
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;

# --- HSTS ---
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;

# --- Security Headers ---
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;

# --- Buffer Limits ---
client_max_body_size 10m;
client_body_buffer_size 16k;
large_client_header_buffers 4 16k;

# --- Timeouts ---
client_body_timeout 30s;
client_header_timeout 30s;
send_timeout 30s;
keepalive_timeout 65s;

记得还要在/etc/nginx/nginx.confhttp块中设置server_tokens off;,并单独创建默认拒绝server块。

故障排除

Nginx修改后无法重新加载:

sudo nginx -t

阅读错误信息。它会告诉你文件和行号。常见原因:缺少分号、指令名拼写错误、引用了不存在的DH参数文件。

某些路径缺少头:

add_header继承陷阱。如果location块有自己的add_header,所有父级头都会被丢弃。在该location块中包含snippet,或在Nginx 1.29.3+上使用add_header_inherit merge;

CSP阻止合法资源:

打开浏览器开发者控制台(F12)。查找Refused to load错误。它们会指出被阻止的资源和负责的CSP指令。逐步将来源添加到你的策略中。

TLS握手失败:

journalctl -u nginx -f

在连接时观察SSL错误。验证你的证书链是否完整:

openssl s_client -connect your-domain.com:443 -servername your-domain.com </dev/null 2>/dev/null | grep "Verify return code"

预期:Verify return code: 0 (ok)

日志位置:

# Error log
sudo tail -20 /var/log/nginx/error.log

# Real-time monitoring
sudo journalctl -u nginx -f

后续步骤: 服务器加固后,设置Nginx限流与DDoS防护来处理滥用流量模式。站点级配置参见Nginx 服务器块:在单台 VPS 上托管多个域名

相关文章: VPS上的Nginx管理 | Nginx配置文件结构详解 | 在Debian 12和Ubuntu 24.04上为Nginx配置Let's Encrypt SSL/TLS证书