Kubernetes Security Hardening: A Practical Guide

February 7, 2022

Kubernetes is powerful but not secure by default. Default configurations prioritize ease of use over security. Production clusters need hardening across multiple dimensions: network, workload, access control, and secrets management.

Here’s a practical guide to Kubernetes security hardening.

Pod Security

Pod Security Standards

# Kubernetes 1.23+ Pod Security Standards
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    # Enforce restricted standard
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted

# Restricted standard requires:
# - Running as non-root
# - Dropping all capabilities
# - Read-only root filesystem (recommended)
# - No privilege escalation
# - Seccomp profile

Secure Pod Specification

apiVersion: v1
kind: Pod
metadata:
  name: secure-app
spec:
  # Don't use host namespaces
  hostNetwork: false
  hostPID: false
  hostIPC: false

  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    runAsGroup: 1000
    fsGroup: 1000
    seccompProfile:
      type: RuntimeDefault

  containers:
    - name: app
      image: myapp:v1.0.0
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop:
            - ALL
        # Only add specific caps if needed
        # capabilities:
        #   add: ["NET_BIND_SERVICE"]

      resources:
        limits:
          cpu: "1"
          memory: "512Mi"
        requests:
          cpu: "100m"
          memory: "256Mi"

      # Use non-writable volumes for temp
      volumeMounts:
        - name: tmp
          mountPath: /tmp
        - name: cache
          mountPath: /var/cache

  volumes:
    - name: tmp
      emptyDir: {}
    - name: cache
      emptyDir: {}

Image Security

image_security:
  use_specific_tags:
    bad: "nginx:latest"
    good: "nginx:1.21.6-alpine"
    best: "nginx@sha256:abc123..."  # Digest

  trusted_registries:
    - Use private registry
    - Scan images before push
    - Sign images (cosign, Notary)

  minimal_images:
    - Use distroless or alpine
    - No shells in production images
    - Remove unnecessary tools
# OPA/Gatekeeper policy for trusted registries
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
  name: allowed-repos
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
  parameters:
    repos:
      - "gcr.io/my-project/"
      - "my-registry.example.com/"

Network Security

Network Policies

# Default deny all
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

---
# Allow specific traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-server-policy
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api-server
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
        - namespaceSelector:
            matchLabels:
              name: ingress-nginx
      ports:
        - port: 8080
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: database
      ports:
        - port: 5432
    - to:
        - namespaceSelector:
            matchLabels:
              name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - port: 53
          protocol: UDP

Service Mesh mTLS

# Istio PeerAuthentication
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: default
  namespace: production
spec:
  mtls:
    mode: STRICT

---
# Authorization Policy
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: api-server-authz
  namespace: production
spec:
  selector:
    matchLabels:
      app: api-server
  action: ALLOW
  rules:
    - from:
        - source:
            principals:
              - cluster.local/ns/production/sa/frontend

RBAC

Least Privilege

# Service account with minimal permissions
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-service-account
  namespace: production

---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: app-role
  namespace: production
rules:
  # Only what's needed
  - apiGroups: [""]
    resources: ["configmaps"]
    resourceNames: ["app-config"]
    verbs: ["get"]
  - apiGroups: [""]
    resources: ["secrets"]
    resourceNames: ["app-secrets"]
    verbs: ["get"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: app-role-binding
  namespace: production
subjects:
  - kind: ServiceAccount
    name: app-service-account
    namespace: production
roleRef:
  kind: Role
  name: app-role
  apiGroup: rbac.authorization.k8s.io

Disable Default Service Account Token

apiVersion: v1
kind: ServiceAccount
metadata:
  name: default
  namespace: production
automountServiceAccountToken: false

Secrets Management

External Secrets

# External Secrets Operator
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: app-secrets
  namespace: production
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: app-secrets
    creationPolicy: Owner
  data:
    - secretKey: database-password
      remoteRef:
        key: production/database
        property: password

Sealed Secrets

# Sealed Secrets for GitOps
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: app-secrets
  namespace: production
spec:
  encryptedData:
    database-password: AgBy3i... # Encrypted
  template:
    metadata:
      name: app-secrets
      namespace: production

Cluster Hardening

API Server

api_server_hardening:
  authentication:
    - Disable anonymous auth
    - Use OIDC for user auth
    - Short-lived tokens

  authorization:
    - RBAC enabled
    - No cluster-admin for users
    - Audit logging enabled

  encryption:
    - Encrypt etcd at rest
    - TLS for all communication

# Example kube-apiserver flags
kube_apiserver_flags:
  - --anonymous-auth=false
  - --audit-log-path=/var/log/kubernetes/audit.log
  - --audit-log-maxage=30
  - --encryption-provider-config=/etc/kubernetes/enc/enc.yaml

etcd

etcd_security:
  encryption_at_rest:
    enabled: true
    provider: aescbc or kms

  access:
    - TLS client certificates
    - Restricted to API server
    - Not exposed to nodes

# Encryption config
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: <base64-encoded-key>
      - identity: {}

Node Security

node_hardening:
  os_level:
    - Minimal OS (Bottlerocket, Flatcar)
    - Regular patching
    - CIS benchmarks

  kubelet:
    - Rotate certificates
    - Protect kubelet API
    - Read-only port disabled

  runtime:
    - Container runtime sandboxing (gVisor, Kata)
    - Seccomp profiles
    - AppArmor/SELinux

Auditing and Monitoring

Audit Logging

# Audit policy
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  # Log all secrets access
  - level: Metadata
    resources:
      - group: ""
        resources: ["secrets"]

  # Log all authentication failures
  - level: Metadata
    users: ["system:anonymous"]

  # Log exec/attach to pods
  - level: Request
    resources:
      - group: ""
        resources: ["pods/exec", "pods/attach"]

  # Don't log read-only endpoints
  - level: None
    users: ["system:kube-proxy"]
    verbs: ["watch"]
    resources:
      - group: ""
        resources: ["endpoints", "services"]

Runtime Security

# Falco rules
- rule: Shell in Container
  condition: >
    spawned_process and
    container and
    shell_procs
  output: >
    Shell spawned in container
    (user=%user.name container=%container.name shell=%proc.name)
  priority: WARNING

- rule: Unexpected Outbound Connection
  condition: >
    outbound and
    container and
    not expected_outbound
  output: >
    Unexpected outbound connection
    (command=%proc.cmdline connection=%fd.name)
  priority: WARNING

Security Checklist

security_checklist:
  pod_security:
    - [ ] Run as non-root
    - [ ] Drop all capabilities
    - [ ] Read-only root filesystem
    - [ ] No privilege escalation
    - [ ] Resource limits set

  network:
    - [ ] Network policies (default deny)
    - [ ] mTLS between services
    - [ ] Egress restrictions

  access_control:
    - [ ] RBAC with least privilege
    - [ ] No cluster-admin for applications
    - [ ] Service accounts per workload
    - [ ] Disable default SA token mount

  secrets:
    - [ ] External secrets management
    - [ ] Encryption at rest
    - [ ] No secrets in images or env

  images:
    - [ ] Trusted registries only
    - [ ] Image scanning
    - [ ] Specific tags/digests

  cluster:
    - [ ] API server secured
    - [ ] etcd encrypted
    - [ ] Audit logging enabled

Key Takeaways

Security is not a feature to add later—it’s a foundation to build on.