Aller au contenu

Le filtre de challenge

Ce que le filtre de preuve de travail fait dans ce POC, comment il est pose, et ce qu'il faut savoir avant de s'appuyer dessus.

Origine

Le filtre est une reecriture en Rust du plugin Go kubewaf-io/pow-proxy-wasm (Apache-2.0). Le protocole sur le fil est identique : memes cookies, meme charge utile signee, memes jetons. Ce qui change est le code, pas le format, et les ecarts sont listes et motives dans le README de la crate.

Le code vit a deux endroits, ce qui est temporaire et a trancher :

Le format OCI etant la seule chose dont la couche de bord a besoin a l'execution, la cible propre est : le depot autonome comme source unique, cette couche ne gardant que l'URL de l'artefact et son empreinte.

Ce qu'il fait

Une requete sans preuve recoit un 403 avec une page de challenge et deux cookies signes (challenge, challenge-sig). Le navigateur resout une preuve de travail, presente la solution, et recoit un cookie de clearance (challenge-clearance) valable trente minutes. Les requetes suivantes passent sans nouveau calcul.

Le cout du calcul retombe sur le client, ce qui est l'interet de la brique : un robot qui ne resout pas la preuve consomme du calcul du bord, mais aucune ressource de l'application protegee.

Le detail du protocole est dans pow-envoy-wasm/docs/protocol.md. Ce document-ci ne couvre que le deploiement.

Ou il se pose

Par la ressource WasmPlugin d'Istio, declaree dans tofu/edge/wasm-plugin.tf :

  • cible : la passerelle entiere (targetRefs sur le Gateway). Istio accepte aussi un HTTPRoute, ce qui permet de ne proteger qu'une route ;
  • phase : AUTHN, donc avant l'authentification, et avant l'application ;
  • failStrategy : FAIL_OPEN. Si le module panique ou ne se charge pas, le trafic passe. Pour un anti-robot, c'est le bon compromis : une panne du filtre ne doit pas devenir une panne de service. Le prix est qu'un chargement rate se traduit par une protection absente, en silence, et non par une alerte ;
  • pluginConfig : la configuration JSON, lue par le module au demarrage.

Activer le filtre

make plugin-test          # 58 tests natifs, avant de publier quoi que ce soit
make plugin-wasm          # module + controle des imports + empreinte SHA-256
make plugin-oci           # artefact OCI local, verifiable sans registre
make plugin-publish       # poussee vers ghcr.io (oras login requis)

Puis dans tofu/edge/terraform.tfvars :

pow_enabled     = true
pow_wasm_url    = "oci://ghcr.io/guillaumeassier/pow-proxy-rust:0.1.0"
pow_wasm_sha256 = "<somme affichee par make plugin-wasm>"

et make apply-edge. Le filtre est desactive par defaut : rien ne change dans le plan tant que pow_enabled est faux.

Reglages

Variable Defaut Role
pow_base_difficulty 18 difficulte de depart, en bits de tete a zero
pow_min_difficulty 12 borne basse
pow_max_difficulty 26 borne haute
pow_client_ip_source source_address source de l'IP liee au jeton
pow_log_level info error, warn, info, debug, trace
pow_priority 10 ordre dans la phase
pow_fail_strategy FAIL_OPEN comportement en cas de panne du module
pow_secret engendre secret HMAC, 32 octets minimum

Difficulte. 18 bits demandent environ un quart de seconde a un poste de bureau, ce qui est invisible pour un humain et couteux pour un parc automatise. Le module ajuste la difficulte tout seul toutes les 5 secondes d'apres le nombre de challenges emis : plus la pression monte, plus la difficulte monte, dans les bornes configurees. Les seuils sont des heuristiques de depart, a ajuster sur du trafic reel.

Source de l'IP. source_address prend l'adresse du pair et ignore les en-tetes de transfert. C'est le bon reglage ici, parce que la passerelle termine elle-meme les connexions : un X-Forwarded-For peut etre ecrit par le client. Ne passe en auto que si un equipement de confiance est devant.

Secret. pow_secret est engendre si tu ne le fournis pas, et atterrit dans le state OpenTofu. Le changer invalide les cookies deja emis, ce qui est sans consequence. tofu output -show-sensitive pow_secret_fingerprint donne son empreinte, pour verifier que le state et le proxy utilisent le meme secret sans afficher le secret.

Verifier que le filtre tourne

