Back to Blog

Static Analysis for the CKS Exam: Scanning Kubernetes Manifests with Kubesec & KubeLinter

A hands-on CKS guide to static analysis of Kubernetes workloads with Kubesec and KubeLinter — how each tool works, how to read Kubesec's score and critical/advise findings, KubeLinter's default checks, configuration and ignore annotations, a full scan-fix-rescan worked example, a Dockerfile review checklist, and how static analysis fits with image scanning and admission control.

By Sailor Team , October 2, 2026

The Supply Chain Security domain of the CKS carries 20% of the exam, and one of its four curriculum bullets reads: “Perform static analysis of user workloads and container images (e.g. Kubesec, KubeLinter).” That is one of the few places where the curriculum names specific tools, so it is worth knowing how those two tools behave rather than only the idea behind them.

Static analysis means inspecting a workload definition — a YAML manifest, a Helm chart, a Dockerfile — before anything runs. No cluster is required and no container starts. The tool reads the file, compares it against a set of rules, and tells you what is risky. It is the cheapest security control you have: a privileged container caught in a manifest costs a one-line edit; the same container caught at runtime is an incident.

This article stays in one lane: using Kubesec and KubeLinter to find and fix insecure workload configuration. The manual version of the skill — recognising a risky securityContext by eye and rewriting it from memory — is covered in the CKS Supply Chain Security guide, along with Trivy image scanning and base-image hardening. Read that first if you want the whole domain; read this for the tooling.

Where Static Analysis Sits in the Security Pipeline

It helps to place static analysis against the controls around it, because exam tasks and real pipelines both combine them.

ControlExaminesWhenTypical toolsAnswers
Static analysisManifests, charts, DockerfilesBefore deployKubesec, KubeLinter, trivy config”Is this workload configured safely?”
Image scanningImage layers and packagesBuild / before deployTrivy”Does this image contain known CVEs?”
Image signingImage digest and signatureBuild / admissionCosign”Is this image authentic and unmodified?”
Admission controlThe live API requestAt deployPod Security Admission, ValidatingAdmissionPolicy, Kyverno, OPA Gatekeeper”Should the cluster accept this?”
Runtime detectionRunning processes and syscallsAfter deployFalco, audit logs”Is something misbehaving right now?”

Static analysis is advisory. It reports; it does not block. A manifest that fails Kubesec can still be applied with kubectl apply unless an admission controller rejects it. That is why static analysis and admission control are usually paired — one gives developers early feedback, the other enforces the same rules at the cluster boundary.

Kubesec: Security Risk Scoring for Kubernetes Resources

Kubesec is an open-source tool from ControlPlane that scores a Kubernetes resource against a fixed set of security rules. Each rule is worth positive or negative points. The output is a single number plus the list of rules behind it.

Ways to run it

# Local binary
kubesec scan deployment.yaml

# Human-readable table instead of the default JSON
kubesec scan deployment.yaml --format table

# From standard input
cat deployment.yaml | kubesec scan -

# Container image — no install needed
docker run -i kubesec/kubesec:v2 scan /dev/stdin < deployment.yaml

# A rendered Helm chart
helm template -f values.yaml ./chart | kubesec scan /dev/stdin

Kubesec can also run as a local HTTP server, which is handy for scripts and CI:

kubesec http 8080 &
curl -sSX POST --data-binary @deployment.yaml http://localhost:8080/scan

There is a public hosted endpoint at v2.kubesec.io/scan that accepts the same POST. The project’s own documentation warns not to submit sensitive YAML to it, and that advice is sound: your manifests describe your infrastructure. Prefer the local binary or container.

A single input file may contain several YAML documents separated by ---; Kubesec scans each and returns one result per resource.

Reading the output

