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-amd64-newbuildroot.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és 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