Skip to content

Set up Kyverno

Estimated time to read: 8 minutes

Pour contrôler ce qu'il se passe dans notre cluster, nous allons utiliser kyverno.

Installation

Kyverno fournit un Helm chart pour simplifer l'installation.

helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update

Une fois configuré, on peut installer le Helm chart:

helm install kyverno kyverno/kyverno -n kyverno --create-namespace

Success

Thank you for installing kyverno! Your release is named kyverno.

The following components have been installed in your cluster:
- CRDs
- Admission controller
- Reports controller
- Cleanup controller
- Background controller

On vérifie que tout est ok coté namespace kyverno

kubectl get all -n kyverno
Les composants installés par kyverno
NAME                                                READY   STATUS    RESTARTS   AGE
pod/kyverno-admission-controller-547894dbc9-srg54   1/1     Running   0          2m43s
pod/kyverno-background-controller-696f7765d-d8knf   1/1     Running   0          2m43s
pod/kyverno-cleanup-controller-567bb6695c-mxf7s     1/1     Running   0          2m43s
pod/kyverno-reports-controller-5799587486-x2gjp     1/1     Running   0          2m43s

NAME                                            TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
service/kyverno-background-controller-metrics   ClusterIP   10.3.236.41    <none>        8000/TCP   2m45s
service/kyverno-cleanup-controller              ClusterIP   10.3.230.129   <none>        443/TCP    2m45s
service/kyverno-cleanup-controller-metrics      ClusterIP   10.3.189.211   <none>        8000/TCP   2m45s
service/kyverno-reports-controller-metrics      ClusterIP   10.3.153.42    <none>        8000/TCP   2m45s
service/kyverno-svc                             ClusterIP   10.3.165.95    <none>        443/TCP    2m45s
service/kyverno-svc-metrics                     ClusterIP   10.3.106.166   <none>        8000/TCP   2m45s

NAME                                            READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/kyverno-admission-controller    1/1     1            1           2m44s
deployment.apps/kyverno-background-controller   1/1     1            1           2m44s
deployment.apps/kyverno-cleanup-controller      1/1     1            1           2m44s
deployment.apps/kyverno-reports-controller      1/1     1            1           2m44s

NAME                                                      DESIRED   CURRENT   READY   AGE
replicaset.apps/kyverno-admission-controller-547894dbc9   1         1         1       2m44s
replicaset.apps/kyverno-background-controller-696f7765d   1         1         1       2m44s
replicaset.apps/kyverno-cleanup-controller-567bb6695c     1         1         1       2m44s
replicaset.apps/kyverno-reports-controller-5799587486     1         1         1       2m44s

NAME                                                      SCHEDULE       SUSPEND   ACTIVE   LAST SCHEDULE   AGE
cronjob.batch/kyverno-cleanup-admission-reports           */10 * * * *   False     0        <none>          2m44s
cronjob.batch/kyverno-cleanup-cluster-admission-reports   */10 * * * *   False     0        <none>          2m44s

Ma première policy

Comme nous l'a demandé le chef de brigade, on veut contrôler nos ingrédients : mettons en place une policy pour limiter l'usage d'images Docker provenant uniquement d'une registry privée en laquelle on a confiance, dans notre demo, le registry de gitlab.com de notre projet.

Définissons notre policy ClusterPolicy qui sera à l'échelle du cluster complet (les ➕ vous donnent des indications pour mieux comprendre)

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  annotations:
    policies.kyverno.io/title: Restrict Image Registries
    policies.kyverno.io/description: >-
      Only images from specific gitlab.com registry are allowed.
spec:
  validationFailureAction: Enforce # (1)
  background: true
  rules:
  - name: validate-registries
    match:
      any:  # (2)
      - resources:
          kinds:
          - Pod
      exclude: # (3)
        any:
          - resources:
              namespaces:
                - kube-system
                - external-dns
                - external-secrets
                - nginx-ingress-controller
                - kyverno
                - cert-manager 
    validate:  # (4)
      message: "Unauthorized registry."
      pattern:
        spec:  # (5)
          =(ephemeralContainers):
          - image: "registry.gitlab.com/*"
          =(initContainers):
          - image: "registry.gitlab.com/*"
          containers:
          - image: "registry.gitlab.com/*"
  1. 💪 On utilise une action de type Enforce qui est bloquante. Il existe aussi le type Audit qui est un warning mais non bloquant
  2. 🔍 On applique à tous les Pod
  3. 🚸 On filtre les namespaces que l'on a déjà instancié et qui sont des namespaces d'administration
  4. 👽 Le type de règle, ici validate
  5. 📝 On spécifie pour chaque type, les origines autorisées

On peut déployer notre policy:

kubectl apply -f kyverno/kyverno-registry-policy.yml

et on vérifie qu'elle est bien opérationnelle

kubectl get clusterpolicies.kyverno.io -n kyverno

Policy au statut Ready

NAME                      ADMISSION   BACKGROUND   VALIDATE ACTION   READY   AGE   MESSAGE
restrict-image-registry   true        true         Enforce           True    18m   Ready

Vérification

Essayons de démarrer un pod avec une image issue de Docker hub:

kubectl run demo-nginx --image=nginx:latest -n demos

Impossible de démarrer le pod

Error from server: admission webhook "validate.kyverno.svc-fail" denied the request:

resource Pod/demos/demo-nginx was blocked due to the following policies

restrict-image-registry:
  validate-registries: 'validation error: Unauthorized registry. rule validate-registries
    failed at path /spec/containers/0/image/'

On met à jour notre Deployment pour utiliser une image issue de la registry GitLab:

apiVersion: apps/v1
kind: Deployment
# ...
spec:
  selector:
    matchLabels:
      app: nginx-hardened
  template:
    metadata:
      labels:
        app: nginx-hardened
    spec:
      containers:
        - image: registry.gitlab.com/yodamad-workshops/kub-workshop/asciinematic:latest # (1)
          name: asciinematic-hardened
          ports:
            - containerPort: 80
      imagePullSecrets:
        - name: gitlabcred # (2)
  1. 🐳 On utilise une image de notre registry privée
  2. 🔐 C'est une registry privée, il faut s'authentifier...

On voit qu'il faut ajout un secret pour être capable de pull une image depuis une registry privée, normal... Mais du coup, il faut créer un secret pour cela. Alors on va en créer un

kubectl create secret gitlabcred regcred --docker-server=<your-registry-server> --docker-username=<your-name> --docker-password=<your-pword> --docker-email=<your-email>

Le chef de la brigade de la sécurité

Pas de secret en dur dans mon cluster !! 🤬

Oups, il a pas tort 😅 On devrait plutôt utiliser external-secrets

On crée un ExternalSecret

---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: external-secret-gitlabcred
  namespace: kyverno
spec:
  refreshInterval: 1m
  secretStoreRef:
    name: gitlab-cluster-secret-store
    kind: ClusterSecretStore
  target:
    name: gitlabcred
    creationPolicy: Owner
    template:
      engineVersion: v2
      type: kubernetes.io/dockerconfigjson # (1)
      data: # (2)
        .dockerconfigjson: "{\"auths\":{\"registry.gitlab.com\":{\"username\":\"{{ .username }}\",\"password\":\"{{ .password }}\",\"auth\":\"{{(printf \"%s:%s\" .username .password) | b64enc }}\"}}}"
  data: # (3)
    - secretKey: username
      remoteRef:
        key: gl_cr_username
    - secretKey: password
      remoteRef:
        key: gl_cr_password
  1. 🐳 On définit le type de template que l'on utilise pour créer le secret
  2. 💽 On crée le dockerconfigjson à partir de 2 variables
  3. 🦊 On récupère le username & le mot de passe depuis GitLab

que l'on déploie

kubectl apply -f kyverno/kyverno-gitlabcred-external-secret.yml

Secret correctement créé

L'ExternalSecret est créé et synchronisé

kubectl get externalsecrets.external-secrets.io -n kyverno
NAME                         STORE                         REFRESH INTERVAL   STATUS         READY
external-secret-gitlabcred   gitlab-cluster-secret-store   1m                 SecretSynced   True

et le secret aussi

kubectl describe secret gitlabcred -n kyverno
Name:         gitlabcred
Namespace:    kyverno
Labels:       reconcile.external-secrets.io/created-by=25390d6b8b839ba8a1d72cfcfe6f6319
Annotations:  reconcile.external-secrets.io/data-hash: 9dfdd28d70dbacf25d05ab5d782ae9a5

Type:  kubernetes.io/dockerconfigjson

Data
====
.dockerconfigjson:  154 bytes

On doit pouvoir déployer notre nouvelle image

kubectl apply -f kyverno/kyverno-asciinematic-hardened.yml
Pour les curieux, le fichier kyverno-asciinematic-hardened.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: asciinematic-hardened
  namespace: demos
spec:
  selector:
    matchLabels:
      app: asciinematic-hardened
  template:
    metadata:
      labels:
        app: asciinematic-hardened
    spec:
      containers:
        - image: registry.gitlab.com/yodamad-workshops/kub-workshop/asciinematic:latest
          name: asciinematic-hardened
          ports:
            - containerPort: 80
      imagePullSecrets:
        - name: gitlabcred

Image déployée

kubectl get po -n demos
NAME                                    READY   STATUS    RESTARTS   AGE
asciinematic-hardened-95b5f9d76-b87j9   1/1     Running   0          64s

Une 2ème policy

Du fait que l'on a forcé sur l'ensemble du cluster que les images proviennent de GitLab, il serait pratique qu'automatiquement lorsque l'on crée un namespace, automatiquement le secret pour se connecter à GitLab via external-secrets se crée.

On a utilisé un type validate lors de notre première policy, dans ce second cas, on va utiliser le type generate qui va automatiquement générer des éléments.

