Java容器化应用的安全加固:SecComp/AppArmor在运行时环境的配置

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 只是容器安全的一部分。要构建一个安全的容器化环境,还需要考虑其他方面的安全措施,例如镜像安全、网络安全、访问控制等。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注