Skip to main content

Kubernetes v1.36 Ships Admission Policies That Can't Be Deleted: Closing the Bootstrap Window in Your Fleet's Guardrails

10 min readDora NodaDora Noda
Share
On this page

Every security guardrail in your Kubernetes cluster — every ValidatingAdmissionPolicy, every webhook configuration, every tenant-isolation rule — is an ordinary API object. kubectl delete validatingwebhookconfiguration my-guardrail works on all of them. Anyone with the right RBAC can remove the exact policy standing between a compromised credential and the rest of your fleet, and Kubernetes will not stop them, because the machinery that could stop them is the thing being deleted.

Kubernetes v1.36 (released April 22, 2026) ships an alpha answer: manifest-based admission control. Instead of creating admission policies through the API, you place them as YAML files in a directory on the control-plane node (staticManifestsDir), and the kube-apiserver loads them before it serves its first request. These static policies never exist in etcd, are invisible to kubectl, and cannot be modified or deleted through the API — by anyone, with any RBAC. For teams running their own multi-tenant platform on a Cluster-API-managed fleet, this closes two long-standing gaps at once: the bootstrap window where policies simply don't exist yet, and the self-protection blind spot where no policy can guard another policy from deletion.

Here's how the feature works, what it actually forecloses for a self-hosted platform's control plane, and what your bootstrap sequencing has to guarantee so the protection is live before the first tenant workload ever schedules.

The Two Gaps API-Based Admission Could Never Close

Admission control is Kubernetes' gatekeeper layer: it intercepts API requests after authentication and authorization but before persistence to etcd, validating or mutating objects against your rules. But the enforcement objects themselves have always had two structural weaknesses that no amount of policy-writing could fix.

Gap 1: The bootstrap window. Admission policies are API resources. They do not exist until someone creates them. Every cluster bootstrap, every restore from backup, every etcd recovery has a period where the API server is up and serving requests but your policies haven't been applied yet. If your platform pipeline runs kubectl apply -f policies/ as a post-bootstrap step, everything that reaches the API server before that step lands is unenforced. In a multi-tenant platform where tenant controllers reconcile continuously, "before that step" is not a theoretical window — it's a race you re-run on every cluster you create or rebuild.

Gap 2: The self-protection blind spot. Kubernetes intentionally skips invoking admission webhooks on types like ValidatingWebhookConfiguration to avoid circular dependencies — a webhook that could block operations on webhook configurations could brick the cluster's ability to fix itself. The consequence: no API-based webhook or policy can prevent the deletion of another webhook or policy. Security vendors have documented webhook deletion as a standard defense-evasion technique for exactly this reason; Elastic ships a prebuilt detection rule for admission webhook modification, and Red Hat Advanced Cluster Security's hardening guidance treats webhook deletion as a first-class attack path. Detection is the best you could do. Prevention was architecturally impossible.

Put the two together and the attack chain against a self-hosted multi-tenant platform is short:

  1. A tenant credential is compromised, or a tenant is granted over-broad RBAC (it happens — cluster-admin for "just this one migration" has a way of persisting).
  2. The attacker runs kubectl delete validatingadmissionpolicy tenant-isolation-baseline.
  3. The policy that denied privileged pods, blocked scheduling onto management nodes, or fenced tenants into their namespaces is gone. Nothing enforced the guardrail's own existence.
  4. The attacker schedules a privileged workload wherever they like — including, in a Cluster API fleet, onto nodes that can reach the management cluster's credentials.

One clarification before going further, because v1.36 shipped two admission-control headlines that are easy to conflate: MutatingAdmissionPolicy graduated to GA in the same release, giving you CEL-based mutation without running webhook servers. That's a different feature solving a different problem (webhook operational overhead). Manifest-based admission control — the subject here — is about where policies live and whether they can be removed, and it's alpha, not GA.

How Manifest-Based Admission Control Actually Works

