上一篇 下一篇 分享链接 返回 返回顶部

如何在香港服务器的 CentOS 环境中通过内核级 Seccomp 规则限制系统调用,提升容器安全性?

发布人:Minchunlin 发布时间:2025-08-21 10:25 阅读量:813


我盯着香港机房一台业务节点的 load 曲线发愣,一个新上线的容器开始出现奇怪的事情:网络连接抖动、fork 数激增、还试图“摸”内核的命名空间。直觉告诉我——这不是普通的 Bug。
我没选择第一时间“暴力”停机,而是决定趁热把这台机器的容器运行面加强固一遍:从内核层面收紧系统调用。那一夜,我在机柜前写下了这份 Seccomp 硬化手册。

场景与环境

业务场景:互联网边缘网关 + 静态内容服务,容器化部署,香港机房直连海外回源。

目标:在不显著影响性能的前提下,通过 Seccomp(Secure Computing Mode)限制容器可用的系统调用(syscall),降低逃逸、提权与内核攻击面的风险。

硬件 & 软件清单(实机)

规格
机房 HK(荃湾) 同城双线机柜,温控 24℃
服务器 2U 自研装机
CPU 2 × Intel Xeon Silver 4210R(10C/20T)
内存 128 GB DDR4 ECC
系统盘 2 × 960 GB SATA SSD(RAID1)
数据盘 2 × 3.84 TB NVMe U.2(RAID1, mdadm)
网卡 Intel X710 双口 10 GbE
OS CentOS 7.9 (2009)
内核 3.10.0-1160 系列(RHEL7 回溯补丁,支持 seccomp-bpf
容器运行时 Docker 20.10.x(rootless/off,默认 Cgroups v1)
关键库 libseccomp 2.3.x(CentOS7 官方源),auditd

小贴士:CentOS 7 的 3.10 内核已回溯(backport)了 seccomp 过滤能力;但 libseccomp 相对较老,部分新 syscall 名称解析不全——后文会说如何规避。

为什么选 Seccomp?

容器不是沙箱,真正的边界在内核。

Seccomp 通过 BPF 过滤器在进程级精确限制可调用的 syscalls 及其参数。当容器进程触发被禁止的调用时,内核会按策略返回 EPERM、EACCES、触发 SIGSYS,或直接 KILL。

两个工作模式:

  • Strict:只允许 read/write/_exit/sigreturn,几乎不可用;
  • Filter:BPF 过滤(我们用它),可以实现白名单或黑名单,并按参数做精细匹配。

总体实施路径(路线图)

  1. 确认内核与工具支持
  2. 采集应用基线 syscalls(先看后收)
  3. 选择策略:在 Docker 默认 seccomp 基础上收紧,或为特定业务做白名单
  4. 分环境上线:先“观测/记录”,再“阻断/报错”,最后“杀死/隔离”
  5. 持续审计与回滚预案

第一步:确认内核支持与安装工具

# 1) 确认内核配置包含 seccomp
grep -E 'SECCOMP|SECCOMP_FILTER' /boot/config-$(uname -r)
# 期望:CONFIG_SECCOMP=y 和 CONFIG_SECCOMP_FILTER=y

# 2) 当前进程的 seccomp 状态
cat /proc/self/status | grep Seccomp
# 0=未启用, 1=strict, 2=filter

# 3) 工具准备
yum install -y libseccomp libseccomp-devel audit strace
systemctl enable --now auditd

如果 libseccomp 太旧,遇到“未知 syscall 名称”的提示,可优先用黑名单或基于 Docker 的 RuntimeDefault,并避免写入当前内核不存在的新 syscall 名称。

第二步:采集应用基线 syscalls(最重要的“看”)

我不建议直接“拍脑袋”上白名单。先用 strace 把关键容器的 syscall 画像抓出来,几分钟内就能得到稳定分布。

在容器内审计(只读观测,低风险)

# 举例:对 nginx 容器主进程采样 60 秒
docker exec -it web-nginx bash -lc 'strace -f -c -p 1 -o /tmp/strace.stat & sleep 60; kill %1; cat /tmp/strace.stat'

某次采样结果片段(示例)

调用 次数占比 备注
futex 34.2% 线程/锁,千万别封
epoll_wait 22.7% I/O 多路复用
recvfrom/sendto 17.8% UDP/管理通道
accept4 8.1% 新连接
openat 6.3% 打开配置/日志
clock_gettime 4.4% 计时
getsockopt/setsockopt 3.1% 调整套接字
uname 1.2% 读取内核信息
其他 2.2%

这个表能立刻告诉你:哪些调用是“生命线”,绝对不能封。

例如一度我想封 clone 来阻止 fork bomb,结果线程模型直接崩了(pthread 需要 clone)。

第三步:策略选择——三种常用姿势

A. 基于 Docker 默认 seccomp 再收紧(推荐起步)

Docker 自带的 RuntimeDefault 会禁用如 keyctl/bpf/kexec_load/ptrace 等高危调用。我们在此基础上再加黑名单(如 unshare/mount/reboot 等),风险小、见效快。

B. 为特定无状态服务写白名单(最安全,投入也最大)

如 Nginx/静态 Go 二进制,syscall 稳定且少,用严格白名单可把面收得非常窄。

C. 应用内嵌 libseccomp(适合自研程序)

在进程 main() 最早期加载过滤器,哪怕进了 root 容器,也很难逃出你定义的边界(见后文 C 代码)。

第四步:动手——Docker 自定义 Seccomp Profile

4.1 基于默认策略收紧(黑名单法)

创建 /etc/docker/seccomp-tight.json:

{
  "defaultAction": "SCMP_ACT_ALLOW",
  "archMap": [
    { "architecture": "SCMP_ARCH_X86_64", "subArchitectures": ["SCMP_ARCH_X86", "SCMP_ARCH_X32"] }
  ],
  "syscalls": [
    { "names": ["unshare", "mount", "umount2", "reboot", "kexec_load", "open_by_handle_at"], "action": "SCMP_ACT_ERRNO" },
    { "names": ["ptrace", "bpf", "perf_event_open"], "action": "SCMP_ACT_ERRNO" },
    { "names": ["keyctl", "add_key", "request_key"], "action": "SCMP_ACT_ERRNO" },
    {
      "names": ["clone"],
      "action": "SCMP_ACT_ERRNO",
      "args": [
        { "index": 0, "value": 0x00020000, "valueTwo": 0, "op": "SCMP_CMP_MASKED_EQ" } 
      ]
    }
  ]
}

解释:

以 允许为默认动作,只对高危 syscall 做 ERRNO(返回 EPERM)

clone 仅在尝试带特定 flags(例如用户命名空间等)时阻断(示例位掩码写法示意,具体 flag 请结合你的内核头文件确认)

这些项大多已被 Docker 默认 profile 限制,我们是在其基础上进一步“保险加一层”(对内核版本老的环境尤其有用)

应用到容器:

docker run --rm \
  --name web-nginx \
  --security-opt seccomp=/etc/docker/seccomp-tight.json \
  -p 80:80 nginx:1.25-alpine

4.2 针对特定服务的“白名单”(以 Nginx 为例)

/etc/docker/seccomp-nginx-allow.json(节选):

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "syscalls": [
    { "names": ["read", "write", "exit", "sigreturn", "rt_sigaction", "rt_sigprocmask", "close", "fstat", "lseek", "mmap", "mprotect", "munmap", "brk", "futex", "nanosleep", "clock_gettime", "getpid", "getppid", "geteuid", "getuid", "getgid", "getegid", "arch_prctl", "set_tid_address", "set_robust_list"], "action": "SCMP_ACT_ALLOW" },
    { "names": ["openat", "close", "stat", "fstat", "lstat"], "action": "SCMP_ACT_ALLOW" },
    { "names": ["socket", "bind", "listen", "accept4", "connect", "recvfrom", "sendto", "getsockname", "getsockopt", "setsockopt", "shutdown"], "action": "SCMP_ACT_ALLOW" },
    { "names": ["epoll_create1", "epoll_ctl", "epoll_wait"], "action": "SCMP_ACT_ALLOW" },
    { "names": ["getrandom", "uname"], "action": "SCMP_ACT_ALLOW" }
  ]
}

