标签: 容器运行时

  • Exit Code 159 连环暴雷:一份“原汁原味”的 Seccomp 配置是如何干碎生产集群的

    排查某核心计费链路故障时,处理了一起令人血压飙升的 P0 事故。现象很简单:核心服务在一次例行发布后陷入无限 CrashLoopBackOff,容器退出码清一色是 159。而真正引发雪崩的,是研发为了绕过报错,随手加上的一句 privileged: true,直接触发了节点级 Falco 规则引擎的“死亡螺旋”,导致整台宿主机 Load Average 飙升至 80+,最终 OOM。

    结论先行:Exit Code 159 意味着进程收到了 SIGSYS (128 + 31) 信号,触发了 Seccomp 机制的系统调用拦截。 事故的根本原因是业务团队为了应付安全合规扫描,从几年前的博客上盲目抄了一份 Seccomp 白名单配置,漏掉了新版 glibc 强依赖的 clone3 系统调用。更不可原谅的是,面对拦截,他们没有去审计日志补齐规则,而是选择直接裸奔,进而引爆了底层的安全监控器。

    防御性编程的底线在于:不要用更大的错误,去掩盖一个你没看懂的报错。 接下来,我们把事故现场扒开,看看底层到底发生了什么。

    现场复原:神秘的 159 退出码与“消失的线程”

    服务起不来,查看 Pod 状态:

    $ kubectl get pods -n billing
    NAME                              READY   STATUS             RESTARTS   AGE
    billing-svc-7f8b9d4c-x9j2k        0/1     CrashLoopBackOff   12         3m
    

    看一眼容器退出日志,没有任何 Java 异常栈,只有一句冰冷的提示:Pod the container terminated with exit code 159.

    遇到 159,老鸟的直觉应该立刻指向 Seccomp(Secure Computing Mode)。登录所在 Node,直接翻内核审计日志:

    $ dmesg -T | grep audit | grep "sig=31"
    [Mon ...] audit: type=1326 audit(1690000000.123:45): auid=4294967295 uid=1000 gid=1000 ses=4294967295 pid=14321 comm="java" exe="/opt/java/bin/java" sig=31 arch=c000003e syscall=435 compat=0 ip=0x7f8a9b8c2d4e code=0x80000000
    

    这是一条标准的 Seccomp 拦截日志。拆解一下核心字段:

    • sig=31:触发了 SIGSYS 信号,内核直接 Kill 了该线程。

    • arch=c000003e:代表 x86_64 架构。

    • syscall=435:重点来了,在 x86_64 下,系统调用号 435 对应的是 clone3

    • code=0x80000000:对应 SECCOMP_RET_KILL_THREAD

    为什么会突然拦截 clone3?排查后发现,业务基础镜像最近升级到了基于 Ubuntu 22.04(内置 glibc 2.34+),而新版 glibc 在创建线程时默认优先使用 clone3。但业务提交的那份陈年 Seccomp 白名单(Default Profile)里,压根没有 435 这个系统调用!

    灾难升级:当“掩耳盗铃”遇上 Falco 规则引擎

    按照正常的逻辑,拿到 syscall=435,去 Seccomp Profile 的 syscalls 列表里加上 clone3 就完事了。但研发团队为了快速恢复,做了一个极其愚蠢的操作:直接在 YAML 里移除了 Seccomp 限制,甚至为了“保险起见”,加了特权模式:

    securityContext:
      privileged: true # 罪恶之源
      # seccompProfile:
      #   type: Localhost
      #   localhostProfile: "strict-profile.json"
    

    Pod 确实跑起来了,但集群的噩梦才刚刚开始。监控大屏上,该 Node 的 CPU 使用率瞬间打满,Falco(容器安全监控系统)的 Pod 疯狂重启。

    抓取 Node 的 top 和 eBPF 性能指标,发现 Falco 正在被按在地上摩擦。为什么?

    因为集群的安全团队在 Falco 中配置了这样一条规则,用于监控特权容器内的可疑命令执行:

    - rule: Privileged Container Exec
      desc: Detect any execve in a privileged container
      condition: >
        evt.type = execve and container
        and container.privileged = true
        and proc.cmdline pmatch ( "sh", "bash", "curl", "wget" )
      output: "Privileged execve (user=%user.name container_id=%container.id command=%proc.cmdline)"
      priority: WARNING
    

    注意那个 pmatch(正则前缀匹配)。业务 Pod 配置了 livenessProbe,每 5 秒执行一次 sh -c "curl -s http://localhost:8080/health"。 由于改成了特权容器,探针的每一次执行都会命中这条 Falco 规则。更要命的是,正则表达式是非常消耗 CPU 的操作。在高并发场景下,海量的 sys_enter_execve 事件涌入 Falco 的 eBPF Ring Buffer,导致 Falco 陷入重度计算,大量事件 Drop:

    # 查看 Falco drop 统计
    $ curl -s http://localhost:8765/metrics | grep falco_stats_drop_count
    falco_stats_drop_count 4589212
    

    最终,Falco 因处理不过来吃光了内存,被宿主机的 OOM Killer 无情干掉,整个节点短暂处于监控盲区。

    技术结论与正规军玩法

    解决这类问题,靠的不是拍脑袋加权限,而是建立正确的安全配置基线和调试方法。

    1. 永远不要用 SECCOMP_RET_KILL 作为默认动作调试 在生产环境引入自定义 Seccomp 前,正确的做法是先将 default action 设置为 SCMP_ACT_LOG。这样内核只会记录审计日志,而不会杀死进程:

    {
      "defaultAction": "SCMP_ACT_LOG",
      "syscalls": [
        {
          "names": ["clone", "clone3", "epoll_pwait", "futex"],
          "action": "SCMP_ACT_ALLOW"
        }
      ]
    }
    

    跑几天后,提取 /var/log/audit/audit.log 里的记录,分析出业务实际需要的 syscall 集合,再切回 SCMP_ACT_ERRNOSCMP_ACT_KILL

    2. 使用 SPO(Security Profiles Operator)自动化录制 不要手工猜系统调用。K8s 官方提供的 Security Profiles Operator 支持 LogEnricher 机制,可以在 Staging 环境跑一遍完整的回归测试,SPO 会自动帮你生成精确到业务级别的 Seccomp/AppArmor Profile。

    3. Falco 规则的防御性优化 Falco 规则引擎极度依赖条件短路(Short-circuit evaluation)。

    • 将高频过滤条件(如 evt.type = execve)放在最前面。

    • 尽量用 in= 替代正则 pmatchregex

    • 必须对 K8s 探针做白名单豁免,绝不能让健康检查触发报警逻辑。

    排查清单:容器运行时安全拦截速查

    遇到容器莫名其妙死亡、无日志退出、或权限拒绝时,请直接核对以下三步:

    1. 核对 Exit Code 159 (Seccomp拦截)

      • 现象:容器 CrashLoopBackOff,退出码 159
      • 命令:dmesg | grep -i seccompjournalctl -k | grep "sig=31"
      • 动作:提取 syscall= 后面的数字,去查阅 ausyscall x86_64 ,确认被拦截的调用(常见如 clone3=435, rseq=334)。
    2. 核对 AppArmor 拦截 (EPERM / Permission Denied)

      • 现象:代码里抛出 EPERM,或者 open/mkdir 报错,但文件权限明明是 777。
      • 命令:dmesg | grep -i apparmor | grep DENIED
      • 动作:检查 profile= 字段,确认是否使用了过于严苛的 AppArmor 模板限制了特定目录的写权限。
    3. 核对 Falco 性能瓶颈 (节点 Load 飙升 / 事件丢弃)

      • 现象:部署 Falco 后宿主机 CPU 升高,应用延迟抖动。
      • 命令:检查监控指标 falco_stats_drop_count
      • 动作:排查是否有规则使用了高昂的 regex,或者审计了太高频的 open / read 等系统调用,务必加上 container.name 的白名单豁免。