[
  {
    "object": "Pod/security-context-demo.default",
    "valid": true,
    "message": "Failed with a score of -30 points",
    "score": -30,
    "scoring": {
      "critical": [
        {
          "selector": "containers[] .securityContext .capabilities .add == SYS_ADMIN",
          "reason": "CAP_SYS_ADMIN is the most privileged capability and should always be avoided",
          "points": -30
        }
      ],
      "advise": [
        {
          "selector": "containers[] .securityContext .runAsNonRoot == true",
          "reason": "Force the running image to run as a non-root user to ensure least privilege",
          "points": 1
        }
      ]
    }
  }
]
FieldMeaning
objectThe resource scanned, as Kind/name.namespace
validWhether the manifest passed schema validation
scoreThe sum of all matched rule points
messagePassed with a score of N points when the score is zero or higher, otherwise Failed with a score of N points
scoring.criticalNegative-point rules that matched — the things to fix
scoring.passedPositive-point rules the manifest already satisfies
scoring.advisePositive-point rules it does not yet satisfy — the hardening you could add

Each entry has a selector (the field path the rule checks), a reason, and points. The selector is effectively the answer: it tells you exactly which field to change.

The practical reading order is: critical first, then advise. A resource with an empty critical list and a positive score has no flagged high-risk settings; working through advise raises the score further.

The rules that matter

You can print the full rule set, with points, from the tool itself:

kubesec print-rules --format table

The rules below are the ones that map to settings you will be asked to recognise and fix.

Rule IDChecksPoints
PrivilegedsecurityContext.privileged == true-30
CapSysAdmincapabilities.add includes SYS_ADMIN-30
HostNetworkspec.hostNetwork == true-9
HostPIDspec.hostPID == true-9
HostIPCspec.hostIPC == true-9
DockerSockhostPath volume for /var/run/docker.sock-9
AllowPrivilegeEscalationallowPrivilegeEscalation == true-7
SecretsAsEnvironmentVariablesSecret values injected through env[].valueFrom.secretKeyRef-5
ServiceAccountNamespec.serviceAccountName is set+3
ApparmorAnyAn AppArmor profile is configured+3
ReadOnlyRootFilesystemreadOnlyRootFilesystem == true+1
RunAsNonRootrunAsNonRoot == true+1
RunAsUserrunAsUser greater than 10000+1
CapDropAllcapabilities.drop includes ALL+1
SeccompAnyA seccomp profile is configured+1
AutomountServiceAccountTokenautomountServiceAccountToken == false+1
LimitsCPU / LimitsMemoryCPU and memory limits are set+1 each
RequestsCPU / RequestsMemoryCPU and memory requests are set+1 each

Point values come from the tool’s rule set and can change between releases, so treat kubesec print-rules on the version in front of you as authoritative. The shape is stable, though: settings that hand a container the host — privileged mode, SYS_ADMIN, host namespaces, the container runtime socket — carry heavy negative weight, while each hardening setting earns a small positive score.

Most rules apply to Pods, Deployments, StatefulSets and DaemonSets. If you scan a kind that no rule covers, Kubesec says so in the message instead of returning a score.

To evaluate only specific rules:

kubesec scan --rules Privileged,RunAsNonRoot,SeccompAny deployment.yaml

Pulling out just what you need

The JSON is easy to filter with jq when a task asks for a specific piece of the result:

# Just the score
kubesec scan deployment.yaml | jq '.[0].score'

# Just the critical selectors
kubesec scan deployment.yaml | jq -r '.[0].scoring.critical[]?.selector'

# Fail a pipeline step if the score is not above a threshold
kubesec scan deployment.yaml | jq --exit-status '.[0].score > 5' >/dev/null

KubeLinter: Lint Checks for YAML, Helm Charts and Kustomize

KubeLinter is an open-source static analysis tool created by StackRox and now maintained under Red Hat. Where Kubesec produces a score, KubeLinter behaves like a linter: it runs a list of named checks and prints one line per violation, each with a remediation hint. It looks at security and production readiness — missing resource limits and broken selectors sit alongside privileged containers.

It accepts three kinds of input and works out which is which:

kube-linter lint pod.yaml              # a single manifest
kube-linter lint manifests/            # a directory of manifests
kube-linter lint charts/web/           # a directory containing Chart.yaml (Helm)
kube-linter lint overlays/prod/        # a directory containing kustomization.yaml

Reading the output

Take this Pod, from the project’s own documentation:

apiVersion: v1
kind: Pod
metadata:
  name: security-context-demo
spec:
  securityContext:
    runAsUser: 1000
    runAsGroup: 3000
    fsGroup: 2000
  volumes:
    - name: sec-ctx-vol
      emptyDir: {}
  containers:
    - name: sec-ctx-demo
      image: busybox
      resources:
        requests:
          memory: "64Mi"
          cpu: "250m"
      command: ["sh", "-c", "sleep 1h"]
      volumeMounts:
        - name: sec-ctx-vol
          mountPath: /data/demo
      securityContext:
        allowPrivilegeEscalation: false

kube-linter lint pod.yaml reports three problems. Trimmed for readability:

pod.yaml: (object: <no namespace>/security-context-demo /v1, Kind=Pod)
  The container "sec-ctx-demo" is using an invalid container image, "busybox".
  (check: latest-tag, remediation: Use a container image with a specific tag other than latest.)

pod.yaml: (object: <no namespace>/security-context-demo /v1, Kind=Pod)
  container "sec-ctx-demo" does not have a read-only root file system
  (check: no-read-only-root-fs, remediation: Set readOnlyRootFilesystem to true in the container securityContext.)

pod.yaml: (object: <no namespace>/security-context-demo /v1, Kind=Pod)
  container "sec-ctx-demo" has memory limit 0
  (check: unset-memory-requirements, remediation: Set memory limits for your container based on its requirements.)

Error: found 3 lint errors

Every finding has the same four parts: the file, the object, the check name, and a remediation. The check name is what you use to look a rule up, enable it, exclude it, or suppress it.

When any check fails, KubeLinter exits with a non-zero status, which is what makes it useful as a pipeline gate. Add --fail-on-invalid-resource if you also want unparseable YAML to fail the run. For machine-readable results use --format json or --format sarif.

The default checks

KubeLinter ships with a set of checks enabled by default and a further set you opt into. List what your version has:

kube-linter checks list

The security-relevant checks that run by default:

CheckFlags
privileged-containerContainers running in privileged mode
privilege-escalation-containerContainers that allow privilege escalation
run-as-non-rootContainers not set to run as non-root
no-read-only-root-fsContainers without a read-only root filesystem
host-network, host-pid, host-ipcPods sharing host namespaces
docker-sockMounts of the Docker socket
sensitive-host-mountsMounts of sensitive host directories such as /, /etc, /proc, /sys
drop-net-raw-capabilityContainers that do not drop NET_RAW
unsafe-sysctlsUse of unsafe sysctls
env-var-secretSecrets apparently placed in plain environment variables
latest-tagImages with no tag or the latest tag
ssh-portWorkloads exposing the SSH port
unset-cpu-requirements, unset-memory-requirementsMissing CPU or memory requests and limits

Useful security checks that are not on by default and must be included explicitly:

CheckFlags
access-to-secretsRBAC bindings that grant access to Secrets
access-to-create-podsRBAC bindings that allow creating Pods
cluster-admin-role-bindingBindings to the cluster-admin ClusterRole
wildcard-in-rulesRBAC rules using wildcards
default-service-accountWorkloads using the default ServiceAccount
non-isolated-podPods not selected by any NetworkPolicy
privileged-portsContainers binding to privileged ports
writable-host-mountHost paths mounted read-write
read-secret-from-env-varSecrets consumed as environment variables
unsafe-proc-mountUse of an unmasked /proc mount

That second table is a good reminder that static analysis is not only about Pod specs: several checks examine RBAC objects and NetworkPolicy coverage in the same set of files.

Configuring which checks run

KubeLinter looks for .kube-linter.yaml (then .kube-linter.yml) in the working directory, or takes a path from --config.

# .kube-linter.yaml — run the defaults plus selected extras, minus one
checks:
  include:
    - "default-service-account"
    - "cluster-admin-role-binding"
    - "writable-host-mount"
  exclude:
    - "no-anti-affinity"
# Run only a small, explicit set
checks:
  doNotAutoAddDefaults: true
  include:
    - "privileged-container"
    - "run-as-non-root"
    - "no-read-only-root-fs"
# Run every built-in check
checks:
  addAllBuiltIn: true

Three rules of precedence are worth remembering:

  • exclude always wins — a check that is both included and excluded is skipped.
  • If doNotAutoAddDefaults and addAllBuiltIn are both true, addAllBuiltIn takes precedence.
  • The same switches exist as flags: --include, --exclude, --do-not-auto-add-defaults, --add-all-built-in.

You can also define custom checks from built-in templates — for example, requiring a label or annotation on every Deployment:

customChecks:
  - name: required-label-owner-team
    template: required-label
    params:
      key: team
    scope:
      objectKinds:
        - DeploymentLike
    remediation: "Add a 'team' label so ownership is clear."

Suppressing a finding on purpose

Some workloads legitimately need what a check forbids — a CNI agent needs host networking; a log collector needs a host path. Suppress per object, with a recorded reason, using an annotation:

metadata:
  annotations:
    ignore-check.kube-linter.io/host-network: "CNI agent requires the host network namespace"

The annotation key is ignore-check.kube-linter.io/ followed by the check name, and the value is a free-text explanation. There is also kube-linter.io/ignore-all to skip every check for an object; use it sparingly, since it hides future findings too.

Kubesec vs KubeLinter vs trivy config

KubesecKubeLintertrivy config
Output styleNumeric score with critical / passed / advise listsOne line per failed check with remediationFindings grouped by severity
FocusPod-level security settingsSecurity plus production readinessMisconfiguration across Kubernetes, Dockerfiles, and other IaC
InputsYAML or JSON manifests, stdinYAML, Helm charts, Kustomize directoriesDirectories and files of several IaC types
Configurable rule setSelect rules with --rulesInclude, exclude, custom checks, per-object ignoresSeverity filters and policies
Pass/fail signalScore and message; filter with jq for an exit codeNon-zero exit on any failed checkExit code controlled by flags
Best atA quick security posture number for one workloadLinting a whole repository or chart in CIOne tool for images and configuration together

They overlap by design. In practice you choose by the question in front of you: “how risky is this one Deployment?” suits Kubesec; “does anything in this directory violate our rules?” suits KubeLinter; “scan the image and its manifests together” suits Trivy.

Worked Example: Scan, Fix, Rescan

Here is a Deployment with several problems.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: payments
  namespace: prod
spec:
  replicas: 2
  selector:
    matchLabels:
      app: payments
  template:
    metadata:
      labels:
        app: payments
    spec:
      hostNetwork: true
      containers:
        - name: api
          image: registry.example.com/payments:latest
          securityContext:
            privileged: true
            allowPrivilegeEscalation: true
          volumeMounts:
            - name: runtime
              mountPath: /var/run/docker.sock
      volumes:
        - name: runtime
          hostPath:
            path: /var/run/docker.sock

Step 1 — scan.

kubesec scan payments.yaml | jq -r '.[0].message, (.[0].scoring.critical[]?.selector)'
kube-linter lint payments.yaml

Kubesec returns a failing message and a critical list covering privileged, allowPrivilegeEscalation, hostNetwork and the Docker socket mount. KubeLinter reports the matching checks — privileged-container, privilege-escalation-container, host-network, docker-sock — and adds latest-tag, run-as-non-root, no-read-only-root-fs and the unset resource requirements.

Step 2 — fix what was flagged. Note that in a Deployment the Pod settings live under spec.template.spec, not at the top level.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: payments
  namespace: prod
spec:
  replicas: 2
  selector:
    matchLabels:
      app: payments
  template:
    metadata:
      labels:
        app: payments
    spec:
      serviceAccountName: payments
      automountServiceAccountToken: false
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: api
          image: registry.example.com/payments:2.4.1
          securityContext:
            privileged: false
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 256Mi
          volumeMounts:
            - name: tmp
              mountPath: /tmp
      volumes:
        - name: tmp
          emptyDir: {}

What changed and why:

  • hostNetwork, privileged and the Docker socket hostPath are gone — the three settings that gave the container a route to the host.
  • allowPrivilegeEscalation: false and capabilities.drop: ["ALL"] remove the ways a process could gain privileges after it starts. Dropping ALL also satisfies KubeLinter’s drop-net-raw-capability.
  • runAsNonRoot with a high runAsUser, and a RuntimeDefault seccomp profile, are set once at Pod level.
  • readOnlyRootFilesystem: true makes the container filesystem immutable; the emptyDir at /tmp gives the application the one writable path it needs.
  • The image is pinned to a version tag, and requests and limits are set.

Step 3 — rescan.

kubesec scan payments.yaml | jq '.[0] | {message, critical: (.scoring.critical // [])}'
kube-linter lint payments.yaml

The Kubesec critical list is now empty and the message reports a pass. KubeLinter’s security findings are cleared; depending on your configuration it may still raise production-readiness checks such as no-anti-affinity, which you either address or exclude deliberately.

Step 4 — apply and confirm the workload still runs.

kubectl apply -f payments.yaml
kubectl -n prod rollout status deployment/payments

That last step is not optional. A scanner cannot tell you that the application writes to a path you just made read-only, or that it needs to bind a port below 1024. A manifest that scans clean but leaves Pods in CrashLoopBackOff has not been fixed.

Static Analysis of Container Images: the Dockerfile

The curriculum bullet says “user workloads and container images”. For images, static analysis means reading the Dockerfile for misconfiguration — distinct from scanning the built image for CVEs. The checklist is short:

Look forWhy it mattersFix
No USER instruction, or USER rootThe container runs as root by defaultCreate and switch to a non-root user
FROM image:latest or no tagThe build is not reproducible and can change underneath youPin a specific version or digest
Secrets in ENV or ARG, or copied into a layerThey persist in image historyInject at runtime from a Secret
A full OS base image for a small binaryLarger attack surface and more CVEsUse a minimal or distroless base and multi-stage builds
ADD with a remote URLFetches unverified content at build timeUse COPY, or download and verify a checksum
Package managers and shells left in the final stageTools an attacker can use after a compromiseRemove them or build from a minimal final stage

trivy config can evaluate a Dockerfile for several of these automatically. The CKS Supply Chain Security guide covers minimising base images and Trivy in detail, and the container images and Dockerfile guide covers the build mechanics.

A Practical Workflow for Exam Tasks and Reviews

A static-analysis task tends to have the same structure: here is a manifest, find what is wrong with it, correct it. A reliable sequence:

  1. Read the task for the required outcome. “Fix the issues the tool reports”, “raise the score above N”, and “write the failing check names to a file” are three different deliverables.
  2. Run the tool that the task names, on the file the task names. If no tool is named, either works; KubeLinter’s remediation text is the quicker read.
  3. Work from the highest-risk findings down. Kubesec’s critical list and KubeLinter’s privilege and host-access checks come first.
  4. Edit the correct level of the manifest. Pod spec for a Pod; spec.template.spec for Deployments, StatefulSets, DaemonSets and Jobs; spec.jobTemplate.spec.template.spec for CronJobs.
  5. Change only what is needed. Stripping settings the application depends on, or rewriting unrelated fields, risks breaking a workload that was otherwise fine.
  6. Rescan to confirm, then apply and check the rollout if the task involves a running workload.

Tools that a task depends on are provided in the exam environment, and the candidate handbook lists the documentation sites you are allowed to open, so check the current list before exam day rather than relying on memory of install commands. The transferable skill is reading a finding and translating it into the right field at the right indentation — which is the same hardened securityContext block the securityContext guide builds up field by field.

From Reporting to Enforcement

Static analysis tells you about a problem; it does not stop anyone applying the manifest. To make the same rules binding, enforce them at admission:

  • Pod Security Admission with the baseline or restricted profile rejects most of what Kubesec scores as critical — privileged containers, host namespaces, privilege escalation. See the microservice vulnerabilities guide.
  • ValidatingAdmissionPolicy, Kyverno or OPA Gatekeeper express custom rules, such as approved registries or mandatory labels. See the admission control deep-dive.
  • Image signature verification ensures that what was scanned is what runs. See the image signing guide.

A sensible pipeline runs KubeLinter or Kubesec on every commit so developers see problems early, and relies on admission control as the gate that cannot be bypassed. For settings that only take effect on the node — seccomp and AppArmor profiles — the system hardening guide explains what the scanner is asking you to configure.

Common Mistakes

MistakeWhy it hurtsDo this instead
Treating a positive Kubesec score as “secure”The score only reflects the rules Kubesec has; it knows nothing about your RBAC, network policy or image contentsRead critical and advise, and pair with image scanning and admission control
Fixing the top level of a DeploymentsecurityContext under spec is ignored; it belongs under spec.template.specEdit the Pod template
Setting readOnlyRootFilesystem without a writable volumeApplications that write to /tmp or a cache directory crashMount an emptyDir at the paths the application writes to
Posting manifests to the hosted Kubesec endpointManifests reveal infrastructure detailsUse the local binary or container
Blanket-ignoring checks to get a clean runReal findings disappear with the noiseUse per-object ignore annotations with a written reason
Assuming static analysis blocks deploymentIt only reportsEnforce with admission control
Skipping the rescanAn indentation slip can leave the setting unappliedRescan, then verify the rollout
Confusing manifest analysis with image scanningA clean manifest can still reference a vulnerable imageRun both

Frequently Asked Questions

Are Kubesec and KubeLinter really on the CKS curriculum?

Yes. The Supply Chain Security domain, weighted at 20%, includes the bullet “Perform static analysis of user workloads and container images (e.g. Kubesec, KubeLinter)”. The “e.g.” matters: the objective is the skill of static analysis, with these two tools named as examples.

What is the difference between Kubesec and KubeLinter?

Kubesec assigns a numeric security score to a resource, listing critical findings and hardening advice. KubeLinter runs a configurable list of named checks across manifests, Helm charts and Kustomize directories, covering security and production readiness, and exits non-zero when any check fails.

What Kubesec score counts as a pass?

Kubesec reports “Passed” when the score is zero or higher and “Failed” when it is negative. A passing message only means no heavy negative rule outweighed the positives — aim for an empty critical list and then work through advise.

Does static analysis replace image scanning with Trivy?

No. Static analysis inspects how a workload is configured; image scanning inspects what the image contains. A perfectly configured Pod can still run an image with critical vulnerabilities, and a clean image can still be run as privileged.

Can KubeLinter scan Helm charts?

Yes. Point kube-linter lint at a directory containing Chart.yaml and it renders the chart and lints the result. It also detects Kustomize directories containing a kustomization.yaml.

How do I disable a KubeLinter check for one workload?

Add the annotation ignore-check.kube-linter.io/ followed by the check name to that object, with a short explanation as the value. To disable a check everywhere, list it under checks.exclude in .kube-linter.yaml.

Do I need a running cluster to use these tools?

No. Both analyse files. That is what makes them suitable for pre-commit hooks and CI jobs, well before a cluster is involved.

Is a clean scan enough to call a workload secure?

No. Static analysis covers configuration that is visible in the manifest. It cannot see runtime behaviour, cluster-wide RBAC, network exposure, or vulnerabilities in the image. It is one layer among several.

Conclusion & Next Steps

Static analysis is the earliest and cheapest check in the supply chain: read the manifest before it runs, and fix what would give a container more access than it needs. For the CKS, be comfortable with both named tools — Kubesec’s score and its critical and advise lists, and KubeLinter’s named checks, configuration file and ignore annotations — and, more importantly, with turning a finding into the right securityContext change at the right level of the manifest. Then rescan, apply, and confirm the workload still runs.

The quickest way to build that fluency is repetition against real manifests on a real cluster. Sailor.sh’s Certified Kubernetes Security Specialist (CKS) Mock Exam Bundle provides a browser-based, exam-style terminal with hands-on security scenarios across all six domains, so you can practise finding and fixing insecure workloads under time pressure. To experiment first at no cost, the CKS practice environment guide and How to Practice CKS for Free show how to set up your own lab.

From here, continue with the CKS Supply Chain Security guide for image scanning and base-image hardening, the admission control deep-dive to enforce what the scanners report, the CKS Study Plan to schedule your preparation, and the CKS Exam Guide 2026 for the full blueprint and logistics.

Limited Time Offer: Get 80% off all Mock Exam Bundles | Sale ends in 7 days. Start learning today.

Claim Now