为 Kubernetes 集群启用 Pod 安全策略

最近有客户反馈在开启了安全策略的集群中部署产品失败,因此研究了一下 Kubernetes 提供的 pod 安全策略。

文中的演示和示例均在 v1.18.17 集群中通过验证。

Pod Security Policies

Pod Security Policies (下文简称 psp 或 pod 安全策略)是一种集群级别的全局资源,能够对 pod 的创建和更新进行细粒度的授权控制。具体来说,一个 psp 对象定义了一组安全性条件,一个 pod 的 spec 字段必须满足这些条件以及适用相关字段的默认值,其创建或更新请求才会被 apiserver 所接受。

具体的 pod 字段和安全条件可见文档 what-is-a-pod-security-policy

启用 Pod Security Policies

Kubernetes 默认不开启 pod 安全策略功能,在集群中启用 pod 安全策略的步骤大体上分为三步:

  1. 在集群中创建指定的安全策略资源。
  2. 通过 RBAC 机制授予创建 pod 的 user 或者被创建 pod 的 service account 使用安全策略资源的权限,通常会将使用权限授予一组 users 或 service accounts。
  3. 启用 apiserver 的 admission-controller 插件。

注意步骤 1、2 可以单独执行,因为它们不会对集群产生实际影响,但需要确保步骤 3 在前两步之后执行。

因为一旦启用 admission-controller 插件,apiserver 会对所有的 pod 创建/更新请求强制执行安全策略检查,如果集群中没有可用的 pod 安全策略资源或者未对安全策略资源预先授权,所有的 pod 创建/更新请求都会被拒绝。包括 kube-system 命名空间下的系统管理组件如 apiserver 本身(由于 apiserver 是受 kubelet 管理的静态 pod,实际上容器依然会运行)。

启用的整体流程如下示意图:

podsecuritypolicy

创建安全策略资源

  1. 在集群中创建一个宽松限制的 PodSecurityPolicy 资源,命名为 privileged

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
    apiVersion: policy/v1beta1
    kind: PodSecurityPolicy
    metadata:
      name: privileged
      annotations:
        seccomp.security.alpha.kubernetes.io/allowedProfileNames: '*'
    spec:
      privileged: true
      allowPrivilegeEscalation: true
      allowedCapabilities:
      - '*'
      volumes:
      - '*'
      hostNetwork: true
      hostPorts:
      - min: 0
        max: 65535
      hostIPC: true
      hostPID: true
      runAsUser:
        rule: 'RunAsAny'
      seLinux:
        rule: 'RunAsAny'
      supplementalGroups:
        rule: 'RunAsAny'
      fsGroup:
        rule: 'RunAsAny'
    
  2. 在集群中创建一个严格限制的 PodSecurityPolicy 资源,命名为 restricted

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    
    apiVersion: policy/v1beta1
    kind: PodSecurityPolicy
    metadata:
      name: restricted
      annotations:
        seccomp.security.alpha.kubernetes.io/allowedProfileNames: 'docker/default,runtime/default'
        apparmor.security.beta.kubernetes.io/allowedProfileNames: 'runtime/default'
        apparmor.security.beta.kubernetes.io/defaultProfileName:  'runtime/default'
    spec:
      privileged: false
      # Required to prevent escalations to root.
      allowPrivilegeEscalation: false
      requiredDropCapabilities:
        - ALL
      # Allow core volume types.
      volumes:
        - 'configMap'
        - 'emptyDir'
        - 'projected'
        - 'secret'
        - 'downwardAPI'
        # Assume that ephemeral CSI drivers & persistentVolumes set up by the cluster admin are safe to use.
        - 'csi'
        - 'persistentVolumeClaim'
      hostNetwork: false
      hostIPC: false
      hostPID: false
      runAsUser:
        # Require the container to run without root privileges.
        rule: 'MustRunAsNonRoot'
      seLinux:
        # This policy assumes the nodes are using AppArmor rather than SELinux.
        rule: 'RunAsAny'
      supplementalGroups:
        rule: 'MustRunAs'
        ranges:
          # Forbid adding the root group.
          - min: 1
            max: 65535
      fsGroup:
        rule: 'MustRunAs'
        ranges:
          # Forbid adding the root group.
          - min: 1
            max: 65535
      readOnlyRootFilesystem: false
    

