🛡️ Defense-in-Depth Security Model

Kubernetes security is layered — no single control is sufficient. Production clusters need independent controls at every layer so that a breach at one layer is contained by the next.

📦
Supply Chain

Signed images (Cosign), SBOM, vulnerability scanning (Trivy), trusted base images, Harbor with enforcement policies

🔍
Runtime Detection

Falco syscall monitoring, eBPF-based anomaly detection, alerting on privilege escalation, shell spawns, unexpected network connections

🐳
Workload Isolation

PodSecurity restricted, seccomp profiles, AppArmor, read-only root filesystem, drop ALL capabilities, non-root UID, no privilege escalation

🔐
Cluster Controls

RBAC least-privilege, OPA/Kyverno admission policies, NetworkPolicy default-deny, Secrets encryption at rest, audit logging

🖥️
Infrastructure

Hardened OS (CIS-benchmarked), private API server endpoint, node IAM least-privilege, VPC/firewall rules, etcd TLS peer auth

kube-bench

Open-source CIS Kubernetes Benchmark scanner. Runs checks against control-plane and worker nodes and produces a pass/fail report.

Falco

CNCF runtime security tool. Detects suspicious syscalls (shell in container, privilege escalation, unexpected file writes) using kernel eBPF probes.

Cosign / Sigstore

Keyless image signing with OIDC identity. Verify images are signed by trusted CI pipelines before allowing deployment.

External Secrets Operator

Syncs secrets from Vault, AWS Secrets Manager, GCP Secret Manager into Kubernetes Secrets. Never store secrets in Git.

📋 CIS Benchmark & API Server Hardening

Run kube-bench

# Run CIS checks on a control-plane node
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job-master.yaml
kubectl logs job/kube-bench-master

# Run on worker nodes
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job-node.yaml
kubectl logs job/kube-bench-node

# Example output:
# [PASS] 1.2.1  Ensure --anonymous-auth is set to false
# [FAIL] 1.2.6  Ensure --kubelet-certificate-authority is set
# [WARN] 1.2.10 Ensure --tls-cipher-suites is set
# [INFO] 1.3.1  Ensure kube-controller-manager flags are set

Critical API Server Hardening Flags

FlagSecure valueRisk if not set
--anonymous-authfalseUnauthenticated requests reach the API server
--authorization-modeNode,RBACMisconfigured auth (ABAC or AlwaysAllow is dangerous)
--enable-admission-pluginsincludes NodeRestrictionNodes can modify other nodes' objects
--audit-log-pathfile path setNo audit trail for incident forensics
--tls-min-versionVersionTLS12TLS 1.0/1.1 are vulnerable to POODLE, BEAST
--encryption-provider-configconfiguredSecrets stored in plaintext in etcd
--profilingfalseProfiling endpoints expose internal data
--service-account-lookuptrueDeleted service accounts can still authenticate

Hardened Pod Security Context

apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 10000
        runAsGroup: 10000
        fsGroup: 10000
        seccompProfile:
          type: RuntimeDefault   # apply default seccomp filter
      containers:
        - name: app
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]         # drop every Linux capability
              add: ["NET_BIND_SERVICE"]  # add back only what's needed
          volumeMounts:
            - name: tmp
              mountPath: /tmp       # writable tmp for read-only root fs
      volumes:
        - name: tmp
          emptyDir: {}
💡 Use Kyverno to enforce this everywhere Apply a ClusterPolicy that mutates every Pod to add these security defaults, and a validating policy that rejects Pods that try to opt out. Operators don't have to remember — the policy engine enforces it.

🔍 Falco Runtime Security & Supply Chain

Install Falco

helm repo add falcosecurity https://falcosecurity.github.io/charts
helm repo update

helm install falco falcosecurity/falco \
  --namespace falco \
  --create-namespace \
  --set driver.kind=ebpf \        # prefer eBPF over kernel module
  --set falcosidekick.enabled=true \
  --set falcosidekick.config.slack.webhookurl=https://hooks.slack.com/... \
  --set falcosidekick.config.slack.minimumpriority=warning

Key Falco Rules (Built-in)

