11.4 Review Kubernetes Security Configuration
11.4 - Review Kubernetes Security Configuration
Kubernetes security is expressed in YAML manifests, Helm values, and operator CRDs—not only in container images. Review who can call the API (RBAC), which pods may talk to each other (NetworkPolicy), which capabilities pods may use (Pod Security Admission), and how sensitive values reach workloads. Production services should mount secrets as volumes, not plain environment variables, when the threat model includes node-level access or process listing.
What This Misconfiguration Is
Cluster misconfiguration allows pods or users to exceed the intended trust boundary. A ClusterRoleBinding to cluster-admin for a default service account, missing NetworkPolicy in a multi-tenant namespace, or secrets injected through env vars visible in /proc are platform flaws that application code cannot fix alone.
The unsafe assumption is that container isolation replaces network and API authorization. Without NetworkPolicy, any compromised pod may reach the Kubernetes API, metadata services, or peer databases on the flat pod network. This maps to CWE-284 (Improper Access Control) and CWE-526 (Exposure of Sensitive Information Through Environmental Variables).
Vulnerability Characteristics (Where to Identify Them)
| Signal | Where to look |
|---|---|
| RBAC escalation | cluster-admin bindings, * verbs on secrets or pods, bind to system:anonymous |
| Default service accounts | Workloads using namespace default SA with automounted token |
| Network flatness | No NetworkPolicy in namespace hosting sensitive workloads |
| Pod privileges | privileged: true, hostPID, hostNetwork, allowPrivilegeEscalation |
| Secrets in env | env.valueFrom.secretKeyRef for DB passwords in Production manifests |
| Image trust | :latest tags, missing digest pin, no admission image signature check |
| PSA gaps | Namespace without pod-security.kubernetes.io/enforce=restricted |
| Host path mounts | Docker socket or /etc/kubernetes mounted into application pods |
Misconfiguration Examples
Use these when reviewing Helm charts, Kustomize overlays, and cluster bootstrap manifests.
Example 1: cluster-admin for default service account
subjects:
- kind: ServiceAccount
name: default
namespace: production
roleRef:
kind: ClusterRole
name: cluster-admin
Any pod using default SA in production can read all secrets cluster-wide and deploy workloads.
Example 2: Secret via projected volume misconfiguration
volumes:
- name: db-credentials
projected:
sources:
- secret:
name: db-credentials
items:
- key: password
path: password
mode: 0444 # world-readable inside container
containers:
- name: api
volumeMounts:
- name: db-credentials
mountPath: /etc/secrets
readOnly: true
# Application still logs mount path contents on startup — review app code
Example 3: Privileged pod without NetworkPolicy
containers:
- name: api
securityContext:
privileged: true
image: myregistry/api:latest
No NetworkPolicy in namespace—privileged pod may reach metadata service, kube-api, and all pod IPs.
Example 4: Missing Pod Security Admission
Namespace without pod-security.kubernetes.io/enforce: restricted allows hostPath, root user, and capability adds in production.
Example 5: Automounted API token on every pod
Default service account with automount enabled and Role granting secrets list in all namespaces—lateral movement after one compromise.
SDK/IaC Sinks and Dangerous Patterns
Kubernetes YAML (manifest sinks)
# RBAC
verbs: ["*"]; resources: ["*"]; apiGroups: ["*"]
clusterRoleRef.name: cluster-admin
# Pod spec
privileged: true
hostNetwork: true
hostPID: true
automountServiceAccountToken: true # default SA
# Secrets
env.valueFrom.secretKeyRef
envFrom.secretRef # bulk env injection
# Images
image: myapp:latest # no digest
Also review: hostPath mounts, capabilities.add: ["SYS_ADMIN"], runAsUser: 0, wildcard Ingress to admin interfaces.
Python (kubernetes client)
from kubernetes import client, config
config.load_incluster_config()
v1 = client.CoreV1Api()
v1.list_secret_for_all_namespaces() # over-permissioned RBAC
secret = v1.read_namespaced_secret("db-credentials", "production")
print(secret.data) # logged
password = os.environ["DATABASE_PASSWORD"]
Also review: kubectl in CI with cluster-admin kubeconfig, Helm --set database.password=....
Java (fabric8 kubernetes-client)
KubernetesClient client = new KubernetesClientBuilder().build();
client.secrets().inAnyNamespace().list();
String password = System.getenv("DATABASE_PASSWORD");
Also review: Spring Cloud Kubernetes secret config import to env vars in prod profile.
Terraform / Helm
resource "kubernetes_cluster_role_binding" "app_admin" {
role_ref { name = "cluster-admin" }
subject { name = "default" }
}
# values.prod.yaml — reintroduces dev RBAC
rbac:
create: true
clusterAdmin: true
secrets:
exposeAsEnv: true
Also review: helm template output before apply, Kustomize patchesStrategicMerge promoting dev overlays.
C# (k8s client / ASP.NET)
var k8s = new Kubernetes(KubernetesClientConfiguration.InClusterConfig());
var secrets = await k8s.CoreV1.ListSecretForAllNamespacesAsync();
var signingKey = Environment.GetEnvironmentVariable("JWT_SIGNING_KEY");
See Kubernetes Python client, fabric8 kubernetes-client, RBAC good practices, and OPA Gatekeeper.
Sample Vulnerable Configuration in Python
Policy-as-code checks (OPA Rego, Python validators in CI) catch risky manifests before deploy.
import sys
import yaml
from pathlib import Path
def review_manifest(doc: dict, path: str) -> list[str]:
findings: list[str] = []
kind = doc.get("kind")
meta = doc.get("metadata", {})
name = meta.get("name", "<noname>")
ns = meta.get("namespace", "default")
if kind == "ClusterRoleBinding":
subj = doc.get("subjects", [])
role = doc.get("roleRef", {})
if role.get("name") == "cluster-admin":
findings.append(f"{path}: ClusterRoleBinding {name} grants cluster-admin")
if kind in ("Deployment", "StatefulSet", "Pod"):
spec = doc.get("spec", {})
pod_spec = spec.get("template", spec).get("spec", spec)
for c in pod_spec.get("containers", []):
for env in c.get("env", []):
if "valueFrom" in env and "secretKeyRef" in env["valueFrom"]:
findings.append(
f"{path}: {kind}/{name} secret {env['name']} via env in ns {ns}"
)
sec = c.get("securityContext", {})
if sec.get("privileged") or pod_spec.get("hostNetwork"):
findings.append(f"{path}: {kind}/{name} privileged or hostNetwork")
if kind == "NetworkPolicy":
return findings # presence is good; checked at namespace level
return findings
def namespace_lacks_netpol(docs: list[dict]) -> bool:
has_policy = any(d.get("kind") == "NetworkPolicy" for d in docs)
has_workload = any(d.get("kind") in ("Deployment", "StatefulSet") for d in docs)
return has_workload and not has_policy
if __name__ == "__main__":
for arg in sys.argv[1:]:
p = Path(arg)
docs = list(yaml.safe_load_all(p.read_text()))
if namespace_lacks_netpol(docs):
print(f"{arg}: workload without NetworkPolicy")
for doc in docs:
if doc:
for f in review_manifest(doc, arg):
print(f)
Step-by-Step Review Walkthrough
- Inventory namespaces and labels. Note which namespaces host production data services and whether Pod Security Admission enforce labels are set.
- Read RBAC bindings. Follow
RoleBindingandClusterRoleBindingsubjects to service accounts used by running Deployments. - Check service account token mount. Prefer
automountServiceAccountToken: falseunless the pod needs Kubernetes API access. - Verify NetworkPolicy default deny. Each sensitive namespace should deny all ingress and egress by default, then allow explicit ports and label selectors.
- Inspect pod security context. Read
securityContextat pod and container level; flag privileged mode, root user, and writable root filesystem. - Review secret delivery. Production DB and API keys should use
volumes.secretmounts; env vars are acceptable only for non-sensitive config with documented rationale. - Cross-check Helm/Kustomize overlays. Production overlay may reintroduce dev-only permissive RBAC if reviewers read only the base chart.
Risk Impact Analysis
Cluster compromise. cluster-admin on a compromised pod service account enables secret theft, workload deployment, and persistence across namespaces.
Lateral movement. Missing NetworkPolicy lets attackers scan internal services, reach unmanaged databases, and hit cloud metadata endpoints from any pod.
Credential exposure. Secrets in environment variables appear in process listings, debug endpoints, crash dumps, and CI-rendered manifest diffs.
Container breakout impact. Privileged pods increase blast radius when a kernel or runtime vulnerability is exploited.
Multi-tenant bleed. Shared clusters without policy and PSA enforcement allow one team's pod to read another team's secrets or APIs.
Vulnerable Examples in Other Formats
Kubernetes YAML (RBAC and secrets)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: app-admin-binding
subjects:
- kind: ServiceAccount
name: default
namespace: production
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: production
spec:
template:
spec:
serviceAccountName: default
containers:
- name: api
image: myregistry/api:latest
env:
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
securityContext:
privileged: true
No NetworkPolicy in namespace; default service account; secret via env; privileged container.
NetworkPolicy (missing default deny)
Namespaces with workloads but zero NetworkPolicy resources allow unrestricted pod-to-pod traffic.
Java (application integration)
// Reads DB password from env — visible in container inspect and /proc
String password = System.getenv("DATABASE_PASSWORD");
// Kubernetes client uses default SA token with cluster-wide list secrets permission
@Configuration
public class K8sConfig {
@Bean
KubernetesClient client() {
return new KubernetesClientBuilder().build(); // in-cluster config
}
}
// Elsewhere: client.secrets().inAnyNamespace().list(); // over-permissioned RBAC
C# (application integration)
// ASP.NET reads JWT signing key from environment variable in Production
var signingKey = Environment.GetEnvironmentVariable("JWT_SIGNING_KEY");
// Mounted volume alternative exists in dev overlay only — prod still uses env
Fix: Safer Patterns and Libraries to Use
Kubernetes YAML
Apply least-privilege RBAC, restricted Pod Security, default-deny networking, and volume-mounted secrets per RBAC good practices and NetworkPolicy.
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: api
namespace: production
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: api-role
namespace: production
rules:
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["api-config"]
verbs: ["get"]
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: production
spec:
template:
spec:
serviceAccountName: api
automountServiceAccountToken: false
containers:
- name: api
image: myregistry/api@sha256:abc123...
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
volumeMounts:
- name: db-credentials
mountPath: /etc/secrets/db
readOnly: true
volumes:
- name: db-credentials
secret:
secretName: db-credentials
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {}
policyTypes: [Ingress, Egress]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-api-to-db
namespace: production
spec:
podSelector:
matchLabels:
app: api
egress:
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
policyTypes: [Egress]
Important: Mount secrets as files under /etc/secrets/... and set file permissions with defaultMode. Application reads the file at startup; avoid logging contents.
Python
Read mounted secret files in application code; validate manifests in CI with the reviewer above or OPA Gatekeeper.
from pathlib import Path
def read_secret(name: str) -> str:
path = Path(f"/etc/secrets/db/{name}")
return path.read_text(encoding="utf-8").strip()
def connect_db():
password = read_secret("password")
# use password in connection setup; never log it
Java
Use Files.readString on mounted paths; restrict Kubernetes client RBAC to required verbs.
String password = Files.readString(Path.of("/etc/secrets/db/password")).trim();
@Configuration
public class K8sConfig {
@Bean
KubernetesClient client() {
return new KubernetesClientBuilder().build();
}
}
// Role should allow get on configmaps/api-config only — not secrets in all namespaces
C
Read secrets from mounted files via File.ReadAllText; keep signing keys out of environment variables in production overlays.
var signingKey = File.ReadAllText("/etc/secrets/jwt/signing-key").Trim();
Follow Pod Security Standards restricted profile for production namespaces unless a documented exception exists.
Verify During Review
- RBAC grants minimum verbs on named resources; no
cluster-adminfor application service accounts. - Service accounts are dedicated per workload; default SA is not used for production pods.
- NetworkPolicy implements default deny plus explicit allow rules for required traffic only.
- Pod Security Admission enforces baseline or restricted profile on production namespaces.
- Production secrets are volume-mounted files, not environment variables, unless risk is accepted in writing.
- Containers run non-root, without privileged mode or unnecessary host namespaces.
- Images use digest pins or immutable tags;
:latestis not deployed to production. - CI runs policy-as-code checks on manifests before apply.