RBAC 身份认证

  1. 分别创建可访问两种安全策略资源的 ClusterRole:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
      name: privileged-psp
    rules:
    - apiGroups: ['policy']
      resources: ['podsecuritypolicies']
      verbs:     ['use']
      resourceNames:
      - privileged
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
      name: restricted-psp
    rules:
    - apiGroups: ['policy']
      resources: ['podsecuritypolicies']
      verbs:     ['use']
      resourceNames:
      - restricted
    
  2. 通过 ClusterRoleBinding (或者 RoleBinding)将创建的 ClusterRole 绑定到指定命名空间下的所有 service account(也可以授权给指定的 user)。

    在 Kubernetes 中大多数 pod 并不是直接使用 user 创建的,而是通常作为 Deployment、ReplicaSet 或其他模板 controller 的子资源,由 controller 间接创建。授予 controller 用户对安全策略的使用权等同于为该 controller 创建的所有 pod 授予使用权,因此授权的推荐做法是授权给目标 pod 的 service account。

    为了进行后续的测试,我们将 privileged-psp 授权给 kubelet 所使用的 system:nodes 用户和 privileged-ns 命名空间下的所有 service account,将 restricted-psp 授权给 restricted-ns 命名空间下的所有 service account:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
      name: privileged-psp-bind
    roleRef:
      kind: ClusterRole
      name: privileged-psp
      apiGroup: rbac.authorization.k8s.io
    subjects:
    # 授权给指定命名空间下的所有 service account(推荐做法):
    - kind: Group
      apiGroup: rbac.authorization.k8s.io
      name: system:serviceaccounts:privileged-ns
    - kind: Group
      apiGroup: rbac.authorization.k8s.io
      name: system:nodes
      namespace: kube-system
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
      name: restricted-psp-bind
    roleRef:
      kind: ClusterRole
      name: restricted-psp
      apiGroup: rbac.authorization.k8s.io
    subjects:
    - kind: Group
      apiGroup: rbac.authorization.k8s.io
      name: system:serviceaccounts:restricted-ns
    

    subjects 字段下添加更多记录还可以授权给所有的 service account 或者所有已授权的 user:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    subjects:
    # 授权给指定的 service account 或者用户(不推荐):
    - kind: ServiceAccount
    name: <authorized service account name>
    namespace: <authorized pod namespace>
    - kind: User
    apiGroup: rbac.authorization.k8s.io
    name: <authorized user name>
    # 授权给所有的 service accounts:
    - kind: Group
    apiGroup: rbac.authorization.k8s.io
    name: system:serviceaccounts
    # 授权给所有已认证的用户:
    - kind: Group
    apiGroup: rbac.authorization.k8s.io
    name: system:authenticated
    

启用 admission controller 插件

在 apiserver 启用 admission controller 的 psp 插件有两种方式:

  1. 在已存在的集群中通过修改 apiserver 的静态 manifest 文件,为 apiserver 增加启动参数 enable-admission-plugins=PodSecurityPolicy。kubelet 会自动检测到变更并重启 apiserver。下面的示例使用 sed 对原有参数进行了替换:

    1
    
    sed -i 's/enable-admission-plugins=NodeRestriction/enable-admission-plugins=NodeRestriction,PodSecurityPolicy/' /etc/kubernetes/manifests/kube-apiserver.yaml
    
  2. 或者在初始化集群时,在 kubeadm 配置文件中添加额外参数(不推荐,默认会拒绝所有 pod 的创建)。

    1
    2
    3
    4
    5
    
    apiVersion: kubeadm.k8s.io/v1beta2
    kind: ClusterConfiguration
    apiServer:
    	extraArgs:
        enable-admission-plugins: "PodSecurityPolicy"
    

验证 psp 的安全限制

接下分别在上文授权过的 privileged-nsrestricted-ns 命名空间进行测试,验证 psp 对 pod 请求的限制。

首先尝试在 restricted-ns 命名空间通过 deployment 创建一个需要使用 hostNetwork 的 pod:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-hostnetwork
spec:
  selector:
    matchLabels:
      run: nginx
  template:
    metadata:
      labels:
        run: nginx
    spec:
      hostNetwork: true
      containers:
      - image: nginx
        imagePullPolicy: Always
        name: nginx-privileged

创建并查看结果:

1
2
3
4
5
6
7
$ kubectl create -f hostnetwork-pod.yaml -n restricted-ns
deployment.apps/nginx-hostnetwork created
$ kubectl get deploy -n restricted-ns nginx-hostnetwork
NAME                READY   UP-TO-DATE   AVAILABLE   AGE
nginx-hostnetwork   0/1     1            0           21s
$ kubectl -n restricted-ns get event | grep "pod security policy"
103s        Warning   FailedCreate             deployment/nginx-hostnetwork                                Error creating: pods "nginx-hostnetwork-" is forbidden: unable to validate against any pod security policy: [spec.securityContext.hostNetwork: Invalid value: true: Host network is not allowed to be used]

由于授权给该命名空间 service account 的安全策略资源禁止 pod 使用 hostNetwork,因此该 deployment 创建 pod 的请求被拒绝。

接着在 privileged-ns 命名空间执行相同的操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ kubectl create -f deploy.yaml -n privileged-ns
deployment.apps/nginx-hostnetwork created
$ kubectl get deploy -n privileged-ns nginx-hostnetwork
NAME                READY   UP-TO-DATE   AVAILABLE   AGE
nginx-hostnetwork   1/1     1            1           34s
$ kubectl get po -n privileged-ns
NAME                                 READY   STATUS   RESTARTS   AGE
nginx-hostnetwork-644cdd6598-twds9   0/1     Error    3          77s
$ kubectl get pod nginx-hostnetwork-644cdd6598-twds9 -o jsonpath='{.metadata.annotations}' -n privileged-ns
map[kubernetes.io/psp:privileged]

授权给该命名空间 service account 的安全策略资源允许 pod 使用 hostNetwork,因此 pod 成功被创建。我们可以通过 pod 的 metadata.annotations 字段检查其适用的安全策略资源。

Pod Security Admission

从 Kubernetes v1.21开始,Pod Security Policy 将被弃用,并将在 v1.25 中删除,Kubernetes 在 1.22 版本引入了 Pod Security Admission 作为其替代者。

为什么要替换 psp

KEP-2579 详细阐述了引入 Pod Security Admission 替代 Pod Security Policy 的三点主要理由:

  1. 将策略与用户或 service account 绑定的模型削弱了安全性。
  2. 功能无法流畅切换,在没有安全策略资源的情况下无法关闭检查。
  3. API 不一致且缺乏灵活性。

新的 Pod Security Admission 机制在易用性和灵活性上都有了很大提升,从使用角度有以下四点显著不同:

  1. 可以在集群中默认开启,只要不设置约束条件就不会触发对 pod 的校验。
  2. 只在命名空间级别生效,可以为不同命名空间通过添加标签的方式设置不同的安全限制。
  3. 可以为特定的用户、命名空间或者运行时设置豁免规则。
  4. 根据实践预设了三种安全等级,不需要由用户单独去设置每一项安全条件。

工作方式

Pod Security Admission 将原来 Pod Security Policy 的安全条件划分成三种预设的安全等级:

  • privileged: 不受限,向 pod 提供所有可用的权限。
  • baseline:最低限度的限制策略,防止已知的特权升级。
  • restricted:严格限制策略,遵循当前 Pod 加固的最佳实践。

