Appliquer les bonnes pratiques de déploiement au sein de votre cluster Kubernetes avec Kyverno

Appliquer les bonnes pratiques de déploiement au sein de votre cluster Kubernetes avec Kyverno

Découverte de l'outil

J'ai eu l'occasion lors de la KubeCon Valence de 2022 de rencontrer l'équipe de l'entreprise Nirmata en charge de l'outil Kyverno.

C'était un outil que je connaissais déjà sans jamais l'avoir mis en place. J'ai donc voulu en savoir plus, et, tester Kyverno pour une utilisation personnelle et vous permettre de faire la même chose à travers cet article.

Kyverno est un projet open source au niveau incubating côté CNCF, il permet de déployer des stratégies (Policies) visant, entre autres, à valider les déploiements au sein de clusters Kubernetes et sécuriser les ressources en appliquant les bonnes pratiques.

Par exemple, il peut empêcher la création de ressources sans label, de Pod possédant une image sans tag, etc.

De plus, il dispose d'une large communauté de contributeurs à travers l'ensemble des dépôts de code sur Github.

Dans la suite de cet article, je vais vous présenter Kyverno, l'installer et vous faire découvrir quelques fonctionnalités.

Pourquoi choisir Kyverno ?

Lors de la certification CKS, j'avais eu l'occasion de manipuler les Pod Security Policies et l'outil open source Open Policy Agent (OPA).

Pour ce qui concerne le premier élément, l'objet PodSecurityPolicy a été déprécié depuis la version 1.21 de Kubernetes et a été totalement supprimé depuis la version 1.25. Celui-ci permettait de fixer des restrictions au niveau des Pod comme le fait d'éviter d'accorder des privilèges trop élevés à un conteneur. Enfin, son successeur le Pod Security Admission est encore trop récent.

Ensuite, pour OPA, cet outil permet, comme Kyverno, de créer des stratégies au sein d'un cluster Kubernetes en mode Policy as Code (PaC).

L'Open Policy Agent a l'avantage d'être une solution généraliste qui ne se limite pas uniquement à Kubernetes, même si une solution spécifique existe comme Gatekeeper.

Là où j'ai trouvé OPA moins pratique c'est à travers le langage Rego, qui est un langage très particulier et pas forcément simple à appréhender. C'est la différence majeure avec Kyverno qui se base sur des fichiers YAML en adoptant la forme d'objets natifs à Kubernetes car il a été pensé uniquement pour fonctionner sur cet orchestrateur.

rego: |
package k8srequiredlabels

violation[{"msg": msg, "details": {"missing_labels": missing}}] {
    provided := {label | input.review.object.metadata.labels[label]}
    required := {label | label := input.parameters.labels[_]}
    missing := required - provided
    count(missing) > 0
    msg := sprintf("you must provide labels: %v", [missing])
}

Voici un exemple de Rego pour valider l'utilisation de labels. Pas si simple...

En conclusion, je trouve que Kyverno est donc plus accessible pour une personne habituée à manipuler des objets Kubernetes sans avoir à apprendre un nouveau langage.

Architecture et fonctionnement

