Skip to content
Control Plane Labs

Kubernetes YAML Linter

Paste a manifest and get severity-ranked findings: removed apiVersions, missing requests and probes, unpinned images, and permissive securityContext.

Privacy: Everything runs in your browser. Manifests are parsed locally with js-yaml and never uploaded, which matters because manifests leak image registries, internal hostnames, and env var names.

Paste a manifest to lint it. Nothing is uploaded.

    Parsed in your browser with js-yaml — your manifests are never uploaded. Line numbers are best-effort: they point at the first matching key in the document, so on a manifest with several containers, trust the resource and path over the line.

    What it does

    Paste one or more manifests, separated by ---, and get back a ranked list of findings with a line number, the resource they belong to, the exact field path, and a link to the Kubernetes documentation for the rule. Errors are things that will break or that hand out root; warnings are things that will page you at 3am; info-level findings are conventions worth adopting but not worth blocking a deploy over.

    The rule set covers what actually goes wrong in review: apiVersion values that no longer exist, missing resource requests and memory limits, absent liveness and readiness probes, images pinned to :latest or to no tag at all, privileged: true, hostNetwork, hostPath volumes, missing runAsNonRoot and allowPrivilegeEscalation: false, dangerous added capabilities, absent labels, and the empty-block indentation mistake that silently drops your entire container list.

    How it works

    Parsing happens in your browser with js-yaml. Documents are split on the raw text before parsing, which is what lets each finding carry a line number from the file you actually pasted rather than from a re-serialised copy. YAML anchors and merge keys are resolved first, so a securityContext inherited through <<: *defaults counts as set — worth knowing, because linters that skip this step produce a wall of false positives on any manifest that uses aliases.

    Rules read the parsed object graph, not a regex over text. That distinction matters for the pod template: a Deployment, StatefulSet, DaemonSet and Job all keep their containers at spec.template.spec.containers, a CronJob buries it under spec.jobTemplate.spec.template.spec, and a bare Pod has it at spec. Each is walked at the right path, including initContainers.

    The deprecation table is the part worth being precise about. It maps apiVersion plus kind pairs to the version that removed them and the replacement group, following the Kubernetes deprecated API migration guide: extensions/v1beta1 Deployments went in 1.16, networking.k8s.io/v1beta1 Ingress and the v1beta1 RBAC types in 1.22, batch/v1beta1 CronJob and policy/v1beta1 PodSecurityPolicy in 1.25. Anything else with beta or alpha in the group gets an info-level note rather than a false alarm.

    Security rules follow the Pod Security Standards rather than an invented house style. hostNetwork, hostPID, hostIPC and privileged are Baseline violations and reported as errors; the explicit allowPrivilegeEscalation: false and runAsNonRoot: true that Restricted demands are warnings, since plenty of legitimate workloads have not got there yet.

    Why there is no CPU limit rule

    The resource rules ask for CPU and memory requests and a memory limit, and deliberately never ask for a CPU limit. This is not an oversight.

    Requests are what the scheduler packs against, and a container with none lands in the BestEffort QoS class, first in line for eviction when a node comes under pressure. A memory limit is a containment boundary: with one, a leak gets OOM-killed inside its own cgroup, and without one it takes the node and every neighbour with it. Both are described in Resource Management for Pods and Containers.

    A CPU limit behaves differently. CPU is compressible, so the limit is enforced by CFS quota — the container is throttled rather than killed. On a bursty request path, a limit that looks generous in aggregate still produces tail-latency spikes every time a burst exhausts the quota inside a 100ms period. Requests already guarantee a floor and let the workload use idle capacity. Flagging a missing CPU limit would push people toward a worse configuration, so the linter stays quiet about it.

    When to reach for it

    Reviewing a manifest somebody else wrote is the obvious one, and the severity filter makes it quick to answer “is there anything here I must block on”. It is also useful on your own output from Helm or Kustomize: pipe helm template through your clipboard and lint the rendered result, which is the thing that actually gets applied.

    The empty-block check earns its place on its own. When spec.template.spec parses to null because the containers list is indented one level too far left, the YAML is perfectly legal and the API server error is unhelpful. Seeing it named explicitly saves the ten minutes you would otherwise spend counting spaces.

    Frequently asked questions

    Does a clean report mean the manifest is valid?+
    No. This checks conventions and known-bad patterns, not the API schema. A typo like continers: or an int where a string belongs will pass here and still be rejected by the API server. kubectl apply --dry-run=server is the only thing that validates against the real schema for your cluster version.
    Why is the line number sometimes on the wrong container?+
    Line numbers are resolved by finding the first matching key in the document, because the parser hands back plain objects with no source positions. On a pod with three containers, all three missing-readiness-probe findings point near the first name: match. The spec.template.spec.containers[1] path in each finding is authoritative; the line is a hint.
    Why does it complain about resource limits when limits caused my last outage?+
    The rule asks for a memory limit and both CPU and memory requests, and deliberately does not ask for a CPU limit. CPU limits throttle via CFS quota and cause latency spikes under bursty load; memory limits contain a leak to one cgroup instead of the whole node. Requests are what the scheduler packs against, so missing requests is the more expensive omission.
    It says my pod template is missing but I can see it.+
    That finding fires when spec.template.spec parses to null, which is what happens when the block under it is indented one level too far left. The YAML is legal, so the parser does not complain — the key simply has no value and containers became a sibling of template. This is the single most common Kubernetes YAML mistake and the reason the linter checks for empty blocks at all.
    Are anchors and merge keys supported?+
    Yes. <<: *anchor is resolved before the rules run, so a securityContext pulled in from an anchor counts as set. Note that YAML anchors are scoped to a single document — referencing an anchor defined above a --- separator is invalid YAML, and you will get a syntax error rather than a lint finding.