Ik had ooit een wiki-pagina met de titel “Cluster conventies”. Resource limits op alles. Geen :latest tags. Geen deploys in de default namespace. Het was een prima pagina. Niemand las hem. Na een half jaar overtrad de helft van het cluster die regels, en ik kwam er pas achter toen er iets omviel.

Een regel die in een doc leeft is een suggestie. Een regel die de API server weigert te accepteren is governance. Dat gat is precies waarom deze post bestaat.

Kubernetes geeft elk team enorme vrijheid. Deploy wat je wilt, configureer het zoals je wilt. Zonder vangrails verandert die vrijheid in een langzaam opbouwende puinhoop die je ontdekt op het slechtst denkbare moment. Ik wil dat het cluster mijn standaarden afdwingt zodat ik ze niet hoef te onthouden, en zodat ik daadwerkelijk snap wat er draait in plaats van te hopen dat mensen de wiki hebben gevolgd.

Kyverno is een policy engine voor Kubernetes. Het valideert, muteert en genereert resources op basis van policies die je schrijft als gewone Kubernetes-native YAML. De rest van deze post bouwt op vanaf de kleinst mogelijke policy tot de volledige set die ik op elk cluster draai, zodat je kunt stoppen zodra je genoeg hebt.

Waarom Kyverno en niet OPA

Je hebt hier opties: Open Policy Agent (OPA) met Gatekeeper, Kyverno en Kubewarden. Ik kwam uit bij Kyverno om een paar eerlijke redenen:

  1. Native YAML. Geen Rego, geen tweede taal om in mijn hoofd te houden.
  2. Validatie, mutatie en generatie in één tool. Ik wil geen drie verschillende dingen voor drie verschillende klusjes.
  3. Declaratief. Policies zijn gewoon Kubernetes resources, dus ze leven in Git naast al het andere.
  4. GitOps-vriendelijk. ArgoCD synct ze zoals elk ander manifest.

OPA/Gatekeeper is echt krachtig, en Rego geeft je een expressiviteit die Kyverno niet kan evenaren voor de meest hardnekkige regels. De afweging is reëel: je leert een nieuwe taal voordat je je eerste nuttige policy schrijft. Voor mijn homelab en de meeste clusters die ik aanraak is die kosten het niet waard. Als je al YAML kent, laat Kyverno je binnen vijf minuten iets nuttigs schrijven.

De drie dingen die een policy kan doen

Voordat we YAML schrijven, onthoud deze drie werkwoorden. Alles wat Kyverno doet is er één van.

Valideren. Resources blokkeren die niet aan je criteria voldoen.

"Je kunt niet deployen zonder resource limits"

Muteren. Resources aanpassen terwijl ze binnenkomen.

"Alle pods krijgen automatisch dit label"

Genereren. Nieuwe resources maken wanneer er andere verschijnen.

"Elke namespace krijgt een default NetworkPolicy"

Dat is het hele mentale model. Valideren is waar bijna iedereen begint, dus daar beginnen wij ook.

Kyverno installeren

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

helm install kyverno kyverno/kyverno \
  --namespace kyverno \
  --create-namespace

Voor GitOps met ArgoCD:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: kyverno
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://kyverno.github.io/kyverno/
    chart: kyverno
    targetRevision: 3.1.4
    helm:
      values: |
        admissionController:
          replicas: 3
        backgroundController:
          replicas: 2
        cleanupController:
          replicas: 2
        reportsController:
          replicas: 2
  destination:
    server: https://kubernetes.default.svc
    namespace: kyverno
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

De simpelst mogelijke policy: vereis een label

Hier is het kleinste nuttige ding dat je kunt schrijven. Het vereist dat elke pod een team label draagt, en niets meer:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-labels
spec:
  validationFailureAction: Enforce
  rules:
    - name: require-team-label
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: "Het label 'team' is verplicht."
        pattern:
          metadata:
            labels:
              team: "?*"

Probeer een pod te maken zonder het label en de API server gooit de deur dicht:

kubectl run test --image=nginx
# Error: Het label 'team' is verplicht.

Dat is het. Eén CRD, en de conventie is nu dragend in plaats van decoratief. Merk op dat niemand iets hoefde te onthouden.

Enforce versus Audit: sloop prod niet op dag één

Voordat je verder gaat, de ene knop die je van jezelf redt. validationFailureAction heeft twee standen.

Enforce blokkeert niet-compliant resources direct. Dit is wat je uiteindelijk in productie wilt.

validationFailureAction: Enforce

Audit laat de resource door maar legt de overtreding vast in een report.

validationFailureAction: Audit

Deze heb ik op de harde manier geleerd. Rol een strenge policy uit in Enforce mode op een cluster dat je wiki maandenlang heeft genegeerd, en je blokkeert de helft van je deploys in één klap. Begin elke nieuwe policy in Audit, kijk wat er zou falen, fix de echte overtreders, en zet hem dan op Enforce. Graceful degradation geldt ook voor je eigen uitrol.

Laag 1: de validatie-policies die hun geld waard zijn

Een label-eis is een warming-up. Dit zijn de regels die een cluster echt gezond houden. Elk is nog steeds een enkele ClusterPolicy, alleen gericht op iets nuttigers.

Vereis resource limits

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-resource-limits
spec:
  validationFailureAction: Enforce
  rules:
    - name: require-limits
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: "CPU en memory limits zijn verplicht."
        pattern:
          spec:
            containers:
              - resources:
                  limits:
                    memory: "?*"
                    cpu: "?*"

Verbied privileged containers

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-privileged
spec:
  validationFailureAction: Enforce
  rules:
    - name: deny-privileged
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: "Privileged containers zijn niet toegestaan."
        pattern:
          spec:
            containers:
              - securityContext:
                  privileged: false

Verbied de latest tag

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-latest-tag
spec:
  validationFailureAction: Enforce
  rules:
    - name: require-image-tag
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: "Gebruik van ':latest' tag is niet toegestaan. Specificeer een versie tag."
        pattern:
          spec:
            containers:
              - image: "!*:latest"

Beperk image registries

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: restrict-registries
spec:
  validationFailureAction: Enforce
  rules:
    - name: allowed-registries
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: "Images moeten van goedgekeurde registries komen."
        pattern:
          spec:
            containers:
              - image: "registry.example.com/* | gcr.io/my-project/*"

Laag 2: mutatie, oftewel fixen in plaats van zeuren

Validatie is de uitsmijter die mensen wegstuurt. Mutatie is de gastheer die hun outfit stilletjes bij de deur bijwerkt. In plaats van een resource te weigeren omdat er een label of security-instelling ontbreekt, kan Kyverno het gewoon toevoegen. Minder friction voor wie deployt, dezelfde eindtoestand voor het cluster.

Voeg default labels toe

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-default-labels
spec:
  rules:
    - name: add-managed-by
      match:
        any:
          - resources:
              kinds:
                - Pod
                - Deployment
                - Service
      mutate:
        patchStrategicMerge:
          metadata:
            labels:
              managed-by: kyverno
              environment: "{{request.namespace}}"

Voeg een default security context toe

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-security-context
spec:
  rules:
    - name: add-run-as-nonroot
      match:
        any:
          - resources:
              kinds:
                - Pod
      mutate:
        patchStrategicMerge:
          spec:
            securityContext:
              runAsNonRoot: true
              seccompProfile:
                type: RuntimeDefault

Voeg image pull secrets toe

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-imagepullsecret
spec:
  rules:
    - name: add-registry-secret
      match:
        any:
          - resources:
              kinds:
                - Pod
      mutate:
        patchStrategicMerge:
          spec:
            imagePullSecrets:
              - name: registry-credentials

Laag 3: generatie, degene die mij verraste

Dit is het werkwoord dat ik het langst onderbenutte, en het is degene die veranderde hoe ik over cluster-defaults denk. Generatie maakt gloednieuwe resources aan als reactie op andere resources die verschijnen. Maak een namespace, en Kyverno kan er een NetworkPolicy en een ResourceQuota in droppen voordat er ook maar één pod gedeployd is.

Een default-deny NetworkPolicy in elke namespace

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: generate-default-networkpolicy
spec:
  rules:
    - name: generate-deny-all
      match:
        any:
          - resources:
              kinds:
                - Namespace
      exclude:
        any:
          - resources:
              namespaces:
                - kube-system
                - kyverno
      generate:
        apiVersion: networking.k8s.io/v1
        kind: NetworkPolicy
        name: default-deny
        namespace: "{{request.object.metadata.name}}"
        data:
          spec:
            podSelector: {}
            policyTypes:
              - Ingress
              - Egress

Elke nieuwe namespace begint dichtgetimmerd. Teams moeten expliciet het verkeer toestaan dat ze nodig hebben. Dat is zero trust als default in plaats van een checklist-item dat iemand vergeet, en het verkleint de blast radius wanneer er toch iets gehackt wordt.

