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:

  1. Enable RBAC with least privilege
  2. Network policies to isolate workloads
  3. Pod security to restrict container capabilities
  4. Secrets management with external tools
  5. 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