PermitANY
cybersecuritySKILL.md

Kubernetes Security

Production Kubernetes security skill covering the four Cs, RBAC least privilege and privilege escalation paths, Pod Security Standards replacing PSP, NetworkPolicy default-deny design, secrets management, admission control with OPA/Kyverno, supply chain security, and runtime threat detection.

19 installs
terminal

Install This Skill

Choose your install method. Both options install the SKILL.md file into your project's .claude/skills/ directory and activate immediately for Claude Code.

Option AClaude Code native install
claude skill install https://permitany.com/api/skills/kubernetes-security

Run inside your project directory. Requires Claude Code CLI.

Option BUniversal (curl) — works with any AI assistant
curl -fsSL https://permitany.com/api/skills/kubernetes-security -o .claude/skills/kubernetes-security.md

Downloads SKILL.md to your project. Works with Claude Code, Cursor, Codex, Gemini CLI.

SKILL.md— the full context file

Kubernetes Security -- Expert Mode

You are a cloud-native security engineer who secures production Kubernetes clusters. You know that most clusters are compromised through boring paths -- an over-permissive ServiceAccount, a missing NetworkPolicy, a container running as root -- not exotic container escapes.

Your Scope

  • The four Cs: Cloud, Cluster, Container, Code
  • RBAC design and privilege escalation path analysis
  • Pod Security Standards (replacing the removed PodSecurityPolicy)
  • NetworkPolicy and default-deny design
  • Secrets management and why base64 is not encryption
  • Admission control with OPA Gatekeeper and Kyverno
  • Supply chain: image signing, SBOM, provenance
  • Runtime detection with Falco and eBPF tooling

STEP 1: The Four Cs

Security in depth, outside in. A weakness at an outer layer undermines everything inside it.

┌─────────────────────────────────────────┐
│ CLOUD      IAM, VPC, managed node access│
│ ┌─────────────────────────────────────┐ │
│ │ CLUSTER  API server, RBAC, etcd,    │ │
│ │          admission, NetworkPolicy   │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ CONTAINER  image, runtime user, │ │ │
│ │ │            capabilities         │ │ │
│ │ │ ┌─────────────────────────────┐ │ │ │
│ │ │ │ CODE  deps, secrets, SAST   │ │ │ │
│ │ │ └─────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────┘ │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────┘

A perfect Pod Security policy does not help if the node's IAM role can read every S3 bucket in the account and any pod can reach the instance metadata service.


STEP 2: RBAC -- Where Most Real Compromises Start

Audit what exists

bash
# Who can do what, cluster-wide kubectl get clusterrolebindings -o wide kubectl get rolebindings --all-namespaces -o wide # Find bindings to the dangerous built-in role kubectl get clusterrolebindings -o json | \ jq -r '.items[] | select(.roleRef.name=="cluster-admin") | "\(.metadata.name): \(.subjects // [] | map(.kind + "/" + .name) | join(", "))"' # Can a specific ServiceAccount do something? kubectl auth can-i --list --as=system:serviceaccount:default:my-app kubectl auth can-i create pods --as=system:serviceaccount:default:my-app -n kube-system # Anything bound to system:anonymous or system:unauthenticated -- critical finding kubectl get clusterrolebindings -o json | \ jq '.items[] | select(.subjects[]?.name | test("anonymous|unauthenticated"))'

The privilege escalation paths that matter

These permissions are equivalent to cluster-admin even though they do not look like it:

PermissionWhy it is cluster-adminPath
create pods in any namespace with privileged SAsMount any ServiceAccount tokenCreate a pod using a privileged SA, read its token
create pods with hostPathRead the host filesystemMount / and read kubelet credentials or etcd
get/list secrets cluster-wideRead every ServiceAccount tokenUse the most privileged token found
create serviceaccounts/tokenMint tokens for any SADirect impersonation
escalate on rolesGrant yourself anythingBypasses the normal privilege-escalation guard
bind on rolesBind existing powerful roles to yourselfSame outcome
impersonateAct as any user or groupDirect
update on nodesAttach to node objectsWorkload scheduling control

When reviewing a Role, ask not "what does this allow" but "what is the most privileged thing reachable from this".

Least-privilege Role pattern

yaml
apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: namespace: payments name: payments-app rules: - apiGroups: [""] resources: ["configmaps"] resourceNames: ["payments-config"] # scope to the specific object verbs: ["get", "watch"]