The mechanism is deliberately boring: files on disk, loaded at startup.

You enable the ManifestBasedAdmissionControlConfig feature gate on the kube-apiserver, then add a staticManifestsDir field to the AdmissionConfiguration file you pass via --admission-control-config-file:

yaml
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/"

Drop your policy YAML into that directory, and the API server loads it before serving any requests. Four plugin types support a static manifests directory:

PluginStatic resource kinds
ValidatingAdmissionPolicyValidatingAdmissionPolicy, ValidatingAdmissionPolicyBinding
MutatingAdmissionPolicyMutatingAdmissionPolicy, MutatingAdmissionPolicyBinding
ValidatingAdmissionWebhookValidatingWebhookConfiguration
MutatingAdmissionWebhookMutatingWebhookConfiguration

Every object defined this way must have a name ending in .static.k8s.io. The suffix is reserved: with the feature gate on, the API server blocks creation of API-based admission objects using it, so there are no collisions, and audit logs and metrics make it obvious when a decision came from a static policy. Duplicate names across manifest files fail API server startup outright — a misconfiguration surfaces immediately rather than silently.

A static policy looks exactly like its API-based counterpart, minus the delivery mechanism. The upstream example denies privileged containers:

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)

The constraints are real, and worth knowing before you plan around the feature:

  • No parameter resources. Static policies can't use spec.paramKind, and static bindings can't use spec.paramRef — there's no etcd to look ConfigMaps up in at load time. Your policy logic must be self-contained CEL.
  • Webhooks must use url, not service. At API server startup the service network may not exist yet, so static webhook configurations can only point at explicit URLs.
  • Not visible via the API. Static policies can't be listed with kubectl get validatingadmissionpolicy. Your observability for them is audit logs, metrics, and the files themselves — a real operational trade-off against API-based policies.
  • Alpha, off by default. This shipped in v1.36 behind a feature gate, one of 25 alpha features in a release with 70 tracked enhancements.

What This Forecloses for a Self-Hosted Platform's Control Plane

The genuinely new capability is not "policies from files." You could always template policies into a bootstrap pipeline. It's this: static policies can intercept operations on admission resources themselves — the exact thing API-based admission is forbidden from doing. Because static policies live outside the API, the circular-dependency problem disappears, and the upstream docs show the pattern explicitly:

yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: "protect-admission-resources.static.k8s.io"
spec:
  matchConstraints:
    resourceRules:
    - apiGroups: ["admissionregistration.k8s.io"]
      apiVersions: ["v1"]
      operations: ["CREATE", "UPDATE", "DELETE"]
      resources:
      - "validatingadmissionpolicies"
      - "validatingadmissionpolicybindings"
      - "mutatingadmissionpolicies"
      - "mutatingadmissionpolicybindings"
      - "validatingwebhookconfigurations"
      - "mutatingwebhookconfigurations"
  validations:
  - expression: >-
      request.userInfo.username in
      ['system:serviceaccount:platform-system:policy-controller']
    message: "admission configuration is managed by the platform; changes via the API are not permitted for this user"

For a multi-tenant, self-hosted PaaS — the kind of platform where each tenant gets namespaces on shared clusters provisioned by Cluster API — walk the earlier attack chain again with this in place:

  • The compromised tenant credential runs kubectl delete validatingadmissionpolicy tenant-isolation-baseline. If the baseline itself is static, the delete has nothing to target: the policy isn't an API object. There is no RBAC grant, however broad, that reaches it.
  • The attacker tries deleting an API-based guardrail instead (your Kyverno or Gatekeeper constraints, say). The static protection policy above intercepts the DELETE on the admissionregistration resource and denies it for any identity other than the platform's own policy controller.
  • The attacker tries to schedule a privileged pod directly. The static deny-privileged baseline was active from the API server's first served request — including during the cluster's bootstrap, a restore, or an etcd recovery.