Een default ResourceQuota per namespace

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: generate-resourcequota
spec:
  rules:
    - name: generate-quota
      match:
        any:
          - resources:
              kinds:
                - Namespace
      exclude:
        any:
          - resources:
              namespaces:
                - kube-system
                - kyverno
      generate:
        apiVersion: v1
        kind: ResourceQuota
        name: default-quota
        namespace: "{{request.object.metadata.name}}"
        data:
          spec:
            hard:
              requests.cpu: "4"
              requests.memory: 8Gi
              limits.cpu: "8"
              limits.memory: 16Gi
              persistentvolumeclaims: "10"

Variabelen: policies context-bewust maken

Met hardcoded waarden kom je ver, maar het goede spul komt uit het lezen van het request. Kyverno ondersteunt JMESPath expressies zodat een policy kan reageren op wie er deployt, in welke namespace, op welk moment:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-namespace-label
spec:
  rules:
    - name: add-ns-to-pods
      match:
        any:
          - resources:
              kinds:
                - Pod
      mutate:
        patchStrategicMerge:
          metadata:
            labels:
              namespace: "{{request.namespace}}"
              created-at: "{{time_now_utc()}}"

Nuttige variabelen:

  • {{request.namespace}} is de namespace van de resource
  • {{request.object.metadata.name}} is de naam van de resource
  • {{request.userInfo.username}} is de gebruiker die het request maakte
  • {{time_now_utc()}} is de huidige timestamp

De resources uitsluiten die je niet kunt aanraken

Eén valkuil waar iedereen in trapt: pas een strenge policy cluster-breed toe en je breekt kube-system of Kyverno zelf. Systeemcomponenten volgen jouw regels niet, en dat zouden ze ook niet moeten. Sluit ze expliciet uit:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-labels
spec:
  rules:
    - name: require-team-label
      match:
        any:
          - resources:
              kinds:
                - Pod
      exclude:
        any:
          - resources:
              namespaces:
                - kube-system
                - kube-public
                - kyverno
          - subjects:
              - kind: ServiceAccount
                name: system:*

Policy reports: de staat van het cluster zien

Afdwinging is de helft van de waarde. De andere helft is zichtbaarheid. Kyverno schrijft policy reports die precies tonen wat er slaagt en wat niet, wat betekent dat ik “is dit cluster nu compliant?” kan beantwoorden zonder iemands geheugen te vertrouwen:

kubectl get policyreport -A
kubectl get clusterpolicyreport

Voorbeeld report:

apiVersion: wgpolicyk8s.io/v1alpha2
kind: PolicyReport
metadata:
  name: polr-ns-default
  namespace: default
results:
  - message: "validation rule 'require-team-label' passed."
    policy: require-labels
    result: pass
    source: kyverno
  - message: "validation rule 'require-limits' failed."
    policy: require-resource-limits
    result: fail
    source: kyverno

Integreer met Prometheus voor alerting op policy violations:

# ServiceMonitor voor Kyverno metrics
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: kyverno
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: kyverno
  endpoints:
    - port: metrics

Schrijf niet alles zelf: curated policy sets

Je hoeft de security-baseline niet opnieuw uit te vinden. Kyverno onderhoudt curated policy-collecties, inclusief kant-en-klare implementaties van de Kubernetes Pod Security Standards:

# Installeer Pod Security Standards (Baseline)
kubectl apply -f https://raw.githubusercontent.com/kyverno/policies/main/pod-security/baseline/

# Installeer Pod Security Standards (Restricted)
kubectl apply -f https://raw.githubusercontent.com/kyverno/policies/main/pod-security/restricted/

Deze geven je de Baseline en Restricted Pod Security Standards als Kyverno policies, geen Rego, geen handwerk.

Het volledige plaatje: wat ik daadwerkelijk draai

Weet je die enkele label-policy van bovenaan nog? Hier eindigt hij. Dit is de kernset die ik op elk cluster zet dat ik bouw, allemaal in Enforce mode na een Audit-shakedown: readiness probes verplicht, geen deploys in default, resource requests verplicht.

# require-probes.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-probes
spec:
  validationFailureAction: Enforce
  rules:
    - name: require-readiness-probe
      match:
        any:
          - resources:
              kinds:
                - Deployment
      exclude:
        any:
          - resources:
              namespaces:
                - kube-system
                - kyverno
      validate:
        message: "Readiness probe is verplicht voor alle deployments."
        pattern:
          spec:
            template:
              spec:
                containers:
                  - readinessProbe:
                      "?*": "?*"
---
# disallow-default-namespace.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-default-namespace
spec:
  validationFailureAction: Enforce
  rules:
    - name: deny-default
      match:
        any:
          - resources:
              kinds:
                - Pod
                - Deployment
                - Service
              namespaces:
                - default
      validate:
        message: "Gebruik van de 'default' namespace is niet toegestaan."
        deny: {}
