Introduction#
Admission controllers intercept API server requests after authentication and authorization, but before persistence. They can mutate objects (add labels, inject sidecars) or validate them (reject non-compliant resources). They are the correct place to enforce cluster-wide policy.
Built-in Admission Controllers#
1
2
3
4
5
6
7
8
# View enabled admission plugins
kube-apiserver --help | grep enable-admission-plugins
# Common always-enabled controllers:
# NamespaceLifecycle, LimitRanger, ServiceAccount,
# DefaultStorageClass, DefaultTolerationSeconds,
# MutatingAdmissionWebhook, ValidatingAdmissionWebhook,
# ResourceQuota, Priority
LimitRanger: Enforces default resource requests/limits when not specified. ResourceQuota: Limits total resource consumption per namespace. PodSecurity: Enforces Pod Security Standards (baseline, restricted).
Webhook Admission Controllers#
Custom admission logic runs as webhooks. The API server calls your HTTP endpoint with an AdmissionReview object.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# ValidatingWebhookConfiguration
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: policy-validator
webhooks:
- name: validate.example.com
rules:
- apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
operations: ["CREATE", "UPDATE"]
clientConfig:
service:
name: policy-webhook
namespace: kube-system
path: /validate
caBundle: <base64-encoded-ca-cert>
admissionReviewVersions: ["v1"]
sideEffects: None
failurePolicy: Fail # Fail or Ignore — Fail blocks on webhook unavailability
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# Webhook server (FastAPI)
from fastapi import FastAPI, Request
import base64, json
app = FastAPI()
@app.post("/validate")
async def validate(request: Request):
body = await request.json()
uid = body["request"]["uid"]
pod = body["request"]["object"]
# Reject pods without resource requests
for container in pod["spec"]["containers"]:
resources = container.get("resources", {})
requests = resources.get("requests", {})
if "memory" not in requests or "cpu" not in requests:
return {
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"response": {
"uid": uid,
"allowed": False,
"status": {
"message": f"Container '{container['name']}' missing resource requests"
}
}
}
return {
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"response": {"uid": uid, "allowed": True}
}
MutatingAdmissionWebhook: Sidecar Injection#
Mutation webhooks modify objects before storage. Istio and Linkerd use this to inject proxy sidecars.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@app.post("/mutate")
async def mutate(request: Request):
body = await request.json()
uid = body["request"]["uid"]
pod = body["request"]["object"]
# Build JSON patch to add a sidecar container
patch = [
{
"op": "add",
"path": "/spec/containers/-",
"value": {
"name": "metrics-sidecar",
"image": "prom/statsd-exporter:latest",
"ports": [{"containerPort": 9102}]
}
},
{
"op": "add",
"path": "/metadata/labels/injected",
"value": "true"
}
]
patch_b64 = base64.b64encode(json.dumps(patch).encode()).decode()
return {
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"response": {
"uid": uid,
"allowed": True,
"patchType": "JSONPatch",
"patch": patch_b64,
}
}
OPA/Gatekeeper for Policy as Code#
Open Policy Agent (OPA) with Gatekeeper is the standard approach for policy enforcement. Policies are written in Rego and stored as CRDs.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# ConstraintTemplate: defines the policy schema and logic
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels
validation:
openAPIV3Schema:
properties:
labels:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
violation[{"msg": msg}] {
provided := {label | input.review.object.metadata.labels[label]}
required := {label | label := input.parameters.labels[_]}
missing := required - provided
count(missing) > 0
msg := sprintf("Missing required labels: %v", [missing])
}
---
# Constraint: apply the policy with parameters
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: require-app-label
spec:
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment"]
parameters:
labels: ["app", "team", "environment"]
Kyverno: Kubernetes-Native Policies#
Kyverno policies are written in YAML, no Rego required.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-resource-limits
spec:
validationFailureAction: Enforce
rules:
- name: check-container-resources
match:
any:
- resources:
kinds: [Pod]
validate:
message: "Resource limits are required for all containers."
pattern:
spec:
containers:
- resources:
limits:
memory: "?*"
cpu: "?*"
Best Practices#
- Set
failurePolicy: Failonly for critical security policies; useIgnorefor non-critical webhooks to prevent cluster lockout. - Always set appropriate timeouts (
timeoutSeconds: 5) — a slow webhook blocks API server requests. - Exclude
kube-systemnamespace from most policies to avoid breaking system components. - Test webhooks against a staging cluster before deploying to production.
- Deploy webhook servers with high availability (multiple replicas).
Conclusion#
Admission controllers are the enforcement point for cluster-wide policy. Use built-in controllers for resource quotas and limits. Use OPA/Gatekeeper or Kyverno for custom policies as code. Reserve custom webhook servers for cases where neither handles your requirements.