Three rules for RBAC that hold up in practice:

  1. Namespace-scoped Role over ClusterRole unless genuinely cluster-wide
  2. Enumerate resourceNames where the object set is known
  3. Never grant * on verbs or resources -- write them out

Disable automatic token mounting

Every pod gets a ServiceAccount token mounted by default. Most applications never call the Kubernetes API.

yaml
apiVersion: v1 kind: ServiceAccount metadata: name: payments-app automountServiceAccountToken: false # default off --- apiVersion: apps/v1 kind: Deployment spec: template: spec: serviceAccountName: payments-app automountServiceAccountToken: false # explicit at pod level too

This single change removes the most common post-compromise escalation path.


STEP 3: Pod Security Standards

PodSecurityPolicy was removed in v1.25. Pod Security Admission is the built-in replacement, with three profiles.

privileged  -- unrestricted. Only for genuine infrastructure workloads.
baseline    -- blocks known privilege escalations. Minimal app changes.
restricted  -- hardened. Enforces non-root, seccomp, dropped capabilities.

Apply per namespace

yaml
apiVersion: v1 kind: Namespace metadata: name: payments labels: pod-security.kubernetes.io/enforce: restricted pod-security.kubernetes.io/enforce-version: v1.29 pod-security.kubernetes.io/audit: restricted pod-security.kubernetes.io/warn: restricted

Roll out with warn and audit first, read the violations, fix the workloads, then switch enforce on. Enforcing immediately on an existing namespace will break deployments.

bash
# What would break? Dry-run the label kubectl label --dry-run=server --overwrite ns payments \ pod-security.kubernetes.io/enforce=restricted

A pod that satisfies restricted

yaml
apiVersion: v1 kind: Pod metadata: name: app spec: automountServiceAccountToken: false securityContext: runAsNonRoot: true runAsUser: 10001 runAsGroup: 10001 fsGroup: 10001 seccompProfile: type: RuntimeDefault containers: - name: app image: registry.example.com/app@sha256:abc123... # digest, not tag securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] resources: limits: { cpu: "500m", memory: "512Mi" } requests: { cpu: "100m", memory: "128Mi" } volumeMounts: - name: tmp mountPath: /tmp volumes: - name: tmp emptyDir: {}

Note readOnlyRootFilesystem: true with an emptyDir mounted at /tmp. Most applications need somewhere writable; give them exactly that and nothing else.

Settings that are never acceptable in production

yaml
securityContext: privileged: true # full host access -- container escape by design hostNetwork: true # sees all host traffic, bypasses NetworkPolicy hostPID: true # can see and signal host processes hostIPC: true volumes: - hostPath: path: / # host filesystem access

If a workload genuinely requires these (some CNI, CSI, and monitoring agents do), isolate it in a dedicated namespace with privileged PSS, restrict who can deploy there, and document the exception.


STEP 4: NetworkPolicy

Kubernetes is flat by default. Every pod can reach every other pod in every namespace. Without NetworkPolicy, a compromised frontend can talk directly to the payments database.

Default deny -- the foundation

yaml
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny-all namespace: payments spec: podSelector: {} # all pods in the namespace policyTypes: ["Ingress", "Egress"]

Apply this to every namespace, then add explicit allows. Starting from deny is the only approach that scales.

Allow only what is needed

yaml
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: api-allows-frontend namespace: payments spec: podSelector: matchLabels: { app: payments-api } policyTypes: ["Ingress", "Egress"] ingress: - from: - namespaceSelector: matchLabels: { kubernetes.io/metadata.name: frontend } podSelector: matchLabels: { app: web } ports: - protocol: TCP port: 8080 egress: # DNS is required -- forgetting this breaks everything - to: - namespaceSelector: matchLabels: { kubernetes.io/metadata.name: kube-system } podSelector: matchLabels: { k8s-app: kube-dns } ports: - { protocol: UDP, port: 53 } - { protocol: TCP, port: 53 } - to: - podSelector: matchLabels: { app: payments-db } ports: - { protocol: TCP, port: 5432 }

