如何使用 AppArmor 限制应用的权限

2023/03/17 22:27 下午 posted in  Kubernetes

众所周知,在云原生环境中,我们可以通过 RBAC 机制控制应用对集群中资源的访问权限,但对于生产环境来说,这些还远远不够,当应用可以访问到宿主机的资源(比如 Linux 权能字、网络访问、文件权限)时,宿主机仍然存在安全风险。对于这种情况,Linux 内核安全模块 AppArmor 补充了基于标准 Linux 用户和组的权限,将程序限制在一组有限的资源中,同时也是对 Pod 的保护,使其免受不必要的攻击。

在开启了 AppArmor 的系统中,容器运行时会给容器使用默认的权限配置,当然,应用也可以使用自定义配置。本文将讲述如何在容器中使用 AppArmor。

如何使用 AppArmor

AppArmor 是一个 Linux 内核安全模块,允许系统管理员使用每个程序的配置文件来限制程序的功能。配置文件可以允许网络访问、原始套接字访问以及在匹配路径上读取、写入或执行文件的权限等功能。

不过,并不是所有的系统都支持 AppArmor。默认情况下,有几个发行版支持该模块,如 Ubuntu 和 SUSE,还有许多发行版提供可选支持。可以通过以下命令检查模块是否已启用 AppArmor:

$ cat /sys/module/apparmor/parameters/enabled
Y

AppArmor 在以下两种类型的配置文件模式下运行:

  • enforce: 在强制模式下,系统开始强制执行规则并在 syslog 或 auditd 中报告违规尝试(仅当安装了 auditd 时)并且不允许操作。
  • complain: 在投诉模式下,系统不执行任何规则。它只会记录违规尝试。

配置文件是位于 /etc/apparmor.d/ 目录下的文本文件。这些文件以它们分析的可执行文件的完整路径命名,但将 / 替换为 .。例如,tcpdump 命令位于 /usr/sbin/tcpdump,等效的 AppArmor 配置文件将命名为 usr.sbin.tcpdump

也可以设置自己的配置文件,比如 sample profile 设置限制所有文件的写权限:

$ cat <<EOF >/etc/apparmor.d/containers/sample
#include <tunables/global>

profile juicefs flags=(attach_disconnected) {
  #include <abstractions/base>
  file,
  mount,
}
EOF

将上述配置生效:

$ apparmor_parser /etc/apparmor.d/containers/sample
$ apparmor_status
apparmor module is loaded.
35 profiles are loaded.
35 profiles are in enforce mode.
   ...
   sample
   ...

AppArmor 主要支持 Capability、File、Network 三种规则:

  • Capability:Linux 进程的 Capability,这里不会授予 Capability,只对进程能力集做有效 mask。比如,capability sys_admin, 表示允许执行系统管理任务。
  • File:
    • 对文件的读写执行等权限。如 /home/** rw, 表示对 /home 下所有文件具备读写权限;
    • 文件系统的挂载规则,包括是否具备挂载、卸载权限,文件系统类型、挂载参数以及挂载路径。如 mount options=ro /dev/foo, 表示允许以只读方式挂载到 /dev/foo 路径;
    • chmod、chown、setuid 等规则。
  • Network:
    • 对网络 socket 的权限,包括 create、accept、bind 等,以及网络的类型、地址等,如 network tcp, 表示支持所有 tcp 类型的网络操作;
    • DBUS、IPC、Signals 等规则。

AppArmor 的配置文件定义的十分灵活,更多具体使用可以参见 AppArmor 文档

容器中使用 AppArmor

在主机上配置好 AppArmor 配置文件后,我们来看如何在容器中使用。

引擎为 Docker

当容器引擎为 Docker 时,作为对比,首先运行一个普通的 nginx 容器,并创建一个 test 文件:

$ docker run --rm -it nginx /bin/bash
root@45bf95280766:/# cd
root@45bf95280766:~# touch test
root@45bf95280766:~# ls
test

接下来运行一个使用上述限制所有文件的写权限的 AppArmor 配置文件 sample 的容器,并创建一个 test 文件:

$ docker run --rm -it --security-opt "apparmor=sample" nginx /bin/bash
root@1d1d8f6b1aa0:/# cd ~
root@1d1d8f6b1aa0:~# touch test
touch: cannot touch 'test': Permission denied

我们可以看到 AppArmor 配置文件阻止了创建文件操作。

引擎为 Containerd

当容器引擎为 Containerd 时,做一样的测试:

$ nerdctl run --rm -it docker.io/library/nginx:latest /bin/bash
root@09e6c02616a7:/# cd ~
root@09e6c02616a7:~# touch test
root@09e6c02616a7:~#
root@09e6c02616a7:~# ls
test

在容器中使用 sample 配置文件:

$ nerdctl run --rm -it --security-opt "apparmor=sample" docker.io/library/nginx:latest /bin/bash
root@8be22275bc9d:/# cd
root@8be22275bc9d:~# touch test
touch: cannot touch 'test': Permission denied

同样,AppArmor 配置文件也阻止了创建文件操作。

Kubernetes 中使用 AppArmor

如何在 Kubernetes 中使用呢?方式为在 Pod 的 annotation 中声明哪个容器使用哪个配置文件,其 key 为 container.apparmor.security.beta.kubernetes.io/<container_name>,value 有 3 个不同的值:

  • runtime/default:使用容器运行时默认的配置(如 docker-default );
  • localhost/<profile_name>:使用节点上生效的配置文件,<profile_name> 为文件名;
  • unconfined:不使用任何 AppArmor 配置文件。

以上面创建的配置文件 sample 为例:

apiVersion: v1
kind: Pod
metadata:
  name: test
  annotations:
    container.apparmor.security.beta.kubernetes.io/app: localhost/sample
spec:
  containers:
  - args:
    - -c
    - sleep 1000000
    command:
    - /bin/sh
    image: ubuntu
    name: app

同样测试 pod 中是否有创建文件的权限:

$ kubectl exec -it test -- bash
root@test:/# cd
root@test:~# touch test
touch: cannot touch 'test': Permission denied

总结

在开启了 AppArmor 的系统中,使用 AppArmor 对节点及 Pod 的保护是非常有必要的,但是 AppArmor 的配置也是比较棘手的。不过社区中已经有较为成熟的解决方案,比如对于快速生成 AppArmor 配置文件,可以用工具 bane。对于每个节点均配置同样的配置文件,可以使用 DaemonSet 来实现,参考案例;也可以节点初始化脚本(例如 Salt、Ansible 等)或镜像;也可以通过将配置文件复制到每个节点并通过 SSH 加载它们,参考示例