Java 容器化应用的安全加固:SecComp/AppArmor 在运行时环境的配置
大家好,今天我们来聊聊如何利用 SecComp 和 AppArmor 这两个 Linux 安全模块,在运行时环境中加固 Java 容器化应用。容器化虽然带来了部署和扩展的便利性,但也引入了新的安全挑战。默认情况下,容器内的进程拥有相对广泛的系统调用权限,这可能被恶意攻击者利用,从而危害宿主机甚至整个系统。SecComp 和 AppArmor 的作用就在于限制容器内进程的权限,缩小攻击面,提升整体安全性。
1. 为什么需要 SecComp 和 AppArmor?
首先,我们必须明确为什么需要这些额外的安全层。
- 减少攻击面: 容器内的 Java 应用可能存在漏洞,攻击者可能利用这些漏洞执行恶意代码。默认情况下,这些恶意代码可以访问大量的系统调用,执行各种操作。通过限制系统调用,即使攻击者成功入侵容器,他们能够执行的操作也大大减少。
- 防御未知漏洞: 即使 Java 应用没有已知漏洞,也可能存在未知的 0day 漏洞。SecComp 和 AppArmor 可以作为纵深防御的一部分,限制未知漏洞的利用。
- 满足合规要求: 许多行业和法规都要求对应用进行安全加固。使用 SecComp 和 AppArmor 可以帮助满足这些合规要求。
- 防止权限提升: 即使攻击者成功利用漏洞,也可能无法利用受限的系统调用来进行权限提升,从而保护宿主机和底层系统。
2. SecComp (Secure Computing Mode) 详解
SecComp 是 Linux 内核提供的一种安全机制,它允许进程在进入安全模式后,只能执行一组预先定义的系统调用。任何尝试执行未定义的系统调用都会导致进程被终止。
2.1 SecComp 的工作原理
SecComp 基于 BPF (Berkeley Packet Filter) 技术,可以编写 BPF 规则来过滤系统调用。当进程执行系统调用时,内核会执行 BPF 规则,根据规则的匹配结果来决定是否允许该系统调用。
2.2 SecComp 的配置方式
SecComp 的配置通常通过 JSON 文件来定义。JSON 文件包含一个 defaultAction 字段,用于指定默认的系统调用处理方式,以及一个 syscalls 数组,用于定义允许或禁止的系统调用。
2.3 SecComp 的模式
SecComp 主要有两种模式:
- Strict Mode: 这是最严格的模式,只允许
exit(),sigreturn(),read(), 和write()四个系统调用。这种模式通常不适用于 Java 应用,因为它需要更多的系统调用才能正常运行。 - Filter Mode: 这种模式允许使用 BPF 规则来更细粒度地控制系统调用。
2.4 SecComp 在 Docker 中的应用
Docker 支持通过 --security-opt seccomp= 参数来指定 SecComp 配置文件。例如:
docker run --security-opt seccomp=profile.json your-java-app
其中 profile.json 是 SecComp 配置文件。
2.5 一个 SecComp 配置示例 (profile.json)
以下是一个示例的 profile.json 文件,它允许 Java 应用常用的系统调用:
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": [
"SCMP_ARCH_X86_64",
"SCMP_ARCH_X86",
"SCMP_ARCH_X32"
],
"syscalls": [
{
"names": [
"read",
"write",
"open",
"close",
"fstat",
"lstat",
"poll",
"lseek",
"mmap",
"mprotect",
"munmap",
"brk",
"rt_sigaction",
"rt_sigprocmask",
"rt_sigreturn",
"ioctl",
"pread64",
"pwrite64",
"readv",
"writev",
"pipe",
"pipe2",
"select",
"sched_yield",
"mremap",
"msync",
"mincore",
"madvise",
"shmget",
"shmat",
"shmdt",
"semget",
"semop",
"semctl",
"msgsnd",
"msgrcv",
"msgget",
"msgctl",
"socket",
"socketpair",
"bind",
"connect",
"listen",
"accept",
"getsockname",
"getpeername",
"sendto",
"recvfrom",
"sendmsg",
"recvmsg",
"shutdown",
"setsockopt",
"getsockopt",
"sendfile",
"recv",
"send",
"epoll_create",
"epoll_ctl",
"epoll_wait",
"clone",
"exit",
"exit_group",
"wait4",
"kill",
"uname",
"semtimedop",
"timer_create",
"timer_settime",
"timer_gettime",
"timer_delete",
"clock_gettime",
"clock_nanosleep",
"fcntl",
"fdatasync",
"fsync",
"getpriority",
"setpriority",
"nanosleep",
"getuid",
"getgid",
"geteuid",
"getegid",
"gettid",
"tgkill",
"statfs",
"fstatfs",
"futex",
"set_robust_list",
"get_robust_list",
"splice",
"tee",
"sync_file_range",
"sync",
"vmsplice",
"move_pages",
"getcpu",
"klogctl",
"ioprio_set",
"ioprio_get",
"inotify_init",
"inotify_add_watch",
"inotify_rm_watch",
"eventfd2",
"getdents",
"getdents64",
"setns",
"process_vm_readv",
"process_vm_writev",
"ptrace",
"capget",
"capset",
"mkdir",
"mkdirat",
"unlink",
"unlinkat",
"rmdir",
"rename",
"renameat",
"symlink",
"symlinkat",
"link",
"linkat",
"readlink",
"readlinkat",
"chown",
"fchown",
"lchown",
"chmod",
"fchmod",
"fchmodat",
"access",
"faccessat",
"truncate",
"ftruncate",
"utimes",
"utimensat",
"stat",
"newfstatat",
"lchown",
"setresuid",
"setresgid",
"getresuid",
"getresgid",
"getgroups",
"setgroups",
"sysinfo",
"umask",
"prctl",
"setrlimit",
"getrlimit",
"sched_getaffinity",
"sched_setaffinity",
"pivot_root",
"mount",
"umount2",
"swapon",
"swapoff",
"reboot",
"adjtimex",
"settimeofday",
"stime",
"create_module",
"init_module",
"finit_module",
"delete_module",
"query_module",
"kcmp",
"fadvise64",
"setuid",
"setgid",
"getppid",
"getsid",
"setpgid",
"getpgid",
"getpgrp",
"setsid",
"getpriority",
"setpriority",
"sched_getscheduler",
"sched_setscheduler",
"sched_getparam",
"sched_setparam",
"getitimer",
"setitimer",
"getrusage",
"acct",
"quotactl",
"syslog",
"personality",
"setdomainname",
"sethostname",
"nfsservctl",
"getmemid",
"pciconfig_read",
"pciconfig_write",
"uselib",
"sysfs",
"modify_ldt",
"security",
"chroot",
"clock_settime",
"clock_adjtime",
"vm86",
"iopl",
"io",
"flock",
"openat",
"getrandom",
"memfd_create",
"seccomp",
"execve",
"membarrier",
"userfaultfd",
"open_by_handle_at",
"migrate_pages",
"perf_event_open",
"bpf",
"fanotify_init",
"fanotify_mark",
"name_to_handle_at",
"open_tree",
"move_mount",
"seccomp",
"pidfd_open",
"pidfd_send_signal"
],
"action": "SCMP_ACT_ALLOW",
"args": []
}
]
}
2.6 如何生成 SecComp Profile
手动编写 SecComp 配置文件非常繁琐且容易出错。可以使用工具来自动生成 SecComp profile。
-
oci-seccomp-bpf-tool: 这个工具可以基于容器的运行时行为生成 SecComp profile.# 安装 oci-seccomp-bpf-tool go install github.com/opencontainers/seccomp-tools/cmd/seccomp-profile@latest # 运行容器并记录系统调用 docker run --rm --name my-java-app -it your-java-app bash # 在容器内执行你的 Java 应用,并进行一些操作 # 退出容器 # 生成 SecComp profile seccomp-profile --pid $(docker inspect --format '{{.State.Pid}}' my-java-app) > profile.json # 停止容器 docker stop my-java-app -
Sysdig Inspect: Sysdig Inspect 是一款强大的容器安全工具,可以用来分析容器的运行时行为,并生成 SecComp profile。
2.7 SecComp 的优点和缺点
优点:
- 简单易用
- 性能开销小
- 有效地限制系统调用
缺点:
- 配置复杂,需要了解应用的系统调用行为
- 可能会阻止一些合法的操作
- 只能控制系统调用,无法控制文件访问等其他权限
3. AppArmor (Application Armor) 详解
AppArmor 是 Linux 内核提供的另一种安全模块,它允许管理员为应用程序定义安全策略,控制应用程序可以访问的文件、网络资源、capabilities 等。
3.1 AppArmor 的工作原理
AppArmor 基于路径名来控制应用程序的访问权限。管理员可以创建一个 AppArmor profile,指定应用程序可以访问的文件和目录,以及可以使用的 capabilities。当应用程序尝试访问受保护的资源时,AppArmor 会检查 profile,根据 profile 的规则来决定是否允许该访问。
3.2 AppArmor 的配置方式
AppArmor 的配置通过文本文件来定义,通常位于 /etc/apparmor.d/ 目录下。每个 profile 对应一个应用程序,profile 文件包含了应用程序的访问控制规则。
3.3 AppArmor 的模式
AppArmor 主要有两种模式:
- Enforce Mode: 这是强制模式,AppArmor 会严格执行 profile 的规则,阻止任何违反规则的行为。
- Complain Mode: 这是投诉模式,AppArmor 不会阻止违反规则的行为,但会将违反规则的行为记录到日志中。这种模式通常用于测试和调试 AppArmor profile。
3.4 AppArmor 在 Docker 中的应用
Docker 默认会为容器应用一个 AppArmor profile。可以使用 --security-opt apparmor= 参数来指定自定义的 AppArmor profile。例如:
docker run --security-opt apparmor=my-java-app your-java-app
其中 my-java-app 是 AppArmor profile 的名称。
3.5 一个 AppArmor 配置示例 (my-java-app)
以下是一个示例的 my-java-app 文件,它允许 Java 应用访问 /opt/java-app/ 目录下的文件:
#include <tunables/global>
profile my-java-app flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
file,
# Allow access to /opt/java-app/
/opt/java-app/** rwk,
# Allow access to /tmp/
/tmp/** rwk,
# Allow access to /dev/urandom
/dev/urandom r,
# Allow access to network
network inet tcp,
network inet udp,
# Signal permissions
signal (receive,send) peer=unconfined,
}
解释:
#include <tunables/global>: 包含全局配置。profile my-java-app flags=(attach_disconnected,mediate_deleted): 定义 profile 的名称和标志。#include <abstractions/base>: 包含基本抽象配置,提供了一些常用的权限。file: 允许文件访问。/opt/java-app/** rwk: 允许对/opt/java-app/目录及其子目录下的文件进行读 (r)、写 (w) 和执行 (k) 操作。/tmp/** rwk: 允许对/tmp/目录及其子目录下的文件进行读 (r)、写 (w) 和执行 (k) 操作。/dev/urandom r: 允许读取/dev/urandom设备。network inet tcp: 允许使用 TCP 网络。network inet udp: 允许使用 UDP 网络。signal (receive,send) peer=unconfined: 允许接收和发送信号。
3.6 AppArmor 的配置命令
aa-genprof: 用于交互式地生成 AppArmor profile。aa-logprof: 用于分析 AppArmor 日志,并根据日志信息更新 profile。apparmor_status: 用于查看 AppArmor 的状态。aa-enforce: 将 profile 设置为 enforce 模式。aa-complain: 将 profile 设置为 complain 模式。
示例:
# 1. 启动容器
docker run --rm --name my-java-app -it your-java-app bash
# 2. 在另一个终端,使用 aa-genprof 生成 profile
sudo aa-genprof my-java-app
# 3. aa-genprof 会询问你是否要监控某个进程。 选择容器的进程ID (可以使用 docker inspect --format '{{.State.Pid}}' my-java-app 找到)
# 4. 在容器内执行你的 Java 应用,并进行一些操作
# 5. 在运行 aa-genprof 的终端,根据提示选择 "S" (Scan) 来扫描日志,并根据提示更新 profile.
# 6. 完成后,选择 "F" (Finish) 来保存 profile.
# 7. 将 profile 设置为 enforce 模式
sudo aa-enforce my-java-app
# 8. 停止容器
docker stop my-java-app
3.7 AppArmor 的优点和缺点
优点:
- 可以控制文件访问、网络资源、capabilities 等多种权限
- 配置灵活,可以根据应用程序的需求进行定制
- 易于使用,可以通过交互式工具生成 profile
缺点:
- 配置复杂,需要了解应用程序的访问模式
- 可能会阻止一些合法的操作
- 性能开销相对较大
4. SecComp 和 AppArmor 的选择
SecComp 和 AppArmor 都是有效的安全模块,但它们适用于不同的场景。
| 特性 | SecComp | AppArmor |
|---|---|---|
| 粒度 | 系统调用级别 | 文件访问、网络资源、capabilities 等 |
| 配置难度 | 较难,需要了解系统调用 | 相对容易,可以通过交互式工具生成 |
| 适用场景 | 限制应用程序可以执行的系统调用 | 控制应用程序可以访问的资源 |
| 性能开销 | 小 | 相对较大 |
| 侧重点 | 减少内核攻击面 | 限制应用程序的行为 |
建议:
- 如果只需要限制应用程序可以执行的系统调用,可以使用 SecComp。
- 如果需要更细粒度地控制应用程序可以访问的资源,可以使用 AppArmor。
- 可以将 SecComp 和 AppArmor 结合使用,以获得更全面的安全防护。
5. Java 应用的特殊考虑
Java 应用运行在 JVM 之上,JVM 本身会执行大量的系统调用。因此,在配置 SecComp 和 AppArmor 时,需要特别注意 JVM 的行为,避免阻止 JVM 正常运行所需的系统调用或文件访问。
- 确定 JVM 所需的系统调用: 可以使用
strace命令来跟踪 JVM 的系统调用,并根据跟踪结果来配置 SecComp profile。 - 确定 JVM 所需的文件访问权限: 可以使用
auditd工具来监控 JVM 的文件访问行为,并根据监控结果来配置 AppArmor profile。 - 使用白名单策略: 尽量使用白名单策略,只允许应用程序所需的系统调用和文件访问,而不是使用黑名单策略,禁止应用程序不需要的系统调用和文件访问。
- 逐步收紧权限: 先使用 complain 模式测试 AppArmor profile,确保应用程序可以正常运行,然后再切换到 enforce 模式。
6. 代码示例
下面是一些代码示例,演示如何在 Java 应用中使用 SecComp 和 AppArmor。
6.1 使用 strace 分析系统调用
# 运行 strace 命令跟踪 JVM 的系统调用
strace -f -c java -version
# 分析 strace 的输出,确定 JVM 所需的系统调用
6.2 使用 auditd 监控文件访问
# 配置 auditd 规则,监控 JVM 的文件访问
sudo auditctl -w /opt/java-app/ -p rwa -k java_app
# 运行 Java 应用
# 查看 auditd 日志,分析 JVM 的文件访问行为
sudo ausearch -k java_app
6.3 在 Java 代码中检测 SecComp 和 AppArmor 是否生效
可以通过检查某些系统调用或文件访问是否被阻止来判断 SecComp 和 AppArmor 是否生效。
import java.io.File;
import java.io.IOException;
public class SecurityCheck {
public static void main(String[] args) {
try {
// 尝试创建一个文件,如果 AppArmor 阻止了文件创建,会抛出 IOException
File file = new File("/tmp/test.txt");
if (file.createNewFile()) {
System.out.println("File created successfully.");
file.delete();
} else {
System.out.println("File creation failed.");
}
} catch (IOException e) {
System.err.println("AppArmor is likely blocking file creation: " + e.getMessage());
}
try {
// 尝试执行一个系统调用(例如,获取系统时间),如果 SecComp 阻止了系统调用,可能会抛出异常或程序崩溃
long currentTimeMillis = System.currentTimeMillis();
System.out.println("Current time: " + currentTimeMillis);
} catch (Exception e) {
System.err.println("SecComp is likely blocking system call: " + e.getMessage());
}
}
}
注意: 这段代码仅仅是一个简单的示例,实际应用中需要根据具体的安全策略和应用程序的需求进行调整。
7. 总结
使用 SecComp 和 AppArmor 可以有效地加固 Java 容器化应用,减少攻击面,提升整体安全性。配置 SecComp 和 AppArmor 需要了解应用程序的行为,并根据应用程序的需求进行定制。可以将 SecComp 和 AppArmor 结合使用,以获得更全面的安全防护。此外,要记得持续监控和更新安全策略,以应对新的安全威胁。
8. 记住要持续监控和更新安全策略
安全是一个持续的过程,而不是一次性的任务。我们应该定期审查和更新安全策略,以应对新的安全威胁和应用程序的变化。同时,应该建立完善的监控机制,及时发现和处理安全事件。
9. 容器安全是多方面的
SecComp 和 AppArmor 只是容器安全的一部分。要构建一个安全的容器化环境,还需要考虑其他方面的安全措施,例如镜像安全、网络安全、访问控制等。