Kubernetes Security Hardening Checklist

April 22, 2019

Kubernetes defaults prioritize ease of use over security. Production clusters need hardening. Here’s a comprehensive checklist.

Cluster Configuration

API Server

Disable anonymous authentication:

--anonymous-auth=false

Enable audit logging:

--audit-log-path=/var/log/kubernetes/audit.log
--audit-log-maxage=30
--audit-log-maxbackup=10
--audit-log-maxsize=100
--audit-policy-file=/etc/kubernetes/audit-policy.yaml

Enable admission controllers:

--enable-admission-plugins=NodeRestriction,PodSecurityPolicy

etcd Security

Enable TLS:

--cert-file=/etc/etcd/server.crt
--key-file=/etc/etcd/server.key
--client-cert-auth=true
--trusted-ca-file=/etc/etcd/ca.crt

Encrypt secrets at rest:

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

Kubelet

Disable anonymous access:

--anonymous-auth=false
--authorization-mode=Webhook

Read-only port:

--read-only-port=0

RBAC

Least Privilege Roles

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: deployment-manager
  namespace: production
rules:
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list", "watch"]

Avoid Cluster-Admin

# Bad - too permissive
kind: ClusterRoleBinding
roleRef:
  kind: ClusterRole
  name: cluster-admin  # Never for regular users

# Good - scoped permissions
kind: RoleBinding
roleRef:
  kind: Role
  name: deployment-manager

Service Account Best Practices

# Don't use default service account
apiVersion: v1
kind: ServiceAccount
metadata:
  name: api-service
automountServiceAccountToken: false  # Only mount when needed
---
# Pod uses specific SA
spec:
  serviceAccountName: api-service
  automountServiceAccountToken: true  # Explicitly enable when needed

Pod Security

Pod Security Standards

apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: v1.25

Security Context

securityContext:
  runAsNonRoot: true
  runAsUser: 1000
  runAsGroup: 1000
  fsGroup: 1000
  seccompProfile:
    type: RuntimeDefault
containers:
- name: app
  securityContext:
    allowPrivilegeEscalation: false
    readOnlyRootFilesystem: true
    capabilities:
      drop:
        - ALL

Resource Limits

containers:
- name: app
  resources:
    limits:
      memory: "256Mi"
      cpu: "500m"
    requests:
      memory: "128Mi"
      cpu: "100m"

Prevents resource exhaustion attacks.

Network Security

Network Policies

# Default deny all
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress
---
# Allow specific traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api
spec:
  podSelector:
    matchLabels:
      app: api
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - port: 8080

Service Mesh mTLS

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

Secrets Management

Don’t Use Environment Variables

# Bad - secrets in env vars
env:
- name: DB_PASSWORD
  valueFrom:
    secretKeyRef:
      name: db-secret
      key: password

# Better - mount as files
volumeMounts:
- name: secrets
  mountPath: /etc/secrets
  readOnly: true
volumes:
- name: secrets
  secret:
    secretName: db-secret

External Secrets

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: db-credentials
spec:
  secretStoreRef:
    name: vault
    kind: ClusterSecretStore
  target:
    name: db-secret
  data:
  - secretKey: password
    remoteRef:
      key: secret/data/db
      property: password

Image Security

Image Policies

# Only allow trusted registries
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: restrict-image-registries
spec:
  validationFailureAction: enforce
  rules:
  - name: validate-registries
    match:
      resources:
        kinds:
        - Pod
    validate:
      message: "Images must come from approved registries"
      pattern:
        spec:
          containers:
          - image: "registry.company.com/* | gcr.io/company/*"

Image Scanning

Integrate scanning in CI/CD:

# Trivy scan
- name: Scan image
  run: trivy image --severity HIGH,CRITICAL --exit-code 1 $IMAGE

Signed Images

# Sigstore/cosign verification
apiVersion: kyverno.io/v1
kind: ClusterPolicy
spec:
  rules:
  - name: verify-signature
    verifyImages:
    - image: "*"
      key: |-
        -----BEGIN PUBLIC KEY-----
        ...
        -----END PUBLIC KEY-----

Audit and Monitoring

Audit Policy

apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: RequestResponse
  resources:
  - group: ""
    resources: ["secrets", "configmaps"]
- level: Metadata
  resources:
  - group: ""
    resources: ["pods", "services"]
- level: None
  resources:
  - group: ""
    resources: ["endpoints", "events"]

Runtime Security

Deploy Falco for runtime detection:

- rule: Shell Spawned in Container
  desc: Detect shell spawned in container
  condition: spawned_process and container and shell_procs
  output: Shell spawned (user=%user.name container=%container.name)
  priority: WARNING

Hardening Checklist

Cluster Level

RBAC

Pod Security

Network

Secrets

Images

Monitoring

Key Takeaways

Security is layers. Each control adds defense depth.