DevSecOps project 2 - Harden a Kubernetes Cluster End to End
Lock down a production Kubernetes cluster using RBAC, Pod Security Admission, NetworkPolicies, OPA Gatekeeper, Falco runtime detection, and automated compliance scanning.
Domains & Technologies
Blueprint Walkthrough
The Breach That Happened Because of Defaults
A fintech startup running on Kubernetes had been in production for eight months. Their application was secure. Their code had no vulnerabilities. Their team was competent.
Then a developer accidentally pushed a pod manifest that had hostPID: true set — a leftover from debugging. The pod started successfully. An attacker who found a remote code execution vulnerability in the application could now escape the container and see every process running on the Kubernetes node — including processes from other customers' workloads on the same node.
The vulnerability was not in the code. It was in a missing policy. Nobody had told Kubernetes "never allow hostPID: true in this cluster." Kubernetes does exactly what you tell it to do. If you do not tell it what to refuse, it refuses nothing.
This capstone hardens a Kubernetes cluster using six layers of security controls. When you finish, a developer cannot accidentally — or deliberately — deploy a privileged container, access another service they should not, escalate their own permissions, or run a cryptocurrency miner without an alert firing within 60 seconds.
The six layers:
┌─────────────────────────────────────────────────────────────┐│ Layer 1: RBAC — Who can do what in the cluster ││ Layer 2: Pod Security Admission — What pods can run ││ Layer 3: NetworkPolicies — What can talk to what ││ Layer 4: OPA Gatekeeper — Custom admission policies ││ Layer 5: Falco — Runtime threat detection ││ Layer 6: kube-bench — Automated CIS compliance scanning │└─────────────────────────────────────────────────────────────┘Understanding the Attack Surfaces
What Kubernetes exposes by default
Kubernetes is secure by design in some areas and dangerously permissive in others. Understanding what is locked by default and what you need to lock yourself is the foundation of cluster hardening.
Locked by default:
- etcd encryption at rest (if configured during cluster bootstrap)
- API server TLS
- Service account token mounting (can be disabled)
Open by default — you must close these:
- All pods in a namespace can reach all other pods in the cluster on any port
- A pod can request root (UID 0) inside the container
- A pod can mount the host filesystem
- Any authenticated user who can create pods can potentially escalate to cluster-admin
- No runtime monitoring — if a container starts a shell or writes a binary, nothing alerts
## Set up a local cluster for this capstone## Use kind (Kubernetes IN Docker) — it runs a full K8s cluster locally ## Install kindcurl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64chmod +x ./kind && sudo mv ./kind /usr/local/bin/kind ## Install kubectlcurl -LO "https://dl.k8s.io/release/$(curl -L -s \ https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"chmod +x kubectl && sudo mv kubectl /usr/local/bin/ ## Install Helm (used to deploy Falco and Gatekeeper)curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash echo "✅ Tools installed"## kind-config.yaml## Create a cluster with the audit log enabled## The audit log records every API request — required for security analysis kind: ClusterapiVersion: kind.x-k8s.io/v1alpha4nodes:- role: control-plane kubeadmConfigPatches: - | kind: ClusterConfiguration apiServer: extraArgs: ## Enable audit logging to file audit-log-path: /var/log/kubernetes/audit.log ## Log all requests at RequestResponse level audit-log-maxage: "30" audit-log-maxbackup: "3" audit-log-maxsize: "100" extraVolumes: - name: audit-log hostPath: /var/log/kubernetes mountPath: /var/log/kubernetes readOnly: false pathType: DirectoryOrCreate## Create the clusterkind create cluster --config kind-config.yaml --name devsecops-capstone ## Verify it is runningkubectl cluster-info --context kind-devsecops-capstonekubectl get nodes echo "✅ Cluster created"Layer 1 — RBAC
Understanding Roles, ClusterRoles, and Bindings
RBAC (Role-Based Access Control) is Kubernetes's permission system. It answers one question: "Can this identity perform this action on this resource?"
The four objects:
- Role — grants permissions within a single namespace
- ClusterRole — grants permissions across the entire cluster
- RoleBinding — attaches a Role (or ClusterRole) to a user, group, or ServiceAccount
- ClusterRoleBinding — attaches a ClusterRole cluster-wide
The most dangerous RBAC misconfiguration is giving a ServiceAccount more permissions than it needs. A compromised pod inherits the permissions of its ServiceAccount. A pod with cluster-admin permissions is a full cluster compromise.
Auditing existing RBAC permissions
## Check who has cluster-admin permissions## This should show only the kubeadm bootstrapper and kube-system accountskubectl get clusterrolebindings \ -o custom-columns='NAME:.metadata.name,ROLE:.roleRef.name,SUBJECTS:.subjects[*].name' \ | grep cluster-admin ## Find all ServiceAccounts that can get secrets cluster-wide## This is a critical misconfiguration — secrets contain tokens and credentialskubectl auth can-i get secrets --all-namespaces --as=system:serviceaccount:default:default ## List all RBAC bindings in the default namespacekubectl get rolebindings,clusterrolebindings -n defaultCreating the application RBAC structure
The principle you are following is least privilege: every ServiceAccount gets the minimum permissions it needs and nothing more.
## rbac/namespaces.yaml## Create separate namespaces for isolation apiVersion: v1kind: Namespacemetadata: name: payment-service labels: ## This label is used by NetworkPolicy and Pod Security Admission app.kubernetes.io/environment: production ## Pod Security Admission label — enforced at namespace level pod-security.kubernetes.io/enforce: restricted pod-security.kubernetes.io/enforce-version: latestapiVersion: v1kind: Namespacemetadata: name: monitoring labels: app.kubernetes.io/environment: monitoring pod-security.kubernetes.io/enforce: baseline pod-security.kubernetes.io/enforce-version: latest## rbac/payment-service-rbac.yaml## ServiceAccount with minimal permissions for the payment service apiVersion: v1kind: ServiceAccountmetadata: name: payment-service namespace: payment-service annotations: ## Document why this ServiceAccount exists description: "ServiceAccount for the payment processing pods"## Prevent the default service account token from being auto-mounted## The payment service does not need to call the Kubernetes APIautomountServiceAccountToken: false## The payment service needs to read its own config from a ConfigMap## That is the ONLY Kubernetes API permission it needsapiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: name: payment-service-reader namespace: payment-servicerules: ## apiGroups: "" means the core API group (Pods, ConfigMaps, Secrets, etc.) - apiGroups: [""] resources: ["configmaps"] ## Only the specific ConfigMap this service needs resourceNames: ["payment-service-config"] verbs: ["get", "watch"]apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: name: payment-service-reader-binding namespace: payment-servicesubjects: - kind: ServiceAccount name: payment-service namespace: payment-serviceroleRef: kind: Role name: payment-service-reader apiGroup: rbac.authorization.k8s.io## rbac/developer-rbac.yaml## Developer access: read-only on application namespaces## Cannot read Secrets (those go through a secrets manager) apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: name: developer-readonly namespace: payment-servicerules: - apiGroups: ["", "apps", "batch"] resources: ["pods", "deployments", "services", "configmaps", "replicasets", "jobs", "events"] verbs: ["get", "list", "watch"] ## Developers can exec into pods for debugging - apiGroups: [""] resources: ["pods/exec", "pods/log"] verbs: ["create", "get"] ## Developers explicitly CANNOT: ## - read Secrets (not listed here) ## - delete resources ## - modify RBAC ## - access other namespacesapiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: name: developer-readonly-binding namespace: payment-servicesubjects: ## Bind to a group — add engineers to the "developers" group in your IdP - kind: Group name: developers apiGroup: rbac.authorization.k8s.ioroleRef: kind: Role name: developer-readonly apiGroup: rbac.authorization.k8s.io## Apply all RBAC resourceskubectl apply -f rbac/ ## Test the payment-service ServiceAccount permissions## Should succeed (has permission to get this ConfigMap)kubectl auth can-i get configmaps/payment-service-config \ --namespace=payment-service \ --as=system:serviceaccount:payment-service:payment-service ## Should fail (does not have permission)kubectl auth can-i get secrets \ --namespace=payment-service \ --as=system:serviceaccount:payment-service:payment-service ## Should fail (cannot access other namespaces)kubectl auth can-i get pods \ --namespace=kube-system \ --as=system:serviceaccount:payment-service:payment-service echo "✅ RBAC layer configured"Layer 2 — Pod Security Admission
What Pod Security Standards enforce
Pod Security Admission (PSA) replaced the deprecated PodSecurityPolicy in Kubernetes 1.25. It enforces security profiles at the namespace level, controlling what pod configurations are allowed to run.
Three built-in profiles:
- Privileged — no restrictions (only for trusted system workloads in kube-system)
- Baseline — prevents the most dangerous settings (privileged mode, hostNetwork, hostPID)
- Restricted — the most secure profile (requires non-root, drops all capabilities, read-only root filesystem)
## Test that the restricted policy actually blocks privileged pods## This should be rejected immediately cat > /tmp/test-privileged-pod.yaml << 'EOF'apiVersion: v1kind: Podmetadata: name: test-privileged namespace: payment-servicespec: containers: - name: nginx image: nginx:alpine securityContext: privileged: trueEOF kubectl apply -f /tmp/test-privileged-pod.yaml## Expected output:## Error from server (Forbidden): error when creating "/tmp/test-privileged-pod.yaml":## pods "test-privileged" is forbidden: violates PodSecurity "restricted:latest":## privileged (container "nginx" must not set securityContext.privileged=true)## deployments/payment-service-deployment.yaml## A compliant pod spec that passes the restricted PSA profile apiVersion: apps/v1kind: Deploymentmetadata: name: payment-service namespace: payment-servicespec: replicas: 2 selector: matchLabels: app: payment-service template: metadata: labels: app: payment-service version: "1.0.0" spec: serviceAccountName: payment-service ## Prevent the service account token from being mounted automountServiceAccountToken: false ## Pod-level security context securityContext: ## Run as a non-root user runAsNonRoot: true runAsUser: 10001 runAsGroup: 10001 ## Prevent privilege escalation via setuid binaries seccompProfile: type: RuntimeDefault containers: - name: payment-service image: ghcr.io/razorpay/payment-service:abc1234 ports: - containerPort: 8080 ## Container-level security context — restricted profile requires all of these securityContext: ## Container cannot escalate privileges (sudo, setuid) allowPrivilegeEscalation: false readOnlyRootFilesystem: true ## Drop ALL Linux capabilities and add back only what is needed ## Most web services need zero capabilities capabilities: drop: ["ALL"] resources: requests: cpu: "100m" memory: "128Mi" limits: cpu: "500m" memory: "256Mi" ## readOnlyRootFilesystem=true means the app cannot write anywhere by default ## Mount writable volumes only for directories that need writes volumeMounts: - name: tmp-dir mountPath: /tmp - name: exports-dir mountPath: /exports volumes: ## emptyDir creates a temporary in-memory filesystem - name: tmp-dir emptyDir: {} - name: exports-dir emptyDir: {}## Apply the compliant deploymentkubectl apply -f deployments/payment-service-deployment.yaml ## Verify it started successfullykubectl get pods -n payment-service## Expected: 2 pods in Running state echo "✅ Pod Security Admission layer configured"Layer 3 — Network Policies
The default network problem
By default, every pod in a Kubernetes cluster can reach every other pod on any port. A compromised frontend pod can connect directly to your database pod on port 5432. An attacker who escapes from the payment-service can probe every service in the cluster.
NetworkPolicy is a Kubernetes firewall that controls pod-to-pod traffic. It works at the IP and port level. Without a NetworkPolicy, all traffic is allowed. With a NetworkPolicy, all traffic not explicitly allowed is denied.
RememberNetworkPolicy requires a CNI (Container Network Interface) plugin that supports it. Flannel does not. Calico, Cilium, and Weave all do. Kind uses Kindnet by default — install Calico for NetworkPolicy support in this capstone.
## Install Calico CNI for NetworkPolicy support in kindkubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml ## Wait for Calico to be readykubectl rollout status daemonset/calico-node -n kube-system --timeout=120s echo "✅ Calico CNI installed — NetworkPolicies now enforced"## network-policies/default-deny-all.yaml## The most important policy: deny everything unless explicitly allowed.## Apply this first, then add allow policies for legitimate traffic. apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: default-deny-all namespace: payment-servicespec: ## podSelector: {} means this policy applies to ALL pods in the namespace podSelector: {} ## policyTypes with both Ingress and Egress means deny all traffic in both directions policyTypes: - Ingress - Egress## network-policies/allow-payment-ingress.yaml## Allow external traffic to reach the payment service from the ingress controller apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-payment-ingress namespace: payment-servicespec: podSelector: matchLabels: app: payment-service policyTypes: - Ingress ingress: - from: ## Only allow traffic from the ingress-nginx namespace - namespaceSelector: matchLabels: kubernetes.io/metadata.name: ingress-nginx ## Within that namespace, only from pods labelled as the ingress controller podSelector: matchLabels: app.kubernetes.io/name: ingress-nginx ports: - protocol: TCP port: 8080## network-policies/allow-payment-egress.yaml## Allow the payment service to make outbound connections it needs apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-payment-egress namespace: payment-servicespec: podSelector: matchLabels: app: payment-service policyTypes: - Egress egress: ## Allow DNS resolution — without this the service cannot resolve any hostnames - ports: - protocol: UDP port: 53 - protocol: TCP port: 53 ## Allow connecting to the database namespace only - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: database podSelector: matchLabels: app: postgresql ports: - protocol: TCP port: 5432 ## Allow connecting to external payment processors (Razorpay, NPCI) ## In production: use CIDR ranges for specific IPs instead of any - ports: - protocol: TCP port: 443## network-policies/allow-monitoring-scrape.yaml## Allow Prometheus to scrape metrics from the payment service apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-monitoring-scrape namespace: payment-servicespec: podSelector: matchLabels: app: payment-service policyTypes: - Ingress ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: monitoring podSelector: matchLabels: app.kubernetes.io/name: prometheus ports: - protocol: TCP port: 9090## Apply all network policieskubectl apply -f network-policies/ ## Test that the policies work ## Deploy a test pod in the payment-service namespacekubectl run test-pod \ --image=busybox:1.35 \ --restart=Never \ --namespace=payment-service \ -- sleep 3600 ## Should FAIL — default deny blocks inter-pod traffickubectl exec -n payment-service test-pod -- \ wget -T 2 -q http://kubernetes.default.svc.cluster.local## Expected: wget: download timed out ## Should SUCCEED — DNS is explicitly allowedkubectl exec -n payment-service test-pod -- \ nslookup kubernetes.default.svc.cluster.local## Expected: successful DNS resolution echo "✅ Network Policy layer configured"Layer 4 — OPA Gatekeeper
Writing custom admission policies
OPA Gatekeeper is an admission controller that validates Kubernetes resource requests against custom policies before they are created. It uses Rego, a policy language, to define rules.
While Pod Security Admission handles common security profiles, Gatekeeper handles your organization-specific rules:
- All production containers must have resource limits set
- Container images must come from the approved registry only
- All deployments must have at least 2 replicas
- Labels for team and cost center are required on every namespace
## Install OPA Gatekeeperkubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/v3.14.0/deploy/gatekeeper.yaml ## Wait for Gatekeeper to be readykubectl rollout status deployment/gatekeeper-controller-manager \ -n gatekeeper-system --timeout=120s echo "✅ OPA Gatekeeper installed"## gatekeeper/allowed-registries-template.yaml## Define the policy template — this is the Rego logic## A ConstraintTemplate defines what the policy checks.## A Constraint (next file) configures where and how it applies. apiVersion: templates.gatekeeper.sh/v1kind: ConstraintTemplatemetadata: name: k8sallowedregistries annotations: description: "Require container images to come from approved registries only"spec: crd: spec: names: kind: K8sAllowedRegistries ## These are the parameters you configure in the Constraint validation: openAPIV3Schema: type: object properties: registries: type: array items: type: string targets: - target: admission.k8s.gatekeeper.sh rego: | package k8sallowedregistries violation[{"msg": msg}] { ## Get every container in the pod spec (including init containers) container := input.review.object.spec.containers[_] ## Check if the image starts with any of the allowed registries not starts_with_allowed(container.image) msg := sprintf( "Container '%v' uses image '%v' which is not from an approved registry. Allowed: %v", [container.name, container.image, input.parameters.registries] ) } ## Check initContainers too violation[{"msg": msg}] { container := input.review.object.spec.initContainers[_] not starts_with_allowed(container.image) msg := sprintf( "Init container '%v' uses image '%v' not from approved registry", [container.name, container.image] ) } starts_with_allowed(image) { ## startswith checks if image begins with one of the approved registry prefixes startswith(image, input.parameters.registries[_]) }## gatekeeper/allowed-registries-constraint.yaml## Apply the policy — specify allowed registries and which namespaces to check apiVersion: constraints.gatekeeper.sh/v1beta1kind: K8sAllowedRegistriesmetadata: name: allowed-registriesspec: ## enforcementAction: deny = block the resource if policy fails ## Use "warn" first when rolling out to see what would be blocked enforcementAction: deny match: kinds: - apiGroups: [""] kinds: ["Pod"] ## Apply to all namespaces except kube-system and gatekeeper-system excludedNamespaces: ["kube-system", "gatekeeper-system"] parameters: registries: - "ghcr.io/razorpay/" - "registry.k8s.io/" - "docker.io/library/"## gatekeeper/require-resource-limits-template.yaml## Policy: all containers must have CPU and memory limits apiVersion: templates.gatekeeper.sh/v1kind: ConstraintTemplatemetadata: name: k8srequirelimits annotations: description: "Require all containers to have CPU and memory resource limits"spec: crd: spec: names: kind: K8sRequireLimits targets: - target: admission.k8s.gatekeeper.sh rego: | package k8srequirelimits violation[{"msg": msg}] { container := input.review.object.spec.containers[_] ## Check if memory limit is missing not container.resources.limits.memory msg := sprintf( "Container '%v' is missing memory limit. Add resources.limits.memory.", [container.name] ) } violation[{"msg": msg}] { container := input.review.object.spec.containers[_] ## Check if CPU limit is missing not container.resources.limits.cpu msg := sprintf( "Container '%v' is missing CPU limit. Add resources.limits.cpu.", [container.name] ) }## gatekeeper/require-resource-limits-constraint.yamlapiVersion: constraints.gatekeeper.sh/v1beta1kind: K8sRequireLimitsmetadata: name: require-resource-limitsspec: enforcementAction: deny match: kinds: - apiGroups: [""] kinds: ["Pod"] excludedNamespaces: ["kube-system", "gatekeeper-system"]## Apply all Gatekeeper policieskubectl apply -f gatekeeper/ ## Test the registry policy — should be rejectedcat > /tmp/test-registry.yaml << 'EOF'apiVersion: v1kind: Podmetadata: name: test-registry namespace: payment-servicespec: containers: - name: app image: nginx:latest resources: limits: cpu: "100m" memory: "128Mi"EOF kubectl apply -f /tmp/test-registry.yaml## Expected:## Error from server (Forbidden): admission webhook "validation.gatekeeper.sh" denied:## Container 'app' uses image 'nginx:latest' which is not from an approved registry.## Allowed: ["ghcr.io/razorpay/", "registry.k8s.io/", "docker.io/library/"] ## Test the resource limits policy — should be rejectedcat > /tmp/test-no-limits.yaml << 'EOF'apiVersion: v1kind: Podmetadata: name: test-no-limits namespace: payment-servicespec: containers: - name: app image: ghcr.io/razorpay/payment-service:latestEOF kubectl apply -f /tmp/test-no-limits.yaml## Expected error about missing resource limits echo "✅ OPA Gatekeeper policies active"Layer 5 — Falco Runtime Detection
Detecting threats that slipped past admission
The four previous layers are all preventive controls. They stop bad configurations before they run. Falco is a detective control — it watches what is actually happening inside running containers and alerts when something suspicious occurs.
Falco uses Linux kernel syscall tracing (via eBPF) to see every system call made by every container. Its rule engine checks those syscalls against patterns that indicate attacks:
- A shell was spawned inside a container (common first step after container escape)
- A binary was written to a directory that should be read-only
- A container opened a sensitive file like
/etc/shadowor/proc/*/mem - A process made a network connection to an unexpected port
## Add the Falco Helm repositoryhelm repo add falcosecurity https://falcosecurity.github.io/chartshelm repo update ## Install Falco with eBPF driver (requires kernel headers)## driver.kind: ebpf = uses eBPF instead of a kernel module (safer, more portable)helm install falco falcosecurity/falco \ --namespace falco \ --create-namespace \ --set driver.kind=ebpf \ --set falcosidekick.enabled=true \ --set falcosidekick.webui.enabled=true ## Wait for Falco to be readykubectl rollout status daemonset/falco -n falco --timeout=180s echo "✅ Falco installed"## falco/custom-rules.yaml## Custom rules specific to the payment service## Add these to the Falco ConfigMap to detect payment-specific threats customRules: payment-service-rules.yaml: |- ## Rule 1: Detect shell execution inside payment-service containers ## A legitimate payment service never needs a shell at runtime - rule: Shell Spawned in Payment Service Container desc: > A shell was executed inside a payment-service container. This is never expected in production — indicates container escape or supply chain attack. condition: > spawned_process and container.name contains "payment-service" and proc.name in (shell_binaries) output: > Shell spawned in payment container (user=%user.name container=%container.name image=%container.image.repository:%container.image.tag shell=%proc.name parent=%proc.pname cmdline=%proc.cmdline) priority: CRITICAL tags: [payment, container, shell] ## Rule 2: Detect unexpected outbound connections from payment service ## The payment service should only connect to the database and Razorpay API - rule: Unexpected Network Connection from Payment Service desc: > The payment-service pod made a network connection to an unexpected destination. Could indicate data exfiltration or command-and-control communication. condition: > outbound and container.name contains "payment-service" and not fd.sip in (payment_allowed_ips) and not fd.sport in (payment_allowed_ports) output: > Unexpected outbound connection from payment service (user=%user.name container=%container.name destination_ip=%fd.sip destination_port=%fd.sport process=%proc.name cmdline=%proc.cmdline) priority: HIGH tags: [payment, network, exfiltration] ## Rule 3: Detect cryptocurrency mining processes ## Mining processes have distinct CPU usage patterns and connect to mining pools - rule: Crypto Miner Process Detected desc: > A process associated with cryptocurrency mining was started. Indicates unauthorized resource use — a compromised container running a miner. condition: > spawned_process and proc.name in (crypto_miners) output: > Crypto miner process started (user=%user.name host=%evt.hostname container=%container.name image=%container.image.repository process=%proc.name cmdline=%proc.cmdline) priority: CRITICAL tags: [cryptomining, resource-abuse] ## Macros used in the rules above - macro: shell_binaries condition: > proc.name in (bash, sh, zsh, fish, dash, ksh, tcsh, csh) - list: crypto_miners items: [xmrig, minerd, cpuminer, ethminer, t-rex, gminer, nbminer] - list: payment_allowed_ips items: ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] - list: payment_allowed_ports items: [5432, 443, 53]## Apply the custom rules via ConfigMapkubectl apply -f falco/custom-rules.yaml ## Restart Falco to pick up the new ruleskubectl rollout restart daemonset/falco -n falco ## Test that Falco detects shell execution## Run a shell inside a payment-service podkubectl exec -n payment-service \ $(kubectl get pods -n payment-service -l app=payment-service -o name | head -1) \ -- /bin/sh -c "echo 'test'" ## Check Falco logs for the alertkubectl logs -n falco daemonset/falco | grep "Shell Spawned" | tail -5## Expected output:## 10:30:45.123456789: Critical Shell spawned in payment container## (user=root container=payment-service image=ghcr.io/razorpay/payment-service:abc1234## shell=sh parent=kubectl cmdline=sh -c echo 'test') echo "✅ Falco runtime detection working"Layer 6 — CIS Compliance Scanning with kube-bench
Automated compliance auditing
kube-bench is an open-source tool that checks Kubernetes cluster configuration against the CIS Kubernetes Benchmark — a set of security recommendations from the Center for Internet Security. It checks everything from API server flags to kubelet configuration to etcd encryption.
Running kube-bench gives you a prioritized list of what to fix to meet compliance standards like SOC 2, PCI-DSS, and ISO 27001.
## Run kube-bench as a Kubernetes Job## The Job runs kube-bench inside the cluster with appropriate permissions cat > /tmp/kube-bench-job.yaml << 'EOF'apiVersion: batch/v1kind: Jobmetadata: name: kube-bench namespace: defaultspec: template: spec: hostPID: true serviceAccountName: default containers: - name: kube-bench image: aquasec/kube-bench:v0.7.2 command: ["kube-bench", "--json", "--outputfile", "/tmp/results.json"] volumeMounts: - name: var-lib-etcd mountPath: /var/lib/etcd readOnly: true - name: var-lib-kubelet mountPath: /var/lib/kubelet readOnly: true - name: etc-kubernetes mountPath: /etc/kubernetes readOnly: true volumes: - name: var-lib-etcd hostPath: path: /var/lib/etcd - name: var-lib-kubelet hostPath: path: /var/lib/kubelet - name: etc-kubernetes hostPath: path: /etc/kubernetes restartPolicy: NeverEOF kubectl apply -f /tmp/kube-bench-job.yaml ## Wait for completionkubectl wait --for=condition=complete job/kube-bench --timeout=120s ## View the results — look at FAIL entries firstkubectl logs job/kube-bench | grep -E "^\[FAIL\]" | head -20## Parse the JSON output for a scorecardkubectl logs job/kube-bench | python3 -c "import json, sysresults = json.load(sys.stdin)total = pass_count = fail_count = warn_count = 0for control in results.get('Controls', []): for test in control.get('tests', []): for result in test.get('results', []): total += 1 status = result.get('status', '') if status == 'PASS': pass_count += 1 elif status == 'FAIL': fail_count += 1 elif status == 'WARN': warn_count += 1 print(f'CIS Benchmark Scorecard:')print(f' Total checks: {total}')print(f' Passed: {pass_count} ({pass_count/total*100:.1f}%)')print(f' Failed: {fail_count} ({fail_count/total*100:.1f}%)')print(f' Warnings: {warn_count} ({warn_count/total*100:.1f}%)')print()print('Focus on FAIL items first. Aim for < 5 FAIL in production.')"Validating All Six Layers Together
## Full end-to-end validation script echo "=== DevSecOps Kubernetes Hardening — Validation ==="echo "" ## Layer 1: RBACecho "--- Layer 1: RBAC ---"## ServiceAccount cannot get secretsRESULT=$(kubectl auth can-i get secrets \ --namespace=payment-service \ --as=system:serviceaccount:payment-service:payment-service 2>&1)if echo "$RESULT" | grep -q "no"; then echo "✅ RBAC: payment-service cannot get secrets"else echo "❌ RBAC: FAIL — payment-service can get secrets (unexpected)"fi ## Layer 2: Pod Security Admissionecho ""echo "--- Layer 2: Pod Security Admission ---"RESULT=$(kubectl apply -f /tmp/test-privileged-pod.yaml 2>&1)if echo "$RESULT" | grep -q "Forbidden"; then echo "✅ PSA: privileged pods are blocked in payment-service namespace"else echo "❌ PSA: FAIL — privileged pod was not blocked"fi ## Layer 3: NetworkPolicyecho ""echo "--- Layer 3: NetworkPolicy ---"RESULT=$(kubectl exec -n payment-service test-pod -- \ wget -T 2 -q http://kube-dns.kube-system.svc.cluster.local 2>&1)if echo "$RESULT" | grep -q "timed out\|Connection refused"; then echo "✅ NetworkPolicy: cross-namespace traffic is blocked"else echo "❌ NetworkPolicy: FAIL — unexpected connection succeeded"fi ## Layer 4: OPA Gatekeeperecho ""echo "--- Layer 4: OPA Gatekeeper ---"RESULT=$(kubectl apply -f /tmp/test-registry.yaml 2>&1)if echo "$RESULT" | grep -q "denied"; then echo "✅ Gatekeeper: unapproved registry is blocked"else echo "❌ Gatekeeper: FAIL — pod from unapproved registry was allowed"fi ## Layer 5: Falcoecho ""echo "--- Layer 5: Falco ---"FALCO_RUNNING=$(kubectl get pods -n falco -l app.kubernetes.io/name=falco \ --field-selector=status.phase=Running --no-headers | wc -l)if [ "$FALCO_RUNNING" -gt 0 ]; then echo "✅ Falco: $FALCO_RUNNING Falco pod(s) running and monitoring"else echo "❌ Falco: FAIL — no Falco pods running"fi ## Layer 6: kube-benchecho ""echo "--- Layer 6: CIS Compliance ---"FAIL_COUNT=$(kubectl logs job/kube-bench 2>/dev/null | grep -c "^\[FAIL\]" || echo "N/A")echo "ℹ️ kube-bench FAIL count: $FAIL_COUNT (target: <10 for new clusters)" echo ""echo "=== Validation Complete ==="Production Checklist
## ─── RBAC ────────────────────────────────────────────────────## No service account with cluster-admin except system accountskubectl get clusterrolebindings -o json | \ python3 -c "import json, sysdata = json.load(sys.stdin)for b in data['items']: if b['roleRef']['name'] == 'cluster-admin': subjects = b.get('subjects', []) for s in subjects: if not s.get('namespace', '').startswith('kube-'): print(f'⚠️ Non-system cluster-admin: {s}')print('✅ RBAC cluster-admin check complete')" ## ─── Pod Security ────────────────────────────────────────────## Verify restricted PSA is applied to production namespaceskubectl get namespaces -o json | \ python3 -c "import json, sysdata = json.load(sys.stdin)for ns in data['items']: labels = ns['metadata'].get('labels', {}) name = ns['metadata']['name'] if name in ['payment-service', 'default']: level = labels.get('pod-security.kubernetes.io/enforce', 'NONE') status = '✅' if level == 'restricted' else '⚠️' print(f'{status} {name}: PSA={level}')" ## ─── NetworkPolicy ────────────────────────────────────────────## Every non-system namespace should have a default-deny policyfor ns in payment-service; do COUNT=$(kubectl get networkpolicies -n $ns \ -o jsonpath='{.items[*].metadata.name}' | tr ' ' '\n' | \ grep -c default-deny || echo 0) if [ "$COUNT" -gt 0 ]; then echo "✅ NetworkPolicy: default-deny present in $ns" else echo "❌ NetworkPolicy: NO default-deny in $ns" fidone ## ─── Gatekeeper ───────────────────────────────────────────────kubectl get constraints 2>/dev/null | grep -v "^NAME" | while read line; do echo "✅ Gatekeeper constraint active: $line"done ## ─── Falco ────────────────────────────────────────────────────kubectl get pods -n falco --field-selector=status.phase=Running --no-headers | \ awk '{print "✅ Falco running: " $1}' echo "✅ All production checks complete"Common Production Mistakes
Giving every new ServiceAccount the default ServiceAccount permissions. Kubernetes automatically creates a default ServiceAccount in every namespace, and newly created pods use it unless specified otherwise. If developers do not explicitly set serviceAccountName in their pod spec, their pod gets whatever permissions the default ServiceAccount has. Set automountServiceAccountToken: false on the default ServiceAccount in every namespace and require explicit ServiceAccount assignments.
Setting NetworkPolicy enforcementAction: warn and never switching to deny. The warn mode is appropriate when first rolling out policies to a cluster with existing workloads — you need to see what would be blocked before actually blocking it. The mistake is leaving it in warn mode permanently. Warnings that do not block are documentation, not security. Set a calendar reminder for two weeks after rollout to review warnings and switch to deny.
Writing Gatekeeper rules that are too broad and breaking legitimate system workloads. A policy that applies to all namespaces including kube-system will break Kubernetes itself — system pods use images from registry.k8s.io which may not match your allowed registry list. Always add excludedNamespaces: ["kube-system", "gatekeeper-system", "cert-manager"] to every Constraint. Test every new policy in enforcementAction: warn first.
Not alerting on Falco CRITICAL events. Falco generates events continuously. Without routing CRITICAL events to Slack or PagerDuty, they are logs that nobody reads. Install Falcosidekick with a Slack webhook configured on day one. A CRITICAL Falco event — shell in container, sensitive file read, crypto miner — should page the on-call engineer within 2 minutes, not be discovered during a weekly log review.
Treating kube-bench WARN as acceptable. kube-bench produces three statuses: PASS, FAIL, and WARN. WARN means the check requires manual investigation because the correct setting depends on your environment. Teams often focus only on FAIL items and ignore all WARNs. Many WARNs represent real security gaps — audit log configuration, anonymous API access, insecure kubelet settings. Review every WARN and document your decision on each one.
Running Falco with the kernel module driver instead of eBPF in production. The kernel module approach requires loading a custom kernel module, which increases the attack surface and can cause system instability if the module crashes. eBPF is sandboxed by the Linux kernel's verifier, cannot crash the system, and requires no kernel module installation. Always use driver.kind: ebpf in production clusters.
Quick Reference
| Layer | Tool | What It Controls | Where Configured |
|---|---|---|---|
| RBAC | kubectl | API access permissions | ClusterRole / Role YAML |
| Pod Security | PSA | Pod spec restrictions | Namespace labels |
| Network | Calico | Pod-to-pod traffic | NetworkPolicy YAML |
| Admission | Gatekeeper | Custom policy gates | ConstraintTemplate + Constraint |
| Runtime | Falco | Live container behavior | Falco rules ConfigMap |
| Compliance | kube-bench | CIS Benchmark score | Scheduled Job |
| Command | What It Does |
|---|---|
kubectl auth can-i ACTION --as=system:serviceaccount:NS:SA |
Test RBAC permissions |
kubectl get networkpolicies -n NAMESPACE |
List active network policies |
kubectl get constraints |
List active Gatekeeper policies |
kubectl logs -n falco daemonset/falco | grep CRITICAL |
Check Falco alerts |
kubectl logs job/kube-bench | grep FAIL |
View CIS benchmark failures |
kubectl label ns NAMESPACE pod-security.kubernetes.io/enforce=restricted |
Apply PSA to namespace |
Videos & Guides
No external walkthroughs, video tutorials, or reference files attached to this blueprint yet.