Drift Sentinel is a Kubernetes validating admission controller that blocks unintended workload drift on UPDATE operations.
It compares the old and new object, strips Kubernetes-managed noise, applies rule-defined include and exclude scopes, and only allows changes that fall under explicitly mutable paths.
UPDATEenforce, warn, dry-run, offclient-go informers/metricsConfigMap rule discovery:
drift-sentinel.k8s.io/rule: "true"Resource bypass:
drift-sentinel.k8s.io/bypass: "true"bypass fieldNamespace mode override:
drift-sentinel.k8s.io/mode: "enforce|warn|dry-run|off"Namespace webhook opt-out label in the default chart:
drift-sentinel.k8s.io/enabled=falseFor each UPDATE admission request:
true, allow the request.status, metadata.managedFields, metadata.resourceVersion, metadata.generation, metadata.uid, metadata.creationTimestamp, metadata.selfLinkinclude paths if configured.exclude paths.mutable and immutable paths.Rules are stored in any namespace as annotated ConfigMaps:
apiVersion: v1
kind: ConfigMap
metadata:
name: drift-sentinel-production
namespace: drift-sentinel
annotations:
drift-sentinel.k8s.io/rule: "true"
data:
spec: |
mode: enforce
priority: 200
namespaces:
- "prod-*"
selectors:
- apiGroup: "apps"
kind: "Deployment"
- apiGroup: "apps"
kind: "StatefulSet"
labels:
- "app=api-service"
- "team"
users:
- "system:admin"
- "system:serviceaccount:team-a:release-bot"
exclude:
- "status"
- "metadata.managedFields"
- "metadata.resourceVersion"
mutable:
- "spec.template.spec.containers[*].image"
- "spec.template.spec.initContainers[*].image"
bypass: "drift-sentinel.k8s.io/bypass"
Supported rule fields:
mode: enforce, warn, dry-run, offpriority: higher wins; ties are broken deterministically by ConfigMap namespace/namenamespaces: namespace glob patternsselectors: API group and kind pairslabels: optional resource label selectors; each entry is either key=value for exact match or key for label presenceexclude: paths removed from comparisoninclude: if set, only these paths are comparedmutable: changed paths allowed within the compared scopeusers: optional exact usernames to which the rule applies; if set and the request user is not in the list, Drift Sentinel allows the requestbypass: resource annotation key that skips enforcementThe path matcher supports a constrained JSONPath-like syntax:
spec.replicasspec.template.spec.containers[*].imagespec.template.spec.containers[0].imagemetadata.*metadata.annotations['kubectl.kubernetes.io/last-applied-configuration']This is not full JSONPath. The implementation is intentionally narrow and deterministic.
| Mode | Violation Behavior |
|---|---|
enforce |
deny the request with 403 |
warn |
allow the request and log the violation |
dry-run |
allow the request and log a would deny reason |
off |
allow the request without blocking |
Precedence:
The Helm chart registers the webhook for:
apps/v1 Deploymentapps/v1 StatefulSetapps/v1 DaemonSetargoproj.io/v1alpha1 RolloutThese defaults are configurable in values.yaml under webhook.rules.
The engine itself matches on API group and kind from rules, but the webhook configuration controls which requests are actually sent to Drift Sentinel.
Chart path:
charts/drift-sentinelThe chart renders:
.Values.defaultRule.enabled.Values.rulesCertificate behavior:
-cert in the release namespaceca.crt, tls.crt, and tls.key, it is reusedcaBundleDefault chart behavior:
webhook.failurePolicy defaults to FaildefaultRule.enabled defaults to trueenforce modeBasic install:
helm repo add drift-sentinel https://dheeth.github.io/drift-sentinel
helm install drift-sentinel drift-sentinel/drift-sentinel -n drift-sentinel --create-namespace
Install with embedded rules:
rules:
- name: drift-sentinel-production
namespace: drift-sentinel
spec: |
mode: enforce
priority: 200
namespaces:
- "prod-*"
selectors:
- apiGroup: "apps"
kind: "Deployment"
mutable:
- "spec.template.spec.containers[*].image"
Example standalone rule manifests are available in:
deploy/examples/rule-strict.yamldeploy/examples/rule-relaxed.yamldeploy/examples/rule-replica-only.yamlRun against a local kubeconfig:
$env:DRIFT_SENTINEL_KUBECONFIG="$HOME\.kube\config"
go run ./cmd/server
The service defaults to :8080 locally. For webhook-style TLS serving, set:
DRIFT_SENTINEL_TLS_CERT_FILEDRIFT_SENTINEL_TLS_KEY_FILE| Variable | Default | Purpose |
|---|---|---|
DRIFT_SENTINEL_ADDRESS |
:8080 |
HTTP or HTTPS listen address |
DRIFT_SENTINEL_LOG_LEVEL |
INFO |
slog log level |
DRIFT_SENTINEL_HEALTH_PATH |
/healthz |
health endpoint path |
DRIFT_SENTINEL_METRICS_PATH |
/metrics |
metrics endpoint path |
DRIFT_SENTINEL_VALIDATE_PATH |
/validate |
admission endpoint path |
DRIFT_SENTINEL_KUBECONFIG |
empty | use local kubeconfig instead of in-cluster config |
DRIFT_SENTINEL_TLS_CERT_FILE |
empty | TLS cert file path |
DRIFT_SENTINEL_TLS_KEY_FILE |
empty | TLS key file path |
DRIFT_SENTINEL_WATCH_RESYNC |
30s |
informer resync period |
DRIFT_SENTINEL_STARTUP_SYNC_TIMEOUT |
30s |
max time to wait for informer cache sync on startup |
DRIFT_SENTINEL_READ_HEADER_TIMEOUT |
5s |
HTTP read header timeout |
DRIFT_SENTINEL_READ_TIMEOUT |
15s |
HTTP read timeout |
DRIFT_SENTINEL_WRITE_TIMEOUT |
15s |
HTTP write timeout |
DRIFT_SENTINEL_IDLE_TIMEOUT |
60s |
HTTP idle timeout |
DRIFT_SENTINEL_SHUTDOWN_TIMEOUT |
10s |
graceful shutdown timeout |
GET /healthzGET /metricsPOST /validateThe admission endpoint expects admission.k8s.io/v1 AdmissionReview requests and returns AdmissionReview responses.
The current metrics surface includes:
drift_sentinel_admission_requests_total{namespace,resource,result}drift_sentinel_violations_total{namespace,resource,field,mode}drift_sentinel_admission_duration_secondsdrift_sentinel_rules_loaded_totaldrift_sentinel_config_events_total{event_type}Admission decisions are logged as structured JSON, including:
cmd/server: process startup and HTTP serverpkg/admission: AdmissionReview handling and validation decisionspkg/diff: path parsing, filtering, and deep diffingpkg/rules: rule parsing, matching, watching, and namespace mode resolutioncharts/drift-sentinel: Helm deploymentdeploy/examples: example rule manifests