Container security audits get done at one of two depths. The shallow version scans images for CVEs, reports a four-digit count of “vulnerabilities,” and produces a finding list nobody reads. The deep version inspects the actual blast radius of a compromised container — who can the workload assume, what data can it reach, what does the cluster’s RBAC actually permit, and which detection signal would fire if any of it went wrong. The shallow version satisfies compliance; the deep version closes incidents. After running container audits as part of Kubernetes and container security engagements across many environments, a consistent picture emerges: the security-relevant findings concentrate in a few well-known categories, and a focused 50-check audit catches more than 95% of the real risk. This is the field-tested checklist — what to inspect, why it matters, what attackers exploit when each check fails, and how to sequence the fixes.
Why most container audits stop at CVE scanning
CVE scanning produces output. Output produces compliance documentation. Compliance documentation closes audit cycles. The combination has trained the industry to optimise for finding count rather than blast-radius reduction.
What CVE scanning actually tells you
A CVE scan tells you “this package version has a known vulnerability.” It does not tell you whether the vulnerable code path is reachable, whether the container has the privilege to do anything dangerous when the vulnerability is exploited, or whether your detection stack would catch the exploitation if it happened. Three containers with the same CVE list can have radically different real-world risk profiles depending on these three questions.
What container audits actually need to cover
The full picture requires inspecting eight layers, each with its own attack patterns:
- Image build pipeline — what gets into the image and how is it verified
- Registry and supply chain — who can push, who can pull, what is verified
- Runtime configuration — privileges, capabilities, resources, mounts
- Cluster security — control plane hardening, etcd, API server
- Identity, RBAC, and secrets — who can do what, how secrets are exposed
- Network policy — what can talk to what
- Observability and detection — would you see an attack
- Incident response readiness — could you respond to one
The 50 checks below distribute across these layers in roughly the proportions that matter — runtime config and RBAC dominate, build pipeline and registry follow, network and detection round it out.
How to use this checklist
Each check below has three components: what to verify, what attacker exploits when the check fails, and the specific remediation. The recommended audit sequence:
- Phase 1 (Day 1-3): checks marked Critical. Run through these against every cluster in scope. Document findings.
- Phase 2 (Week 1-2): checks marked High. Surface the patterns that show up across multiple workloads.
- Phase 3 (Week 2-4): checks marked Medium. Use these to build the longer-term hardening backlog.
- Phase 4 (Month 2): continuous controls that prevent recurrence.
Score each check as Pass, Fail, or N/A per workload/cluster. Aggregate to a heat map. Fix highest-blast-radius failures first.
Image build pipeline (Checks 1-10)
This is where supply-chain attacks land. Most production breaches involving containers begin with a malicious or vulnerable image entering the registry through a permissive build pipeline.
| # | Check | Severity | What attackers exploit when this fails |
|---|---|---|---|
| 1 | Base images pinned by digest, not tag (@sha256:... not :latest) | High | Image pull-time substitution; supply-chain compromise via tag re-pointing |
| 2 | Base images from a curated approved list, not arbitrary registries | High | Malicious image upload to public registries; typosquatting |
| 3 | Multi-stage builds used to strip build dependencies from final image | Medium | Larger attack surface; build tools available at runtime |
| 4 | No secrets in image layers (verified via layer inspection, not source) | Critical | Credential extraction from leaked images; old layer secret recovery |
| 5 | No interactive credentials in build args | High | CI log leak; build cache leak |
| 6 | Dockerfile uses non-root USER directive | High | Privilege escalation if container is compromised |
| 7 | Image signed via cosign / Notation / Sigstore | High | Image tampering between registry and cluster |
| 8 | SBOM generated and attached to image | Medium | Inability to respond to disclosed CVEs without manual inventory |
| 9 | Image scan integrated into CI with severity gate | High | Vulnerable images shipped to production |
| 10 | Build pipeline runs in isolated environment (not on developer machines) | Medium | Build-host compromise contaminates production images |
The single highest-leverage check in this category is #4 — secrets in image layers. Even when a developer removes a secret in a later layer, the secret remains in the earlier layer and can be extracted by anyone with image pull access. Common offenders: API keys baked into early COPY directives, SSH keys used during build, npm/pip credentials.
Verification command for the most common secret patterns:
docker save image:tag | tar -xO --wildcards '*/layer.tar' | \
tar -tO 2>/dev/null | grep -aE '(AWS|API_KEY|SECRET|PASSWORD|BEGIN.*PRIVATE)'
For CI integration of image signing and SBOM, cosign + syft is the current de facto stack:
syft my-registry/my-app:1.2.3 -o spdx-json > sbom.json
cosign sign --key cosign.key my-registry/my-app:1.2.3
cosign attest --predicate sbom.json --key cosign.key my-registry/my-app:1.2.3
Registry and supply chain (Checks 11-16)
The registry is the bridge between build and runtime. A compromised registry, or a registry with weak access control, defeats the security model that depends on trusting the images it serves.
| # | Check | Severity | What attackers exploit when this fails |
|---|---|---|---|
| 11 | Registry requires authentication for both push and pull | Critical | Anonymous image upload; supply-chain injection |
| 12 | Push credentials issued per-pipeline, not shared | High | Credential leak from one pipeline compromises all |
| 13 | Image pull policy enforced via admission controller (only signed images) | Critical | Unsigned or unauthorised images deployed |
| 14 | Vulnerability rescanning enabled on new CVE disclosures | High | Long-running images with newly-disclosed CVEs invisible |
| 15 | Image retention policy in place, old images removed | Low | Forgotten old images with credentials or vulnerabilities |
| 16 | Cross-region replication keys rotated and access-logged | Medium | Replication compromise; stale credential abuse |
Check #13 is the most underutilised registry control. Admission controllers like Kyverno or OPA Gatekeeper can enforce “only deploy images signed by our cosign key” — preventing both supply-chain attacks and accidental deployment of unintended images.
# Kyverno policy: require cosign-signed images
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-signed-images
spec:
validationFailureAction: enforce
rules:
- name: verify-signature
match:
resources:
kinds: [Pod]
verifyImages:
- imageReferences: ["my-registry/*"]
attestors:
- entries:
- keys:
publicKeys: |
-----BEGIN PUBLIC KEY-----
<cosign public key>
-----END PUBLIC KEY-----
Runtime configuration (Checks 17-26)
Runtime configuration determines what a compromised container can actually do. This is the densest category in the audit because the failures here turn small vulnerabilities into large incidents.
| # | Check | Severity | What attackers exploit when this fails |
|---|---|---|---|
| 17 | privileged: false on all production pods | Critical | Direct host compromise from container escape |
| 18 | runAsNonRoot: true enforced via Pod Security Admission | High | Container processes running as root; easier privilege escalation |
| 19 | allowPrivilegeEscalation: false on all production pods | High | Setuid binary escalation inside container |
| 20 | readOnlyRootFilesystem: true where workload supports it | Medium | Filesystem-based persistence; binary modification |
| 21 | Linux capabilities dropped to least required (drop ALL, add specific) | High | Capability abuse for network/filesystem operations |
| 22 | No mount of host paths (hostPath), or restricted to approved list | Critical | Host filesystem read/write from container |
| 23 | No hostNetwork, hostPID, hostIPC on production pods | Critical | Cluster-wide visibility from a single pod |
| 24 | Resource limits (CPU/memory) set on every container | Medium | DoS; noisy-neighbour; OOM-kill cascade |
| 25 | Liveness and readiness probes do not expose sensitive endpoints | Low | Information disclosure via probe responses |
| 26 | Pod Security Admission set to restricted or equivalent enforcement | High | Workloads can opt out of all the above with no central enforcement |
Pod Security Admission (PSA) is the central enforcer for most of this category. The restricted profile enforces #17, #18, #19, #20, #21, parts of #22, and #23 — but only if the namespace label is set. Audit every namespace for the PSA label:
kubectl get namespaces -L pod-security.kubernetes.io/enforce,pod-security.kubernetes.io/audit,pod-security.kubernetes.io/warn
Any production namespace without enforce=restricted (or baseline with documented justification) fails check #26.
For deeper context on the hardening config that backs these checks, see our Kubernetes security hardening for containers post.
Cluster security (Checks 27-36)
The cluster control plane is the highest-blast-radius surface. A compromise of the API server, etcd, or kubelet typically means cluster takeover.
| # | Check | Severity | What attackers exploit when this fails |
|---|---|---|---|
| 27 | API server bound to private network or restricted CIDR | Critical | Internet-exposed API server; brute-force kubeconfig recovery |
| 28 | API server audit logging enabled with appropriate policy | High | Lack of forensic evidence; missed detection signal |
| 29 | etcd encryption at rest enabled with KMS-backed key | Critical | Secret extraction from etcd snapshots |
| 30 | etcd access restricted to control plane nodes only | Critical | Direct etcd read/write bypasses API authorisation |
| 31 | Kubelet authentication/authorisation set to webhook mode | Critical | Anonymous kubelet API access; pod exec from outside |
| 32 | Kubelet port not exposed beyond node | High | Lateral movement via kubelet API |
| 33 | Control plane TLS certificates rotated automatically | Medium | Stale certificates; manual rotation drift |
| 34 | Anonymous auth disabled on API server | High | Probing API surface without credentials |
| 35 | API server TLS uses strong cipher suite, no TLS < 1.2 | Medium | Downgrade attacks; protocol-level weaknesses |
| 36 | CIS Kubernetes Benchmark compliance scan run and remediated | Medium | Systematic miss of well-known weak settings |
For managed Kubernetes (EKS, GKE, AKS), checks #29, #30, #31, #33, #34 are partially handled by the cloud provider — but only partially. Each provider has knobs the customer owns. EKS, for example, requires the customer to explicitly enable secrets encryption with KMS; the default cluster has no envelope encryption.
CIS Benchmark scanning is fastest via kube-bench:
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml
kubectl logs job/kube-bench
Identity, RBAC, and secrets (Checks 37-44)
The container’s identity surface — service accounts, RBAC roles, secrets — is what determines blast radius. Most container compromises become cluster compromises through identity misconfiguration. The patterns mirror the broader cloud identity problems documented in our AWS IAM audit walkthrough.
| # | Check | Severity | What attackers exploit when this fails |
|---|---|---|---|
| 37 | Default service account has no RBAC permissions | Critical | Pods using default SA have unintended cluster access |
| 38 | Service account tokens not automounted unless explicitly needed | High | Pod-to-API-server access from compromised workloads |
| 39 | ClusterRoleBindings to cluster-admin audited and minimised | Critical | Service account compromise = cluster admin |
| 40 | Service account-to-IAM mapping (IRSA/Workload Identity) scoped to least privilege | Critical | Pod-to-cloud-account lateral movement |
| 41 | Secrets stored in dedicated secret store (Vault/Secrets Manager), not as plain k8s secrets | High | Plain k8s secrets are base64-encoded, not encrypted, easily exfiltrated |
| 42 | Secrets injected via projected volumes or external secrets operator | High | Secrets in environment variables are visible in process listings and crash dumps |
| 43 | Image pull secrets per-namespace, not cluster-wide | Medium | Compromise of one workload exposes pull credentials for all images |
| 44 | Service account audit log review quarterly | Medium | Stale or unused service accounts retain dangerous permissions |
Check #40 is the highest-leverage cross-system control. A pod’s service account often maps to a cloud IAM role (via IRSA on EKS, Workload Identity on GKE, Pod Identity on AKS). Overly broad role permissions turn a single pod compromise into a cloud-account compromise.
A common production pattern showing the failure mode:
# Bad: pod's IRSA role has s3:* on *
apiVersion: v1
kind: ServiceAccount
metadata:
name: analytics-pod
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/analytics-pod-role
# analytics-pod-role policy:
# Action: s3:*
# Resource: "*"
The correct version scopes both the action and the resource:
# Good: scoped to specific S3 bucket and specific actions
# analytics-pod-role policy:
# Action: ["s3:GetObject", "s3:PutObject"]
# Resource: ["arn:aws:s3:::analytics-data-bucket/*"]
Network policy and segmentation (Checks 45-49)
Network policy is the layer that limits lateral movement within the cluster. Without it, a compromised pod can reach every other pod, every internal service, and often the cloud metadata endpoint.
| # | Check | Severity | What attackers exploit when this fails |
|---|---|---|---|
| 45 | Default-deny network policy in every production namespace | Critical | Unrestricted lateral movement on cluster compromise |
| 46 | Egress restricted (no arbitrary outbound from pods) | High | Data exfiltration; C2 callbacks; cryptocurrency mining |
| 47 | Cloud metadata endpoint blocked at network policy level (169.254.169.254) | Critical | IMDS abuse for credential theft (especially on EKS without IMDSv2 only) |
| 48 | Service mesh mTLS enabled if mesh is deployed | Medium | Cleartext inter-service traffic; spoofing |
| 49 | Network policies tested via automated policy verification tool | Medium | Policy drift; rules that don’t actually do what they claim |
Check #47 is a top-3 finding by frequency in production audits. The instance metadata endpoint accessible from pods, combined with IMDSv1 enabled on the node, allows credential theft via SSRF in any pod-hosted application — a single XSS payload becomes cloud credential theft. Block at the network policy level:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-imds
namespace: production
spec:
podSelector: {}
policyTypes: [Egress]
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except: [169.254.169.254/32]
Combined with enforcing IMDSv2-only at the node level, this closes one of the most common cloud-to-container escalation paths.
Observability and detection (Check 50)
The single most underutilised check, treated as one because the detection coverage need is unitary — either you’d see an attack on a container or you wouldn’t.
| # | Check | Severity | What attackers exploit when this fails |
|---|---|---|---|
| 50 | Runtime detection deployed (Falco, Tetragon, or equivalent) with alerts routed to SOC | High | Container compromise proceeds without detection; long dwell time |
Runtime detection at the container layer is meaningfully different from log-based detection. Falco watches kernel-level syscalls; Tetragon watches eBPF-level events. They catch behaviours like:
- Shell spawned inside a container that should never have an interactive shell
- Outbound connection to an unexpected IP from a workload pod
- Process executing from a writable filesystem location
- Sensitive file read (e.g.,
/etc/shadowfrom inside container) - Privileged operation by a container that has no business doing it
Without runtime detection, the dwell time between container compromise and discovery is typically measured in weeks. With it, the same compromise is caught in minutes.
For organisations layering on broader detection, the runtime telemetry plugs into managed SOC monitoring and continuous vulnerability management. The AWS-native detection layer documented in our Security Hub vs GuardDuty vs Inspector comparison covers complementary categories.
What to fix first — severity matrix
After running the 50 checks, the question is sequencing. The matrix that works in practice:
| Finding category | Fix priority | Why |
|---|---|---|
| Privileged containers in production (#17) | Day 1 | Direct host compromise risk |
| Host path mounts (#22) and host network (#23) | Day 1 | Cluster-wide blast radius |
| Cluster admin RBAC over-grants (#39) | Day 1-3 | Service account compromise = cluster takeover |
| IRSA / Workload Identity over-permissioned (#40) | Day 1-3 | Pod-to-cloud lateral movement |
| Metadata endpoint not blocked (#47) | Day 1-3 | SSRF-to-credential-theft pipeline |
| Default-deny network policy missing (#45) | Week 1 | Lateral movement enabler |
| Pod Security Admission not enforced (#26) | Week 1-2 | Central enforcer for half the runtime checks |
| Image signing not enforced (#13) | Week 2-3 | Supply-chain attack surface |
| Runtime detection not deployed (#50) | Week 2-4 | Closes the dwell-time gap |
| Secrets stored as plain k8s secrets (#41) | Week 3-4 | Easier than it sounds with External Secrets Operator |
| CIS Benchmark remediations (#36) | Month 2 | Long tail; lower individual blast radius |
| SBOM generation and retention policy (#8, #15) | Month 2-3 | Supports incident response when CVE disclosures land |
The pattern across mature container security programmes: the day-1 fixes drop risk by 50-60%; the week-1+ fixes drop it another 20-30%; the month-2+ fixes pick up the long tail. Front-load the high-blast-radius work.
Continuous controls after the audit
The 50 checks produce a point-in-time view. The view ages out fast — new workloads, new images, new RBAC bindings land every day. Sustaining the audit’s improvements requires continuous controls.
The minimum continuous controls
- Admission control via Kyverno or OPA Gatekeeper enforcing the most critical of the 50 checks (no privileged, no host mounts, signed images, scoped service accounts) as Deny rules
- Pod Security Admission set to
restrictedin production namespaces,baselineelsewhere - Image scan + signature verification gate in the CI pipeline, failing the build on critical findings
- Runtime detection (Falco/Tetragon) with alerts routed to a SIEM and triaged
- Quarterly RBAC review — list every cluster-admin and high-permission RoleBinding, justify or remove
- Quarterly cluster CIS benchmark scan with delta tracking against last quarter
- Periodic image inventory cleanup — registry retention, unused image removal
For broader context on how these controls fit into a complete cloud security program, the cloud security assessment service walks through the operational sequencing. Layered together, the controls turn the audit from an annual exercise into an ongoing programme that keeps the risk reduction durable.
The argument was never whether containers can be secured. They can be. The argument is whether the secured state survives contact with production engineering velocity — and the difference between programmes that hold the line and programmes that drift is whether the continuous controls listed above are actually running, not just documented. The 50 checks are the snapshot; the continuous controls are the system. Most teams that audit once and then drift end up with the same audit findings the next year, just with different workload names attached.