Loading learning content...
Kubernetes has transformed how we deploy and manage applications, but it has also introduced new security challenges. A Kubernetes cluster runs with significant privileges, orchestrates access to sensitive data, and exposes a large attack surface across nodes, networks, and control plane components.
The stakes are high. A compromised Kubernetes cluster can lead to:
Kubernetes security requires defense in depth—multiple layers of protection so that a breach in one layer doesn't compromise the entire system. This page covers the essential security controls for production Kubernetes operations.
By the end of this page, you'll understand how to implement Pod Security Standards, configure RBAC for least privilege, enforce network segmentation with Network Policies, manage secrets securely, harden container images, protect the supply chain, and implement runtime security monitoring.
Kubernetes security spans multiple layers, from the cluster infrastructure through application code. Understanding these layers helps you build comprehensive protection.
The 4 C's of Cloud-Native Security:
Each layer must be secured—a vulnerability at any layer can compromise the entire stack.
| Layer | Key Controls | Kubernetes Components |
|---|---|---|
| Cloud/Datacenter | Network segmentation, IAM, encryption at rest | Node VMs, storage, load balancers |
| Cluster | API authentication, RBAC, audit logging, admission control | API server, etcd, kubelet |
| Container | Pod Security, image scanning, runtime policies | Pods, containers, images |
| Code | Secrets management, dependency scanning, input validation | Application code, ConfigMaps, Secrets |
In managed Kubernetes (EKS, GKE, AKS), the cloud provider secures the control plane. You're responsible for worker node security, pod security, network policies, RBAC, and application security. In self-managed clusters, you own everything.
Pod Security Standards (PSS) define three profiles that restrict what pods can do. Pod Security Admission (PSA) enforces these standards at the namespace level, replacing the deprecated PodSecurityPolicy.
The Three Profiles:
1. Privileged (Unrestricted)
2. Baseline (Minimally Restrictive)
3. Restricted (Highly Restrictive)
123456789101112131415161718192021222324252627282930313233343536
# Apply Pod Security Standards via namespace labelsapiVersion: v1kind: Namespacemetadata: name: production labels: # Enforce restricted profile (block non-compliant pods) pod-security.kubernetes.io/enforce: restricted pod-security.kubernetes.io/enforce-version: latest # Warn on violations (log but allow) pod-security.kubernetes.io/warn: restricted pod-security.kubernetes.io/warn-version: latest # Audit violations (add to audit log) pod-security.kubernetes.io/audit: restricted pod-security.kubernetes.io/audit-version: latest ---# Namespace for system components (privileged)apiVersion: v1kind: Namespacemetadata: name: kube-system labels: pod-security.kubernetes.io/enforce: privileged ---# Development namespace (baseline for flexibility)apiVersion: v1kind: Namespacemetadata: name: development labels: pod-security.kubernetes.io/enforce: baseline pod-security.kubernetes.io/warn: restricted # Warn about restricted violations12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
# Pod that complies with Restricted profileapiVersion: v1kind: Podmetadata: name: secure-app namespace: productionspec: # No host namespaces hostNetwork: false hostPID: false hostIPC: false securityContext: # Run as non-root runAsNonRoot: true runAsUser: 1000 runAsGroup: 1000 fsGroup: 1000 # Secure sysctls only sysctls: [] # Seccomp profile (required for restricted) seccompProfile: type: RuntimeDefault containers: - name: app image: myapp:v1.0.0 securityContext: # Cannot become root allowPrivilegeEscalation: false # Run as non-root user runAsNonRoot: true runAsUser: 1000 # Drop all capabilities capabilities: drop: - ALL # Read-only root filesystem readOnlyRootFilesystem: true # Privileged = false (default, but explicit) privileged: false # If app needs to write, use emptyDir volumeMounts: - name: tmp mountPath: /tmp - name: cache mountPath: /app/cache volumes: - name: tmp emptyDir: {} - name: cache emptyDir: {}Start with 'warn' mode to identify violations without blocking deployments. Fix applications one by one, then move to 'enforce'. Many applications need changes: run as non-root user, don't write to filesystem (use emptyDir), don't require capabilities.
RBAC (Role-Based Access Control) is Kubernetes' authorization mechanism. It controls who can do what on which resources. Properly configured RBAC is essential—overly permissive RBAC is a primary attack vector.
RBAC Components:
Subjects (Who):
Principle of Least Privilege:
Every identity should have only the minimum permissions needed. Start with no access, add specific permissions as needed. Never grant cluster-admin to applications.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
# Role: permissions within a namespaceapiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: namespace: production name: deployment-managerrules:# Can manage deployments- apiGroups: ["apps"] resources: ["deployments"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]# Can view pods and logs- apiGroups: [""] resources: ["pods", "pods/log"] verbs: ["get", "list", "watch"]# Can view services- apiGroups: [""] resources: ["services"] verbs: ["get", "list", "watch"] ---# RoleBinding: grant role to a groupapiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: name: deployment-managers-binding namespace: productionsubjects:# Grant to a group (managed via OIDC/IdP)- kind: Group name: deployment-managers apiGroup: rbac.authorization.k8s.ioroleRef: kind: Role name: deployment-manager apiGroup: rbac.authorization.k8s.io ---# ServiceAccount for application podsapiVersion: v1kind: ServiceAccountmetadata: name: myapp-sa namespace: productionautomountServiceAccountToken: false # Opt-in only ---# Role for the application (minimal permissions)apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: namespace: production name: myapp-rolerules:# Only read configmaps it needs- apiGroups: [""] resources: ["configmaps"] resourceNames: ["myapp-config"] # Specific resource names! verbs: ["get", "watch"]# Only read its own secret- apiGroups: [""] resources: ["secrets"] resourceNames: ["myapp-credentials"] verbs: ["get"] ---# Bind role to service accountapiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: name: myapp-binding namespace: productionsubjects:- kind: ServiceAccount name: myapp-sa namespace: productionroleRef: kind: Role name: myapp-role apiGroup: rbac.authorization.k8s.ioresources: ['*'] grants access to everything in the API groupverbs: ['*'] includes delete, escalate, impersonateUse tools like kubectl-who-can (who can delete pods?), rbac-lookup (what permissions does this user have?), and rakkess (show access matrix) to audit your RBAC configuration. Regular audits catch permission creep.
By default, all pods in a Kubernetes cluster can communicate with each other—no network segmentation. Network Policies implement microsegmentation, controlling which pods can talk to which other pods or external endpoints.
Why Network Policies Matter:
Network Policy Behavior:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
# Default deny all ingress (baseline security)apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: default-deny-ingress namespace: productionspec: podSelector: {} # Applies to all pods in namespace policyTypes: - Ingress # Only affects ingress; egress still allowed ---# Default deny all egress (strict environments)apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: default-deny-egress namespace: productionspec: podSelector: {} policyTypes: - Egress ---# Allow frontend to receive traffic from ingress controllerapiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-frontend-ingress namespace: productionspec: podSelector: matchLabels: app: frontend policyTypes: - Ingress ingress: - from: - namespaceSelector: matchLabels: name: ingress-nginx # Traffic from ingress namespace podSelector: matchLabels: app: ingress-nginx # Specifically from ingress controller pods ports: - protocol: TCP port: 8080 ---# Allow frontend to talk to backend APIapiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: frontend-to-backend namespace: productionspec: podSelector: matchLabels: app: frontend policyTypes: - Egress egress: - to: - podSelector: matchLabels: app: backend ports: - protocol: TCP port: 8080 ---# Allow backend to reach databaseapiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: backend-to-database namespace: productionspec: podSelector: matchLabels: app: backend policyTypes: - Egress egress: - to: - podSelector: matchLabels: app: postgres ports: - protocol: TCP port: 5432 ---# Allow DNS resolution for all podsapiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-dns namespace: productionspec: podSelector: {} policyTypes: - Egress egress: - to: - namespaceSelector: {} podSelector: matchLabels: k8s-app: kube-dns ports: - protocol: UDP port: 53Network Policies require a CNI plugin that supports them. Calico, Cilium, and WeaveNet support Network Policies. The default kubenet and AWS VPC CNI (without add-ons) do NOT. Check your CNI before relying on Network Policies!
Kubernetes Secrets store sensitive data (passwords, tokens, certificates). However, native Kubernetes Secrets are only base64 encoded, not encrypted. Additional protection is essential.
Kubernetes Secrets Limitations:
Defense Layers for Secrets:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
# Encryption configuration for etcd (control plane config)# /etc/kubernetes/enc/enc.yamlapiVersion: apiserver.config.k8s.io/v1kind: EncryptionConfigurationresources: - resources: - secrets - configmaps providers: # Use AES-GCM with 256-bit key - aescbc: keys: - name: key1 secret: <base64-encoded-32-byte-key> # Fallback to identity (for reading old unencrypted secrets) - identity: {} ---# External Secrets Operator: sync from AWS Secrets ManagerapiVersion: external-secrets.io/v1beta1kind: ExternalSecretmetadata: name: database-credentials namespace: productionspec: refreshInterval: 1h secretStoreRef: name: aws-secrets-manager kind: SecretStore target: name: database-credentials # K8s secret to create creationPolicy: Owner data: - secretKey: password # Key in K8s secret remoteRef: key: prod/database/master # Path in Secrets Manager property: password # JSON property to extract ---# Sealed Secret (for GitOps - safe to commit to git)# Created with: kubeseal < secret.yaml > sealed-secret.yamlapiVersion: bitnami.com/v1alpha1kind: SealedSecretmetadata: name: database-password namespace: productionspec: encryptedData: # Encrypted with cluster's sealing key password: AgBOQ...encrypted...data... ---# CSI Secret Store (mount directly from Vault/AWS)apiVersion: secrets-store.csi.x-k8s.io/v1kind: SecretProviderClassmetadata: name: vault-database-credsspec: provider: vault parameters: vaultAddress: "https://vault.example.com:8200" roleName: "database-role" objects: | - objectName: "password" secretPath: "secret/data/production/database" secretKey: "password"Container images are the artifact you deploy. If an image is compromised—through malicious base images, vulnerable dependencies, or supply chain attacks—your cluster is compromised.
Image Security Controls:
:latest in production123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
# ValidatingAdmissionPolicy (K8s 1.26+)# Block images without digest or from untrusted registriesapiVersion: admissionregistration.k8s.io/v1kind: ValidatingAdmissionPolicymetadata: name: image-policyspec: failurePolicy: Fail matchConstraints: resourceRules: - apiGroups: [""] apiVersions: ["v1"] operations: ["CREATE", "UPDATE"] resources: ["pods"] validations: # Require images from trusted registry - expression: | object.spec.containers.all(c, c.image.startsWith('registry.example.com/') || c.image.startsWith('gcr.io/my-project/') ) message: "Images must be from trusted registries" # Require image digest (not just tag) - expression: | object.spec.containers.all(c, c.image.contains('@sha256:') ) message: "Images must use digest, not tag" ---# Kyverno policy (alternative to ValidatingAdmissionPolicy)apiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: require-signed-imagesspec: validationFailureAction: Enforce background: true rules: - name: verify-signature match: any: - resources: kinds: - Pod verifyImages: - imageReferences: - "registry.example.com/*" attestors: - count: 1 entries: - keys: publicKeys: |- -----BEGIN PUBLIC KEY----- <your cosign public key> -----END PUBLIC KEY----- ---# Image pull secrets for private registriesapiVersion: v1kind: Secretmetadata: name: registry-credentials namespace: productiontype: kubernetes.io/dockerconfigjsondata: .dockerconfigjson: <base64-encoded-docker-config> ---# ServiceAccount with imagePullSecretsapiVersion: v1kind: ServiceAccountmetadata: name: production-sa namespace: productionimagePullSecrets:- name: registry-credentials| Image Type | Example | Size | Use Case |
|---|---|---|---|
| Scratch | scratch | 0 MB | Static binaries (Go) |
| Distroless | gcr.io/distroless/static | ~2 MB | Static binaries with CA certs |
| Alpine | alpine:3.19 | ~5 MB | When shell/packages needed |
| Wolfi/Chainguard | cgr.dev/chainguard/static | ~2 MB | Hardened, minimal, updated |
| Ubuntu/Debian | ubuntu:22.04 | ~77 MB | Legacy apps requiring glibc |
The :latest tag is mutable—it can point to different images over time. This breaks reproducibility, makes rollbacks impossible, and can introduce untested changes. Always use immutable tags (v1.2.3) or digests (sha256:abc123).
Preventive controls (RBAC, Network Policies, Pod Security) reduce attack surface, but determined attackers may still get through. Runtime security detects and responds to malicious activity as it happens.
Runtime Security Tools:
What Runtime Security Detects:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
# Falco custom rules for Kubernetes security# Deploy Falco as DaemonSet with host access - rule: Shell Spawned in Container desc: Detect shell spawned inside a container (potential breakout attempt) condition: > spawned_process and container and shell_procs and not shell_allowed_container output: > Shell spawned in container (user=%user.name container=%container.name shell=%proc.name parent=%proc.pname cmdline=%proc.cmdline container_id=%container.id image=%container.image.repository) priority: WARNING tags: [container, shell, mitre_execution] - rule: Unexpected Outbound Connection desc: Detect container connecting to unexpected IP condition: > outbound and container and not allowed_outbound_ip and not k8s_known_connections output: > Unexpected outbound connection (command=%proc.cmdline connection=%fd.name container=%container.name image=%container.image.repository) priority: NOTICE - rule: Sensitive File Read desc: Detect read of sensitive files (credentials, keys) condition: > open_read and container and ( fd.name startswith /etc/shadow or fd.name startswith /etc/passwd or fd.name contains id_rsa or fd.name contains .kube/config or fd.name contains .aws/credentials ) output: > Sensitive file read (file=%fd.name command=%proc.cmdline container=%container.name) priority: WARNING - rule: Kubernetes Secret Mounted and Read desc: Detect when mounted K8s secrets are read unexpectedly condition: > open_read and container and fd.name startswith /var/run/secrets/kubernetes.io/serviceaccount and not k8s_trusted_processes output: > Kubernetes secret read (file=%fd.name proc=%proc.name container=%container.name pod=%k8s.pod.name namespace=%k8s.ns.name) priority: WARNING - rule: Cryptomining Detected desc: Detect cryptomining activity condition: > spawned_process and container and ( proc.name in (cryptominer_processes) or proc.cmdline contains "stratum+tcp://" or proc.cmdline contains "-o pool." or proc.cmdline contains "xmrig" or proc.cmdline contains "minerd" ) output: > Cryptomining process detected (command=%proc.cmdline container=%container.name image=%container.image.repository) priority: CRITICALDetection without response is just logging. Integrate Falco with: 1) Alerting (PagerDuty, Slack) for human response 2) Automated response (kill pod, isolate network) for critical threats 3) Forensics (capture state before cleanup). Note: Automated kill should be used carefully to avoid self-denial-of-service.
Kubernetes audit logs record all requests to the API server. They're essential for security investigations, compliance, and detecting anomalous behavior.
Audit Log Stages:
Audit Policy Levels:
Considerations:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
# Kubernetes API Server Audit PolicyapiVersion: audit.k8s.io/v1kind: Policy # Don't log requests to these URLsomitStages:- "RequestReceived" rules:# Don't log read-only endpoints (reduces volume)- level: None users: ["system:kube-probe"] # Don't log health checks- level: None nonResourceURLs: - "/healthz*" - "/livez*" - "/readyz*" - "/metrics" # Log secret access at Request level (who accessed what)- level: RequestResponse resources: - group: "" resources: ["secrets"] # Log RBAC changes (security critical)- level: RequestResponse resources: - group: "rbac.authorization.k8s.io" resources: ["roles", "rolebindings", "clusterroles", "clusterrolebindings"] # Log pod creation/deletion (detect cryptominers, etc)- level: Request verbs: ["create", "delete", "patch", "update"] resources: - group: "" resources: ["pods"] - group: "apps" resources: ["deployments", "daemonsets", "statefulsets"] # Log authentication failures- level: Metadata users: ["system:anonymous"] verbs: ["*"] # Log exec into pods (security sensitive)- level: RequestResponse resources: - group: "" resources: ["pods/exec", "pods/attach", "pods/portforward"] # Default: log metadata for everything else- level: Metadata resources: - group: "" - group: "apps" - group: "batch" - group: "networking.k8s.io"Audit logs are most useful when analyzed, not just stored. Ship audit logs to your SIEM (Splunk, Elastic, etc.) and create alerts for: 1) Secret access by unexpected service accounts 2) RBAC changes 3) exec/attach to production pods 4) Anonymous authentication attempts. Regular review catches security issues before they become incidents.
Kubernetes security requires a layered approach—no single control is sufficient. Let's consolidate the essential security controls for production clusters:
Module Complete:
Congratulations! You've completed the Kubernetes Operations module. You now have comprehensive knowledge of:
These operational skills distinguish production-ready Kubernetes environments from development clusters.
You've mastered Kubernetes Operations—the essential knowledge for running secure, observable, and efficient Kubernetes clusters in production. Apply these practices systematically to build infrastructure that scales reliably and survives real-world operational challenges.