Aller au contenu principal

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.

PolitiquePortéeEffet
eai-default-denynamespace enterprise-aiingress: [{}] et egress: [{}] : aucun flux, pas même DNS
eai-deny-metadatatous les pods du namespaceegressDeny vers 169.254.169.254/32, 169.254.0.0/16, fd00:ec2::254/128
eai-deny-cloud-metadatatout le cluster (CiliumClusterwideNetworkPolicy)même refus pour toutes les charges, quel que soit le namespace
eai-deny-kube-apiservertous les pods sauf control-planeegressDeny vers l'entité kube-apiserver
default-denyeai-data, openbao, eai-securityrefus 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​

ChargeEntrée autoriséeSortie autorisée
control planeAPISIX, portail, observabilité sur 8080PostgreSQL, 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 fondaucunePostgreSQL, NATS, Redis, OpenBao, OTLP
migrationaucunePostgreSQL, OpenBao
portailcontrôleur d'ingress, APISIX sur 3000control plane, Keycloak, OTLP
LiteLLMcontrol plane, observabilité sur 4000vLLM, Ollama, PostgreSQL, Redis, OpenBao, OTLP, FQDN des fournisseurs sur 443
vLLMLiteLLM, control plane, observabilité sur 8000aucune (poids préchargés, HF_HUB_OFFLINE=1)
workers multimodauxaucuneNATS, 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-dns reste en ClusterIP, jamais exposé par LoadBalancer ou NodePort (la politique Kyverno eai-restrict-service-exposure bloque les NodePort hors ingress et apisix). 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.conf du nœud lorsque celui-ci pointe vers un résolveur public. Ajouter policy sequential et health_check 5s.
  • DNSSEC ou DoT vers le résolveur : soit le résolveur amont valide DNSSEC (Unbound auto-trust-anchor-file, BIND dnssec-validation auto), soit CoreDNS parle DoT au résolveur : forward . tls://10.0.0.53 { tls_servername <dns-host> }. Ne pas activer le plugin dnssec de CoreDNS (il signe, il ne valide pas).
  • Pas de récursion pour l'extérieur : bloc cluster.local servi par le plugin kubernetes avec pods verified (et non pods insecure), pour qu'une requête a-b-c-d.ns.pod.cluster.local ne réponde que pour un pod existant.
  • Journalisation : plugin log filtré sur denial et error, exporté vers OpenSearch (index eai-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-visibility et les règles des charges ciblent k8s-app: kube-dns. Avec NodeLocal DNSCache, ajouter le sélecteur k8s-app: node-local-dns dans 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 30 et ratelimit (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.