Pular para o conteúdo
EdgeServers
Blog

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 de maio de 2026 · 9 min · por 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 ALL capabilities (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: 53

Yes, 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 Secret that's not from a known-good ServiceAccount (this catches credential exfiltration)
  • exec into 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:

  1. Disable auto-mounting of the default SA token at the namespace level:
apiVersion: v1
kind: ServiceAccount
metadata:
  name: default
  namespace: workloads
automountServiceAccountToken: false
  1. 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 configmap

Combine 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 exec to 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.