Skip to content

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

bash
# 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.yaml

Scanning a Pod Manifest

Create a sample Pod manifest to analyze:

yaml
# insecure-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: insecure-app
spec:
  containers:
    - name: app
      image: nginx
      securityContext:
        privileged: true
        runAsUser: 0
      ports:
        - containerPort: 80

Scan it with kubesec:

bash
kubesec scan insecure-pod.yaml

Sample Output:

json
[
  {
    "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 RangeMeaningAction
Positive (> 0)Good security postureContinue improving
Zero (0)Baseline -- no security controls addedAdd recommended controls
Negative (< 0)Critical security issues presentMust fix before deploying

Scoring elements:

CategoryDescriptionPoints
criticalSevere security issuesNegative (e.g., -30)
adviseRecommended improvementsPositive (e.g., +1 to +3)
passedChecks that are satisfiedInformational

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

yaml
# 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: 8080
bash
kubesec scan secure-pod.yaml

Sample Output:

json
[
  {
    "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:

bash
# 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/scan

Exam 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:

yaml
# 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: 80
bash
kubesec scan insecure-deployment.yaml

Common Security Issues kubesec Catches

Issuekubesec IDPointsRemediation
Privileged containerPrivileged-30Set privileged: false
Running as rootRunAsUser+1 (advise)Set runAsUser: > 10000
No read-only root FSReadOnlyRootFilesystem+1 (advise)Set readOnlyRootFilesystem: true
No run-as-non-rootRunAsNonRoot+1 (advise)Set runAsNonRoot: true
No resource limitsLimitsCPU, LimitsMemory+1 each (advise)Add resource limits
No service accountServiceAccountName+3 (advise)Set serviceAccountName
No AppArmor profileApparmorAny+3 (advise)Add AppArmor annotation
No seccomp profileSeccompAny+1 (advise)Add seccomp annotation/field
Host network enabledHostNetwork-9Remove hostNetwork: true
Host PID enabledHostPID-9Remove hostPID: true
Allow privilege escalationAllowPrivilegeEscalation+1 (advise)Set allowPrivilegeEscalation: false
Capability SYS_ADMINCapSysAdmin-30Drop unnecessary capabilities

Other Static Analysis Tools

kube-score

kube-score performs static analysis on Kubernetes object definitions and provides human-readable recommendations.

bash
# Install
kubectl krew install score
# Or download binary directly

# Scan a manifest
kube-score score deployment.yaml

Sample 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 upgrades

Checkov

Checkov is a comprehensive static analysis tool that supports Kubernetes, Terraform, CloudFormation, and more.

bash
# Install
pip install checkov

# Scan Kubernetes manifests
checkov -d /path/to/k8s-manifests/ --framework kubernetes

# Scan a single file
checkov -f deployment.yaml --framework kubernetes

Sample 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-app

Conftest

Conftest is an OPA-based testing tool for configuration files. It lets you write custom policies in Rego.

bash
# Install
brew install conftest
# Or download binary

# Write a custom policy
mkdir -p policy
txt
# 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])
}
bash
# Test the manifest against policies
conftest test deployment.yaml

Sample 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 failures

Tool Comparison

Featurekubeseckube-scoreCheckovConftest
CKS Exam FocusHighLowLowLow
Scoring SystemNumericalPass/Fail/WarningPass/FailPass/Fail
Custom PoliciesNoNoYes (Python)Yes (Rego)
Output FormatJSONText/JSONText/JSON/SARIFText/JSON
Kubernetes SpecificYesYesMulti-frameworkMulti-format
CI/CD IntegrationGoodGoodExcellentGood
InstallationBinary/DockerBinary/kubectl pluginpipBinary/brew

Practical Remediation Guide

When kubesec identifies issues, here is how to fix the most common ones:

Before: Insecure Manifest

yaml
apiVersion: v1
kind: Pod
metadata:
  name: app
spec:
  containers:
    - name: app
      image: nginx:latest
      securityContext:
        privileged: true

kubesec score: -30

After: Remediated Manifest

yaml
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: 256Mi

kubesec score: 12+

Changes made:

  1. Removed privileged: true (+30 points recovery)
  2. Added runAsNonRoot: true and runAsUser: > 10000
  3. Added readOnlyRootFilesystem: true
  4. Added allowPrivilegeEscalation: false
  5. Dropped all capabilities
  6. Added resource requests and limits
  7. Added a dedicated service account
  8. Set seccomp profile to RuntimeDefault
  9. Pinned image to digest instead of :latest tag
  10. Disabled service account token auto-mounting

Key Takeaways

Summary

  1. kubesec is the primary CKS exam tool for static analysis -- know the command and scoring system
  2. A negative score means critical security issues are present (e.g., privileged containers)
  3. The critical section in kubesec output shows blocking issues that must be fixed
  4. The advise section shows recommended improvements for better security posture
  5. kube-score and checkov provide additional static analysis capabilities
  6. Conftest allows writing custom OPA/Rego policies for manifest validation
  7. 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: true results in a score of -30
  • Confusing kubesec (manifest analysis) with Trivy (image vulnerability scanning)
  • Not knowing the kubesec scan <file> command syntax

Released under the MIT License.