Static Analysis of Kubernetes Manifests
Overview
Static analysis examines Kubernetes resource manifests before they are applied to the cluster, catching security misconfigurations early in the development lifecycle. Unlike image scanning (which looks for known CVEs in packages), static analysis evaluates the configuration of your workloads -- security contexts, privilege escalation, resource limits, and more.
CKS Exam Relevance
kubesec is the primary static analysis tool tested on the CKS exam. You should know how to scan manifests, interpret the scoring output, and identify which recommendations to implement. Understanding of other tools like kube-score and checkov provides additional context.
Static Analysis in the Pipeline
kubesec
kubesec is a risk analysis tool for Kubernetes resources. It provides a numerical score based on security best practices and gives actionable recommendations.
Installation
# Download the binary
curl -sSL https://github.com/controlplaneio/kubesec/releases/download/v2.14.0/kubesec_linux_amd64.tar.gz | \
tar xz -C /usr/local/bin kubesec
# Verify installation
kubesec version
# Alternative: run as a Docker container
docker run -i kubesec/kubesec:v2.14.0 scan /dev/stdin < pod.yamlScanning a Pod Manifest
Create a sample Pod manifest to analyze:
# insecure-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: insecure-app
spec:
containers:
- name: app
image: nginx
securityContext:
privileged: true
runAsUser: 0
ports:
- containerPort: 80Scan it with kubesec:
kubesec scan insecure-pod.yamlSample Output:
[
{
"object": "Pod/insecure-app.default",
"valid": true,
"fileName": "insecure-pod.yaml",
"message": "Failed with a score of -30 points",
"score": -30,
"scoring": {
"critical": [
{
"id": "Privileged",
"selector": "containers[] .securityContext .privileged == true",
"reason": "Privileged containers can allow almost completely unrestricted host access",
"points": -30
}
],
"advise": [
{
"id": "ApparmorAny",
"selector": ".metadata .annotations .\"container.apparmor.security.beta.kubernetes.io/app\"",
"reason": "Well defined AppArmor policies may provide greater protection from unknown threats. WARNING: NOT PRODUCTION READY",
"points": 3
},
{
"id": "ServiceAccountName",
"selector": ".spec .serviceAccountName",
"reason": "Service accounts restrict Kubernetes API access and should be configured with least privilege",
"points": 3
},
{
"id": "SeccompAny",
"selector": ".metadata .annotations .\"seccomp.security.alpha.kubernetes.io/pod\"",
"reason": "Seccomp profiles set minimum privilege and secure against unknown threats",
"points": 1
},
{
"id": "LimitsCPU",
"selector": "containers[] .resources .limits .cpu",
"reason": "Enforcing CPU limits prevents DOS via resource exhaustion",
"points": 1
},
{
"id": "LimitsMemory",
"selector": "containers[] .resources .limits .memory",
"reason": "Enforcing memory limits prevents DOS via resource exhaustion",
"points": 1
},
{
"id": "RequestsCPU",
"selector": "containers[] .resources .requests .cpu",
"reason": "Enforcing CPU requests aids a]](proper scheduling and resource management",
"points": 1
},
{
"id": "RequestsMemory",
"selector": "containers[] .resources .requests .memory",
"reason": "Enforcing memory requests aids proper scheduling and resource management",
"points": 1
},
{
"id": "ReadOnlyRootFilesystem",
"selector": "containers[] .securityContext .readOnlyRootFilesystem == true",
"reason": "An immutable root filesystem prevents applications from writing to their local disk",
"points": 1
},
{
"id": "RunAsNonRoot",
"selector": "containers[] .securityContext .runAsNonRoot == true",
"reason": "Force the running image to run as a non-root user to ensure least privilege",
"points": 1
},
{
"id": "RunAsUser",
"selector": "containers[] .securityContext .runAsUser -gt 10000",
"reason": "Run as a high-UID user to avoid conflicts with the host's user table",
"points": 1
}
]
}
}
]Understanding kubesec Scoring
| Score Range | Meaning | Action |
|---|---|---|
| Positive (> 0) | Good security posture | Continue improving |
| Zero (0) | Baseline -- no security controls added | Add recommended controls |
| Negative (< 0) | Critical security issues present | Must fix before deploying |
Scoring elements:
| Category | Description | Points |
|---|---|---|
critical | Severe security issues | Negative (e.g., -30) |
advise | Recommended improvements | Positive (e.g., +1 to +3) |
passed | Checks that are satisfied | Informational |
Exam Tip
On the CKS exam, if asked to "use kubesec to identify security issues," focus on the critical section first -- these are the blocking issues. The advise section shows improvements you should make.
Scanning a Secure Pod
# secure-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
serviceAccountName: app-sa
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: registry.company.com/app@sha256:abc123def456
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
ports:
- containerPort: 8080kubesec scan secure-pod.yamlSample Output:
[
{
"object": "Pod/secure-app.default",
"valid": true,
"fileName": "secure-pod.yaml",
"message": "Passed with a score of 12 points",
"score": 12,
"scoring": {
"passed": [
{
"id": "ReadOnlyRootFilesystem",
"selector": "containers[] .securityContext .readOnlyRootFilesystem == true",
"reason": "An immutable root filesystem prevents applications from writing to their local disk",
"points": 1
},
{
"id": "RunAsNonRoot",
"selector": "containers[] .securityContext .runAsNonRoot == true",
"reason": "Force the running image to run as a non-root user to ensure least privilege",
"points": 1
},
{
"id": "RunAsUser",
"selector": "containers[] .securityContext .runAsUser -gt 10000",
"reason": "Run as a high-UID user to avoid conflicts with the host's user table",
"points": 1
},
{
"id": "LimitsCPU",
"selector": "containers[] .resources .limits .cpu",
"reason": "Enforcing CPU limits prevents DOS via resource exhaustion",
"points": 1
},
{
"id": "ServiceAccountName",
"selector": ".spec .serviceAccountName",
"reason": "Service accounts restrict Kubernetes API access and should be configured with least privilege",
"points": 3
}
],
"advise": [
{
"id": "ApparmorAny",
"selector": ".metadata .annotations .\"container.apparmor.security.beta.kubernetes.io/app\"",
"reason": "Well defined AppArmor policies may provide greater protection from unknown threats",
"points": 3
}
]
}
}
]Using kubesec via API
kubesec also offers a hosted scanning API:
# Scan using the public API
curl -sSX POST --data-binary @pod.yaml https://v2.kubesec.io/scan
# Scan from stdin
cat deployment.yaml | curl -sSX POST --data-binary @- https://v2.kubesec.io/scanExam Note
On the CKS exam, you will likely use the local kubesec scan command rather than the API. However, the API is useful for quick testing in practice environments without installing the binary.
Scanning a Deployment
kubesec can scan Deployments, not just Pods:
# insecure-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: nginx:latest
securityContext:
runAsUser: 0
ports:
- containerPort: 80kubesec scan insecure-deployment.yamlCommon Security Issues kubesec Catches
| Issue | kubesec ID | Points | Remediation |
|---|---|---|---|
| Privileged container | Privileged | -30 | Set privileged: false |
| Running as root | RunAsUser | +1 (advise) | Set runAsUser: > 10000 |
| No read-only root FS | ReadOnlyRootFilesystem | +1 (advise) | Set readOnlyRootFilesystem: true |
| No run-as-non-root | RunAsNonRoot | +1 (advise) | Set runAsNonRoot: true |
| No resource limits | LimitsCPU, LimitsMemory | +1 each (advise) | Add resource limits |
| No service account | ServiceAccountName | +3 (advise) | Set serviceAccountName |
| No AppArmor profile | ApparmorAny | +3 (advise) | Add AppArmor annotation |
| No seccomp profile | SeccompAny | +1 (advise) | Add seccomp annotation/field |
| Host network enabled | HostNetwork | -9 | Remove hostNetwork: true |
| Host PID enabled | HostPID | -9 | Remove hostPID: true |
| Allow privilege escalation | AllowPrivilegeEscalation | +1 (advise) | Set allowPrivilegeEscalation: false |
| Capability SYS_ADMIN | CapSysAdmin | -30 | Drop unnecessary capabilities |
Other Static Analysis Tools
kube-score
kube-score performs static analysis on Kubernetes object definitions and provides human-readable recommendations.
# Install
kubectl krew install score
# Or download binary directly
# Scan a manifest
kube-score score deployment.yamlSample Output:
apps/v1/Deployment web-app
[CRITICAL] Container Security Context ReadOnlyRootFilesystem
· web -> The container running as root, which is not recommended.
Set readOnlyRootFilesystem to true
[CRITICAL] Container Security Context User Group ID
· web -> The container is running with a low user ID (0).
A user ID below 10000 is not recommended
[WARNING] Container Resources
· web -> CPU limit is not set
· web -> Memory limit is not set
[WARNING] Container Image Tag
· web -> Image with latest tag
Using a fixed tag is recommended to avoid accidental upgradesCheckov
Checkov is a comprehensive static analysis tool that supports Kubernetes, Terraform, CloudFormation, and more.
# Install
pip install checkov
# Scan Kubernetes manifests
checkov -d /path/to/k8s-manifests/ --framework kubernetes
# Scan a single file
checkov -f deployment.yaml --framework kubernetesSample Output:
Passed checks: 12, Failed checks: 8, Skipped checks: 0
Check: CKV_K8S_1: "Do not admit containers wishing to share the host process ID namespace"
PASSED for resource: Deployment.default.web-app
Check: CKV_K8S_9: "Do not allow privilege escalation"
FAILED for resource: Deployment.default.web-app
Guide: https://docs.bridgecrew.io/docs/ensure-containers-do-not-allow-privilege-escalation
Check: CKV_K8S_20: "Do not use the default namespace"
FAILED for resource: Deployment.default.web-app
Check: CKV_K8S_22: "Use read-only filesystem for containers where possible"
FAILED for resource: Deployment.default.web-app
Check: CKV_K8S_28: "Do not allow containers to run with capabilities"
FAILED for resource: Deployment.default.web-app
Check: CKV_K8S_37: "Ensure that the --client-cert-auth argument is set to true"
PASSED for resource: Deployment.default.web-appConftest
Conftest is an OPA-based testing tool for configuration files. It lets you write custom policies in Rego.
# Install
brew install conftest
# Or download binary
# Write a custom policy
mkdir -p policy# policy/kubernetes.rego
package main
deny[msg] {
input.kind == "Deployment"
container := input.spec.template.spec.containers[_]
container.securityContext.privileged == true
msg := sprintf("Container '%s' in Deployment '%s' must not be privileged", [container.name, input.metadata.name])
}
deny[msg] {
input.kind == "Deployment"
container := input.spec.template.spec.containers[_]
endswith(container.image, ":latest")
msg := sprintf("Container '%s' in Deployment '%s' must not use ':latest' tag", [container.name, input.metadata.name])
}
deny[msg] {
input.kind == "Deployment"
container := input.spec.template.spec.containers[_]
not container.resources.limits
msg := sprintf("Container '%s' in Deployment '%s' must define resource limits", [container.name, input.metadata.name])
}# Test the manifest against policies
conftest test deployment.yamlSample Output:
FAIL - deployment.yaml - main - Container 'web' in Deployment 'web-app' must not use ':latest' tag
FAIL - deployment.yaml - main - Container 'web' in Deployment 'web-app' must define resource limits
2 tests, 0 passed, 0 warnings, 2 failuresTool Comparison
| Feature | kubesec | kube-score | Checkov | Conftest |
|---|---|---|---|---|
| CKS Exam Focus | High | Low | Low | Low |
| Scoring System | Numerical | Pass/Fail/Warning | Pass/Fail | Pass/Fail |
| Custom Policies | No | No | Yes (Python) | Yes (Rego) |
| Output Format | JSON | Text/JSON | Text/JSON/SARIF | Text/JSON |
| Kubernetes Specific | Yes | Yes | Multi-framework | Multi-format |
| CI/CD Integration | Good | Good | Excellent | Good |
| Installation | Binary/Docker | Binary/kubectl plugin | pip | Binary/brew |
Practical Remediation Guide
When kubesec identifies issues, here is how to fix the most common ones:
Before: Insecure Manifest
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: nginx:latest
securityContext:
privileged: truekubesec score: -30
After: Remediated Manifest
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
serviceAccountName: app-service-account
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: registry.company.com/nginx@sha256:abc123
securityContext:
privileged: false
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mikubesec score: 12+
Changes made:
- Removed
privileged: true(+30 points recovery) - Added
runAsNonRoot: trueandrunAsUser: > 10000 - Added
readOnlyRootFilesystem: true - Added
allowPrivilegeEscalation: false - Dropped all capabilities
- Added resource requests and limits
- Added a dedicated service account
- Set seccomp profile to RuntimeDefault
- Pinned image to digest instead of
:latesttag - Disabled service account token auto-mounting
Key Takeaways
Summary
- kubesec is the primary CKS exam tool for static analysis -- know the command and scoring system
- A negative score means critical security issues are present (e.g., privileged containers)
- The
criticalsection in kubesec output shows blocking issues that must be fixed - The
advisesection shows recommended improvements for better security posture - kube-score and checkov provide additional static analysis capabilities
- Conftest allows writing custom OPA/Rego policies for manifest validation
- Static analysis should happen before deployment -- ideally in CI/CD pipelines
Common Exam Pitfalls
- Not recognizing that a negative kubesec score means critical issues exist
- Forgetting that
privileged: trueresults in a score of -30 - Confusing kubesec (manifest analysis) with Trivy (image vulnerability scanning)
- Not knowing the
kubesec scan <file>command syntax