ADR 0002 — Connecteur MCP Kubernetes en lecture seule
Statut : accepté, exposition distante par Cloudflare Access Service Auth
Issue : #75
Décision : utiliser d'abord containers/kubernetes-mcp-server derrière une
façade d'authentification, avec deux barrières indépendantes : filtrage MCP et
RBAC Kubernetes minimal.
Contexte
Les agents externes doivent pouvoir diagnostiquer le cluster sans recevoir le
kubeconfig administrateur. Un outil « lecture seule » reste risqué : les noms,
annotations et événements peuvent contenir des instructions malveillantes ou
des données privées, et certaines sous-ressources de lecture (nodes/proxy,
pods/log) donnent accès à beaucoup plus d'informations que leur verbe HTTP ne
le suggère.
Choix retenu pour le PoC
kubernetes-mcp-serverv0.0.66, épinglé au digestsha256:6d650f4bd6ac303ad82713c997e73a2d001602f9bf17392c9b9a0e30e29c6423;- seul le toolset
coreest chargé ; read_only = true, outils d'exécution et de logs désactivés ;Secret,ConfigMap,ServiceAccountet RBAC refusés dans la configuration ;- ServiceAccount et ClusterRole par liste blanche, sans rôle Kubernetes
view; - aucune permission
create,update,patch,delete,bind,impersonateouescalate; - aucune permission sur
pods/log,pods/exec,pods/attach,nodes/proxy,services/proxyou les tokens de ServiceAccount ; - aucun Ingress ; Cloudflare Tunnel sortant avec deux connecteurs et NetworkPolicy limitant l'entrée MCP à ces connecteurs, avec egress serveur limité à
CoreDNS, au Service Kubernetes sur
10.43.0.1:443et aux control-planes sur leur port6443; - ConfigMap généré avec suffixe de hash pour garantir un rollout à chaque changement de la configuration de sécurité ;
- limites HTTP, ressources du conteneur et contexte de sécurité restrictifs.
Le dossier apps/ovh-primary/cluster-observer est référencé par la
Kustomization racine pour valider le runtime dans le cluster réel. Cette
activation expose mcp.djayzone.com sans publier l'origine : le Service reste
ClusterIP et Cloudflare Access exige un Service Token distinct par agent.
Modèle de menace
| Menace | Barrière prévue | Preuve attendue avant activation |
|---|---|---|
| Prompt injection dans un objet ou événement | Instructions serveur et traitement comme données non fiables | scénario de test injectant une annotation hostile |
| Lecture d'un Secret | ressource refusée par MCP et absente du RBAC | auth can-i = no et appel MCP refusé |
| Commande ou mutation | mode read-only, outils désactivés, RBAC sans verbes d'écriture | tests négatifs create/patch/delete/exec |
| Accès Kubelet indirect | aucune permission nodes/proxy; outils node log/stats désactivés |
auth can-i get nodes/proxy = no |
| Exfiltration par logs ou ConfigMap | outils logs désactivés, ressources non autorisées | appel MCP refusé et RBAC = no |
| Valeurs sensibles écrites directement dans une spec Pod | risque résiduel documenté : get/list pods expose les variables d'environnement littérales, même sans droit sur les Secrets |
audit des specs avant activation, suppression des valeurs sensibles inline et tests sur les réponses MCP |
| Exfiltration réseau après lecture d'un objet hostile | deny ingress et egress limité à DNS + API Kubernetes | test réseau négatif vers Internet et les autres namespaces |
| Vol de l'identité MCP | identité par client, expiration, rotation et révocation | test de révocation sans redéploiement applicatif |
| Déni de service | limites de corps, rate limit, timeouts et ressources bornées | test de surcharge contrôlé |
| Contournement de GitOps | aucun outil d'écriture ou de réconciliation | absence des outils et permissions Flux d'écriture |
Matrice outils et RBAC
| Besoin | Outil MCP | Permission Kubernetes | Décision |
|---|---|---|---|
| Pods non prêts | pods_list, resources_get |
get/list/watch pods,pods/status |
autorisé |
| Événements récents | events_list |
get/list/watch events |
autorisé, contenu non fiable |
| Workloads | resources_list/get |
lecture apps et batch |
autorisé |
| Flux | resources_list/get |
lecture des CRD Flux sélectionnées | autorisé |
| Certificats | resources_list/get |
lecture cert-manager et ACME | autorisé |
| Capacité nœuds | resources_list/get, nodes_top |
lecture nodes et metrics.k8s.io |
autorisé sans proxy Kubelet |
| Logs | pods_log |
get pods/log |
refusé en V1 |
| Kubelet | nodes_log, nodes_stats_summary |
get nodes/proxy |
refusé |
| Configuration | ressources ConfigMap/Secret/RBAC | permissions correspondantes | refusé |
| Remédiation | create/update/delete/exec/Flux reconcile | verbes d'écriture | refusé |
Authentification distante
Cloudflare Access applique une politique Service Auth. Chaque agent reçoit son
couple CF-Access-Client-Id / CF-Access-Client-Secret, révocable séparément.
Les valeurs restent hors Git et le token du tunnel est provisionné directement
dans un Secret Kubernetes. Aucun jeton partagé permanent n'est acceptable.
L'authentification de l'utilisateur MCP et l'identité Kubernetes du serveur sont
deux responsabilités distinctes : le serveur conserve toujours son RBAC minimal.
Étapes suivantes
- rendre le PoC hors ligne et vérifier sa configuration — terminé ;
- lancer le workload interne et inventorier exactement
tools/list; - ajouter des tests négatifs automatisés pour chaque interdiction ;
- tester Cloudflare Access avec les clients réels ;
- ajouter une supervision externe qui ne requiert pas de secret partagé.
Retour arrière
Retirer cloudflared.yaml coupe l'exposition sans publier l'origine. Révoquer le
Service Token concerné coupe un seul agent ; la rotation du token de tunnel
invalide les connecteurs existants.