The three NetworkPolicy traps

  1. Forgetting DNS egress. A default-deny egress policy blocks DNS, and every outbound connection fails with a name resolution error that looks nothing like a network policy problem.
  1. No CNI enforcement. NetworkPolicy objects are accepted by the API server even if the CNI plugin does not implement them. Flannel does not. Verify enforcement with a real connectivity test, never assume.
  1. namespaceSelector and podSelector in the same list item vs separate items. Combined in one item means AND (that pod in that namespace). As separate list items it means OR. Getting this wrong silently widens the policy.
bash
# Verify a policy actually blocks kubectl run test --rm -it --image=nicolaka/netshoot --restart=Never -n payments -- \ curl -m 5 http://payments-db:5432

Block the cloud metadata endpoint

yaml
egress: - to: - ipBlock: cidr: 0.0.0.0/0 except: - 169.254.169.254/32 # cloud instance metadata

Pods reaching the metadata service can often retrieve the node's IAM credentials. This is one of the most common cloud escalation paths from a compromised container.


STEP 5: Secrets

Kubernetes Secrets are base64, not encrypted

bash
kubectl get secret db-creds -o jsonpath='{.data.password}' | base64 -d

Anyone with get secrets reads them in plaintext. By default they are also stored unencrypted in etcd.

Enable encryption at rest

yaml
# EncryptionConfiguration on the API server apiVersion: apiserver.config.k8s.io/v1 kind: EncryptionConfiguration resources: - resources: ["secrets"] providers: - kms: # external KMS -- preferred name: cloud-kms endpoint: unix:///var/run/kmsplugin/socket.sock - identity: {} # fallback, must be last

After enabling, rewrite existing secrets so they are re-encrypted:

bash
kubectl get secrets --all-namespaces -o json | kubectl replace -f -

Better: do not store secrets in the cluster at all

External Secrets Operator  -- syncs from Vault / AWS Secrets Manager / Azure Key Vault
Secrets Store CSI Driver   -- mounts directly from the external store, never in etcd
Workload Identity          -- no stored credential at all; the pod assumes a cloud role

Workload Identity is the strongest option where the cloud supports it -- there is no secret to steal because authentication is based on the pod's verified identity.

Never do this

yaml
env: - name: DB_PASSWORD value: "hunter2" # in the manifest, in git, in every log

Scan for it:

bash
gitleaks detect --source . --verbose trufflehog filesystem ./manifests

STEP 6: Admission Control

Pod Security Admission covers pod-level hardening. For everything else -- required labels, allowed registries, resource limits, ingress hostname rules -- use a policy engine.

Kyverno (YAML-native, easier adoption)

yaml
apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: require-signed-images spec: validationFailureAction: Enforce rules: - name: check-registry match: any: - resources: kinds: ["Pod"] validate: message: "Images must come from the approved registry" pattern: spec: containers: - image: "registry.example.com/*"

OPA Gatekeeper (Rego, more expressive)

rego
package k8srequiredlabels violation[{"msg": msg}] { provided := {label | input.review.object.metadata.labels[label]} required := {label | label := input.parameters.labels[_]} missing := required - provided count(missing) > 0 msg := sprintf("Missing required labels: %v", [missing]) }

Choose Kyverno if the team writes YAML, Gatekeeper if you need complex logic or already use Rego elsewhere. Both support audit mode -- always start there.

Policies worth enforcing from day one

[ ] Images from approved registries only
[ ] Images referenced by digest, not mutable tags
[ ] Resource requests and limits required
[ ] No :latest tag
[ ] Required labels for ownership and cost allocation
[ ] No privileged containers outside the exception namespace
[ ] ReadOnlyRootFilesystem required
[ ] Ingress hostnames must match an owned domain

STEP 7: Supply Chain

bash
# Scan images before they reach the cluster trivy image --severity HIGH,CRITICAL registry.example.com/app:1.2.3 grype registry.example.com/app:1.2.3 # Generate and store an SBOM syft registry.example.com/app:1.2.3 -o spdx-json > sbom.json # Sign the image cosign sign --key cosign.key registry.example.com/app@sha256:abc123 # Verify at admission time cosign verify --key cosign.pub registry.example.com/app@sha256:abc123

Enforce signature verification in the cluster

yaml
apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: verify-image-signature spec: validationFailureAction: Enforce rules: - name: verify-signature match: any: - resources: kinds: ["Pod"] verifyImages: - imageReferences: ["registry.example.com/*"] attestors: - entries: - keys: publicKeys: |- -----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----

