Kubernetes deep dive: pod security policy
Kubernetes: PodSecurityPolicy
Introduction
Au début de Kubernetes, le contrôle sur les pods admis dans le cluster était
quasi inexistant, il n’y avait globalement que deux alternatives : autoriser
ou non les pods privilégiés. Ce mode “privilégié” correspondait au parramètre
--privileged
de Docker.
Les PodSecurityPolicies, que nous appellerons PSP dans la suite de l’article, permettent justement de contrôler finement les specifications d’un pod avant de l’accepter sur le cluster. Elles permettent notamment de contrôler :
- Les types de volumes utilisés
- Le GID du point de montage d’un volume
- Système de fichier en lecture seule
- Les capabilities Kernel autorisées (sysctl, etc)
- Les profiles seccomp/apparmor
- Les Pods privilégiés
- Utilisation des hostPorts
- Les UID/GID utilisés par le Pod
- Les élévations de privilèges ( = sudo )
Alors à quoi cela ressemble ? Ci dessous l’exemple d’une policy privileged
qui
autorise tout, qui est le fonctionnement par défaut d’un cluster sans
PodSecurityPolicy d’activé.
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'
Activer les PodSecurityPolicy
Les PodSecurityPolicy sont, à l’heure où j’écris cet article, en beta. Elles
doivent être activées via un admission
controller.
Suivant votre méthode de déploiement il faudra ajouter à l’API server,
l’admission controller PodSecurityPolicy
et le redémarrer.
En ce qui concerne les services managés, la plupart supportent les PodSecurityPolicy:
- Activées par défaut dans EKS : liste des admission controller
- Activable sur GKE :
gcloud beta container clusters create [CLUSTER_NAME] --enable-pod-security-policy
- Activable sur AKS :
az aks update --resource-group myResourceGroup --name myAKSCluster --enable-pod-security-policy
Une mise en garde cependant, activer les PodSecurityPolicy sur un cluster existant peut potentiellement casser le cluster si les bonnes policies ne sont pas en place mais nous y reviendrons.
Principe de fonctionnement
Le principe de fonctionnement des PSP n’est pas des plus plus intuitif. La
première règle est que si les PSP sont activées sur le cluster, tous les
pods doivent utiliser au moins une policy pour pouvoir être acceptés par l’API
server. Voilà la raison pour laquelle vous pouvez “casser” votre cluster en
activant les PSP sans créer les policies adéquates. L’association entre les PSP
et les pods qui les utilisent est effectuée via les
RBAC. En effet
chaque pod dispose d’un ServiceAccount, et chaque namespace dispose d’un
ServiceAccount default
utilisé par les pods qui ne spécifient pas
explicitement de ServiceAccount. C’est donc ces ServiceAccount qui vont
obtenir le droit d’utiliser les PSP.
Pour associer un ServiceAccount avec une PSP, il faut créer un Role ou ClusterRole pouvant utiliser cette policy et il faut ensuite utiliser un RoleBinding ou un ClusterRoleBinding pour associer le ServiceAccount à ce Role ou ClusterRole.
Exemple avec la PSP privileged
vu plus haut :
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: psp:privileged
rules:
- apiGroups: ['policy']
resources: ['podsecuritypolicies']
verbs: ['use']
resourceNames:
- privileged
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: psp:mySa:privileged
roleRef:
kind: ClusterRole
name: psp:privileged
apiGroup: rbac.authorization.k8s.io
subjects:
# Authorize specific service accounts:
- kind: ServiceAccount
name: mySa
namespace: myNamespace
Il est également possible de specifier tous les ServiceAccounts d’un namespace:
# Authorize all service accounts in a namespace:
- kind: Group
apiGroup: rbac.authorization.k8s.io
name: system:serviceaccounts:myNamespace
Ou encore tous les utilisateurs authentifiés de tous les namespaces:
# Or equivalently, all authenticated users in a namespace:
- kind: Group
apiGroup: rbac.authorization.k8s.io
name: system:authenticated
Précedence
Une pod (via son ServiceAccount) peut donc être autorisé à utiliser plusieurs PSP, mais une seule sera appliquée.
Il faut tout d’abord faire la différence entre deux types de PSP, les mutable policy
et les non mutable policy
. La plupart des PSP ne font que vérifier que
le pod respecte bien les règles (non mutable policy
) mais certains annotations
d’une PSP peuvent permettre d’appliquer des règles seccomp et/ou apparmor au
pod, dans ce cas la policy modifie le pod, c’est donc une mutable policy
.
Par exemple prenons une PSP:
---
apiVersion: extensions/v1beta1
kind: PodSecurityPolicy
metadata:
name: default
annotations:
seccomp.security.alpha.kubernetes.io/allowedProfileNames: 'docker/default'
seccomp.security.alpha.kubernetes.io/defaultProfileName: 'docker/default'
spec:
privileged: false
allowPrivilegeEscalation: false
allowedCapabilities: [] # default set of capabilities are implicitly allowed
volumes:
- 'configMap'
- 'emptyDir'
- 'projected'
- 'secret'
- 'downwardAPI'
- 'persistentVolumeClaim'
hostNetwork: false
hostIPC: false
hostPID: false
runAsUser:
rule: 'RunAsAny'
seLinux:
rule: 'RunAsAny'
supplementalGroups:
rule: 'RunAsAny'
fsGroup:
rule: 'RunAsAny'
et une autre:
---
apiVersion: extensions/v1beta1
kind: PodSecurityPolicy
metadata:
name: default
spec:
privileged: false
allowPrivilegeEscalation: false
allowedCapabilities: [] # default set of capabilities are implicitly allowed
volumes:
- 'configMap'
- 'emptyDir'
- 'projected'
- 'secret'
- 'downwardAPI'
- 'persistentVolumeClaim'
hostNetwork: false
hostIPC: false
hostPID: false
runAsUser:
rule: 'RunAsAny'
seLinux:
rule: 'RunAsAny'
supplementalGroups:
rule: 'RunAsAny'
fsGroup:
rule: 'RunAsAny'
La différence entre les deux est l’annotation présente dans l’une et
inexistante dans l’autre. Si un pod ne spécifie pas de configuration seccomp
dans sa spec, la seconde policy sera matchée, car la première est
une mutable policy
, c’est a dire qu’elle modifie le pod pour y
rajouter les annotations.
Maintenant que cela est clair, le choix de la PSP est fait de la manière
suivante. La priorité est donnée aux non mutable policy
. Si plusieurs non mutable policy
sont matchées, le choix se fait par ordre
alphabétique.
Nous allons maintenant voir comment utiliser ces policy pour sécuriser son cluster Kubernetes avec des policy par défaut.
PSP : Best practices
Lorsque nous déployons des clusters chez nos clients, nous essayons de pousser l’adoption de ces bonnes pratiques dès le départ, en effet il est plus difficile d’activer les PSP sur un cluster existant que de les activer au début d’un projet et de pousser l’adoption.
Nous allons par le suite voir les deux cas, d’une part sur un cluster vierge et d’autre part comment activer les PSP sur un cluster existant sans casser la compatibilité.
PSP par défaut
En général, on distingue deux PSP par défaut sur les clusters :
- une policy
privileged
qui autorise tout. - une policy
default
qui autorise uniquement des fonctionnalités jugées sans risque pour des namespaces/workloads non privilégiés.
Les resources utilisées dans la documentation Kubernetes et dans cet article sont disponibles ici
Sécuriser AWS EKS
Sur EKS, les PSP sont activées par défaut et EKS propose par défaut un mode de
compatibilité en autorisant tous les pods à utiliser une PSP
eks-privileged
.
L’implementation est réalisée de la façon suivante:
Une PSP eks.privileged
:
apiVersion: extensions/v1beta1
kind: PodSecurityPolicy
metadata:
seccomp.security.alpha.kubernetes.io/allowedProfileNames: '*'
labels:
eks.amazonaws.com/component: pod-security-policy
kubernetes.io/cluster-service: "true"
name: eks.privileged
spec:
allowPrivilegeEscalation: true
allowedCapabilities:
- '*'
fsGroup:
rule: RunAsAny
hostIPC: true
hostNetwork: true
hostPID: true
hostPorts:
- max: 65535
min: 0
privileged: true
runAsUser:
rule: RunAsAny
seLinux:
rule: RunAsAny
supplementalGroups:
rule: RunAsAny
volumes:
- '*'
Un ClusterRole eks:podsecuritypolicy:privileged
:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
eks.amazonaws.com/component: pod-security-policy
kubernetes.io/cluster-service: "true"
name: eks:podsecuritypolicy:privileged
rules:
- apiGroups:
- policy
resourceNames:
- eks.privileged
resources:
- podsecuritypolicies
verbs:
- use
Ansi qu’un ClusterRoleBinding:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
annotations:
kubernetes.io/description: Allow all authenticated users to create privileged pods.
labels:
eks.amazonaws.com/component: pod-security-policy
kubernetes.io/cluster-service: "true"
name: eks:podsecuritypolicy:authenticated
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: eks:podsecuritypolicy:privileged
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: Group
name: system:authenticated
Globalement cette combinaison permet à n’importe quel pod d’utiliser n’importe quelle fonctionnalité sans restriction. C’est le comportement par défaut sur EKS.
Nous allons voir maintenant comment restreindre et sécuriser notre cluster. L’objectif est le suivant :
- Permettre aux noeuds ainsi qu’au namespace
kube-system
de fonctionner correctement et de pouvoir utiliser une policyprivileged
. - Permettre à tous les autres namespaces d’utiliser une policy par défaut sécurisée.
- Supprimer la politique par défaut de EKS
Nous allons créer deux PSP.
privileged
:
---
apiVersion: extensions/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'
default
:
---
apiVersion: extensions/v1beta1
kind: PodSecurityPolicy
metadata:
name: default
annotations:
seccomp.security.alpha.kubernetes.io/allowedProfileNames: 'docker/default'
seccomp.security.alpha.kubernetes.io/defaultProfileName: 'docker/default'
spec:
privileged: false
allowPrivilegeEscalation: false
allowedCapabilities: [] # default set of capabilities are implicitly allowed
volumes:
- 'configMap'
- 'emptyDir'
- 'projected'
- 'secret'
- 'downwardAPI'
- 'persistentVolumeClaim'
hostNetwork: false
hostIPC: false
hostPID: false
runAsUser:
rule: 'RunAsAny'
seLinux:
rule: 'RunAsAny'
supplementalGroups:
rule: 'RunAsAny'
fsGroup:
rule: 'RunAsAny'
Ensuite nous allons autoriser les composants internes de Kubernetes ainsi que le
namespace kube-system
à utiliser la policy privileged
.
Tout d’abord un ClusterRole qui match la policy privileged
:
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: privileged-psp
rules:
- apiGroups: ['policy']
resources: ['podsecuritypolicies']
verbs: ['use']
resourceNames: ['privileged']
- apiGroups: ['extensions']
resources: ['podsecuritypolicies']
verbs: ['use']
resourceNames: ['privileged']
Puis un RoleBinding dans le namespace kube-system
pour autoriser
l’utilisation de cette policy :
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: privileged-psp-nodes
namespace: kube-system
roleRef:
kind: ClusterRole
name: privileged-psp
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: Group
apiGroup: rbac.authorization.k8s.io
name: system:nodes
- kind: User
apiGroup: rbac.authorization.k8s.io
name: kubelet # Legacy node ID
- kind: Group
apiGroup: rbac.authorization.k8s.io
name: system:serviceaccounts:kube-system
Ces objets permettent de remplir la condition 1
.
Maintenant nous allons permettre à tous les autres ServiceAccount de tous les
namespaces d’utiliser la policy default
.
ClusterRole :
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: default-psp
rules:
- apiGroups: ['policy']
resources: ['podsecuritypolicies']
verbs: ['use']
resourceNames: ['default']
- apiGroups: ['extensions']
resources: ['podsecuritypolicies']
verbs: ['use']
resourceNames: ['default']
ClusterRoleBinding :
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: default-psp
roleRef:
kind: ClusterRole
name: default-psp
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: Group
name: system:authenticated
apiGroup: rbac.authorization.k8s.io
Avec ces objets, nous remplissons maintenant la conditions 2
.
Nous pouvons maintenant supprimer les resources présentes par défaut sur EKS :
kubectl --kubeconfig kubeconfig delete psp eks.privileged
kubectl --kubeconfig kubeconfig delete clusterrolebinding eks:podsecuritypolicy:authenticated
kubectl --kubeconfig kubeconfig delete clusterrole eks:podsecuritypolicy:privileged
Nous pouvons maintenant verifier le bon fonctionnement. Par exemple avec un
kubectl -n kube-system get pods coredns-7ddddf5cc7-nqf4p -o yaml
sur
un des pods CoreDNS, on remarque la psp utilisée est eks.privileged
.
En effet nous avons supprimé la policy par défaut mais
les services deja présents à ce moment là après la création du cluster
l’utilise encore.
apiVersion: v1
kind: Pod
metadata:
annotations:
eks.amazonaws.com/compute-type: ec2
kubernetes.io/psp: eks.privileged
...
Nous allons supprimer tous les pods du namespace
kube-system
: kubectl -n kube-system delete pods --all
. Une fois les pods
redémarrés, vous pouvez verifier que la nouvelle PSP est bien utilisée :
$ kubectl -n kube-system get pods coredns-7ddddf5cc7-nqf4p -o yaml
apiVersion: v1
kind: Pod
metadata:
annotations:
eks.amazonaws.com/compute-type: ec2
kubernetes.io/psp: privileged
N’importe quel pod créé dans le namespace kube-system
pourra fonctionner de
manière privilégiée.
Nous allons maintenant tester le fonctionnement d’une workload classique.
En utilisant par exemple le namespace default
et le cas d’un utilisateur
lambda potentiellement non trusté.
Lançons un deployment nginx
:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
On remarque les pods nginx se lancent sans problème, en effet le Deployment n’a besoin d’aucun privilège particulier.
Essayons maintenant de rajouter un volume de l’host par exemple :
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
volumeMounts:
- name: etc
mountPath: /etc
volumes:
- name: etc
hostPath:
path: /etc
On remarque que rien ne se passe. Pas de rolling update, pas d’erreur, rien. En effet, pour verifier le comportement des PSP il faut descendre au niveau des ReplicaSet (gérés par les Deployment).
Si l’on regarde coté ReplicaSet, la rolling update a bien été demandée :
NAME DESIRED CURRENT READY AGE
nginx-deployment-5c99864ddf 1 0 0 2m5s
nginx-deployment-7bfb85948d 3 3 3 4m58s
Regardons de plus prêt le ReplicaSet bloqué :
$ kubectl describe rs nginx-deployment-5c99864ddf
Name: nginx-deployment-5c99864ddf
Namespace: default
Selector: app=nginx,pod-template-hash=5c99864ddf
Labels: app=nginx
pod-template-hash=5c99864ddf
Annotations: deployment.kubernetes.io/desired-replicas: 3
deployment.kubernetes.io/max-replicas: 4
deployment.kubernetes.io/revision: 5
Controlled By: Deployment/nginx-deployment
Replicas: 0 current / 1 desired
Pods Status: 0 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
Labels: app=nginx
pod-template-hash=5c99864ddf
Containers:
nginx:
Image: nginx
Port: 80/TCP
Host Port: 0/TCP
Environment: <none>
Mounts:
/mount from etc (rw)
Volumes:
etc:
Type: HostPath (bare host directory volume)
Path: /etc
HostPathType:
Conditions:
Type Status Reason
---- ------ ------
ReplicaFailure True FailedCreate
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedCreate 52s (x15 over 2m14s) replicaset-controller Error creating: pods "nginx-deployment-5c99864ddf-" is forbidden: unable to validate against any pod security policy: [spec.volumes[0]: Invalid value: "hostPath": hostPath volumes are not allowed to be used]
Le pod ne peut pas être créé car il requière un volume de l’host, ce qui n’est
pas permis par le PSP default
.
Activation des PSP sur un cluster générique kubeadm
Pour d’autres clusters génériques tels que des clusters déployés avec Kubeadm, la demarche présentée pour EKS reste correcte. Dans le cas d’un cluster où les PSP ne sont pas activées mais avec des workloads existants, prenez garde à bien respecter les étapes dans l’ordre :
- Création des PSP et ressources associées pour
default
etprivileged
- Activation des PSP dans l’API server.
En effet, il n’y a en général pas de PSP par défaut, et activer les PSP sans les préparer en amont reviendrai à empêcher tous les pods du cluster de démarrer faute de matcher une policy existante.
Pour activer les PSP sur l’API server, si vous utilisez un ficher de configuration Kubeadm, vous pouvez rajouter les champs suivants :
apiServer:
extraArgs:
enable-admission-plugins: NodeRestriction,PodSecurityPolicy
Vous pouvez ensuite kubeadm upgrade
votre cluster. Si vous utilisez une
méthode de déploiement autre, il faut rajouter le flag
--enable-admission-plugins=NodeRestriction,PodSecurityPolicy
à l’API server.
Conclusion
Je vous invite à jouer avec la policy par défaut afin de découvrir les possibilités offerte par les PSP. L’important étant de trouver un bon compromis entre les politiques internes de build des images Docker et les prérequis de sécurité afin d’une part de sécuriser vos clusters Kubernetes tout en communiquant aux développeurs les best practices de build d’images Docker et les critères d’acceptation des images et applications sur vos clusters.