k8s-security — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited k8s-security (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
This skill covers the cluster and workload layer on top of container-hardening. Image content and build live there; what K8s does with those images (scheduling, RBAC, networking, secrets, runtime policy) lives here.
Activates on:
Deployment, StatefulSet, DaemonSet, Job, ServiceAccount, Role(Binding), ClusterRole(Binding), NetworkPolicy, ValidatingAdmissionPolicy, Helm charts, Kustomize overlays.security-review when K8s is in scope.ir-runbook for response).container-hardening. This skill takes the image as given.iac-security. Manifest level here, infrastructure level there.cicd-hardening. GitOps controllers (Argo CD, Flux) we mention only as context here.secrets-scanner. External Secrets Operator bridges both.cve-triage.recon-agent + web-exploit-triage.Six phases. Phases 1–3 form the cluster baseline, phase 4 bridges to external secrets, phase 5 covers runtime.
The three controls that should be in place from day one in a new cluster.
RBAC discipline:
automountServiceAccountToken: false on the namespace-default service account and on every Pod that doesn't need API access. The default SA mounted into every pod is the most-abused attack trampoline.verbs: ["*"], resources: ["*"]) are red flags. Split read and write, scope to specific resources, prefer Role (namespace-scoped) over ClusterRole where possible.cluster-admin ClusterRole is for human operators only, and even then preferably via just-in-time escalation.system:anonymous, no escalation to cluster-admin.kubectl auth can-i --list --as=system:serviceaccount:<ns>:<sa>. Or tools like rbac-lookup, krane, permission-manager.Pod Security Standards (PSS) have replaced PodSecurityPolicy since Kubernetes 1.25. Three levels, enforced per namespace via labels:
Labels per namespace:
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: restrictedenforce blocks, audit logs, warn warns at kubectl apply. Start with warn + audit, migrate to enforce once findings are cleaned up.
Admission controllers for rules PSS doesn't cover:
readOnlyRootFilesystem: true). Default choice for teams without Rego experience.iac-security phase 4. Default if you already use OPA.One of these is enough. All three together is maintenance burden without added value.
This is the bridge between container-hardening (what's in the image) and what K8s actually does with it. Every Deployment/StatefulSet/Job needs this block:
spec:
template:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: <registry>/<image>@sha256:<digest>
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
# only add what's strictly needed:
# add: ["NET_BIND_SERVICE"]
resources:
limits:
cpu: "500m"
memory: "512Mi"
requests:
cpu: "100m"
memory: "128Mi"Per field, briefly:
emptyDir or persistent-volume mounts; the rest is read-only. Malware persistence inside the container becomes harder.Always only with mutable tags (which you don't want in production).Without NetworkPolicy, intra-cluster traffic is wide open. Install default-deny, then allow per Pod/namespace what's needed.
Default-deny per namespace:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: payments
spec:
podSelector: {}
policyTypes: [Ingress, Egress]Then explicit allow policies:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-from-web
namespace: payments
spec:
podSelector:
matchLabels: { app: payment-api }
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels: { role: web }
podSelector:
matchLabels: { app: web-frontend }
ports:
- protocol: TCP
port: 8080Egress to the internet: an allowlist of DNS-resolvable hostnames is not possible with vanilla NetworkPolicy (which works on pod/namespace selectors and IP CIDRs). For FQDN egress use a CNI with FQDN support (Cilium, Calico Enterprise) or an egress proxy (Istio egress gateway, Squid).
Cilium and Calico also offer ClusterwideNetworkPolicy for policies that hover above namespaces — useful for baseline defaults.
kind: Secret is base64, not encryption. Anyone with RBAC access to secrets.get in the namespace reads everything. Encrypted-at-rest in etcd is possible if you set --encryption-provider-config on the kube-apiserver — but that's opt-in and often skipped.
Three approaches, increasing in maturity:
For all three: secret rotation remains an external responsibility (see secrets-scanner phase 4). K8s ESO only handles sync.
IRSA (IAM Roles for Service Accounts) on EKS, Workload Identity on GKE, Managed Identity on AKS: bind the SA directly to a cloud IAM role, no static API key needed. Default recommendation for cloud workloads.
What's actually happening in the cluster, and do you notice it in time?
--audit-policy-file on kube-apiserver. At minimum: every RBAC change, every secret read by non-system accounts, every exec/attach action, every escalate/bind verb. Stream to a SIEM (see siem-query and log-triage).cat /etc/shadow, chmod 777 in prod namespaces, network connections to known C2. Default for runtime threat detection in open source.AuthorizationPolicy or Linkerd policy for mTLS enforcement between services. Doesn't replace NetworkPolicy; complements it at L7.Alerts from Falco etc. go to detection-engineer for rule tuning and ir-runbook for response.
Layer 1: scope (every namespace has a PSS label? every Pod has a securityContext? every namespace has a default-deny NetworkPolicy?), assumptions ("we use Workload Identity" only when you've actually seen the SA annotations), gaps (audit log enabled, Falco alerts routed, runtime monitoring also covers kube-system), consistency (PSS level matches actual securityContext settings).
Layer 2: K8s API versions and field names correct (PSS syntax changed at 1.25, VAP is 1.30+), CVE references for K8s components verified against NVD, no fabricated Kyverno policy snippets that don't run against real CRDs.
K8s security review — <cluster/namespace/app>
Scope: <manifests, Helm charts, namespaces>
Cluster baseline:
RBAC: <audit findings, wildcards, over-permissive>
PSS per namespace:<table: namespace → enforce/audit/warn>
Admission: <Kyverno | Gatekeeper | VAP | none>
Workload hardening:
Pods without securityContext: <N>
runAsNonRoot false: <N>
readOnlyRootFilesystem false: <N>
capabilities not dropped: <N>
No resource limits: <N>
Network:
Namespaces without default-deny: <list>
Egress policies present: <yes/no per namespace>
FQDN egress via: <CNI | proxy | none>
Secrets:
Native K8s Secrets in use: <N, source-of-truth or ESO-synced?>
Encryption at rest etcd: <on/off>
Workload Identity: <yes/no per service>
Runtime:
Audit log on: <yes/no>
Falco/Tetragon: <deployed, alerts routed>
Service mesh: <Istio/Linkerd/none, mTLS scope>
Findings (severity-sorted)
Verification-loop: ...Per finding: location (manifest or resource), CIS-benchmark ID where relevant, CWE-ID where applicable, severity, fix (YAML snippet rather than prose).
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.