The problem: a cluster trusts whatever it's given
- Without admission controls, a cluster runs any workload it's given, including:
- privileged pods, which can escape the container and take over the node
- images no one has scanned, carrying known vulnerabilities into production
- Without workload identity, every pod on a node shares that node's cloud permissions. Compromise one pod and you get everything the node can do in AWS.
- Pipelines and clusters are often secured separately, so a workload that slips past one check meets nothing else.
The approach: two checkpoints and a scoped identity
Build → Image scan (Trivy / Aqua) → Admission policy (Kyverno) → Namespace + RBAC
→ Running pod with its own IAM role (IRSA)
- In the pipeline: catch vulnerable images before they're pushed or deployed.
- At admission: reject insecure pod specs no matter how they reach the cluster.
- At runtime: each pod gets only its own scoped cloud permissions.
What I built
1. Scanning in the pipeline
Trivy and Aqua image scanning built into CI/CD, alongside SAST/DAST, so vulnerable images fail the build.
Illustrative: fail the pipeline on high or critical vulnerabilities.
trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed \
registry.example.com/payments-api:${BUILD_TAG}
2. Kyverno admission policies
Kyverno policies that block privileged pods, alongside Kubernetes RBAC and namespace isolation.
Typical additions to a policy library like this are running as non-root, blocking host networking and host paths, and allowing images only from approved registries.
Illustrative: Kyverno's standard "disallow privileged containers" policy.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-privileged-containers
spec:
validationFailureAction: Enforce # start with Audit, switch to Enforce once clean
background: true
rules:
- name: privileged-containers
match:
any:
- resources:
kinds: [Pod]
validate:
message: "Privileged containers are not allowed."
pattern:
spec:
=(ephemeralContainers):
- =(securityContext):
=(privileged): "false"
=(initContainers):
- =(securityContext):
=(privileged): "false"
containers:
- =(securityContext):
=(privileged): "false"
3. Pod-level IAM with IRSA
EKS OIDC / IRSA gives each workload its own scoped IAM role across 8+ clusters, instead of shared node-level credentials.
Illustrative: an IRSA trust policy that only one service account can use.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::111122223333:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:sub": "system:serviceaccount:payments:payments-api",
"oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:aud": "sts.amazonaws.com"
}
}
}]
}
4. Posture across clusters
Wiz CSPM with EKS scanning enabled through cross-account roles, for visibility into what's actually running.
How it works
- Kyverno is a Kubernetes admission controller with policies written in YAML. It can validate, mutate and generate resources, and verify image signatures.
- Pod Security Policies were removed in Kubernetes 1.25. The built-in replacement is Pod Security Admission, with three levels (privileged / baseline / restricted). Kyverno adds finer-grained, custom control on top.
- IRSA links a Kubernetes service account to an IAM role through the cluster's OIDC provider. The pod receives short-lived credentials for its own role only. EKS Pod Identity is the newer alternative with simpler setup.
Things to watch
- Roll out in
Audit, thenEnforce. Switching straight to Enforce on a busy cluster blocks existing workloads on their next restart. Fix the violations Audit reports first. - Handle exceptions explicitly. Use
PolicyExceptionresources for genuine exceptions (some system components need privileges) rather than weakening the policy for everyone. - Newer Kyverno versions move the failure action to the rule level (
failureAction). Check the syntax for your version. - Pin the IRSA
subcondition exactly. A wildcard or missingsubcondition lets any service account in the cluster assume the role. - Scanning thresholds need an owner.
--ignore-unfixedand severity filters keep builds moving, but someone must track the vulnerabilities they let through.
What it solves
- Insecure workloads are rejected before they run, at two independent checkpoints.
- A compromised pod gets only its own permissions, not the node's.
- Consistent rules across clusters, applied by policy instead of by reviewer memory.
What it doesn't solve
- Vulnerabilities disclosed after deployment. A clean scan at build time can go stale. Running images need rescanning.
- Runtime behaviour. Admission control checks the pod spec, not what the container does once running. That's runtime detection, a separate layer.
- Over-permissioned roles. IRSA scopes credentials to the pod; the role itself still needs least-privilege design.
- Workloads admitted before the policies existed. Background scans report them, but they keep running until they're fixed or redeployed.
Checklist
- [ ] Image scanning fails the build on high and critical vulnerabilities
- [ ] Admission policies block privileged pods (and other agreed risks)
- [ ] Policies rolled out in Audit before Enforce
- [ ] Exceptions handled with explicit, reviewed exception resources
- [ ] RBAC and namespace isolation per workload or team
- [ ] Each workload has its own IAM role, with an exact
subcondition - [ ] Running images rescanned for newly disclosed vulnerabilities
Further reading
- Kyverno documentation: https://kyverno.io/docs/
- Kyverno policy library: https://kyverno.io/policies/
- Kubernetes Pod Security Standards: https://kubernetes.io/docs/concepts/security/pod-security-standards/
- IAM roles for service accounts (IRSA): https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html
- Trivy: https://github.com/aquasecurity/trivy