Skip to content

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:

{
  "kubeconfig_ref": "env://KUBECONFIG_BASE64"
}

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 IDs
  • soc2, nist_800_53, pci_dss and iso27001

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"]
}