标签: Seccomp

  • 深入 K8S 运行时安全陷阱排查:自定义 Seccomp 白名单漏配 clone3 引发的 JVM 启动雪崩与 Falco 报警风暴实战

    排查过程中最怕遇到的,就是“没有报错日志”的故障。某次核心业务集群的基础镜像批量升级后,数百个 Java 微服务 Pod 在重启后瞬间陷入 CrashLoopBackOff,无任何业务日志输出。最终查明,根本原因并非基础镜像本身的业务兼容性问题,而是安全团队通过 Webhook 强推的 Seccomp 自定义严格白名单中,遗漏了新版 glibc (>=2.34) 强依赖的 clone3 系统调用,且错误地配置了阻断动作,导致 JVM 底层线程创建直接崩溃。

    防御性编程和安全加固绝对不是在网上抄一段 JSON 配置闭着眼睛全量下发。缺乏灰度压测和系统调用 Profiling 的“一刀切”安全策略,其破坏力甚至远超真实的黑客攻击。

    诡异的故障现场

    排查过程中,研发反馈新发布的 Pod 全部无法就绪。 第一反应是拉取日志:

    kubectl logs -f <pod-name> --previous
    

    结果是空的。JVM 连初始化的堆栈都没来得及吐出就暴毙了。 查看 Pod 事件:

    kubectl describe pod <pod-name>
    # 重点信息截取:
    # State:          Waiting
    # Reason:         CrashLoopBackOff
    # Last State:     Terminated
    # Reason:         Error
    # Exit Code:      132
    

    Exit Code 132,这是一个极其关键的信号。132 = 128 + 4,对应的是 SIGILL(非法指令),但在容器安全上下文里,通常是由于触发了 Seccomp 的 SCMP_ACT_KILL 或 SCMP_ACT_KILL_PROCESS,内核发送了 SIGSYS(Bad system call,信号 31),在某些容器运行时的封装下会以类似异常退出码体现。如果是 SCMP_ACT_ERRNO,退出码通常是 1 并伴随 Operation not permitted。

    深入内核层排查

    既然应用层没有线索,直接下沉到 Node 节点看内核审计日志。 登录故障 Pod 所在的宿主机,执行:

    dmesg -T | grep -i seccomp
    # 或者查看 audit 日志
    tail -n 100 /var/log/audit/audit.log | grep -i seccomp
    

    屏幕上密密麻麻全是类似这样的拦截记录:

    type=1326 audit(169xxxxxxx.xxx:xxxx): auid=4294967295 uid=1000 gid=1000 ses=4294967295 pid=124555 comm="java" exe="/opt/java/openjdk/bin/java" sig=31 arch=c000003e syscall=435 compat=0 ip=0x7f8a9b1c2d49 code=0x80000000
    

    拆解这段内核日志,核心线索浮出水面:

    • comm="java":涉事进程是 JVM。

    • arch=c000003e:代表 x86_64 架构。

    • syscall=435:查阅 Linux 系统调用表,x86_64 的 435 号是 clone3。

    • code=0x80000000:对应 Seccomp 动作 SECCOMP_RET_KILL_THREAD。

    致命的 ENOSYS 与 EPERM 陷阱

    为什么突然会用到 clone3? 基础镜像从基于 Debian 10 升级到了 Debian 11/12(或 Ubuntu 22.04),底层的 glibc 版本跨越了 2.34。在旧版本中,glibc 使用 clone (syscall 56) 来创建线程;而在新版本中,默认改用更现代的 clone3 (syscall 435)。

    此时,查看安全团队通过 K8S Mutating Webhook 全局注入的 seccompProfile:

    {
      "defaultAction": "SCMP_ACT_ERRNO",
      "syscalls": [
        {
          "names": ["clone", "execve", "read", "write", "...(省略约200个常规系统调用)..."],
          "action": "SCMP_ACT_ALLOW"
        }
      ]
    }
    

    这就是典型的低级失误: 配置文件里白名单只放行了 clone,完全漏掉了 clone3 和 rseq。

    更致命的是 Seccomp 机制的一个隐藏大坑:优雅降级被破坏。 如果内核不支持 clone3,glibc 调用后内核会返回 ENOSYS (Function not implemented),glibc 捕获到 ENOSYS 后会自动降级使用老的 clone,业务无感。 但是!在这个自定义的 Seccomp 配置中,未命中白名单的调用会被直接 SCMP_ACT_ERRNO(返回 EPERM,Operation not permitted)或者 SCMP_ACT_KILL。当 glibc 拿到 EPERM 时,它认为“权限不足”,不会触发降级逻辑,而是直接抛出异常导致进程崩溃。

    Falco 报警风暴与监控瘫痪

    更有戏剧性的是,集群里其实部署了 Falco 作为运行时 IDS。当我们去查看 Falco 告警时,发现 ES 和 Slack 已经被报警风暴淹没:

    {"output":"Unexpected syscall clone3 dropped by seccomp (user=root pod=app-xyz-6d... syscall=clone3)","priority":"Warning","rule":"Seccomp Drop Detected"}
    

    每秒数以万计的并发启动失败,触发了海量的 Falco 审计事件,直接把本地的 journald 队列打满,并引发了宿主机的 CPU 瞬时飙升(Load Average 冲破宿主机核心数),最终导致告警组件触发 Rate Limit 被迫丢弃数据。这正是典型的“告警疲劳”反噬。配置了安全监控,却不在发布前做好基线对齐,真出事时这些监控反倒成了雪上加霜的帮凶。

    技术结论与避坑建议

    1. 拒绝盲目的一刀切白名单:在 K8S 环境中,推荐直接使用容器运行时的默认配置 RuntimeDefault。Docker/containerd 的默认 Seccomp 配置文件由内核和容器领域的顶尖专家维护,兼顾了安全性与兼容性(例如他们早就处理好了 clone3 返回 ENOSYS 而非 EPERM 的逻辑)。

    2. Profile 必须经过 Audit 阶段:如果要推行极度严格的自定义 Seccomp 或 AppArmor 白名单,必须先在沙盒环境使用 SCMP_ACT_LOG(仅记录不拦截)运行全量回归测试,通过 audit.log 收集完整的系统调用基线,再切换到 SCMP_ACT_ERRNO。

    3. 利用 eBPF 工具自动化生成基线:推荐使用 Kubernetes Security Profiles Operator (SPO) 结合 eBPF,在业务正常运行时录制所需的系统调用,自动生成 Seccomp JSON,而不是靠人工拍脑袋写。

    4. 关注底层依赖变更:基础镜像升级绝不只是改个 FROM 那么简单,glibc 的更新(如引入 rseq, clone3, statx)会直接改变应用底层的 syscall 足迹。

    同类问题排查清单(Seccomp/AppArmor/Falco 拦截排查速查)

    1. 进程异常退出且无日志 (Exit Code 132 / 1)
    2. 动作:立即执行 dmesg -T | grep -iE 'seccomp|apparmor|audit'。
    3. 目标:寻找 sig=31 或 apparmor="DENIED" 的关键日志,定位被拦截的具体资源或系统调用编号。

    4. 系统调用编号反查

    5. 动作:使用 ausyscall x86_64 翻译系统调用号(如 ausyscall x86_64 435 返回 clone3)。
    6. 目标:明确是哪个底层动作触发了安全规则。

    7. 确认 Pod 实际生效的安全策略

    8. 动作:kubectl get pod -o yaml | grep -A 5 securityContext
    9. 目标:检查 seccompProfile 是否被设为了非预期的 Localhost 自定义配置,或者 AppArmor 注解是否被意外注入。

    10. 验证 Falco 性能损耗

    11. 动作:如果节点系统负载飙升,通过 pidstat -p 1 检查 Falco CPU 占用,并查看 /var/log/falco/ 中的丢包率(drop metrics)。如果是规则过于宽泛导致系统调用捕获量过大,需临时调配 Falco 规则引擎过滤高频无害事件。
  • 深入 Seccomp 白名单陷阱排查:glibc 升级引发的 clone3 拦截与容器无限 CrashLoop 实战

    某次核心业务重构,开发团队将基础镜像从 CentOS 7 迁移至 Debian 11 后,发布到生产环境的全量 Pod 瞬间陷入 CrashLoopBackOff。业务端一口咬定是 K8S 底层挂载卷权限配置错误,因为 Java 应用日志仅留下一句干瘪的 java.io.IOException: Cannot run program: error=1, Operation not permitted,随后进程崩溃。

    最终结论直接抛出:这是一起典型的容器运行时安全策略(Seccomp)误杀事件。新版镜像内置的 glibc >= 2.34 默认启用了 clone3 系统调用,而集群默认下发的自定义 Seccomp 白名单(Profile)严重老化,未包含此 Syscall。内核按照策略配置返回 EPERM(Operation not permitted),导致进程创建直接被拒。

    解决方案:在 Seccomp Profile 中补齐 clone3(435)及 faccessat2(439),或在预发环境先将 defaultAction 降级为 SCMP_ACT_LOG 进行系统调用采样。

    案发现场:被误导的权限报错

    排查过程中,开发拿着 Operation not permitted 的日志找过来,要求运维检查容器的 runAsUser 和 Volume 的 fsGroup。

    这是一个非常经典的排查误区。在 Linux 系统中,EPERM 错误码(Error Number 1)不仅代表文件系统权限不足,当进程触发了被内核拦截的动作(如被 Seccomp、AppArmor 或 SELinux 阻断)时,同样会抛出这个错误。

    盲目去排查文件系统权限纯粹是浪费时间。容器起不来,且报底层权限错误,直接下钻到 Node 节点看内核审计日志。

    执行以下命令:

    # 在 Pod 所在宿主机上直接抓取 Seccomp 审计日志
    ausearch -m SECCOMP --just-one
    # 如果没有 auditd,直接看内核环形缓冲区
    dmesg -T | grep -i seccomp
    

    立刻拿到实锤证据:

    audit: type=1326 audit(1690000000.123:4567): auid=4294967295 uid=1000 gid=1000 ses=4294967295 subj=unconfined pid=123456 comm="java" exe="/opt/java/bin/java" sig=0 arch=c000003e syscall=435 compat=0 ip=0x7f8a9b123456 code=0x50000
    

    拨云见日:Syscall 435 的身世

    看懂上面这行内核日志,是搞容器安全的基操。 核心关注三个字段:

    1. type=1326:标准的安全计算模式(Seccomp)拦截事件。

    2. arch=c000003e:表示当前架构是 x86_64(AUDIT_ARCH_X86_64)。

    3. syscall=435:被拦截的具体系统调用号。

    不知道 435 是什么?用 ausyscall 查一下:

    $ ausyscall x86_64 435
    clone3
    

    为什么仅仅升级了基础镜像,就会突然触发 clone3 拦截?

    底层原理在于:在 Linux 5.3 内核中,引入了更具扩展性的 clone3 系统调用来替代传统的 clone/fork/vfork。而从 glibc 2.34 开始,标准库的进程创建封装函数(如 posix_spawn)默认优先尝试调用 clone3。如果在容器内执行了类似 Runtime.getRuntime().exec() 的代码,底层就会触发该系统调用。

    当时集群下发的 Seccomp Profile 还是两年前从某个 GitHub 仓库 Copy 下来的“业界最佳实践”。这个陈旧的白名单里只有 clone,压根没有 clone3。

    当未在白名单中的 Syscall 被触发时,由于策略的默认行为被设置为 SCMP_ACT_ERRNO:

    {
      "defaultAction": "SCMP_ACT_ERRNO",
      "architectures": [
        "SCMP_ARCH_X86_64"
      ],
      "syscalls": [ ... ]
    }
    

    内核不会杀掉进程(如果是 SCMP_ACT_KILL,应用会直接收到 SIGSYS 信号并 Dump Core,反而好查),而是向调用方返回一个 errno,默认就是 EPERM。这就是业务层看到 Operation not permitted 的直接原因。

    治本之策:防御性安全与可观测性

    安全加固决不能以牺牲可用性为代价。直接在生产环境强推严苛的 Seccomp 白名单,无异于在高速公路上埋地雷。

    正确的修复与落地路径:

    1. 更新 Seccomp 规则:在 syscalls 列表中补齐现代 glibc 常用的系统调用。除了 clone3,通常还需要加上 faccessat2(439)和 statx(332),这几个是基础镜像升级引发血案的常客。
    {
      "names": [
        "clone3",
        "faccessat2",
        "statx"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
    
    1. 灰度与审计模式(Audit Mode): 任何新的 Seccomp 规则上线前,必须先将 defaultAction 设置为 SCMP_ACT_LOG。这样内核只会记录审计日志,而不会真正阻断请求,跑一周后回收日志,确认无误再切回 SCMP_ACT_ERRNO。

    2. 引入 Falco 规则引擎监控: 靠人工看 dmesg 是低效的。应当利用 Falco 这类基于 eBPF 的运行时安全工具,将 Seccomp 和内核事件提取为结构化告警。 编写一段简单的 Falco 规则,专门捕捉异常进程创建:

    - rule: Unexpected Syscall Attempt (Seccomp missing)
      desc: Detect processes trying to use modern syscalls blocked by legacy seccomp profiles
      condition: >
        evt.type=syscall and evt.dir=< and evt.rawres=-1 
        and (evt.arg.error=EPERM or evt.arg.error=ENOSYS)
      output: >
        Syscall blocked (user=%user.name pod=%k8s.pod.name container=%container.name syscall=%evt.type)
      priority: WARNING
    

    一旦有被拦截的系统调用,SRE 团队能第一时间收到告警,而不是等业务反馈“我的 Pod 起不来了,存储坏了”。

    同类问题速查排查清单

    1. 确认安全组件拦截层级:看到 EPERM / Operation not permitted,首查 dmesg/audit。确认是 Seccomp (Syscall 阻断) 还是 AppArmor/SELinux (MAC 文件/能力阻断)。

    2. 翻译 Syscall 号:拿到 syscall=XXX 后,必须结合 arch 使用 ausyscall 进行转译。不同架构下同一个编号代表的系统调用完全不同。

    3. 查验 glibc/内核版本匹配度:应用基础镜像升级跨度较大时(如 Alpine 3.12 -> 3.16,或 Debian 10 -> 12),大概率会引入新的系统调用封装,需提前审核 Seccomp 白名单。

    4. 抓取 Core Dump 判断:若应用直接闪退且退出码为 159,通常是被触发了 SIGSYS (Bad system call)。这说明 Seccomp 配置成了 SCMP_ACT_KILL,检查 dmesg 即可破案。

  • 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_ERRNO 或 SCMP_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、= 替代正则 pmatch 或 regex。

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

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

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

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

      • 现象:容器 CrashLoopBackOff,退出码 159。
      • 命令:dmesg | grep -i seccomp 或 journalctl -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 的白名单豁免。