Kubernetes Manifest Linting with Kube-score and Kube-linter: A CI/CD Integration Guide

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

The Problem That Bites You in Production

You’ve written your Kubernetes deployment YAML, pushed it, and everything looks fine in staging. Then, three weeks later, a pod gets OOMKilled in production because someone forgot to set resource limits. Or worse — your cluster gets partially compromised because a container is running as root with a permissive security context that nobody noticed.

This isn’t a hypothetical scenario. In my experience working with Kubernetes clusters across multiple environments, manifest quality issues are one of the most consistent sources of production incidents. The root cause is almost always the same: YAML files get written quickly, reviewed superficially in pull requests, and nobody catches the missing readinessProbe or the allowPrivilegeEscalation: true lurking in the spec.

The fix is straightforward: static analysis for your Kubernetes manifests, baked into your CI/CD pipeline so issues get caught before they ever reach a cluster. In my real-world experience, this is one of the essential skills to master when maintaining Kubernetes infrastructure at any serious scale.

Two Tools Worth Knowing

There are two tools I reach for when setting up manifest quality gates: Kube-score and Kube-linter. They complement each other well — Kube-score focuses on general best practices and gives each resource a score, while Kube-linter leans heavier into security-specific checks. Using both gives solid coverage without much overlap.

Kube-score

Kube-score reads your manifest files and evaluates each resource against a set of best practice checks. It flags issues like missing resource requests/limits, missing health probes, containers running as root, and non-pinned image tags. Each finding is categorized by severity: CRITICAL, WARNING, or OK. The scoring model makes it easy to track improvement over time.

Kube-linter

Kube-linter, originally from StackRox (now part of Red Hat), runs 30+ built-in checks with a strong emphasis on security posture. It catches privileged containers, missing network policies, overly permissive RBAC, and similar misconfigurations that kube-score might treat more leniently. The two tools have different check philosophies, which is exactly why running both is worth the small extra CI time.

Hands-on: Installing and Running Both Tools

Installing Kube-score

Grab the binary from GitHub releases:

# Linux (x86_64)
curl -Lo kube-score https://github.com/zegl/kube-score/releases/latest/download/kube-score_linux_amd64
chmod +x kube-score
sudo mv kube-score /usr/local/bin/

# macOS via Homebrew
brew install kube-score

# Verify installation
kube-score version

Installing Kube-linter

# Linux
curl -Lo kube-linter https://github.com/stackrox/kube-linter/releases/latest/download/kube-linter-linux
chmod +x kube-linter
sudo mv kube-linter /usr/local/bin/

# macOS via Homebrew
brew install kube-linter

# Verify installation
kube-linter version

Running Your First Check

Take a typical deployment manifest that looks passable on a quick review:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-app
        image: my-app:latest

Run kube-score against it:

kube-score score deployment.yaml

The output surfaces several real problems:

apps/v1/Deployment my-app                                             💥 CRITICAL
    [CRITICAL] Container Security Context
        Container 'my-app' does not have a security context set
    [CRITICAL] Container Resources
        Container 'my-app' does not have resource requests set
        Container 'my-app' does not have resource limits set
    [WARNING] Container Liveness Probe
        Container 'my-app' does not have a liveness probe set
    [WARNING] Container Readiness Probe
        Container 'my-app' does not have a readiness probe set
    [WARNING] Deployment has a single replica — not resilient to node failures

Now run kube-linter on the same file:

kube-linter lint deployment.yaml
deployment.yaml: container "my-app" does not have a read-only root file system (check: no-read-only-root-fs)
deployment.yaml: container "my-app" has no resource limits (check: resource-limits)
deployment.yaml: container "my-app" is not set to runAsNonRoot (check: run-as-non-root)
deployment.yaml: container "my-app" has an image using a mutable tag (check: no-latest-image)

From a single 18-line YAML, both tools surfaced genuine production risks. That’s the value of automated linting.

A Corrected Manifest

Here’s the same deployment after addressing the critical findings:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
      containers:
      - name: my-app
        image: my-app:1.2.3          # pinned tag — never :latest
        resources:
          requests:
            cpu: "100m"
            memory: "128Mi"
          limits:
            cpu: "500m"
            memory: "256Mi"
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities:
            drop:
            - ALL
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 15
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10

Wiring Both Tools into CI/CD with GitHub Actions

Running these tools locally is useful, but the real value comes from making them required gates in your pipeline. Every pull request that touches Kubernetes manifests should run both checks automatically — no exceptions, no bypass.

# .github/workflows/k8s-lint.yml
name: Kubernetes Manifest Checks

on:
  pull_request:
    paths:
      - 'k8s/**'
      - 'manifests/**'
      - '**/*.yaml'
      - '**/*.yml'

jobs:
  kube-score:
    name: Kube-score Analysis
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install kube-score
        run: |
          curl -Lo kube-score https://github.com/zegl/kube-score/releases/latest/download/kube-score_linux_amd64
          chmod +x kube-score && sudo mv kube-score /usr/local/bin/

      - name: Run kube-score
        run: |
          find k8s/ -name "*.yaml" | \
            xargs kube-score score --exit-one-on-warning

  kube-linter:
    name: Kube-linter Security Checks
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run kube-linter
        uses: stackrox/kube-linter-action@v1
        with:
          directory: k8s/
          config: .kube-linter.yaml

The --exit-one-on-warning flag makes kube-score fail the pipeline on warnings too, not just critical issues. I usually start without this flag when onboarding an existing project (to avoid an overwhelming number of failures on day one), then add it once the critical findings are cleared.

Customizing Kube-linter Rules

Not every check applies to every environment. Kube-linter supports a config file for enabling or disabling specific checks:

# .kube-linter.yaml
checks:
  addAllBuiltIn: true
  exclude:
    - "minimum-three-replicas"      # staging runs 2 replicas intentionally
    - "required-annotation-email"   # not using this annotation convention

List all available checks with:

kube-linter checks list

Linting Helm-rendered Output

If your project uses Helm, lint the rendered manifests — not the raw templates. Template-level linting misses values-driven configuration problems:

# Pipe helm template output directly to kube-score
helm template my-release ./chart -f values.prod.yaml | kube-score score -

# Same for kube-linter
helm template my-release ./chart -f values.prod.yaml > /tmp/rendered.yaml
kube-linter lint /tmp/rendered.yaml

JSON Output for CI Dashboards

For teams that want to track findings over time or integrate with reporting tools, kube-score supports JSON output:

# Show only CRITICAL findings from JSON output
kube-score score deployment.yaml --output-format json | \
  jq '[.[] | .checks[] | select(.grade == "CRITICAL")]'

Practical Tips from Working with Real Clusters

  • Triage incrementally. Adding these tools to a mature project usually surfaces dozens of findings. Fix CRITICALs first, then work through WARNINGs over subsequent sprints rather than trying to clean everything in one PR.
  • Resource limits protect the whole cluster. A single pod without limits can starve neighboring workloads through noisy-neighbor effects. This is a cluster stability issue, not just a pod configuration detail.
  • Pinned image tags are non-negotiable. Both tools flag :latest and similar mutable tags. Unpinned images make deployments non-reproducible and can introduce unexpected changes during rolling restarts.
  • Run checks in pre-commit hooks too. Use pre-commit to run kube-score locally before commits reach CI — faster feedback loop, fewer failed pipelines.

Where to Go From Here

Once kube-score and kube-linter are running in your pipeline, you’ve eliminated the most common class of Kubernetes manifest problems at the source. The natural next step is pairing static CI checks with runtime admission control — tools like Kyverno or OPA/Gatekeeper enforce the same rules at the cluster level, so even manifests applied directly via kubectl get validated before pods are scheduled.

But the CI integration alone is already a significant improvement. Get these checks running on pull requests, fix the CRITICALs systematically, and within a few sprints your manifest quality will be in a genuinely different place than where you started.

Share: