What You'll Build
In this lab you will install Kyverno on your EKS cluster, manage it via the App of Apps GitOps pattern, and deploy a ClusterPolicy that requires Cosign signatures on all india-squad images admitted to the squad-prod namespace. You will test the policy in Audit mode to observe violations without blocking, promote it to Enforce mode, and verify that an unsigned image is rejected while a signed image from Exercise 20's pipeline is admitted.
You will also verify that the Kyverno policy is itself managed by GitOps: manually downgrading the policy to Audit mode via kubectl and watching ArgoCD's self-heal revert it back to Enforce within the sync interval, demonstrating that even admission control policies are protected against manual cluster overrides by the GitOps workflow, confirming that the GitOps workflow governs security policy as strictly as application code.
Prerequisites
- EKS cluster with ArgoCD from Module 2 Lab 14, with the App of Apps structure in the india-squad-gitops repository.
- The squad-prod namespace created in the cluster from the same lab.
- A signed container image from Exercise 20's DevSecOps pipeline—the image must have been signed by the MY_ORG/india-squad-helm build.yml workflow using Cosign.
- Helm 3 and the argocd CLI installed locally for the Kyverno installation and App of Apps GitOps integration steps.
- Cosign CLI installed locally for the final verification step that confirms the signature is valid outside of Kyverno's admission check.
Setup — Install Kyverno via Helm and GitOps
Install Kyverno directly with Helm first to get it running, then add it to the App of Apps GitOps structure so subsequent updates are managed via Git. The three-replica admission controller configuration ensures that Kyverno's webhook remains available during rolling updates and node failures—a single-replica Kyverno with `failurePolicy: Fail` on the validating webhook would cause cluster-wide admission failures if the Kyverno pod restarts.
# Prerequisites: EKS cluster with ArgoCD installed (from M2 Lab 14).
# We will install Kyverno and deploy admission policies via GitOps.
# 1. Install Kyverno on the EKS cluster using Helm
helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
helm upgrade --install kyverno kyverno/kyverno \
--namespace kyverno --create-namespace \
--set admissionController.replicas=3 \
--set backgroundController.replicas=2 \
--set reportsController.replicas=2 \
--wait
# Verify Kyverno is running
kubectl get pods -n kyverno
# Expected: kyverno-admission-controller-*, kyverno-background-controller-*
# kyverno-cleanup-controller-*, kyverno-reports-controller-*
# 2. Add Kyverno to the GitOps repository as an App of Apps child
# (using the ArgoCD structure from M2 Lab 14)
mkdir -p india-squad-gitops/apps
cat > india-squad-gitops/apps/kyverno.yaml << 'EOF'
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.x.x'
helm:
values: |
admissionController:
replicas: 3
destination:
server: https://kubernetes.default.svc
namespace: kyverno
syncPolicy:
automated: { prune: true, selfHeal: true }
syncOptions: [CreateNamespace=true]
EOF
cd india-squad-gitops && git add apps/kyverno.yaml
git commit -m 'feat: add Kyverno to App of Apps'
git pushStep 1 — Deploy Image Signing Policy via GitOps
Add the ClusterPolicy to the GitOps repository in Audit mode and create an ArgoCD Application that manages the policies directory. Starting in Audit mode is critical: it reveals how many existing pods in squad-prod are running unsigned images before the policy blocks new admissions, giving the team time to ensure the signed image pipeline is working before enforcement begins.
# Step 1: Create and deploy the image signing ClusterPolicy via GitOps.
# Add the policy to the GitOps repository
mkdir -p india-squad-gitops/policies
cat > india-squad-gitops/policies/require-image-signature.yaml << 'EOF'
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-india-squad-signature
annotations:
policies.kyverno.io/title: Require India Squad Image Signature
policies.kyverno.io/category: Supply Chain Security
spec:
validationFailureAction: Audit # start in Audit mode
background: true
rules:
- name: verify-cosign-signature
match:
any:
- resources:
kinds: [Pod]
namespaces: [squad-prod]
verifyImages:
- imageReferences:
- '123456789.dkr.ecr.ap-south-1.amazonaws.com/india-squad-api*'
attestors:
- entries:
- keyless:
subject: >-
https://github.com/MY_ORG/india-squad-helm/.github/workflows/devsecops.yml@refs/heads/main
issuer: 'https://token.actions.githubusercontent.com'
EOF
# Create an ArgoCD Application for the policies directory
cat > india-squad-gitops/apps/squad-policies.yaml << 'EOF'
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: squad-policies
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/MY_ORG/india-squad-gitops
targetRevision: main
path: policies/
destination:
server: https://kubernetes.default.svc
namespace: kyverno
syncPolicy:
automated: { prune: true, selfHeal: true }
EOF
cd india-squad-gitops
git add policies/ apps/squad-policies.yaml
git commit -m 'feat: add image signing policy in Audit mode'
git push
# Verify the policy was applied by ArgoCD
argocd app sync squad-policies
kubectl get clusterpolicy require-india-squad-signatureStep 2 — Test Audit Mode, Then Enforce
Deploy an unsigned nginx image to confirm it is admitted in Audit mode and that the violation is recorded in the PolicyReport. Check the PolicyReport resource to see the recorded violation. Then promote the policy to Enforce mode by updating the GitOps repository and pushing the change, which ArgoCD applies automatically. Verify the policy mode changed by querying the ClusterPolicy status.
# Step 2: Test Audit mode — deploy unsigned image and check the PolicyReport.
# Deploy an unsigned nginx image to squad-prod (should PASS in Audit mode)
kubectl apply -f - << 'EOF'
apiVersion: v1
kind: Pod
metadata:
name: unsigned-test
namespace: squad-prod
spec:
containers:
- name: nginx
image: nginx:latest
restartPolicy: Never
EOF
# The pod is created (Audit mode does not block)
kubectl get pod unsigned-test -n squad-prod
# Check the PolicyReport for the violation record
kubectl get policyreport -n squad-prod
kubectl describe policyreport -n squad-prod
# Expected: result: fail for unsigned-test / verify-cosign-signature
# Clean up the test pod
kubectl delete pod unsigned-test -n squad-prod
# Now promote the policy to Enforce mode
sed -i 's/validationFailureAction: Audit/validationFailureAction: Enforce/' \
india-squad-gitops/policies/require-image-signature.yaml
cd india-squad-gitops
git add policies/require-image-signature.yaml
git commit -m 'security: promote image signing policy to Enforce mode'
git push
argocd app sync squad-policies
kubectl get clusterpolicy require-india-squad-signature -o jsonpath='{.spec.validationFailureAction}'
# Expected: EnforceStep 3 — Verify Blocking and Admission
Verify the two outcomes that confirm the policy is working correctly: an unsigned image is blocked with a clear error message, and the signed image from Exercise 20 is admitted and its tag is mutated to a digest by Kyverno's `mutateDigest: true` option. The digest mutation is the supply chain security guarantee: even if someone replaces the image behind the tag after signing, the pod spec is permanently bound to the signed bytes.
# Step 3: Verify Enforce mode blocks unsigned images.
# Attempt to deploy the unsigned nginx image (should be REJECTED)
kubectl apply -f - << 'EOF'
apiVersion: v1
kind: Pod
metadata:
name: unsigned-test
namespace: squad-prod
spec:
containers:
- name: nginx
image: nginx:latest
restartPolicy: Never
EOF
# Expected error:
# Error from server: admission webhook 'mutate.kyverno.svc-fail' denied the request:
# resource Pod/squad-prod/unsigned-test was blocked due to the following policies:
# require-india-squad-signature:
# verify-cosign-signature: image nginx:latest not found in rules
# Now deploy the SIGNED image from the DevSecOps pipeline (should PASS)
# Replace <sha> with the actual commit SHA from the Exercise 20 pipeline
SIGNED_IMAGE='123456789.dkr.ecr.ap-south-1.amazonaws.com/india-squad-api:<sha>'
kubectl apply -f - << EOF
apiVersion: v1
kind: Pod
metadata:
name: signed-test
namespace: squad-prod
spec:
containers:
- name: api
image: $SIGNED_IMAGE
restartPolicy: Never
EOF
# Expected: pod/signed-test created
kubectl get pod signed-test -n squad-prod
# Note the image in the pod spec — Kyverno mutateDigest replaces the tag with digest
kubectl get pod signed-test -n squad-prod \
-o jsonpath='{.spec.containers[0].image}'
# Expected: .../india-squad-api@sha256:... (digest, not tag)Step 4 — End-to-End Verification
Run the complete verification suite: confirm the policy mode, confirm unsigned images are rejected, confirm the signed image's digest reference, verify the Cosign signature from the CLI, and perform the GitOps self-heal test by manually downgrading the policy and watching ArgoCD restore the Enforce mode. The self-heal test is the final proof that the supply chain enforcement is itself protected by the GitOps workflow.
# Step 4: Verify the complete supply chain enforcement chain.
# 1. Confirm the ClusterPolicy is Enforce and healthy
kubectl get clusterpolicy require-india-squad-signature \
-o jsonpath='{.spec.validationFailureAction}'
# Expected: Enforce
kubectl get clusterpolicy require-india-squad-signature \
-o jsonpath='{.status.conditions[0].status}'
# Expected: True (policy is ready)
# 2. Confirm unsigned pod is rejected
kubectl run unsigned-verify --image=python:3.12 -n squad-prod --restart=Never 2>&1 | grep 'blocked'
# Expected: 'was blocked due to the following policies'
# 3. Confirm signed pod is admitted and image is pinned to digest
kubectl get pod signed-test -n squad-prod \
-o jsonpath='{.spec.containers[0].image}' | grep 'sha256'
# Expected: contains '@sha256:' — Kyverno mutated the tag to digest
# 4. Verify the Cosign signature from the CLI
IMAGE=$(kubectl get pod signed-test -n squad-prod \
-o jsonpath='{.spec.containers[0].image}')
cosign verify \
--certificate-identity-regexp='https://github.com/MY_ORG/india-squad-helm' \
--certificate-oidc-issuer='https://token.actions.githubusercontent.com' \
$IMAGE
# Expected: Verification for ...: true
# 5. Confirm GitOps manages the policy (rollback test)
# Change Enforce back to Audit directly in kubectl:
kubectl patch clusterpolicy require-india-squad-signature \
--type=json -p='[{"op":"replace","path":"/spec/validationFailureAction","value":"Audit"}]'
kubectl get clusterpolicy require-india-squad-signature \
-o jsonpath='{.spec.validationFailureAction}'
# Expected: Audit (manual change applied)
# ArgoCD self-heal reverts it back to Enforce within the sync interval:
sleep 180 # wait for ArgoCD sync
kubectl get clusterpolicy require-india-squad-signature \
-o jsonpath='{.spec.validationFailureAction}'
# Expected: Enforce (ArgoCD reverted the manual change)Warning: Configure a namespace exclusion for the kyverno namespace itself in the verifyImages policy. If Kyverno's own pods need to restart and the verifyImages policy applies to all namespaces including kyverno, the Kyverno pods may fail to restart because their images are not signed with your organisation's Cosign identity. This creates a deadlock where Kyverno is unavailable but also preventing itself from restarting. Add `namespaces: [squad-prod, squad-staging]` to the match block and explicitly exclude system namespaces from all image verification policies.
Extension Challenge: Add a Kyverno PSS Baseline policy to the squad-prod namespace that prevents privileged containers, hostPath volumes, and host network access. Test it by attempting to deploy a pod with `securityContext.privileged: true` and confirming it is rejected. Then add a Kyverno mutating policy for the squad-staging namespace that automatically adds `runAsNonRoot: true` and `allowPrivilegeEscalation: false` to containers that omit these fields, and verify the mutation is applied by inspecting the pod spec after admission.