上线安全检查清单
把全专栏的结论压成一份可以逐条打勾的清单——按「今天就做」「这周做」「有余力再做」分三档,附一段可以直接跑的外部体检脚本。
作者 David更新于 第 29 篇(共 29 篇)
上线安全检查清单
全专栏的结论压成一页。按投入产出比分三档,从上往下做。
第一档:今天就做(1~2 小时,挡住绝大多数自动化攻击)
域名与账号
- 域名注册商账号:独立随机密码 + 2FA(优先硬件密钥/TOTP,别用纯短信)
- 注册邮箱不在本域名下(否则域名一丢,找回通道同时被切断)
-
whois确认clientTransferProhibited等状态已开 - 自动续费已开,且付款方式未过期
- DNS 托管商账号同样开 2FA
暴露面
-
curl -sI https://你的域名/.env→ 404 -
curl -sI https://你的域名/.git/config→ 404 - 数据库、Redis、Docker API 没有对公网开放
- 应用端口只绑回环(
127.0.0.1:xxxx),由本机反代访问 - Nginx 有
default_server拒绝未知 Host(return 444)
响应头(几行配置,收益最大)
-
Strict-Transport-Security(先max-age=300试跑,别一上来就preload) -
X-Content-Type-Options: nosniff -
Referrer-Policy: strict-origin-when-cross-origin -
X-Frame-Options: DENY或 CSP 的frame-ancestors 'none' - 每条都带
always,且用 404 页验证过
凭据
- 跑一次
gitleaks detect --source .扫全历史 - 构建产物里搜过密钥:
grep -rE "sk-|AKIA|BEGIN.*PRIVATE" .next/static/ dist/ - 确认没有密钥用
NEXT_PUBLIC_*/VITE_*前缀
会话
- 会话 Cookie 有
HttpOnly; Secure; SameSite=Lax - 不写
Domain属性(除非确实要跨子域共享)
第二档:这周做
认证与授权
- 密码用 Argon2id / bcrypt / scrypt,不是 SHA
- 登录接口有限流(按账号递增延迟 + 按 IP 硬限)
- 登录响应文案与耗时都统一(不存在的用户也跑一次哈希)
- 登录后重新生成会话 ID
- 改密码使其它所有会话失效
- 密码重置:token 随机、存哈希、15 分钟过期、一次性
- 重置链接的域名从配置读,不用
req.headers.host - 两个账号互测越权,并固化成自动化测试
输入与输出
- 所有数据库查询用参数化;
grep过queryRawUnsafe之类 - 表名/列名/排序方向走白名单映射
- 入口用 zod/joi 做 schema 校验(顺带防住 NoSQL 注入与批量赋值)
- 命令执行用参数数组,不过 shell
-
grep过innerHTML/dangerouslySetInnerHTML/v-html,逐个确认数据来源 - 拼进
href/src的 URL 做了协议白名单
依赖与容器
-
pnpm audit --prod干净,或每条都做过可达性判断并记录 - CI 里接了依赖扫描(带过期时间的显式豁免机制)
- 容器
USER非 root(docker run --rm img id验证过) - 没有
privileged/docker.sock/net=host - 镜像用多阶段构建,翻层确认没夹带密钥
可观测
- Nginx 结构化日志(
escape=json) - 记录认证事件、授权拒绝、敏感操作
- 日志脱敏(不打印完整请求头/体/异常对象)
- 证书过期监控(独立于续期工具,从外部实测)
第三档:有余力再做
- CSP:先 report-only 跑两周,再逐步收紧到 nonce +
strict-dynamic - CAA 记录 + CT 日志监控
- DNSSEC(注意换 DNS 服务商时的 DS 同步顺序)
- 子域名接管扫描(定时任务)
- 源站只认 CDN(防火墙锁回源段 + 回源鉴权)
- SSRF 网络层隔离(出网代理或 NetworkPolicy)
- 数据库最小权限(不给 DELETE/DROP)、考虑 RLS
- 用户上传内容放独立域名
- 云凭据改用 OIDC 短期凭据
- 备份离线/不可变,且演练过恢复
- 应急信息卡写好,关键账号至少两人可访问
一段可以直接跑的外部体检
#!/usr/bin/env bash
D="${1:?用法: ./check.sh example.com}"
echo "════ 体检 $D ════"
echo "── 指纹泄露(应为空)──"
curl -sSI "https://$D/" | grep -iE "^server:|x-powered-by:" || echo " ✓ 干净"
echo "── 安全响应头 ──"
for h in strict-transport-security x-content-type-options x-frame-options \
referrer-policy content-security-policy; do
v=$(curl -sSI "https://$D/" | grep -i "^$h:" | head -1)
[ -n "$v" ] && echo " ✓ $v" || echo " ✗ 缺失: $h"
done
echo "── 404 页是否也带头(验证 always)──"
n=$(curl -sSI "https://$D/__nope__" | grep -icE "strict-transport|x-frame|nosniff")
[ "$n" -ge 2 ] && echo " ✓ 带了 $n 个" || echo " ✗ 只有 $n 个,检查 always 与 add_header 覆盖"
echo "── 敏感文件(应为 404)──"
for p in .env .git/config .git/HEAD config.json backup.sql; do
c=$(curl -s -o /dev/null -w "%{http_code}" "https://$D/$p")
[ "$c" = "200" ] && echo " ✗✗ 严重: /$p 返回 200" || echo " ✓ /$p → $c"
done
echo "── 未知 Host(应被拒)──"
curl -sI "https://$D/" -H "Host: invalid.test" 2>/dev/null | head -1 || echo " ✓ 连接被拒"
echo "── TLS ──"
echo | openssl s_client -connect "$D:443" -servername "$D" 2>/dev/null \
| grep -E "Protocol|Cipher|Verify return code"
echo | openssl s_client -connect "$D:443" -servername "$D" 2>/dev/null \
| openssl x509 -noout -dates
echo "── DNS ──"
echo " CAA: $(dig +short CAA "$D" | tr '\n' ' ')"
echo " DS: $(dig +short DS "$D" | head -1)"
echo " SPF: $(dig +short TXT "$D" | grep -i spf)"
echo " DMARC: $(dig +short TXT "_dmarc.$D")"只在你自己拥有的域名上运行。
最后:三条比清单更重要的话
一、清单会过期,思路不会。 新的攻击手法会不断出现,但「数据不要变成代码」「默认拒绝」「假设这一层会失守」这些原则不变。理解为什么,比记住做什么更耐用。
二、先做最薄的那一环。 安全水平由最弱的一环决定。给一个域名没开 2FA 的站上顶配 WAF,是把钱花在了已经很厚的地方。先按威胁建模那章的方法找出最薄处。
三、改完一定要验证。
「我配了那个头」和「那个头确实出现在线上响应里」之间,隔着 CDN 缓存、add_header 覆盖、配置没 reload 一堆现实。本专栏每章都给了验证命令,请真的跑一遍。
回到开头
专栏第一章说过:攻击者不会先想「我要打 XSS」,他会沿着请求的路径,从最松的那一层进来。
现在你手上有了这条路径的完整地图——域名、边缘、Nginx、前端、后端、依赖、架构。每一层都知道会出什么问题、长什么样、怎么验证。
剩下的就是动手了。
← 回到:专栏首页 · 相关:Nginx 详解 · HTTP/1.1 详解