# la configuration a-t-elle ete acceptee ?
kubectl -n edge logs deploy/istio-ingressgateway -c istio-proxy | grep pow-proxy

# le plugin est-il charge du cote du plan de controle ?
istioctl x wasm-status   # si istioctl est disponible

# la preuve de travail repond-elle ?
curl -i http://<edge_hostname>.<dns_zone>/ | head -20

Le module journalise au demarrage sa difficulte, sa source d'IP, la longueur du secret et les fenetres de validite. Si tu ne vois pas ces lignes, le filtre n'est pas charge : verifie l'URL de l'artefact et l'empreinte avant de chercher plus loin.

Execution reelle

Le module a tourne pour la premiere fois sur la passerelle le 19 octobre 2026, sur demo.guillaume.ink. Il est publie dans le registre de conteneurs Scaleway, en lecture anonyme, et tire par istiod comme n'importe quelle image.

# publication, avec les cles Scaleway deja chargees
make -C plugins/pow-proxy wasm-publish \
     IMAGE=rg.fr-par.scw.cloud/exoledge/pow-proxy-rust:0.1.3

Deux defauts sont apparus des la premiere execution, et aucun test natif ne pouvait les voir.

Le module paniquait a la premiere requete. Reponse 503 wasm_fail_stream, et dans le journal du proxy : missing ContextType on root_context. Le contexte racine ne declarait pas le type de ses contextes enfants, information dont le SDK a besoin quand aucun constructeur n'est enregistre. Corrige par fn get_type() sur RootContext.

La page de defi s'affichait en texte brut. Le filtre envoyait les cookies du challenge sans Content-Type. Un navigateur affichait alors le source de la page, le script ne s'executait pas, et un visiteur restait bloque devant du code. Un client en ligne de commande ne le voit pas : c'est un defaut invisible aux tests qui parlent HTTP.

Apres correction, la chaine complete a ete mesuree :

1. sans preuve          : 403, 8761 octets (page de defi)
2. charge utile         : diff=18 ctx=<ip du client>
3. nonce trouve         : 186508 en 0.13 s
4. solution presentee   : 200, clearance=oui
5. nouvelle connexion   : 200, "exoledge poc edge"

Un navigateur reel resout le defi seul et charge la page. Les trois issues se distinguent a la reponse : les deux pages de verification font une dizaine de kilo-octets, le refus sec aucun octet.

Le captcha ne menait nulle part. Sa page chargeait un composant ALTCHA servi par /__challenge/altcha.js, que rien ne servait : ni le filtre, ni le decideur, ni une route de la passerelle. La requete du composant arrivait donc sur le filtre, qui la defiait a son tour, et le visiteur restait devant une case a cocher qui ne repondait jamais. Un navigateur pilote, c'est-a-dire exactement le client que le scoring envoie vers cette issue, ne pouvait pas entrer.

La page ne depend plus d'aucune ressource externe : elle pose la question, resout le meme defi que la page de preuve de travail, et rend le nombre trouve au bord par le cookie challenge-nonce. Le defi est desormais emis aussi pour l'issue captcha, sinon la page n'avait rien a resoudre. Le clic ne remplace pas la preuve, il la precede : ce qui distingue un robot, ici, reste le calcul.

Deux contraintes decouvertes a l'usage.

La solution est liee a la connexion qui a recu le defi. Deux requetes separees ne suffisent pas : il faut garder la meme connexion TCP entre l'emission et la presentation, sinon la solution est refusee et un nouveau challenge est emis. C'est voulu, cela empeche de resoudre une preuve pour la faire presenter par un autre. tools/solve_challenge.py montre le client correct.

Le plugin ne connait aucune exclusion. Pose sur la passerelle entiere, il defie aussi /.well-known/acme-challenge/, donc l'autorite de certification, donc le renouvellement. Le transport exclut desormais ce chemin avant le moteur. Sans cette exclusion, le certificat expire au bout de soixante jours sans qu'aucune alerte ne se declenche.

Ce qui reste non verifie

  • aucune mesure de performance cote bord. Le cout cote client est connu, le cout du chemin rapide dans le proxy non.
  • aucun test de charge ni de comportement sous attaque reelle. La difficulte dynamique est une heuristique, pas un mecanisme valide en production.
  • la clearance n'a pas ete eprouvee en expiration ni en rotation de secret sur un trafic vivant.