## 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 :

```bash
kubectl version --short
# Exemple de sortie :
# Client Version: v1.28.2
# Server Version: v1.28.2
```

```bash
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 :

```bash
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 :

```bash
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 :

```bash
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 :

```bash
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 :

```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` :

```yaml
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 :

```bash
kubectl apply -f validating-webhook.yaml
```

Testez ensuite avec un pod sans l'étiquette :

```bash
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 :

```yaml
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 :

```go
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` :

```bash
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` :

```yaml
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 :

```yaml
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 :

```bash
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 :

```bash
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 :

```bash
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 :

1. **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 temporairement `failurePolicy: Ignore` ou supprimez la configuration du webhook.

```bash
kubectl delete validatingwebhookconfiguration require-app-label
```

Mais attention : supprimer le webhook autorise des objets invalides. Ayez un correctif prêt.

2. **Non-concordance ou expiration du certificat CA** : Si le `caBundle` est 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 pour `x509: certificate signed by unknown authority`. Renouvelez le certificat et mettez à jour le `caBundle` dans toutes les configurations de webhook.

3. **Délai d'attente** : Si le webhook prend trop de temps, les requêtes échouent avec des erreurs de délai. Augmentez `timeoutSeconds` ou optimisez le code du webhook. Surveillez la latence du webhook à l'aide de métriques.

4. **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`.

5. **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 describe` pour vérifier.

6. **Mauvaise configuration du sélecteur de namespace** : Si vous utilisez `namespaceSelector`, testez avec un namespace qui correspond. Utilisez `kubectl label namespace` pour 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).
- [ ] `failurePolicy` est explicitement défini et documenté. Pour une validation critique, utilisez `Fail` ; pour une mutation non critique, utilisez `Ignore`.
- [ ] `timeoutSeconds` est 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.