Pour ce qui est du fonctionnement de l'outil, Kyverno va opérer au niveau des parties Mutating admission (modification d'objets) et Validating admission (validation d'objet) de la requête HTTP envoyée au kube-apiserver dans le but de modifier l'objet demandé, d'autoriser ou non sa création dans l'orchestrateur.

C'est pour cela qu'il s'exécute comme un Dynamic Admission Controller.

En plus de cela, Kyverno va aussi reporter les ressources qui ne correspondent pas aux stratégies définies (Policy) à travers des rapports ou dans les événements du cluster.

La documentation officielle de Kyverno met à disposition un schéma pour expliquer le fonctionnement de l'outil et les différents éléments qui composent son architecture :

kyverno-architecture

  • Le PolicyController se charge d'analyser les stratégies mises en place au sein de Kyverno et de faire de manière continue des scans ;
  • Le GenerateController s'occupe du cycle de vie des ressources générées via l'instruction generate que l'on verra plus tard ;
  • Enfin, le Webhook est en interaction avec les requêtes du kube-apiserver qu'elles proviennent des phases du Mutating admission ou Validating admission. Le Monitor crée et gère les configurations.

Une fois Kyverno installé, vous serez en mesure de créer des stratégies (Policy) et différentes règles au sein de votre cluster.

Deux types d'objet pour les stratégies sont disponibles :

  • ClusterPolicy pour définir des règles sur l'ensemble du cluster ;
  • Policy pour définir des règles sur un namespace en particulier.

Chaque stratégie se compose d'une ou plusieurs règles.

Ces règles peuvent contenir des règles de correspondance match avec la possibilité d'exclure des ressources et peuvent se baser sur plusieurs caractéristiques :

  • kind, qui représente le type de la ressource ;
  • name, pour le nom de la ressource ;
  • label, pour le ou les paires clé-valeur associées à la ressource ;
  • namespace, pour l'espace de nom au sein du cluster ;
  • role, qui correspond à un rôle associé à un utilisateur ;
  • group, pour un groupe d'utilisateurs ;
  • et enfin, il est possible aussi d'associer un nom d'utilisateur user.

Enfin, chaque règle peut effectuer une seule action :

  • validate, dans le but de valider une ou plusieurs ressources en imposant que celles-ci disposent de labels par exemple ;
  • mutate pour modifier une ou plusieurs ressources en utilisant des patchs au format JSONPatch - RFC 6902 ;
  • generate pour créer une ou plusieurs ressources additionnelles ;
  • verifyImages qui utilise le projet open source cosign pour vérifier la signature d'images.

Kyverno dispose d'une fonctionnalité pratique dans le cas d'une mise en place d'un panel de stratégies sur un cluster Kubernetes existant afin de ne pas impacter le fonctionnement de celui-ci.

En effet, il est possible de spécifier le paramètre validationFailureAction en audit plutôt qu'en enforce ce qui permet de ne pas bloquer la création de ressources mais de remonter l'information dans les logs.

Dernier point intéressant, Kyverno possède une base de données avec des Policies déjà prêtes à l'emploi, ce qui évite de réinventer la roue et permet de se baser sur des stratégies déjà existantes.

Installation et découverte de l'outil

Passons maintenant à l'installation de Kyverno, avant toute chose, il est recommandé de regarder la compatibilité de chaque version de l'outil avec la version du cluster Kubernetes où Kyverno sera installé.

Dans la suite, j'ai choisi la version v1.8.0 qui est la dernière version mise à disposition lors de l'écriture de cet article et mon cluster est en version v1.25.3.

La manière la plus commune est d'installer Kyverno en utilisant le chart Helm officiel, néanmoins, plusieurs modes d'installation sont disponibles également.

# Ajoute le repository Kyverno à Helm
helm repo add kyverno https://kyverno.github.io/kyverno/
# Permet de mettre à jour l'ensemble des repo
helm repo update
# Crée le namespace Kyverno
kubectl create ns kyverno
# Installe Kyverno dans le namespace du même nom en mode haute disponibilité
helm install kyverno kyverno/kyverno --version 2.6.0 --namespace kyverno --set replicaCount=3

La commande ci-dessous permet de vérifier que Kyverno est correctement déployé :

$ kubectl -n kyverno get deploy
NAME      READY   UP-TO-DATE   AVAILABLE   AGE
kyverno   3/3     3            3           108s

$ kubectl -n kyverno get pods
NAME                       READY   STATUS    RESTARTS   AGE
kyverno-5f7d9f9675-672lf   1/1     Running   0          75s
kyverno-5f7d9f9675-7lwn2   1/1     Running   0          75s
kyverno-5f7d9f9675-cr5r6   1/1     Running   0          75s

Les Pod sont en statut Running et sont Ready, tout semble bon.

Stratégie de validation

Il est maintenant temps de déployer notre première ClusterPolicy, qui sera assez simple à comprendre avec pour objectif de vérifier la présence d'un tag au niveau de l'image du conteneur. La version brute de cette stratégie se trouve ici.

Cette stratégie se compose de deux règles :

  • La première permet de vérifier qu'un tag est bien associé à l'image d'un Pod ;
  • La seconde rejette le tag :latest d'une image.

On retrouve dans ces deux règles une structure identique :

  • La partie match pour appliquer la règle sur une ou plusieurs ressources spécifiques, dans ce cas de figure, on est au niveau du Pod.
  • La partie validate pour vérifier selon un modèle (pattern) la structure du Pod. Dans cette situation, on regarde le champ image et son format.

Pour exécuter notre stratégie, il est nécessaire d'exécuter cette ligne de commande :

kubectl create -f https://raw.githubusercontent.com/kyverno/policies/main/best-practices/disallow_latest_tag/disallow_latest_tag.yaml

Petit rappel, une ClusterPolicy n'a pas de namespace en particulier, elle est active sur l'ensemble du cluster.

Maintenant, pour tester, on peut créer un simple Pod avec comme image nginx sans tag particulier :

kubectl run nginx --image=nginx

La ClusterPolicycréée est en mode audit, c'est pourquoi la création du Pod n'a pas été bloquée. Cependant, on peut récupérer le rapport qui est un objet de type policyreports :

$ kubectl get policyreports
NAME                       PASS   FAIL   WARN   ERROR   SKIP   AGE
cpol-disallow-latest-tag   1      1      0      0       0      2m30s

$ kubectl get policyreports cpol-disallow-latest-tag -o yaml | grep "result: fail" -B10
- category: Best Practices
  message: 'validation error: An image tag is required. rule require-image-tag failed
    at path /spec/containers/0/image/'
  policy: disallow-latest-tag
  resources:
  - apiVersion: v1
    kind: Pod
    name: nginx
    namespace: default
    uid: f2f2cba4-7bc2-4456-b206-9609c00e4154
  result: fail

Pour mettre à jour la Policy en mode enforce, cette commande va mettre à jour notre stratégie :

kubectl patch clusterpolicy disallow-latest-tag --type merge --patch '{"spec": {"validationFailureAction": "enforce"}}'

Maintenant, si on retente la création du Pod, on tombe sur cette erreur assez parlante :

$ kubectl run nginx --image=nginx
Error from server: admission webhook "validate.kyverno.svc-fail" denied the request:

policy Pod/default/nginx for resource violation:

disallow-latest-tag:
  require-image-tag: 'validation error: An image tag is required. rule require-image-tag
    failed at path /spec/containers/0/image/'

Et si on fixe le tag, pas d'erreur et le Podest créé :

$ kubectl run nginx-tag --image=nginx:1.23.2
pod/nginx-tag created

$ kubectl get po
NAME        READY   STATUS    RESTARTS   AGE
nginx-tag   1/1     Running   0          10s

Stratégie de mutation

Après avoir testé la validation au sein de Kyverno, les stratégies de mutation permettent d'effectuer des changements sur des ressources qui souhaitent être créées. Les mutations peuvent ajouter, modifier ou supprimer du contenu.

Un point important à prendre en compte, c'est que les mutations sont effectuées avant la validation. Il ne faut donc pas que la règle de validation vienne contredire le changement appliqué par la stratégie de mutation.

L'exemple le plus simple et rapide à mettre en place est de se baser sur cette ClusterPolicy avec le format brut via ce lien.

Quelques modifications sont à réaliser pour en faire une Policy qui ne s'appliquera qu'au namespace app-backend en appliquant plusieurs labels recommandés :

cat <<EOF | kubectl create -f -
apiVersion: kyverno.io/v1
kind: Policy
metadata:
  name: add-labels
  namespace: app-backend
  annotations:
    policies.kyverno.io/title: Add Labels
    policies.kyverno.io/category: Sample
    policies.kyverno.io/severity: medium
    policies.kyverno.io/subject: Label
    policies.kyverno.io/description: >-
      This policy performs a simple mutation which adds labels
      to Pods, Services, ConfigMaps, and Secrets.
spec:
  rules:
  - name: add-labels
    match:
      resources:
        kinds:
        - Pod
        - Service
        - ConfigMap
        - Secret
    mutate:
      patchStrategicMerge:
        metadata:
          labels:
            app.kubernetes.io/component: app-backend
            app.kubernetes.io/part-of: my-big-app
            app.kubernetes.io/managed-by: helm
EOF

On part du principe que notre namespace contiendra uniquement des applications backend et que celles-ci appartiennent à une grosse application du nom de my-big-app, le tout déployé par Helm.

C'est parti pour tester cette mutation :

# Création du namespace
kubectl create ns app-backend
# Création du pod
kubectl -n app-backend run test-mutate --image=nginx:1.23.2
# Récupération des labels
kubectl -n app-backend get pod test-mutate -o yaml | grep -i "labels:" -A5

On retrouve bien nos trois labels définis :

  labels:
    app.kubernetes.io/component: app-backend
    app.kubernetes.io/managed-by: helm
    app.kubernetes.io/part-of: my-big-app
    run: test-mutate
  name: test-mutate

Stratégie de génération

Comme dit plus haut, la partie generate permet de créer des ressources supplémentaires à chaque création ou mise à jour de ressource. Je trouve cette partie extrêmement intéressante pour automatiser la création de ressources.

Par exemple, dès lors qu'un utilisateur, éventuellement un administrateur du cluster, souhaite créer un nouveau namespace il peut être intéressant d'ajouter une NetworkPolicy visant à restreindre les communications d'entrée et de sortie.

C'est ce que Kyverno met à disposition via cette stratégie avec la version brute ici.

...
  rules:
  - name: default-deny
    match:
      resources:
        kinds:
        - Namespace
    generate:
      apiVersion: networking.k8s.io/v1
      kind: NetworkPolicy
      name: default-deny
      namespace: "{{request.object.metadata.name}}"
      synchronize: true
      data:
        spec:
          podSelector: {}
          policyTypes:
          - Ingress
          - Egress

En prenant une partie de cette ClusterPolicy, on s'aperçoit que celle-ci s'applique sur les namespace en créant un objet de type NetworkPolicy ayant pour nom default-deny et qui s'applique au nom du namespace préalablement créé via cette instruction {{request.object.metadata.name}}. La propriété synchronize permet de garder la définition de l'objet même si une mise à jour intervient.

kubectl create -f https://raw.githubusercontent.com/kyverno/policies/main/best-practices/add_network_policy/add_network_policy.yaml

On va tester la création d'un namespace :

$ kubectl create ns test-ns
namespace/test-ns created

$ kubectl -n test-ns get netpol
NAME           POD-SELECTOR   AGE
default-deny   <none>         25s

La NetworkPolicy a bien été générée. Si on essaye de la supprimer, celle-ci sera recréée systématiquement grâce au paramètre synchronize: true :

$ kubectl -n test-ns delete netpol default-deny
networkpolicy.networking.k8s.io "default-deny" deleted

$ kubectl -n test-ns get netpol
NAME           POD-SELECTOR   AGE
default-deny   <none>         5s

Stratégie de vérification d'images

Dernière étape, la vérification d'images avec cosign de Sigstore permettant entre autres de vérifier que les images sont signées par une clé publique.

Kyverno fournit un exemple disponible à cette adresse, il est également possible d'utiliser un Key Management Service (KMS) et intégrer la vérification de signature avec les services managés des fournisseurs de Cloud voire même celui de Vault.

Cette fonctionnalité est très intéressante pour valider la provenance des images et vérifier que celles-ci n'ont pas été altérées.

Conclusion

Kyverno est un produit rapide et simple à mettre en place qui s'accompagne d'une multitude de stratégies sur étagère et prête à l'emploi.

Je suis convaincu qu'il doit être intégré dès l'installation d'un cluster Kubernetes pour suivre et renforcer l'utilisation des standards et bonnes pratiques en matière de sécurité.

Enfin, cet outil offre aussi plusieurs fonctionnalités et va plus loin que la simple validation de schéma pour les ressources à déployer. La génération, mutation de ressources et même la vérification d'images permettent d'ajouter une couche d'automatisation et de sécurité dans votre orchestrateur.

En conclusion, Kyverno est l'outil indispensable pour toute personne touchant de près ou de loin à l'univers Kubernetes.