Modele de securite et isolation reseau de la microVM
Vue d'ensemble : voir
../ARCHITECTURE.md.
Modele de securite
- La seule surface d'attaque exposee par la microVM vers l'exterieur passe
par
net-proxy: c'est le seul point d'entree reseau que la VM peut joindre, jamaisidentity-proxynimcp-gatewaydirectement (voir ci-dessous). Aucun acces direct de la VM au reste du cluster. - Isolation memoire/noyau assuree par Firecracker (jailer, seccomp, cgroups) plutot que par la seule isolation de conteneur d'un Pod.
- Authentification externe : JWT emis par le fournisseur OIDC configure
(
ATELIER_OIDC_ISSUER_URL, Keycloak en dev), seule source de verite identite. Pas de gestion d'utilisateurs locale dans Atelier lui-meme.
Isolation reseau de la microVM : mecanisme concret
Il ne suffit pas d'affirmer "aucun acces direct au reste du cluster" au
niveau applicatif (allowlist de net-proxy) : sans application au niveau
paquet, rien n'empeche la VM d'ouvrir une connexion TCP brute vers l'IP du
pod (eth0), l'API server Kubernetes, un autre pod, ou un service de
metadata cloud, en contournant net-proxy entierement. Cible retenue,
deux composants distincts pour deux transports distincts, plus une
decision de design qui reduit net-proxy a l'unique destination que la VM
connaisse :
mcp-gateway: isolation structurelle, plus un alias HTTP optionnel vianet-proxy. Cible a terme : expose nativement viavsock(AF_VSOCK, adressage CID/port), pas sur le reseau IP de la VM — rien de ce qui transite par le tap reseau ne peut l'atteindre par ce chemin, et rien d'externe au couple hote/VM ne peut atteindre ce vsock. Limite assumee (comme la limite CONNECT/MITM d'identity-proxyci-dessous) : ce transportvsockn'est pas construit — voirdocs/PROGRESS.md, section "mcp-gateway: premier serveur MCP reel".net-proxyexpose en attendant l'alias HTTPmcp-gateway(ATELIER_MCP_GATEWAY_ADDR,crates/net-proxy/src/internal.rs, branche cotecrates/controller/src/reconcile.rs) — c'est aujourd'hui le seul chemin fonctionnel versmcp-gateway, pas une simple alternative pour les clients qui prefèrent HTTP/SSE. La garantie de securite recherchee (mcp-gateway jamais joint directement par la VM) tient malgre tout : ce chemin passe par le meme port quenet-proxy, pas par un port supplementaire a autoriser separement, exactement comme pouridentity-proxy(point 2 ci-dessous).identity-proxy: jamais joint directement par la VM, uniquement vianet-proxy. Decision de design (revisee) : la premiere version configuraitidentity-proxycomme un secondHTTP_PROXYque la VM pouvait joindre directement pour certains hotes, lui-meme chainant versnet-proxy. Rejetee : elle donnait a la VM une deuxieme destination reseau directe a autoriser au pare-feu, agrandissant la surface sans necessite, et cassait la garantie "net-proxy tranche toujours l'allowlist en premier" (identity-proxy aurait pu recevoir une requete qu'aucune allowlist n'avait encore validee). Design retenu : la VM ne configure qu'un seulHTTP_PROXY,net-proxy— c'est lui qui, apres avoir juge une destination autorisee, chaine la requete versidentity-proxy(ATELIER_IDENTITY_PROXY_ADDRcote net-proxy) si ce dernier est configure ;identity-proxydecide alors, selon ses propres regles (Workshop-scoped, pas connues de net-proxy), d'injecter un credential ou de relayer tel quel, puis se connecte directement a la destination finale — jamais en repassant parnet-proxy, ce qui boucleraient indefiniment puisque net-proxy chaine deja tout l'egress autorise vers identity-proxy. Limite connue : unCONNECT(HTTPS) est un tunnel TCP opaque, le contenu est chiffre bout-a-bout — identity-proxy ne peut donc pas y injecter d'en-tete sans devenir un MITM TLS actif, ce qui n'est pas fait ; l'injection ne fonctionne que pour les requetes HTTP en clair relayees en forme absolue.- Application au niveau paquet : pare-feu sur le device TAP de la
VM. La VM recoit une seule interface reseau (le TAP link-local
/30deja implemente danscrates/firecracker/src/network.rs, ex. hote169.254.0.1, guest169.254.0.2) et une route par defaut vers l'IP hote. Comme tous les conteneurs d'un meme pod partagent une seule et meme network namespace,net-proxylie sur0.0.0.0est deja joignable depuis la VM a169.254.0.1:<son port>sans aucun NAT ni forwarding — c'est de la livraison locale dans la meme netns. Il ne faut donc pas reutiliser tel quel leMASQUERADEinconditionnel desetup_link_local_tap(legitime pour la microVM "builder" deimage-builder, qui a explicitement besoin de sortir vers un registre OCI/depot git quelconque, elle-meme isolee autrement — voir "Reseau kind ↔ registre" dansPROGRESS.md) : pour la VM de l'agent, la sortie doit rester fermee par defaut. - Ne pas poser de regle
MASQUERADE/FORWARD -j ACCEPTverseth0pour ce TAP : sans route de sortie, un paquet vers une destination autre que169.254.0.1est simplement injoignable. - Poser explicitement, en defense en profondeur (le sysctl
net.ipv4.ip_forwardest global au netns du pod — une autre microVM du meme pod qui l'active ne doit pas rouvrir cette voie par accident) :(chaine dediee par VM, nettoyee auiptables -N atelier-vm-<id> iptables -A atelier-vm-<id> -p tcp -d 169.254.0.1 --dport <port net-proxy> -j ACCEPT iptables -A atelier-vm-<id> -p udp -d 169.254.0.1 --dport 53 -j ACCEPT iptables -A atelier-vm-<id> -p tcp -d 169.254.0.1 --dport 53 -j ACCEPT iptables -A atelier-vm-<id> -j DROP iptables -A INPUT -i <tap> -j atelier-vm-<id> iptables -A FORWARD -i <tap> -j DROPteardown(), symetrique a ce que fait dejaNetworkSetup::teardownpour la regle NAT de la VM "builder"). Une seule ligneACCEPTpour un port applicatif — celui denet-proxy— puisqueidentity-proxyet l'aliasmcp-gatewayne sont plus atteints qu'a travers lui. Le port de controle denet-proxy(ATELIER_NET_PROXY_CONTROL_ADDR, le websocket/portforwarddestine aapi-server) reste hors de cette liste : la VM ne doit jamais pouvoir l'atteindre, seulapi-serverle peut, depuis l'exterieur du pod. - Consequence assumee du DNS relaye par
net-proxy: pas de resolveur DNS ouvert vers l'exterieur pour la VM en dehors denet-proxylui-meme, qui applique la meme allowlist que l'egress HTTP(S) (un nom refuse recoitREFUSEDsans jamais atteindre l'upstream —crates/net-proxy/src/dns.rs) et filtre en plus les requetes a questions multiples et les typesANY/AXFR/IXFR, memes pour un nom autorise.
Fait : vm-supervisor cree desormais ce TAP (crates/vm-supervisor/src/main.rs,
setup_link_local_tap + NetworkSetup::restrict_to_net_proxy) et boote la
VM avec. Le guest n'a pas d'init personnalise (contrairement a la microVM
"builder") : l'adresse/route par defaut sont posees par le noyau lui-meme
via le parametre de boot ip=<guest>::<host>:<masque>::eth0:off
(autoconfiguration IP standard Linux, ne necessite aucune cooperation de
l'init du guest) — verifie reellement (IP-Config: Complete: device=eth0,
ipaddr=169.254.0.2, mask=255.255.255.252, gw=169.254.0.1), de meme que les
regles iptables (iptables -S atelier-vm-<tap> confirme les ACCEPT
port net-proxy/DNS puis le DROP final).
Ferme : passerelle transparente, zero configuration interne au guest.
L'idee initialement envisagee ici (injecter HTTP_PROXY/HTTPS_PROXY/un
resolveur DNS dans l'image construite par image-builder) a ete
abandonnee : un vrai bug trouve en testant ministack-workshop en a
montre la limite avant meme d'etre construite — l'etape RUN apt-get
d'un Dockerfile, executee par envbuilder dans la microVM "builder",
n'herite jamais de HTTP_PROXY (contrairement au clone git et au push
registre, qui eux passent bien par cette variable), ce qui aurait rendu
la meme approche fragile une fois appliquee a la VM de l'agent, pour
n'importe quel devcontainer arbitraire fourni par l'utilisateur d'un
Workshop — jamais garanti de respecter ces variables.
Solution retenue a la place : net-proxy devient une passerelle
transparente — le guest n'a besoin d'absolument aucune configuration
reseau particuliere (ni HTTP_PROXY, ni resolveur DNS specifique), il n'a
meme pas besoin de savoir que net-proxy existe.
NetworkSetup::enable_transparent_gateway(crates/firecracker/src/network.rs) pose, en plus de la chainefilterexistante (inchangee), une chainenatdediee sur le TAP :iptables -t nat -N atelier-vm-nat-<tap> iptables -t nat -A atelier-vm-nat-<tap> -p tcp --dport 80 -j REDIRECT --to-port <port HTTP transparent> iptables -t nat -A atelier-vm-nat-<tap> -p tcp --dport 443 -j REDIRECT --to-port <port TLS transparent> iptables -t nat -A atelier-vm-nat-<tap> -p udp --dport 53 -j REDIRECT --to-port 53 iptables -t nat -A atelier-vm-nat-<tap> -p tcp --dport 53 -j REDIRECT --to-port 53 iptables -t nat -A PREROUTING -i <tap> -j atelier-vm-nat-<tap>REDIRECTreecrit l'IP de destination vers celle de l'interface d'entree avant la decision de routage : le paquet devient une livraison locale (cheminINPUT), jamais un transitFORWARD—sysctl net.ipv4.ip_forwardreste a 0 comme avant,FORWARD -j DROPreste inchange pour tout ce qui n'est pas explicitement 80/443/53. Le raisonnement de la section precedente ("ne pas activerip_forward, risque partage entre VMs du meme netns") reste donc valide et n'est pas contourne par ce mecanisme.net-proxyecoute deux ports supplementaires (ATELIER_NET_PROXY_TRANSPARENT_HTTP_ADDR/_TLS_ADDR, defaut0.0.0.0:3180/:3181) : le port HTTP transparent reutilise tel quelhandle_connection(une requete origin-form +Host:y arrive deja dans le format attendu) ; le port TLS transparent lit le SNI duClientHelloen clair, sans jamais dechiffrer (crates/net-proxy/src/tls_sni.rs, meme principe quessl_prereadnginx/req.ssl_sniHAProxy), verifie l'allowlist, puis relaie les octets tels quels — aucun certificat, aucune confiance CA a gerer, la validation TLS de bout en bout reste intacte cote guest.- Le port 53 (DNS) est redirige selon le meme principe quel que soit
le serveur DNS que le guest croit utiliser (
REDIRECTmatche sur le port de destination, pas sur l'IP) — ferme le trou DNS mentionne ci-dessus sans configuration guest non plus. - La VM de l'agent avait deja une route par defaut vers
net-proxysans aucune configuration interne (ip=kernel, voir plus haut) : rien a changer cotevm-supervisorau-dela d'appelerenable_transparent_gatewaya la place derestrict_to_net_proxy. La VM "builder" (atelier-builder-vm-init, notre propre bootstrap, jamais le contenu du Workshop), elle, n'avait pas de route par defaut — une ligneip route add default via <host_ip>a ete ajoutee, seule "configuration interne" necessaire, et seulement a un composant de plateforme, jamais au devcontainer de l'utilisateur. - Verifie reellement contre le Workshop de demo
ministack-workshop(Dockerfile non modifie) :apt-get install systemd(viaarchive.ubuntu.com/security.ubuntu.com, HTTP transparent),deb.nodesource.com(HTTPS transparent, feature Node.js), et l'integralite du build (docker-in-docker, claude-code, code-server) aboutissent avec succes,imageDigestpublie et WorkshopRunning(crates/firecracker/tests/network.rs, nouveau testenables_transparent_redirect_without_touching_forward, verifie en plus le contenu exact des regles viaiptables -t nat -SsousCAP_NET_ADMINreel).