凌晨3点被报警砸醒,核心交易集群爆发大规模 CrashLoopBackOff,大盘 QPS 呈断崖式下跌。快速拉取 Pod 状态,退出码清一色是 159。结论先行:安全团队在生产环境强推的所谓“零信任” Seccomp 白名单,漏掉了 rseq(Restartable Sequences)和 clone3 系统调用,导致高并发下底层 glibc/Go runtime 被内核直接发送 SIGSYS 斩首;更荒唐的是,排查期间我试图 kubectl exec 进容器抓 strace,直接触发了 Falco 的“防入侵联动”,把排查节点给 Cordon(不可调度)了。
这是一起典型的、脱离一线业务真实运行机制的安全事故。所谓的“安全左移”,绝不能以牺牲系统可用性为代价。
诡异的 159 退出码与案发现场
业务线同学反馈:代码没动,配置没动,只有流量上来时 Pod 会随机暴毙。 查看 K8s 事件,没有任何 OOMKilled 的迹象,只有冰冷的退出码:
$ kubectl get pod -n prod-core
NAME READY STATUS RESTARTS AGE
trade-engine-7f89c4d5b-x2k9q 0/1 CrashLoopBackOff 12 15m
trade-engine-7f89c4d5b-z8m2a 0/1 CrashLoopBackOff 15 15m
$ kubectl describe pod trade-engine-7f89c4d5b-x2k9q | grep -A 5 "State:"
State: Waiting
Reason: CrashLoopBackOff
Last State: Terminated
Reason: Error
Exit Code: 159
Exit Code 159,稍微有点底层经验的人看到这个数字立刻就能反应过来:128 + 31 = 159。
在 Linux 中,Signal 31 是 SIGSYS(Bad system call)。这意味着进程尝试调用了一个内核不认识、或者被安全机制强行阻断的系统调用。
直接切到宿主机,翻看内核日志:
$ dmesg -T | grep audit | tail -n 3
[Wed May 15 03:12:45 2024] audit: type=1326 audit(1715713965.123:4567): auid=4294967295 uid=1000 gid=1000 ses=4294967295 subj=unconfined pid=14325 comm="trade-engine" exe="/app/trade-engine" sig=31 arch=c000003e syscall=334 compat=0 ip=0x7f8a9b8c7d6e code=0x0
[Wed May 15 03:12:47 2024] audit: type=1326 audit(1715713967.890:4568): auid=4294967295 uid=1000 gid=1000 ses=4294967295 subj=unconfined pid=14388 comm="trade-engine" exe="/app/trade-engine" sig=31 arch=c000003e syscall=435 compat=0 ip=0x7f8a9b8c7e10 code=0x0
重点看两个数字:syscall=334 和 syscall=435,架构是 arch=c000003e(x86_64)。
用 ausyscall 翻译一下:
$ ausyscall x86_64 334
rseq
$ ausyscall x86_64 435
clone3
愚蠢的安全策略:当沙盒变成绞肉机
为什么会突然拦截 rseq 和 clone3?
查阅变更记录,安全团队在凌晨2点通过 Kyverno MutatingWebhook 给所有 namespace 强制注入了一个严格的 Seccomp Profile。
这帮天才为了做到所谓的“最小权限”,使用了一款基于 eBPF 的动态追踪工具,在测试环境跑了 5 分钟业务,把这 5 分钟内捕获到的系统调用抓出来,直接生成了白名单。
为什么这种做法极其致命?
-
并发场景下的特有调用:
rseq(Restartable Sequences)是 Linux 4.18 引入的特性,现代 glibc (>=2.35) 和 Go (>=1.19) 极度依赖它来实现无锁的 per-CPU 数据结构。在测试环境几 QPS 的负载下,线程根本不需要激烈竞争,运行时可能不会高频触发rseq相关路径;而到了生产环境的万级 QPS,底层 Runtime 一旦触发rseq,直接撞在 Seccomp 的墙上。 -
致命的 Default Action:生成工具极其愚蠢地将默认拦截动作设置为了
SCMP_ACT_KILL_THREAD。
{
"defaultAction": "SCMP_ACT_KILL_THREAD",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": ["epoll_pwait", "futex", "read", "write", "..."],
"action": "SCMP_ACT_ALLOW"
}
]
}
如果是防御性编程思维,遇到不在白名单的 Syscall,正确的阻断动作应该是 SCMP_ACT_ERRNO(配合返回 ENOSYS)。
如果配置为 ERRNO,当 glibc 调用 clone3 或 rseq 被拒时,它会优雅地收到 ENOSYS(系统调用未实现),然后 Fallback(降级) 到老版本的 clone 或传统的加锁机制,业务顶多性能掉一点,绝对不会崩溃。
但配成 KILL_THREAD,内核连说话的机会都不给,直接一刀把线程砍了,进程当场暴毙(SIGSYS)。
Falco 绞杀:排查过程中的二次伤害
为了现场验证,我试图 kubectl exec 进其中一个还在 CrashLoop 边缘挣扎的 Pod,想挂个 strace 看下具体是哪段代码触发的:
$ kubectl exec -it trade-engine-7f89c4d5b-x2k9q -n prod-core -- /bin/bash
刚敲下回车,命令卡死。紧接着,监控大盘上该 Pod 所在的整个 Node 直接变成了 SchedulingDisabled,Node 上其余 40 多个正常 Pod 开始被强行驱逐(Evicted)!
看了一眼系统安全群,机器人正在疯狂报警:
[Falco Alert] Critical: Terminal shell in container detected. Pod: trade-engine... Rule: Terminal shell in container. Action: Webhook triggered -> Cordon & Drain Node.
我特么当时血压就上来了。
安全团队部署的 Falco 规则引擎,配置了极度激进的 SOAR(安全编排自动化响应)。他们监测到 exec /bin/bash 操作,不分青红皂白(不区分发起方是 CI/CD、未知 IP 还是具有集群 admin 权限的 SRE 堡垒机),直接调用自建的 Webhook 把宿主机给 Cordon 并 Drain 了。
这种缺乏上下文联动、且具有毁灭性控制平面权限的“自动化防御”,在生产环境就是一颗随时引爆的定时炸弹。如果黑客发现了这个规则,只需要伪造请求批量触发报警,就能利用你们自己的安全工具,把你们的生产集群主动瘫痪掉。
技术结论与避坑建议
直接停用 Kyverno webhook 恢复生产后,我给安全团队扔了复盘报告。容器运行时安全不是拿着开源扫描器生成个 JSON 就能上生产的,必须遵循以下底线:
-
Seccomp 的平滑落地法则 永远不要在生产环境直接上
SCMP_ACT_KILL。第一阶段必须是SCMP_ACT_LOG,跑满一个完整的业务高峰期,通过分析dmesg审计日志收集全量 Syscall。 -
正确理解 ENOSYS 的降级语义 拦截未知的现代系统调用(如
clone3,bpf,rseq),强烈建议将 Action 设置为SCMP_ACT_ERRNO并返回ENOSYS。这符合 POSIX 标准,能让大多数现代编程语言的 Runtime 平滑降级到旧版系统调用,避免 SIGSYS 导致的血案。 -
AppArmor / Falco 联动的爆炸半径控制 安全告警(Detection)和阻断(Enforcement)必须解耦。Falco 检测到异常 shell,可以告警,可以打 Tag,甚至可以隔离特定的 Pod 网络(NetworkPolicy),但绝不允许直接越权调用 K8s API 执行 Node 级别的破坏性操作(Cordon/Drain)。防御系统的权限必须遵循最小化原则。
同类问题速查清单 (Troubleshooting Checklist)
- 如何确认是 Seccomp 导致的 SIGSYS?
- 检查 Pod 退出码是否为
159。 -
登录所在 Node,执行
dmesg -T | grep audit | grep sig=31。 -
如何翻译 Audit 日志中的 Syscall ID?
- 查看日志中的
syscall=XXX和arch=XXX。 -
使用 auditd 工具包翻译:
ausyscall x86_64(如ausyscall x86_64 435->clone3)。 -
如何排查 Falco Webhook 导致的集群异常抖动?
- 检查 Falco 日志:
journalctl -u falco | grep "Notice"或查看 Falco-sidecar 日志。 -
检查 K8s 审计日志 (kube-apiserver audit log),过滤发出
Cordon/Evict请求的 ServiceAccount,确认是否为安全告警组件的联动行为。 -
高频被遗漏的基础系统调用有哪些?
rseq(334),clone3(435),prctl(157 – 经常被 Java 线程管理需要),statx(332 – 很多新版 DB driver 依赖)。编写白名单时需格外留意。