Build hardening

dockerfile
FROM golang:1.22 AS build WORKDIR /src COPY . . RUN CGO_ENABLED=0 go build -o /app ./cmd/app FROM gcr.io/distroless/static-debian12:nonroot COPY --from=build /app /app USER nonroot:nonroot ENTRYPOINT ["/app"]

Distroless or scratch base images remove the shell, package manager, and most of the attack surface. An attacker who achieves RCE in a distroless container has no sh, no curl, and no apt.


STEP 8: Runtime Detection

Prevention fails eventually. You need to see it when it does.

yaml
# Falco rule -- shell spawned in a container - rule: Terminal shell in container desc: A shell was spawned in a container condition: > spawned_process and container and shell_procs and proc.tty != 0 output: > Shell spawned (user=%user.name container=%container.name image=%container.image.repository cmd=%proc.cmdline) priority: WARNING

Signals worth alerting on

[ ] Shell spawned inside a container
[ ] Process writing to a known binary directory
[ ] Outbound connection to an unexpected destination
[ ] ServiceAccount token read by a process that is not the application
[ ] Pod created with privileged or hostPath
[ ] RBAC binding to cluster-admin created
[ ] Secret accessed by an unusual identity
[ ] Container escape indicators -- nsenter, docker.sock access
[ ] Crypto mining process signatures
[ ] Access to 169.254.169.254 from a pod

Enable and use the audit log

yaml
# Audit policy -- log the things that matter apiVersion: audit.k8s.io/v1 kind: Policy rules: - level: RequestResponse resources: - group: "" resources: ["secrets", "serviceaccounts/token"] - level: RequestResponse resources: - group: "rbac.authorization.k8s.io" resources: ["clusterrolebindings", "rolebindings"] - level: Metadata omitStages: ["RequestReceived"]

Ship these to the SIEM. Without the audit log you cannot answer "who created that binding" after an incident.


STEP 9: Common Issues -- Symptom, Cause, Fix

Issue 1: Compromised pod reached the cloud account

  • Cause: pod reached 169.254.169.254 and retrieved node IAM credentials
  • Fix: block metadata in NetworkPolicy egress; move to Workload Identity / IRSA so the node role is minimal

Issue 2: NetworkPolicy applied, traffic still flows

  • Cause: CNI does not enforce NetworkPolicy (Flannel) or the policy selector does not match
  • Verify: deploy a test pod and curl; check CNI documentation
  • Fix: migrate to Calico, Cilium, or the cloud provider's CNI with enforcement

Issue 3: Everything broke after default-deny egress

  • Cause: DNS egress to kube-dns not permitted
  • Fix: add the DNS egress rule to every default-deny policy

Issue 4: Deployment rejected after enabling restricted PSS

  • Cause: workload runs as root, lacks seccomp, or does not drop capabilities
  • Fix: fix the workload; use warn mode first to find all violations before enforcing

Issue 5: ServiceAccount token found in a compromised container

  • Cause: automountServiceAccountToken left at the default
  • Fix: set it false on every ServiceAccount and pod that does not call the API

Issue 6: A developer escalated to cluster-admin

  • Cause: they had create pods in a namespace containing a privileged ServiceAccount
  • Fix: audit which SAs exist per namespace; separate privileged workloads into a locked-down namespace

Issue 7: Secrets visible in the audit log

  • Cause: audit policy at RequestResponse for secrets without care, logs shipped broadly
  • Fix: keep the level but restrict access to the audit log itself; it is a crown-jewel data source

Issue 8: Image with a critical CVE in production

  • Cause: no admission-time scanning gate, or :latest tag pulled a new vulnerable build
  • Fix: pin by digest, scan in CI, enforce signature and scan status at admission

Issue 9: etcd backup stolen, all secrets exposed

  • Cause: encryption at rest not enabled
  • Fix: enable KMS encryption, rewrite all secrets, rotate everything that was in the backup

Issue 10: Cannot tell what happened during an incident

  • Cause: audit logging disabled or not shipped off-cluster
  • Fix: enable the audit policy above, ship to the SIEM, retain per the IR policy

Quick Checklists

Cluster hardening baseline

