Durcissement réseau et DNS
Version anglaise : en/network-dns-hardening.md.
Principe
Chaque namespace part d'un refus total, en entrée comme en sortie, DNS compris. Chaque charge de travail ouvre ensuite uniquement les flux dont elle a besoin. Les règles du chart sont rendues par deploy/helm/enterprise-ai/templates/control-plane/networkpolicy.yaml (variante Cilium par défaut, variante NetworkPolicy Kubernetes avec global.networkPolicy=kubernetes). Les namespaces de données et de sécurité sont couverts par deploy/cilium/data-services.yaml, les politiques globales par deploy/cilium/baseline.yaml.
| Politique | Portée | Effet |
|---|---|---|
eai-default-deny | namespace enterprise-ai | ingress: [{}] et egress: [{}] : aucun flux, pas même DNS |
eai-deny-metadata | tous les pods du namespace | egressDeny vers 169.254.169.254/32, 169.254.0.0/16, fd00:ec2::254/128 |
eai-deny-cloud-metadata | tout le cluster (CiliumClusterwideNetworkPolicy) | même refus pour toutes les charges, quel que soit le namespace |
eai-deny-kube-apiserver | tous les pods sauf control-plane | egressDeny vers l'entité kube-apiserver |
default-deny | eai-data, openbao, eai-security | refus total, puis une politique par service |
Les règles egressDeny l'emportent toujours sur les règles d'autorisation : une faille SSRF dans une application ne permet pas de lire les identifiants d'instance (AWS IMDS, GCP, Azure, OpenStack).
Matrice des flux du chart
| Charge | Entrée autorisée | Sortie autorisée |
|---|---|---|
| control plane | APISIX, portail, observabilité sur 8080 | PostgreSQL, NATS, Redis, Qdrant, Temporal, LiveKit (eai-data), stockage S3 externe 443, OpenFGA, OPA, Keycloak (eai-security), OpenBao 8200, OTLP, Opik, ClickHouse, LiteLLM 4000, vLLM 8000, Ollama 11434, API Kubernetes, FQDN des fournisseurs sur 443 |
| worker de fond | aucune | PostgreSQL, NATS, Redis, OpenBao, OTLP |
| migration | aucune | PostgreSQL, OpenBao |
| portail | contrôleur d'ingress, APISIX sur 3000 | control plane, Keycloak, OTLP |
| LiteLLM | control plane, observabilité sur 4000 | vLLM, Ollama, PostgreSQL, Redis, OpenBao, OTLP, FQDN des fournisseurs sur 443 |
| vLLM | LiteLLM, control plane, observabilité sur 8000 | aucune (poids préchargés, HF_HUB_OFFLINE=1) |
| workers multimodaux | aucune | NATS, stockage S3 externe 443, OTLP |
Seuls le control plane et LiteLLM sortent vers Internet, par liste de FQDN (networkSecurity.providerFQDNs dans values.yaml, identique à deploy/cilium/egress-providers.yaml). Pour couper un sens, networkSecurity.providerEgress.controlPlane ou .litellm passe à false.
DNS : liste blanche L7 par charge
Le proxy DNS de Cilium applique, pour chaque charge, une liste matchName / matchPattern :
- noms internes limités aux namespaces réellement appelés, par exemple
*.eai-data.svc.cluster.local; - noms externes limités aux FQDN des fournisseurs, uniquement pour le control plane et LiteLLM ;
- vLLM n'a aucun accès DNS.
Une requête vers un autre nom reçoit REFUSED : un pod compromis ne peut ni résoudre un domaine arbitraire, ni exfiltrer des données en les encodant dans des sous-domaines (tunnel DNS). Cilium fait l'union des règles DNS de toutes les politiques qui sélectionnent un pod : aucune politique ne doit donc contenir matchPattern: "*". La politique eai-dns-visibility de baseline.yaml se limite désormais à *.svc.cluster.local, et le test test_platform_security_manifests.py refuse tout motif *.
Recommandation côté application : utiliser des noms pleinement qualifiés (nats.eai-data.svc.cluster.local), ce que fait values-prod.yaml. Avec des noms courts, le résolveur essaie les domaines de recherche, ces requêtes intermédiaires sont refusées et la résolution ralentit.
CoreDNS et NodeLocal DNSCache
- Pas de résolveur ouvert : le Service
kube-dnsreste enClusterIP, jamais exposé parLoadBalancerouNodePort(la politique Kyvernoeai-restrict-service-exposurebloque les NodePort horsingressetapisix). CoreDNS n'écoute que sur le réseau des pods. - Transfert amont restreint : dans le Corefile,
forward . <résolveurs de l'entreprise>avec des adresses explicites, jamais/etc/resolv.confdu nœud lorsque celui-ci pointe vers un résolveur public. Ajouterpolicy sequentialethealth_check 5s. - DNSSEC ou DoT vers le résolveur : soit le résolveur amont valide DNSSEC (Unbound
auto-trust-anchor-file, BINDdnssec-validation auto), soit CoreDNS parle DoT au résolveur :forward . tls://10.0.0.53 { tls_servername <dns-host> }. Ne pas activer le plugindnssecde CoreDNS (il signe, il ne valide pas). - Pas de récursion pour l'extérieur : bloc
cluster.localservi par le pluginkubernetesavecpods verified(et nonpods insecure), pour qu'une requêtea-b-c-d.ns.pod.cluster.localne réponde que pour un pod existant. - Journalisation : plugin
logfiltré surdenialeterror, exporté vers OpenSearch (indexeai-logs-*) par le collecteur OpenTelemetry ; le proxy DNS Cilium publie aussi les refus dans Hubble (hubble observe --verdict DROPPED --protocol dns). - NodeLocal DNSCache : si utilisé, la politique
eai-dns-visibilityet les règles des charges ciblentk8s-app: kube-dns. Avec NodeLocal DNSCache, ajouter le sélecteurk8s-app: node-local-dnsdans les mêmes règles (même port 53, mêmes règles L7) ; le cache local transfère vers CoreDNS en TCP et n'a pas d'autre amont. - Limitation de débit :
cache 30etratelimit(plugin externe) ou la limite de requêtes de NodeLocal DNSCache protègent CoreDNS d'un pod qui inonde le DNS.
Vérification hors ligne
helm template eai deploy/helm/enterprise-ai -n enterprise-ai -f deploy/helm/enterprise-ai/values-prod.yaml | kubeconform -strict -summary
services/control-plane/.venv/bin/python -m pytest services/control-plane/tests -q -k platform
Sur un cluster de test (jamais en production sans fenêtre de changement) : hubble observe --namespace enterprise-ai --verdict DROPPED doit montrer les refus attendus, et kubectl exec d'un pod portail vers 169.254.169.254 doit échouer.