在Ubuntu系统上通过libseccomp库来限制容器内的系统调用,本质上就是利用Linux内核的seccomp(secure computing mode)机制,给容器进程设置一个"白名单"或"黑名单"过滤规则,只允许特定的系统调用通过,其余全部拦截并终止进程。具体做法是:先安装libseccomp开发库,然后编写C程序调用seccomp_init()和seccomp_rule_add()等API构建过滤规则,最后加载到容器进程中生效。这套方案在Docker、Podman、LXC等容器运行时中已经被广泛采用,是容器安全加固的核心手段之一。
很多人一听到"限制系统调用"就觉得复杂,其实原理很简单。Linux内核有几百个系统调用,容器里的应用根本不需要全部。比如一个只跑Web服务的容器,根本不需要mount、pivot_root、ptrace这些调用。把这些危险调用禁掉,即使容器被攻破,攻击者能做的事情也极其有限。libseccomp就是帮你用编程方式精确控制这件事的库。
一、Ubuntu上安装libseccomp的完整步骤在Ubuntu 20.04、22.04、24.04等主流版本上,libseccomp的安装非常直接。你需要安装开发包和运行时库两部分。
sudo apt update sudo apt install libseccomp-dev libseccomp2 sudo apt install seccomp-tools
libseccomp-dev是开发头文件和静态库,写程序时要用。libseccomp2是运行时共享库,程序运行时依赖它。seccomp-tools是一套命令行工具,可以用来编译和加载seccomp规则,不写代码也能用。
装完之后验证一下版本:
dpkg -l | grep libseccomp seccomp-tools --version
一般会看到libseccomp2版本在2.5.x以上,这是比较新的稳定版本,支持的系统调用过滤规则非常完善。如果你用的是Ubuntu 18.04,版本可能偏旧,建议升级系统或者从源码编译最新版。
二、libseccomp的核心API和工作原理libseccomp提供了一套C语言API,核心流程就三步:初始化过滤器、添加规则、加载过滤器。
第一步,调用seccomp_init()创建一个空的过滤器上下文。这个函数会返回一个scmp_filter_ctx结构体指针,后续所有操作都围绕它进行。
第二步,用seccomp_rule_add()逐条添加允许或拒绝的系统调用规则。你可以指定系统调用号、参数条件、动作(允许、拒绝、杀死进程、返回错误码等)。
第三步,调用seccomp_load()把构建好的规则加载到当前进程。一旦加载成功,内核就会对该进程的所有系统调用进行过滤,不符合规则的直接被拦截。
这里有个关键概念:默认策略。你可以设置默认动作为SCMP_ACT_ALLOW(允许所有)然后逐个拒绝危险调用,也可以设置为SCMP_ACT_KILL(杀死进程)然后逐个允许需要的调用。生产环境强烈推荐后者,也就是"默认拒绝,白名单放行"策略,这是最安全的做法。
三、手写一个完整的seccomp限制程序示例下面是一个实际可用的C程序,它限制容器内进程只能使用read、write、exit、exit_group等基本调用,其他全部拒绝。
#include <seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main(int argc, char *argv[])
{
scmp_filter_ctx ctx;
int rc;
/* 初始化过滤器,默认动作为杀死进程 */
ctx = seccomp_init(SCMP_ACT_KILL);
if (ctx == NULL) {
fprintf(stderr, "seccomp_init failed: %s\n", strerror(errno));
return 1;
}
/* 允许基本的系统调用 */
rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
if (rc < 0) {
fprintf(stderr, "failed to add read rule\n");
goto cleanup;
}
rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
if (rc < 0) {
fprintf(stderr, "failed to add write rule\n");
goto cleanup;
}
rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);
if (rc < 0) {
fprintf(stderr, "failed to add exit rule\n");
goto cleanup;
}
rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
if (rc < 0) {
fprintf(stderr, "failed to add exit_group rule\n");
goto cleanup;
}
/* 允许mmap,很多程序需要内存映射 */
rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mmap), 0);
if (rc < 0) {
fprintf(stderr, "failed to add mmap rule\n");
goto cleanup;
}
rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(munmap), 0);
if (rc < 0) {
fprintf(stderr, "failed to add munmap rule\n");
goto cleanup;
}
/* 允许brk和madvise,动态内存管理需要 */
rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk), 0);
if (rc < 0) goto cleanup;
rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(madvise), 0);
if (rc < 0) goto cleanup;
/* 加载过滤器到内核 */
rc = seccomp_load(ctx);
if (rc < 0) {
fprintf(stderr, "seccomp_load failed: %s\n", strerror(errno));
goto cleanup;
}
printf("Seccomp filter loaded successfully!\n");
/* 这里可以执行你的容器应用逻辑 */
/* 比如 execvp 启动目标程序 */
cleanup:
seccomp_release(ctx);
return rc < 0 ? 1 : 0;
}
编译命令:
gcc -o seccomp_demo seccomp_demo.c -lseccomp
这个程序加载规则后,如果容器内进程尝试调用open、socket、clone等被禁止的系统调用,内核会直接发送SIGKILL信号杀死进程。你可以根据实际容器应用的需求,往白名单里添加更多调用。
四、在Docker容器中实际应用libseccompDocker本身就内置了libseccomp支持,默认使用一个预定义的seccomp配置文件,路径通常在/etc/docker/seccomp.json或者/usr/share/docker/seccomp.json。这个默认配置已经禁用了大约44个危险的系统调用。
如果你想自定义更严格的规则,可以这样操作:
docker run --security-opt seccomp=/path/to/your-profile.json your-image
你也可以用seccomp-tools提供的命令行工具直接生成和加载规则,不需要写C代码:
# 生成一个只允许read/write/exit的规则文件 seccomp-tools generate --default-action SCMP_ACT_KILL \ -s read -s write -s exit -s exit_group -s mmap -s munmap \ -o my-seccomp-rule.json # 在Docker中使用 docker run --security-opt seccomp=my-seccomp-rule.json nginx
Podman的用法类似,也支持--security-opt seccomp参数。LXC/LXD容器则在配置文件中通过lxc.seccomp.profile指定规则文件路径。
五、如何确定容器需要哪些系统调用这是实际操作中最关键也最容易踩坑的环节。规则写少了,容器应用正常运行;规则写多了,安全形同虚设。正确的做法是先用strace工具追踪容器应用的实际调用,再根据结果制定白名单。
strace -f -e trace=all -o /tmp/trace.log your-container-app
跑一段时间后,查看trace.log,统计所有出现过的系统调用。然后把这些调用加入白名单,其余全部拒绝。注意要覆盖应用的各种运行场景,比如启动阶段、正常处理请求阶段、异常处理阶段,不同阶段可能用到不同的调用。
还有一个技巧:先用SCMP_ACT_ERRNO(EPERM)作为默认动作,这样被拒绝的调用会返回"权限不足"错误而不是直接杀死进程。这在调试阶段非常有用,能看到应用哪里出了问题而不是直接崩溃。
六、libseccomp的高级用法和注意事项libseccomp不只是简单的允许/拒绝,还支持基于参数条件的精细过滤。比如你可以允许open系统调用,但限制它只能打开/dev/null和/tmp下的文件。这需要用到SCMP_CMP和SCMP_ARG等参数匹配宏,写起来比较复杂,但安全效果极好。
另外要注意架构差异。x86_64和aarch64的系统调用号不同,如果你的规则文件要跨平台使用,需要分别生成。seccomp-tools支持--arch参数指定目标架构。
性能方面,seccomp过滤是在内核态完成的,开销极小,几乎不影响容器性能。但规则数量过多(几百条以上)会有轻微的性能影响,因为每条规则都需要匹配。实际使用中,精简的白名单规则通常只有几十条,完全不用担心性能问题。
还有一点很多人忽略:seccomp规则一旦加载就不能修改,只能重新加载新的过滤器。所以在容器启动脚本中,要确保规则在应用启动之前就加载完毕,顺序不能乱。
七、生产环境的最佳实践建议第一,永远使用"默认拒绝"策略,不要图省事用"默认允许"。第二,用strace充分测试后再上线,不要凭感觉写规则。第三,规则文件要版本管理,纳入CI/CD流程,每次应用更新都要重新验证系统调用需求。第四,结合AppArmor或SELinux做多层防护,seccomp只是其中一层,不能单独依赖。第五,定期更新libseccomp库,关注安全公告,新的内核漏洞可能需要新增过滤规则。
总结来说,在Ubuntu上用libseccomp限制容器系统调用,安装简单、API清晰、效果显著。它不是什么高深技术,但需要你对Linux系统调用有基本了解,对容器内应用的行为有充分掌握。把这两点做好,你的容器安全等级会提升一个档次。
