Introduction
Les webhooks d'admission sont un point d'extension puissant de Kubernetes qui permet d'intercepter les requêtes API avant la persistance des objets. Ils permettent la validation, la mutation et l'application de politiques que les contrôleurs d'admission intégrés ne peuvent pas gérer. Pourtant, de nombreux opérateurs peinent à passer d'une idée vague à une configuration de production vérifiée.
Cet article est une exploration pratique approfondie des concepts avancés des webhooks d'admission. Vous apprendrez à déployer un serveur de webhook, à le configurer pour valider ou muter des ressources, à gérer les modes de défaillance et à résoudre les problèmes courants. Nous couvrons les aspects internes, l'architecture et des exemples concrets avec les commandes et les sorties attendues.
L'objectif est la sécurité opérationnelle : observer avant de modifier, limiter le rayon d'impact, utiliser des espaces réservés au lieu de secrets, vérifier le résultat et documenter comment récupérer si l'état attendu n'est pas atteint.
Inventaire des versions et de l'environnement
Avant de toucher aux webhooks, confirmez la version de votre cluster et ses capacités. Les webhooks d'admission sont GA depuis Kubernetes 1.9 et fonctionnent sur tout cluster conforme. Vérifiez la version du serveur d'API et les plugins d'admission activés :
kubectl version --short
# Exemple de sortie :
# Client Version: v1.28.2
# Server Version: v1.28.2
kubectl get --raw /apis/admissionregistration.k8s.io/v1 | jq .
# Ceci devrait retourner les ressources API incluant validatingwebhookconfigurations et mutatingwebhookconfigurations
Le serveur de webhook peut s'exécuter à l'intérieur du cluster ou à l'extérieur. S'il est à l'intérieur, il doit être joignable par le serveur d'API via un Service. Vérifiez que le Service et ses endpoints existent :
kubectl get svc webhook-service -n webhook-system
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# webhook-service ClusterIP 10.96.100.10 <none> 443/TCP 5m
kubectl get endpoints webhook-service -n webhook-system
# NAME ENDPOINTS AGE
# webhook-service 172.17.0.5:8443 5m
Si l'endpoint est vide, vos pods de serveur de webhook ne sont pas prêts ou les étiquettes ne correspondent pas au sélecteur du Service. Inspectez les pods :
kubectl get pods -n webhook-system -o wide
# NAME READY STATUS RESTARTS AGE IP NODE
# webhook-server-6f6d5b7c9-abcde 1/1 Running 0 5m 172.17.0.5 kind-worker
Si un pod est en attente, décrivez-le pour voir les problèmes de planification :
kubectl describe pod webhook-server-6f6d5b7c9-abcde -n webhook-system
# Events:
# Type Reason Age From Message
# ---- ------ ---- ---- -------
# Warning FailedScheduling 2m default-scheduler 0/3 nodes are available: 3 Insufficient cpu.
Avant d'appliquer toute configuration de webhook, prenez un instantané des configurations existantes pour pouvoir revenir en arrière :
kubectl get validatingwebhookconfigurations -o yaml > validating-backup.yaml
kubectl get mutatingwebhookconfigurations -o yaml > mutating-backup.yaml
Stockez ces sauvegardes à l'extérieur du cluster si possible. Si votre webhook casse le cluster, vous devrez peut-être supprimer la configuration en urgence (voir Modes de défaillance).
Configuration du serveur de webhook
Un serveur de webhook est simplement un serveur HTTPS qui traite les requêtes AdmissionReview. Il doit présenter un certificat TLS signé par une CA de confiance pour le serveur d'API. Le champ caBundle dans la configuration du webhook doit contenir ce certificat CA au format PEM, encodé en base64.
Une approche simple consiste à utiliser une CA auto-signée et à générer des certificats pour le serveur. Vous pouvez utiliser openssl ou cert-manager. Pour les tests, utilisez un outil local comme kind ou minikube où vous pouvez créer un certificat et modifier facilement la configuration du webhook. Voici un squelette minimal de serveur de webhook en Go :
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
admissionv1 "k8s.io/api/admission/v1"
)
func handleValidate(w http.ResponseWriter, r *http.Request) {
body, _ := ioutil.ReadAll(r.Body)
var admissionReview admissionv1.AdmissionReview
json.Unmarshal(body, &admissionReview)
// Construire la réponse
admissionResponse := &admissionv1.AdmissionResponse{
UID: admissionReview.Request.UID,
Allowed: true,
}
response := admissionv1.AdmissionReview{
TypeMeta: admissionv1.TypeMeta{
APIVersion: "admission.k8s.io/v1",
Kind: "AdmissionReview",
},
Response: admissionResponse,
}
respBytes, _ := json.Marshal(response)
w.Write(respBytes)
}
func main() {
http.HandleFunc("/validate", handleValidate)
http.ListenAndServeTLS(":8443", "/tls/tls.crt", "/tls/tls.key", nil)
}
Ce serveur autorise chaque requête. Un vrai serveur inspectera l'objet de la requête, appliquera des politiques et retournera un booléen Allowed et éventuellement des Warnings ou un message de Status.
Configuration d'un ValidatingWebhook
Une ValidatingWebhookConfiguration indique au serveur d'API quelles requêtes envoyer à votre webhook. Voici un exemple qui valide que chaque pod dans le namespace production a une étiquette app :
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: require-app-label
webhooks:
- name: require-app-label.example.com
clientConfig:
service:
namespace: webhook-system
name: webhook-service
path: "/validate"
port: 443
caBundle: <certificat CA encodé en base64>
rules:
- operations: ["CREATE", "UPDATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
scope: "Namespaced"
namespaceSelector:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: In
values: ["production"]
admissionReviewVersions: ["v1"]
sideEffects: None
failurePolicy: Fail
Appliquez-le :
kubectl apply -f validating-webhook.yaml
Testez ensuite avec un pod sans l'étiquette :
kubectl run nginx --image=nginx -n production
# Error from server (Forbidden): error when creating "STDIN": admission webhook "require-app-label.example.com" denied the request: missing app label
Si le serveur de webhook est hors service et que failurePolicy est Fail, toutes les créations de pods dans le namespace production seront bloquées. C'est pourquoi on utilise souvent failurePolicy: Ignore en production, mais cela peut autoriser des objets invalides. Considérez toujours le compromis.
Configuration d'un MutatingWebhook
Une MutatingWebhookConfiguration modifie les objets avant leur persistance. Un usage courant est d'injecter des valeurs par défaut ou des conteneurs side-car. Voici un exemple qui ajoute une étiquette team à chaque pod si elle est absente :
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: add-team-label
webhooks:
- name: add-team-label.example.com
clientConfig:
service:
namespace: webhook-system
name: webhook-service
path: "/mutate"
port: 443
caBundle: <certificat CA encodé en base64>
rules:
- operations: ["CREATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
namespaceSelector:
matchLabels:
webhook-enabled: "true"
admissionReviewVersions: ["v1"]
sideEffects: None
reinvocationPolicy: IfNeeded
failurePolicy: Ignore
Dans votre code de webhook de mutation, vous décodez l'objet entrant, le modifiez et retournez un JSONPatch :
patch := []map[string]interface{}{
{
"op": "add",
"path": "/metadata/labels/team",
"value": "platform",
},
}
patchBytes, _ := json.Marshal(patch)
admissionResponse.Patch = patchBytes
admissionResponse.PatchType = new(admissionv1.PatchType)
*admissionResponse.PatchType = admissionv1.PatchTypeJSONPatch
Après application, créez un pod dans un namespace étiqueté webhook-enabled=true :
kubectl create ns test
kubectl label ns test webhook-enabled=true
kubectl run nginx --image=nginx -n test
kubectl get pod nginx -n test --show-labels
# NAME READY STATUS RESTARTS AGE LABELS
# nginx 1/1 Running 0 5s team=platform
Concepts avancés : correspondance, délai d'attente et effets de bord
Les configurations de webhook permettent une correspondance fine à l'aide de objectSelector pour filtrer selon les étiquettes de l'objet. Par exemple, pour valider uniquement les pods avec l'étiquette env=prod :
objectSelector:
matchLabels:
env: prod
Le champ timeoutSeconds (défaut 10, max 30) contrôle combien de temps le serveur d'API attend la réponse de votre webhook. Un webhook lent peut retarder toutes les requêtes correspondantes. Définissez-le explicitement :
timeoutSeconds: 5
Testez avec un webhook lent : simulez un délai de 10 secondes. Vous verrez les journaux du serveur d'API comme :
E1110 10:00:00.123456 1 dispatcher.go:200] failed calling webhook "slow.example.com": Post "https://webhook-service.webhook-system.svc:443/validate": context deadline exceeded
Définissez correctement sideEffects. None signifie que le webhook ne fait pas de changements sortants qui doivent être pris en compte lors des simulations (dry-run). Si votre webhook appelle des systèmes externes, utilisez sideEffects: Some ou Unknown. Le serveur d'API utilise cela pour décider s'il autorise les requêtes dry-run.
Vérification et diagnostics
La vérification du comportement du webhook est essentielle. Voici une approche systématique.
Premièrement, confirmez que la configuration du webhook a été créée correctement :
kubectl get validatingwebhookconfiguration require-app-label -o yaml
# Vérifiez que caBundle est rempli, que les règles correspondent et que failurePolicy est comme prévu.
Vérifiez les journaux du serveur de webhook pour les requêtes entrantes :
kubectl logs deployment/webhook-server -n webhook-system --tail=50
# Cherchez les requêtes d'admission avec UID et informations sur l'objet.
Si le webhook n'est pas appelé, vérifiez le sélecteur de namespace et le sélecteur d'objet. Vous pouvez utiliser kubectl api-resources pour vérifier les groupes API et les ressources.
Activez la journalisation d'audit sur le serveur d'API si possible. Les appels de webhook d'admission apparaissent dans les journaux d'audit avec des annotations incluant des informations sur admissionwebhook.
Pour une requête refusée, le serveur d'API retourne un message d'erreur clair provenant du status.message du webhook. Si le message n'est pas utile, améliorez les réponses d'erreur de votre webhook.
Utilisez kubectl auth can-i pour simuler les permissions ? Pas directement pour les webhooks, mais vous pouvez créer un objet de test avec une simulation :
kubectl apply -f test-pod.yaml --dry-run=server
# Si la simulation échoue à cause du webhook, le webhook a été invoqué avec succès.
Modes de défaillance et récupération
Les webhooks peuvent casser votre cluster s'ils sont mal configurés. Les modes de défaillance courants incluent :
- Serveur de webhook indisponible : Si le serveur est hors service et
failurePolicy: Fail, les requêtes vers les ressources correspondantes sont bloquées. Récupération : définissez temporairementfailurePolicy: Ignoreou supprimez la configuration du webhook.
kubectl delete validatingwebhookconfiguration require-app-label
Mais attention : supprimer le webhook autorise des objets invalides. Ayez un correctif prêt.
- Non-concordance ou expiration du certificat CA : Si le
caBundleest incorrect ou expiré, le serveur d'API ne peut pas vérifier le certificat du serveur de webhook. Vérifiez les journaux du serveur d'API pourx509: certificate signed by unknown authority. Renouvelez le certificat et mettez à jour lecaBundledans toutes les configurations de webhook.
- Délai d'attente : Si le webhook prend trop de temps, les requêtes échouent avec des erreurs de délai. Augmentez
timeoutSecondsou optimisez le code du webhook. Surveillez la latence du webhook à l'aide de métriques.
- RBAC manquant pour le serveur de webhook : Si le serveur de webhook a besoin de lire des ressources, assurez-vous que son ServiceAccount a les RBAC appropriés. Testez en vérifiant les journaux du serveur de webhook pour les erreurs
forbidden.
- Non-concordance des ports : Assurez-vous que le targetPort du Service correspond au port du conteneur et au port de la configuration du webhook. Utilisez
kubectl describepour vérifier.
- Mauvaise configuration du sélecteur de namespace : Si vous utilisez
namespaceSelector, testez avec un namespace qui correspond. Utilisezkubectl label namespacepour ajouter des étiquettes.
Les étapes de récupération doivent être planifiées avant le déploiement. Sauvegardez toujours les configurations existantes et entraînez-vous à supprimer un webhook problématique pour restaurer rapidement la fonctionnalité du cluster.
Liste de vérification opérationnelle
Avant de déployer un webhook en production, passez en revue cette liste :
- [ ] Le serveur de webhook s'exécute en tant que Deployment avec au moins 2 répliques pour la haute disponibilité.
- [ ] Un PodDisruptionBudget est défini pour assurer la disponibilité pendant les drainages de nœuds.
- [ ] La NetworkPolicy autorise le serveur d'API à atteindre le Service du webhook (port 443).
- [ ] La validité du certificat TLS est surveillée (par exemple, via cert-manager ou des alertes personnalisées).
- [ ]
failurePolicyest explicitement défini et documenté. Pour une validation critique, utilisezFail; pour une mutation non critique, utilisezIgnore. - [ ]
timeoutSecondsest ajusté pour éviter des retards inutiles dans les requêtes. - [ ] La configuration du webhook est versionnée dans Git et appliquée via CI/CD.
- [ ] La procédure de retour en arrière est testée : la suppression de la configuration du webhook restaure le comportement normal de l'API en quelques secondes.
- [ ] Les journaux et les métriques sont collectés depuis le serveur de webhook pour le dépannage.
- [ ] Le webhook d'admission est inclus dans les exercices de reprise après sinistre.
Après le déploiement, vérifiez avec une série de requêtes de test, à la fois valides et invalides, pour vous assurer que le webhook se comporte comme prévu.
Pièges courants et comment les éviter
De nombreuses équipes rencontrent les mêmes problèmes avec les webhooks. Voici les principaux pièges et leurs solutions.
Piège 1 : Bloquer toutes les ressources en raison de règles trop larges.
Si vous définissez rules avec resources: ["*"] et failurePolicy: Fail, une défaillance du webhook peut rendre le cluster inutilisable, vous empêchant même de supprimer la configuration du webhook elle-même. Évitez les règles larges. Ciblez plutôt des ressources et des namespaces spécifiques. Si vous devez utiliser des règles larges, définissez failurePolicy: Ignore et surveillez les erreurs.
Piège 2 : Oublier de mettre à jour caBundle après la rotation des certificats.
Les certificats expirent. Configurez un renouvellement automatisé et mettez à jour les configurations de webhook. Utilisez des outils comme cert-manager avec un injecteur de CA, ou un contrôleur qui modifie les configurations de webhook. Sinon, vous verrez des erreurs x509: certificate expired.
Piège 3 : Boucles de mutation.
Si un webhook de mutation modifie un objet d'une manière qui déclenche le même webhook à nouveau, vous pouvez créer une boucle infinie. Kubernetes a une sauvegarde : reinvocationPolicy et il évite de ré-invoquer le même webhook sur le même objet si l'objet n'a pas changé ? En fait, le serveur d'API suit les mutations et peut ré-invoquer si d'autres webhooks ont changé l'objet, mais un webhook qui continue de modifier le même objet peut provoquer des patchs répétés. Assurez-vous que votre webhook est idempotent : vérifiez si l'état souhaité existe déjà avant de patcher.
Piège 4 : Ignorer le support dry-run.
Si votre webhook a des effets de bord, comme appeler une API externe, vous devez définir sideEffects: Some et gérer les requêtes dry-run. Sinon, les opérations dry-run pourraient provoquer de vrais effets de bord. Dans votre code de webhook, vérifiez admissionReview.Request.DryRun et ignorez les actions à effet de bord si vrai.
Piège 5 : Ne pas définir les demandes et limites de ressources sur le serveur de webhook.
Le serveur de webhook est sur le chemin des requêtes. S'il manque de CPU ou de mémoire, la latence d'admission augmente. Définissez des demandes et limites appropriées, et surveillez avec HPA.
Piège 6 : Utiliser des certificats auto-signés sans distribution correcte de la CA.
En production, utilisez une PKI appropriée. Si vous utilisez des certificats auto-signés, assurez-vous que la CA est stockée de manière sécurisée et incluse dans la configuration du webhook. Évitez de copier la CA depuis une machine locale sans versionnage.
En anticipant ces pièges, vous pouvez concevoir un système de webhook robuste et maintenable.
Conclusion
Les webhooks d'admission sont essentiels pour appliquer les politiques Kubernetes et automatiser la mutation des ressources. Cet article a parcouru la configuration d'un serveur de webhook, la configuration des webhooks de validation et de mutation, la gestion des modes de défaillance et l'évitement des erreurs courantes.
Comme prochaine étape, choisissez une règle de validation à faible risque, déployez un webhook dans un cluster de test et suivez la liste de vérification. Enregistrez l'état actuel, appliquez le webhook, testez avec des ressources autorisées et refusées, et assurez-vous de pouvoir revenir en arrière rapidement.
Une implémentation fiable de webhook rend la défaillance visible, protège les données sensibles, limite les changements aux ressources prévues et définit les étapes de récupération avant qu'un incident ne force la décision.