Install any skill in seconds. Free to start, no credit card required.
Get Started Free →This skill covers hardening managed Kubernetes clusters on EKS, AKS, and GKE by implementing Pod Security Standards, network policies, workload identity, RBAC scoping, image admission controls, and runtime security monitoring. It addresses cloud-specific security features including IRSA for EKS, Workload Identity for GKE, and Managed Identities for AKS.
.claude/skills/securing-kubernetes-on-cloud/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-16 | ✗→✓ | ▲ Improved | — | — |
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-06 | ✗→✓ | ▲ Improved | — | — |
| case-02 | ✗→✓ | ▲ Improved | — | — |
| case-11 | ✓→✓ | = Same ✓ | — | — |
Do not use for non-Kubernetes container deployments like ECS Fargate or Azure Container Instances, for application-level security within containers (see securing-serverless-functions), or for CI/CD pipeline security (see implementing-cloud-devsecops).
Apply Pod Security Admission labels at the namespace level to enforce the Restricted profile in production namespaces. Pod Security Policies were removed in Kubernetes v1.25 and replaced with Pod Security Admission.
yaml# Production namespace with restricted Pod Security Standard apiVersion: v1 kind: Namespace metadata: name: production labels: pod-security.kubernetes.io/enforce: restricted pod-security.kubernetes.io/enforce-version: latest pod-security.kubernetes.io/audit: restricted pod-security.kubernetes.io/warn: restricted --- # Staging namespace with baseline enforcement apiVersion: v1 kind: Namespace metadata: name: staging labels: pod-security.kubernetes.io/enforce: baseline pod-security.kubernetes.io/audit: restricted pod-security.kubernetes.io/warn: restricted
yaml# Pod spec compliant with restricted profile apiVersion: v1 kind: Pod metadata: name: secure-app namespace: production spec: automountServiceAccountToken: false securityContext: runAsNonRoot: true runAsUser: 1000 fsGroup: 1000 seccompProfile: type: RuntimeDefault containers: - name: app image: company/app:v2.1@sha256:abc123... securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] resources: limits: cpu: "500m" memory: "256Mi" requests: cpu: "100m" memory: "128Mi"
Eliminate static cloud credentials in pods by binding Kubernetes service accounts to cloud IAM roles.
bash# EKS: IAM Roles for Service Accounts (IRSA) eksctl create iamserviceaccount \ --cluster production-cluster \ --namespace production \ --name web-app-sa \ --attach-policy-arn arn:aws:iam::123456789012:policy/WebAppS3ReadOnly \ --approve # GKE: Workload Identity gcloud iam service-accounts create web-app-sa \ --project=my-gcp-project gcloud iam service-accounts add-iam-policy-binding \ web-app-sa@my-gcp-project.iam.gserviceaccount.com \ --role roles/storage.objectViewer \ --member "serviceAccount:my-gcp-project.svc.id.goog[production/web-app-sa]" kubectl annotate serviceaccount web-app-sa \ --namespace production \ iam.gke.io/gcp-service-account=web-app-sa@my-gcp-project.iam.gserviceaccount.com # AKS: Azure AD Workload Identity az identity create --name web-app-identity --resource-group production-rg az identity federated-credential create \ --name web-app-federation \ --identity-name web-app-identity \ --resource-group production-rg \ --issuer "$(az aks show -n production-cluster -g production-rg --query oidcIssuerProfile.issuerUrl -o tsv)" \ --subject system:serviceaccount:production:web-app-sa
Deploy network policies to restrict pod-to-pod communication following the principle of least privilege. By default, Kubernetes allows all pods to communicate with each other.
yaml# Default deny all ingress and egress in production namespace apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny-all namespace: production spec: podSelector: {} policyTypes: - Ingress - Egress --- # Allow web-app to receive traffic from ingress controller only apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-ingress-to-web namespace: production spec: podSelector: matchLabels: app: web-app policyTypes: - Ingress ingress: - from: - namespaceSelector: matchLabels: name: ingress-nginx ports: - protocol: TCP port: 8080 --- # Allow web-app to connect to database only apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-web-to-db namespace: production spec: podSelector: matchLabels: app: web-app policyTypes: - Egress egress: - to: - podSelector: matchLabels: app: postgres ports: - protocol: TCP port: 5432 - to: - namespaceSelector: {} podSelector: matchLabels: k8s-app: kube-dns ports: - protocol: UDP port: 53
Scope Kubernetes RBAC roles to specific namespaces and resources. Avoid ClusterRoleBindings for non-administrative users.
yaml# Developer role scoped to specific namespace apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: developer-role namespace: staging rules: - apiGroups: [""] resources: ["pods", "pods/log", "services", "configmaps"] verbs: ["get", "list", "watch"] - apiGroups: ["apps"] resources: ["deployments"] verbs: ["get", "list", "watch", "update", "patch"] # Explicitly deny secrets access --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: developer-binding namespace: staging subjects: - kind: Group name: developers apiGroup: rbac.authorization.k8s.io roleRef: kind: Role name: developer-role apiGroup: rbac.authorization.k8s.io
Use admission controllers to enforce that only signed images from trusted registries are deployed. Implement OPA/Gatekeeper or Kyverno for policy enforcement.
yaml# Kyverno policy: require images from approved registries apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: restrict-image-registries spec: validationFailureAction: Enforce rules: - name: validate-registries match: any: - resources: kinds: ["Pod"] validate: message: "Images must come from approved registries" pattern: spec: containers: - image: "123456789012.dkr.ecr.us-east-1.amazonaws.com/* | gcr.io/my-gcp-project/*" --- # Kyverno policy: require image digest (no mutable tags) apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: require-image-digest spec: validationFailureAction: Enforce rules: - name: require-digest match: any: - resources: kinds: ["Pod"] validate: message: "Images must use digest references, not tags" pattern: spec: containers: - image: "*@sha256:*"
Deploy runtime security tools to detect anomalous behavior inside containers including process execution, file system modifications, and network connections.
bash# Deploy Falco for runtime threat detection helm repo add falcosecurity https://falcosecurity.github.io/charts helm install falco falcosecurity/falco \ --namespace falco-system --create-namespace \ --set falcosidekick.enabled=true \ --set falcosidekick.config.slack.webhookurl="https://hooks.slack.com/services/xxx" # Run kube-bench for CIS Kubernetes Benchmark assessment kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job-eks.yaml kubectl logs -l app=kube-bench
| Term | Definition | |------|------------| | Pod Security Standards | Three profiles (Privileged, Baseline, Restricted) enforced via Pod Security Admission that control pod security context capabilities | | Workload Identity | Cloud-native mechanism binding Kubernetes service accounts to cloud IAM roles for credential-free cloud API access (IRSA, GKE WI, AKS MI) | | Network Policy | Kubernetes resource defining allowed ingress and egress traffic flows between pods, enforced by the CNI plugin | | Admission Controller | Kubernetes plugin that intercepts API requests before persistence to validate or mutate resources against security policies | | RBAC | Role-Based Access Control in Kubernetes, defining what actions (verbs) identities can perform on which resources in which namespaces | | Seccomp Profile | Linux kernel feature restricting the system calls a container process can make, reducing the kernel attack surface | | Service Mesh | Infrastructure layer (Istio, Linkerd) providing mutual TLS, traffic policies, and observability for service-to-service communication |
Context: GuardDuty Extended Threat Detection generates an AttackSequence:EKS/CompromisedCluster finding. A developer pulled a public Docker image containing an embedded XMRig cryptominer that executes at container startup.
Approach:
Pitfalls: Deleting the pod before capturing the image digest and audit logs destroys forensic evidence. Blocking only the specific image tag allows the attacker to re-push with a different tag.
Kubernetes Security Assessment Report
=======================================
Cluster: production-cluster (EKS 1.29)
Provider: AWS (us-east-1)
Assessment Date: 2025-02-23
Tool: kube-bench v0.8.0 + manual review
CIS KUBERNETES BENCHMARK RESULTS:
Total Controls: 124
Passed: 98 (79%)
Failed: 18 (15%)
Warnings: 8 (6%)
CRITICAL FINDINGS:
[K8S-001] 3 namespaces lack Pod Security Standards enforcement
Namespaces: monitoring, logging, default
Remediation: Apply restricted PSA labels
[K8S-002] Default service account tokens auto-mounted in 12 deployments
Risk: Credential theft if container is compromised
Remediation: Set automountServiceAccountToken: false
[K8S-003] No network policies in production namespace
Risk: Unrestricted lateral movement between all pods
Remediation: Deploy default-deny policy with explicit allow rules
HIGH FINDINGS:
[K8S-004] 5 pods running as root with privileged security context
[K8S-005] Images deployed using mutable tags (:latest) in 8 deployments
[K8S-006] RBAC ClusterRoleBinding grants cluster-admin to developers group| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-11 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +18 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.