Kubernetes manifest — source audit

Kubernetes manifest — source audit

TL;DR: Kubernetes manifest audit reads YAML for Pod-spec privileges (root, host-namespaces, capabilities), RBAC over-grants, secrets in env vs Secret refs, missing NetworkPolicies, NetworkPolicy gaps, ingress / LoadBalancer surface, and admission-policy bypasses. The high-value findings are usually in Pod SecurityContexts and ClusterRoleBindings, not where you’d grep for “password”. Companion to terraform-and-iac-source-audit and container-escape-techniques.

What’s in scope

1
2
3
find . -name '*.yaml' -o -name '*.yml' | xargs grep -l 'apiVersion: '
find . -name 'Chart.yaml' -o -name 'values.yaml' -o -path '*/templates/*'
find . -name 'kustomization.yaml'

For helm charts, also render: helm template ./chart --debug | tee rendered.yaml. The template often hides defaults that audit must catch.

Bug class 1 — privileged Pod

1
2
3
4
5
6
7
# BAD
spec:
  containers:
    - name: app
      image: ...
      securityContext:
        privileged: true                # full host kernel access

Grep:

1
grep -rn 'privileged:\s*true' .

Privileged containers can mount any host device, load kernel modules, escape the container trivially. Should appear only in CNI daemons, storage drivers, or audited debug Pods.

Bug class 2 — root Pod

1
2
3
4
5
6
# BAD — defaults
spec:
  containers:
    - name: app
      image: ...
      # no securityContext.runAsNonRoot → may run as root

Audit:

1
2
grep -rn 'runAsUser:\s*0' .
grep -rn 'runAsNonRoot:' .

Good shape:

1
2
3
4
5
6
7
8
9
securityContext:
  runAsNonRoot: true
  runAsUser: 10001
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities:
    drop: ["ALL"]
  seccompProfile:
    type: RuntimeDefault

Bug class 3 — host namespaces

1
2
3
4
spec:
  hostNetwork: true      # BAD — pod is on the host network
  hostPID: true          # BAD — sees all host processes
  hostIPC: true          # BAD — shares IPC namespace

hostNetwork: true makes container = host’s network namespace, defeating NetworkPolicies and exposing all host network services. Required only by some CNIs and node-exporter-style agents.

Bug class 4 — hostPath mounts

1
2
3
4
volumes:
  - name: docker
    hostPath:
      path: /var/run/docker.sock     # BAD — container can talk to the host's container runtime

Mounting the container runtime socket is a clean container escape. Other dangerous paths:

  • / (or /host) — full host filesystem.
  • /var/lib/kubelet — kubelet credentials.
  • /etc — host configuration.
  • /proc — host process info-leak.

Grep:

1
grep -rnB1 -A3 'hostPath:' .

Bug class 5 — capabilities

1
2
3
securityContext:
  capabilities:
    add: ["SYS_ADMIN", "NET_ADMIN", "SYS_PTRACE"]    # BAD

SYS_ADMIN is essentially “root”. NET_ADMIN, SYS_PTRACE, SYS_MODULE, DAC_READ_SEARCH are individually dangerous.

Audit:

1
grep -rnA3 'capabilities:' .

The default capability set in Docker (cap_chown, cap_dac_override, cap_fowner, cap_kill, cap_net_bind_service, etc.) is already broader than most workloads need. drop: ["ALL"] then add back the few you actually use.

Bug class 6 — RBAC over-grants

1
2
3
4
5
6
7
8
9
10
11
12
# BAD — cluster admin to a service account
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: app-binding
roleRef:
  kind: ClusterRole
  name: cluster-admin
subjects:
  - kind: ServiceAccount
    name: default
    namespace: app

Service accounts bound to cluster-admin are “owned-cluster” with a single Pod compromise.

Greps:

1
2
3
4
grep -rnB2 -A5 'cluster-admin' .
grep -rnE 'resources:\s*\[\s*"\*"\s*\]' .       # ClusterRole with all resources
grep -rnE 'verbs:\s*\[\s*"\*"\s*\]' .            # all verbs
grep -rn 'NonResourceURLs.*\*' .

Common ClusterRole over-grants:

  • pods/execexec into any pod = code exec across cluster.
  • pods/portforward — proxy traffic anywhere.
  • secrets (get/list/watch) at cluster scope — read all secrets.
  • serviceaccounts/token — mint tokens for any service account.

Bug class 7 — secrets in env vs Secret refs

1
2
3
4
5
6
7
8
9
10
11
12
# BAD
env:
  - name: DB_PASSWORD
    value: "Hunter2"

# OK
env:
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: db-secret
        key: password

Audit:

1
grep -rnE 'env:' . -A20 | grep -E 'PASSWORD|TOKEN|SECRET|KEY' | grep -v secretKeyRef

Even with Secret refs, audit who can read the namespace’s secrets via RBAC.

Bug class 8 — missing NetworkPolicy

Kubernetes is open-by-default — every Pod can talk to every other Pod and reach the API server. NetworkPolicies are the only in-cluster network boundary.

1
grep -rn 'kind: NetworkPolicy' .

A namespace without a default-deny NetworkPolicy is a flat network. Default-deny shape:

1
2
3
4
5
6
7
8
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: app
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]

Then explicitly allow each needed flow.

Bug class 9 — ingress / LoadBalancer surface

1
2
spec:
  type: LoadBalancer        # provisions cloud LB → public IP

Audit:

  • Is the workload supposed to be public?
  • Is there an Ingress with annotations limiting the source IPs?
  • For Ingress controllers, what’s the tls: config?

Bug class 10 — admission policy bypasses

Pod Security Admission (PSA) replaces the older PodSecurityPolicy. Namespaces are labelled:

1
2
3
4
metadata:
  labels:
    pod-security.kubernetes.io/enforce: privileged      # BAD — anything goes
    pod-security.kubernetes.io/enforce: restricted      # good

Audit:

1
grep -rn 'pod-security.kubernetes.io' .

Without restricted (or a custom OPA/Kyverno policy enforcing equivalent), the cluster has no admission-time guardrails.

Bug class 11 — image policy

1
2
3
4
5
6
spec:
  containers:
    - image: nginx                # BAD: no tag, no digest
    - image: nginx:latest         # BAD: mutable tag
    - image: nginx:1.25           # OK if pinned in registry
    - image: nginx@sha256:abc...  # best: digest pin

Combined with image signature verification (Sigstore / cosign), digest pinning is the only honest answer.

Bug class 12 — Service Account token auto-mount

1
2
3
4
# By default, every Pod mounts its SA token at /var/run/secrets/kubernetes.io/serviceaccount/
# A compromised Pod uses that token against the API server.
spec:
  automountServiceAccountToken: false       # for workloads that don't talk to the API

Audit Pods that don’t need API access; they should disable automounting.

Bug class 13 — etcd / kubelet exposure

These are infrastructure findings but show up in Helm charts that ship etcd / kubelet config:

  • etcd encryption at rest enabled?
  • kubelet authorization-mode: AlwaysAllow?
  • Anonymous auth on kubelet?

Tools

  • kube-bench — CIS benchmark for clusters (not manifests).
  • kube-score — manifest linter for security/best-practice.
  • kubesec — risk score per manifest.
  • kubeaudit — Pod-spec issues.
  • trivy config — YAML config scanning.
  • checkov / kics — multi-format including K8s.
  • OPA / Gatekeeper / Kyverno — policy-as-code admission control.

Source-audit checklist

  • No privileged: true outside daemons that justify it.
  • All workloads runAsNonRoot: true, allowPrivilegeEscalation: false.
  • No hostNetwork, hostPID, hostIPC.
  • No risky hostPath mounts.
  • Capabilities drop: ["ALL"] then explicit add.
  • No cluster-admin to non-admin Service Accounts.
  • No verbs: "*" / resources: "*" ClusterRoles unjustified.
  • Secrets only via Secret refs, not env values.
  • Every namespace has default-deny NetworkPolicy.
  • LoadBalancer/Ingress exposure is intentional and scoped.
  • PSA labels at restricted or custom OPA equivalent.
  • Images pinned by tag or digest, signed where possible.
  • automountServiceAccountToken: false for non-API workloads.

References