πŸ“šBook Signing at KubeCon EU 2026Meet us at Booking.com HQ (Mon 18:30-21:00) & vCluster booth #521 (Tue 24 Mar, 12:30-1:30pm) β€” free book giveaway!RSVP Booking.com Event
Security intermediate ⏱ 15 minutes K8s 1.28+

Kyverno Drift Prevention for GitOps

Prevent configuration drift in GitOps workflows using Kyverno: block manual kubectl edits, enforce ArgoCD/Flux ownership, and detect out-of-band changes

By Luca Berton β€’ β€’ πŸ“– 5 min read

πŸ’‘ Quick Answer: Use Kyverno to block direct kubectl edit/apply/patch on resources managed by ArgoCD or Flux, ensuring all changes flow through Git. Only the GitOps controller’s ServiceAccount is allowed to mutate protected resources.

The Problem

GitOps promises β€œGit is the single source of truth,” but:

  • Developers kubectl edit deployments directly (bypassing Git)
  • Emergency patches done manually are never committed back
  • ArgoCD shows β€œSynced” but actual state differs from desired
  • Drift causes outages when ArgoCD re-syncs and reverts changes
  • Compliance requires proof that all changes went through approval

The Solution

Block Manual Edits on GitOps-Managed Resources

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: prevent-gitops-drift
spec:
  validationFailureAction: Enforce
  background: false
  rules:
    - name: block-manual-updates
      match:
        any:
          - resources:
              kinds:
                - Deployment
                - StatefulSet
                - Service
                - ConfigMap
                - Secret
              namespaceSelector:
                matchLabels:
                  gitops/managed: "true"
              selector:
                matchExpressions:
                  - key: app.kubernetes.io/managed-by
                    operator: In
                    values:
                      - argocd
                      - Helm
      exclude:
        any:
          - subjects:
              - kind: ServiceAccount
                name: argocd-application-controller
                namespace: argocd
              - kind: ServiceAccount
                name: argocd-server
                namespace: argocd
          - clusterRoles:
              - system:kube-controller-manager
              - system:kube-scheduler
      validate:
        message: |
          This resource is managed by GitOps (ArgoCD).
          Direct modifications are not allowed.
          Please commit your change to Git and let ArgoCD sync it.
          
          Resource: {{ request.object.kind }}/{{ request.object.metadata.name }}
          User: {{ request.userInfo.username }}
          
          GitOps repo: Check ArgoCD Application for source URL.
        deny: {}

Allow Emergency Break-Glass

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: gitops-break-glass
spec:
  validationFailureAction: Enforce
  rules:
    - name: allow-emergency-with-annotation
      match:
        any:
          - resources:
              kinds:
                - Deployment
                - StatefulSet
              annotations:
                gitops/emergency-override: "true"
      exclude:
        any:
          - subjects:
              - kind: Group
                name: platform-admins
      validate:
        message: "Only platform-admins can use emergency override"
        deny: {}

    - name: expire-emergency-override
      match:
        any:
          - resources:
              kinds:
                - Deployment
                - StatefulSet
              annotations:
                gitops/emergency-override: "true"
      validate:
        cel:
          expressions:
            - expression: |
                has(object.metadata.annotations['gitops/emergency-expires']) &&
                timestamp(object.metadata.annotations['gitops/emergency-expires']) > now
              message: "Emergency override must have a valid, non-expired timestamp"

Detect Drift via Background Scanning

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: detect-label-drift
spec:
  validationFailureAction: Audit
  background: true
  rules:
    - name: required-labels-present
      match:
        any:
          - resources:
              kinds:
                - Deployment
                - StatefulSet
              namespaceSelector:
                matchLabels:
                  gitops/managed: "true"
      validate:
        cel:
          expressions:
            - expression: |
                has(object.metadata.labels) &&
                'app.kubernetes.io/version' in object.metadata.labels &&
                'app.kubernetes.io/managed-by' in object.metadata.labels
              message: "GitOps-managed resources must retain standard labels"

Protect Specific Fields (Allow Scaling, Block Image Change)

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: protect-critical-fields
spec:
  validationFailureAction: Enforce
  background: false
  rules:
    - name: block-image-change
      match:
        any:
          - resources:
              kinds:
                - Deployment
              operations:
                - UPDATE
              namespaceSelector:
                matchLabels:
                  gitops/managed: "true"
      exclude:
        any:
          - subjects:
              - kind: ServiceAccount
                name: argocd-application-controller
                namespace: argocd
      preconditions:
        any:
          - key: "{{ request.object.spec.template.spec.containers[0].image }}"
            operator: NotEquals
            value: "{{ request.oldObject.spec.template.spec.containers[0].image }}"
      validate:
        message: "Image changes must go through GitOps pipeline"
        deny: {}

Flux-Specific Drift Prevention

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: prevent-flux-drift
spec:
  validationFailureAction: Enforce
  rules:
    - name: block-manual-flux-resources
      match:
        any:
          - resources:
              kinds:
                - Deployment
                - StatefulSet
              annotations:
                kustomize.toolkit.fluxcd.io/name: "?*"
      exclude:
        any:
          - subjects:
              - kind: ServiceAccount
                name: kustomize-controller
                namespace: flux-system
              - kind: ServiceAccount
                name: helm-controller
                namespace: flux-system
      validate:
        message: "This resource is managed by Flux. Commit changes to Git."
        deny: {}

Common Issues

Kyverno blocks ArgoCD sync

  • Cause: ArgoCD ServiceAccount not in exclude list
  • Fix: Add exact ServiceAccount name + namespace to exclude

HPA can’t scale deployments

  • Cause: Policy blocks all updates including HPA controller
  • Fix: Exclude system:kube-controller-manager or HPA ServiceAccount

Developers can’t debug in staging

  • Cause: Policy too broad β€” blocks even read-like operations
  • Fix: Only match UPDATE/PATCH operations; allow GET/LIST

Best Practices

  1. Label namespaces gitops/managed: true β€” opt-in, not global
  2. Exclude system controllers β€” HPA, VPA, kube-controller-manager
  3. Break-glass with audit trail β€” emergency annotation + expiry
  4. Audit mode first β€” discover who’s making manual changes
  5. Clear error messages β€” tell user WHERE to commit instead
  6. Protect images, allow replicas β€” not all drift is equal

Key Takeaways

  • Kyverno blocks manual kubectl mutations on GitOps-managed resources
  • Only GitOps controller ServiceAccounts are excluded from the deny rule
  • Break-glass: emergency annotation with expiry timestamp for incidents
  • Background scanning detects label/config drift even without admission
  • Flux and ArgoCD have different ServiceAccount patterns β€” configure both
  • HPA/VPA scaling must be explicitly allowed (exclude their ServiceAccounts)
  • Clear deny messages guide developers to the correct GitOps workflow
#kyverno #gitops #argocd #drift-detection #policy
Luca Berton
Written by Luca Berton

Principal Solutions Architect specializing in Kubernetes, AI/GPU infrastructure, and cloud-native platforms. Author of Kubernetes Recipes and creator of CopyPasteLearn courses.

Kubernetes Recipes book cover

Want More Kubernetes Recipes?

This recipe is from Kubernetes Recipes, our 750-page practical guide with hundreds of production-ready patterns.

Luca Berton Ansible Pilot Ansible by Example Open Empower K8s Recipes Terraform Pilot CopyPasteLearn ProteinLens