1 - 在集群级别应用 Pod 安全标准

Pod 安全准入(PSA)在 v1.23 及更高版本默认启用, 因为它已进阶为 Beta。 Pod 安全准入是在创建 Pod 时应用 Pod 安全标准的准入控制器。 本教程将向你展示如何在集群级别实施 baseline Pod 安全标准, 该标准将标准配置应用于集群中的所有名字空间。

要将 Pod 安全标准应用于特定名字空间, 请参阅在名字空间级别应用 Pod 安全标准

如果你正在运行 v1.26 以外的 Kubernetes 版本, 请查阅该版本的文档。

准备开始

在你的工作站中安装以下内容:

正确选择要应用的 Pod 安全标准

Pod 安全准入 允许你使用以下模式应用内置的 Pod 安全标准enforceauditwarn

要收集信息以便选择最适合你的配置的 Pod 安全标准,请执行以下操作:

  1. 创建一个没有应用 Pod 安全标准的集群:

    kind create cluster --name psa-wo-cluster-pss --image kindest/node:v1.24.0
    

    输出类似于:

    Creating cluster "psa-wo-cluster-pss" ...
    ✓ Ensuring node image (kindest/node:v1.24.0) 🖼
    ✓ Preparing nodes 📦  
    ✓ Writing configuration 📜
    ✓ Starting control-plane 🕹️
    ✓ Installing CNI 🔌
    ✓ Installing StorageClass 💾
    Set kubectl context to "kind-psa-wo-cluster-pss"
    You can now use your cluster with:
    
    kubectl cluster-info --context kind-psa-wo-cluster-pss
    
    Thanks for using kind! 😊
    
  1. 将 kubectl 上下文设置为新集群:

    kubectl cluster-info --context kind-psa-wo-cluster-pss
    

    输出类似于:

    Kubernetes control plane is running at https://127.0.0.1:61350
    
    CoreDNS is running at https://127.0.0.1:61350/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
    
    To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
    
  1. 获取集群中的名字空间列表:

    kubectl get ns
    

    输出类似于:

    NAME                 STATUS   AGE
    default              Active   9m30s
    kube-node-lease      Active   9m32s
    kube-public          Active   9m32s
    kube-system          Active   9m32s
    local-path-storage   Active   9m26s
    
  1. 使用 --dry-run=server 来了解应用不同的 Pod 安全标准时会发生什么:

    1. Privileged

      kubectl label --dry-run=server --overwrite ns --all \                    
      pod-security.kubernetes.io/enforce=privileged
      

      输出类似于:

      namespace/default labeled
      namespace/kube-node-lease labeled
      namespace/kube-public labeled
      namespace/kube-system labeled
      namespace/local-path-storage labeled
      
    2. Baseline

      kubectl label --dry-run=server --overwrite ns --all \
      pod-security.kubernetes.io/enforce=baseline
      

      输出类似于:

      namespace/default labeled
      namespace/kube-node-lease labeled
      namespace/kube-public labeled
      Warning: existing pods in namespace "kube-system" violate the new PodSecurity enforce level "baseline:latest"
      Warning: etcd-psa-wo-cluster-pss-control-plane (and 3 other pods): host namespaces, hostPath volumes
      Warning: kindnet-vzj42: non-default capabilities, host namespaces, hostPath volumes
      Warning: kube-proxy-m6hwf: host namespaces, hostPath volumes, privileged
      namespace/kube-system labeled
      namespace/local-path-storage labeled
      
    3. Restricted

      kubectl label --dry-run=server --overwrite ns --all \
      pod-security.kubernetes.io/enforce=restricted
      

      输出类似于:

      namespace/default labeled
      namespace/kube-node-lease labeled
      namespace/kube-public labeled
      Warning: existing pods in namespace "kube-system" violate the new PodSecurity enforce level "restricted:latest"
      Warning: coredns-7bb9c7b568-hsptc (and 1 other pod): unrestricted capabilities, runAsNonRoot != true, seccompProfile
      Warning: etcd-psa-wo-cluster-pss-control-plane (and 3 other pods): host namespaces, hostPath volumes, allowPrivilegeEscalation != false, unrestricted capabilities, restricted volume types, runAsNonRoot != true
      Warning: kindnet-vzj42: non-default capabilities, host namespaces, hostPath volumes, allowPrivilegeEscalation != false, unrestricted capabilities, restricted volume types, runAsNonRoot != true, seccompProfile
      Warning: kube-proxy-m6hwf: host namespaces, hostPath volumes, privileged, allowPrivilegeEscalation != false, unrestricted capabilities, restricted volume types, runAsNonRoot != true, seccompProfile
      namespace/kube-system labeled
      Warning: existing pods in namespace "local-path-storage" violate the new PodSecurity enforce level "restricted:latest"
      Warning: local-path-provisioner-d6d9f7ffc-lw9lh: allowPrivilegeEscalation != false, unrestricted capabilities, runAsNonRoot != true, seccompProfile
      namespace/local-path-storage labeled
      

从前面的输出中,你会注意到应用 privileged Pod 安全标准不会显示任何名字空间的警告。 然而,baselinerestricted 标准都有警告,特别是在 kube-system 名字空间中。

设置模式、版本和标准

在本节中,你将以下 Pod 安全标准应用于最新(latest)版本:

  • enforce 模式下的 baseline 标准。
  • warnaudit 模式下的 restricted 标准。

baseline Pod 安全标准提供了一个方便的中间立场,能够保持豁免列表简短并防止已知的特权升级。

此外,为了防止 kube-system 中的 Pod 失败,你将免除该名字空间应用 Pod 安全标准。

在你自己的环境中实施 Pod 安全准入时,请考虑以下事项:

  1. 根据应用于集群的风险状况,更严格的 Pod 安全标准(如 restricted)可能是更好的选择。

  2. kube-system 名字空间进行赦免会允许 Pod 在其中以 privileged 模式运行。 对于实际使用,Kubernetes 项目强烈建议你应用严格的 RBAC 策略来限制对 kube-system 的访问, 遵循最小特权原则。

  3. 创建一个配置文件,Pod 安全准入控制器可以使用该文件来实现这些 Pod 安全标准:

    mkdir -p /tmp/pss
    cat <<EOF > /tmp/pss/cluster-level-pss.yaml 
    apiVersion: apiserver.config.k8s.io/v1
    kind: AdmissionConfiguration
    plugins:
    - name: PodSecurity
      configuration:
        apiVersion: pod-security.admission.config.k8s.io/v1
        kind: PodSecurityConfiguration
        defaults:
          enforce: "baseline"
          enforce-version: "latest"
          audit: "restricted"
          audit-version: "latest"
          warn: "restricted"
          warn-version: "latest"
        exemptions:
          usernames: []
          runtimeClasses: []
          namespaces: [kube-system]
    EOF
    
  1. 在创建集群时配置 API 服务器使用此文件:

    cat <<EOF > /tmp/pss/cluster-config.yaml 
    kind: Cluster
    apiVersion: kind.x-k8s.io/v1alpha4
    nodes:
    - role: control-plane
      kubeadmConfigPatches:
      - |
        kind: ClusterConfiguration
        apiServer:
            extraArgs:
              admission-control-config-file: /etc/config/cluster-level-pss.yaml
            extraVolumes:
              - name: accf
                hostPath: /etc/config
                mountPath: /etc/config
                readOnly: false
                pathType: "DirectoryOrCreate"
      extraMounts:
      - hostPath: /tmp/pss
        containerPath: /etc/config
        # optional: if set, the mount is read-only.
        # default false
        readOnly: false
        # optional: if set, the mount needs SELinux relabeling.
        # default false
        selinuxRelabel: false
        # optional: set propagation mode (None, HostToContainer or Bidirectional)
        # see https://kubernetes.io/docs/concepts/storage/volumes/#mount-propagation
        # default None
        propagation: None
    EOF
    
  1. 创建一个使用 Pod 安全准入的集群来应用这些 Pod 安全标准:

    kind create cluster --name psa-with-cluster-pss --image kindest/node:v1.24.0 --config /tmp/pss/cluster-config.yaml
    

    输出类似于:

    Creating cluster "psa-with-cluster-pss" ...
     ✓ Ensuring node image (kindest/node:v1.24.0) 🖼 
     ✓ Preparing nodes 📦  
     ✓ Writing configuration 📜 
     ✓ Starting control-plane 🕹️ 
     ✓ Installing CNI 🔌 
     ✓ Installing StorageClass 💾 
    Set kubectl context to "kind-psa-with-cluster-pss"
    You can now use your cluster with:
    
    kubectl cluster-info --context kind-psa-with-cluster-pss
    
    Have a question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community 🙂
    
  1. 将 kubectl 指向集群

    kubectl cluster-info --context kind-psa-with-cluster-pss
    

    输出类似于:

    Kubernetes control plane is running at https://127.0.0.1:63855
    CoreDNS is running at https://127.0.0.1:63855/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
    
    To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
    
  1. 创建以下 Pod 规约作为在 default 名字空间中的一个最小配置:

    cat <<EOF > /tmp/pss/nginx-pod.yaml
    apiVersion: v1
    kind: Pod
    metadata:
      name: nginx
    spec:
      containers:
        - image: nginx
          name: nginx
          ports:
            - containerPort: 80
    EOF
    
  1. 在集群中创建 Pod:

    kubectl apply -f /tmp/pss/nginx-pod.yaml
    

    输出类似于:

    Warning: would violate PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "nginx" must set securityContext allowPrivilegeEscalation=false), unrestricted capabilities (container "nginx" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "nginx" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "nginx" must set securityContext seccompProfile.type to "RuntimeDefault" or "Localhost")
    pod/nginx created
    

清理

现在通过运行以下命令删除你上面创建的集群:

kind delete cluster --name psa-with-cluster-pss
kind delete cluster --name psa-wo-cluster-pss

