Files
jiu/docs/plans/security-hardening.html
wangjia 4966892030 docs: DDoS/爬取洪峰应急 runbook + 安全三件套 plan 阅读版(todo #1)
拍板:不预购高防/CDN,接受极端容量攻击黑洞期风险;应急走 runbook
(判定→CC 处置→高防/CDN 切换→黑洞预案,DNS TTL 平时保持 ≤600s)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 10:42:23 +08:00

61 lines
12 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>安全三件套实现计划 — 酒库管理系统</title><style>
:root{
--primary:#2563AC; --primary-dark:#154072; --danger:#D14343; --danger-bg:#FDECEC;
--success:#2E8B57; --success-bg:#E6F3EC; --warn:#B45309; --warn-bg:#FFF4E5; --accent:#8B2331;
--ink:#232934; --muted:#6E7888; --border:#DCE2EB; --paper:#F5F7FA; --head:#F0F4FF;
}
*{box-sizing:border-box;font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;}
body{margin:0;background:var(--paper);color:var(--ink);padding:28px;line-height:1.7;max-width:1000px;}
h1{font-size:22px;margin:0 0 4px;}
h2{font-size:17px;margin:30px 0 12px;color:var(--primary-dark);border-bottom:2px solid var(--head);padding-bottom:6px;}
.sub{color:var(--muted);font-size:13px;margin-bottom:18px;}
.card{background:#fff;border:1px solid var(--border);border-radius:10px;padding:16px 20px;margin:14px 0;font-size:13.5px;}
code{font-family:ui-monospace,Menlo,monospace;font-size:12px;background:#EEF2F8;padding:1px 5px;border-radius:4px;color:var(--primary-dark);}
ul,ol{margin:6px 0;padding-left:22px;} li{margin:4px 0;font-size:13px;}
.chk{color:var(--success);font-weight:700;margin-right:4px;}
</style></head><body>
<h1>安全三件套:限流状态外置 Redis + 公开接口反爬(轻量)+ 边缘层加固(todo #1/#2/#3</h1><div class="sub">2026-07-05 定稿 · 执行真相源为 plan .md,本页为阅读版 · 状态:已批准,本地实现完成待发版</div>
<h2>Context</h2><div class="card"><ul>
<li style="list-style:none;margin-left:-18px">三条安全 todo 一次做完。现状:限流桶(<code>internal/middleware/ratelimit.go</code> keyedLimiter)与登录失败锁(<code>internal/service/auth.go:45-120</code> loginLimiter)全是<b>进程内存态</b>,重启清零、无法多实例;商品公开接口 public_id 是 UUIDv4 不可枚举,但<b>店铺列表接口</b>shop_code 可猜,S001…)是真实爬取面;ali 安全组还留着 <b>3389 对全网开放</b>的系统默认残留,nginx 只有 limit_req 无 limit_conn。原 todo #1 写的 AWS Shield/EC2 已过时(EC2 已下线),按阿里云现状重定义。#3 用户拍板<b>轻量版</b>(日配额+UA 拦截+防盗链;不做滑块/签名/登录墙)。</li>
<li style="list-style:none;margin-left:-18px">代码顺序 <b>#2 → #3 → #1</b>#3 日配额复用 #2 的 Counter;#1 纯运维)。<b>所有生产变更(发版 / ssh ali / 安全组 CLI)单独设批准点,用户明示后才执行</b>;本地改码+测试先行。机器仅 1.8GiB RAMmysql+jiu+payd+nginx 在跑),redis 内存上限克制。</li>
</ul></div>
<h2>阶段 #2 · 限流/登录锁外置 Redis</h2><div class="card"><ul>
<li style="list-style:none;margin-left:-18px">外置:per-IP/per-shop 限流桶、登录失败计数与锁、(为 #3 预留)日配额 Counter。不动:web ticket / 会话(已在 MySQL)、login_attempts 审计表。</li>
<li><span class="chk"></span><b>新包 <code>backend/internal/ratelimit</code></b><code>store.go</code>(接口+<code>Init(addr)</code>/<code>Default()</code>,addr 空或 Ping 失败→内存并 warn,<code>Default()</code> 未 Init 惰性内存——现有测试零改动的关键)、<code>memory.go</code>keyedLimiter+loginLimiter 原样迁入,janitor 一起)、<code>redis.go</code>(限流用 <code>go-redis/redis_rate/v10</code> GCRA 对齐 rate+burst;登录锁 INCR+EXPIRE 30m、达阈值 <code>SET locked: EX</code> 并清计数,三行 Lua 保原子;键前缀 <code>jiu:rl:</code>)、<code>fallback.go</code>(redis 报错逐调用降级内存+限频 warn,恢复自动回 redis)。接口:<code>Limiter.Allow(key)→{Allowed,RetryAfter}</code><code>Counter.Incr(key,ttl)→int64</code><code>FailLocker.Locked/RecordFailure(key,max,lockFor)/Reset</code></li>
<li><span class="chk"></span><b>调用方改造</b><code>middleware/ratelimit.go</code> 改用 <code>ratelimit.Default().NewLimiter(n,per,burst)</code>429 用 Result.RetryAfter<code>RateLimitByIP/ByShop</code> 签名改 <code>(n,per,burst)</code>router.go:56-61 六处同步;Enabled=false 全放行、keyFn 空串放行语义不变。<code>service/auth.go</code> 删 loginLimiter~75 行)改 <code>ratelimit.Default().FailLocker()</code>lockFor 传 <code>config.C.Session.LockMinutes</code><code>main.go</code> 在 config.Load 后 <code>ratelimit.Init(config.C.Redis.Addr)</code><code>config/config.go</code><code>Redis.Addr</code> + <code>BindEnv(&quot;redis.addr&quot;,&quot;REDIS_ADDR&quot;)</code>,默认空=内存模式</li>
<li><span class="chk"></span><b>基础设施</b>go.mod 加 go-redis/v9、redis_rate/v10、miniredis/v2test)。<code>deploy/docker-compose.jiu.yml</code><code>jiu-redis</code>redis:7-alpine、<code>127.0.0.1:6379</code><code>--maxmemory 64mb --maxmemory-policy noeviction --appendonly no --save &quot;&quot; --rename-command CONFIG &quot;&quot;</code>、无 volume(刻意无持久化:redis 重启丢限流计数可接受,noeviction 防锁键被逐出,回环无密码 payd 容器够不着)。<code>production.env.template</code><code>REDIS_ADDR=127.0.0.1:6379</code>(非密钥,deploy-server.sh 漂移检查正好当上线核对项)</li>
<li><span class="chk"></span><b>测试</b>:现有 ratelimit/auth 测试零改动通过(内存路径);<code>memory_test.go</code> 迁移对等用例;<code>redis_test.go</code>miniredisrate+burst、锁定+FastForward 过期解锁、Counter TTL);<code>fallback_test.go</code>(Close 后不 500、降级仍限流);<code>Init(&quot;坏地址&quot;)</code> 不 panic。DoD<code>go build/vet/test ./...</code> 全绿、零真实外部依赖</li>
</ul></div>
<h2>阶段 #3 · 公开接口反爬(轻量版)</h2><div class="card"><ul>
<li><span class="chk"></span><b>日配额二级闸</b><code>middleware/dailyquota.go</code>——<code>DailyQuota(scope, limit)</code>key=<code>dq:&lt;scope&gt;:&lt;yyyymmdd&gt;:&lt;ip&gt;</code><code>Counter().Incr(key, 26h)</code>,超限 429+Retry-After(到午夜)Counter 出错放行(不误伤)、Enabled=false/limit=0 放行。配置默认:<code>daily_product_per_ip=1000</code>(单品 API+OG 页同池)、<code>daily_shoplist_per_ip=300</code>。挂载:<code>/product/:public_id</code><code>public/products/:public_id</code><code>public/shops/:shop_code/products</code>release/errors/pay-callback 不挂。内存版 Counter 带 TTL janitor(防随机 IP 灌爆)</li>
<li><span class="chk"></span><b>店铺列表收紧</b><code>handler/public.go:235</code> page_size 上限 50→20(已核实唯一调用方 client 固定传 20,无破坏);<code>public_test.go</code> 加断言:公开两接口响应 JSON 不含 cost/purchase_price 等敏感字段名(防未来回归泄露,参考 pricing_fields_test.go 写法)</li>
<li><span class="chk"></span><b>测试</b>dailyquota 小配额打满 429、跨 scope 独立、关闭放行、Counter 错误放行;page_size 夹到 20</li>
</ul></div>
<h2>阶段 #1 · 边缘层加固</h2><div class="card"><ul>
<li><span class="chk"></span><b>nginx</b><code>deploy/nginx-jiu-ali.conf</code>CI 随 server 发版下发):</li>
<li style="list-style:none;margin-left:-18px"> - 新 zone<code>jiu_shoplist 2r/s</code>(列表专属,location <code>~ ^/api/v1/public/shops/</code> <b>须放现有 <code>(public|auth)</code> 正则之前</b>)、<code>limit_conn_zone jiu_conn</code><code>jiu_dl</code></li>
<li style="list-style:none;margin-left:-18px"> - UA map ×2<code>$jiu_bad_ua</code>(空 UA + python-requests/scrapy/go-http-client/aiohttp/okhttp 等抓取库)用于 API 公开路径;<code>$jiu_bad_ua_lib</code>(同表<b>去掉空 UA</b>)用于 <code>/product/</code><code>/app/product/</code> OG 页——社交分享爬虫 UA 不可控,OG 路径不拦空 UA;<b>不封 curl/wget</b>(运维自用+攻击者改 UA 零成本)</li>
<li style="list-style:none;margin-left:-18px"> - 公开 API location 加 <code>limit_conn jiu_conn 20</code>shops 路径 10);<code>/downloads/</code><code>limit_conn jiu_dl 3</code> + <code>limit_rate_after 10m; limit_rate 4m</code>(上线前查实例带宽再定值)</li>
<li style="list-style:none;margin-left:-18px"> - <code>/images/</code> 防盗链:<code>valid_referers none blocked server_names 51yanmei.com *.51yanmei.com</code>invalid→403<code>none</code> 放行空 Referer——微信 webview/App 直连必须放行;只挡第三方网页热链)</li>
<li><span class="chk"></span><b>安全组</b>aliyun CLI,批准点):Describe 留档 → <code>RevokeSecurityGroup</code> 删 3389/0.0.0.0/0 → 复核;22/443/80/ICMP 不动 → baize 台账记变更</li>
<li><span class="chk"></span><b>DDoS runbook</b>:新建 <code>docs/runbooks/ddos-response.html</code>(沿用项目深色 HTML 家族样式)+ 登记 <code>docs/index.html</code>。内容:判定指标(带宽/QPS/CPU steal/控制台 DDoS 事件页)、基础防护黑洞机制(约 5G 阈值、黑洞 2.5-24h)、决策树(CC→调紧 zone+封 IP;容量型→按量高防 ¥2-3k/月起 CNAME 切换 30min,或套 CDN/ESA,回源 182.92.213.171:443)、DNS TTL 平时保持 ≤600s、黑洞期客户公告话术。<b>明确决策:不预购高防/CDN</b>,接受极端容量攻击下的黑洞期不可用</li>
</ul></div>
<h2>上线顺序(每个【批准点】单独等用户点头)</h2><div class="card"><ul>
<li>本地全部改码+测试全绿 → 分阶段 commitfeat(backend) ×2 + devops ×1 + docs ×1</li>
<li>【批准点】server 发版走 CI:后端二进制(线上无 REDIS_ADDR → 仍内存模式,行为与现状一致的安全中间态)+ 新 nginx conf 同时带出 → 验收 nginx 项(UA 403 / 防盗链 403 / 微信分享卡片不误伤 / hey 并发出 429)</li>
<li>【批准点】ssh ali<code>free -m</code> 核内存(余量 &lt;150MB 则 maxmemory 降 32mb)→ 部署 compose 拉起 jiu_redis → ping 通 → <code>production.env</code> 加 REDIS_ADDR → <code>systemctl restart jiu</code> → 验收:触发限流看 429、<code>redis-cli --scan &#x27;jiu:rl:*&#x27;</code> 有键、restart jiu 后键仍在(跨重启达成)、<code>docker stop jiu_redis</code> 降级不 500 再 start</li>
<li>【批准点】aliyun CLI 删 3389 → baize 记账</li>
<li>回滚路径:nginx=上一 tag 重发;redis=env 删 REDIS_ADDR 重启(回内存模式);安全组=Authorize 原参数回加</li>
<li>收尾:todo #1/#2/#3 标 done 等验收;plan 定稿 HTML 阅读版落 <code>docs/</code>、登记 index(全局双产物规则)</li>
</ul></div>
<h2>明确不做(写入 runbook/commit message</h2><div class="card"><ul>
<li style="list-style:none;margin-left:-18px">签名链接(QR 已印刷不可过期、UUIDv4 已不可枚举)|滑块/验证码(过重,杀零门槛分享体验)|登录后可见(杀死扫码分享功能)|预购高防/CDN(成本不匹配体量,转 runbook 预案)|封 curl/wget/泛&quot;bot&quot; UA(误伤运维与社交分享爬虫)</li>
</ul></div>
<h2>风险</h2><div class="card"><ul>
<li>1.8GiB 内存紧:redis 实占 &lt;80MB,上线前实测 free -m</li>
<li>REDIS_ADDR「模板有、线上无」窗口期 deploy-server.sh 会告警一次(步骤 2→3 之间),预期内</li>
<li>shops 路径从 jiu_pub zone 移入 jiu_shoplist 后不再占原配额,行为变化在预期内</li>
</ul></div>
</body></html>