Skip to main content

Kubernetes Posture

Self-Hosted Kubernetes

AIControls does not evaluate Kubernetes posture itself. It detects whether Kyverno is installed, reads the PolicyReport and ClusterPolicyReport verdicts Kyverno already maintains, and groups them by policy category. You choose which policies count. Where the engine or its policies are missing, or report access cannot be established, posture is Unknown — never passing — and the API explains missing coverage and unreadable evidence.

Self-managed installations only

Workload Trust is available only in private, self-managed AIControls installations, on premises or in a cloud cluster that you run. It needs a dedicated cluster that you run, with Kyverno, Kyverno Runtime, and cluster OIDC and RBAC access in place. Nirmata-hosted (SaaS) workspaces don't include it. See Feature availability.

note

Kubernetes posture is available on self-hosted Kubernetes only, as part of Runtime Controls, and is on by default there (runtimeControls.posture.enabled defaults to true). The endpoints are served on Nirmata-hosted tenants too, but answer with enabled: false and a reason.

Why a reader, not an evaluator​

The first question a security reviewer asks about an untrusted workload is: if this were compromised, what could it reach? The answer lives in Kubernetes posture — RBAC grants, NetworkPolicy exposure, pod security configuration, hardening-benchmark compliance.

Kyverno evaluates all of that already: admission checks on every write, background scans on every existing resource, verdicts published as policy reports on the cluster itself. Building a second evaluator inside AIControls would replicate those policies, drift from what the cluster's engine enforces, and demand broad read access to workloads, RBAC objects, and Secrets that a report reader never needs. AIControls consumes those verdicts and retains their source evidence. Report collection and policy-to-standard mapping determine what appears in the posture rollup; missing access stays Unknown.

What it reads​

The posture reader has its own ClusterRole, separate from the runtime sensor's, granting list/watch on reports and list on policies:

API groupResourcesPurpose
wgpolicyk8s.iopolicyreports, clusterpolicyreportsThe verdicts (the report API stock Kyverno writes by default)
kyverno.iopolicies, clusterpoliciesThe installed-policy inventory and its library annotations
policies.kyverno.iothe CEL policy kinds (validatingpolicies and siblings)The same inventory for Kyverno 1.15+ policy types

