众所周知,在云原生环境中,我们可以通过 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 等规则。
- 对网络 socket 的权限,包括 create、accept、bind 等,以及网络的类型、地址等,如
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 加载它们,参考示例。