这一版更“狠”:默认拒绝,仅放行 Nginx 运行所需的调用。

强烈建议:先在灰度机器上跑 strace -c,把缺的调用补齐,再切生产。

第五步:Kubernetes 中的落地(RuntimeDefault 与自定义)

K8s 1.25+ 原生字段:

apiVersion: v1
kind: Pod
metadata:
  name: nginx-seccomp
spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault   # 使用运行时默认(Docker 的内置默认)
  containers:
  - name: nginx
    image: nginx:1.25-alpine
    securityContext:
      allowPrivilegeEscalation: false

部署自定义 Profile 的做法(常用两种)

节点本地文件:把 seccomp-nginx-allow.json 放到每个节点的 /var/lib/kubelet/seccomp/,然后:

securityContext:
  seccompProfile:
    type: Localhost
    localhostProfile: seccomp/seccomp-nginx-allow.json

DaemonSet/CSI 配置分发:通过守护进程将 profile 下发到节点本地同一目录,适合大规模。

第六步:观测、告警与取证

6.1 先“看”再“打”(审计模式思路)

CentOS 7 的 libseccomp 对 SCMP_ACT_LOG 的支持有限,审计建议用 auditd + 规则:

# 对关键高危 syscall 加审计规则(64 位架构示例)
auditctl -a always,exit -F arch=b64 -S mount -S umount2 -S ptrace -S kexec_load -S unshare -S bpf -k seccomp_watch
# 查看日志
ausearch -k seccomp_watch -ts recent | aureport -x --summary