接下来

  • 运行一个 shell 脚本 一次执行前面的所有步骤:
    1. 创建一个基于 Pod 安全标准的集群级别配置
    2. 创建一个文件让 API 服务器消费这个配置
    3. 创建一个集群,用这个配置创建一个 API 服务器
    4. 设置 kubectl 上下文为这个新集群
    5. 创建一个最小的 Pod yaml 文件
    6. 应用这个文件,在新集群中创建一个 Pod
  • Pod 安全准入
  • Pod 安全标准
  • 在名字空间级别应用 Pod 安全标准

2 - 在名字空间级别应用 Pod 安全标准

Pod 安全准入(PSA)在 v1.23 及更高版本默认启用, 因为它升级到测试版(beta)。 Pod 安全准入是在创建 Pod 时应用 Pod 安全标准的准入控制器。 在本教程中,你将应用 baseline Pod 安全标准,每次一个名字空间。

你还可以在集群级别一次将 Pod 安全标准应用于多个名称空间。 有关说明,请参阅在集群级别应用 Pod 安全标准

准备开始

在你的工作站中安装以下内容:

创建集群

  1. 按照如下方式创建一个 KinD 集群:

    kind create cluster --name psa-ns-level --image kindest/node:v1.23.0
    

    输出类似于:

    Creating cluster "psa-ns-level" ...
     ✓ Ensuring node image (kindest/node:v1.23.0) 🖼 
     ✓ Preparing nodes 📦  
     ✓ Writing configuration 📜 
     ✓ Starting control-plane 🕹️ 
     ✓ Installing CNI 🔌 
     ✓ Installing StorageClass 💾 
    Set kubectl context to "kind-psa-ns-level"
    You can now use your cluster with:
    
    kubectl cluster-info --context kind-psa-ns-level
    
    Not sure what to do next? 😅  Check out https://kind.sigs.k8s.io/docs/user/quick-start/
    
  1. 将 kubectl 上下文设置为新集群:

    kubectl cluster-info --context kind-psa-ns-level
    

    输出类似于:

    Kubernetes control plane is running at https://127.0.0.1:50996
    CoreDNS is running at https://127.0.0.1:50996/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
    
    To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
    

创建名字空间

创建一个名为 example 的新名字空间:

kubectl create ns example

输出类似于:

namespace/example created

应用 Pod 安全标准

  1. 使用内置 Pod 安全准入所支持的标签在此名字空间上启用 Pod 安全标准。 在这一步中,我们将根据最新版本(默认值)对基线 Pod 安全标准发出警告。

    kubectl label --overwrite ns example \
      pod-security.kubernetes.io/warn=baseline \
      pod-security.kubernetes.io/warn-version=latest
    
  1. 可以使用标签在任何名字空间上启用多个 Pod 安全标准。 以下命令将强制(enforce) 执行基线(baseline)Pod 安全标准, 但根据最新版本(默认值)对受限(restricted)Pod 安全标准执行警告(warn)和审核(audit)。

    kubectl label --overwrite ns example \
      pod-security.kubernetes.io/enforce=baseline \
      pod-security.kubernetes.io/enforce-version=latest \
      pod-security.kubernetes.io/warn=restricted \
      pod-security.kubernetes.io/warn-version=latest \
      pod-security.kubernetes.io/audit=restricted \
      pod-security.kubernetes.io/audit-version=latest
    

验证 Pod 安全标准

  1. example 名字空间中创建一个最小的 Pod:

    cat <<EOF > /tmp/pss/nginx-pod.yaml
    apiVersion: v1
    kind: Pod
    metadata:
      name: nginx
    spec:
      containers:
        - image: nginx
          name: nginx
          ports:
            - containerPort: 80
    EOF
    
  1. 将 Pod 规约应用到集群中的 example 名字空间中:

    kubectl apply -n example -f /tmp/pss/nginx-pod.yaml
    

    输出类似于:

    Warning: would violate PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "nginx" must set securityContext allowPrivilegeEscalation=false), unrestricted capabilities (container "nginx" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "nginx" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "nginx" must set securityContext seccompProfile.type to "RuntimeDefault" or "Localhost")
    pod/nginx created
    
  1. 将 Pod 规约应用到集群中的 default 名字空间中:

    kubectl apply -n default -f /tmp/pss/nginx-pod.yaml
    

    输出类似于:

    pod/nginx created
    

以上 Pod 安全标准仅被应用到 example 名字空间。 你可以在没有警告的情况下在 default 名字空间中创建相同的 Pod。

清理

现在通过运行以下命令删除你上面创建的集群:

kind delete cluster --name psa-ns-level

接下来

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

