Policy Management on EKS

Pod Security Admission covers pod hardening. Everything else an organisation wants to require — images only from its own registry, resource requests on every container, no latest tags, a cost-centre label — needs a policy engine that hooks into admission control.

The options

EngineLanguageRuns whereValidateMutateGenerateBackground scan
ValidatingAdmissionPolicyCELInside the API serverYesNoNoNo
KyvernoYAML + CELWebhook pods in the clusterYesYesYesYes
OPA GatekeeperRegoWebhook pods in the clusterYesYesNoYes (audit)
  • ValidatingAdmissionPolicy (VAP) is built into Kubernetes. No webhook, no pods, no extra latency, and it cannot be taken down by a node failure. It only validates, and CEL cannot call out to external data.
  • Kyverno writes policies as Kubernetes resources, which most teams find easiest. It can also add defaults, create companion resources (a default NetworkPolicy for every new namespace) and verify image signatures.
  • OPA Gatekeeper is the choice when policy is already written in Rego or shared with systems outside Kubernetes, such as Terraform plan checks.

A reasonable default on EKS: VAP for the handful of rules that must never be bypassed, Kyverno for everything that needs mutation, generation or reporting.

A built-in policy with no moving parts

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: require-ecr-images
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
  validations:
    - expression: >-
        object.spec.containers.all(c,
          c.image.startsWith('111122223333.dkr.ecr.eu-west-1.amazonaws.com/'))
      message: "Images must come from the organisation's ECR registry."
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: require-ecr-images
spec:
  policyName: require-ecr-images
  validationActions: ["Deny"]
  matchResources:
    namespaceSelector:
      matchExpressions:
        - key: kubernetes.io/metadata.name
          operator: NotIn
          values: ["kube-system"]

Start a new policy with validationActions: ["Warn", "Audit"] and switch to Deny once the audit log is clean.

The same idea in Kyverno, with a mutation

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-requests
spec:
  validationFailureAction: Audit
  background: true
  rules:
    - name: containers-must-set-requests
      match:
        any:
          - resources:
              kinds: ["Pod"]
      exclude:
        any:
          - resources:
              namespaces: ["kube-system", "kyverno"]
      validate:
        message: "CPU and memory requests are required."
        pattern:
          spec:
            containers:
              - resources:
                  requests:
                    cpu: "?*"
                    memory: "?*"

background: true makes Kyverno evaluate resources that already exist and write PolicyReport objects, so you can measure compliance before enforcing.

EKS-specific failure modes

Webhook-based engines sit in the request path of the API server. On EKS the control plane is in an AWS-managed VPC and reaches your webhook pods through ENIs in your subnets, which creates failure modes that do not exist on a self-managed control plane.

SymptomCauseFix
Every kubectl apply times out with context deadline exceededThe control plane cannot reach the webhook: node security group blocks the webhook port from the cluster security groupAllow the webhook’s target port (often 9443 or 8443) from the control plane
Cluster cannot recover after all nodes were replacedWebhook has failurePolicy: Fail and matches the pods needed to bring the webhook backExclude kube-system and the engine’s own namespace; run 3 replicas across zones
Webhook unreachable with a custom CNIPods have overlay IPs the control plane cannot route toRun the webhook with hostNetwork: true
Node scale-up stallsWebhook pods were evicted and nothing can be admittedPDB, priority class, and keep the engine off Spot

Rules of thumb: always exclude kube-system; give the engine a high priorityClassName; set webhook timeouts low (a few seconds); and think hard before using failurePolicy: Fail on a rule that matches every pod.

A starter policy set

PolicyMode to startEngine
Images only from approved registriesEnforceVAP
No :latest tag; digest pinning for productionAuditKyverno
CPU and memory requests requiredAuditKyverno
Required labels: app, team, cost-centreAuditKyverno
Block LoadBalancer Services without the internal annotationEnforceVAP
Default-deny NetworkPolicy generated for each new namespaceGenerateKyverno
Namespaces must carry Pod Security labelsEnforceVAP
Verify image signatures (cosign / AWS Signer)AuditKyverno

Keep policies in Git and test them in CI with the engine’s CLI (kyverno test, gator test) so a broken policy is caught before it reaches a cluster.

Across the wiki