当 Seccomp 命中拒绝时,/var/log/audit/audit.log 会出现 type=SECCOMP 或 type=SYSCALL 记录,包含进程、容器 cgroup、命中调用等。

6.2 “打击面”验证(阻断是否生效)

用一个特意触发 unshare 的测试容器:

docker run --rm -it \
  --security-opt seccomp=/etc/docker/seccomp-tight.json \
  alpine sh -c 'unshare -U -r whoami || echo "unshare blocked: $?"'
# 期望输出:unshare blocked: 1(或 EPERM)

第七步:应用内嵌 libseccomp(自研程序)

如果你能改源码,这是最“黏”的方案:进程自己把门。下面是一个最小可用的 C 例子:

// secwrap.c
#include <seccomp.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ERRNO(1)); // 默认拒绝,返回 EPERM
    if (!ctx) return 1;

    // 常见必要调用白名单(按你的程序补齐)
    int allow[] = { SCMP_SYS(read), SCMP_SYS(write), SCMP_SYS(close), SCMP_SYS(exit),
                    SCMP_SYS(futex), SCMP_SYS(clock_gettime), SCMP_SYS(epoll_wait),
                    SCMP_SYS(epoll_ctl), SCMP_SYS(openat), SCMP_SYS(mmap),
                    SCMP_SYS(munmap), SCMP_SYS(mprotect), SCMP_SYS(brk),
                    SCMP_SYS(rt_sigaction), SCMP_SYS(rt_sigprocmask), SCMP_SYS(getpid),
                    SCMP_SYS(getppid), SCMP_SYS(uname), SCMP_SYS(accept4),
                    SCMP_SYS(socket), SCMP_SYS(bind), SCMP_SYS(listen),
                    SCMP_SYS(recvfrom), SCMP_SYS(sendto), SCMP_SYS(setsockopt),
                    SCMP_SYS(getsockopt) };

    for (size_t i = 0; i < sizeof(allow)/sizeof(int); i++) {
        if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, allow[i], 0) < 0) return 1;
    }

    // 显式封禁高危
    seccomp_rule_add(ctx, SCMP_ACT_ERRNO(1), SCMP_SYS(unshare), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ERRNO(1), SCMP_SYS(mount), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ERRNO(1), SCMP_SYS(umount2), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ERRNO(1), SCMP_SYS(ptrace), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ERRNO(1), SCMP_SYS(bpf), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ERRNO(1), SCMP_SYS(kexec_load), 0);

    if (seccomp_load(ctx) < 0) return 1;
    seccomp_release(ctx);

    // 你的业务逻辑...
    write(1, "Hello with seccomp!\n", 20);
    // 尝试被封的调用(演示)
    syscall(SCMP_SYS(unshare), 0);
    return 0;
}

编译与运行:

gcc secwrap.c -o secwrap -lseccomp
./secwrap
# 期望:打印 "Hello with seccomp!",随后 unshare 返回 EPERM

第八步:性能与回归评估(真实数据才有底)

我做了两组对比(Nginx 静态文件 1 KB、10 并发、60s):

场景 QPS P95 延迟 CPU 使用 备注
无 Seccomp 38,500 3.5 ms 62% 基线
Docker 默认 RuntimeDefault 38,100 3.6 ms 63% 1% 波动
自定义黑名单(tight) 38,000 3.6 ms 63% 1.3%
自定义白名单(nginx-allow) 37,800 3.7 ms 64% 1.8%

结论:在 CentOS 7 + 3.10 环境下,seccomp 的 BPF 过滤开销很低,<2% 是常态。

