Ubuntu和Debian上的Nginx安全加固
在默认配置之上加固Nginx:安全头、TLS 1.3、HSTS、HTTP方法限制和访问控制。每条指令都关联到它所防御的具体攻击。
默认的Nginx安装能提供流量服务,但不会保护流量。默认配置会泄漏版本信息、接受任何HTTP方法、不发送安全头,并使用OpenSSL提供的TLS设置。
本指南在Debian 12和Ubuntu 24.04上加固Nginx。每个章节先说明威胁,然后展示阻止它的指令。你可以在运行中的服务器上应用这些更改,无需停机。
前提条件:
- Nginx已安装并至少通过HTTPS提供一个站点(在Debian 12和Ubuntu 24.04上为Nginx配置Let's Encrypt SSL/TLS证书)
- root或sudo权限
- 对Nginx配置结构有基本了解(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.conf的http块中添加server_tokens off;。这会从Server响应头和默认错误页面中移除版本号。攻击者会扫描具有已知CVE的特定版本。隐藏版本不能修复漏洞,但会增加定向攻击的成本。
在/etc/nginx/nginx.conf的http块内添加:
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块中添加头,server或http级别的所有安全头将从该location的响应中消失。
两种处理方式:
- Nginx 1.29.3+: 在子块中使用
add_header_inherit merge;以保留父级头。 - 旧版本: 在任何添加自己头的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.conf的http块中添加):
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 Forbidden或HTTP/1.1 405 Not Allowed。
curl -sI -X GET https://your-domain.com
返回HTTP/1.1 200 OK(或你的正常响应代码)。
如何按IP地址限制管理路径的访问?
管理面板、监控仪表板和状态端点不应从公共互联网访问。Nginx的allow和deny指令在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从上到下评估allow和deny,在第一个匹配处停止。将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}"
应返回403或405。
外部审计可将域名提交到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.conf的http块中设置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证书