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 :
plugins/pow-proxy/dans ce depot, pour iterer sans changer de contexte ;- github.com/GuillaumeASSIER/pow-envoy-wasm, depot autonome qui publie l'artefact OCI.
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 (
targetRefssur leGateway). Istio accepte aussi unHTTPRoute, 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.