特性状态: Kubernetes v1.4 [beta]

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 版本:

    kubectl get nodes -o=jsonpath=$'{range .items[*]}{@.metadata.name}: {@.status.nodeInfo.kubeletVersion}\n{end}'
    
    gke-test-default-pool-239f5d02-gyn2: v1.4.0
    gke-test-default-pool-239f5d02-x1kf: v1.4.0
    gke-test-default-pool-239f5d02-xwux: v1.4.0
    
  1. AppArmor 内核模块已启用 —— 要使 Linux 内核强制执行 AppArmor 配置文件, 必须安装并且启动 AppArmor 内核模块。默认情况下,有几个发行版支持该模块, 如 Ubuntu 和 SUSE,还有许多发行版提供可选支持。要检查模块是否已启用,请检查 /sys/module/apparmor/parameters/enabled 文件:

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

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

  1. 容器运行时支持 AppArmor —— 目前所有常见的 Kubernetes 支持的容器运行时都应该支持 AppArmor, 像 DockerCRI-Ocontainerd。 请参考相应的运行时文档并验证集群是否满足使用 AppArmor 的要求。
  1. 配置文件已加载 —— 通过指定每个容器都应使用的 AppArmor 配置文件, AppArmor 会被应用到 Pod 上。如果指定的任何配置文件尚未加载到内核, Kubelet(>= v1.4)将拒绝 Pod。 通过检查 /sys/kernel/security/apparmor/profiles 文件, 可以查看节点加载了哪些配置文件。例如:

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

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

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

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

保护 Pod

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

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

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

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

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

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

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

kubectl get events | grep Created
22s        22s         1         hello-apparmor     Pod       spec.containers{hello}   Normal    Created     {kubelet e2e-test-stclair-node-pool-31nt}   Created container with docker id 269a53b202d3; Security:[seccomp=unconfined apparmor=k8s-apparmor-example-deny-write]

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

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

举例

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

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

#include <tunables/global>

