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.
| Control | Examines | When | Typical tools | Answers |
|---|---|---|---|---|
| Static analysis | Manifests, charts, Dockerfiles | Before deploy | Kubesec, KubeLinter, trivy config | ”Is this workload configured safely?” |
| Image scanning | Image layers and packages | Build / before deploy | Trivy | ”Does this image contain known CVEs?” |
| Image signing | Image digest and signature | Build / admission | Cosign | ”Is this image authentic and unmodified?” |
| Admission control | The live API request | At deploy | Pod Security Admission, ValidatingAdmissionPolicy, Kyverno, OPA Gatekeeper | ”Should the cluster accept this?” |
| Runtime detection | Running processes and syscalls | After deploy | Falco, 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
}
]
}
}
]
| Field | Meaning |
|---|---|
object | The resource scanned, as Kind/name.namespace |
valid | Whether the manifest passed schema validation |
score | The sum of all matched rule points |
message | Passed with a score of N points when the score is zero or higher, otherwise Failed with a score of N points |
scoring.critical | Negative-point rules that matched — the things to fix |
scoring.passed | Positive-point rules the manifest already satisfies |
scoring.advise | Positive-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 ID | Checks | Points |
|---|---|---|
Privileged | securityContext.privileged == true | -30 |
CapSysAdmin | capabilities.add includes SYS_ADMIN | -30 |
HostNetwork | spec.hostNetwork == true | -9 |
HostPID | spec.hostPID == true | -9 |
HostIPC | spec.hostIPC == true | -9 |
DockerSock | hostPath volume for /var/run/docker.sock | -9 |
AllowPrivilegeEscalation | allowPrivilegeEscalation == true | -7 |
SecretsAsEnvironmentVariables | Secret values injected through env[].valueFrom.secretKeyRef | -5 |
ServiceAccountName | spec.serviceAccountName is set | +3 |
ApparmorAny | An AppArmor profile is configured | +3 |
ReadOnlyRootFilesystem | readOnlyRootFilesystem == true | +1 |
RunAsNonRoot | runAsNonRoot == true | +1 |
RunAsUser | runAsUser greater than 10000 | +1 |
CapDropAll | capabilities.drop includes ALL | +1 |
SeccompAny | A seccomp profile is configured | +1 |
AutomountServiceAccountToken | automountServiceAccountToken == false | +1 |
LimitsCPU / LimitsMemory | CPU and memory limits are set | +1 each |
RequestsCPU / RequestsMemory | CPU 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:
| Check | Flags |
|---|---|
privileged-container | Containers running in privileged mode |
privilege-escalation-container | Containers that allow privilege escalation |
run-as-non-root | Containers not set to run as non-root |
no-read-only-root-fs | Containers without a read-only root filesystem |
host-network, host-pid, host-ipc | Pods sharing host namespaces |
docker-sock | Mounts of the Docker socket |
sensitive-host-mounts | Mounts of sensitive host directories such as /, /etc, /proc, /sys |
drop-net-raw-capability | Containers that do not drop NET_RAW |
unsafe-sysctls | Use of unsafe sysctls |
env-var-secret | Secrets apparently placed in plain environment variables |
latest-tag | Images with no tag or the latest tag |
ssh-port | Workloads exposing the SSH port |
unset-cpu-requirements, unset-memory-requirements | Missing CPU or memory requests and limits |
Useful security checks that are not on by default and must be included explicitly:
| Check | Flags |
|---|---|
access-to-secrets | RBAC bindings that grant access to Secrets |
access-to-create-pods | RBAC bindings that allow creating Pods |
cluster-admin-role-binding | Bindings to the cluster-admin ClusterRole |
wildcard-in-rules | RBAC rules using wildcards |
default-service-account | Workloads using the default ServiceAccount |
non-isolated-pod | Pods not selected by any NetworkPolicy |
privileged-ports | Containers binding to privileged ports |
writable-host-mount | Host paths mounted read-write |
read-secret-from-env-var | Secrets consumed as environment variables |
unsafe-proc-mount | Use 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:
excludealways wins — a check that is both included and excluded is skipped.- If
doNotAutoAddDefaultsandaddAllBuiltInare bothtrue,addAllBuiltIntakes 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
| Kubesec | KubeLinter | trivy config | |
|---|---|---|---|
| Output style | Numeric score with critical / passed / advise lists | One line per failed check with remediation | Findings grouped by severity |
| Focus | Pod-level security settings | Security plus production readiness | Misconfiguration across Kubernetes, Dockerfiles, and other IaC |
| Inputs | YAML or JSON manifests, stdin | YAML, Helm charts, Kustomize directories | Directories and files of several IaC types |
| Configurable rule set | Select rules with --rules | Include, exclude, custom checks, per-object ignores | Severity filters and policies |
| Pass/fail signal | Score and message; filter with jq for an exit code | Non-zero exit on any failed check | Exit code controlled by flags |
| Best at | A quick security posture number for one workload | Linting a whole repository or chart in CI | One 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,privilegedand the Docker sockethostPathare gone — the three settings that gave the container a route to the host.allowPrivilegeEscalation: falseandcapabilities.drop: ["ALL"]remove the ways a process could gain privileges after it starts. DroppingALLalso satisfies KubeLinter’sdrop-net-raw-capability.runAsNonRootwith a highrunAsUser, and aRuntimeDefaultseccomp profile, are set once at Pod level.readOnlyRootFilesystem: truemakes the container filesystem immutable; theemptyDirat/tmpgives 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 for | Why it matters | Fix |
|---|---|---|
No USER instruction, or USER root | The container runs as root by default | Create and switch to a non-root user |
FROM image:latest or no tag | The build is not reproducible and can change underneath you | Pin a specific version or digest |
Secrets in ENV or ARG, or copied into a layer | They persist in image history | Inject at runtime from a Secret |
| A full OS base image for a small binary | Larger attack surface and more CVEs | Use a minimal or distroless base and multi-stage builds |
ADD with a remote URL | Fetches unverified content at build time | Use COPY, or download and verify a checksum |
| Package managers and shells left in the final stage | Tools an attacker can use after a compromise | Remove 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:
- 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.
- 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.
- Work from the highest-risk findings down. Kubesec’s
criticallist and KubeLinter’s privilege and host-access checks come first. - Edit the correct level of the manifest. Pod spec for a Pod;
spec.template.specfor Deployments, StatefulSets, DaemonSets and Jobs;spec.jobTemplate.spec.template.specfor CronJobs. - Change only what is needed. Stripping settings the application depends on, or rewriting unrelated fields, risks breaking a workload that was otherwise fine.
- 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
baselineorrestrictedprofile 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
| Mistake | Why it hurts | Do 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 contents | Read critical and advise, and pair with image scanning and admission control |
| Fixing the top level of a Deployment | securityContext under spec is ignored; it belongs under spec.template.spec | Edit the Pod template |
Setting readOnlyRootFilesystem without a writable volume | Applications that write to /tmp or a cache directory crash | Mount an emptyDir at the paths the application writes to |
| Posting manifests to the hosted Kubesec endpoint | Manifests reveal infrastructure details | Use the local binary or container |
| Blanket-ignoring checks to get a clean run | Real findings disappear with the noise | Use per-object ignore annotations with a written reason |
| Assuming static analysis blocks deployment | It only reports | Enforce with admission control |
| Skipping the rescan | An indentation slip can leave the setting unapplied | Rescan, then verify the rollout |
| Confusing manifest analysis with image scanning | A clean manifest can still reference a vulnerable image | Run 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.