Kubernetes Scanner
A manifest is a request to run code with certain powers. This scanner finds manifests by their apiVersion: marker and by Kubernetes folder names, then matches regular expressions against the raw text for privileged mode, root, host namespaces, ALL capabilities, hardcoded secrets, unpinned images and public load balancers. Helm values files get their own secret check.
Which files it reads
There is no Dockerfile-style name for a manifest, so the scanner finds them two ways and merges the lists.
1The scanner gets every file in the repository index and looks for YAML. Kubernetes manifests have no special file name, so the name alone cannot identify one.
2Strategy 1: the marker. Any .yaml or .yml file whose text contains apiVersion: is treated as a manifest, wherever it lives.
3Strategy 2: the folders. Seven folder names (k8s, deploy, charts, manifests, helm, kubernetes, kube) are read straight from the checkout on disk, so Helm values files without an apiVersion are still found. A file named values.yaml is routed to the Helm secret check only.
4YAML that has no apiVersion: and sits outside those folders is ignored. This keeps CI workflows and app config out of the scan.
Walking a manifest
Seven checks run in a fixed order over one string. A bar marks the text each one matched.
1One manifest, two documents. Seven checks run over the whole text in this order. As with every scanner in the group, the YAML is never parsed: a regular expression decides.
2Privileged. privileged\s*:\s*true anywhere in the file. privileged: false does not match.
3Root. Either runAsUser: 0 or runAsNonRoot: false. Note what is not here: a manifest that simply omits runAsNonRoot is not flagged.
4Host namespaces. hostNetwork: true and hostPID: true are separate patterns, each with its own finding. The pod sees the node's network or process table.
5Secret in env. The regex needs a name: that contains PASSWORD, SECRET, API_KEY, TOKEN or a database URL, with value: on the very next line. The literal is then checked against the placeholder list.
6Image tag. image: x:latest anywhere in the file. A second pattern tries to catch an image with no tag at all, but it is anchored to the end of the text, so it only sees an untagged image on the file's last line.
7Capabilities. capabilities: followed within 50 characters by add: and then ALL, as a list item or inline list. Linux capabilities are the kernel's root powers, split up. ALL gives them all back.
8LoadBalancer. type: LoadBalancer with none of the three internal-load-balancer annotations (AWS, GCP, Azure) anywhere in the file means the service gets a public address.
9Seven findings from one file, none with a line number. The checks are separate regular expressions over the same text, so one block of YAML can feed several rules.
What it deliberately skips
Each row is a line the scanner meets. Step through for the verdict and the reason.
1Variable references. Skipped values: those starting with $ or ${, <, a run of asterisks, your-, xxx, TODO, CHANGE_ME or empty. Helm template values such as {{.Values.x}} are skipped as well.
2Placeholders. A manifest with CHANGE_ME is a template waiting for a real value, not a leak.
3The safe form never matches. A secret read from a Kubernetes Secret has no value: line to catch. This is the fix the scanner steers toward.
4The false case. privileged: false is the hardened setting, and the pattern wants true.
5Untagged images are mostly missed. An image with no tag means latest, and the scanner tries to catch it, but the pattern is end-anchored. Only an untagged image on the final line of the file is reported. An explicit :latest is always caught.
6Internal load balancers are fine. The scanner searches the whole file for one of three cloud annotations before reporting.
7Helm values files get a different rule set. Only the four Helm secret patterns run on values.yaml: password, secret or API key, token, database URL. Empty and placeholder values are skipped.
The fix, line by line
The fixed form is the one the regular expressions do not match.
1Before: the seven findings from the walk. Each step fixes one line and the matching check goes quiet.
2Turn privileged off. If a container needs one device, grant only the specific capability it needs.
3Use a non-root user. Add runAsNonRoot: true alongside it so the kubelet refuses to start a root image.
4Leave the pod in its own network namespace. Remove the line, or set it false.
5Read it from a Secret. The manifest holds a reference, not the password. No value: line, so nothing for the regex to find.
6Pin the image. A release tag, or a digest, so a rollout means what you intended.
7Drop everything, then add back only what is needed. drop: ["ALL"] does not match the check.
8Mark it internal unless it is meant to face the internet. Seven fixes, zero findings.
Why it matters
Kubernetes misconfigurations are the usual way a compromised container turns into a compromised cluster. A privileged container can escape to the host, access other containers and take over the cluster. A container that adds every capability, shares the host network or runs as root removes most of the isolation that was the reason for using containers. A password in an env value is readable by anyone who can read the manifest.
Tesla (2018). Attackers found an unsecured Kubernetes dashboard with no authentication on Tesla's AWS infrastructure, used it to reach AWS credentials, and deployed cryptocurrency miners. Docker runc, CVE-2019-5736 (2019). A container escape in runc let a malicious container overwrite the host binary and gain root. Privileged containers made exploitation trivial.
Vulnerable vs fixed code
# BAD: the walk above
hostNetwork: true
containers:
- name: api
image: myorg/api:latest
securityContext:
privileged: true
runAsUser: 0
capabilities:
add: ["ALL"]
env:
- name: DB_PASSWORD
value: "s3cretpass"# GOOD: secure pod security context
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
containers:
- name: app
securityContext:
privileged: false
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
resources:
limits:
cpu: "500m"
memory: "512Mi"
requests:
cpu: "100m"
memory: "128Mi"Checks and limits
Every check, with the severity and confidence the code assigns.
| Check | Fires when | Severity | Conf. |
|---|---|---|---|
| Privileged | privileged: true | Critical | 0.95 |
| Root | runAsUser: 0 or runAsNonRoot: false | High | 0.90 |
| Host namespace | hostNetwork: true, hostPID: true (one finding each) | High | 0.90 |
| Env secret | name with PASSWORD, SECRET, API_KEY, TOKEN, or a DB URL, then a literal value: | Critical | 0.85 |
| Image tag | :latest anywhere; an untagged image only on the file's last line | Low | 0.65 |
| ALL capabilities | capabilities: then add: then ALL | Critical | 0.95 |
| LoadBalancer | type: LoadBalancer with no internal annotation | Medium | 0.75 |
| Helm values | plaintext password, secret or API key, token or database URL in values.yaml | High | 0.80 |
Resource limits, hostPath mounts, a missing runAsNonRoot: true and network policies are not checked. Severity runs from Low to Critical. Findings carry the file path only, with no line number.