この記事はKubernetes3 Advent Calendar 2020 20日目の記事です。
マルチテナントなKubernetesクラスターを作成する際に活きてくるKubernetesのセキュリティ機能の一つにPod Security Policy(PSP)
という機能があります。
https://kubernetes.io/docs/concepts/policy/pod-security-policy
具体的な設定に関するプラクティスについては過去記事をみていただけると幸いです。
マルチテナントクラスターにおけるPod Security Policyの個人的ベストプラクティス
https://qiita.com/jlandowner/items/b17562f7f1c7045d99fc
今回はPodSecurityPolicyについて、個人的にドキュメントだけではよく理解できなかった以下の点をソースから確認してみたいと思います。
- Admission Controllerなのに、なぜ実行時にrootユーザーでの実行拒否ができるのか?
- PodのServiceAccountとPodを作成したユーザーのPSP、どう選択されるのか?
検証環境の準備
検証環境はEKS v1.18を使用します。
まずは特権PSPと制限付きPSPを用意します。PSPでは許可ポリシーが優先されるため、特権PSPは特に何にも付与せず、クラスター全体に制限付きPSPを適用します。
特権PSPはEKSにデフォルトで存在するeks.privileged
、制限付きPSPはEKS Best PracticeのRecommendationのrestricted
を使用します。
事前準備として、krew(krewのインストール手順はこちら)からkubectlプラグインであるkubectxとpsp-utilをインストールします。
kubectl krew install ctx
kubectl krew install psp-util
一般ユーザーの作成
EKSではクラスターを作成した際のIAMロールがsystem:master
グループに所属されますので、一般ユーザーを改めて作成します。
具体的な設定は長いので折り畳んでおきます。
新たにIAMロールを作成し、EKSのグループに所属させます。詳細は公式の手順に譲ります。
設定結果は以下の通りです。
# aws-auth configMap
$ kubectl describe cm aws-auth -n kube-system
Name: aws-auth
Namespace: kube-system
Labels: <none>
Annotations: ...
Data
====
mapRoles:
----
- rolearn: arn:aws:iam::XXXXXXXXXXXXX:role/eksctl-test-eks-1-nodegroup-NodeG-NodeInstanceRole-XXXXXXXXXXXXX
username: system:node:{{EC2PrivateDNSName}}
groups:
- system:bootstrappers
- system:nodes
- rolearn: arn:aws:iam::XXXXXXXXXXXXX:role/eks-user-role
username: user-role
groups:
- user-role
# AWS CLIのConfig
$ cat ~/.aws/config
[default]
region = ap-northeast-1
output = json
[profile user-role]
region = ap-northeast-1
output = json
role_arn = arn:aws:iam::XXXXXXXXXXXXX:role/eks-user-role
source_profile = default
# Kubeconfig
$ cat ~/.kube/config
・・・省略
- name: arn:aws:eks:ap-northeast-1:XXXXXXXXXXXXX:cluster/test-eks-1-user-role
user:
exec:
apiVersion: client.authentication.k8s.io/v1alpha1
args:
- --region
- ap-northeast-1
- eks
- get-token
- --cluster-name
- land-EKS-1
- --profile # user-roleのプロファイルを使用する
- user-role # user-roleのプロファイルを使用する
command: aws
特権ユーザーと作成した一般ユーザーを切り替えられるようにcontextを2つ用意しておきます。
$ kubectl ctx
arn:aws:eks:ap-northeast-1:XXXXXXXXXXXX:cluster/test-eks-1 # 特権
arn:aws:eks:ap-northeast-1:XXXXXXXXXXXX:cluster/test-eks-1-user-role # 一般
PSPの設定
具体的な設定は長いので折り畳んでおきます。
# クラスター全体に特権PSPが適用されているため外す
kubectl delete ClusterRoleBinding eks:podsecuritypolicy:authenticated
# 制限付きPSPの適用
cat <<EOF | kubectl apply -f -
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'
seccomp.security.alpha.kubernetes.io/defaultProfileName: 'runtime/default'
apparmor.security.beta.kubernetes.io/defaultProfileName: 'runtime/default'
spec:
privileged: false
# Required to prevent escalations to root.
allowPrivilegeEscalation: false
# This is redundant with non-root + disallow privilege escalation,
# but we can provide it for defense in depth.
requiredDropCapabilities:
- ALL
# Allow core volume types.
volumes:
- 'configMap'
- 'emptyDir'
- 'projected'
- 'secret'
- 'downwardAPI'
# Assume that persistentVolumes set up by the cluster admin are safe to use.
- '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
EOF
# クラスター全体に制限付きPSPを適用
kubectl psp-util attach restricted --group system:authenticated
kubectl psp-util attach restricted --group system:unauthenticated
設定結果は以下の通りです。
$ kubectl psp-util tree
📙 PSP eks.privileged
└── 📕 ClusterRole eks:podsecuritypolicy:privileged
📙 PSP restricted
└── 📕 ClusterRole psp-util.restricted
└── 📘 ClusterRoleBinding psp-util.restricted
└── 📗 Subject{Kind: Group, Name: system:authenticated, Namespace:}
└── 📗 Subject{Kind: Group, Name: system:unauthenticated, Namespace: }
1. Admission Controllerなのに、なぜ実行時にrootユーザーでの実行拒否ができるのか?
Pod Security PolicyはControl PlaneにAdmission Controllerとして実装されており、Podリソースの作成時にPodリソースの設定内容がPodSecurityPolicyリソースで定義したポリシーを満たしているかを確認し、バリデーションエラーの場合は作成を拒否します。
ただしこれだけではOpenPolicyAgentや自前で実装するValidation Webhookでも実現できるものです。PodSecurityPolicyの強力な機能の一つに、コンテナ実行時にポリシーを満たしていなければ実行をブロックする機能があります。これはどのように実装されているのでしょうか?
試してみる
試しに一般ユーザーでprivileged: true
なPodを作成してみます。
$ kubectl ctx arn:aws:eks:ap-northeast-1:XXXXXXXXXXXX:cluster/test-eks-1-user-role
Switched to context "arn:aws:eks:ap-northeast-1:XXXXXXXXXXXX:cluster/land-EKS-1-user-role".
$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: test-nginx
spec:
containers:
- name: nginx
image: nginx:alpine
securityContext:
privileged: true
EOF
Error from server (Forbidden): error when creating "STDIN": pods "test-nginx" is forbidden: unable to validate against any pod security policy: [spec.containers[0].securityContext.privileged: Invalid value: true: Privileged containers are not allowed]
エラーとなりPodの作成ができませんでした。次にprivileged: true
を外して起動してみます。
$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: test-nginx
spec:
containers:
- name: nginx
image: nginx:alpine
EOF
pod/test-nginx created
作成ができたので、ステータスを確認します。
$ kubectl get po test-nginx
NAME READY STATUS RESTARTS AGE
test-nginx 0/1 CreateContainerConfigError 0 18s
$ kubectl describe po test-nginx
・・・省略
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 81s default-scheduler Successfully assigned default/test-nginx to ip-10-110-100-199.ap-northeast-1.compute.internal
Normal Pulled 14s (x8 over 80s) kubelet, ip-10-110-100-199.ap-northeast-1.compute.internal Container image "nginx:alpine" already present on machine
Warning Failed 14s (x8 over 80s) kubelet, ip-10-110-100-199.ap-northeast-1.compute.internal Error: container has runAsNonRoot and image will run as root
Podの作成は成功しているものの、ステータスはCreateContainerConfigError
となり、Error: container has runAsNonRoot and image will run as root
として実行できません。
ソースをみてみる
Pod Security PolicyはAdmission controllerとして実装されており、APIサーバーの起動オプションで有効にするか無効にするか制御できます。
https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#podsecuritypolicy
Admission controllerの実装はplugin
ディレクトリにあります。
https://github.com/kubernetes/kubernetes/blob/master/plugin/pkg/admission/security/podsecuritypolicy
実際のバリデーションロジックはpkg/srcurity/podsecuritypolicy
にありますが、先の例のようにprivileged: true
のような設定内容は、Admission Controller内でPod適用時にバリデートされます。
if !s.psp.Spec.Privileged && privileged != nil && *privileged {
allErrs = append(allErrs, field.Invalid(scPath.Child("privileged"), *privileged, "Privileged containers are not allowed"))
}
ではrootユーザーでの実行拒否はどのように実装されているかというと、Admission ControllerでチェックされたPSPの情報をkubeletに連携し、kubeletが実行時にチェックしています。
root実行拒否を例にとって確認します。
Admission controllerでは、先に示したバリデーションだけでなく、他のコンポーネントにシグナルを送るためにPodのSecurityContextをミューテートし、情報を付与します。Root実行拒否については、PSPの設定を元にsecurityContextにRunAsNonRoot
フラグを設定します。
// if we're using the non-root strategy set the marker that this container should not be
// run as root which will signal to the kubelet to do a final check either on the runAsUser
// or, if runAsUser is not set, the image UID will be checked.
if sc.RunAsNonRoot() == nil && sc.RunAsUser() == nil && s.psp.Spec.RunAsUser.Rule == policy.RunAsUserStrategyMustRunAsNonRoot {
nonRoot := true
sc.SetRunAsNonRoot(&nonRoot)
}
先ほど実行時エラーになったPodをyamlで確認するとsecurityContextに以下のような情報が付与されています。
$ kubectl get pod test-nginx -o yaml
・・・省略
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
runAsNonRoot: true
・・・
kubelet側の実装では、startContainer
メソッド内で呼ばれるgenerateContainerConfig
メソッドにてこのフラグをチェックし、実行するコンテナのUIDを判定しています。
// Verify RunAsNonRoot. Non-root verification only supports numeric user.
if err := verifyRunAsNonRoot(pod, container, uid, username); err != nil {
return nil, cleanupAction, err
}
そのため、実行時にCreateContainerConfigError
となるのですね。
2. PodのServiceAccountとPodを作成したユーザーのPSP、どう選択されるのか?
Pod Security Policyを適用する際は、PodSecurityPolicyを使用(use)するClusterRoleを作成し、ClusterRoleBindingまたはRoleBindingで紐付けします。
紐づけられる対象は通常のClusterRoleBindingと同様にUser、GroupまたはServiceAccountです。
ここで注意が必要なのは、通常Deployment等でPodを作成しようとすると、実際にPodを作成するのはDeploymentを作成したユーザーではなく、Control Plane内のReplicaset Controller
がPodを作成します。
公式ドキュメントにはDeploymentやDaemonSetが作成したPodにPSPを適用するために、Podが使用するServiceAccountにPSP付与することで適用する必要がある、とあります。
Most Kubernetes pods are not created directly by users. Instead, they are typically created indirectly as part of a Deployment, ReplicaSet, or other templated controller via the controller manager. Granting the controller access to the policy would grant access for all pods created by that controller, so the preferred method for authorizing policies is to grant access to the pod's service account (see example).
https://kubernetes.io/docs/concepts/policy/pod-security-policy/#authorizing-policies
試してみる
先ほどrootユーザーで実行時エラーとなったPodをDeploymentで作成してみます。
$ cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-nginx
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: test-nginx
template:
metadata:
labels:
app: test-nginx
spec:
containers:
- image: nginx:alpine
name: nginx
EOF
deployment.apps/test-nginx created
$ kubectl get po
NAME READY STATUS RESTARTS AGE
pod/test-nginx-65c4cf9ff5-pf2tk 0/1 CreateContainerConfigError 0 4s
実行時エラーになりました。SerivceAccountは指定していないので、default
になっています。
$ kubectl get pod/test-nginx-65c4cf9ff5-pf2tk -o jsonpath='{.spec.serviceAccount}'
default
次にdefault
ServiceAccountに特権PSPを付与してみます。
$ kubectl psp-util attach eks.privileged --sa default -n default
Managed ClusterRole is not found...Created
Managed ClusterRoleBinding is not found...Created
$ kubectl psp-util tree
📙 PSP eks.privileged
└── 📕 ClusterRole eks:podsecuritypolicy:privileged
└── 📕 ClusterRole psp-util.eks.privileged
└── 📘 ClusterRoleBinding psp-util.eks.privileged
└── 📗 Subject{Kind: ServiceAccount, Name: default, Namespace: default}
📙 PSP restricted
└── 📕 ClusterRole psp-util.restricted
└── 📘 ClusterRoleBinding psp-util.restricted
└── 📗 Subject{Kind: Group, Name: system:authenticated, Namespace: }
└── 📗 Subject{Kind: Group, Name: system:unauthenticated, Namespace: }
先ほどDeploymentから作成されたPodを削除して、新しいPodが作られるか確認してみます。
$ kubectl delete pod/test-nginx-65c4cf9ff5-pf2tk
pod "test-nginx-65c4cf9ff5-pf2tk" deleted
$ kubectl get po
NAME READY STATUS RESTARTS AGE
test-nginx-65c4cf9ff5-nfs2h 1/1 Running 0 14s
新しくできたPodは無事作成されていました。
次にdefault
ServiceAccountに付与したPSPの適用解除し、今度はkube-system内のreplicaset-controller
というServiceAccountにPSPを付与してみます。
$ kubectl psp-util detach eks.privileged --sa default -n default
$ kubectl psp-util attach eks.privileged --sa replicaset-controller -n kube-system
$ kubectl psp-util tree
📙 PSP eks.privileged
└── 📕 ClusterRole eks:podsecuritypolicy:privileged
└── 📕 ClusterRole psp-util.eks.privileged
└── 📘 ClusterRoleBinding psp-util.eks.privileged
└── 📗 Subject{Kind: ServiceAccount, Name: replicaset-controller, Namespace: kube-system}
📙 PSP restricted
└── 📕 ClusterRole psp-util.restricted
└── 📘 ClusterRoleBinding psp-util.restricted
└── 📗 Subject{Kind: Group, Name: system:authenticated, Namespace: }
└── 📗 Subject{Kind: Group, Name: system:unauthenticated, Namespace: }
それではこの状態でもう一度Podを削除してみます。
$ kubectl delete pod test-nginx-65c4cf9ff5-nfs2h
pod "test-nginx-65c4cf9ff5-nfs2h" deleted
$ kubectl get po
NAME READY STATUS RESTARTS AGE
test-nginx-65c4cf9ff5-6s5vb 1/1 Running 0 28s
起動しました。つまりServiceAccountかPodの作成ユーザーのどちらかにでもrootコンテナを作れる権限があれば実行できるようです。
反対にマルチテナントクラスターで各サービスチームにrootコンテナで動かせないようにしたい場合は、各サービスチームが使用できるServiceAccountに権限を付与しないのはもちろんのことですが、Replicaset controller
はクラスター内の全Podを作成するため、これらが使用するkube-system内のServiceAccountに付与してしまうと、DeploymentでどんなPodでも作成できてしまいます。
ソースをみてみる
Admission Controllerの実装は非常にシンプルです。使用するPSPを選ぶ条件として、単純にServiceAccountと作成Userの両方をOR条件で確認しているだけです。
func isAuthorizedForPolicy(ctx context.Context, user, sa user.Info, namespace, policyName string, authz authorizer.Authorizer) bool {
// Check the service account first, as that is the more common use case.
return authorizedForPolicy(ctx, sa, namespace, policyName, authz) ||
authorizedForPolicy(ctx, user, namespace, policyName, authz)
}
注意点
マルチテナントクラスターでは、サービスチームにNamespaceを割り当てることが多いかと思いますので、system:serviceaccounts:<Namespace名>
グループにPSPを割り当てるとNamespace内の全ServiceAccountに同じPod Security Policyを適用することができます。
しかし、kube-systemについてはReplicaset controller
やDaemonSet controller
が使用するServiceAccountがあるので、system:serviceaccounts:kube-system
グループを使用すると、DeploymentやDaemonSetでどんなPodでも作成できてしまいます。
そのためkube-systemではCNIコンテナやkube-proxyが使用するServiceAccountに個別でPSPを適用する必要があります。ただこれを忘れてCNIコンテナに特権PSPが付与されていないとCNIコンテナが起動せず、NodeステータスがReadyにならないのでご注意を。