RuleWhat it detectsPriority
Terminal shell in containerSomeone exec'd a shell (bash, sh) into a running containerNOTICE
Write below rootFile created in / by a non-root processERROR
Modify binary dirsWrite to /bin, /sbin, /usr/bin etc.ERROR
Outbound connection to C&CUnexpected egress to an IP not in allowlistWARNING
Privilege escalation via setuidProcess calls setuid(0) to become rootCRITICAL
Read sensitive fileAccess to /etc/shadow, /etc/kubernetes/pki/*WARNING
Container run as rootContainer's main process running with UID 0WARNING

Custom Falco Rule

# Detect any curl/wget in a production container (data exfiltration)
- rule: Unexpected Network Tool in Container
  desc: Detects curl/wget execution in production pods
  condition: >
    spawned_process and container and
    proc.name in (curl, wget, nc, ncat, netcat) and
    not proc.pname in (package-manager, apt, yum)
  output: >
    Network tool executed in container
    (user=%user.name cmd=%proc.cmdline container=%container.name
     image=%container.image.repository:%container.image.tag)
  priority: WARNING
  tags: [network, exfiltration]

Image Supply Chain — Cosign Signing

# Sign image after CI build (keyless with OIDC)
cosign sign --yes ghcr.io/my-org/my-app:v1.2.3

# Verify signature
cosign verify \
  --certificate-identity=https://github.com/my-org/my-app/.github/workflows/build.yaml@refs/heads/main \
  --certificate-oidc-issuer=https://token.actions.githubusercontent.com \
  ghcr.io/my-org/my-app:v1.2.3

Kyverno — Enforce Signed Images

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-signed-images
spec:
  validationFailureAction: Enforce
  rules:
    - name: verify-image-signature
      match:
        resources: { kinds: [Pod] }
      verifyImages:
        - imageReferences: ["ghcr.io/my-org/*"]
          attestors:
            - count: 1
              entries:
                - keyless:
                    subject: "https://github.com/my-org/*"
                    issuer: "https://token.actions.githubusercontent.com"
                    rekor:
                      url: https://rekor.sigstore.dev
🚨 Never pull images from public registries in production Use a private registry (Harbor) as a proxy cache. This eliminates DockerHub rate limits, gives you an audit trail of every pulled image, and lets you enforce vulnerability and signature policies at the registry layer.

🔑 Secrets Management & Audit Logs

Encrypt Secrets at Rest in etcd

# /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources: [secrets]
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: <base64-encoded-32-byte-key>
      - identity: {}   # fallback for unencrypted (migration)

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

# Verify: check raw etcd value is encrypted
etcdctl get /registry/secrets/default/my-secret --print-value-only
# k8s:enc:aescbc:v1:key1:...   ← encrypted prefix confirms it worked

# Re-encrypt all existing secrets
kubectl get secrets -A -o json | kubectl replace -f -

External Secrets Operator (ESO)

Pull secrets from AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, or Azure Key Vault into Kubernetes Secrets — automatically rotated, never stored in Git.

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: db-credentials
  namespace: my-app
spec:
  refreshInterval: 1h         # re-sync every hour (picks up rotations)
  secretStoreRef:
    name: aws-secretsmanager
    kind: ClusterSecretStore
  target:
    name: db-credentials      # name of the K8s Secret to create
    creationPolicy: Owner
  data:
    - secretKey: username
      remoteRef:
        key: prod/my-app/db
        property: username
    - secretKey: password
      remoteRef:
        key: prod/my-app/db
        property: password
🚨 Never commit secrets to Git — even encrypted Sealed Secrets and SOPS are better than nothing, but ESO pulling from a proper secrets store is the gold standard. Secrets in Git, even encrypted, become a liability when encryption keys rotate or are leaked.

Audit Log Analysis for Security Events

# High-value audit log queries (using jq or your SIEM)

# Who accessed Secrets in the last hour?
cat /var/log/kubernetes/audit.log | jq -r '
  select(.objectRef.resource == "secrets" and .verb == "get") |
  [.requestReceivedTimestamp, .user.username, .objectRef.namespace, .objectRef.name]
  | @tsv' | sort | uniq

# Detect privilege escalation attempts
cat audit.log | jq -r '
  select(.responseStatus.code == 403) |
  [.user.username, .verb, .objectRef.resource, .requestReceivedTimestamp]
  | @tsv' | sort | uniq -c | sort -rn | head -20

# Who created ClusterRoleBindings? (RBAC escalation)
cat audit.log | jq -r '
  select(.objectRef.resource == "clusterrolebindings" and
         .verb in ["create","update","patch"]) |
  [.user.username, .objectRef.name, .requestReceivedTimestamp] | @tsv'

# kubectl exec into pods (lateral movement indicator)
cat audit.log | jq -r '
  select(.objectRef.subresource == "exec") |
  [.user.username, .objectRef.namespace, .objectRef.name, .requestReceivedTimestamp]
  | @tsv'

📝 Knowledge Check

Q1. A Falco alert fires: "Terminal shell in container — user=root cmd=bash container=api namespace=production". What does this indicate and what should you do immediately?
  • A) A developer is legitimately debugging — dismiss the alert
  • B) Possible container compromise or unauthorized access — investigate, isolate the pod, preserve forensics
  • C) The container's liveness probe is misconfigured and spawning shells
  • D) Falco has a false-positive — shell spawns in production are normal
B) Investigate and isolate. A root shell in a production container is a high-severity security event. Immediately: capture the pod's network connections and process list (kubectl debug), cordon the node to prevent lateral movement, preserve pod logs and forensic data, then delete the compromised pod. Treat it as a breach until proven otherwise.
Q2. You add --encryption-provider-config to kube-apiserver. What must you do to ensure existing Secrets are also encrypted?
  • A) Nothing — the flag retroactively encrypts all existing data in etcd
  • B) Restart etcd — it will re-encrypt on startup
  • C) Run kubectl get secrets -A -o json | kubectl replace -f - to force a re-write through the API server
  • D) Delete and recreate all Secrets manually
C) Force a re-write via kubectl replace. The encryption config only encrypts data written after the flag is set. Existing Secrets remain plaintext in etcd until they are re-written through the API server (which encrypts on write). The command reads all secrets and writes them back, triggering encryption for each.
Q3. What is the key advantage of External Secrets Operator over storing encrypted secrets (SOPS/Sealed Secrets) in Git?
  • A) ESO is faster at injecting secrets into pods
  • B) ESO pulls live values from a secrets store with automatic rotation — secrets never exist in Git in any form, and rotations propagate automatically
  • C) ESO supports more secret types than SOPS
  • D) ESO eliminates the need for RBAC on secrets
B) Live pull with automatic rotation. With SOPS/Sealed Secrets, the encrypted secret value still lives in Git — if the encryption key is leaked, historical commits are compromised. ESO fetches the current value from a proper secrets store (Vault, AWS SM) on a refresh interval. When a secret rotates in the store, ESO propagates it to the Kubernetes Secret automatically — no Git commit needed.