# ...
  policies.kyverno.io/description: >-
    Secrets like registry credentials often need to exist in multiple
    Namespaces so Pods there have access. Manually duplicating those Secrets
    is time consuming and error prone. This policy will copy a
    Secret called `gitlabcred` which exists in the `kyverno` Namespace to
    new Namespaces when they are created. It will also push updates to
    the copied Secrets should the source Secret be changed.
  spec:
    rules:
      - name: sync-image-pull-secret
        # ...
        generate: # (1)
          apiVersion: v1
          kind: Secret
          name: regcred
          namespace: "{{request.object.metadata.name}}"
          synchronize: true
          clone: # (2)
            namespace: kyverno
            name: gitlabcred
  1. 👽 Type generate
  2. 🧬 Action de clone d'un object existant

Déployons cette nouvelle policy

kubectl apply -f kyverno/kyverno-sync-regcred.yml
Pour les curieux, le fichier kyverno-sync-regcred.yml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: sync-secrets
  annotations:
    policies.kyverno.io/title: Sync Secrets
    policies.kyverno.io/category: Sample
    policies.kyverno.io/subject: Secret
    policies.kyverno.io/minversion: 1.6.0
    policies.kyverno.io/description: >-
      Secrets like registry credentials often need to exist in multiple
      Namespaces so Pods there have access. Manually duplicating those Secrets
      is time consuming and error prone. This policy will copy a
      Secret called `gitlabcred` which exists in the `kyverno` Namespace to
      new Namespaces when they are created. It will also push updates to
      the copied Secrets should the source Secret be changed.
spec:
  rules:
    - name: sync-image-pull-secret
      match:
        any:
          - resources:
              kinds:
                - Namespace
      generate:
        apiVersion: v1
        kind: Secret
        name: regcred
        namespace: "{{request.object.metadata.name}}"
        synchronize: true
        clone:
          namespace: kyverno
          name: gitlabcred

On vérifie que la policy est créée et opérationnelle

kubectl get clusterpolicies.kyverno.io -n kyverno

Policy créée et opérationnelle

NAME                      ADMISSION   BACKGROUND   VALIDATE ACTION   READY   AGE   MESSAGE
restrict-image-registry   true        true         Enforce           True    66m   Ready # (1)
sync-secrets              true        true         Audit             True    26s   Ready
  1. 🚔 On retrouve bien notre première policy

Vérification

Vérifions que notre nouvelle policy fonctionne bien en créant un nouveau namespace

kubectl create ns demo-kyverno

Normalement, on devrait avoir un secret dans notre nouveau namespace

kubectl get secret -n demo-kyverno

Secret créé

NAME         TYPE                          DATA   AGE
regcred   kubernetes.io/dockerconfigjson   1      112s

Voyons si l'on essaie de déployer une image provenant de notre registry privée dans notre nouveau namespace

kubectl run demo-nginx --image=registry.gitlab.com/yodamad-workshops/kub-workshop/nginx:hardened -n demo-kyverno

Image déployée

kubectl get po -n demo-kyverno
NAME         READY   STATUS    RESTARTS   AGE
demo-nginx   1/1     Running   0          15s

Et vérifions que notre première policy est bien valable dans notre nouveau namespace

kubectl run demo-nginx --image=nginx:latest -n demo-kyverno

Impossible de déployer

Error from server: admission webhook "validate.kyverno.svc-fail" denied the request:

resource Pod/demo-kyverno/demo-nginx was blocked due to the following policies

restrict-image-registry:
  validate-registries: 'validation error: Unauthorized registry. rule validate-registries
    failed at path /spec/containers/0/image/'

Suivi de nos policies

Kyverno permet de facilement voir les policies qui ont été exécutées

kubectl get policyreports -n demo-kyverno -o wide

La policy a été exécutée avec succès

NAME                                   KIND   NAME         PASS   FAIL   WARN   ERROR   SKIP   AGE
3e3ad989-01be-46fc-afc9-7eea0f78a74c   Pod    demo-nginx   1      0      0      0       0      5m9s

On peut aussi vérifier à l'échelle du cluster

kubectl get policyreports -A
L'ensemble des reports du cluster
NAMESPACE      NAME                                   PASS   FAIL   WARN   ERROR   SKIP   AGE
demo-kyverno   3e3ad989-01be-46fc-afc9-7eea0f78a74c   1      0      0      0       0      14m
demos          4649657f-4994-47eb-a15b-a3e2b14e82db   0      1      0      0       0      88m
demos          718a97de-06af-4df8-b7d0-17f7ca9b5d02   1      0      0      0       0      34m
demos          81d369ad-0b5a-46ba-85b6-0e73e7572afe   0      1      0      0       0      88m

On voit que les policies se sont exécutées sur les composants installés avant la policy et que certains sont aussi status FAIL

Pour aller plus loin

Kyverno propose d'autres types de policies.

Article sur le sujet #autopromo

Un article décrivant plus en détails cela est dispo sur le blog

Notre cocotte est sécurisée et avec des bons produits issus de la filière locale 😉

Il est temps de se reposer un peu ➡️