openreports.io reports (Kyverno's opt-in successor report API) and the apps read the version check uses come from the Runtime Controls ClusterRole, which renders alongside. The posture-specific ClusterRole grants no Secret, workload, or RBAC-object access. The separate Runtime Controls role already includes workload reads used for runtime integration and attribution; the report reader does not add those grants. Secrets posture comes from Kyverno policies that evaluate Secret usage; AIControls sees only their pass or fail verdicts.

At startup and whenever the posture reader is created or restarted, including a lease handoff, SelfSubjectAccessReview checks the reader’s required operations: list and watch on the served report resources used by posture, and list on served policy kinds and Deployments. It does not check every verb in the ClusterRoles. Each authorization-probe pass has a 30-second context deadline. Missing required permissions and incomplete-probe diagnostics appear as structured entries under rbacGaps on the status endpoint. Incomplete API discovery is reported as unknown authorization coverage, not an empty successful probe; a positively absent CRD is not a permission gap. The optional gap reason identifies diagnostics such as API discovery incomplete; authorization coverage unknown rather than asking for a nonexistent grant.

The posture reader selects one report API using Kyverno evidence. An API must serve both its namespaced and cluster-scoped report resources. When both openreports.io and wgpolicyk8s.io are served, bounded scans look for Kyverno results: wgpolicyk8s.io is selected when it contains Kyverno results, and openreports.io is selected when only it contains them. If neither contains Kyverno results, the reader defaults to wgpolicyk8s.io. Runtime sensor results alone do not select OpenReports. If OpenReports is the only served report API, posture remains Unknown until a Kyverno result is observed there. If WG reports are the only served API, they remain the stock default without requiring existing results. This selection is specific to posture and does not change the separate audit-report writer’s OpenReports preference. When posture selects OpenReports, one reader feeds both the runtime and posture consumers, which keep their source filters and storage semantics separate. When posture selects WG reports, the runtime OpenReports reader remains alongside the WG posture reader. A shared lease coordinates these readers so replicas do not duplicate watches.

Report selection and ingestion recognize the exact result sources kyverno, Kyverno, KyvernoValidatingPolicy, KyvernoImageValidatingPolicy, KyvernoGeneratingPolicy, and KyvernoMutatingPolicy. They share the kyverno evidence source in AIControls. Runtime sensor results (kyverno-runtime) and unrecognized producers remain separate; a source name starting with “Kyverno” is not sufficient.

Kyverno can first write reports.kyverno.io/v1 EphemeralReport or ClusterEphemeralReport objects while processing admission or background results. Its report controller aggregates those temporary objects into the published WG PolicyReports or OpenReports. AIControls reads the published reports, not the temporary objects, and needs no additional report permissions for this lifecycle. If you see only ephemeral reports, check that Kyverno's report controller has completed aggregation; missing published results do not count as passing posture. AIControls does not install Kyverno or create admission policies.

If discovery or the evidence scan cannot establish which API to use, report availability is Unknown and the existing reader and mirrored facts are retained. A confirmed change of report API removes the previous flavor’s facts before starting the replacement reader. A positively unavailable report API also clears retired facts. A temporary error does not establish that the reports were deleted.

If engine absence is confirmed, cluster and workload posture responses omit retained report counts and category results and remain Unknown. This does not delete stored evidence or stop the separate Runtime Controls report reader; a transient engine-discovery error retains the evidence with Unknown posture.

The three states​

Engine detection produces one of three user-facing states, and each needs a different screen. Collapsing them into one scorecard is how a cluster with no policy engine ends up looking clean.

userStateWhat was detectedWhat you see
no-enginekyverno.io is not servedPosture is Unknown. The guidance says plainly that no policy engine was detected, that an unevaluated cluster carries higher risk than an evaluated one with findings, and links to the Kyverno install docs. No result reads as passing.
engine-no-policiesKyverno is installed; no policy is installedKyverno's distribution and version are shown, but posture is still Unknown. Install Kyverno policies that apply to your workloads.
engine-with-policiesKyverno is installed with policiesPass and fail counts per policy category from ingested verdicts when the report API is available. If report access is unavailable or unknown, a workload remains Unknown with a reason even though policies are installed.

Each Kubernetes API discovery request has a 10-second HTTP timeout. A discovery timeout produces Unknown with a reason, not a conclusion that the API is absent; a sequence of discovery calls can take longer than one request’s timeout.

Two additional states describe incomplete detection: unknown (API discovery did not answer, so the engine’s presence could not be established) and engine-policies-unknown (Kyverno is present but its policy inventory could not be read). The latter includes a failed list of any served policies.kyverno.io CEL policy kind or a failed later page of a policy list. Inventory is available only after every page has been read successfully; partial inventory is not treated as complete. The most recent access probe replaces the shared rbacGaps snapshot. Later permission failures appear in detection or reader-health reasons; they do not change rbacGaps until the next access probe.

A workload requires an available report API as well as an available engine, readable policy inventory, at least one installed policy, and at least one counted result for the workload to be evaluated. Missing report CRDs, denied list/watch access, or a report-reader failure produce Unknown with a reason. This includes failures to persist a report update, deletion, or relist cleanup: a successful Kubernetes read alone does not prove the mirrored evidence is current. After a failure, workloads remain Unknown until report listing, mirror persistence, and watch setup recover successfully; retained facts do not establish current coverage. Cluster-level state describes available policy coverage. A workload is Unknown when it has no counted verdicts, even if the cluster has policies and a healthy reader. Zero workload results never establish a passing state. Error and skipped results count as collected evidence, but retain their distinct outcomes and do not imply a pass.

Reader health is renewed every 5 seconds while the reader is active and expires after 15 seconds without renewal. A crash or failed health write therefore becomes Unknown within that window, not instantaneously. The heartbeat renews the current state, including pending or Unknown; it does not turn an unhealthy source healthy. Every full relist first marks the source pending. List pages and mirror writes have 30-second deadlines, and a source becomes healthy only after the complete paginated list is persisted and its watch is established. Watch sessions have a one-minute safety deadline, then reconnect from the last successfully mirrored resource version without replaying the full inventory. Full reconciliation runs every 10 minutes; that deadline is preserved across watch reconnects. Explicit watch errors, expired resource versions, or persistence failures trigger an earlier full relist. Health is pending during reconnect and returns healthy once a watch is established from a valid snapshot cursor. Both namespaced and cluster-scoped sources must be healthy. Older health records without an expiry remain Unknown until renewed.

The distribution is a labelled heuristic, not an API contract: an image from a Nirmata registry identifies Nirmata Kyverno (N4K), anything else Kyverno OSS, and the response carries the evidence (Deployment, image, version label) so a wrong guess is debuggable. When multiple admission-controller Deployments match, AIControls selects the first by namespace and then name in alphabetical order, so the evidence stays consistent across probes. An unreadable Deployment degrades to "Kyverno, distribution unknown" rather than failing detection.

Policy categories and the policy selection​

AIControls groups report results by the policy's policies.kyverno.io/category annotation, such as Pod Security Standards (Baseline) or Best Practices. A policy without the annotation, or one that is no longer installed, goes under Uncategorized. AIControls doesn't map policies to compliance standards or controls.

Severity values are trimmed and lowercased. Supported values are critical, high, medium, low, and info; missing or unsupported values become unknown. A supported installed policy annotation takes precedence; otherwise a supported report-result severity provides the fallback. An invalid annotation therefore does not hide a valid report severity. Cluster and workload rollups use the same rule.

If installed policies in different namespaces or policy kinds share the same name, the policy inventory is Unknown with a reason. AIControls doesn't choose one policy's annotations or severity arbitrarily.

The policy selection decides which policies count. Open Settings → Workloads and use the Kyverno policies card (see Review Kubernetes posture), or use the API. It has two modes:

  • All policies (the default): every policy that reports on a workload counts, and every one must pass.
  • Selected policies: only the policies you select count. You pick them from the installed Kyverno policies, so you can select a policy before its first report. Results from other policies are ignored: they neither pass nor fail the workload.

Under Selected policies, each selected policy that applies to a workload's kind must have reported a result before the workload can pass. A policy applies when its match kinds include the workload's kind, or Pod for a workload that runs pods, such as a Deployment. Until every applicable selected policy reports, for example before the first background scan finishes, the workload is Unknown and the response lists the missing policies in notScanned. Unknown never passes. A failing selected policy still fails the workload.

A namespaced policy applies only to workloads in its own namespace. Under match.all, only the kinds that every entry names count. A match.any entry that names no kinds, such as a selector-only entry, matches every kind. A policy whose match blocks AIControls can't read, or whose resource names it doesn't recognize, is assumed to apply. Apart from that, AIControls reads only the match kinds: it doesn't evaluate exclude blocks or namespace and object selectors. If a selected policy excludes a workload that way, the workload stays Unknown until you remove that policy from the selection.

The selection applies to every cluster in the deployment, so select policies by names that every cluster uses.

The selection is stored in the AIControls database as one document, and a PUT replaces it wholesale:

{ "mode": "selected", "policies": ["disallow-privileged-containers", "require-labels"] }

The selection is applied when you read, not when reports are ingested, so a change applies to existing results immediately on every replica.

Upgrade

Earlier versions let you map policies to standards and controls. That mapping, GET and PUT /api/v1/posture/mapping, the per-standard rollup, and the Kyverno posture signal in the deployment risk band are removed. On upgrade, AIControls deletes any mapping overrides you saved. Nothing replaces them: results are grouped by category instead, and the risk band no longer reads Kyverno results.

Where you see posture​

Each workload's Posture tab shows pass and fail counts grouped by policy category. Expand a category to see each policy, rule, message, and resource behind the counts. The endpoints report a userState of no-engine, engine-no-policies, engine-with-policies, engine-policies-unknown, or unknown. Every state except engine-with-policies leaves posture Unknown. Pod Security Standards are evaluated built in, so they don't depend on Kyverno. To evaluate policy-report posture, install Kyverno. For the steps, see Review Kubernetes posture.

Risk contribution​

The per-workload endpoint exposes the workload's state, result counts, and fail/warn counts by policy severity. Rule R4 of the registration readiness verdict reads it. The deployment risk band doesn't read Kyverno results.

Until then, review the evidence directly. Unknown means evaluation coverage has not been established; it must not be interpreted as a passing result or as lower risk than an evaluated workload. A workload's detail page shows these results on its Posture tab.

Enable or disable​

Posture is part of the Runtime Controls integration and follows its eligibility rules. Turn only the posture reader off, keeping the rest of Runtime Controls on:

runtimeControls:
enabled: true
posture:
enabled: false

With posture.enabled: false, the chart requests no posture ClusterRole and the posture-specific Kyverno detection and report reader do not run. The posture evidence endpoints answer enabled: false with a disabled by config reason. Runtime Controls remains enabled: its separate OpenReports ingester and its ClusterRole still operate, so disabling posture does not stop all policy-report reads.

The chart’s shipped value is true, so omitting the setting from your override file leaves posture enabled after Helm merges the chart defaults. Set it explicitly to false or null to disable it. If the posture block or flag is absent from the final values passed to the template, the template also renders the application setting as false and omits the posture ClusterRole; this defensive fallback is different from omitting an override.

Endpoints​

In the table, Portal permits admin or viewer roles; Admin permits only admins. Evidence, inventory, selection, and assessment reads return 200 with enabled: false and a reason when posture is unavailable. Settings reads instead return the configuration directly, including defaults when the integration is inactive. An availability response must not be mistaken for an empty or passing cluster.

EndpointAccessReturns
GET /api/v1/posture/statusPortaluserState, the last detection (engine state, distribution, version, evidence, installed policies, resolved report API), rbacGaps, and how many facts and workloads have been mirrored
GET /api/v1/posture/summary?namespace=PortalThe per-category rollup: result counts, the number of policies, findings by severity, and failing workloads for each category, with the active selection
GET /api/v1/posture/workloads/by-ref/{namespace}/{kind}/{name}PortalSame response as the {key} route, with separate path segments. Use _ as {namespace} for a cluster-scoped subject.
GET /api/v1/posture/workloads/{key} (deprecated)PortalOne workload's counted verdicts grouped by category, with state (evaluated or unknown) and a reason, the active selection, and notScanned; key is the percent-encoded <namespace>/<Kind>/<name> (or /<Kind>/<name> for a cluster-scoped subject such as a ClusterRole)
GET /api/v1/posture/policy-selectionPortalThe stored selection (mode and policies) and the available installed policies, each with its category
PUT /api/v1/posture/policy-selectionAdminReplaces the selection wholesale. mode is all or selected; selected requires at least one policy. Rejected with 400 otherwise. Send {"mode":"all"} to restore the default.
GET /api/v1/posture/workloads?search=&page=&pageSize=PortalSearch and paginate workload keys. Returns enabled, items (each with workloadKey), and the matching total.
GET /api/v1/posture/workloads/by-ref/{namespace}/{kind}/{name}/assessmentPortalSame response as the deprecated {key}/assessment route, with separate path segments. It also accepts the factRef and standard parameters described in the next row.
GET /api/v1/posture/workloads/{key}/assessment (deprecated)PortalThe deterministic baseline, stored advisory assessment (including typed evidence and independent gaps), stale status, optional enrichment, and evidenceTotal / evidenceTruncated preview metadata. Counts cover all collected facts; result previews contain at most 100 entries.
GET /api/v1/posture/workloads/{key}/assessment?factRef=&standard= (deprecated; also available on the by-ref route)PortalOne current citation as {item: WorkloadResult}, including facts outside the preview. Requires a 64-character hexadecimal fact reference and a nonempty UTF-8 standard value of at most 128 bytes, which is the result's policy category; invalid parameters return 400, absent facts or a fact not in that category 404, and workloads exceeding the 10,000-fact limit 422. Does not call an LLM.
GET /api/v1/posture/assessment/settingsPortalThe current assessment configuration directly, or default settings when the integration is inactive. Its enabled field controls assessment generation, rather than reporting integration availability.
PUT /api/v1/posture/assessment/settingsAdminReplaces the assessment configuration and returns it. Invalid settings or unknown fields return 400; an inactive integration returns 503.
curl "$YOUR_WORKSPACE_URL/api/v1/posture/summary" \
-H "Authorization: Bearer $ADMIN_TOKEN"

Detection runs every five minutes on one replica under a lease, so a Kyverno install after AIControls is picked up without a restart. Reports are mirrored by a list-and-watch loop: a report Kyverno rewrites replaces its rows wholesale, a deleted report removes them, and a periodic full list prunes any report whose delete event was missed. Background-scan cadence is Kyverno’s, not AIControls’; the lastObservedAt timestamps tell you how fresh a verdict is.

Workload attribution uses the namespace explicitly carried by the referenced resource or report scope, never the report object’s metadata namespace. An empty subject namespace stays empty and produces /<Kind>/<name>, including for cluster-scoped custom resource kinds. Namespaced subjects must carry their namespace to produce <namespace>/<Kind>/<name>.

Result counts retain each evaluated subject. For example, two Pods owned by the same Deployment with the same policy/rule failure contribute two report results to that workload, rather than overwriting each other. These counts differ from failingWorkloads, which counts distinct workloads with a fail or warn result.

AI Posture Assessments​

The optional AI Analyst assessment layer uses available report evidence, collection availability, deployed policy configuration, and enabled supporting signals to produce an advisory narrative and rating for each workload. It is disabled by default. Missing report findings do not prevent an assessment of available context and evidence gaps, but do not establish passing posture. The deterministic Kyverno evidence baseline remains available without an LLM. Assessments are computed in the background, with a minimum interval between recomputations, and stored as the current workload snapshot. When a workload leaves the current inventory, its stored assessment and supporting context are pruned; direct reads do not return the orphaned snapshot.

The default signal set includes per-category counts, Kyverno engine and policy inventory, and bounded individual findings. Disabling individual findings omits their messages while retaining the fact metadata needed for citations. Optional signals add trimmed workload manifests, Runtime Controls findings, or declared behavior tiers. They are disabled by default and supply context, not additional policy verdicts. Optional context refreshes at the maximum assessment age; changed facts or settings can trigger earlier recomputation. The scorecard’s Supporting context panel displays the snapshot used for the assessment, separately from Kyverno verdicts, and labels stale snapshots. Runtime Controls findings use category: detail so the category stays distinct when you read or copy the text.

Manifest context requires a separate, get-only posture-manifests ClusterRole. Install it by enabling runtimeControls.posture.assessment.signals.manifestExcerpt in your Helm values. Enabling only the UI setting without the grant produces Not assessed with a reason. Supported kinds are Pod, Deployment, StatefulSet, DaemonSet, ReplicaSet, Job, and CronJob. Other kinds produce Not assessed with a reason naming the unsupported workload kind; this is distinct from a permissions or read failure. The collector excludes environment values, labels, annotations, Secret contents, and ConfigMap contents.

Each container’s manifest context retains at most 16 resource requests and 16 resource limits, prioritizing CPU, memory, and ephemeral storage. If entries are omitted, the container carries resourcesTruncated: true. A trimmed manifest larger than 64 KiB (65,536 bytes) produces Not assessed with an explicit size-limit reason; the oversized manifest is not sent to the model or persisted as supporting context.

Configure and review assessments​

  1. Configure the global Internal AI provider and model. Assessment calls use that configuration and are attributed to the k8s-posture-assessment purpose.
  2. Open Settings → Security → AI Posture Assessment as an administrator. Enable assessments, select whether to include individual Kyverno findings, and set the maximum findings per standard.
  3. Optionally enter Operator guidance. This text is appended to the fixed prompt; the evidence handling and citation requirements remain in place. Set the maximum assessment age and minimum recompute interval, then select Save.
  4. Open Security Posture → Kubernetes workload scorecard and search for a workload. This page is available when advanced surfaces are enabled. Review its Kyverno evidence baseline alongside the assessment and open evidence citations to inspect the source report results.
  5. Review Evidence gaps and Assessment context evidence separately from report results. Open a context citation to inspect its type, supplied details, and observation time. Installed policy configuration and declared behavior do not prove workload compliance or runtime verification.

Inventory and background processing​

Workload search runs on the server after a short pause in typing and returns matching pages with a total count. The inventory includes report-backed workloads, active discovered workloads, and registered workloads in the current cluster; it is not restricted to the first 10,000 workloads.

The leased background worker processes pages of 16 workloads with at most four assessments running concurrently. It saves the next cursor position before processing a page, so a restart or lease handover continues beyond that page. Interrupted items are retried when the cursor wraps through the inventory. Optional context collection and the provider call share a two-minute deadline per due workload. These bounds and the one-minute pause between sweeps mean the configured intervals determine eligibility, not a guaranteed completion time. Assessment age starts when processing completes.

Assessment settings​

The configuration block is runtimeControls.posture.assessment. Saved administrator settings are shared across replicas and refreshed without a restart. If a stored settings document cannot be decoded or fails validation, the service falls back to its configured defaults so administrators can open the settings page and save a valid replacement.

Each changed settings document is committed together with configuration-change evidence identifying the administrator. The serialized settings, including operator guidance, stay redacted in that evidence. If the audit write fails, the save fails and the previous settings remain unchanged. Saving an identical document does not create another event.

You can inspect assessment settings even when workload inventory is unavailable or still loading. If the posture integration is inactive, the page shows default settings; saving requires an active integration and reports the server's error when it is unavailable.

FieldDefaultMeaning
enabledfalseEnable the optional background LLM assessment.
signals.kyvernoFindingstrueInclude individual finding messages. Counts and citation metadata remain included.
signals.topFindingsPerStandard5Maximum cited inputs per category, from 1 to 20.
signals.manifestExcerptfalseInclude a sanitized workload spec; requires the separate Helm permission grant.
signals.runtimeControlsFindingsfalseInclude bounded Runtime Controls observations as context.
signals.declaredBehaviorTierfalseInclude the workload's declared access tier as context.
promptOverrideEmptyAppended operator guidance, at most 4,000 bytes.
maxStalenessMinutes60Maximum assessment age, at most 10,080 minutes.
minRecomputeIntervalMinutes5Minimum interval between recomputations; at least 1 minute and no greater than the maximum age.

Assessments must cite supplied evidence. Report findings cite exact Kyverno facts and their policy category. Context findings cite typed availability, policy, configuration, runtime, or declaration snapshots; they are informational observations, not policy verdicts. Collection and coverage gaps are displayed independently of the model's rating, including when the assessment is unavailable. A failed call or invalid response yields Not assessed with a reason. Changed evidence or settings and expired assessments are shown as stale. Review the baseline and gaps before acting on an older narrative. The rating never changes baseline counts or influences enforcement.

The scorecard shows a bounded preview of up to 100 result entries across categories. A partial-preview notice gives the displayed and total entry counts; per-category counts still cover all collected facts. Report citation buttons load current facts outside the preview on demand. Preview and report citation messages are limited to 1,000 UTF-8 bytes; consult the source report for longer message text. A report fact is reported missing only when the lookup confirms it is no longer present. Context citations instead resolve from the stored assessment snapshot, including when it is stale; they never query the report-fact endpoint. A missing context reference is explicitly unavailable.

Assessment input is capped at 10,000 facts per workload. If a workload exceeds this cap, the service does not send a partial baseline to the model: it marks the baseline Unknown and the rating Not assessed, with an explicit limit reason. No LLM call is made for that workload; other workloads continue to be assessed.

Advisory boundary and agent access​

Generated assessments are not exposed through policy context or policy-generation fields. They cannot approve or deny requests, enable enforcement, or change workload verification. Assessment generation and reads are separate from the request-evaluation path. Use deterministic policy evidence and explicit administrator decisions for enforcement; a generated rating is advisory only.

Agents can use the read-only get_k8s_posture_assessment tool to retrieve the current baseline and stored assessment with citations. Reading through an agent, API, or UI does not trigger an LLM call.

See also​