---
# require-requests.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-requests
spec:
  validationFailureAction: Enforce
  rules:
    - name: require-cpu-memory-requests
      match:
        any:
          - resources:
              kinds:
                - Pod
      exclude:
        any:
          - resources:
              namespaces:
                - kube-system
      validate:
        message: "CPU en memory requests zijn verplicht."
        pattern:
          spec:
            containers:
              - resources:
                  requests:
                    memory: "?*"
                    cpu: "?*"

Test policies voordat ze het cluster raken

Policies zijn code, dus ik behandel ze als code. De Kyverno CLI laat me een policy tegen een voorbeeld-resource checken op mijn laptop voordat er iets bij een echte API server komt:

# Installeer CLI
brew install kyverno

# Test policy tegen een resource
kyverno apply policy.yaml --resource pod.yaml

# Test alle policies in een directory
kyverno apply ./policies/ --resource ./manifests/

Maak een test suite:

# test/values.yaml
policies:
  - policy.yaml
resources:
  - resources/valid-pod.yaml
  - resources/invalid-pod.yaml
results:
  - policy: require-labels
    resource: valid-pod.yaml
    result: pass
  - policy: require-labels
    resource: invalid-pod.yaml
    result: fail

Draai tests:

kyverno test ./test/

Background scanning: vangen wat er al is

Admission control ziet alleen nieuwe requests. Maar je cluster zit vol pods die zijn aangemaakt voordat je hier iets van schreef. Zet background scanning aan en Kyverno audit de resources die al draaien:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: audit-existing
spec:
  validationFailureAction: Audit
  background: true  # Enable background scanning
  rules:
    - name: check-labels
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: "Label 'team' ontbreekt."
        pattern:
          metadata:
            labels:
              team: "?*"

Nu krijg je reports voor elke pod in het cluster, niet alleen die na jouw komst zijn aangemaakt. Het is het verschil tussen de deur bewaken en daadwerkelijk weten wat er in het gebouw staat.

Uitzonderingen, en waarom ze een beetje mogen prikken

Soms moet een workload echt een regel breken. node-exporter wil een privileged container, en dat is prima. Kyverno heeft er een first-class object voor zodat je de policy zelf niet hoeft te verwateren:

apiVersion: kyverno.io/v1
kind: PolicyException
metadata:
  name: allow-privileged-monitoring
  namespace: monitoring
spec:
  exceptions:
    - policyName: disallow-privileged
      ruleNames:
        - deny-privileged
  match:
    any:
      - resources:
          kinds:
            - Pod
          namespaces:
            - monitoring
          names:
            - node-exporter*

Schrijf op waarom elke uitzondering bestaat, het liefst in het manifest zelf. Een PolicyException is een benoemd, version-controlled gat in je verdediging, wat precies is hoe ik mijn gaten wil: zichtbaar. Als je merkt dat je deze maand een derde toevoegt, is de policy waarschijnlijk verkeerd, niet de workloads.

Vang violations bij merge, niet bij deploy

De beste plek om een fout manifest te weigeren is in een merge request, lang voordat het een cluster bereikt. Draai dezelfde CLI in je pipeline:

# .gitlab-ci.yml
validate-policies:
  stage: test
  image: ghcr.io/kyverno/kyverno-cli:latest
  script:
    - kyverno apply ./policies/ --resource ./k8s/
  rules:
    - if: $CI_MERGE_REQUEST_ID

Nu krijgt een developer de feedback in zijn MR-pipeline, samen met de rest van zijn tests, terwijl de wijziging nog goedkoop te fixen is.

Wat dit me oplevert

Ga terug naar die wiki-pagina waarmee ik begon. Dit veranderde toen de regels uit de doc en in Kyverno trokken.

Zonder policy engine leven standaarden in wiki’s die niemand leest, vangen reviews problemen inconsistent afhankelijk van wie er kijkt, en stapelt non-compliance zich stilletjes op tot er iets breekt. Met Kyverno zijn de standaarden code in Git, is afdwinging automatisch en direct, en duikt elke overtreding op in een report dat ik daadwerkelijk kan queryen.

Dit is low-friction governance. Het punt is dat de strengheid niemand aandacht kost. Ik vraag developers niet om een conventie-pagina uit hun hoofd te leren, en ik hoef het zelf niet te controleren. Het cluster onthoudt de regels zodat mijn hoofd dat niet hoeft, en dat is de hele reden dat ik systemen zo bouw.

De beste policies zijn degene waar je over ophoudt na te denken. De mijne draaien al maanden stilletjes, en het enige moment dat ik ze opmerk is wanneer er één een fout tegenhoudt die ik anders had geshipt.