Kubernetes is powerful but not secure by default. Default configurations prioritize ease of use over security. Production clusters need hardening across multiple dimensions: network, workload, access control, and secrets management.
Here’s a practical guide to Kubernetes security hardening.
Pod Security
Pod Security Standards
# Kubernetes 1.23+ Pod Security Standards
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
# Enforce restricted standard
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
# Restricted standard requires:
# - Running as non-root
# - Dropping all capabilities
# - Read-only root filesystem (recommended)
# - No privilege escalation
# - Seccomp profile
Secure Pod Specification
apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
# Don't use host namespaces
hostNetwork: false
hostPID: false
hostIPC: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:v1.0.0
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
# Only add specific caps if needed
# capabilities:
# add: ["NET_BIND_SERVICE"]
resources:
limits:
cpu: "1"
memory: "512Mi"
requests:
cpu: "100m"
memory: "256Mi"
# Use non-writable volumes for temp
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /var/cache
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
Image Security
image_security:
use_specific_tags:
bad: "nginx:latest"
good: "nginx:1.21.6-alpine"
best: "nginx@sha256:abc123..." # Digest
trusted_registries:
- Use private registry
- Scan images before push
- Sign images (cosign, Notary)
minimal_images:
- Use distroless or alpine
- No shells in production images
- Remove unnecessary tools
# OPA/Gatekeeper policy for trusted registries
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
name: allowed-repos
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
repos:
- "gcr.io/my-project/"
- "my-registry.example.com/"
Network Security
Network Policies
# Default deny all
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
# Allow specific traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-server-policy
namespace: production
spec:
podSelector:
matchLabels:
app: api-server
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
- namespaceSelector:
matchLabels:
name: ingress-nginx
ports:
- port: 8080
egress:
- to:
- podSelector:
matchLabels:
app: database
ports:
- port: 5432
- to:
- namespaceSelector:
matchLabels:
name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- port: 53
protocol: UDP
Service Mesh mTLS
# Istio PeerAuthentication
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: STRICT
---
# Authorization Policy
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: api-server-authz
namespace: production
spec:
selector:
matchLabels:
app: api-server
action: ALLOW
rules:
- from:
- source:
principals:
- cluster.local/ns/production/sa/frontend
RBAC
Least Privilege
# Service account with minimal permissions
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-service-account
namespace: production
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: app-role
namespace: production
rules:
# Only what's needed
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["app-config"]
verbs: ["get"]
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["app-secrets"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app-role-binding
namespace: production
subjects:
- kind: ServiceAccount
name: app-service-account
namespace: production
roleRef:
kind: Role
name: app-role
apiGroup: rbac.authorization.k8s.io
Disable Default Service Account Token
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: production
automountServiceAccountToken: false
Secrets Management
External Secrets
# External Secrets Operator
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-secrets
namespace: production
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: app-secrets
creationPolicy: Owner
data:
- secretKey: database-password
remoteRef:
key: production/database
property: password
Sealed Secrets
# Sealed Secrets for GitOps
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: app-secrets
namespace: production
spec:
encryptedData:
database-password: AgBy3i... # Encrypted
template:
metadata:
name: app-secrets
namespace: production
Cluster Hardening
API Server
api_server_hardening:
authentication:
- Disable anonymous auth
- Use OIDC for user auth
- Short-lived tokens
authorization:
- RBAC enabled
- No cluster-admin for users
- Audit logging enabled
encryption:
- Encrypt etcd at rest
- TLS for all communication
# Example kube-apiserver flags
kube_apiserver_flags:
- --anonymous-auth=false
- --audit-log-path=/var/log/kubernetes/audit.log
- --audit-log-maxage=30
- --encryption-provider-config=/etc/kubernetes/enc/enc.yaml
etcd
etcd_security:
encryption_at_rest:
enabled: true
provider: aescbc or kms
access:
- TLS client certificates
- Restricted to API server
- Not exposed to nodes
# Encryption config
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-key>
- identity: {}
Node Security
node_hardening:
os_level:
- Minimal OS (Bottlerocket, Flatcar)
- Regular patching
- CIS benchmarks
kubelet:
- Rotate certificates
- Protect kubelet API
- Read-only port disabled
runtime:
- Container runtime sandboxing (gVisor, Kata)
- Seccomp profiles
- AppArmor/SELinux
Auditing and Monitoring
Audit Logging
# Audit policy
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# Log all secrets access
- level: Metadata
resources:
- group: ""
resources: ["secrets"]
# Log all authentication failures
- level: Metadata
users: ["system:anonymous"]
# Log exec/attach to pods
- level: Request
resources:
- group: ""
resources: ["pods/exec", "pods/attach"]
# Don't log read-only endpoints
- level: None
users: ["system:kube-proxy"]
verbs: ["watch"]
resources:
- group: ""
resources: ["endpoints", "services"]
Runtime Security
# Falco rules
- rule: Shell in Container
condition: >
spawned_process and
container and
shell_procs
output: >
Shell spawned in container
(user=%user.name container=%container.name shell=%proc.name)
priority: WARNING
- rule: Unexpected Outbound Connection
condition: >
outbound and
container and
not expected_outbound
output: >
Unexpected outbound connection
(command=%proc.cmdline connection=%fd.name)
priority: WARNING
Security Checklist
security_checklist:
pod_security:
- [ ] Run as non-root
- [ ] Drop all capabilities
- [ ] Read-only root filesystem
- [ ] No privilege escalation
- [ ] Resource limits set
network:
- [ ] Network policies (default deny)
- [ ] mTLS between services
- [ ] Egress restrictions
access_control:
- [ ] RBAC with least privilege
- [ ] No cluster-admin for applications
- [ ] Service accounts per workload
- [ ] Disable default SA token mount
secrets:
- [ ] External secrets management
- [ ] Encryption at rest
- [ ] No secrets in images or env
images:
- [ ] Trusted registries only
- [ ] Image scanning
- [ ] Specific tags/digests
cluster:
- [ ] API server secured
- [ ] etcd encrypted
- [ ] Audit logging enabled
Key Takeaways
- Kubernetes defaults are not secure; hardening is required
- Use Pod Security Standards (restricted profile for production)
- Run containers as non-root with minimal capabilities
- Implement network policies starting with default deny
- RBAC with least privilege; disable default service account tokens
- Use external secrets management (Vault, cloud provider)
- Scan and sign container images; use trusted registries
- Encrypt etcd at rest; protect API server
- Enable audit logging and runtime security monitoring
- Security is layers: pod, network, access, secrets, cluster
Security is not a feature to add later—it’s a foundation to build on.