From Docker Compose to Kubernetes: Production Operational Realities
Docker and Docker Compose provide an exceptional developer ergonomics experience for local testing and single-node service deployments, as examined in our guide on production MySQL with Compose. However, when organizations scale toward distributed, mission-critical systems, Docker Compose exhibits structural limits: it lacks autonomous node failure remediation, lacks dynamic internal service mesh routing, and cannot coordinate zero-downtime rolling updates across heterogeneous hosts.
Kubernetes (K8s) overcomes these limitations by orchestrating multi-node server clusters into an unified compute fabric. Yet, this architectural power drastically magnifies the attack surface if deployed with factory-default configurations:
- Default-Allow Flat Networks: By default in Kubernetes, any Pod in any namespace can initiate TCP/UDP connections to any other Pod across the cluster, permitting immediate lateral movement upon a single application breach.
- *Overprivileged Containers (Privileged Pods):* Pods running as
root(UID 0), with writable root filesystems, or retaining broad Linux capabilities (CAPSYSADMIN), paving the way for container escape vulnerabilities directly into the host operating system kernel, as analyzed in what is Docker. - Exposed ServiceAccount Tokens: Kubernetes automatically mounts a signed JWT representing the Pod's ServiceAccount identity at
/var/run/secrets/kubernetes.io/serviceaccount/token. If an attacker breaches the application and the ServiceAccount holds broad API privileges, full cluster takeover becomes feasible. - Unrestricted API Server Ingress: Publicly exposed API servers lacking strict OIDC identity federation or running with permissive RBAC assignments granting
cluster-adminto service automation.
Hardening Kubernetes requires a multi-tier defense-in-depth model spanning from API admission control down to eBPF kernel event tracing.
Defense-in-Depth Architecture in Kubernetes
Enterprise Kubernetes security relies on concentric layers isolating identity, network flow, and Linux kernel execution:
graph TD
subgraph Control_Plane ["🛡️ Control Plane (API Server)"]
User["Developer / CI/CD"]
OIDC["OIDC / Cloud IAM Authentication"]
RBAC["RBAC Authorization (Least Privilege)"]
PSA["Pod Security Admission (Enforce: Restricted)"]
end
subgraph Data_Plane ["⚙️ Data Plane (Worker Node)"]
subgraph CNI_Layer ["CNI Network Filtering (NetworkPolicy)"]
NetPol["Default-Deny Ingress & Egress"]
end
subgraph Pod_Runtime ["Pod Isolation (Hardened Runtime)"]
SecComp["Seccomp Profile: RuntimeDefault"]
NonRoot["runAsNonRoot: true (UID >= 10000)"]
ReadOnly["readOnlyRootFilesystem: true"]
DropCaps["capabilities.drop: ['ALL']"]
end
subgraph Kernel_Audit ["Runtime Threat Detection (eBPF)"]
Falco["Falco eBPF: Shell Spawn & Anomaly Engine"]
end
end
User -->|TLS 1.3 + Bearer Token| OIDC
OIDC --> RBAC
RBAC --> PSA
PSA -->|Validated Manifest| CNI_Layer
CNI_Layer -->|Whitelisted Traffic| Pod_Runtime
Pod_Runtime --> Kernel_Audit
Kernel_Audit -.->|Security Incident Alert| SIEM["SIEM / SOC Alerting"]
classDef control fill:#e1f5fe,stroke:#0288d1,stroke-width:2px;
classDef data fill:#e8f5e9,stroke:#388e3c,stroke-width:2px;
classDef runtime fill:#fff3e0,stroke:#f57c00,stroke-width:2px;
classDef audit fill:#ffebee,stroke:#d32f2f,stroke-width:2px;
class User,OIDC,RBAC,PSA control;
class CNI_Layer data;
class SecComp,NonRoot,ReadOnly,DropCaps runtime;
class Falco,SIEM audit;
1. Pod Security Standards (PSS) & Pod Security Admission (PSA)
Following the removal of legacy PodSecurityPolicies (PSP) in Kubernetes 1.25, the built-in standard is Pod Security Admission (PSA), which categorizes workloads into three governance profiles:
- Privileged: Unrestricted. Intended strictly for low-level node daemons (CNI network drivers, CSI storage agents).
- Baseline: Enforces basic security guardrails with minimal configuration adjustments.
- Restricted: Implements the most stringent hardening controls aligned with CIS Kubernetes Benchmarks.
Namespace-Level PSA Enforcement
To ensure that insecure containers cannot be deployed into production namespaces, annotate namespaces with the enforcement labels:
apiVersion: v1
kind: Namespace
metadata:
name: production-apps
labels:
# Reject deployment of any Pod violating the Restricted standard
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
# Warn operators during kubectl apply if templates violate guidelines
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: latest
# Log audit events for compliance analysis
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: latest
Hardened Production Pod Manifest
Under the restricted profile, every deployed Pod must define its securityContext parameters explicitly:
apiVersion: apps/v1
kind: Deployment
metadata:
name: secure-backend-service
namespace: production-apps
spec:
replicas: 3
selector:
matchLabels:
app: backend-service
template:
metadata:
labels:
app: backend-service
spec:
# Disable ServiceAccount token auto-mounting if the app does not call the K8s API
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: application
image: ghcr.io/enterprise/backend-service:v1.4.2
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
limits:
cpu: "1"
memory: "1Gi"
requests:
cpu: "250m"
memory: "512Mi"
volumeMounts:
# Ephemeral in-memory scratch space for required temporary file operations
- name: tmp-volume
mountPath: /tmp
volumes:
- name: tmp-volume
emptyDir:
medium: Memory
sizeLimit: 64Mi
Key security parameters:
allowPrivilegeEscalation: false: Blocks binaries with thesetuidbit from elevating execution privileges within the container.readOnlyRootFilesystem: true: Locks down the root filesystem as read-only. Even if an attacker finds an application exploit, they cannot download exploit toolkits (curl,nmap), compile shellcode, or alter system binaries.capabilities.drop: ["ALL"]: Strips all administrative Linux kernel capabilities (such asCAPNETRAWorCAPSYSPTRACE).seccompProfile: RuntimeDefault: Enforces native Linux system call filtering to restrict dangerous or unused kernel calls.
2. Network Microsegmentation with NetworkPolicies
By default, Kubernetes network topology is completely open: any Pod can communicate with any other Pod. To establish a Zero Trust baseline, the foundational step in every namespace is applying a Default-Deny policy across both Ingress and Egress.
Policy 1: Global Default-Deny Policy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production-apps
spec:
podSelector: {} # Selects all pods in the namespace
policyTypes:
- Ingress
- Egress
Once applied, all Pods in the namespace are isolated from both inbound connections and outbound internet traffic, neutralizing lateral reconnaissance.
Policy 2: Granular Service Whitelisting
Next, explicitly authorize required traffic channels:
- Ingress: Inbound TCP on port 8080 originating strictly from pods labeled
role: ingress-controllerlocated in theingress-nginxnamespace. - Egress: Outbound DNS queries to Kubernetes CoreDNS (
kube-dnson port 53 UDP/TCP) and database traffic to the private PostgreSQL cluster on port 5432:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-service-policy
namespace: production-apps
spec:
podSelector:
matchLabels:
app: backend-service
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
podSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
ports:
- protocol: TCP
port: 8080
egress:
# Allow DNS resolution
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# Allow connection to private database tier
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: database-tier
podSelector:
matchLabels:
app: postgres-cluster
ports:
- protocol: TCP
port: 5432
Critical Implementation Note:
NetworkPoliciesare only enforced if the cluster utilizes a CNI plugin with policy filtering support, such as Cilium, Calico, or AWS VPC CNI with network policy engine enabled. Default basic bridge plugins do not enforce filtering.
3. RBAC and Workload Identity: Eliminating Static Secrets
Role-Based Access Control (RBAC) should favor namespace-scoped Role bindings over cluster-wide ClusterRole permissions, strictly enumerating verbs and resources:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: production-apps
name: deployment-operator
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "update", "patch"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
Workload Identity (AWS EKS Pod Identity / IRSA)
In cloud environments, containerized microservices frequently interact with cloud services like Amazon S3, DynamoDB, or Secrets Manager.
An antipattern is creating long-lived static AWS credentials (AWSACCESSKEYID and AWSSECRETACCESSKEY) and storing them in Kubernetes Secrets. If the cluster is compromised, the keys leak and persist indefinitely.
The enterprise standard is Workload Identity (EKS Pod Identity or IAM Roles for Service Accounts - IRSA):
- A Kubernetes ServiceAccount is bound to an AWS IAM Role via an OIDC token minted by the cluster API server.
- The AWS SDK within the container automatically assumes the IAM role via
sts:AssumeRoleWithWebIdentity. - Credentials are short-lived, rotated automatically every hour, and never written to disk.
4. Runtime Threat Detection with Falco eBPF
Even with static admission controls and network microsegmentation, runtime behavior must be monitored continuously. Falco is the CNCF standard for runtime security detection in Kubernetes, leveraging Linux eBPF (Extended Berkeley Packet Filter) probes inside the operating system kernel.
Falco intercepts system calls in real-time and evaluates rules for abnormal behavior:
- Interactive shell execution inside production containers (
execof/bin/bashor/bin/sh). - File writes to system binary paths (
/bin,/sbin,/usr/bin). - Unexpected outbound network socket connections to external IP ranges.
- Anomalous ServiceAccount token reads.
Example Falco rule to alert on container shell execution:
- rule: Terminal shell in container
desc: A shell was spawned inside a container with an attached terminal
condition: >
spawned_process and container
and shell_procs and proc.tty != 0
and container.name != "debug-sidecar"
output: >
Critical Alert: Interactive shell spawned in container
(user=%user.name pod=%k8s.pod.name ns=%k8s.ns.name
cmd=%proc.cmdline image=%container.image.repository)
priority: WARNING
tags: [container, shell, mitre_execution]
Events are streamed in real time to corporate SIEM platforms or automated incident response pipelines, enabling security teams to isolate compromised Pods immediately.
Enterprise Kubernetes Hardening Checklist
- [x] Pod Security Admission configured with
enforce: restrictedacross all production namespaces. - [x] Containers hardened with
readOnlyRootFilesystem: true,runAsNonRoot: true, andcapabilities.drop: ["ALL"]. - [x] Automatic ServiceAccount token mounting disabled (
automountServiceAccountToken: false) where API access is unnecessary. - [x] Network microsegmentation implemented with
default-deny-allNetworkPolicies. - [x] Static cloud credentials replaced by federated Workload Identity / EKS Pod Identity.
- [x] State-aware CNI plugin (Cilium or Calico) deployed and actively enforcing policies.
- [x] Continuous runtime auditing configured via Falco eBPF for immediate breach detection.
If you need to audit your organization's cloud architecture, implement Zero Trust bastions, or harden container clusters and Kubernetes workloads, explore my Cybersecurity Consulting Services or reach out directly via About Me.