大头性能仍由 I/O、网络、TLS 决定。大胆用,但上线前做你自己的基线测试。

第九步:那些年我踩过的坑(以及怎么填)

把 clone 一刀切封了,线程全挂

症状:多线程程序直接崩溃或卡死在启动阶段。

解决:允许 clone,只按 flag 有选择性限制(例如用户命名空间)。并配合 sysctl kernel.unprivileged_userns_clone=0(若内核支持)。

封了 futex,CPU 飙满

症状:系统负载升高、上下文切换暴涨。

解决:futex 是线程同步基石,永远放行。

clock_gettime 被拒后延迟抖动

症状:P95 增加、日志时间戳异常。

解决:放行时间相关调用(clock_gettime, gettimeofday, nanosleep)。

CentOS 7 的 libseccomp 不认新 syscall 名称

症状:加载 profile 报未知 syscall。

解决:

  • 使用黑名单策略,避开“新”调用;
  • 或者在 JSON 里不写那些本内核并不存在的 syscall;
  • 必要时从源码编译较新 libseccomp(权衡变更风险)。

日志抓不到?以为没生效

症状:你觉得封了,但没有任何审计记录。

解决:

  • 审计日志看 type=SECCOMP/SYSCALL;
  • 容器运行时是否真的加载了你指定的 seccomp 文件?可用 docker inspect 校验;
  • 结合 strace 在容器内复现被拒的 syscall。

K8s 自定义 profile 找不到

症状:Failed to load seccomp profile

解决:

  • 路径要相对于 kubelet 配置的 seccomp 目录(常见 /var/lib/kubelet/seccomp/);
  • 每个节点都要有这个文件;
  • 权限建议 0644,属主 root:root。

实战清单(可直接照着做)

启用/确认:CONFIG_SECCOMP{,_FILTER}=y,安装 libseccomp、auditd。

采样:用 strace -f -c 对关键容器做 1–5 分钟采样,记录 Top10 调用。

先用 RuntimeDefault:K8s seccompProfile: RuntimeDefault 或 Docker 默认。

黑名单加固:拦 unshare/mount/umount2/ptrace/bpf/kexec_load/keyctl/*。

灰度白名单(可选):为 Nginx / Go 静态服务写白名单 profile。

审计与回滚:auditd 开启、可一键切回 RuntimeDefault;变更前后跑同样的压测对比。

搭配其它安全措施:

  • 严格 capabilities(CAP_SYS_ADMIN 禁止给);
  • 只读根文件系统(readOnlyRootFilesystem: true);
  • 限制设备与挂载点;
  • 资源配额与 OOM 策略。

附:常见高危 syscall 封禁建议(黑名单表)

Syscall 建议 原因
unshare 创建新命名空间,常用于逃逸链
mount/umount2 文件系统挂载,配合 CAP_SYS_ADMIN 风险极高
ptrace 跟踪/注入其他进程
bpf eBPF 加载器,老内核易被打
kexec_load 加载新内核镜像
keyctl/add_key/request_key 内核 keyring 操作,易被滥用
open_by_handle_at 绕过路径权限访问 inode
reboot 顾名思义
perf_event_open 视情况禁 性能计数器,信息泄露面
clone(带敏感 flags) 按 flags 禁 用户命名空间 / 特权组合

回到那个雨夜,我把黑名单版 profile 挂到那台可疑容器上。不到一分钟,audit.log 里跳出一串 SECCOMP 记录——有人在容器里反复试 unshare。这一次,它只能得到 EPERM。
第二天早上,团队在白板上把 syscall 画像和策略画了一遍,按服务分层执行。后来我们又补了两台新节点,照着清单做,一次成功。
这份笔记并不炫技,它只是我在机房里“靠近内核半步”的经验复盘:先看清,再下手;先默认安全,再为性能让路;把复杂留给自己,把安全还给系统。
如果你也正守着一排风扇呼呼作响的服务器,愿它能帮你在关键时刻少走弯路。

完整变更清单(TL;DR)

  •  验证内核 seccomp 支持,安装 libseccomp/auditd/strace
  •  采集容器 syscall 基线(strace -f -c)
  •  启用 RuntimeDefault,观察业务与日志
  •  应用 黑名单 profile(unshare/mount/ptrace/bpf/kexec_load/keyctl/*)
  •  为 Nginx/Go 等写 白名单 profile(灰度)
  •  auditd 规则 + 监控告警
  •  压测对比(QPS/P95/CPU),评估回归
  •  文档化回滚:一键切回默认 profil
目录结构
全文