The guardrail's existence is no longer part of the cluster's mutable state. That's the foreclosure: an entire class of "step 1: delete the policy" attacks and "the policy wasn't applied yet" races stops being possible, rather than merely detectable.

Be honest about the boundary, though. Static policies are files on the control-plane node. An attacker with root on the control plane — or write access to the machine image, the KubeadmControlPlane spec, or whatever provisions those files — can change or remove them and restart the API server. Manifest-based admission control moves the trust anchor from "etcd state plus RBAC" to "control-plane filesystem plus your infrastructure pipeline." That's a much smaller, much better-audited surface, but it is not zero. It also does nothing about admission bypass paths that never touch admission at all, like exec-ing into an existing privileged pod.

Bootstrap Sequencing on a Cluster API Fleet: Live Before the First Tenant Pod

The feature only delivers its promise if the files are on the node before kube-apiserver starts. On a Cluster-API-managed fleet, that means the policy delivery moves out of your GitOps/apply pipeline and into the control-plane machine specification. The sequencing checklist:

1. Bake the files into the control-plane spec, not a post-bootstrap step. In a KubeadmControlPlane, that means the AdmissionConfiguration file and every static policy manifest ride in spec.kubeadmConfigSpec.files, so kubeadm writes them to disk during node provisioning — before the static pod for kube-apiserver ever starts.

2. Wire the API server flags in clusterConfiguration. Set --admission-control-config-file via apiServer.extraArgs, enable the ManifestBasedAdmissionControlConfig feature gate, and mount the admission directory into the API server static pod via apiServer.extraVolumes. All three travel with the control-plane spec, so every new control-plane node — including replacements during a rolling upgrade and rebuilds after a failure — comes up with the policies already enforced.

3. Treat the policy directory as immutable infrastructure. Changes go through the same review-and-rollout path as an API server version bump: update the KubeadmControlPlane spec, let CAPI roll the control plane. This is slower than kubectl apply — deliberately. Slow, audited change to guardrails is the point.

4. Verify before admitting tenants. After bootstrap, assert the protection is live before the platform marks the cluster ready for tenant scheduling: attempt a canary violation (create a privileged pod as a non-platform identity, attempt a policy deletion) and require the denial. A cluster that fails the canary never receives a tenant workload.

5. Compare against what you're replacing. The old sequence — cluster becomes ready, pipeline applies policies, tenants onboard — has failure modes at every arrow: the pipeline lags, the restore predates the policies, a human applies tenants before policies. The new sequence has one: the machine spec is wrong, which the canary in step 4 catches before any tenant is exposed.

Given alpha status, the pragmatic posture for 2026: run the feature gate in your non-production fleet now and design your control-plane specs so static delivery is a config change, not a re-architecture. In production meanwhile, keep the compensating controls this feature will eventually retire — least-privilege RBAC on admissionregistration.k8s.io (no tenant identity should ever hold delete on those resources), and audit-log alerting on any webhook or policy mutation. Watch the KEP for the beta graduation and for the file-watching/reload semantics to settle.

Guardrails Should Not Be Optional State

There's a quiet architectural statement in this feature: some cluster state is too important to be cluster state. Kubernetes has spent a decade putting everything in the API — and the API's uniformity is why a single compromised credential could unwind a platform's entire enforcement layer with one delete verb. v1.36's answer moves the enforcement layer's foundation down a level, into the same trust domain as the API server binary itself. For platform teams running their own fleets, that's the correct direction: your isolation guarantees should be provisioned like infrastructure, versioned like infrastructure, and exactly as hard to remove as the control plane they protect.

Expect the pattern to spread. The same logic that makes admission policies file-delivered applies to any control-plane-critical configuration that currently exists only as deletable API state — and as the feature matures toward beta, shipping a security baseline in the machine image may become as standard as shipping a CNI.


Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, on a Cluster-API-managed fleet whose guardrails are part of the platform, not an afterthought. Star the repo on GitHub or deploy your first app today.

Sources

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex