Post

Kubescape C-0013 Remediation — Tier 1

Kubescape C-0013 Remediation — Tier 1

Kubescape C-0013 Remediation — Tier 1

Follow-up to the baseline scan. C-0013 (“non-root containers”) was the single largest own-app failure category at 47 failed / 14 passed. Before touching any of the 27 flagged Deployments, kubectl exec -- id -u against each one showed they weren’t uniform risk: 9 were already running as non-root at runtime and just missing the securityContext declaration Kubescape checks for — a zero-behavior-change fix. The other 18 are genuinely running as root or couldn’t be checked (no id binary in the image), and need real image-behavior verification before touching — deliberately out of scope here. This post covers just the safe 9.

What was fixed

AppNamespaceObserved UIDManaged by
drawiodefault1001ArgoCD (Drawio.git)
grafanadefault472ArgoCD (Grafana.git)
kafkadefault1000ArgoCD (kafka.git)
kafka-uidefault100ArgoCD (kafka.git)
n8ndefault1000ArgoCD (n8n.git)
pgadmindefault5050plain kubectl
prometheusdefault65534plain kubectl
rustfsdefault10001plain kubectl
sftpgodefault1000plain kubectl

Each got a pod-level securityContext (runAsNonRoot: true, runAsUser, runAsGroup matching the UID already observed at runtime) — declaring existing behavior, not changing it.

Steps taken

ArgoCD-managed (drawio, grafana, kafka, kafka-ui, n8n): edited the local manifest clone, committed, and pushed to each repo’s k3s branch. Before pushing, git fetch turned up that several repos were ahead of the local clone — Renovate had already merged image-tag bumps (drawio 31.4.2→31.4.6, n8n 2.38.3→2.41.2, kafka 4.2.1→4.3.1, grafana 13.2.1→13.2.2) that ArgoCD had already synced to the live cluster independently of the stale local files. Rebased onto origin/k3s before pushing so the security fix landed on top of the current state rather than reverting those bumps. ArgoCD picked up each push within one sync cycle (forced via the argocd.argoproj.io/refresh annotation rather than waiting out the default poll interval).

Plain kubectl (pgadmin, prometheus, rustfs, sftpgo): edited the local manifest and kubectl apply -f, keeping the local file as the source of truth these aren’t git-tracked against a remote for.

Verification

Per app: kubectl rollout status, confirmed Ready 1/1 with zero restarts, kubectl exec -- id to confirm the actual runtime UID/GID matched what was declared, and a functional check — HTTP 200 through each web UI, plus a direct kafka-broker-api-versions.sh call against the broker to confirm client connectivity wasn’t affected by the kafka/kafka-ui change together.

Two apps needed extra patience rather than being a real problem: drawio (Tomcat) and kafka-ui (Spring Boot) both took 25–90 seconds to bind their HTTP port on this cluster’s Raspberry Pi CM4 hardware — normal JVM cold-start cost, not a failure. Waited for their own startup-complete log lines instead of guessing a timeout.

All 9 ArgoCD applications cluster-wide stayed Synced/Healthy throughout — no collateral drift or breakage in anything untouched by this change.

Results

C-0013 specifically:

 FailedPassedCompliance %
Before471423.0
After382337.7

Exactly the 9 targeted resources moved from failed to passed — verified by diffing the full resource list between scans, not just the counts. Zero regressions anywhere else in the cluster.

Overall compliance score: 62.3 → 62.6. A small move — C-0013 is one control among 30 failing ones, and these 9 resources mostly still fail other controls (missing resource limits, other hardening controls), so they don’t drop out of the overall failed-resource count even though this specific control now passes for them.

Raw output:

1
2
kubescape/kubescape-results-20260924-c0013-fix.json
kubescape/kubescape-results-20260924-c0013-fix.pdf

What’s still open

The other 18 “own app” C-0013 failures are deliberately not part of this pass: activemq, dbeaver, docsite (both namespaces), git-ollama-tools, gluetun, homepage, mosquitto, postgres, postgres-mcp, media-indexer/frontend, media-indexer/redis, cloudflared-deployment, gatus, and portainer are either confirmed running as root right now or couldn’t be checked at all. Declaring runAsNonRoot: true on those blind risks breaking live services (the database, mail, the media indexer, this doc site) — each needs the image’s actual non-root support verified and a canary rollout before it’s safe to land, not a batch edit. gluetun in particular fronts a multi-container arr-stack pod built on linuxserver.io-style images that typically need to start as root by design (PUID/PGID pattern) and may not be fixable via securityContext alone.

This post is licensed under CC BY 4.0 by the author.