Kubernetes Security Hardening: From Defaults to Defense
Secure your Kubernetes clusters with practical hardening techniques — RBAC, network policies, pod security, secrets management, and runtime protection.
Kubernetes Security Hardening: From Defaults to Defense
Kubernetes out of the box is not secure. Default configurations prioritize ease of use over security. A fresh cluster is an attacker’s playground.
This guide transforms a default K8s deployment into a hardened production environment.
The Attack Surface
Before hardening, understand what you’re defending:
┌─────────────────────────────────────────────────────────────┐
│ Kubernetes Attack Surface │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ API Server │ │ etcd │ │ Kubelet │ │
│ │ (TCP 6443) │ │ (TCP 2379) │ │ (TCP 10250) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ └──────────────────┴──────────────────┘ │
│ │ │
│ ┌────────────────────────┴────────────────────────┐ │
│ │ Container Runtime │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Pod A │ │ Pod B │ │ Pod C │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Common attack vectors:
- Exposed API server with weak authentication
- Overly permissive RBAC (everyone is admin)
- No network policies (all pods can talk to all pods)
- Privileged containers (root access to host)
- Secrets in plain text (environment variables, ConfigMaps)
1. API Server Security
Enable Authentication
Never expose the API server without authentication:
# /etc/kubernetes/manifests/kube-apiserver.yaml
spec:
containers:
- command:
- kube-apiserver
- --anonymous-auth=false
- --authentication-token-webhook-config-file=/etc/kubernetes/auth-webhook.yaml
- --authorization-mode=Node,RBAC
- --enable-admission-plugins=NodeRestriction,PodSecurityPolicy
Audit Logging
Know who did what:
# audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# Log all requests at the Metadata level
- level: Metadata
resources:
- group: ""
resources: ["secrets", "configmaps"]
# Log pod exec/attach at RequestResponse level
- level: RequestResponse
resources:
- group: ""
resources: ["pods/exec", "pods/attach"]
# Don't log read-only endpoints
- level: None
users: ["system:kube-proxy"]
verbs: ["watch"]
Apply with:
kube-apiserver --audit-policy-file=/etc/kubernetes/audit-policy.yaml \
--audit-log-path=/var/log/kubernetes/audit.log
2. RBAC: Least Privilege Access
Audit Current Permissions
# Who can create pods?
kubectl auth can-i create pods --all-namespaces --list
# What can a service account do?
kubectl auth can-i --list --as=system:serviceaccount:default:myapp
Create Minimal Roles
# app-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: production
name: app-reader
rules:
- apiGroups: [""]
resources: ["pods", "services"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get"]
# NO: create, update, delete, exec
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app-reader-binding
namespace: production
subjects:
- kind: ServiceAccount
name: myapp
namespace: production
roleRef:
kind: Role
name: app-reader
apiGroup: rbac.authorization.k8s.io
Dangerous Permissions to Avoid
# NEVER grant these without careful consideration:
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"] # God mode
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list"] # Can read all secrets
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"] # Shell into any pod
3. Network Policies
By default, all pods can communicate with all other pods. Lock it down.
Default Deny All
# deny-all.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
Allow Specific Traffic
# allow-frontend-to-backend.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-policy
namespace: production
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
---
# Allow backend to reach database
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: database-policy
namespace: production
spec:
podSelector:
matchLabels:
app: database
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: backend
ports:
- protocol: TCP
port: 5432
Block Metadata API
Prevent SSRF to cloud metadata:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: block-metadata
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.169.254/32 # AWS/GCP metadata
- 100.100.100.200/32 # Azure metadata
4. Pod Security
Pod Security Standards (PSS)
Kubernetes 1.25+ uses Pod Security Admission:
# namespace-restricted.yaml
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
Secure Pod Spec
apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:1.0@sha256:abc123... # Use digest, not tags
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
limits:
cpu: "500m"
memory: "128Mi"
requests:
cpu: "100m"
memory: "64Mi"
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /var/cache
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
automountServiceAccountToken: false # Disable if not needed
5. Secrets Management
Never Do This
# BAD: Secrets in environment variables
env:
- name: DATABASE_PASSWORD
value: "supersecret123"
# BAD: Secrets in ConfigMap
apiVersion: v1
kind: ConfigMap
data:
db-password: "supersecret123"
Use External Secrets
# external-secrets.yaml (with External Secrets Operator)
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: database-credentials
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: db-secret
data:
- secretKey: password
remoteRef:
key: production/database
property: password
Encrypt etcd
# Generate encryption key
head -c 32 /dev/urandom | base64
# encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-key>
- identity: {}
6. Image Security
Scan Images
# Integrate with CI/CD
# .github/workflows/security.yml
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
severity: 'CRITICAL,HIGH'
exit-code: '1'
Admission Control
Block unsigned or vulnerable images:
# kyverno-policy.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-signed-images
spec:
validationFailureAction: Enforce
rules:
- name: verify-signature
match:
resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "registry.example.com/*"
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
7. Runtime Security
Deploy Falco
Monitor for suspicious activity:
# falco-rules.yaml
- rule: Shell in Container
desc: Detect shell execution in container
condition: >
spawned_process and
container and
proc.name in (bash, sh, zsh)
output: >
Shell executed in container
(user=%user.name container=%container.name shell=%proc.name)
priority: WARNING
- rule: Sensitive File Access
desc: Detect access to sensitive files
condition: >
open_read and
container and
fd.name in (/etc/shadow, /etc/passwd, /root/.ssh/*)
output: >
Sensitive file accessed
(file=%fd.name container=%container.name)
priority: CRITICAL
Security Checklist
## API Server
□ Anonymous auth disabled
□ RBAC enabled
□ Audit logging configured
□ Admission controllers enabled
## RBAC
□ No cluster-admin for apps
□ Service accounts with minimal permissions
□ Regular permission audits
## Network
□ Default deny NetworkPolicy
□ Namespace isolation
□ Metadata API blocked
□ Ingress/egress rules defined
## Pods
□ Non-root containers
□ Read-only root filesystem
□ No privileged containers
□ Capabilities dropped
□ Resource limits set
## Secrets
□ External secrets manager
□ etcd encryption enabled
□ No secrets in env vars or ConfigMaps
## Images
□ Image scanning in CI/CD
□ Signed images only
□ No latest tags
□ Private registry
## Runtime
□ Falco or similar deployed
□ Audit logs monitored
□ Alerting configured
Conclusion
Kubernetes security is a journey, not a destination. Start with the basics:
- Enable RBAC with least privilege
- Network policies to isolate workloads
- Pod security to restrict container capabilities
- Secrets management with external tools
- Runtime monitoring to detect breaches
The goal isn’t perfect security — it’s making attacks expensive enough that attackers move on.
Related: Docker Security Best Practices