三种等级从宽松到严格递增,各自包含了不同限度的安全条件,适用于不同的 pod 工作场景。此外还可以将安全等级设置为固定的 Kubernetes 版本,这样即使集群升级到了新的版本且新版本的安全等级定义发生变化,依然可以按旧版本的安全条件对 pod 进行检验。

当 pod 与安全等级冲突时,我们可通过三种模式来选择不同的处理方式:

  • enforce:只允许符合安全等级要求的 pod,拒绝与安全等级冲突的 pod。
  • audit:只将安全等级冲突记录在集群 event 中,不会拒绝 pod。
  • warn:与安全等级冲突时会向用户返回一个警告信息,但不会拒绝 pod。

audit 和 warn 模式是独立的,如果同时需要两者的功能必须分别设置两种模式。

应用安全策略不再需要创建单独的集群资源,只需在启用 Pod Security Admission 后为命名空间设置如下控制标签:

1
2
pod-security.kubernetes.io/<mode>: <level>
pod-security.kubernetes.io/<mode>-version: <version>

在旧版本集群中启用 psa

虽然 Pod Security Admission 是一个在 Kubernetes v1.22 引入的功能,但旧版本可以通过安装 PodSecurity admission webhook 来启用该功能,具体步骤如下:

1
2
3
4
git clone https://github.com/kubernetes/pod-security-admission.git
cd pod-security-admission/webhook
make certs
kubectl apply -k .

以上来自官方文档的步骤在 v1.18.17 集群中执行时会有两个兼容性问题,具体问题和解决方案如下:

  1. kubectl 内置的 kustomize 版本不支持 "replacements" 字段:

    $ kubectl apply -k .
    error: json: unknown field "replacements"
    

    解决方案:安装最新版本的 kusomize 然后在同一目录执行

    1
    
    $ kustomize build . | kubectl apply -f -
    
  2. manifest/50-deployment.yaml 文件中定义的 Deployment.spec.template.spec.containers[0].securityContext 字段在 v1.19 版本才开始引入,因此 v1.18 需要将该字段修改为对应的 annotation 版本,详见 Seccomp

    error: error validating "STDIN": error validating data: ValidationError(Deployment.spec.template.spec.containers[0].securityContext): unknown field "seccompProfile" in io.k8s.api.core.v1.SecurityContext; if you choose to ignore these errors, turn validation off with --validate=false
    

验证 psa 的安全限制

首先创建一个新的命名空间 psa-test 用于测试,并将其定义强制应用 baseline 安全等级,并对 restricted 等级进行警告和审计:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
apiVersion: v1
kind: Namespace
metadata:
  name: psa-test
  labels:
    pod-security.kubernetes.io/enforce: baseline
    pod-security.kubernetes.io/enforce-version: v1.18

    # We are setting these to our _desired_ `enforce` level.
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: v1.18
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: v1.18

接着在该命名空间中创建上文示例中用过的 deployment:

1
2
3
4
5
6
7
$ kubectl create -f hostnetwork-pod.yaml -n psa-test
deployment.apps/nginx-hostnetwork created
$ kubectl get deploy -n psa-test nginx-hostnetwork
NAME                READY   UP-TO-DATE   AVAILABLE   AGE
nginx-hostnetwork   0/1     0            0           17s
$ kubectl -n psa-test get event | grep PodSecurity
104s        Warning   FailedCreate        replicaset/nginx-hostnetwork-644cdd6598   Error creating: admission webhook "pod-security-webhook.kubernetes.io" denied the request: pods "nginx-hostnetwork-644cdd6598-7rb5m" is forbidden: violates PodSecurity "baseline:v1.23": host namespaces (hostNetwork=true)

与 psp 的示例相比,psa 实现了基本一致的安全检查结果,但易用程度有了很大提升。

参考链接

updatedupdated2023-06-062023-06-06