Nginx 是 VPS 上最常见的 Web 入口:WordPress、Next.js、n8n、Gitea、面板、API 服务,最后大多都会挂到 Nginx 后面。
但很多 VPS 的 Nginx 配置只做到“能访问”:80 跳 443、反代到后端、证书能续期。真正上线后,风险往往藏在这些地方:TLS 版本太旧,安全响应头缺失,后台路径没有限速,源站 IP 暴露,错误页面泄露信息,访问日志没人看,Cloudflare 开了但源站仍然能被直连。
这篇文章不讲 Nginx 怎么安装,也不重复 502、521、证书续期这些排查。目标只有一个:把一台普通 VPS 上的 Nginx 从“能跑”加固到“更适合长期暴露公网”。
上线前建议按这个顺序检查:
- 只开放 80/443,后端服务不要直接暴露公网;
- TLS 只保留 TLS 1.2 / TLS 1.3;
- 开启 HSTS,但不要一上来就 preload;
- 添加基础安全响应头:CSP、X-Frame-Options、X-Content-Type-Options、Referrer-Policy;
- 隐藏 Nginx 版本号;
- 给登录、搜索、API、Webhook 做请求限速;
- 限制上传大小和超时时间;
- 如果接入 Cloudflare,只允许 Cloudflare 回源 IP;
- 日志要能看出攻击、限速和异常状态码;
- 用 SSL Labs、Mozilla Observatory、SecurityHeaders 做外部验证。
不要追求一次性把所有安全头拉满。安全配置的目标不是“复制一段最硬核配置”,而是在不影响业务的前提下逐步收紧。
很多人以为自己只开放了 Nginx,实际 Docker、Node、数据库、Redis 也在公网监听。
先在 VPS 上看监听端口:
ss -tulpen
重点看 0.0.0.0 和 ::。如果看到这些,就要警惕:
0.0.0.0:3000 # 应用端口直接暴露
0.0.0.0:5432 # PostgreSQL 暴露
0.0.0.0:6379 # Redis 暴露
0.0.0.0:3306 # MySQL 暴露
生产环境更推荐:
- Nginx 监听公网 80/443;
- 应用只监听
127.0.0.1或 Docker 内网; - 数据库、Redis 不映射公网端口;
- 管理后台走 VPN、SSH 隧道、白名单或至少加 Basic Auth。
Docker Compose 里不要这样:
ports:
- "3000:3000"
- "5432:5432"
更安全的方式是后端只暴露给 Nginx:
services:
app:
expose:
- "3000"
或者只绑本地:
ports:
- "127.0.0.1:3000:3000"
如果数据库已经暴露公网,先看这篇:
2026 年还在用 TLS 1.0 / 1.1 就不合适了。大多数小站直接保留 TLS 1.2 和 TLS 1.3 即可:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
为什么 ssl_prefer_server_ciphers off?因为现代 TLS 配置更依赖客户端和服务端协商,尤其 TLS 1.3 不再按老式 cipher 顺序工作。除非你非常清楚兼容性需求,不要复制十年前的长 cipher 列表。
如果你想生成更标准的配置,可以参考 Mozilla SSL Configuration Generator(现在迁移到 TLSRef Configurator)。生成时按站点受众选择:
| 场景 | 建议档位 |
|---|---|
| 普通博客、SaaS、工具站 | Intermediate |
| 只面向现代浏览器 | Modern |
| 必须兼容很老设备 | Old,但不推荐 |
证书续期失败不是加固问题,但会直接导致站点不可用。如果 certbot 经常失败,可以看:
HSTS 的作用是告诉浏览器:以后访问这个域名必须走 HTTPS,避免降级攻击。
基础配置:
add_header Strict-Transport-Security "max-age=31536000" always;
确认所有子域名都支持 HTTPS 后,再考虑:
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
不要一上来就加 preload。一旦进入浏览器 preload 列表,如果你以后有某个子域名暂时不能 HTTPS,移除过程会很慢,排障成本非常高。
正确流程是:
- 先确保主域名 HTTPS 正常;
- 开
max-age=300测试几分钟; - 再提高到一天、一周;
- 最后再提高到一年或两年;
- 确认所有子域名都支持 HTTPS 后再考虑
includeSubDomains。
如果你已经遇到 HTTPS 跳转循环,可以先排查这里:
OWASP Secure Headers Project 推荐用 HTTP 响应头减少浏览器侧风险。Nginx 里常见写法如下:
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
这些头的作用:
| Header | 作用 | 风险 |
|---|---|---|
X-Content-Type-Options | 防止浏览器 MIME 嗅探 | 通常安全 |
X-Frame-Options | 降低点击劫持风险 | 嵌入 iframe 的业务要谨慎 |
Referrer-Policy | 减少 Referer 泄露 | 统计系统可能受影响 |
Permissions-Policy | 禁用不需要的浏览器能力 | WebRTC/定位业务要单独放开 |
注意 always 很重要。Nginx 官方文档里 add_header 默认只对部分状态码生效,加上 always 后,错误响应也会带上这些头。
Content-Security-Policy 很强,但也最容易把网站搞坏。它可以限制脚本、图片、字体、iframe 从哪里加载,但 WordPress 插件、统计脚本、广告脚本、评论系统都可能被它拦掉。
建议从温和版本开始:
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data: https:; script-src 'self' 'unsafe-inline' https:; style-src 'self' 'unsafe-inline' https:; font-src 'self' data: https:; connect-src 'self' https:; frame-ancestors 'self'; upgrade-insecure-requests" always;
这不是最严格的 CSP,但适合作为第一版。上线后用浏览器控制台看是否有资源被拦,再逐步收紧。
更稳的做法是先用 Report-Only:
add_header Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report" always;
确认没有误伤后,再切换成正式 CSP。
隐藏 Nginx 版本号:
server_tokens off;
它能减少信息暴露,但不是核心防线。攻击者不会因为看不到版本号就停止扫描。真正重要的是及时更新系统和 Nginx:
apt update
apt list --upgradable | grep nginx
如果你使用的是宝塔、OpenResty、Nginx Proxy Manager,也要关注它们自己的更新节奏,不要只看系统包。
登录页、搜索页、API、Webhook、评论提交、密码重置接口,都适合做限速。
在 http 段定义限速区:
limit_req_zone $binary_remote_addr zone=login_zone:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api_zone:10m rate=10r/s;
在具体 location 使用:
location /login {
limit_req zone=login_zone burst=5 nodelay;
proxy_pass http://127.0.0.1:3000;
}
location /api/ {
limit_req zone=api_zone burst=20 nodelay;
proxy_pass http://127.0.0.1:3000;
}
Nginx 的 limit_req 使用漏桶算法。rate=5r/m 代表每分钟平均 5 次,burst=5 允许短时间突发,但超过后会被延迟或拒绝。
如果你不确定阈值,可以先 dry-run:
limit_req_dry_run on;
limit_req_log_level notice;
先观察日志,再正式拦截。否则很容易把真实用户也挡掉。
如果已经遭遇 CC 攻击,可以参考:
很多 VPS 出问题不是因为攻击很高级,而是因为上传、慢连接、超时没有边界。
基础配置:
client_max_body_size 20m;
client_body_timeout 15s;
client_header_timeout 15s;
keepalive_timeout 30s;
send_timeout 30s;
如果是后台上传图片,可以只对上传路径放宽:
location /admin/upload {
client_max_body_size 100m;
proxy_pass http://127.0.0.1:3000;
}
不要全站无脑设置 client_max_body_size 500m。上传越大,越容易被滥用,也越容易触发磁盘和内存压力。
如果遇到 413,可以看:
很多人开了 Cloudflare WAF,以为源站安全了。问题是:如果攻击者知道你的 VPS IP,可以绕过 Cloudflare 直接打源站。
加固思路:
- DNS 只暴露 Cloudflare 代理记录;
- 源站防火墙只允许 Cloudflare IP 访问 80/443;
- SSH 端口只允许自己的 IP 或走 VPN;
- 不要在邮件、历史 DNS、错误页面里泄露源站 IP。
UFW 示例:
ufw default deny incoming
ufw allow OpenSSH
ufw allow from 173.245.48.0/20 to any port 443 proto tcp
ufw allow from 103.21.244.0/22 to any port 443 proto tcp
ufw enable
Cloudflare IP 段会变化,生产环境建议写脚本定期同步官方 IP 列表,不要长期手动维护。
下面是一份适合普通 VPS 反代应用的起点模板:
server_tokens off;
limit_req_zone $binary_remote_addr zone=login_zone:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api_zone:10m rate=10r/s;
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
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;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data: https:; script-src 'self' 'unsafe-inline' https:; style-src 'self' 'unsafe-inline' https:; font-src 'self' data: https:; connect-src 'self' https:; frame-ancestors 'self'; upgrade-insecure-requests" always;
client_max_body_size 20m;
client_body_timeout 15s;
client_header_timeout 15s;
keepalive_timeout 30s;
send_timeout 30s;
location /login {
limit_req zone=login_zone burst=5 nodelay;
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;
}
location /api/ {
limit_req zone=api_zone burst=20 nodelay;
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;
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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;
}
}
上线前必须测试:
nginx -t
systemctl reload nginx
curl -I https://example.com
加固不是写完配置就结束。至少用这些工具验证:
| 工具 | 看什么 |
|---|---|
| SSL Labs Server Test | TLS 版本、证书链、评级 |
| Mozilla Observatory | 安全响应头、CSP、HSTS |
| SecurityHeaders.com | 响应头是否缺失 |
| curl -I | 实际响应是否带 header |
| nginx -t | 配置语法是否正确 |
本机检查:
curl -I https://example.com | grep -Ei "strict|content-security|x-frame|referrer|permissions"
如果你用了 Cloudflare,要同时测:
- 通过 Cloudflare 访问的域名;
- 直连源站 IP(如果能直连,说明源站保护还没做好);
- 真实业务页面,不要只测首页。
不是。响应头要服务业务。CSP、COOP、COEP 这类头可能影响第三方脚本、iframe、支付、统计和登录。如果不理解影响,先用基础头,再逐步收紧。
需要。Cloudflare 是第一层,Nginx 是源站最后一道门。尤其是后台、API、Webhook 这种业务路径,Nginx 本地限速能避免规则遗漏或源站被直连。
普通 VPS 小站不建议一开始就开。先确认所有子域名都有 HTTPS,再逐步增加 max-age。preload 一旦误入列表,恢复很慢。
只能减少信息暴露,不能替代升级。真正重要的是及时更新 Nginx、OpenSSL、系统内核和上游应用。
Caddy 自动 HTTPS 做得很好,但安全响应头、限速、源站保护、后台访问控制仍然需要按业务配置。Caddy 省掉的是证书管理,不是所有安全工作。
VPS 上的 Nginx 安全加固,不是把网上的“最强配置”复制一遍,而是按风险顺序收口:先减少暴露面,再修 TLS,再补安全响应头,再给高风险路径限速,最后用外部工具验证。
对大多数小站、自托管工具和独立开发者来说,做到这些已经能避开大量低成本扫描、错误配置和源站直连风险。真正长期稳定的 VPS,不是永远不被攻击,而是每一层都有边界、日志和回滚空间。
