10 mai 2022

Kubernetes étant souvent un élément critique des infrastructures, il est nécessaire d’imposer des bonnes pratiques de sécurité. Les PodSecurityPolicy (PSP) de Kubernetes sont dépréciées, alors comment faire ? Nativement, le control plane de Kubernetes n’offre pas la possibilité de définir des politiques de sécurité strictes. D’après notre expérience, Kyverno est le meilleur outil pour imposer des règles de sécurité.

 

Dans la suite de cet article, je vous présenterai Kyverno d’un point de vue théorique, comment l’installer et un exemple concret d’utilisation : la gestion dynamique des droits RBAC associés aux environnements de développement.

 

Pour ceux qui souhaitent une introduction à Kubernetes, cet article est pour vous !

 

Enfin, je présenterai plusieurs points d’attention importants qui nous ont causé des problèmes par le passé.

 

C’est parti !

Théorie

Généralités

Dans tout l’article, nous allons utiliser le vocabulaire associé aux ressources Kyverno : Policy, Rule, ...

Kyverno est donc un policy engine pour Kubernetes. Il permet de :

  • Définir des policies en tant que ressources Kubernetes ;
  • Valider, modifier ou générer des ressources à la volée via ces policies ;
  • Bloquer des ressources non conformes grâce à un admission controller ;
  • Journaliser les violations de policies dans des rapports.

Avantages

  • Définir des policies de sécurité pour interdir la création de ressources non sécurisées ;
  • Simplifier la vie des Ops via des mutations de ressources à la volée ;
  • Possibilité de configurer les policies en mode audit (sans blocage) ou enforce ;
  • Une écriture de policy simple (par rapport à GateKeeper notamment)

Inconvénients

  • Difficile de créer des policies avec une logique très spécifique et/ou complexe ;
  • Kyverno est un Single Point Of Failure : certains connaissent le côté sombre des adminssion controller : si les pods Kyverno ne sont plus disponibles, plus aucune ressource Kubernetes ne peut se déployer sur le cluster. Je vous donnerai quelques astuces pour éviter tout problème dans la suite de l’article.

Kubernetes Webhook

Kyverno fonctionne en tant que dynamic admission controller dans le cluster Kubernetes.

Le webhook Kyverno reçoit des requêtes de l'API server lors des étapes de "validating admission" et "mutating admission" :

Kubernetes_webhook

Policy & Rule

