Kubernetes et sécurité
Objectifs
- Découvrir comment configurer Kubernetes pour garantir certaines propriétés de sécurité
Kubernetes avec minikube
Suivez la section Kubernetes avec minikube pour obtenir un cluster avec minikube. Si vous enchainez depuis le TD Kubernetes, supprimez votre machine minikube pour repartir d’un cluster vide:
$ minikube delete
$ minikube start --iso-url=file://$(pwd)/minikube-v1.34.0-amd64.iso
Obliger les conteneurs à tourner avec des permissions restreintes
Pour séparer logiquement les déploiements dans notre cluster nous allons utiliser le concept de namespace dans Kubernetes. Attention, ces namespaces sont un concept distinct des namespaces du noyau Linux qui sont utilisés pour créer les conteneurs.
Nous allons utiliser les Pod Security Standards pour contraindre les conteneurs d’un namespace Kubernetes à tourner avec des permissions restreintes (non root
, pas d’élévations de privilèges, etc.).
Pour cela, nous allons créer un namespace et lui ajouter un label pour appliquer une politique restrictive. Pour avoir plus de contrôle sur les paramètres utilisés pour créer les objets Kubernetes, nous allons utiliser des descriptions au format YAML que nous allons passer à kubectl
via l’entrée standard (stdin ou -
) :
$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Namespace
metadata:
name: insacvl
labels:
pod-security.kubernetes.io/enforce: restricted
EOF
Vous pouvez retrouver l’intégralité des options possibles dans les pages de référence de la documentation Kubernetes (exemple pour les namespaces).
Vérifier que le namespace a bien été créé :
$ kubectl describe namespaces insacvl
Essayons maintenant de créer un déploiement qui contient un conteneur tournant en root
par défaut dans ce namespace :
$ kubectl apply --namespace insacvl -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: template-nginx
template:
metadata:
labels:
app: template-nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
EOF
Noter que nous obtenons un warning mais que Kubernetes accepte la création du déploiement :
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")
deployment.apps/nginx-deployment created
Notre déploiement est créé mais n’est pas prêt et nous n’avons pas de pods :
$ kubectl get deployments --namespace insacvl
$ kubectl get pods --namespace insacvl
Question 1 : Retrouver la raison de l’échec du déploiement :
$ kubectl get deployments --namespace insacvl -o json
Nous allons donc mettre à jour notre déploiement pour appliquer les contraintes demandées :
$ kubectl apply --namespace insacvl -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: template-nginx
template:
metadata:
labels:
app: template-nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
seccompProfile:
type: "RuntimeDefault"
ports:
- containerPort: 80
EOF
Le status de notre déploiement est désormais “en cours” (Progressing) :
$ kubectl get deployments --namespace insacvl -o json
Mais notre pod n’est toujours pas démarré :
$ kubectl get pods --namespace insacvl
Question 2 : Retrouver la raison de l’échec de la création du pod :
$ kubectl get pods --namespace insacvl nginx-deployment-86d8f64d7-tw8lm -o json
Nous allons donc à nouveau mettre à jour notre déploiement pour appliquer les contraintes demandées :
$ kubectl apply --namespace insacvl -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: template-nginx
template:
metadata:
labels:
app: template-nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
seccompProfile:
type: "RuntimeDefault"
ports:
ports:
- containerPort: 8080
EOF
Kubernetes essaie désormais de créer le conteneur mais celui-ci ne fonctionne pas (CrashLoopBackOff) parce qu’il n’est pas prévu pour s’éxécuter sous un utilisateur non-privilégié :
$ kubectl get pods --namespace insacvl
$ kubectl get pods --namespace insacvl nginx-deployment-68dccfb96c-6mwmf -o json
Remplacer l’image du conteneur par une image conçue pour tourner sous un utilisateur non-privilégié :
$ kubectl apply --namespace insacvl -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: template-nginx
template:
metadata:
labels:
app: template-nginx
spec:
containers:
- name: nginx
image: quay.io/fedora/nginx-124:latest
command: ["nginx", "-g", "daemon off;"]
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
seccompProfile:
type: "RuntimeDefault"
ports:
ports:
- containerPort: 80
EOF
Il n’est plus nécessaire de préciser un utilisateur puisque l’image tourne sous un utilisateur non root
par défaut. Voir la documentation pour plus de détails.
Notre conteneur fonctionne désormais et notre déploiement est prêt :
$ kubectl get pods --namespace insacvl
$ kubectl get deployments --namespace insacvl
Installation de Kubewarden
Nous allons utiliser le projet Kubewarden pour mettre en place des politiques de sécurité sur notre cluster Kubernetes de façon automatique et plus avancées que les politiques inclues par défaut dans Kubernetes.
La première étape pour installer Kubewarden est d’installer certmanager, un gestionnaire de certificats X.509 pour Kubernetes et OpenShift :
$ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.3/cert-manager.yaml
$ kubectl wait --for=condition=Available deployment --timeout=2m -n cert-manager --all
$ kubectl get pods --namespace cert-manager
Ensuite, nous allons installer Helm, qui est un “gestionaire de paquets” pour Kubernetes :
$ curl -LO https://get.helm.sh/helm-v3.14.0-linux-amd64.tar.gz
$ echo "f43e1c3387de24547506ab05d24e5309c0ce0b228c23bd8aa64e9ec4b8206651 helm-v3.14.0-linux-amd64.tar.gz" | sha256sum --check
$ tar xf helm-v3.14.0-linux-amd64.tar.gz
$ sudo install -o root -g root -m 0755 linux-amd64/helm /usr/local/bin/helm
Et nous pouvons ensuite poursuivre l’installation de Kuberwarden :
$ helm repo add kubewarden https://charts.kubewarden.io
$ helm repo update kubewarden
$ helm install --wait -n kubewarden --create-namespace kubewarden-crds kubewarden/kubewarden-crds
$ helm install --wait -n kubewarden kubewarden-controller kubewarden/kubewarden-controller
$ helm install --wait -n kubewarden kubewarden-defaults kubewarden/kubewarden-defaults
Obliger tous les namespaces à avoir les annotations de sécurité
Nous allons désormais appliquer une politique de sécurité sur notre cluster à l’aide de Kuberwarden pour automatiser la mise en place de ces contraintes de sécurité sur les namespaces nouvellement créés ou mis à jour.
Nous allons créer une ClusterAdmissionPolicy qui s’appliquera à tout le cluster (sauf certains namespaces explicitement exclus).
Cette politique (psa-label-enforcer) obligera tous les namespaces à avoir le label pod-security.kubernetes.io/enforce: restricted
pour appliquer les restrictions de sécurité aux conteneurs :
$ kubectl apply -f - <<EOF
apiVersion: policies.kubewarden.io/v1
kind: ClusterAdmissionPolicy
metadata:
name: pod-security-admission-enforcer-default-mode
spec:
module: registry://ghcr.io/kubewarden/policies/psa-label-enforcer:v0.1.1
rules:
- apiGroups: [""]
apiVersions: ["v1"]
resources: ["namespaces"]
operations:
- CREATE
- UPDATE
mutating: true
namespaceSelector:
matchExpressions:
- key: "kubernetes.io/metadata.name"
operator: NotIn
values: ["kube-system", "kube-public", "kube-node-lease"]
settings:
modes:
enforce: "restricted"
EOF
Une fois la politique crée, nous devons attendre qu’elle soit déployée sur le cluster :
$ kubectl get clusteradmissionpolicies.policies.kubewarden.io pod-security-admission-enforcer-default-mode
$ kubectl get clusteradmissionpolicies.policies.kubewarden.io pod-security-admission-enforcer-default-mode -o json
Pour vérifier que cette politique fonctionne comme prévu, nous allons créer un namespace et y créer des déploiements avec des conteneurs qui tournent en root
par défaut comme dans la section précédente :
$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Namespace
metadata:
name: insacvl-kubewarden
EOF
Noter que nous n’avons pas spécifié le label pod-security.kubernetes.io/enforce: restricted
mais qu’il a été automatiquement ajouté par Kuberwarden à notre namespace à la création :
$ kubectl get namespace insacvl-kubewarden -o yaml
Essayons maintenant de deployer un conteneur que l’on fait tourner en root
:
$ kubectl apply --namespace insacvl-kubewarden -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: template-nginx
template:
metadata:
labels:
app: template-nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
seccompProfile:
type: "RuntimeDefault"
ports:
ports:
- containerPort: 80
EOF
Nous pouvons alors constater que le pods n’est pas démarré :
$ kubectl get deployments --namespace insacvl-kubewarden
$ kubectl get pods --namespace insacvl-kubewarden
$ kubectl get pods --namespace insacvl-kubewarden nginx-deployment-86d8f64d7-stzpm -o json
Mettons à jour notre déploiement pour utiliser une image durcie :
$ kubectl apply --namespace insacvl-kubewarden -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: template-nginx
template:
metadata:
labels:
app: template-nginx
spec:
containers:
- name: nginx
image: quay.io/fedora/nginx-124:latest
command: ["nginx", "-g", "daemon off;"]
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
seccompProfile:
type: "RuntimeDefault"
ports:
ports:
- containerPort: 8080
EOF
Vérifier que le conteneur est désormais fonctionnel :
$ kubectl get deployments --namespace insacvl-kubewarden
$ kubectl get pods --namespace insacvl-kubewarden
$ kubectl get pods --namespace insacvl-kubewarden nginx-deployment-86d8f64d7-stzpm -o json
Autoriser uniquement les conteneurs signés
Note : Si vous n’avez pas encore réalisé le TD CI/CD avec GitHub et signatures avec cosign, commencez par celui-ci avant de poursuivre ici.
Nous allons ensuite appliquer une politique de sécurité sur notre cluster à l’aide de Kuberwarden pour valider les signatures des images de conteneurs qui serons utilisées pour lancer les conteneurs.
Nous allons créer une autre ClusterAdmissionPolicy qui s’appliquera à tout le cluster (sauf certains namespaces explicitement exclus).
Cette politique (verify-image-signatures) obligera les images de conteneur qui matchent l’expression quay.io/travier/*
à avoir une signature cosign valide :
$ kubectl apply -f - <<EOF
apiVersion: policies.kubewarden.io/v1
kind: ClusterAdmissionPolicy
metadata:
name: verify-image-signatures
spec:
module: registry://ghcr.io/kubewarden/policies/verify-image-signatures:v0.1.7
rules:
- apiGroups: ["", "apps", "batch"]
apiVersions: ["v1"]
resources: ["pods", "deployments", "statefulsets", "replicationcontrollers", "jobs", "cronjobs"]
operations:
- CREATE
- UPDATE
mutating: true
namespaceSelector:
matchExpressions:
- key: "kubernetes.io/metadata.name"
operator: NotIn
values: ["kube-system", "kube-public", "kube-node-lease"]
settings:
signatures:
- image: "quay.io/travier/*"
pubKeys:
- |
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEviYlbu3VB0KQ1h97SDbQqndpM04q
Yi/UcUSxoB4ho8gfnw0b61REAtgleIDExQ3+PqEZoXB+nQ+NeAbSDwkHYA==
-----END PUBLIC KEY-----
EOF
Attendre que la politique soit déployée sur le cluster :
$ kubectl get clusteradmissionpolicies.policies.kubewarden.io verify-image-signatures
$ kubectl get clusteradmissionpolicies.policies.kubewarden.io verify-image-signatures -o json
Créer un nouveau namespace:
$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Namespace
metadata:
name: insacvl-cosign
EOF
Créer un déploiement qui utilise une image de conteneur non signée :
$ kubectl apply --namespace insacvl-cosign -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: image-signature-test
labels:
app: cosign-test
spec:
replicas: 1
selector:
matchLabels:
app: template-cosign-test
template:
metadata:
labels:
app: template-cosign-test
spec:
containers:
- name: cosign
image: quay.io/travier/cosign-example:unsigned
command: ["sleep", "10000"]
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
seccompProfile:
type: "RuntimeDefault"
EOF
Vous devriez obtrenir une erreur :
Error from server: error when creating "STDIN": admission webhook "clusterwide-verify-image-signatures.kubewarden.admission" denied the request: Resource image-signature-test is not accepted: verification of image quay.io/travier/cosign-example:unsigned failed: Host error: Callback evaluation failure: no signatures found for image: quay.io/travier/cosign-example:unsigned
Aucun déploiement ou pod n’a été créé :
$ kubectl get deployments --namespace insacvl-cosign
$ kubectl get pods --namespace insacvl-cosign
Modifier notre déploiement pour utiliser une image de conteneur signée :
$ kubectl apply --namespace insacvl-cosign -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: image-signature-test
labels:
app: cosign-test
spec:
replicas: 1
selector:
matchLabels:
app: template-cosign-test
template:
metadata:
labels:
app: template-cosign-test
spec:
containers:
- name: cosign
image: quay.io/travier/cosign-example:latest-cosign
command: ["sleep", "10000"]
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
seccompProfile:
type: "RuntimeDefault"
EOF
Vérifier que notre conteneur est bien lancé :
$ kubectl get deployments --namespace insacvl-cosign
$ kubectl get pods --namespace insacvl-cosign
Lancer une autre image de conteneur :
$ kubectl apply --namespace insacvl-cosign -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: template-nginx
template:
metadata:
labels:
app: template-nginx
spec:
containers:
- name: nginx
image: quay.io/fedora/nginx-124:latest
command: ["nginx", "-g", "daemon off;"]
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
capabilities:
drop:
- "ALL"
seccompProfile:
type: "RuntimeDefault"
ports:
ports:
- containerPort: 8080
EOF
Question 3 : Que constatez vous ?
Question 4 : Comment pourrions nous modifier la politique pour corriger ce problème ?
Modifiez ensuite le déploiement et la configuration de la politique pour utiliser votre propre image de conteneur signée que vous avez créée dans le TD 4.3.
Références
- Kubernetes :
- Kubewarden :
- CRI-O :
- Images de conteneurs durcies : Projet Software Collections
- Exemples d’images de conteneurs signées : github.com/travier/cosign-test