Kubernetes security
In this series (14 parts)
- Why Kubernetes exists
- Kubernetes architecture
- Core Kubernetes objects
- Kubernetes networking
- Storage in Kubernetes
- Kubernetes configuration and secrets
- Resource management and autoscaling
- Kubernetes workload types
- Kubernetes observability
- Kubernetes security
- Helm and package management
- GitOps with ArgoCD
- Kubernetes cluster operations
- Service mesh concepts
Prerequisite: Kubernetes observability.
A running cluster is an attack surface. Every pod, service account, and open port is a potential entry point. Kubernetes ships with security primitives, but none enforce least privilege by default. You configure them deliberately or they do nothing.
This article covers RBAC, PodSecurity Standards, network policies, admission controllers, and audit logging. For a broader perspective, see K8s security in depth.
RBAC: Role-Based Access Control
RBAC binds subjects (users, groups, service accounts) to permissions defined in Roles or ClusterRoles.
flowchart LR U["User / ServiceAccount"] -->|bound by| RB["RoleBinding"] RB -->|references| R["Role"] R -->|grants| P["Permissions on Resources"] U2["User / ServiceAccount"] -->|bound by| CRB["ClusterRoleBinding"] CRB -->|references| CR["ClusterRole"] CR -->|grants| CP["Cluster-wide Permissions"]
Roles are namespace-scoped. ClusterRoles apply across the entire cluster.
A Role defines permissions within a namespace. A RoleBinding attaches it to a subject.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: app
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "watch", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
namespace: app
name: read-pods-binding
subjects:
- kind: User
name: developer
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
The user developer can now list pods in the app namespace. Nothing else. ClusterRoles work identically but span all namespaces. Use them for cluster-wide resources like nodes. Grant the minimum verbs required. Avoid wildcards in production.
ServiceAccounts
Every pod runs as a ServiceAccount. If you skip specifying one, the pod uses the default account, which may carry more permissions than intended.
apiVersion: v1
kind: ServiceAccount
metadata:
namespace: app
name: backend-sa
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
namespace: app
name: backend-configmap-reader
subjects:
- kind: ServiceAccount
name: backend-sa
namespace: app
roleRef:
kind: Role
name: configmap-reader
apiGroup: rbac.authorization.k8s.io
Setting automountServiceAccountToken: false prevents the token from being mounted automatically. This limits blast radius if a pod is compromised. Override it per-pod only when the workload actually needs API access.
PodSecurity Standards
PodSecurity Standards replace the deprecated PodSecurityPolicy. Three profiles control what a pod is allowed to do.
| Profile | Purpose |
|---|---|
| Privileged | No restrictions. System-level workloads only. |
| Baseline | Blocks known privilege escalations. Good default. |
| Restricted | Maximum lockdown. Non-root, dropped capabilities. |
Enforce at the namespace level with labels:
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
Any pod violating the restricted profile is rejected. The audit and warn modes log violations without blocking, useful during migration.
Network Policies: Zero-Trust Networking
By default, every pod can reach every other pod. Network policies change that. Start with deny-all, then open specific paths.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
namespace: app
name: deny-all
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
namespace: app
name: allow-frontend-to-backend
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
The first policy blocks everything. The second opens one path: frontend pods can reach backend on port 8080. Network policies require a CNI plugin that supports them, such as Calico or Cilium. The default kubenet plugin ignores them silently.
Admission Controllers
Admission controllers intercept API requests after authentication but before persistence. They validate or mutate objects.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: deny-root-pods
webhooks:
- name: deny-root.example.com
admissionReviewVersions: ["v1"]
sideEffects: None
clientConfig:
service:
name: admission-webhook
namespace: kube-system
path: /validate
caBundle: <base64-encoded-ca-cert>
rules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["pods"]
failurePolicy: Fail
Setting failurePolicy: Fail rejects requests when the webhook is unreachable. Safer than Ignore. Tools like OPA Gatekeeper and Kyverno let you write custom policies as code.
Audit Logging
Audit logs record every request to the API server. Who did what, when, to which resource.
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Metadata
resources:
- group: ""
resources: ["secrets", "configmaps"]
- level: RequestResponse
resources:
- group: ""
resources: ["pods"]
verbs: ["create", "delete"]
- level: None
resources:
- group: ""
resources: ["events"]
Four levels: None, Metadata, Request, RequestResponse. Log secrets at Metadata only so you track access patterns without recording values. Pass the policy to the API server with --audit-policy-file.
What comes next
RBAC, PodSecurity Standards, network policies, and admission controllers form the foundation. Layer them for defense in depth. Security is continuous, not a one-time configuration.
The next article covers Helm and package management, where you will template and distribute these manifests as reusable charts.