SAST scanner · reads the file text

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.

SASTLow to CriticalInfrastructure Misconfiguration8 checksevery scan depth

Which files it reads

There is no Dockerfile-style name for a manifest, so the scanner finds them two ways and merges the lists.

Path in the repositoryDecisionGoes to deploy/app.yaml contains apiVersion:Manifest checks (7) k8s/service.yml contains apiVersion:Manifest checks (7) config/crd.yaml contains apiVersion:, folder does not matterManifest checks (7) charts/api/values.yaml no apiVersion, but it sits under charts/Helm secret check (1) docker-compose.yml yaml, but no apiVersion: and no k8s foldernot scanned .github/workflows/ci.yml yaml, but no apiVersion:not scanned config/app.yaml yaml, but no apiVersion:not scanned

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.

deploy/app.yamlread as one string, never parsed 1apiVersion: apps/v1 2kind: Deployment 3spec: 4 template: 5 spec: 6 hostNetwork: true 7 containers: 8 - name: api 9 image: myorg/api:latest 10 securityContext: 11 privileged: true 12 runAsUser: 0 13 capabilities: 14 add: ["ALL"] 15 env: 16 - name: DB_PASSWORD 17 value: "s3cretpass" 18--- 19kind: Service 20spec: { type: LoadBalancer } Checks Privileged containerCritical · conf 0.95 FLAG Runs as rootHigh · conf 0.90 FLAG Host network or PID namespaceHigh · conf 0.90 FLAG Secret in an env valueCritical · conf 0.85 FLAG :latest or untagged imageLow · conf 0.65 FLAG ALL capabilities addedCritical · conf 0.95 FLAG LoadBalancer, not internalMedium · conf 0.75 FLAG Critical, conf 0.95. The container gets the host's devices and can take over the node. High, conf 0.90. High, conf 0.90. One finding per namespace type. Critical, conf 0.85. Three patterns: password, API key or secret, database URL. Low, conf 0.65. Critical, conf 0.95. Medium, conf 0.75. Real output for this file: 7 findings.

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.

What the scanner meetsVerdictWhy value: "${DB_PASSWORD}"after name: DB_PASSWORDSKIP Values starting with $ or ${ are variable references. Also skipped: <..>, your-, xxx, TODO. value: CHANGE_MEafter name: API_TOKENSKIP CHANGE_ME is on the placeholder list. The list is case sensitive. valueFrom: { secretKeyRef: ... }the right way to read a secretSKIP The secret regex needs value: on the line right after name:. valueFrom never matches. privileged: falsesecurityContextSKIP Only the literal true is matched. image: nginxuntagged image in the middle of a manifestSKIP The untagged pattern ends in $ without multiline mode, so it only matches on the file's last line: a blind spot. service.beta.kubernetes.io/aws-load-balancer-internalannotation in the same file as a LoadBalancerSKIP Any one of three internal annotations (AWS, GCP, Azure) clears the LoadBalancer finding. token: ""in charts/api/values.yamlSKIP The Helm patterns need at least one non-quote character after the colon. Placeholders are skipped too.

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.

Line the check matched, and the line after the fix findings in this file: 7 findings in this file: 6 findings in this file: 5 findings in this file: 4 findings in this file: 3 findings in this file: 2 findings in this file: 1 findings in this file: 0 Privileged containerprivileged: trueprivileged: falseFLAGclear Runs as rootrunAsUser: 0runAsUser: 1000FLAGclear Host namespaceshostNetwork: truehostNetwork: falseFLAGclear Secret in an env valuevalue: "s3cretpass"valueFrom: { secretKeyRef: { name: db, key: password } }FLAGclear :latest or untagged imageimage: myorg/api:latestimage: myorg/api:1.4.2FLAGclear ALL capabilities addedadd: ["ALL"]drop: ["ALL"]FLAGclear LoadBalancer, not internalspec: { type: LoadBalancer }annotations: { service.beta.kubernetes.io/aws-load-balancer-internal: "true" }FLAGclear

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

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.

Real-world breaches

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"

Checks and limits

Every check, with the severity and confidence the code assigns.

CheckFires whenSeverityConf.
Privilegedprivileged: trueCritical0.95
RootrunAsUser: 0 or runAsNonRoot: falseHigh0.90
Host namespacehostNetwork: true, hostPID: true (one finding each)High0.90
Env secretname with PASSWORD, SECRET, API_KEY, TOKEN, or a DB URL, then a literal value:Critical0.85
Image tag:latest anywhere; an untagged image only on the file's last lineLow0.65
ALL capabilitiescapabilities: then add: then ALLCritical0.95
LoadBalancertype: LoadBalancer with no internal annotationMedium0.75
Helm valuesplaintext password, secret or API key, token or database URL in values.yamlHigh0.80
Docs and code

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.