安全与合规谁能做什么?成长型团队的角色权限与审计日志实践
一个共用的管理员账号,加上一次被遗忘的周末升职,就可能让零售商损失数千美元。本文讲如何设计角色、禁止高风险职责组合、按金额给退款设关卡,并保留一份没人能悄悄改动的审计日志。
从 CDN、WAF 到私有网络的安全分层,以及由滚动发布、blue/green 和 expand and contract 迁移组成的交付流水线,最后附上流量尖峰当天的上线清单。
Author
Anichur Rahaman

在第 4 篇里结账卡住的投诉出现前 17 分钟,也就是同一个开票日的早上 9:44,票务网站的值班工程师面对的是另一个麻烦。10:00 开售,大约 1,000 名买家会在同一分钟内涌入。一位开发同事想发一个一行代码的修复,改订单邮件里的一个错别字。9:52,监控面板显示少数几个地址每分钟发起 4,000 次登录尝试,像是黄牛脚本在预热。(这是虚构的场景,并非真实事件。)
那天早上的结局,取决于几周前做的两个决定:互联网能触及系统的多大一部分,以及发布时是否需要停站。如果答案是“全部”和“需要”,工程师只能拒绝这个修复,再祈祷脚本自己消停。
这最后一篇把两件事讲完:从互联网到数据的一条加固路径,以及一条永不停站的交付流水线。内容来自我自己的现场笔记,涉及为限时秒杀、开票、考试开考这类可预知的流量尖峰所做的准备。这是某一套环境的经验,不是放之四海而皆准的配方。
本文是“高并发系统工程”系列的第 5 篇,也是最后一篇。前几篇:第 1 篇,Web 层与自动伸缩;第 2 篇,队列与 Worker;第 3 篇,数据层;第 4 篇,可观测性。
没有哪个单一产品能保护整套系统。防火墙治不了泄露的密码,再强的密码也挡不住对公网敞开的数据库。务实的做法是分层,每一层都假定前一层偶尔会失守。
OWASP Top 10 是个不错的现实参照。当前版本 OWASP Top 10:2025 把 Broken Access Control(访问控制失效)排第一,Security Misconfiguration(安全配置错误)排第二。两者说的都是系统怎么搭起来的,而不是什么稀奇的漏洞利用。这和我见到的一致:多数事故源于一个没关的端口、一个没改的默认值,或一项给得太宽的权限。

在所有对外的入口前面放一层带 Web 应用防火墙(WAF)的 CDN。它能吸收大流量冲击,直接返回缓存页面而不碰你的服务器,还能在常见攻击模式到达代码之前拦下。对尖峰来说它同时保护了你的容量:边缘层应答的每个请求,Web 节点根本看不到。
有三项设置比其他的更重要:
把源站锁死,只接受来自边缘网络的流量。如果你的服务器对公网任何人都直接应答,WAF 就只是个建议,而不是一道管控。
所有传输都要加密。客户端支持的地方用 TLS 1.3,并把 TLS 1.2 作为底线。PCI DSS 4.0 要求公网上的持卡人数据使用强加密,并排除 SSL 和早期 TLS 版本,所以只要收银行卡,就该关掉 TLS 1.0 和 1.1。
设计上的关键问题是 TLS 在哪里终止。很多部署在 CDN 终止一次,在宿主机反向代理再终止一次,之后以明文 HTTP 送进容器。最后这一跳在私有的宿主机网络里没问题,前提是你确定它真的是私有的。如果流量要穿过你控制不了的网络,这一跳也要加密。
另一个陷阱是各层限制对不上。我运维过的一个平台里,两层 nginx 前后串联:一层宿主机代理负责终止 TLS,一层容器代理挡在 PHP-FPM 前面。宿主机默认的请求体上限是 1 MB,于是上传一直悄悄失败,直到各层口径一致:两个代理里的 client_max_body_size,以及 PHP 里的 post_max_size 和 upload_max_filesize。这是可靠性问题,但它也会诱使人们盲目调大限制。上限只定一次,每一层都配上,并写下来。
最有价值的规则也最乏味:除了边缘层和 load balancer,其余一律不对外。Web 节点、Worker、数据库、缓存和搜索节点都放在私有网络。托管的数据库和缓存只接受来自该网络的连接,这样光凭一个泄露的密码,攻击者也进不来。
再加上防火墙规则,写明谁可以和谁通信,例如:
| 组件 | 谁可以连接 | 对外? |
|---|---|---|
| CDN / WAF | 互联网 | 是 |
| Load balancer | 仅限边缘网络的地址段 | 是,受限 |
| Web 与 SSR 节点 | 仅限 load balancer | 否 |
| Worker 与调度器 | 外部无人可入 | 否 |
| 数据库与缓存 | Web 节点和 Worker,经私有地址 | 否 |
| SSH 与管理访问 | 跳板机或 VPN,仅限密钥 | 否 |
拓扑一变就重看这张表。多数“我们敞开了一个月”的故事,都始于一次没人记录的临时改动。
网络隔离不只是对着互联网。我曾在一台主机上发现几个相邻项目共用同一个 Docker 网络,每个项目都把自己的 PHP 服务命名为 app。代理被配置为把请求发到端口 9000 的 app,而这个名字解析到了好几个容器。结果一半的请求落到了另一个项目的 PHP-FPM 上。
什么都没有崩溃。页面只是来自错误的代码、错误的配置,有时还连着错误的数据库。这既是可靠性问题,也是数据泄露问题。
解法很简单:每个项目一个独立网络,服务名保持唯一,只需要访问别处的服务就不要把它暴露到该网络里。审查任何一套部署时,直接问一句:这个容器能不能访问到它本不该访问的东西?
三个习惯能挡住很大一部分损失。
最后要假定终究会出问题。备份要放在攻击者用同一套凭证碰不到的地方,并且练习恢复。这部分我单独写在 面向业务系统的勒索软件防护备份里,这里只补一句:没试过的恢复不是方案,只是侥幸。
让人害怕的发布会让大家不敢打补丁、不敢改动,所以目标是让发布变得无聊。第一条规则:只构建一次,发布完全相同的制品。在一台机器上构建镜像并推送,所有节点拉取同一个镜像。切换流量前,先核对各节点的 image ID。永远不要在正在服务的节点上更新源码并构建:相隔一小时构建的两个节点可能悄悄变得不一样,构建中途失败还可能弄坏线上服务器。
配置与镜像分开走,所以同一个制品从预发环境到生产环境原样不变。这样你才能说:测试过的就是发布出去的。
“零停机”通常栽在数据库变更上。滚动发布期间,新旧代码同时跑在同一个表结构上,所以迁移必须对两者都成立。
标准做法是 expand and contract(扩展再收缩)模式,也叫 parallel change:
举个带算术的例子,数字为虚构。有一张 240 万行的 customers 表,要把 phone 列改名为 contact_phone。Expand:新增可空列 contact_phone。Migrate:新代码每次保存都同时写两列,后台任务每批复制 5,000 行。一共 480 批,每批约两秒,总计大约 16 分钟,期间不会给任何用户锁表。切换读取前,核对 phone 有值而 contact_phone 为空的行数是否为零,然后才收缩。
删除是唯一不可逆的一步,所以要等。迁移在站点提供服务时执行,并校验结果:迁移前后的行数,以及确认没有残留转换到一半的数据。发布窗口前先取一份最新的数据库转储,代码也要保留回滚指针。
不同层需要不同策略。无状态的 PHP 后端,我用滚动发布,一次一个节点:


有两个节点时,站点始终有一台健康的服务器。对幂等请求,代理上的重试规则可以把偶发故障对用户藏起来,但不要对会扣银行卡的请求启用。
服务端渲染的前端,我更喜欢 blue/green。在旧版本旁边启动新版本,先单独测试,再改一个代理配置文件并平滑重载来切换。回滚就是反方向做同一步,大约一秒。StoreConsole 自己的生产环境也是这样运行的。
Worker 和调度器需要单独照看。切换 Worker 之前先排空队列:停止接新任务,给在途任务充足的宽限时间跑完,再启动新版本。调度器最后启动,并且只在一个节点上,免得只发布了一半的系统把周期任务跑两遍,或者跑在错误的代码上。
命令返回并不代表发布结束,验证过才算。跑一遍端到端检查,覆盖一次真实的购买或提交,确认队列在流动,然后进入观察期:在宣布完成之前,持续盯一段约定的时间,看错误率、p95 延迟和队列等待时长。可以用本系列第 4 篇讲的面板,遇到吓人的告警,先对照真实进程核实再行动。
大流量活动之前,我用下面这份清单:
回到 9:44。有了这些,工程师就能对错别字修复说可以。镜像一小时前就构建并测试过,节点逐个滚动,每分钟 4,000 次登录尝试只撞上登录路径上的挑战,买家照常浏览,不受打扰。数据库没有公网地址,脚本在边缘层之后找不到任何可以下手的东西。修复上线,观察期在 9:55 结束,比开售早五分钟。
下表列出各层如何出问题,以及怎样快速检查。
| 层级 | 常见问题 | 快速检查 |
|---|---|---|
| 边缘层 | 源站可被直接访问 | 从边缘网络之外请求源站地址 |
| 网络 | 数据库对公网开放 | 从外部主机做端口扫描 |
| 主机 | 共用服务别名 | 列出每个名称解析到哪些容器 |
| 密钥 | 凭证被打进镜像 | 搜索镜像各层和仓库历史 |
| 发布 | 各节点运行不同的构建 | 核对每个节点的 image ID |
| 数据库 | 高负载下的破坏性迁移 | 按 expand and contract 审查;先在副本上演练 |
Anichur Rahaman 是一名软件架构师,也是 StoreConsole 的创建者。他为成长型企业设计电商与 ERP 系统,专注于事件驱动架构、数据完整性和自托管部署。
About the Author
Anichur Rahaman
Continue Reading