Kubernetes
A Continuous Compliance Framework plugin that collects Kubernetes resources and evaluates OPA/Rego policies against them.
Supports multiple authentication methods (EKS via AWS STS, kubeconfig for any cluster) and concurrent multi-cluster collection.
Breaking change in this release. The plugin now implements the
RunnerV2interface and evaluates policies per individual resource rather than per cluster. The Rego input schema has moved fromv1(input.clusters[...]) tov2(input.main+input.context). Existing policies that readinput.clusters[...]must be rewritten. Companion policy repos (plugin-k8s-policies,plugin-k8s-opres-policies) will be updated in a coordinated follow-up.
This page is for operators who run the plugin-k8s plugin with the CCF Agent, and for developers who write policies for it. It documents v0.2.1 and is generated from the plugin's README at v0.2.1.
Configuration
Plugin Configuration
The plugin receives configuration as flat string fields from the CCF agent. All structured values are JSON-encoded strings.
| Field | Required | Description |
|---|---|---|
clusters | Yes | JSON array of cluster connection configs |
resources | Yes | JSON array of Kubernetes resource types to collect (e.g. "nodes", "pods", "deployments") |
main_resources | No | JSON array — subset of resources that produces per-instance subjects and evidence. Defaults to all of resources. Types in resources but not in main_resources are collected as policy context only. |
identity_labels | No | JSON object mapping identity-label key → ordered list of metadata.labels keys to try. Default: {"app_name": ["app.kubernetes.io/name", "app"]}. First match wins; falls back to metadata.name when none are present. |
namespace_include | No | JSON array of namespaces to include (empty = all) |
namespace_exclude | No | JSON array of namespaces to exclude |
policy_labels | No | JSON object of key-value labels added to evidence metadata |
policy_input | No | JSON object of custom fields merged into the Rego input document. Reserved keys: schema_version, source, main, context, subject, fleet. |
Cluster Configuration
Each entry in clusters has:
| Field | Required | Provider | Description |
|---|---|---|---|
name | Yes | all | Unique display name for this cluster |
provider | No | all | "eks" (default) or "kubeconfig" |
region | Yes | eks | AWS region |
cluster_name | Yes | eks | EKS cluster name |
role_arn | No | eks | IAM role ARN to assume before authenticating |
kubeconfig | No | kubeconfig | Path to kubeconfig file (default: ~/.kube/config) |
context | No | kubeconfig | Kubeconfig context to use (default: current-context) |
Configuration Examples
Single EKS cluster — evidence per pod, nodes as context
{
"clusters": "[{\"name\":\"prod\",\"region\":\"us-east-1\",\"cluster_name\":\"prod-eks\"}]",
"resources": "[\"nodes\",\"pods\"]",
"main_resources": "[\"pods\"]",
"namespace_exclude": "[\"kube-system\",\"kube-public\"]",
"policy_labels": "{\"team\":\"platform\",\"environment\":\"production\"}",
"policy_input": "{\"expected_azs\":[\"us-east-1a\",\"us-east-1b\",\"us-east-1c\"]}"
}
Custom identity label
{
"clusters": "[{\"name\":\"prod\",\"region\":\"us-east-1\",\"cluster_name\":\"prod-eks\"}]",
"resources": "[\"pods\"]",
"identity_labels": "{\"app_name\":[\"app.company.io/service\",\"app.kubernetes.io/name\"],\"team\":[\"team.company.io/owner\"]}"
}
EKS with cross-account role assumption
{
"clusters": "[{\"name\":\"prod\",\"region\":\"us-east-1\",\"cluster_name\":\"prod-eks\",\"role_arn\":\"arn:aws:iam::123456789012:role/eks-readonly\"}]",
"resources": "[\"nodes\",\"pods\"]",
"policy_input": "{\"expected_azs\":[\"us-east-1a\",\"us-east-1b\"]}"
}
Local cluster via kubeconfig (kind, minikube, k3s)
{
"clusters": "[{\"name\":\"local-dev\",\"provider\":\"kubeconfig\"}]",
"resources": "[\"pods\",\"services\",\"deployments\"]",
"namespace_include": "[\"default\",\"app\"]"
}
Multi-cluster (mixed providers)
{
"clusters": "[{\"name\":\"prod-east\",\"region\":\"us-east-1\",\"cluster_name\":\"prod-east-eks\"},{\"name\":\"prod-west\",\"region\":\"us-west-2\",\"cluster_name\":\"prod-west-eks\"},{\"name\":\"dev\",\"provider\":\"kubeconfig\",\"context\":\"kind-dev\"}]",
"resources": "[\"nodes\",\"pods\"]",
"main_resources": "[\"pods\"]",
"namespace_exclude": "[\"kube-system\"]",
"policy_input": "{\"expected_azs\":[\"us-east-1a\",\"us-east-1b\",\"us-west-2a\",\"us-west-2b\"]}"
}
Evidence collected
Subjects and evidence model
During Init, the plugin registers one SubjectTemplate per entry in main_resources (e.g. k8s-pods, k8s-nodes). Each template has the identity label keys:
cluster_namenamespace(only for namespaced resources)app_name(resolved frommetadata.labelsviaidentity_labels; falls back tometadata.name)name
For cluster-scoped resources such as nodes, the emitted evidence labels still include namespace with an empty string so label contracts stay stable, but the corresponding SubjectTemplate omits namespace from its identity keys and rendering.
During Eval, every concrete Kubernetes resource instance that matches a main_resources type becomes its own subject and receives its own evidence for every configured policy path. Policies are invoked once per (resource instance, policy path) pair. Evidence is batched per cluster, but may be sent via multiple CreateEvidence calls when the evidenceBatchSize threshold is reached.
Policies
The policy bundles that work with this plugin, at their newest releases. The controls are the ones the bundle's policies list in their metadata.
| Bundle | Latest version | Controls covered | Compatible plugin lines |
|---|---|---|---|
plugin-k8s-opres-policies | v0.2.0 | none listed | latest |
Policy Input Schema (v2)
Every policy evaluation receives one main resource and the full cluster snapshot as context.
{
"schema_version": "v2",
"source": "plugin-kubernetes",
"main": { /* the single Kubernetes resource being evaluated (unstructured) */ },
"subject": {
/* stable per-resource identity metadata derived for evidence and policy use */
},
"context": {
"cluster": { "name": "prod", "region": "us-east-1", "provider": "eks" },
"resources": {
"nodes": [ /* every collected node in this cluster */ ],
"pods": [ /* every collected pod in this cluster — includes the one in input.main */ ]
}
},
"fleet": {
/* multi-cluster collection metadata always included in the evaluation input */
}
}
Any fields from policy_input config are merged at the top level. Reserved keys (schema_version, source, main, context, subject, fleet) cannot be overridden.
Field reference
| Path | Type | Source | Description |
|---|---|---|---|
input.schema_version | string | Plugin | Always "v2" |
input.source | string | Plugin | Always "plugin-kubernetes" |
input.main | object | Plugin | The full unstructured Kubernetes resource currently being evaluated |
input.subject | object | Plugin | Normalized subject identity for the resource under evaluation (cluster_name, resource_type, name, identifier, identity_labels, and namespace for namespaced resources) |
input.context.cluster | object | Plugin | {name, region, provider} for the cluster this resource belongs to |
input.context.resources | object | Plugin | Map of resource type → array of Kubernetes objects (full cluster snapshot, including the main resource) |
input.fleet | object | Plugin | Multi-cluster snapshot keyed by cluster name, each with {cluster, resources} |
input.<custom_key> | any | policy_input | User-defined fields |
Policy data
Any policy_data set on this plugin's block in the agent configuration is passed
to every policy evaluation and is available to Rego as data.<key> (unlike
policy_input, which is merged into input):
plugins:
kubernetes:
# ...source, policies and config as usual...
policy_data:
allowed_registries:
- ghcr.io/compliance-framework
Example: writing a policy
package compliance_framework.pod_has_node_binding
import rego.v1
violation contains {"remarks": msg} if {
input.main.kind == "Pod"
not input.main.spec.nodeName
msg := sprintf("Pod %q has no nodeName assigned", [input.main.metadata.name])
}
# Cross-reference sibling resources via input.context
violation contains {"remarks": msg} if {
input.main.kind == "Pod"
node_name := input.main.spec.nodeName
not node_exists(node_name)
msg := sprintf("Pod %q binds to unknown node %q", [input.main.metadata.name, node_name])
}
node_exists(name) if {
some n in input.context.resources.nodes
n.metadata.name == name
}
title := "Pod node binding"
description := "Every pod must bind to a known node in its cluster."
Key patterns:
input.mainis always the single resource under evaluation.input.context.resources.<type>is the full cluster snapshot; iterate withsome r in ....input.context.clustergives the cluster name/region/provider for labels and messaging.- Filter out the main resource from peer checks with
r.metadata.uid != input.main.metadata.uid.