profile k8s-apparmor-example-deny-write flags=(attach_disconnected) {
  #include <abstractions/base>

  file,

  # 拒绝所有文件写入
  deny /** w,
}

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

NODES=(
    # 你的节点的可通过 SSH 访问的域名
    gke-test-default-pool-239f5d02-gyn2.us-central1-a.my-k8s
    gke-test-default-pool-239f5d02-x1kf.us-central1-a.my-k8s
    gke-test-default-pool-239f5d02-xwux.us-central1-a.my-k8s)
for NODE in ${NODES[*]}; do ssh $NODE 'sudo apparmor_parser -q <<EOF
#include <tunables/global>

profile k8s-apparmor-example-deny-write flags=(attach_disconnected) {
  #include <abstractions/base>

  file,

  # Deny all file writes.
  deny /** w,
}
EOF'
done

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

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

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

kubectl get events | grep hello-apparmor
14s        14s         1         hello-apparmor   Pod                                Normal    Scheduled   {default-scheduler }                           Successfully assigned hello-apparmor to gke-test-default-pool-239f5d02-gyn2
14s        14s         1         hello-apparmor   Pod       spec.containers{hello}   Normal    Pulling     {kubelet gke-test-default-pool-239f5d02-gyn2}   pulling image "busybox"
13s        13s         1         hello-apparmor   Pod       spec.containers{hello}   Normal    Pulled      {kubelet gke-test-default-pool-239f5d02-gyn2}   Successfully pulled image "busybox"
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]
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 来验证容器是否实际使用该配置文件运行:

kubectl exec hello-apparmor -- cat /proc/1/attr/current
k8s-apparmor-example-deny-write (enforce)

最后,我们可以看到,如果我们尝试通过写入文件来违反配置文件会发生什么:

kubectl exec hello-apparmor -- touch /tmp/test
touch: /tmp/test: Permission denied
error: error executing remote command: command terminated with non-zero exit code: Error executing in Docker Container: 1

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

kubectl create -f /dev/stdin <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: hello-apparmor-2
  annotations:
    container.apparmor.security.beta.kubernetes.io/hello: localhost/k8s-apparmor-example-allow-write
spec:
  containers:
  - name: hello
    image: busybox:1.28
    command: [ "sh", "-c", "echo 'Hello AppArmor!' && sleep 1h" ]
EOF
pod/hello-apparmor-2 created
kubectl describe pod hello-apparmor-2
Name:          hello-apparmor-2
Namespace:     default
Node:          gke-test-default-pool-239f5d02-x1kf/
Start Time:    Tue, 30 Aug 2016 17:58:56 -0700
Labels:        <none>
Annotations:   container.apparmor.security.beta.kubernetes.io/hello=localhost/k8s-apparmor-example-allow-write
Status:        Pending
Reason:        AppArmor
Message:       Pod Cannot enforce AppArmor: profile "k8s-apparmor-example-allow-write" is not loaded
IP:
Controllers:   <none>
Containers:
  hello:
    Container ID:
    Image:     busybox
    Image ID:
    Port:
    Command:
      sh
      -c
      echo 'Hello AppArmor!' && sleep 1h
    State:              Waiting
      Reason:           Blocked
    Ready:              False
    Restart Count:      0
    Environment:        <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-dnz7v (ro)
Conditions:
  Type          Status
  Initialized   True
  Ready         False
  PodScheduled  True
Volumes:
  default-token-dnz7v:
    Type:    Secret (a volume populated by a Secret)
    SecretName:    default-token-dnz7v
    Optional:   false
QoS Class:      BestEffort
Node-Selectors: <none>
Tolerations:    <none>
Events:
  FirstSeen    LastSeen    Count    From                        SubobjectPath    Type        Reason        Message
  ---------    --------    -----    ----                        -------------    --------    ------        -------
  23s          23s         1        {default-scheduler }                         Normal      Scheduled     Successfully assigned hello-apparmor-2 to e2e-test-stclair-node-pool-t1f5
  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 的 DaemonSet 来确保加载了正确的配置文件。 可以在这里找到实现示例。
  • 在节点初始化时,使用节点初始化脚本(例如 Salt、Ansible 等)或镜像。
  • 通过将配置文件复制到每个节点并通过 SSH 加载它们,如示例

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

禁用 AppArmor

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

--feature-gates=AppArmor=false

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

编写配置文件

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

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

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

API 参考

Pod 注解

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

  • 键名container.apparmor.security.beta.kubernetes.io/<container_name>, 其中 <container_name> 与 Pod 中某容器的名称匹配。 可以为 Pod 中的每个容器指定单独的配置文件。
  • 键值:对配置文件的引用,如下所述

配置文件引用

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

接下来

其他资源:

4 - 使用 seccomp 限制容器的系统调用

特性状态: Kubernetes v1.19 [stable]

Seccomp 代表安全计算(Secure Computing)模式,自 2.6.12 版本以来,一直是 Linux 内核的一个特性。 它可以用来沙箱化进程的权限,限制进程从用户态到内核态的调用。 Kubernetes 能使你自动将加载到 节点上的 seccomp 配置文件应用到你的 Pod 和容器。

识别你的工作负载所需要的权限是很困难的。在本篇教程中, 你将了解如何将 seccomp 配置文件加载到本地的 Kubernetes 集群中, 如何将它们应用到 Pod,以及如何开始制作只为容器进程提供必要的权限的配置文件。

教程目标

  • 了解如何在节点上加载 seccomp 配置文件
  • 了解如何将 seccomp 配置文件应用到容器上
  • 观察容器进程对系统调用的审计
  • 观察指定的配置文件缺失时的行为
  • 观察违反 seccomp 配置文件的行为
  • 了解如何创建细粒度的 seccomp 配置文件
  • 了解如何应用容器运行时所默认的 seccomp 配置文件

准备开始

为了完成本篇教程中的所有步骤,你必须安装 kindkubectl

本篇教程演示的某些示例仍然是 Beta 状态(自 v1.25 起),另一些示例则仅使用 seccomp 正式发布的功能。 你应该确保,针对你使用的版本, 正确配置了集群。

本篇教程也使用了 curl 工具来下载示例到你的计算机上。 你可以使用其他自己偏好的工具来自适应这些步骤。

下载示例 seccomp 配置文件

这些配置文件的内容将在稍后进行分析, 现在先将它们下载到名为 profiles/ 的目录中,以便将它们加载到集群中。

{
    "defaultAction": "SCMP_ACT_LOG"
}

{
    "defaultAction": "SCMP_ACT_ERRNO"
}

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
    ],
    "syscalls": [
        {
            "names": [
                "accept4",
                "epoll_wait",
                "pselect6",
                "futex",
                "madvise",
                "epoll_ctl",
                "getsockname",
                "setsockopt",
                "vfork",
                "mmap",
                "read",
                "write",
                "close",
                "arch_prctl",
                "sched_getaffinity",
                "munmap",
                "brk",
                "rt_sigaction",
                "rt_sigprocmask",
                "sigaltstack",
                "gettid",
                "clone",
                "bind",
                "socket",
                "openat",
                "readlinkat",
                "exit_group",
                "epoll_create1",
                "listen",
                "rt_sigreturn",
                "sched_yield",
                "clock_gettime",
                "connect",
                "dup2",
                "epoll_pwait",
                "execve",
                "exit",
                "fcntl",
                "getpid",
                "getuid",
                "ioctl",
                "mprotect",
                "nanosleep",
                "open",
                "poll",
                "recvfrom",
                "sendto",
                "set_tid_address",
                "setitimer",
                "writev"
            ],
            "action": "SCMP_ACT_ALLOW"
        }
    ]
}

执行这些命令:

mkdir ./profiles
curl -L -o profiles/audit.json https://k8s.io/examples/pods/security/seccomp/profiles/audit.json
curl -L -o profiles/violation.json https://k8s.io/examples/pods/security/seccomp/profiles/violation.json
curl -L -o profiles/fine-grained.json https://k8s.io/examples/pods/security/seccomp/profiles/fine-grained.json
ls profiles

你应该看到在最后一步的末尾列出有三个配置文件:

audit.json  fine-grained.json  violation.json

使用 kind 创建本地 Kubernetes 集群

为简单起见,kind 可用来创建加载了 seccomp 配置文件的单节点集群。 Kind 在 Docker 中运行 Kubernetes,因此集群的每个节点都是一个容器。 这允许将文件挂载到每个容器的文件系统中,类似于将文件加载到节点上。

apiVersion: kind.x-k8s.io/v1alpha4
kind: Cluster
nodes:
- role: control-plane
  extraMounts:
  - hostPath: "./profiles"
    containerPath: "/var/lib/kubelet/seccomp/profiles"

下载该示例 kind 配置,并将其保存到名为 kind.yaml 的文件中:

curl -L -O https://k8s.io/examples/pods/security/seccomp/kind.yaml

你可以通过设置节点的容器镜像来设置特定的 Kubernetes 版本。 有关此类配置的更多信息, 参阅 kind 文档中节点小节。 本篇教程假定你正在使用 Kubernetes v1.26。

作为 Beta 特性,你可以将 Kubernetes 配置为使用容器运行时默认首选的配置文件, 而不是回退到 Unconfined。 如果你想尝试,请在继续之前参阅 启用使用 RuntimeDefault 作为所有工作负载的默认 seccomp 配置文件

有了 kind 配置后,使用该配置创建 kind 集群:

kind create cluster --config=kind.yaml

新的 Kubernetes 集群准备就绪后,找出作为单节点集群运行的 Docker 容器:

docker ps

你应该看到输出中名为 kind-control-plane 的容器正在运行。 输出类似于:

CONTAINER ID        IMAGE                  COMMAND                  CREATED             STATUS              PORTS                       NAMES
6a96207fed4b        kindest/node:v1.18.2   "/usr/local/bin/entr…"   27 seconds ago      Up 24 seconds       127.0.0.1:42223->6443/tcp   kind-control-plane

如果观察该容器的文件系统, 你应该会看到 profiles/ 目录已成功加载到 kubelet 的默认 seccomp 路径中。 使用 docker exec 在 Pod 中运行命令:

# 将 6a96207fed4b 更改为你从 “docker ps” 看到的容器 ID
docker exec -it 6a96207fed4b ls /var/lib/kubelet/seccomp/profiles
audit.json  fine-grained.json  violation.json

你已验证这些 seccomp 配置文件可用于在 kind 中运行的 kubelet。

启用使用 RuntimeDefault 作为所有工作负载的默认 seccomp 配置文件

特性状态: Kubernetes v1.25 [beta]

要使用 Seccomp(安全计算模式)配置文件来设定默认值,你必须要在启用 SeccompDefault 特性门控的情况下运行 kubelet (这是默认值)。 你还必须显式地启用每个节点的默认行为,以及相应的 --seccomp-default 命令行标志。两者必须同时启用才能使用该特性。

如果启用,kubelet 将会默认使用 RuntimeDefault seccomp 配置文件, (这一配置文明是由容器运行时定义的),而不是使用 Unconfined(禁用 seccomp)模式。 默认的配置文件旨在提供一组限制性较强且能保留工作负载功能的安全默认值。 不同容器运行时及其不同发布版本之间的默认配置文件可能有所不同, 例如在比较来自 CRI-O 和 containerd 的配置文件时。

与其他工作负载相比,某些工作负载可能需要更少的系统调用限制。 这意味着即使使用 RuntimeDefault 配置文件,它们也可能在运行时失败。 要应对此类故障,你可以:

  • 将工作负载显式运行为 Unconfined
  • 禁用节点的 SeccompDefault 特性。还要确保工作负载被调度到禁用该特性的节点上。
  • 为工作负载创建自定义 seccomp 配置文件。

如果你将此特性引入到类似的生产集群中, Kubernetes 项目建议你在部分节点上启用此特性门控, 然后在整个集群范围内推出更改之前,测试工作负载执行情况。

你可以在相关的 Kubernetes 增强提案(KEP) 中找到可能的升级和降级策略的更详细信息: 默认启用 Seccomp

Kubernetes 1.26 允许你配置 Seccomp 配置文件, 当 Pod 的规约未定义特定的 Seccomp 配置文件时应用该配置文件。 这是一个 Beta 特性,默认启用相应的 SeccompDefault 特性门控。 但是,你仍然需要为要使用它的每个节点启用此默认设置。

如果你正在运行 Kubernetes 1.26 集群并希望启用该特性,请使用 --seccomp-default 命令行参数运行 kubelet, 或通过 kubelet 配置文件启用。

要在 kind 启用特性门控, 请确保 kind 提供所需的最低 Kubernetes 版本, 并在 kind 配置中 启用了 SeccompDefault 特性:

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
featureGates:
  SeccompDefault: true
nodes:
  - role: control-plane
    image: kindest/node:v1.23.0@sha256:49824ab1727c04e56a21a5d8372a402fcd32ea51ac96a2706a12af38934f81ac
    kubeadmConfigPatches:
      - |
        kind: JoinConfiguration
        nodeRegistration:
          kubeletExtraArgs:
            seccomp-default: "true"        
  - role: worker
    image: kindest/node:v1.23.0@sha256:49824ab1727c04e56a21a5d8372a402fcd32ea51ac96a2706a12af38934f81ac
    kubeadmConfigPatches:
      - |
        kind: JoinConfiguration
        nodeRegistration:
          kubeletExtraArgs:
            feature-gates: SeccompDefault=true
            seccomp-default: "true"        

如果集群已就绪,则运行一个 Pod:

kubectl run --rm -it --restart=Never --image=alpine alpine -- sh

现在应该附加了默认的 seccomp 配置文件。 这可以通过使用 docker exec 为 kind 上的容器运行 crictl inspect 来验证:

docker exec -it kind-worker bash -c \
    'crictl inspect $(crictl ps --name=alpine -q) | jq .info.runtimeSpec.linux.seccomp'
{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32"],
  "syscalls": [
    {
      "names": ["..."]
    }
  ]
}

创建使用容器运行时默认 seccomp 配置文件的 Pod

大多数容器运行时都提供了一组合理的、默认被允许或默认被禁止的系统调用。 你可以通过将 Pod 或容器的安全上下文中的 seccomp 类型设置为 RuntimeDefault 来为你的工作负载采用这些默认值。

这是一个 Pod 的清单,它要求其所有容器使用 RuntimeDefault seccomp 配置文件:

apiVersion: v1
kind: Pod
metadata:
  name: default-pod
  labels:
    app: default-pod
spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: test-container
    image: hashicorp/http-echo:0.2.3
    args:
    - "-text=just made some more syscalls!"
    securityContext:
      allowPrivilegeEscalation: false

创建此 Pod:

kubectl apply -f https://k8s.io/examples/pods/security/seccomp/ga/default-pod.yaml
kubectl get pod default-pod

此 Pod 应该显示为已成功启动:

NAME        READY   STATUS    RESTARTS   AGE
default-pod 1/1     Running   0          20s

最后,你看到一切正常之后,请清理:

kubectl delete pod default-pod --wait --now

使用 seccomp 配置文件创建 Pod 以进行系统调用审计

首先,将 audit.json 配置文件应用到新的 Pod 上,该配置文件将记录进程的所有系统调用。

这是该 Pod 的清单:

apiVersion: v1
kind: Pod
metadata:
  name: audit-pod
  labels:
    app: audit-pod
spec:
  securityContext:
    seccompProfile:
      type: Localhost
      localhostProfile: profiles/audit.json
  containers:
  - name: test-container
    image: hashicorp/http-echo:0.2.3
    args:
    - "-text=just made some syscalls!"
    securityContext:
      allowPrivilegeEscalation: false

在集群中创建 Pod:

kubectl apply -f https://k8s.io/examples/pods/security/seccomp/ga/audit-pod.yaml

此配置文件不限制任何系统调用,因此 Pod 应该成功启动。

kubectl get pod/audit-pod
NAME        READY   STATUS    RESTARTS   AGE
audit-pod   1/1     Running   0          30s

为了能够与容器暴露的端点交互, 创建一个 NodePort 类型的 Service, 允许从 kind 控制平面容器内部访问端点。

kubectl expose pod audit-pod --type NodePort --port 5678

检查 Service 在节点上分配的端口。

kubectl get service audit-pod

输出类似于:

NAME        TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
audit-pod   NodePort   10.111.36.142   <none>        5678:32373/TCP   72s

现在,你可以使用 curl 从 kind 控制平面容器内部访问该端点,位于该服务所公开的端口上。 使用 docker exec 在属于该控制平面容器的容器中运行 curl 命令:

# 将 6a96207fed4b 更改为你从 “docker ps” 看到的控制平面容器 ID
docker exec -it 6a96207fed4b curl localhost:32373
just made some syscalls!

你可以看到该进程正在运行,但它实际上进行了哪些系统调用? 因为这个 Pod 在本地集群中运行,你应该能够在 /var/log/syslog 中看到它们。 打开一个新的终端窗口并 tail 来自 http-echo 的调用的输出:

tail -f /var/log/syslog | grep 'http-echo'

你应该已经看到了一些由 http-echo 进行的系统调用的日志, 如果你在控制平面容器中 curl 端点,你会看到更多的写入。

例如:

Jul  6 15:37:40 my-machine kernel: [369128.669452] audit: type=1326 audit(1594067860.484:14536): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=51 compat=0 ip=0x46fe1f code=0x7ffc0000
Jul  6 15:37:40 my-machine kernel: [369128.669453] audit: type=1326 audit(1594067860.484:14537): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=54 compat=0 ip=0x46fdba code=0x7ffc0000
Jul  6 15:37:40 my-machine kernel: [369128.669455] audit: type=1326 audit(1594067860.484:14538): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=202 compat=0 ip=0x455e53 code=0x7ffc0000
Jul  6 15:37:40 my-machine kernel: [369128.669456] audit: type=1326 audit(1594067860.484:14539): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=288 compat=0 ip=0x46fdba code=0x7ffc0000
Jul  6 15:37:40 my-machine kernel: [369128.669517] audit: type=1326 audit(1594067860.484:14540): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=0 compat=0 ip=0x46fd44 code=0x7ffc0000
Jul  6 15:37:40 my-machine kernel: [369128.669519] audit: type=1326 audit(1594067860.484:14541): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=270 compat=0 ip=0x4559b1 code=0x7ffc0000
Jul  6 15:38:40 my-machine kernel: [369188.671648] audit: type=1326 audit(1594067920.488:14559): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=270 compat=0 ip=0x4559b1 code=0x7ffc0000
Jul  6 15:38:40 my-machine kernel: [369188.671726] audit: type=1326 audit(1594067920.488:14560): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=29064 comm="http-echo" exe="/http-echo" sig=0 arch=c000003e syscall=202 compat=0 ip=0x455e53 code=0x7ffc0000

通过查看每一行的 syscall= 条目,你可以开始了解 http-echo 进程所需的系统调用。 虽然这些不太可能包含它使用的所有系统调用,但它可以作为此容器的 seccomp 配置文件的基础。

在转到下一部分之前清理该 Pod 和 Service:

kubectl delete service audit-pod --wait
kubectl delete pod audit-pod --wait --now

使用导致违规的 seccomp 配置文件创建 Pod

出于演示目的,将配置文件应用于不允许任何系统调用的 Pod 上。

此演示的清单是:

apiVersion: v1
kind: Pod
metadata:
  name: violation-pod
  labels:
    app: violation-pod
spec:
  securityContext:
    seccompProfile:
      type: Localhost
      localhostProfile: profiles/violation.json
  containers:
  - name: test-container
    image: hashicorp/http-echo:0.2.3
    args:
    - "-text=just made some syscalls!"
    securityContext:
      allowPrivilegeEscalation: false

尝试在集群中创建 Pod:

kubectl apply -f https://k8s.io/examples/pods/security/seccomp/ga/violation-pod.yaml

Pod 创建,但存在问题。 如果你检查 Pod 状态,你应该看到它没有启动。

kubectl get pod/violation-pod
NAME            READY   STATUS             RESTARTS   AGE
violation-pod   0/1     CrashLoopBackOff   1          6s

如上例所示,http-echo 进程需要相当多的系统调用。 这里 seccomp 已通过设置 "defaultAction": "SCMP_ACT_ERRNO" 被指示为在发生任何系统调用时报错。 这是非常安全的,但消除了做任何有意义的事情的能力。 你真正想要的是只给工作负载它们所需要的权限。

在转到下一部分之前清理该 Pod:

kubectl delete pod violation-pod --wait --now

使用只允许必要的系统调用的 seccomp 配置文件创建 Pod

如果你看一看 fine-grained.json 配置文件, 你会注意到第一个示例的 syslog 中看到的一些系统调用, 其中配置文件设置为 "defaultAction": "SCMP_ACT_LOG"。 现在的配置文件设置 "defaultAction": "SCMP_ACT_ERRNO", 但在 "action": "SCMP_ACT_ALLOW" 块中明确允许一组系统调用。 理想情况下,容器将成功运行,并且你看到没有消息发送到 syslog

此示例的清单是:

apiVersion: v1
kind: Pod
metadata:
  name: fine-pod
  labels:
    app: fine-pod
spec:
  securityContext:
    seccompProfile:
      type: Localhost
      localhostProfile: profiles/fine-grained.json
  containers:
  - name: test-container
    image: hashicorp/http-echo:0.2.3
    args:
    - "-text=just made some syscalls!"
    securityContext:
      allowPrivilegeEscalation: false

在你的集群中创建 Pod:

kubectl apply -f https://k8s.io/examples/pods/security/seccomp/ga/fine-pod.yaml
kubectl get pod fine-pod

此 Pod 应该显示为已成功启动:

NAME        READY   STATUS    RESTARTS   AGE
fine-pod   1/1     Running   0          30s

打开一个新的终端窗口并使用 tail 来监视提到来自 http-echo 的调用的日志条目:

# 你计算机上的日志路径可能与 “/var/log/syslog” 不同
tail -f /var/log/syslog | grep 'http-echo'

接着,使用 NodePort Service 公开 Pod:

kubectl expose pod fine-pod --type NodePort --port 5678

检查节点上的 Service 分配了什么端口:

kubectl get service fine-pod

输出类似于:

NAME        TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
fine-pod    NodePort   10.111.36.142   <none>        5678:32373/TCP   72s

使用 curl 从 kind 控制平面容器内部访问端点:

# 将 6a96207fed4b 更改为你从 “docker ps” 看到的控制平面容器 ID
docker exec -it 6a96207fed4b curl localhost:32373
just made some syscalls!

你应该在 syslog 中看不到任何输出。 这是因为配置文件允许所有必要的系统调用,并指定如果调用列表之外的系统调用应发生错误。 从安全角度来看,这是一种理想的情况,但需要在分析程序时付出一些努力。 如果有一种简单的方法可以在不需要太多努力的情况下更接近这种安全性,那就太好了。

在转到下一部分之前清理该 Pod 和服务:

kubectl delete service fine-pod --wait
kubectl delete pod fine-pod --wait --now

接下来

你可以了解有关 Linux seccomp 的更多信息: