kubernetes
The CIS-aligned Kubernetes security baseline we ship on day one
Pod Security Standards, Kyverno policies, NetworkPolicies, audit logging — the controls we apply to every customer cluster before workloads arrive.
20 mai 2026 · 9 min · par Sudhanshu K.
The CIS-aligned Kubernetes security baseline we ship on day one
Most Kubernetes clusters in production weren't hardened — they were deployed. Default settings, default service account permissions, no admission policies, no network policies, no audit log. They work. They also pass a basic pen test in about 12 minutes.
This post is the security baseline we ship on day one for every cluster we manage. It's CIS Kubernetes Benchmark v1.10 aligned, but pragmatic — we've cut the items that are obsolete or cause more operational pain than security gain in 2026, and added a few things CIS doesn't cover yet.
If you run a Kubernetes cluster of any size, every item below is either "do this" or "have a documented reason for not doing it." There's no third option.
Pod Security Standards in restricted mode
PodSecurityPolicy is dead. Its replacement, Pod Security Standards enforced at the namespace level via pod-security.kubernetes.io/* labels, is the baseline for 2026:
apiVersion: v1
kind: Namespace
metadata:
name: workloads
labels:
pod-security.kubernetes.io/enforce: "restricted"
pod-security.kubernetes.io/enforce-version: "v1.29"
pod-security.kubernetes.io/audit: "restricted"
pod-security.kubernetes.io/warn: "restricted"restricted mode rejects pods that:
- Run as root
- Have
allowPrivilegeEscalation: true - Use host networking, host PID, or host IPC
- Don't drop
ALLcapabilities (and add only specific ones back) - Don't set
seccompProfile: RuntimeDefault - Mount writeable hostPath volumes
For the small minority of workloads that genuinely need elevated privileges (a CSI driver, a CNI agent, a node-local monitoring daemon), we put them in their own namespace with the privileged profile, and we treat that namespace's contents as part of the cluster's TCB — heavily scrutinized, slowly changed, no random new YAML.
Kyverno: admission policies as code
PSS handles the well-trodden controls, but you want admission policies tailored to your environment. Kyverno is our pick over OPA Gatekeeper for one reason: policies are pure YAML, no Rego. Engineers can write and read them.
Our baseline Kyverno policies (excerpted):
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-non-root
spec:
validationFailureAction: Enforce
rules:
- name: check-non-root
match:
any:
- resources: { kinds: [Pod] }
validate:
message: "Pods must run as non-root"
pattern:
spec:
securityContext:
runAsNonRoot: true
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-image-signatures
spec:
validationFailureAction: Enforce
rules:
- name: verify-signatures
match:
any:
- resources: { kinds: [Pod] }
verifyImages:
- imageReferences: ["ghcr.io/example/*"]
attestors:
- entries:
- keyless:
subject: "https://github.com/example/repo/.github/workflows/build.yml@refs/heads/main"
issuer: "https://token.actions.githubusercontent.com"That last one is critical and underused: require signed images at admission time, with keyless Cosign signatures tied to a specific GitHub Actions workflow OIDC identity. An attacker who steals a registry credential cannot push a malicious image that the cluster will run, because they can't forge the OIDC signature.
This is the cybersecurity baseline we run for every managed Kubernetes customer — verifiable supply chain from commit to running pod.
NetworkPolicies: default-deny, opt-in connectivity
In a default Kubernetes cluster, every pod can talk to every other pod. That's not the model you want.
The baseline:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: workloads
spec:
podSelector: {}
policyTypes: [Ingress, Egress]Apply this to every workload namespace and nothing talks to anything until you explicitly allow it. Then layer in specific allow policies:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: app-allow-dns
namespace: workloads
spec:
podSelector:
matchLabels: { app: my-app }
policyTypes: [Egress]
egress:
- to:
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: kube-system }
podSelector:
matchLabels: { k8s-app: kube-dns }
ports:
- protocol: UDP
port: 53Yes, this is more work upfront. Yes, it's worth it. The blast radius of a compromised pod drops from "the entire cluster" to "the explicitly-named services that pod is allowed to talk to."
For EKS customers we use the AWS VPC CNI's native NetworkPolicy support; for GKE and AKS we run Cilium, which gives us L7 policies on top of L3/L4.
Audit logging: capture everything, alert on the right things
Default audit policy is too quiet. The baseline policy we ship:
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
- RequestReceived
rules:
# High-priority: secret reads
- level: Request
resources:
- group: ""
resources: ["secrets", "configmaps"]
verbs: ["get", "list", "watch"]
# Privileged operations
- level: RequestResponse
resources:
- group: ""
resources: ["pods/exec", "pods/portforward", "pods/proxy"]
# Service account token requests
- level: Request
resources:
- group: ""
resources: ["serviceaccounts/token"]
# RBAC changes
- level: RequestResponse
resources:
- group: "rbac.authorization.k8s.io"
resources: ["roles", "rolebindings", "clusterroles", "clusterrolebindings"]
verbs: ["create", "update", "patch", "delete"]
# Everything else at Metadata level
- level: Metadata
omitStages: ["RequestReceived"]Audit logs get shipped to a SIEM (we run Loki for some customers, Splunk for others, Datadog for the rest). Alerts fire on:
- Any read of a
Secretthat's not from a known-good ServiceAccount (this catches credential exfiltration) execinto a production pod by a human user- RBAC binding changes outside of a GitOps-driven sync window
- Service account token requests with unusual scopes
These are the alerts that catch the actual attacks, as opposed to the noise (someone scanned port 22 on a node — yes, the internet is scanning your nodes, that's not an incident).
ServiceAccount tokens: stop using default
A surprisingly common finding in our pen-test engagements: production deployments using default ServiceAccount, which gets a token mounted into every pod, and that token has — in older clusters — surprisingly broad RBAC.
The fix is in two parts:
- Disable auto-mounting of the default SA token at the namespace level:
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: workloads
automountServiceAccountToken: false- Create purpose-specific SAs for workloads that need API access:
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-app
namespace: workloads
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: my-app
namespace: workloads
rules:
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["my-app-config"]
verbs: ["get"] # ONLY get, ONLY this configmapCombine with token request audiences so a token issued for talking to the Kubernetes API can't be reused for talking to an external service.
etcd encryption at rest
If etcd is unencrypted, anyone with read access to the etcd data directory can extract every Secret in the cluster as plaintext. For cloud-managed control planes (EKS, GKE, AKS), this is handled — but you need to verify with the provider's KMS encryption flag set, not just the default.
For self-managed clusters, this is your responsibility:
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources: ["secrets"]
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-32-byte-key>
- identity: {}Pair this with key rotation every 90 days — otherwise the encryption is theatre.
What we don't ship
To close the loop, the controls you'll see in some CIS-aligned guides that we don't enforce by default:
- Service mesh mTLS for east-west traffic everywhere — useful for specific compliance contexts, but Istio/Linkerd add operational complexity that doesn't pay off for many workloads. NetworkPolicies + cluster-internal TLS termination are usually enough.
- Hostname verification on every pod-to-pod call — overkill; rely on NetworkPolicies and pod identity.
- Restricting
kubectl execto a tiny allowlist of users — engineers will work around it (port-forward into a debug pod), and the audit log catches the legitimate uses anyway.
The point of a security baseline isn't to maximize controls. It's to apply the controls that materially change attacker economics — and skip the ones that mainly produce yellow warnings in compliance dashboards.
If your cluster currently has fewer than half of these in place, you're not unusual. But the gap matters more in 2026 than it did three years ago — Kubernetes is now a primary target, not a curiosity. We're happy to do the baseline assessment as a one-off; it usually surfaces 8-15 control gaps and gives you a prioritized remediation plan.
Sudhanshu K. leads Kubernetes security engagements at EdgeServers (RemotIQ Pty Ltd, ABN 91 682 628 128). She has hardened more EKS, GKE, and AKS clusters than she has hairs left.