Keeping your Kubernetes cluster secure is like protecting a house – you need multiple locks, alarms, and safety measures. This guide explains how to protect your Kubernetes cluster in simple terms, with detailed explanations of every security feature.
Understanding Security Threats
Before we protect our cluster, let’s understand what we’re protecting against:
External Attacks: Imagine someone trying to break into your house. In Kubernetes, this means hackers trying to access your cluster without permission.
Compromised Containers: Think of this as a Trojan horse – malicious code running inside what looks like a legitimate container.
Lateral Movement: Once an attacker gets in through one door, they try to move to other rooms. In Kubernetes, this means spreading from one compromised pod to others.
Data Exfiltration: Thieves stealing your valuables. In our case, attackers stealing sensitive data like passwords or customer information.
Resource Hijacking: Someone using your electricity to run their appliances. Attackers might use your cluster resources to mine cryptocurrency or launch attacks on others.
RBAC: Who Can Do What?
RBAC (Role-Based Access Control) is like giving different keys to different people in your organization. Not everyone needs access to everything.
The Three Main Components
1. Subjects (Who): These are the “people” who want to do something:
- Users: Real humans like developers or administrators
- Service Accounts: Think of these as robot users – automated processes that need permissions
- Groups: Teams of users, like “developers” or “admins”
2. Resources (What): These are the things in your cluster:
- Pods (running containers)
- Deployments (instructions for creating pods)
- Services (ways to access pods)
- And many more Kubernetes objects
3. Verbs (Actions): What someone can do with resources:
- get: Look at a single resource (like reading a file)
- list: See all resources of a type (like listing all files in a folder)
- watch: Monitor for changes (like getting notifications when files change)
- create: Make new resources
- update: Modify existing resources
- delete: Remove resources
Creating a Basic Role
Let’s create a role that allows someone to read pods (but not change them):
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: production # This role only works in the "production" namespace
name: pod-reader # We're calling this role "pod-reader"
rules:
- apiGroups: [""] # Empty string means core API group
resources: ["pods"] # This rule applies to pods
verbs: ["get", "list", "watch"] # Allowed actions: view pods
- apiGroups: [""]
resources: ["pods/log"] # Also allow reading pod logs
verbs: ["get", "list"] # Can view and list logs
What this means: Anyone with this role can look at pods and read their logs in the production namespace, but they can’t create, delete, or modify them. It’s like giving someone read-only access to a folder.
Creating a Cluster-Wide Role
Sometimes you need permissions that work across all namespaces:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole # ClusterRole works everywhere, not just one namespace
metadata:
name: node-reader
rules:
- apiGroups: [""]
resources: ["nodes"] # Kubernetes worker machines
verbs: ["get", "list", "watch"] # Can view nodes
- apiGroups: ["metrics.k8s.io"] # A different API group
resources: ["nodes"]
verbs: ["get", "list"] # Can also view node metrics
What this means: This role lets someone view information about all the physical/virtual machines (nodes) in your cluster and their performance metrics. This is useful for monitoring tools.
Giving Someone a Role (RoleBinding)
Creating a role is like making a key. Now we need to give that key to someone:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: production # This binding only works in production namespace
subjects: # These are the people/accounts getting the role
- kind: User
name: jane # A human user named Jane
apiGroup: rbac.authorization.k8s.io
- kind: ServiceAccount
name: monitoring-sa # A robot account for monitoring
namespace: production
roleRef: # This points to the role we're giving them
kind: Role
name: pod-reader # The role we created earlier
apiGroup: rbac.authorization.k8s.io
What this means: We’re giving the “pod-reader” role to two subjects: a user named Jane and a service account called “monitoring-sa”. Both can now view pods in the production namespace.
Cluster-Wide Access
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding # Works across all namespaces
metadata:
name: read-nodes
subjects:
- kind: Group
name: sre-team # Everyone in the SRE team
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: node-reader # The cluster-wide role we created
apiGroup: rbac.authorization.k8s.io
What this means: Everyone in the “sre-team” group can now view nodes across the entire cluster, not just in one namespace.
Creating Service Accounts Safely
Service accounts are like robot users for your applications. Here’s how to create them securely:
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: production
automountServiceAccountToken: false # Don't automatically give tokens
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: secure-app
spec:
template:
spec:
serviceAccountName: app-sa # Use our custom service account
automountServiceAccountToken: true # Now we explicitly enable it
containers:
- name: app
image: myapp:1.0
What this means: By default, we disable automatic token mounting (which is like automatically handing out keys). Then, only for applications that actually need access to the Kubernetes API, we explicitly enable it. This follows the principle of “don’t give access unless needed.”
Testing Permissions
Before giving someone access, test it:
bash
# Check if YOU can create deployments in production
kubectl auth can-i create deployments --namespace production
# Check what a service account can do (see all its permissions)
kubectl auth can-i --list --as=system:serviceaccount:production:app-sa
# Pretend to be Jane and try to get pods (test her permissions)
kubectl get pods --as=jane --namespace=production
What this means: These commands let you test permissions without actually giving them to someone. It’s like trying your keys before cutting copies.
Pod Security: Making Containers Safe
Think of pods as apartments in a building. We need rules to keep each apartment safe and prevent tenants from doing dangerous things.
Three Security Levels
Privileged: No restrictions – like letting tenants do whatever they want. Never use this in production!
Baseline: Some basic restrictions – like “no loud parties after 10 PM.” This prevents the most obviously dangerous things.
Restricted: Strict security – like a maximum-security building with lots of rules. This is what you should use in production.
Enforcing Security at the Namespace Level
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted # Block non-compliant pods
pod-security.kubernetes.io/audit: restricted # Log violations
pod-security.kubernetes.io/warn: restricted # Warn users
What this means: We’re telling Kubernetes that the entire “production” namespace must follow restricted security rules. Any pod that doesn’t comply will be:
- Blocked from running (enforce)
- Logged for security audits (audit)
- Show warnings to the person trying to create it (warn)
A Secure Pod Configuration
Here’s a pod configured with maximum security:
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
securityContext: # Security settings for the whole pod
runAsNonRoot: true # Don't run as root user (like admin)
runAsUser: 1000 # Run as user ID 1000 (a regular user)
fsGroup: 2000 # Files created belong to group 2000
seccompProfile: # Security profile that limits system calls
type: RuntimeDefault
containers:
- name: app
image: myapp:1.0
securityContext: # Security settings for this specific container
allowPrivilegeEscalation: false # Can't gain more privileges
readOnlyRootFilesystem: true # Can't write to main filesystem
runAsNonRoot: true # Double-check: not running as root
runAsUser: 1000
capabilities: # Linux capabilities (special permissions)
drop:
- ALL # Remove all special capabilities
volumeMounts:
- name: tmp
mountPath: /tmp # App can write to /tmp (temporary files)
resources:
limits: # Maximum resources this container can use
memory: "128Mi" # Max 128 megabytes of RAM
cpu: "500m" # Max 0.5 CPU cores
requests: # Minimum resources guaranteed
memory: "64Mi" # Guaranteed 64 MB RAM
cpu: "250m" # Guaranteed 0.25 CPU cores
volumes:
- name: tmp
emptyDir: {} # Temporary storage that gets wiped when pod dies
What this means:
- The app runs as a regular user (not admin/root), so even if hacked, it has limited power
- The main filesystem is read-only, so hackers can’t modify system files
- All special Linux capabilities are removed – it can only do basic operations
- We give it a temporary folder (/tmp) for any files it needs to create
- We set resource limits so one container can’t hog all the memory/CPU
Understanding Security Context Options
Let me explain each security setting in detail:
securityContext:
# Run as specific user (not root)
runAsUser: 1000 # User ID 1000 (regular user)
runAsGroup: 3000 # Primary group ID
fsGroup: 2000 # Group ID for files created
# Prevent privilege escalation
allowPrivilegeEscalation: false # Can't become root later
# Drop all capabilities
capabilities:
drop:
- ALL # Remove all special Linux powers
add:
- NET_BIND_SERVICE # Only add back what's absolutely needed
# (this one allows binding to ports < 1024)
# Read-only root filesystem
readOnlyRootFilesystem: true # Can't modify system files
# Seccomp profile
seccompProfile:
type: RuntimeDefault # Restrict which system calls are allowed
# SELinux (Linux security module)
seLinuxOptions:
level: "s0:c123,c456" # Security level label
What this means:
- runAsUser: Like logging into a computer as a regular user instead of administrator
- allowPrivilegeEscalation: Prevents the “sudo” equivalent – can’t gain admin powers
- capabilities: Linux has special powers like “bind to privileged ports” or “change system time.” We remove all of them, then only add back the minimum needed
- readOnlyRootFilesystem: Like write-protecting a CD – can read but not write
- seccompProfile: Filters dangerous system calls (like preventing apps from loading kernel modules)
Network Policies: Building Firewalls
Network Policies are like building walls and doors between different parts of your application. By default, Kubernetes allows all traffic – like a house with no doors. Let’s add some.
Block Everything by Default
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {} # Empty means "all pods in this namespace"
policyTypes:
- Ingress # Block incoming traffic
- Egress # Block outgoing traffic
What this means: This creates a default “deny everything” rule. It’s like locking all the doors. Now we’ll selectively open specific doors that we need.
Allow Specific Traffic In (Ingress)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: production
spec:
podSelector:
matchLabels:
app: backend # This rule applies to pods labeled "backend"
policyTypes:
- Ingress # We're defining incoming traffic rules
ingress:
- from:
- podSelector:
matchLabels:
app: frontend # Only allow traffic from pods labeled "frontend"
ports:
- protocol: TCP
port: 8080 # Only on port 8080
What this means: Backend pods can only receive traffic from frontend pods, and only on port 8080. It’s like saying “only people from the reception desk can enter the office, and only through the main door.”
Allow Specific Traffic Out (Egress)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-db-egress
namespace: production
spec:
podSelector:
matchLabels:
app: backend # This applies to backend pods
policyTypes:
- Egress # We're defining outgoing traffic rules
egress:
- to:
- podSelector:
matchLabels:
app: postgres # Can talk to postgres pods
ports:
- protocol: TCP
port: 5432 # On database port
- to: # Second rule: allow DNS
- namespaceSelector:
matchLabels:
name: kube-system # DNS is in kube-system namespace
ports:
- protocol: UDP
port: 53 # DNS port
What this means: Backend pods can:
- Talk to database pods on port 5432 (the PostgreSQL port)
- Use DNS for looking up hostnames (essential for most apps)
Without the DNS rule, your app couldn’t resolve domain names!
Cross-Namespace Communication
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-from-monitoring
namespace: production
spec:
podSelector:
matchLabels:
app: api # This applies to API pods
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: monitoring # Traffic from monitoring namespace
podSelector:
matchLabels:
app: prometheus # Specifically from Prometheus pods
ports:
- protocol: TCP
port: 8080 # On metrics port
What this means: API pods accept traffic from Prometheus (monitoring tool) in the monitoring namespace. This lets you monitor your production apps without allowing all traffic between namespaces. It’s like giving the building inspector a key to inspect any apartment.
Secrets Management: Protecting Passwords
Secrets are sensitive data like passwords, API keys, and certificates. Never put these in your code!
Creating Secrets Securely
bash
# Create from command line (passwords typed directly)
kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password='$ecureP@ssw0rd'
# Create from files (better for certificates)
kubectl create secret generic tls-certs \
--from-file=tls.crt=./server.crt \
--from-file=tls.key=./server.key
What this means:
- First command: Create a secret named “db-credentials” with username and password
- Second command: Create a secret from certificate files on your computer
genericmeans it’s a standard key-value secret (as opposed to special types like Docker credentials)
Using Secrets in Your Applications
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: myapp:1.0
env: # Environment variables
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: db-credentials # Get from this secret
key: username # Specifically the "username" key
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
volumeMounts:
- name: tls
mountPath: "/etc/tls" # Mount certificates as files here
readOnly: true # Make them read-only
volumes:
- name: tls
secret:
secretName: tls-certs # Load from this secret
What this means:
- Your app gets
DB_USERNAMEandDB_PASSWORDas environment variables (like system variables in Windows) - TLS certificates are mounted as actual files in
/etc/tls/directory - The app never knows where these values came from – it just sees environment variables and files
###External Secrets (Professional Setup)
For production, store secrets in a dedicated vault (like HashiCorp Vault) instead of Kubernetes:
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
namespace: production
spec:
provider:
vault:
server: "https://vault.example.com" # Your Vault server
path: "secret" # Path in Vault where secrets are stored
version: "v2" # Vault API version
auth:
kubernetes:
mountPath: "kubernetes"
role: "production-role" # Vault role for authentication
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: production
spec:
refreshInterval: 1h # Sync from Vault every hour
secretStoreRef:
name: vault-backend # Use the SecretStore we defined above
kind: SecretStore
target:
name: db-credentials # Create a Kubernetes secret with this name
creationPolicy: Owner
data:
- secretKey: username # Key in Kubernetes secret
remoteRef:
key: database/production # Path in Vault
property: username # Property to fetch
- secretKey: password
remoteRef:
key: database/production
property: password
What this means:
- Secrets are stored in Vault (a specialized secrets management system)
- External Secrets Operator automatically syncs them to Kubernetes every hour
- If you update a secret in Vault, it automatically updates in Kubernetes
- This is much more secure than storing secrets directly in Kubernetes
Image Security: Trust Your Containers
Container images are like software installers. You need to make sure they’re safe and from trusted sources.
Using Secure Images
apiVersion: v1
kind: Pod
metadata:
name: scanned-app
annotations:
seccomp.security.alpha.kubernetes.io/pod: runtime/default
spec:
containers:
- name: app
# Use image with cryptographic digest (sha256 hash)
image: myregistry.io/myapp:v1.0@sha256:abc123def456...
imagePullPolicy: Always # Always check for updates
What this means:
- The
@sha256:abc123...part is like a fingerprint of the exact image - This ensures you always get the exact same image, not a modified version
imagePullPolicy: Alwaysmeans check for updates every time (safer but slower)
Accessing Private Container Registries
bash
# Create credentials for private registry
kubectl create secret docker-registry regcred \
--docker-server=myregistry.io \
--docker-username=user \
--docker-password=pass \
--docker-email=user@example.com
apiVersion: v1
kind: Pod
metadata:
name: private-app
spec:
imagePullSecrets:
- name: regcred # Use these credentials to download images
containers:
- name: app
image: myregistry.io/private-app:1.0 # Private image
What this means:
- First command creates a secret with Docker registry login credentials
- The pod uses these credentials to pull images from your private registry
- Like logging into a website before downloading files
Admission Controllers: The Bouncers
Admission controllers check every request before it enters your cluster. They’re like bouncers at a club checking IDs.
Requiring Labels (Using OPA Gatekeeper)
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels
validation:
openAPIV3Schema:
properties:
labels:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
# This is Rego language (policy language)
violation[{"msg": msg, "details": {"missing_labels": missing}}] {
provided := {label | input.review.object.metadata.labels[label]}
required := {label | label := input.parameters.labels[_]}
missing := required - provided
count(missing) > 0
msg := sprintf("Missing required labels: %v", [missing])
}
What this code does: It creates a template that checks if resources have required labels.
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: require-app-label
spec:
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment"] # Check all Deployments
parameters:
labels: ["app", "owner", "environment"] # These labels are required
What this means:
- Every Deployment must have labels “app”, “owner”, and “environment”
- If someone tries to create a Deployment without these labels, it will be rejected
- This helps with organization – you always know which team owns which app
Audit Logging: Recording Everything
Audit logs are like security cameras – they record who did what and when.
Audit Policy Configuration
# Save this as /etc/kubernetes/audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Metadata # Log basic info (who, what, when)
omitStages:
- RequestReceived # Don't log when request first arrives
resources:
- group: ""
resources: ["secrets", "configmaps"] # Log all secret/config access
- level: Request # Log full request body
verbs: ["create", "update", "patch", "delete"] # Only for changes
resources:
- group: ""
- group: "apps"
- level: Metadata
resources:
- group: ""
resources: ["pods/log", "pods/status"] # Log access to pod logs
What this means:
- Metadata level: Log who did it, what resource, when – but not the full details
- Request level: Log everything including the full request (more detailed)
- We log all access to secrets (very important!)
- We log all create/update/delete operations
- We log when people read pod logs
API Server Configuration
bash
# Add these flags to kube-apiserver
--audit-policy-file=/etc/kubernetes/audit-policy.yaml
--audit-log-path=/var/log/kubernetes/audit.log # Where to save logs
--audit-log-maxage=30 # Keep logs for 30 days
--audit-log-maxbackup=10 # Keep 10 backup files
--audit-log-maxsize=100 # Each log file max 100MB
What this means: These settings tell the Kubernetes API server how to handle logs – where to save them, how long to keep them, and how much disk space to use.
Runtime Security: Watching for Threats
Runtime security watches your running containers for suspicious behavior.
Falco Rule Example
- rule: Unauthorized Process
desc: Detect unauthorized processes in containers
condition: >
spawned_process and # A new process started
container and # Inside a container
not proc.name in (allowed_processes) # Not in allowed list
output: >
Unauthorized process started
(user=%user.name command=%proc.cmdline container=%container.name)
priority: WARNING
What this means:
- Falco watches for new processes starting in containers
- If a process starts that’s not on the allowed list, it generates an alert
- The alert shows who ran it, what command, and which container
- This catches hackers trying to run shells or malicious tools
Security Checklist
Cluster Level (Infrastructure)
- ✅ Enable RBAC: So not everyone is admin
- ✅ Implement Pod Security Standards: Enforce secure pod configurations
- ✅ Configure Network Policies: Build firewalls between components
- ✅ Enable audit logging: Record all actions
- ✅ Use admission controllers: Check requests before allowing them
- ✅ Encrypt etcd at rest: Protect the database that stores cluster state
- ✅ Enable API server encryption: Protect secrets stored in Kubernetes
- ✅ Implement node authorization: Ensure nodes can only access their own pods
Workload Level (Your Applications)
- ✅ Run as non-root: Don’t use admin/root accounts
- ✅ Use read-only root filesystem: Prevent file modification
- ✅ Drop all capabilities: Remove all special Linux permissions
- ✅ Set resource limits: Prevent resource hogging
- ✅ Use specific image tags/digests: Know exactly what you’re running
- ✅ Scan images for vulnerabilities: Check for known security issues
- ✅ Implement health checks: Detect and restart unhealthy pods
- ✅ Use Pod Disruption Budgets: Ensure availability during updates
Access Control
- ✅ Principle of least privilege: Give minimum permissions needed
- ✅ Separate service accounts per app: Don’t share credentials
- ✅ Regular RBAC audits: Review who has access to what
- ✅ Rotate credentials regularly: Change passwords/keys periodically
- ✅ Use external secret management: Store secrets in Vault, not Kubernetes
- ✅ Implement MFA for humans: Require two-factor authentication
- ✅ Monitor privileged actions: Alert on admin activities
Conclusion
Kubernetes security is like home security – you need multiple layers:
- Locks on doors (RBAC)
- Alarm system (audit logging)
- Security cameras (runtime monitoring)
- Safe for valuables (secrets management)
- Firewall (network policies)
No single security measure is perfect. By combining all these layers, you create a strong defense.
Key Takeaways
- Always use RBAC with least privilege – Don’t give admin access by default
- Enforce Pod Security Standards – Make secure configurations the default
- Implement default-deny Network Policies – Block everything, then allow what’s needed
- Never hardcode secrets – Use proper secrets management
- Scan images – Only run trusted, scanned container images
- Enable audit logging – Record everything for security investigations
- Use admission controllers – Prevent bad configurations before they start
Next Steps
- Audit your current cluster – Check what security measures you’re missing
- Start with Pod Security Standards – Easiest win for immediate security improvement
- Create Network Policies – Start with one namespace, then expand
- Set up secrets management – Move to Vault or similar
- Enable monitoring – Deploy Falco or similar runtime security
- Schedule regular reviews – Security is ongoing, not one-time