Kubernetes In-Cluster Scanner¶
The Kubernetes In-Cluster Scanner (kubernetes-inspector) is a first-class VirtualSutra agent that audits live Kubernetes clusters for security misconfigurations using the Kubernetes API - no cloud credentials required. It is dispatched directly by the orchestrator, one task per K8SCloudConfig in the inspection scope, running in parallel with cloud and codebase scanner tasks.
Availability: Pro and Enterprise ยท Tenant-scoped
See the Tier & Scope Matrix for how this is enforced today.
Image: ghcr.io/virtual-sutra/kubernetes-inspector Redis stream: vsutra:tasks:kubernetes_inspector AgentType: KUBERNETES_INSPECTOR
Capabilities¶
| Check group | Checks | CIS K8s Benchmark |
|---|---|---|
| Pod checks | Privileged containers | 5.2.1 |
| Missing resource limits (CPU/memory) | 5.2.4 | |
:latest image tags | 5.2.8 | |
| Secrets in plain-text environment variables | 5.4.1 | |
| RBAC checks | ClusterAdmin bindings | 5.1.1 |
| Default service account token mounts | 5.1.6 | |
| Network checks | Missing NetworkPolicies per namespace | 5.3.2 |
| PSS checks | Pod Security Standards baseline violations | 5.2.* |
Compliance mappings are emitted for cis_kubernetes and nist_csf (NSA Kubernetes Hardening Guide).
Prerequisites¶
The scanner connects to the Kubernetes API server. No cloud provider credentials are needed.
- In-cluster: the agent pod's ServiceAccount must be able to list what a scan reads. The Helm chart grants that (see In-cluster).
- External: a kubeconfig with read access to the target cluster(s). The scan only lists: every read, and what grants it, are in Access a scan needs.
Configuration¶
Kubernetes clusters to scan are declared in InspectionConfig.scope.kubernetes_clusters as a list of K8SCloudConfig objects.
{
"scope": {
"kubernetes_clusters": [
{
"cluster_name": "prod-us-east-1",
"kubeconfig_ref": "env://KUBECONFIG_BASE64",
"context": "arn:aws:eks:us-east-1:123456789012:cluster/prod",
"namespaces": [],
"skip_namespaces": ["kube-system", "kube-public"],
"enable_pod_checks": true,
"enable_rbac_checks": true,
"enable_network_checks": true,
"enable_pss_checks": true
}
]
}
}
K8SCloudConfig fields¶
| Field | Type | Default | Description |
|---|---|---|---|
cluster_name | string | required | Friendly label used in resource_id and findings |
kubeconfig_ref | string | null | Secret ref that resolves to a kubeconfig, raw or base64-encoded (e.g. env://KUBECONFIG_BASE64, vault://secret/data/k8s#kubeconfig). A value with no scheme is taken as the kubeconfig itself. Leave null for in-cluster ServiceAccount. |
context | string | null | kubeconfig context name. Uses current-context when null. |
gcp_sa_key_ref | string | null | GCP service account key reference for GKE exec-based auth bypass (e.g. env://GCP_SA_KEY). Required when the kubeconfig uses gke-gcloud-auth-plugin exec auth without the gcloud SDK present. |
namespaces | list[string] | [] | Namespaces to scan. Empty list = all namespaces minus skip_namespaces. |
skip_namespaces | list[string] | [] | Namespaces to exclude from scanning. |
enable_pod_checks | bool | true | Run pod security checks |
enable_rbac_checks | bool | true | Run RBAC checks |
enable_network_checks | bool | true | Run NetworkPolicy checks |
enable_pss_checks | bool | true | Run Pod Security Standards checks |
Authentication¶
In-cluster (default)¶
Leave kubeconfig_ref unset. The agent uses the pod's mounted ServiceAccount token automatically.
The Helm chart creates a ClusterRole and ClusterRoleBinding named after the release (<release>-kubernetes-inspector) and binds it to the agent's ServiceAccount, unless rbac.create is false. It is broader than a scan needs: get and list (no watch) on pods, namespaces, serviceaccounts, services, configmaps, clusterroles, clusterrolebindings, roles, rolebindings, networkpolicies, podsecuritypolicies, jobs, cronjobs, deployments, replicasets, statefulsets and daemonsets. A scan only uses the list reads in Access a scan needs. On the SaaS profile the chart creates no ClusterRole and unmounts the token, because a hosted inspector scans tenants' clusters through their kubeconfigs and refuses the in-cluster fallback.
Where the chart does not create the role, bind the read-only vsutra-scanner-read ClusterRole shown under Access a scan needs, with a ServiceAccount subject for the agent's ServiceAccount, which grants exactly those reads.
External clusters¶
Set kubeconfig_ref to a secret reference that resolves to the kubeconfig. The value may be the kubeconfig itself (YAML) or base64 of it:
# The kubeconfig as it is ...
export KUBECONFIG_RAW="$(kubectl config view --minify --flatten)"
# ... or base64 of it, for a secret store (SECRETS_BACKEND=env)
base64 -w 0 ~/.kube/config > kubeconfig.b64
export KUBECONFIG_BASE64="$(cat kubeconfig.b64)"
Then reference it in inspection.json:
The exact rule, and what is refused, is in Kubernetes scanning. In short: a value that is a kubeconfig is used as given; otherwise it is base64-decoded once and must then be one; anything else is refused, naming kubeconfig_ref and never the value, with no fall back to in-cluster credentials.
A saved connection's credential is sent to the scanner raw; a secret you keep under env:// or vault:// conventionally holds base64. Both work.
The client is built from a new kubeconfig that holds only the allowed fields of the selected context. A kubeconfig that would run a command, read a file in the scanner's pod, use the pod's own cloud identity or route through a proxy is refused, naming the field. Allowed: token and/or client-certificate-data with client-key-data for the user; server (an https:// URL), certificate-authority-data, insecure-skip-tls-verify and tls-server-name for the cluster. The one exec block accepted is gke-gcloud-auth-plugin, which is never run (see below).
GKE clusters¶
A kubeconfig whose user is the gke-gcloud-auth-plugin exec block needs gcp_sa_key_ref: the scanner never runs the plugin, and exchanges the service-account key for an access token itself. The key's token_uri must be Google's. A GKE cluster saved from a GCP discovery is reached the same way with its cloud account's key, whose service account needs roles/container.viewer on the cluster's project.
Findings Schema¶
resource_type values¶
resource_type | Used for |
|---|---|
k8s:deployment, k8s:statefulset, k8s:daemonset, k8s:cronjob, k8s:job | Pod-based checks (privileged, resource limits, image tags, env secrets, default service account, Pod Security Standards) on a workload of that kind |
k8s:replicaset | The same, for a ReplicaSet nothing controls, or one whose owner could not be read (pods-only access) |
k8s:workload | The same, for any other owner (a ReplicationController, an Argo Rollout, a custom resource); its kind is in the workload_kind tag |
k8s:pod | The same, for a pod nothing controls and a static pod |
k8s:namespace | Namespace-level checks (missing NetworkPolicy) |
k8s:clusterrolebinding | RBAC checks (ClusterAdmin bindings) |
resource_id format¶
{cluster_name}/{namespace}/{kind}/{name} a workload (kind in lower case: deployment, cronjob, ...)
{cluster_name}/{namespace}/{pod_name} a pod nothing controls, a static pod
{cluster_name}/{namespace} a namespace-level finding
{cluster_name}/{binding_name} a cluster-scoped finding
Examples:
prod-us-east-1/default/deployment/web(one finding for all of the Deployment's pods)prod-us-east-1/default/my-pod(a bare pod)prod-us-east-1/default(namespace-level finding)prod-us-east-1/cluster-admin-binding(cluster-scoped)
Every segment is escaped (% as %25, then / as %2F), so a cluster name with a / (an EKS ARN) stays one segment. Tell the forms apart by the number of segments, never by a segment's value.
Workloads with no running pod¶
A workload with no running pod is assessed from its pod template, under the same resource id, with the tag assessed_from: template. See Workloads with no running pod.
Compliance controls¶
Each finding states its controls in compliance_controls, per check, for the frameworks where a control exists for what the check reads:
cis_kubernetes- CIS Kubernetes Benchmark recommendation numbers (the resource-limits and image-tag checks have no CIS recommendation and state none)nist_csf- NIST Cybersecurity Framework 2.0 subcategory IDssoc2,nist_800_53,pci_dssandiso27001
Environment Variables¶
| Variable | Default | Description |
|---|---|---|
REDIS_URL | required | Redis connection URL |
LOG_LEVEL | INFO | Logging level |
K8S_API_TIMEOUT_SECONDS | 30 | How long a request to the cluster's API server waits for an answer. The connection itself gives up after 10 seconds, and a request that is reset or unanswered is tried once more |
PUSHGATEWAY_URL | - | Prometheus Pushgateway for metrics |
Example inspection.json¶
{
"run_name": "prod-k8s-audit",
"scope": {
"kubernetes_clusters": [
{
"cluster_name": "prod-us-east-1",
"kubeconfig_ref": "env://KUBECONFIG_BASE64",
"skip_namespaces": ["kube-system", "kube-public", "monitoring"],
"enable_pod_checks": true,
"enable_rbac_checks": true,
"enable_network_checks": true,
"enable_pss_checks": true
},
{
"cluster_name": "staging-eu-west-1",
"kubeconfig_ref": "vault://secret/data/k8s/staging#kubeconfig",
"namespaces": ["default", "app", "data"],
"enable_pod_checks": true,
"enable_rbac_checks": true,
"enable_network_checks": false,
"enable_pss_checks": true
}
]
},
"compliance_frameworks": ["cis_kubernetes", "nist_csf"]
}