Security

Kubernetes Security: A Complete Guide for Beginners

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:

  1. Talk to database pods on port 5432 (the PostgreSQL port)
  2. 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
  • generic means 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_USERNAME and DB_PASSWORD as 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: Always means 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

  1. Always use RBAC with least privilege – Don’t give admin access by default
  2. Enforce Pod Security Standards – Make secure configurations the default
  3. Implement default-deny Network Policies – Block everything, then allow what’s needed
  4. Never hardcode secrets – Use proper secrets management
  5. Scan images – Only run trusted, scanned container images
  6. Enable audit logging – Record everything for security investigations
  7. Use admission controllers – Prevent bad configurations before they start

Next Steps

  1. Audit your current cluster – Check what security measures you’re missing
  2. Start with Pod Security Standards – Easiest win for immediate security improvement
  3. Create Network Policies – Start with one namespace, then expand
  4. Set up secrets management – Move to Vault or similar
  5. Enable monitoring – Deploy Falco or similar runtime security
  6. Schedule regular reviews – Security is ongoing, not one-time

Leave a Reply

Your email address will not be published. Required fields are marked *