Une Policy Kyverno est composée des champs suivant (pour plus d'infos: kubectl explain policy.spec) :

  • rules : une ou plusieurs rules définissent la policy
  • background : si true, la policy s'applique à toutes les ressources Kubernetes existantes du cluster, sinon elle s'applique seulement aux nouvelles ressources
  • validationFailureAction : le mode d'action de la policy : audit ou enforce

Une Rule contient les champs suivants (pour plus d'infos: kubectl explain policy.spec.rules) :

  • match : pour sélectionner les ressources
  • exclude (optionnel) : pour exclure des ressources de la sélection
  • mutate, validate, generate ou verifyImages : selon le type de policy, permet de modifier, valider, générer une ressource ou vérifier la signature d’une image (en beta)

policy_rule

Audit vs Enforce

Kyverno dispose de 2 modes de fonctionnements(validationFailureAction) :

  • audit : ne bloque aucun déploiement, mais génère un rapport indiquant quand les policies spécifiées ne sont pas respectées et pourquoi
  • enforce : bloque complètement la création de ressources ne respectant pas les policies

Policy Report

Les rapports de Policies (Policy Reports) sont des ressources Kubernetes qui peuvent être listées simplement :

kubectl get policyreport -A

Pour un namespace donné, on peut lister les violations de policy avec la commande :

kubectl describe polr polr-ns-default | grep "Result: \\+fail" -B10


Installation

Kyverno peut être installé sur les clusters via un simple chart Helm. Rien de plus simple, c’est toute la puissance de Kubernetes :

kelm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
helm install kyverno  --namespace kyverno --create-namespace --values values.yaml

Voici les points importants à considérer dans les values.yaml du chart :

# https://github.com/kyverno/kyverno/blob/main/charts/kyverno/values.yaml

# Garantir la HA de kyverno via 3 replicas
replicaCount: 3

config:
  # Les types de ressources qui ne doivent pas être monitorées par Kyverno. 
  # Le premier champ est le type de ressource, le deuxième le namespace, le troisième son nom
  resourceFilters:
  - "[Event,*,*]"
  - "[*,kube-system,*]"    # Très important ! Evitez de contrôler kube-system pour garantir le bon fonctionnement du cluster
  - "[*,kube-public,*]"
  - "[*,kube-node-lease,*]"
  - "[Node,*,*]"
  - "[APIService,*,*]"
  - "[TokenReview,*,*]"
  - "[SubjectAccessReview,*,*]"
  - "[SelfSubjectAccessReview,*,*]"
  - "[*,kyverno,*]"        # Idem pour le namesapce kyverno, il vaut mieux éviter que Kyverno se contrôle lui-même
  - "[Binding,*,*]"
  - "[ReportChangeRequest,*,*]"
  - "[ClusterReportChangeRequest,*,*]"

Quelques remarques à propos de l’installation :

Exemple de policy

Une liste d’exemples simples est fournie dans la documentation de Kyverno.

J’aimerais vous présenter un cas d’usage un peu plus avancé : la gestion dynamique des droits RBAC. Voici le cas d’usage que nous avons rencontré. Nous avons mis en place chez un client des environnements de développement à la volée dans Kubernetes.

Nous avons autorisé les développeurs, via un job de CI Gitlab, à tester leurs applications dans des environnements créés à la volée. Ces environnements sont dans des namespaces dédiés créés également à la volée.

Comment fournir au runner Gitlab associé des droits RBAC sur des namespaces qui n’existent pas encore ? Malheureusement, Kubernetes ne permet pas cela via RBAC, mais avec Kyverno, c’est très simple.

Il suffit en effet :

  • De donner au runner les droits RBAC de créer des namespaces
  • De donner les droits RBAC sur ce namespace via une Policy Kyverno : une Mutation Policy peut simplement créer une RoleBinding en réaction à la création du namespace

Voici les détails d’implémentation :

  • Le compte de service k8s gitlab-runner-ephemeral-env est uniquement autorisé à créer des namespaces
apiVersion: v1
kind: ServiceAccount
metadata:
  name: gitlab-runner-ephemeral-env
  labels:
    app: gitlab-runner-ephemeral-env
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: gitlab-runner-ephemeral-env
  labels:
    app: gitlab-runner-ephemeral-env
rules:
- apiGroups: ["*"]
  resources: ["namespaces"]
  verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: gitlab-runner-ephemeral-env
  labels:
    app: gitlab-runner-ephemeral-env
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: gitlab-runner-ephemeral-env
subjects:
- kind: ServiceAccount
  name: gitlab-runner-ephemeral-env
  namespace: gitlab
  • À la création d’un namespace, un rolebinding est créé entre lui et le ClusterRole cluster-admin via une ClusterPolicy Kyverno
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-rbac-rules-env-volee
  annotations:
    policies.kyverno.io/title: Add RBAC permissions for ephemeral environments.
    policies.kyverno.io/category: Multi-Tenancy
    policies.kyverno.io/subject: RBAC
    policies.kyverno.io/description: >-
      Add RBAC rules when a namespace is created by a specific gitlab runner (gitlab-runner-env-volee), useful for ephemeral
      environments.
spec:
  background: false
  rules:
  - name: create-rbac
    match:
      resources:
        kinds:
        - Namespace
      subjects:
      - kind: ServiceAccount
        name: gitlab-runner-ephemeral-env
        namespace: gitlab
    generate:
      kind: RoleBinding
      name: ephemeral-namespace-admin
      namespace: ""
      synchronize: true
      data:
        subjects:
        - kind: ServiceAccount
          name: gitlab-runner-ephemeral-env
          namespace: gitlab
        roleRef:
          kind: ClusterRole
          name: cluster-admin
          apiGroup: rbac.authorization.k8s.io

 

Limites de Kyverno

Je vais vous détailler dans cette partie plusieurs problèmes rencontrés lors de l’implémentation de Kyverno. Outre le fait que Kyverno soit un SPOF sur tous les namespaces qu’il surveille, les policies sont assez compliquées à écrire et à débugguer. Sans compter que Kyverno peut avoir des effets de bords avec d’autres outils comme ArgoCD.

Les policies sont complexes à écrire

Globalement, les policies Kyverno peuvent être assez difficiles à écrire. La documentation présente beaucoup d’exemples, mais tout le mécanisme de filtrage et de mutation des ressources peut être un peu déroutant au début.

Prenons un exemple concert. On veut interdire le paramètre privileged: true sauf à deux types de pods (comme sur le schéma suivant) :

  • Les pods dans le namespace debug
  • Les pods dans le namespace gitlab dont le nom comment par runner

match_pods

En suivant la documentation, on est tenté d’écrire la policy suivante :

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-privileged-containers
  annotations:
    policies.kyverno.io/category: Pod Security Standards (Baseline)
    policies.kyverno.io/severity: medium
    policies.kyverno.io/subject: Pod
    policies.kyverno.io/description: >-
      Privileged mode disables most security mechanisms and must not be allowed. This policy
      ensures Pods do not call for privileged mode.
spec:
  validationFailureAction: audit
  background: true
  rules:
    - name: priviledged-containers
      match:
        resources:
          kinds:
            - Pod
      exclude:
        any:
        - resources:
            namespaces:
            - "debug"
        # Whitelisting
        - resources:
            namespaces:
              - "gitlab"
            names:
            - "runner-*"
      validate:
        message: >-
          Privileged mode is disallowed. The fields spec.containers[*].securityContext.privileged
          and spec.initContainers[*].securityContext.privileged must not be set to true.
        pattern:
          spec:
            =(initContainers):
              - =(securityContext):
                  =(privileged): "false"
            containers:
              - =(securityContext):
                  =(privileged): "false"

Cette policy ne fonctionne pas, le mécanisme de filtrage n’est pas effectif. Après quelques recherches, voici le fix à appliquer :

18,20c18,21
<         resources:
<           kinds:
<             - Pod
---
>         all:
>         - resources:
>             kinds:
>               - Pod

Rien n’indique dans la documentation un changement de comportement entre ces deux manières de filtrer des ressources. Pas facile de debug une policy qui ne fonctionne pas... heureusement, la communauté est active, et quelqu’un nous a rapidement proposé la solution sur Slack.

Attention aux Mutation Webhook

D’expérience, il faut toujours faire attention aux Mutation Webhooks, qui peuvent porter à confusion les équipes DevOps. Les Mutations Webhooks Kubernetes induisent par nature une différence entre les ressources spécifiées et les ressources réellement déployées sur le cluster.

Si un Ops n’est pas au fait de l’existence de ces mutations, il peut perdre beaucoup de temps à comprendre pourquoi telle ou telle ressource apparaît ou possède certains attributs.

De la même façon, si un cluster possède un nombre trop important de MutationPolicy, il peut y avoir des incompatibilités entre les policies, ou des effets de bords difficiles à identifier.

Je recommande d’utiliser les Mutations Webhook avec parcimonie, et de les documenter très clairement. Cela peut être extrêmement pratique (e.g : ajouter l’adresse d’un proxy HTTP en variable d’environnement de tous les pods d’un namespace), mais il vaut mieux éviter si possible d’en abuser.

Effets de bords avec ArgoCD

Nous avons également rencontré quelques difficultés avec les clusters Kubernetes dont la CD est gérée via ArgoCD.

Lorsqu’une policy Kyverno est créée et concerne une ressource qui déploie des conteneurs, comme des pods, Kyverno modifie intelligemment les rules pour que les policies prennent en compte tous les types de ressource Kubernetes qui déploie des conteneurs.

Par exemple, si on crée cette policy :

apiVersion : kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: restrict-image-registries
spec:
  validationFailureAction: enforce
  rules:
  - name: validate-registries
    match:
      any:
      - resources:
          kinds:
          - Pod
    validate:
      message: "Images may only come from our internal enterprise registry."
      pattern:
        spec:
          containers:
          - image: "registry.domain.com/*"

Kyverno va modifier la policy à la volée via un Mutation Webhook comme ceci :

aspec:
  background: true
  failurePolicy: Fail
  rules:
  - match:
      any:
      - resources:
          kinds:
          - Pod
    name: validate-registries
    validate:
      message: Images may only come from our internal enterprise registry.
      pattern:
        spec:
          containers:
          - image: registry.domain.com/*
  - match:
      any:
      - resources:
          kinds:
          - DaemonSet
          - Deployment
          - Job
          - StatefulSet
    name: autogen-validate-registries
    validate:
      message: Images may only come from our internal enterprise registry.
      pattern:
        spec:
          template:
            spec:
              containers:
              - image: registry.domain.com/*
  - match:
      any:
      - resources:
          kinds:
          - CronJob
    name: autogen-cronjob-validate-registries
    validate:
      message: Images may only come from our internal enterprise registry.
      pattern:
        spec:
          jobTemplate:
            spec:
              template:
                spec:
                  containers:
                  - image: registry.domain.com/*
  validationFailureAction: enforce

Que se passe-t-il dans le cas où la policy Kyverno a été créée via Argo ? Argo va détecter un changement entre le fichier Yaml de la policy déclarée et la ressource effectivement déployée dans le cluster. On assiste alors à un va-et-vient permanent entre Argo et Kyverno, qui modifient tour à tour la Policy Kyverno.

Pour indiquer à Argo que ces changements ne sont pas à prendre en compte, il suffit d’utiliser le mot-clé ignoreDifferences dans l’application Argo :

ignoreDifferences:
    # Kyverno auto-generates rules to make policies smarter. We want ArgoCD to
    # ignore the auto-generated rules.
    # For more information: https://kyverno.io/docs/writing-policies/autogen/
    - group: kyverno.io
      kind: ClusterPolicy
      jqPathExpressions:
        - .spec.rules[] | select( .name | startswith("autogen-") )

 

Conclusion

Voilà, vous savez désormais en quoi consiste Kyverno, comment l’installer, puis l’utiliser pour sécuriser votre cluster Kubernetes ! Encore une fois, utilisez les Mutation Webhook avec parcimonie, testez bien vos policies en mode audit au préalable, et n’hésitez pas à contacter la communauté en cas de problème.