[ ] 1. API server not exposed to the internet, or restricted by IP allowlist
[ ] 2. Anonymous auth disabled; no bindings to system:unauthenticated
[ ] 3. RBAC audited -- no unnecessary cluster-admin bindings
[ ] 4. automountServiceAccountToken false by default
[ ] 5. Pod Security Admission enforcing baseline minimum, restricted where possible
[ ] 6. Default-deny NetworkPolicy in every namespace
[ ] 7. Metadata endpoint blocked in egress policy
[ ] 8. Secrets encrypted at rest with KMS
[ ] 9. Admission policy engine deployed and enforcing
[ ] 10. Audit logging enabled and shipped off-cluster
[ ] 11. Runtime detection deployed (Falco or equivalent)
[ ] 12. Node OS patched; managed node groups auto-upgrading

Pre-deployment workload review

[ ] 1. runAsNonRoot true with an explicit UID
[ ] 2. allowPrivilegeEscalation false
[ ] 3. capabilities drop ALL
[ ] 4. readOnlyRootFilesystem true
[ ] 5. seccompProfile RuntimeDefault
[ ] 6. Resource requests and limits set
[ ] 7. Image by digest from an approved registry, signature verified
[ ] 8. Image scanned, no unaccepted HIGH/CRITICAL findings
[ ] 9. Dedicated ServiceAccount with a minimal Role
[ ] 10. NetworkPolicy defining exactly the required ingress and egress
[ ] 11. No secrets in env values; mounted from an external store
[ ] 12. No hostPath, hostNetwork, hostPID, or privileged

Incident triage in a cluster

[ ] 1. Isolate: apply a deny-all NetworkPolicy to the affected pod
[ ] 2. Preserve: snapshot the pod and node before deleting anything
[ ] 3. Scope: which ServiceAccount did it have, what could that token do?
[ ] 4. Audit log: what API calls came from that identity?
[ ] 5. Lateral: did it reach the metadata endpoint or other namespaces?
[ ] 6. Rotate: every credential reachable from the compromised identity
[ ] 7. Root cause: image vulnerability, exposed service, or supply chain?
[ ] 8. Fix the class of issue, not just the instance

PermitANY Resources

Automation Scripts (https://permitany.com/scripts)

  • /scripts/container-security-scanner -- Image and container configuration scanning
  • /scripts/cloud-security-auditor -- Cloud posture assessment around the cluster
  • /scripts/secrets-scanner -- Detect secrets in manifests and code
  • /scripts/host-vulnerability-scanner -- Node-level vulnerability assessment

AI Prompts (https://permitany.com/prompts)

  • /prompts/vulnerability-prioritization -- Triage image and cluster CVEs by real risk
  • /prompts/zero-trust-design -- Apply Zero Trust principles to the cluster
  • /prompts/incident-response-guide -- NIST-aligned IR for a container compromise

Working Style

When a user asks about Kubernetes security:

  1. Establish the environment -- managed (EKS/GKE/AKS) or self-managed changes who owns the control plane
  2. Ask what is already in place before recommending; most clusters have some controls
  3. Prioritise by real-world attack path, not by checklist length. The order that matters: RBAC and token mounting, NetworkPolicy default-deny, metadata blocking, Pod Security, then supply chain
  4. Give working YAML, not descriptions -- and include the parts people forget, like DNS egress
  5. Always recommend audit or warn mode before enforcement
  6. For any finding, explain the escalation path it enables. "This SA can list secrets" means little; "this SA can list secrets, so a compromise of this pod yields every token in the cluster" lands

The question that finds the real risk fastest: "if an attacker gets RCE in your least important pod, what can they reach?" Walk that path concretely -- token, network, metadata, cloud -- and the priorities become obvious.

Further Learning

Recommended books to go deeper on this topic.

Container Security

Liz Rice · O'Reilly · The Linux constructs containers are built on, how a misconfiguration breaks isolation, and image and runtime hardening

Kubernetes Best Practices, Second Edition

Brendan Burns, Eddie Villalba, Dave Strebel, Lachlan Evenson · O'Reilly · Blueprints for requests and limits, probes, rollouts, storage and multi-tenancy, from the people who built much of it

Practical Cloud Security: A Guide for Secure Design and Deployment

Chris Dotson · O'Reilly Media · Vendor-neutral cloud security fundamentals - IAM, network segmentation, encryption, and logging across AWS, Azure, and GCP

Tags

kubernetesk8scontainerrbacnetwork-policypod-securityopakyvernosupply-chaincloud-native