使用 AppArmor 限制容器对资源的访问

    AppArmor 是一个 Linux 内核安全模块, 它补充了基于标准 Linux 用户和组的权限,将程序限制在一组有限的资源中。 AppArmor 可以配置为任何应用程序减少潜在的攻击面,并且提供更加深入的防御。 它通过调整配置文件进行配置,以允许特定程序或容器所需的访问, 如 Linux 权能字、网络访问、文件权限等。 每个配置文件都可以在 强制(enforcing) 模式(阻止访问不允许的资源)或 投诉(complain) 模式(仅报告冲突)下运行。

    AppArmor 可以通过限制允许容器执行的操作, 和/或通过系统日志提供更好的审计来帮助你运行更安全的部署。 但是,重要的是要记住 AppArmor 不是灵丹妙药, 只能做部分事情来防止应用程序代码中的漏洞。 提供良好的限制性配置文件,并从其他角度强化你的应用程序和集群非常重要。

    • 查看如何在节点上加载配置文件示例
    • 了解如何在 Pod 上强制执行配置文件
    • 了解如何检查配置文件是否已加载
    • 查看违反配置文件时会发生什么
    • 查看无法加载配置文件时会发生什么

    准备开始

    确保:

    1. Kubernetes 版本至少是 v1.4 —— AppArmor 在 Kubernetes v1.4 版本中才添加了对 AppArmor 的支持。 早于 v1.4 版本的 Kubernetes 组件不知道新的 AppArmor 注解并且将会 默认忽略 提供的任何 AppArmor 设置。 为了确保你的 Pod 能够得到预期的保护,必须验证节点的 Kubelet 版本:

      1. gke-test-default-pool-239f5d02-gyn2: v1.4.0
      2. gke-test-default-pool-239f5d02-x1kf: v1.4.0
      3. gke-test-default-pool-239f5d02-xwux: v1.4.0
    2. AppArmor 内核模块已启用 —— 要使 Linux 内核强制执行 AppArmor 配置文件, 必须安装并且启动 AppArmor 内核模块。默认情况下,有几个发行版支持该模块, 如 Ubuntu 和 SUSE,还有许多发行版提供可选支持。要检查模块是否已启用,请检查 /sys/module/apparmor/parameters/enabled 文件:

      1. cat /sys/module/apparmor/parameters/enabled
      2. Y

      如果 Kubelet 包含 AppArmor 支持(>= v1.4), 但是内核模块未启用,它将拒绝运行带有 AppArmor 选项的 Pod。

    说明:

    Ubuntu 携带了许多没有合并到上游 Linux 内核中的 AppArmor 补丁, 包括添加附加钩子和特性的补丁。Kubernetes 只在上游版本中测试过,不承诺支持其他特性。

    1. 容器运行时支持 AppArmor —— 目前所有常见的 Kubernetes 支持的容器运行时都应该支持 AppArmor, 像 Docker、 或 containerd。 请参考相应的运行时文档并验证集群是否满足使用 AppArmor 的要求。

    2. 配置文件已加载 —— 通过指定每个容器都应使用的 AppArmor 配置文件, AppArmor 会被应用到 Pod 上。如果指定的任何配置文件尚未加载到内核, Kubelet(>= v1.4)将拒绝 Pod。 通过检查 /sys/kernel/security/apparmor/profiles 文件, 可以查看节点加载了哪些配置文件。例如:

      1. ssh gke-test-default-pool-239f5d02-gyn2 "sudo cat /sys/kernel/security/apparmor/profiles | sort"
      1. apparmor-test-deny-write (enforce)
      2. apparmor-test-audit-write (enforce)
      3. docker-default (enforce)
      4. k8s-nginx (enforce)

      有关在节点上加载配置文件的详细信息,请参见。

    只要 Kubelet 版本包含 AppArmor 支持(>=v1.4), 如果不满足这些先决条件,Kubelet 将拒绝带有 AppArmor 选项的 Pod。 你还可以通过检查节点就绪状况消息来验证节点上的 AppArmor 支持(尽管这可能会在以后的版本中删除):

    1. kubectl get nodes -o=jsonpath='{range .items[*]}{@.metadata.name}: {.status.conditions[?(@.reason=="KubeletReady")].message}{"\n"}{end}'
    1. gke-test-default-pool-239f5d02-gyn2: kubelet is posting ready status. AppArmor enabled
    2. gke-test-default-pool-239f5d02-x1kf: kubelet is posting ready status. AppArmor enabled
    3. gke-test-default-pool-239f5d02-xwux: kubelet is posting ready status. AppArmor enabled

    说明:

    AppArmor 配置文件是按 逐个容器 的形式来设置的。 要指定用来运行 Pod 容器的 AppArmor 配置文件,请向 Pod 的 metadata 添加注解:

    1. container.apparmor.security.beta.kubernetes.io/<container_name>: <profile_ref>

    <container_name> 的名称是配置文件所针对的容器的名称,<profile_def> 则设置要应用的配置文件。 <profile_ref> 可以是以下取值之一:

    • runtime/default 应用运行时的默认配置
    • localhost/<profile_name> 应用在主机上加载的名为 <profile_name> 的配置文件
    • unconfined 表示不加载配置文件

    有关注解和配置文件名称格式的详细信息,请参阅 。

    Kubernetes AppArmor 强制执行机制首先检查所有先决条件都已满足, 然后将所选的配置文件转发到容器运行时进行强制执行。 如果未满足先决条件,Pod 将被拒绝,并且不会运行。

    要验证是否应用了配置文件,可以在容器创建事件中查找所列出的 AppArmor 安全选项:

    1. kubectl get events | grep Created

    你还可以通过检查容器的 proc attr,直接验证容器的根进程是否以正确的配置文件运行:

    1. kubectl exec <pod_name> -- cat /proc/1/attr/current
    1. k8s-apparmor-example-deny-write (enforce)

    举例

    本例假设你已经设置了一个集群使用 AppArmor 支持。

    首先,我们需要将要使用的配置文件加载到节点上。配置文件拒绝所有文件写入:

    1. #include <tunables/global>
    2. profile k8s-apparmor-example-deny-write flags=(attach_disconnected) {
    3. #include <abstractions/base>
    4. file,
    5. # 拒绝所有文件写入
    6. deny /** w,
    7. }

    由于我们不知道 Pod 将被调度到哪里,我们需要在所有节点上加载配置文件。 在本例中,我们将使用 SSH 来安装概要文件, 但是在中讨论了其他方法。

    1. NODES=(
    2. # 你的节点的可通过 SSH 访问的域名
    3. gke-test-default-pool-239f5d02-gyn2.us-central1-a.my-k8s
    4. gke-test-default-pool-239f5d02-x1kf.us-central1-a.my-k8s
    5. for NODE in ${NODES[*]}; do ssh $NODE 'sudo apparmor_parser -q <<EOF
    6. #include <tunables/global>
    7. profile k8s-apparmor-example-deny-write flags=(attach_disconnected) {
    8. #include <abstractions/base>
    9. file,
    10. # Deny all file writes.
    11. deny /** w,
    12. }
    13. EOF'
    14. done

    接下来,我们将运行一个带有拒绝写入配置文件的简单 “Hello AppArmor” Pod:

    pods/security/hello-apparmor.yaml

    1. apiVersion: v1
    2. kind: Pod
    3. metadata:
    4. name: hello-apparmor
    5. annotations:
    6. # 告知 Kubernetes 去应用 AppArmor 配置 "k8s-apparmor-example-deny-write"。
    7. # 请注意,如果节点上运行的 Kubernetes 不是 1.4 或更高版本,此注解将被忽略。
    8. container.apparmor.security.beta.kubernetes.io/hello: localhost/k8s-apparmor-example-deny-write
    9. spec:
    10. containers:
    11. - name: hello
    12. image: busybox:1.28
    13. command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ]
    1. kubectl create -f ./hello-apparmor.yaml

    如果我们查看 Pod 事件,我们可以看到 Pod 容器是用 AppArmor 配置文件 “k8s-apparmor-example-deny-write” 所创建的:

    1. kubectl get events | grep hello-apparmor
    1. 14s 14s 1 hello-apparmor Pod Normal Scheduled {default-scheduler } Successfully assigned hello-apparmor to gke-test-default-pool-239f5d02-gyn2
    2. 14s 14s 1 hello-apparmor Pod spec.containers{hello} Normal Pulling {kubelet gke-test-default-pool-239f5d02-gyn2} pulling image "busybox"
    3. 13s 13s 1 hello-apparmor Pod spec.containers{hello} Normal Pulled {kubelet gke-test-default-pool-239f5d02-gyn2} Successfully pulled image "busybox"
    4. 13s 13s 1 hello-apparmor Pod spec.containers{hello} Normal Created {kubelet gke-test-default-pool-239f5d02-gyn2} Created container with docker id 06b6cd1c0989; Security:[seccomp=unconfined apparmor=k8s-apparmor-example-deny-write]
    5. 13s 13s 1 hello-apparmor Pod spec.containers{hello} Normal Started {kubelet gke-test-default-pool-239f5d02-gyn2} Started container with docker id 06b6cd1c0989

    我们可以通过检查该配置文件的 proc attr 来验证容器是否实际使用该配置文件运行:

    1. k8s-apparmor-example-deny-write (enforce)
    1. kubectl exec hello-apparmor -- touch /tmp/test
    1. touch: /tmp/test: Permission denied
    2. error: error executing remote command: command terminated with non-zero exit code: Error executing in Docker Container: 1

    最后,让我们看看如果我们试图指定一个尚未加载的配置文件会发生什么:

    1. kubectl create -f /dev/stdin <<EOF
    1. apiVersion: v1
    2. kind: Pod
    3. metadata:
    4. name: hello-apparmor-2
    5. annotations:
    6. container.apparmor.security.beta.kubernetes.io/hello: localhost/k8s-apparmor-example-allow-write
    7. spec:
    8. containers:
    9. - name: hello
    10. image: busybox:1.28
    11. command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ]
    12. EOF
    13. pod/hello-apparmor-2 created
    1. kubectl describe pod hello-apparmor-2
    1. Name: hello-apparmor-2
    2. Namespace: default
    3. Node: gke-test-default-pool-239f5d02-x1kf/
    4. Start Time: Tue, 30 Aug 2016 17:58:56 -0700
    5. Status: Pending
    6. Reason: AppArmor
    7. Message: Pod Cannot enforce AppArmor: profile "k8s-apparmor-example-allow-write" is not loaded
    8. IP:
    9. Controllers: <none>
    10. Containers:
    11. hello:
    12. Container ID:
    13. Image: busybox
    14. Image ID:
    15. Port:
    16. Command:
    17. sh
    18. -c
    19. echo 'Hello AppArmor!' && sleep 1h
    20. State: Waiting
    21. Reason: Blocked
    22. Ready: False
    23. Restart Count: 0
    24. Environment: <none>
    25. Mounts:
    26. /var/run/secrets/kubernetes.io/serviceaccount from default-token-dnz7v (ro)
    27. Conditions:
    28. Type Status
    29. Initialized True
    30. Ready False
    31. PodScheduled True
    32. Volumes:
    33. default-token-dnz7v:
    34. Type: Secret (a volume populated by a Secret)
    35. SecretName: default-token-dnz7v
    36. Optional: false
    37. QoS Class: BestEffort
    38. Node-Selectors: <none>
    39. Tolerations: <none>
    40. Events:
    41. FirstSeen LastSeen Count From SubobjectPath Type Reason Message
    42. --------- -------- ----- ---- ------------- -------- ------ -------
    43. 23s 23s 1 {default-scheduler } Normal Scheduled Successfully assigned hello-apparmor-2 to e2e-test-stclair-node-pool-t1f5
    44. 23s 23s 1 {kubelet e2e-test-stclair-node-pool-t1f5} Warning AppArmor Cannot enforce AppArmor: profile "k8s-apparmor-example-allow-write" is not loaded

    注意 Pod 呈现 Pending 状态,并且显示一条有用的错误信息: Pod Cannot enforce AppArmor: profile "k8s-apparmor-example-allow-write" is not loaded。 还用相同的消息记录了一个事件。

    Kubernetes 目前不提供任何本地机制来将 AppArmor 配置文件加载到节点上。 有很多方法可以设置配置文件,例如:

    • 通过在每个节点上运行 Pod 的 来确保加载了正确的配置文件。 可以在这里找到实现示例。
    • 在节点初始化时,使用节点初始化脚本(例如 Salt、Ansible 等)或镜像。
    • 通过将配置文件复制到每个节点并通过 SSH 加载它们,如。

    调度程序不知道哪些配置文件加载到哪个节点上,因此必须将全套配置文件加载到每个节点上。 另一种方法是为节点上的每个配置文件(或配置文件类)添加节点标签, 并使用节点选择器确保 Pod 在具有所需配置文件的节点上运行。

    如果你不希望 AppArmor 在集群上可用,可以通过命令行标志禁用它:

    1. --feature-gates=AppArmor=false

    禁用时,任何包含 AppArmor 配置文件的 Pod 都将导致验证失败,且返回 “Forbidden” 错误。

    说明:

    即使此 Kubernetes 特性被禁用,运行时仍可能强制执行默认配置文件。 当 AppArmor 升级为正式版 (GA) 时,禁用 AppArmor 功能的选项将被删除。

    编写配置文件

    获得正确指定的 AppArmor 配置文件可能是一件棘手的事情。幸运的是,有一些工具可以帮助你做到这一点:

    • aa-genprofaa-logprof 通过监视应用程序的活动和日志并准许它所执行的操作来生成配置文件规则。 提供了进一步的指导。
    • bane 是一个用于 Docker的 AppArmor 配置文件生成器,它使用一种简化的画像语言(profile language)。

    想要调试 AppArmor 的问题,你可以检查系统日志,查看具体拒绝了什么。 AppArmor 将详细消息记录到 dmesg, 错误通常可以在系统日志中或通过 journalctl 找到。 更多详细信息参见 。

    指定容器将使用的配置文件:

    • 键名container.apparmor.security.beta.kubernetes.io/<container_name>, 其中 <container_name> 与 Pod 中某容器的名称匹配。 可以为 Pod 中的每个容器指定单独的配置文件。
    • 键值:对配置文件的引用,如下所述
    • runtime/default:指默认运行时配置文件。
      • 等同于不指定配置文件,只是它仍然需要启用 AppArmor。
      • 实际上,许多容器运行时使用相同的 OCI 默认配置文件,在此处定义:
    • :按名称引用加载到节点(localhost)上的配置文件。
    • unconfined:这相当于为容器禁用 AppArmor。

    任何其他配置文件引用格式无效。

    接下来

    其他资源: