Kubernetes Security: Essential Steps to Protect Your Cluster

Security tutorial - IT technology blog
Security tutorial - IT technology blog

When your Kubernetes cluster becomes a target

I learned a hard lesson last year. A server got brute-forced over SSH in the middle of the night, and I had to scramble to deal with it half-asleep. Since then I’ve made it a rule: whatever infrastructure I deploy, security gets set up on day one — not after something goes wrong.

Kubernetes is no exception. A lot of teams spin up a cluster, get their app running, and ship straight to production — security is something they’ll deal with later. And “later” usually never comes until there’s an incident.

This isn’t theory. In 2018, the RedLock team discovered that Tesla’s Kubernetes cluster had its API server exposed to the internet with no authentication — attackers were using it to mine cryptocurrency. Containers ran as root, pods communicated freely, secrets were stored as unencrypted base64, and the API server had no access restrictions. This is the reality of many production clusters I’ve reviewed, not just Tesla’s.

Why Kubernetes is insecure by default

K8s was designed to be flexible and easy to use first. Security is a layer you add yourself — it doesn’t come out of the box. Specifically:

  • RBAC is not enabled by default on some older versions or freshly created managed clusters.
  • Pods run as root unless you explicitly define a SecurityContext.
  • Network Policy doesn’t exist until you write one — every pod in the cluster can freely talk to every other pod.
  • Secrets are just base64: anyone with read access to etcd or the Secret resource can read everything in plaintext.
  • The default Service Account is mounted into every pod and can call the Kubernetes API.

Put these together: a single compromised container is enough for an attacker to escalate privileges across the entire cluster. I’ve tested this in a lab — it took less than 10 minutes to go from a compromised pod to cluster-admin access.

Security layers you need to configure

1. RBAC — Access Control

The foundation of everything is least privilege: give each identity only the permissions it actually needs, nothing more.

# Create a Role that only allows reading Pods in the "app" namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: app
  name: pod-reader
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-pods
  namespace: app
subjects:
- kind: ServiceAccount
  name: my-service-account
  namespace: app
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

Avoid ClusterRole with verbs: ["*"] or resources: ["*"] at all costs — this is a mistake I see in nearly every cluster that was set up in a hurry. Audit periodically to catch excessive permissions:

# Check what a service account is allowed to do
kubectl auth can-i --list --as=system:serviceaccount:app:my-service-account

# Find ClusterRoleBindings assigned to anonymous users
kubectl get clusterrolebindings -o json | jq '.items[] | select(.subjects[]?.name=="system:anonymous")'

2. Pod Security — Restricting what containers can do

Since Kubernetes 1.25, Pod Security Admission replaces PodSecurityPolicy (deprecated since 1.21). Enforce policies at the namespace level:

# Apply the "restricted" policy to the production namespace
kubectl label namespace production \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/audit=restricted \
  pod-security.kubernetes.io/warn=restricted

In your Pod/Deployment spec, always declare SecurityContext explicitly:

securityContext:
  runAsNonRoot: true
  runAsUser: 1000
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false
  capabilities:
    drop:
    - ALL

The container runs as UID 1000, with a read-only filesystem and all Linux capabilities dropped. Even if the container is compromised, the attacker can’t do much — they’re stuck in a very tight sandbox.

3. Network Policy — Firewalls between Pods

By default, pod A can call pod B directly even if there’s no reason for that to happen. Network Policy lets you define exactly which traffic is allowed:

# Only allow the frontend to call the backend, nothing else
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-policy
  namespace: app
spec:
  podSelector:
    matchLabels:
      role: backend
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          role: frontend
    ports:
    - protocol: TCP
      port: 8080
  egress:
  - to:
    - podSelector:
        matchLabels:
          role: database
    ports:
    - protocol: TCP
      port: 5432

One important caveat: Network Policy requires a CNI plugin that supports it — Calico, Cilium, or Weave all work. If you’re using Flannel, be careful: policies get created but have absolutely no effect, with no warning whatsoever.

4. Secret Management — Don’t trust Kubernetes Secrets by default

Base64 is not encryption. Anyone with read access to etcd or who can run kubectl get secret gets the plaintext immediately. Two approaches:

Option 1 — Enable encryption at rest for etcd (requires control plane access):

# Create EncryptionConfiguration
cat > /etc/kubernetes/encryption-config.yaml <<EOF
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
    - secrets
    providers:
    - aescbc:
        keys:
        - name: key1
          secret: $(head -c 32 /dev/urandom | base64)
    - identity: {}
EOF

# Add to kube-apiserver: --encryption-provider-config=/etc/kubernetes/encryption-config.yaml

Option 2 (recommended) — External Secrets Operator integrated with HashiCorp Vault or AWS Secrets Manager. Secrets are never stored in etcd and are only injected into pods at runtime:

# Install External Secrets Operator
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets -n external-secrets --create-namespace

5. Audit Logging and Runtime Security

Detecting anomalous behavior early is just as important as prevention. Enable audit logging on the API server:

# audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Metadata
  resources:
  - group: ""
    resources: ["secrets", "configmaps"]
- level: RequestResponse
  verbs: ["delete", "create"]
  resources:
  - group: ""
    resources: ["pods"]

Falco adds a runtime detection layer — detecting shell spawning inside containers, unusual file access, suspicious network connections, all in real time. On production this is non-negotiable:

helm repo add falcosecurity https://falcosecurity.github.io/charts
helm install falco falcosecurity/falco --namespace falco --create-namespace \
  --set falco.grpc.enabled=true \
  --set falco.grpcOutput.enabled=true

Prioritized security checklist

The NSA and CISA once issued a joint advisory on K8s security, concluding there’s no silver bullet — only multiple overlapping layers of defense are effective. If one layer gets bypassed, the next one stops the attacker. Practical priority order:

  1. Don’t expose the API server to the internet — use a VPN or bastion host. This is the most severe mistake and the easiest to fix.
  2. Keep Kubernetes up to date — K8s patches CVEs on a release cycle; older versions have publicly known vulnerabilities.
  3. RBAC with least privilege — review regularly, remove permissions that are no longer needed.
  4. SecurityContext on every Pod — set it in your Helm chart or default template so nothing slips through.
  5. Network Policy — start with deny-all, then open up traffic as needed.
  6. Scan images before deploying — integrate Trivy or Grype directly into your CI/CD pipeline.
  7. Proper secret management — at minimum encryption at rest, ideally an external vault.
  8. Audit logs + runtime monitoring — Falco for production, no exceptions.
# Scan an image with Trivy before pushing
trivy image --severity HIGH,CRITICAL your-image:tag

# Check for misconfigurations in YAML manifests
trivy config ./k8s-manifests/

# Audit the cluster against the CIS Kubernetes Benchmark
docker run --pid=host --userns=host --rm -v /etc:/etc:ro \
  -v /var:/var:ro -v /usr/lib/systemd:/usr/lib/systemd:ro \
  aquasec/kube-bench:latest

What I always emphasize with my team: security has to be part of the process, not a separate task. Integrate Trivy into your CI pipeline, enforce Pod Security at the admission controller, put RBAC reviews into sprint retrospectives — once it becomes routine, it stops feeling like a burden.

A Kubernetes cluster will never reach a state of “perfect security.” But with the steps above, you’ve pushed the risk down to an acceptable level — and more importantly, you’ll know when something unusual happens, instead of finding out after the damage is done.

Share: