Manifest-Based Admission Control in Kubernetes
API-based admission policies — ValidatingAdmissionPolicy, ValidatingWebhookConfiguration, and their binding resources — are created and deleted through the Kubernetes API and stored in etcd. This design works well in stable clusters but leaves two well-defined security gaps: policies are not enforced during the window between API server startup and policy creation (the bootstrap gap), and a sufficiently privileged user can delete the policies that are supposed to protect the cluster (the self-protection gap).
Kubernetes v1.36 introduces manifest-based admission control as an alpha feature to address both gaps. Policies and webhook configurations are defined as files on disk and loaded by the API server at startup, before serving any requests. Because these resources are never stored in etcd and are not visible or modifiable through the Kubernetes API, they cannot be deleted or tampered with by cluster-level operations.
1. The Bootstrap and Self-Protection Gaps
Before: During cluster bootstrap, an etcd restore, or any recovery scenario where the API server restarts, there is a window in which the API server is serving requests but no admission policies have been loaded. Any workload created or modified during that window bypasses all admission enforcement. For webhooks, the same window exists: the webhook configuration object must be created through the API after the server starts, which means early requests are not intercepted.
The self-protection gap is equally significant. Kubernetes does not invoke admission webhooks or policies against the types that define them — ValidatingWebhookConfiguration, ValidatingAdmissionPolicy, ValidatingAdmissionPolicyBinding — to avoid circular dependencies. A user with permissions to delete these resources can remove all admission controls, and there is no admission control in the chain that can stop them.
After: Manifest-based admission control loads resources from files on disk at API server startup, before the server handles any request. These resources are not stored in etcd and are not visible or changeable through the Kubernetes API. Manifest-based policies and webhooks can intercept operations on API-based admission configuration resources themselves, closing the self-protection gap.
2. Enabling Manifest-Based Admission Control
Manifest-based admission control requires the ManifestBasedAdmissionControlConfig feature gate, which is alpha in Kubernetes v1.36 and disabled by default. Enable it on the API server:
kube-apiserver \
--feature-gates=ManifestBasedAdmissionControlConfig=true \
--admission-control-config-file=/etc/kubernetes/admission/config.yaml \
...
Version skew: This feature is not available before v1.36. Managed Kubernetes offerings (EKS, GKE, AKS) typically do not expose alpha feature gates — confirm that your control-plane configuration supports custom feature gates before relying on this feature in managed environments.
3. Configuring the Static Manifests Directory
The feature is configured through the AdmissionConfiguration file already required for webhook admission. Add a staticManifestsDir field under each plugin's configuration section:
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: ValidatingAdmissionPolicy
configuration:
apiVersion: apiserver.config.k8s.io/v1
kind: ValidatingAdmissionPolicyConfiguration
staticManifestsDir: "/etc/kubernetes/admission/validating-policies/"
- name: ValidatingAdmissionWebhook
configuration:
apiVersion: apiserver.config.k8s.io/v1
kind: WebhookAdmissionConfiguration
kubeConfigFile: "/etc/kubernetes/webhook-kubeconfig.yaml"
staticManifestsDir: "/etc/kubernetes/admission/validating-webhooks/"
- name: MutatingAdmissionWebhook
configuration:
apiVersion: apiserver.config.k8s.io/v1
kind: WebhookAdmissionConfiguration
kubeConfigFile: "/etc/kubernetes/webhook-kubeconfig.yaml"
staticManifestsDir: "/etc/kubernetes/admission/mutating-webhooks/"
- name: MutatingAdmissionPolicy
configuration:
apiVersion: apiserver.config.k8s.io/v1
kind: MutatingAdmissionPolicyConfiguration
staticManifestsDir: "/etc/kubernetes/admission/mutating-policies/"
staticManifestsDir accepts absolute paths only. At startup, the API server reads all direct-child files with .yaml, .yml, or .json extensions from the directory. Subdirectories and files with other extensions are silently ignored. Glob patterns and relative paths are not supported.
4. Supported Resource Types and Naming
Each plugin loads a specific set of resource types. Only admissionregistration.k8s.io/v1 is supported:
| Plugin | Accepted resource types |
|---|---|
ValidatingAdmissionPolicy | ValidatingAdmissionPolicy, ValidatingAdmissionPolicyBinding |
MutatingAdmissionPolicy | MutatingAdmissionPolicy, MutatingAdmissionPolicyBinding |
ValidatingAdmissionWebhook | ValidatingWebhookConfiguration |
MutatingAdmissionWebhook | MutatingWebhookConfiguration |
Each plugin's directory must contain only the resource types permitted for that plugin. Mixing types across the wrong directory causes the API server to fail at startup.
Naming requirement: Every object defined in a manifest file must have a name ending in .static.k8s.io. When the ManifestBasedAdmissionControlConfig feature gate is enabled, the API server blocks creation of any API-based admission objects that use a .static.k8s.io name, making the suffix exclusive to manifest-based resources. Duplicate names — two files in the same plugin directory defining objects of the same type and name — also cause the API server to fail at startup with a descriptive error.
5. Writing a Static Policy
The policy and binding YAML is identical to their API-based equivalents, with two constraints: names must end in .static.k8s.io, and bindings must reference a policy name that also ends in .static.k8s.io.
The following example denies privileged containers across all namespaces except kube-system. Place this file at /etc/kubernetes/admission/validating-policies/deny-privileged.yaml:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: "deny-privileged.static.k8s.io"
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
variables:
- name: allContainers
expression: >-
object.spec.containers +
(has(object.spec.initContainers) ? object.spec.initContainers : []) +
(has(object.spec.ephemeralContainers) ? object.spec.ephemeralContainers : [])
validations:
- expression: >-
!variables.allContainers.exists(c,
has(c.securityContext) && has(c.securityContext.privileged) &&
c.securityContext.privileged == true)
message: "Privileged containers are not allowed"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: "deny-privileged-binding.static.k8s.io"
spec:
policyName: "deny-privileged.static.k8s.io"
validationActions:
- Deny
matchResources:
namespaceSelector:
matchExpressions:
- key: "kubernetes.io/metadata.name"
operator: NotIn
values: ["kube-system"]
Both objects can reside in the same file or in separate files; the API server reads all matching files in the directory. Manifest files are decoded using the strict decoder, which rejects files containing duplicate fields or unknown fields — an invalid file causes the API server to fail at startup.
6. Limitations
Manifest-based resources operate in isolation from the cluster and cannot reference any API objects:
- No
spec.paramKind— Policies cannot reference ConfigMaps or other cluster resources as parameters. All policy logic must be embedded directly in the CEL expression. - No
spec.paramRef— Bindings cannot reference a separate parameter object. - No
clientConfig.service— Static webhook configurations must specifyclientConfig.url. At API server startup the cluster service network is not yet available, so service-based webhook targets cannot be reached. spec.policyNamein bindings must reference a policy also ending in.static.k8s.io, defined in the same manifest file set.- Not modifiable at runtime — Manifest-based resources are not visible or changeable through the Kubernetes API. Updating a static policy requires editing the file on disk and restarting the API server.
- Alpha stability — The configuration format and behavior may change before this feature graduates to beta or GA. Do not rely on manifest-based policies as the sole admission control layer in production clusters running alpha features.
7. Verification
After enabling the feature gate and placing policy files in the configured directory, verify enforcement by testing that the policy rejects a request it should deny:
# Attempt to create a privileged pod — should be rejected by the static policy
kubectl run test-privileged \
--image=busybox \
--restart=Never \
--overrides='{
"spec": {
"containers": [{
"name": "test",
"image": "busybox",
"securityContext": {"privileged": true}
}]
}
}'
A correctly loaded static policy causes the request to be denied with the message defined in the policy's validations[].message field. If the request succeeds, verify that the feature gate is active on the API server and that the policy files are present in the configured directory with the correct .static.k8s.io names.
To confirm that API-based policies cannot be given .static.k8s.io names when the feature gate is enabled:
kubectl apply -f - <<EOF
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: test-policy.static.k8s.io
spec:
matchConstraints:
resourceRules: []
validations: []
EOF
# Expected: error — name is reserved for manifest-based admission control

References
This article is based on information from the following official sources:
- Manifest-Based Admission Control - Kubernetes Documentation
- Validating Admission Policy - Kubernetes Documentation
- Kubernetes v1.36: Admission Policies That Can't Be Deleted - Kubernetes Blog