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.
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. |
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) |
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.
{
"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\"]}"
}{
"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\"]}"
}{
"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\"]}"
}{
"clusters": "[{\"name\":\"local-dev\",\"provider\":\"kubeconfig\"}]",
"resources": "[\"pods\",\"services\",\"deployments\"]",
"namespace_include": "[\"default\",\"app\"]"
}{
"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\"]}"
}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.
| 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 |
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.