Cloudflare Access 2026 : la technique Zero Trust que peu de devs connaissent (Identity-Aware Proxy + One-Time PIN)
Votre page de login est du code que vous exécutez pour des inconnus, des milliers de fois par jour. En 2026, avec des bots pilotés par l'IA qui testent des identifiants volés à la chaîne, c'est la mauvaise architecture. Cloudflare Access place un Identity-Aware Proxy devant votre application : l'authentification se fait avant votre code, pas dedans. Tutoriel complet avec One-Time PIN, guard NestJS pour une API, validation JWT à l'origine et service tokens.

Il y a une fonctionnalité Cloudflare que presque personne n'utilise en dehors des grandes DSI, alors qu'elle est gratuite jusqu'à 50 utilisateurs et qu'elle se configure en une quinzaine de minutes. Elle ne protège pas votre application contre les attaques. Elle fait mieux : elle fait en sorte que votre application ne soit jamais atteinte par un attaquant.
Le principe tient en une phrase : l'authentification se fait avant l'application, pas dedans. Votre code Django, Laravel ou Node n'est jamais exécuté pour un inconnu. Il ne peut donc être ni attaqué, ni exploité, ni même reconnu.
Cette nuance paraît mineure. En 2026, à l'ère des attaques industrialisées par l'IA, elle change tout.
Ce qui a changé en 2026 : on ne « casse » plus, on « se connecte »
Le rapport de threat intelligence 2026 de Cloudflare documente un basculement que tout développeur devrait avoir en tête. La formule des équipes de Cloudflare est parlante : les attaquants sont passés du breaking in au logging in. Ils ne cherchent plus à casser la porte, ils se connectent avec des clés valides.
Les chiffres sont brutaux :
- 94 % des tentatives de connexion observées proviennent de bots, pas d'humains.
- 63 % des connexions utilisent des identifiants déjà compromis ailleurs — un mot de passe fuité chez un autre service, rejoué chez vous.
- Le vol de jetons de session est identifié comme particulièrement dangereux, parce qu'il neutralise l'authentification multifacteur : le voleur arrive après l'étape MFA, il n'a pas à la franchir.
- Le record de DDoS atténué s'établit désormais à 31,4 Tbps.
Et la couche IA par-dessus : les attaquants utilisent l'IA générative pour cartographier un réseau en temps réel, écrire du code d'exploitation sur mesure et produire des deepfakes. Concrètement, un acteur peu compétent peut aujourd'hui monter une opération à fort impact, et un serveur exposé n'est plus scanné par un script bête mais par un agent qui adapte sa stratégie.
Le résultat pour vous : votre formulaire de login n'est plus une porte, c'est une cible permanente. Il est sollicité en continu par des machines qui testent des combinaisons issues de fuites réelles, à une cadence qu'aucun humain ne pourrait soutenir.
Le vrai problème : un formulaire de login, c'est du code exécuté pour des inconnus
Prenez une application Django avec un /admin protégé par mot de passe. Quand un bot appelle cette URL, voici ce qui se passe réellement sur votre serveur :
- Nginx accepte la connexion TCP et négocie le TLS.
- La requête est transmise à Gunicorn, puis à Django.
- Django charge les middlewares, ouvre une session, parse le formulaire.
- Django interroge la base de données pour trouver l'utilisateur.
- Django calcule un hash de mot de passe (PBKDF2 ou Argon2 — volontairement coûteux en CPU).
- Django renvoie « identifiants incorrects ».
À l'étape 6, vous avez déjà exécuté votre code métier, votre ORM, votre base de données et une fonction de hachage coûteuse pour quelqu'un dont vous savez qu'il n'a rien à faire là. Multipliez par quelques milliers de tentatives par jour et vous obtenez trois problèmes distincts :
- Une surface d'exploitation. Chaque ligne de code atteignable par un inconnu est une ligne qui peut contenir une faille — la vôtre, ou celle d'une dépendance dont la CVE sort demain matin.
- Une surface de reconnaissance. Le simple fait que votre serveur réponde différemment selon l'URL renseigne l'attaquant : quel framework, quelle version, quels chemins existent.
- Un coût. Le hachage de mot de passe est délibérément lent. C'est une excellente défense contre le vol de base, mais cela signifie aussi que chaque tentative de brute-force vous coûte du CPU réel.
Ajouter fail2ban, un rate-limit et un WAF améliore les choses, et c'est une bonne pratique — j'en parle dans le guide fail2ban pour serveur Ubuntu et dans le guide WAF 2026. Mais toutes ces défenses partagent le même défaut de conception : elles filtrent après que la requête a atteint votre code. Elles réduisent l'exposition, elles ne la suppriment pas.
Le principe : déplacer l'authentification avant l'application
L'idée est d'insérer, entre internet et votre serveur, un composant dont l'unique rôle est de répondre à une question : qui es-tu ? Tant que la réponse n'est pas satisfaisante, la requête n'est pas transmise. Elle meurt sur le réseau Cloudflare, à des milliers de kilomètres de votre VPS.
La différence avec un WAF est fondamentale, et elle mérite d'être posée clairement :
| WAF | Identity-Aware Proxy | |
|---|---|---|
| Question posée | Cette requête est-elle malveillante ? | Qui envoie cette requête ? |
| Méthode | Détection de motifs, scoring, heuristiques | Preuve d'identité vérifiable |
| Défaut par défaut | Autoriser, sauf si suspect | Refuser, sauf si prouvé |
| Faux négatif possible | Oui — une attaque inédite passe | Non — sans identité valide, rien ne passe |
| Votre code s'exécute | Oui, si le WAF laisse passer | Non, tant que l'identité n'est pas prouvée |
Le WAF est un videur qui reconnaît les fauteurs de trouble. L'Identity-Aware Proxy est un videur qui a la liste des invités. Face à des attaques générées par IA, dont la forme change à chaque tentative, la seconde approche est structurellement plus solide : on ne peut pas contourner par créativité une porte qui exige une identité vérifiable.
Le vocabulaire, du plus général au plus précis
Ce mécanisme porte plusieurs noms selon le niveau d'abstraction, et c'est une source de confusion permanente. Voici la hiérarchie exacte :
Zero Trust — le modèle général. On ne fait confiance à personne par défaut, même à l'intérieur du réseau. Chaque requête doit prouver son identité, à chaque fois. Il n'y a plus de « périmètre de confiance » : le réseau interne n'est pas plus fiable qu'internet.
ZTNA (Zero Trust Network Access) — la catégorie de produits qui appliquent ce modèle à l'accès aux applications. C'est le terme que vous verrez dans les analyses de marché et les appels d'offres, souvent opposé au VPN traditionnel.
Identity-Aware Proxy (IAP) — le nom technique exact du composant : un proxy inverse qui vérifie qui vous êtes avant de transmettre la requête à l'application. C'est ce terme que vous croiserez dans la documentation d'ingénierie.
Cloudflare Access — le nom commercial de l'implémentation chez Cloudflare.
Et la méthode d'authentification que nous allons utiliser ici s'appelle One-Time PIN (OTP), ou authentification sans mot de passe par code à usage unique.
Retenez surtout la logique : Zero Trust est la philosophie, ZTNA la catégorie, Identity-Aware Proxy le mécanisme, Cloudflare Access le produit.
D'où ça vient : Google, 2009, et la fin du périmètre
Ce n'est pas une mode de 2026. L'origine est datée et documentée.
En 2009, Google est victime d'Operation Aurora, une attaque avancée qui touche également une trentaine d'autres entreprises américaines. Les attaquants pénètrent le réseau interne, puis se déplacent latéralement de machine en machine, précisément parce que l'intérieur du réseau était considéré comme fiable. Une fois le périmètre franchi, tout était atteignable.
La réponse de Google s'appelle BeyondCorp, lancé en interne dès 2009 et publié en 2014. Le principe est radical pour l'époque : supprimer la notion de réseau d'entreprise privilégié. Les applications internes de Google sont déployées sur internet, et l'accès passe systématiquement par un proxy centralisé qui vérifie l'identité de l'utilisateur et l'état de son appareil. Plus de VPN, plus d'« intérieur » implicitement sûr.
En janvier 2018, Cloudflare lance Access avec un argument marketing assumé : l'équivalent de BeyondCorp, sans avoir besoin d'être employé de Google. C'est exactement ce que c'est — et c'est aujourd'hui disponible dans le plan gratuit.
One-Time PIN : l'authentification sans mot de passe, sans fournisseur d'identité
Cloudflare Access s'intègre avec Google Workspace, Microsoft Entra ID, Okta, GitHub, tout fournisseur SAML ou OIDC. Mais il embarque aussi un mécanisme intégré qui ne demande aucune intégration : le One-Time PIN.
Le fonctionnement est direct. L'utilisateur saisit son adresse e-mail. Si cette adresse est autorisée par une politique Access, Cloudflare lui envoie un code. Il saisit le code, il entre. Sinon, il ne reçoit rien.
Les détails opérationnels, tirés de la documentation officielle :
- Le code expire au bout de 10 minutes après la demande.
- Il est à usage unique : demander un nouveau code invalide immédiatement le précédent.
- L'expéditeur est
[email protected], domainenotify.cloudflare.com. Si votre organisation filtre les e-mails, mettez ce domaine et les IP104.30.16.2à104.30.16.7en liste verte. - Un utilisateur bloqué ne reçoit pas d'e-mail du tout — pas de fuite d'information sur l'existence du compte.
- Access ne journalise la tentative d'authentification qu'après saisie du code, pas à la demande d'envoi. C'est un point à connaître pour vos audits.
L'intérêt réel : il n'y a aucun mot de passe à stocker, à hacher, à faire tourner ou à se faire voler. Un identifiant volé dans une fuite ne sert à rien ici. Un bot qui teste des paires login/mot de passe non plus. La seule façon d'entrer est d'avoir accès à la boîte mail autorisée, à l'instant T, dans une fenêtre de 10 minutes.
Mise en pratique : protéger une application en 15 minutes
Le scénario : une application interne (Django admin, Grafana, Portainer, un back-office, un outil métier) tourne sur un VPS, et vous voulez qu'elle soit accessible depuis n'importe où, mais uniquement par trois personnes nommément désignées.
Étape 1 : fermer l'origine avec un tunnel
C'est le prérequis. Si votre serveur reste joignable directement par son IP publique, un attaquant peut contourner Cloudflare Access en tapant l'IP. La combinaison correcte est Cloudflare Tunnel + Cloudflare Access : le tunnel rend l'origine injoignable, Access filtre l'entrée par l'identité.
La procédure complète est détaillée dans le guide Cloudflare Tunnel pour sécuriser un VPS. En résumé : installez cloudflared, créez un tunnel, publiez le service local (http://localhost:8000) sur un hostname public, puis fermez tous les ports entrants avec UFW.
Si votre application est déjà publiquement routable et que vous ne voulez pas de tunnel, c'est possible — mais vous devez alors protéger l'IP d'origine autrement, sinon la protection est cosmétique.
Étape 2 : activer One-Time PIN comme fournisseur d'identité
Dans le tableau de bord Cloudflare, allez dans Zero Trust > Integrations > Identity providers, puis Add new identity provider, et choisissez One-time PIN. Il n'y a rien à configurer : pas de clé d'API, pas de client secret, pas de callback URL.
Étape 3 : créer l'application Access
Toujours dans Zero Trust, allez dans Access controls > Applications > Create new application, choisissez Self-hosted and private, puis Add public hostname.
Renseignez :
- Application name : un nom parlant, il apparaîtra sur la page de connexion.
- Domain : le hostname à protéger, par exemple
admin.votre-domaine.com. Le domaine doit appartenir à une zone Cloudflare active. Les wildcards sont acceptés si vous voulez couvrir plusieurs chemins. - Session duration : la durée de validité du jeton. Cloudflare vérifie chaque requête HTTP pour un jeton valide. Pour un back-office sensible, 24 heures est un bon compromis ; pour un outil critique, descendez à 1 heure.
Étape 4 : écrire la politique d'accès
C'est l'étape qui décide de tout. Les applications Access sont deny by default : sans politique, personne n'entre, y compris vous.
Créez une politique :
- Action : Allow
- Include :
Emails→ la liste explicite des adresses autorisées, ouEmails ending in→@votre-entreprise.com
Vous pouvez durcir avec des règles Require cumulatives : pays de connexion, plage d'IP, authentification par clé de sécurité matérielle FIDO2/WebAuthn, appartenance à un groupe.
Étape 5 : tester
Ouvrez https://admin.votre-domaine.com dans une fenêtre de navigation privée. Vous devez voir la page de connexion Cloudflare — pas votre application. Saisissez une adresse non autorisée : aucun e-mail n'arrive. Saisissez une adresse autorisée : le code arrive, et vous accédez à l'application.
À ce stade, essayez de faire un curl direct sur l'URL. Vous obtenez une redirection vers la page d'authentification, jamais le contenu. Votre application n'a pas été sollicitée une seule fois.
Le piège de configuration qui annule toute la protection
C'est l'erreur la plus fréquente, et elle est silencieuse.
Si vous créez une politique avec Include : One-time PIN sans restreindre les adresses e-mail, vous venez d'autoriser toute personne disposant d'une adresse e-mail sur terre à recevoir un code et à entrer. La documentation Cloudflare le signale explicitement, et beaucoup de configurations trouvées en ligne reproduisent cette erreur.
One-Time PIN est une méthode d'authentification, pas une règle d'autorisation. Elle prouve que vous contrôlez une boîte mail. Elle ne dit rien sur le fait que vous ayez le droit d'entrer.
La règle à retenir : dans le bloc Include, mettez toujours soit une liste d'e-mails explicites, soit un domaine e-mail précis. Jamais « OTP » seul.
Vérifiez également que vous couvrez bien tous les chemins d'accès. Une application Access sur admin.votre-domaine.com ne protège pas votre-domaine.com/admin si ce dernier est servi par la même origine sur un autre hostname.
Fermer la porte de derrière : valider le JWT à l'origine
Cloudflare Access protège le chemin qui passe par Cloudflare. Un attaquant qui découvre l'IP de votre serveur et l'appelle directement contourne toute la chaîne. Le tunnel règle ce problème dans la plupart des cas, mais la défense en profondeur exige une seconde vérification : votre application doit vérifier elle-même que la requête vient bien d'Access.
Le mécanisme : après authentification, Access transmet un jeton JWT signé à votre origine, de deux façons.
- Dans l'en-tête
Cf-Access-Jwt-Assertion— c'est la méthode recommandée, la plus fiable. - Dans le cookie
CF_Authorization— utilisable pour les requêtes navigateur, mais le cookie n'est pas garanti d'être transmis. Ne construisez pas votre vérification dessus.
Pour valider ce jeton, votre application a besoin de deux éléments :
- Les clés publiques, disponibles sur
https://<votre-team-name>.cloudflareaccess.com/cdn-cgi/access/certs. L'endpoint exposekeys(format JWK),public_certetpublic_certs(format PEM). - Le tag AUD de l'application, un identifiant unique visible dans les paramètres avancés de votre application Access. Il ne change jamais, sauf si vous supprimez et recréez l'application.
Un point critique sur la rotation : les clés tournent toutes les 6 semaines, et l'ancienne clé reste valide 7 jours après la rotation. Ne codez donc jamais une clé en dur dans votre application. Récupérez le jeu de clés depuis l'endpoint et faites correspondre le kid du JWT à la bonne clé — sinon votre authentification cassera silencieusement six semaines après la mise en production.
Sur une application Django, l'implémentation se résume à un middleware qui lit l'en-tête, vérifie la signature avec la clé correspondante, contrôle l'aud et l'expiration, puis rejette tout ce qui ne passe pas. L'e-mail vérifié se trouve dans la claim email du jeton : vous pouvez l'utiliser pour créer ou retrouver l'utilisateur en base, et supprimer complètement votre écran de login applicatif. Des paquets communautaires existent pour Django, mais le code manuel tient en une cinquantaine de lignes et reste préférable pour ne pas ajouter une dépendance sur un chemin critique.
Si vous utilisez Cloudflare Tunnel, l'option Protect with Access dans les paramètres du tunnel effectue cette validation pour vous côté cloudflared.
Et les machines : les service tokens
Une objection immédiate : si l'authentification exige une boîte mail et un navigateur, comment font vos scripts, vos CI/CD et vos intégrations ?
Réponse : les service tokens. Access génère une paire Client ID / Client Secret, et le client machine s'authentifie en envoyant deux en-têtes :
CF-Access-Client-Id: <votre-client-id>.access
CF-Access-Client-Secret: <votre-client-secret>
Le flux OAuth navigateur est court-circuité. Le mécanisme est sans état : chaque requête porte ses identifiants, il n'y a pas de session à maintenir. En pratique, cela veut dire qu'un curl depuis votre pipeline GitLab CI, un webhook entrant ou un cron sur un autre serveur peuvent traverser Access proprement, sans que vous ayez à ouvrir une exception d'IP.
Pour les services tiers qui n'acceptent qu'un seul en-tête personnalisé, une application Access peut aussi être configurée pour accepter le service token dans un en-tête unique.
Un service token doit avoir sa propre politique Access (Include : Service Auth), distincte de celle des humains. Ne mélangez pas les deux dans une seule règle.
Si vous automatisez la création de tout cela plutôt que de cliquer dans le tableau de bord, la démarche infrastructure-as-code est décrite dans l'article sur l'API et le CLI Cloudflare.
Cas pratique : protéger une API NestJS
Tout ce qui précède décrit une application consultée dans un navigateur : un /admin Django, un Grafana, un back-office. L'utilisateur voit la page de connexion Cloudflare, reçoit son code, entre.
Une API NestJS n'est pas ça. Personne ne la consulte à la main. Elle est appelée par un frontend, par un job CI, par un autre service. Il n'y a pas de navigateur pour afficher un écran de connexion, pas de boîte mail pour recevoir un code. Le One-Time PIN n'a aucun sens ici.
Le mécanisme change donc, mais le principe reste identique : Access authentifie avant que Nest ne route quoi que ce soit. Vos guards, vos pipes, vos intercepteurs et votre ORM ne sont jamais atteints par un appelant non identifié.
Concrètement, une API derrière Access a deux catégories d'appelants, et chacune a son mécanisme :
| Appelant | Mécanisme | Ce que porte le JWT |
|---|---|---|
| Un humain via un frontend (SPA, mobile) | Session Access, obtenue par IdP ou OTP sur le domaine | claim email |
| Une machine (CI/CD, cron, service tiers) | Service token, en-têtes CF-Access-Client-Id / CF-Access-Client-Secret |
claim common_name (le Client ID) |
Dans les deux cas, Cloudflare transmet à votre origine le même en-tête Cf-Access-Jwt-Assertion. Votre API n'a qu'un seul code de vérification à écrire.
Le guard NestJS
Installez les deux dépendances :
npm install jsonwebtoken jwks-rsa
npm install -D @types/jsonwebtoken
Puis le guard :
// src/auth/cloudflare-access.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Request } from 'express';
import { JwksClient } from 'jwks-rsa';
import { verify, JwtHeader, JwtPayload } from 'jsonwebtoken';
// Ex. : https://mon-equipe.cloudflareaccess.com
const TEAM_DOMAIN = process.env.CF_ACCESS_TEAM_DOMAIN!;
// Le tag AUD de l'application, dans ses parametres avances
const AUD = process.env.CF_ACCESS_AUD!;
@Injectable()
export class CloudflareAccessGuard implements CanActivate {
// cache: true est indispensable. Les cles tournent toutes les 6 semaines :
// on ne les code jamais en dur, et on ne les refetch pas a chaque requete.
private readonly jwks = new JwksClient({
jwksUri: `${TEAM_DOMAIN}/cdn-cgi/access/certs`,
cache: true,
cacheMaxAge: 6 * 60 * 60 * 1000,
rateLimit: true,
jwksRequestsPerMinute: 10,
});
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest<Request>();
// L'en-tete est la source fiable. Le cookie CF_Authorization n'est pas
// garanti d'etre transmis : ne construisez pas la verification dessus.
const token = req.header('Cf-Access-Jwt-Assertion');
if (!token) throw new UnauthorizedException('Jeton Access absent');
const payload = await new Promise<JwtPayload>((resolve, reject) => {
verify(
token,
(header: JwtHeader, callback) => {
// On resout la cle par son `kid`. C'est ce qui rend la rotation
// des cles totalement transparente pour l'application.
this.jwks.getSigningKey(header.kid, (err, key) =>
err ? callback(err) : callback(null, key!.getPublicKey()),
);
},
{ audience: AUD, issuer: TEAM_DOMAIN, algorithms: ['RS256'] },
(err, decoded) =>
err
? reject(new UnauthorizedException(err.message))
: resolve(decoded as JwtPayload),
);
});
// L'identite deja verifiee par Cloudflare, exposee aux controleurs.
req['accessUser'] = {
email: payload.email as string | undefined,
sub: payload.sub as string,
// Un service token n'a pas d'email : c'est `common_name` qui porte
// le Client ID. Utile pour tracer quel automate a appele quoi.
serviceTokenId: payload['common_name'] as string | undefined,
};
return true;
}
}
Trois détails qui font la différence entre un guard correct et un guard qui casse en production :
algorithms: ['RS256']est obligatoire. Sans cette contrainte explicite, une bibliothèque JWT mal configurée peut accepter un jeton signé avecalg: none. C'est la faille de validation JWT la plus classique.audience: AUDn'est pas décoratif. Sans vérification de l'aud, un jeton valide émis pour une autre application de votre compte Cloudflare serait accepté par cette API. Un utilisateur autorisé sur votre Grafana entrerait dans votre API.cache: trueavec unkidrésolu dynamiquement. C'est ce qui vous évite la panne silencieuse six semaines après la mise en production, au moment de la rotation des clés.
L'enregistrer globalement
Un guard sur quelques contrôleurs laisse forcément une route oubliée. Enregistrez-le globalement, et ouvrez explicitement les rares exceptions :
// src/app.module.ts
import { APP_GUARD } from '@nestjs/core';
import { CloudflareAccessGuard } from './auth/cloudflare-access.guard';
@Module({
providers: [{ provide: APP_GUARD, useClass: CloudflareAccessGuard }],
})
export class AppModule {}
Pensez à prévoir une route de health check accessible sans jeton, sinon votre monitoring d'uptime tombera en 401 en permanence. Créez un décorateur @Public() et testez-le dans le guard via Reflector, plutôt que de retirer le guard global.
Appeler l'API depuis un pipeline CI
Côté machine, l'appel reste un simple curl avec deux en-têtes. Aucune exception d'IP à maintenir, aucune liste blanche à mettre à jour quand votre runner change d'adresse :
curl -sS https://api.votre-domaine.com/v1/reindex \
-X POST \
-H "CF-Access-Client-Id: ${CF_ACCESS_CLIENT_ID}" \
-H "CF-Access-Client-Secret: ${CF_ACCESS_CLIENT_SECRET}" \
-H "Content-Type: application/json"
Stockez les deux valeurs en variables masquées dans GitLab CI ou GitHub Actions, et donnez-leur leur propre politique Access (Include : Service Auth), distincte de celle des humains.
Le piège CORS, spécifique aux SPA
C'est le point sur lequel butent la plupart des intégrations frontend, et il est contre-intuitif.
Si votre SPA est sur app.votre-domaine.com et votre API NestJS sur api.votre-domaine.com, le navigateur émet une requête préliminaire OPTIONS avant l'appel réel. Or le navigateur n'envoie jamais de cookies avec une requête OPTIONS, par conception. Access ne voit donc aucune session, et répond 403 — même si l'utilisateur est parfaitement authentifié. Votre appel échoue avant d'avoir commencé.
Ce n'est pas un bug de votre code Nest, et aucune configuration enableCors() côté NestJS ne le corrigera : la requête n'atteint jamais votre application.
La correction se fait dans Access, pas dans Nest. Allez dans Zero Trust > Access controls > Applications, ouvrez l'application de l'API, puis Advanced settings > CORS settings, et configurez :
- Access-Control-Allow-Origin : l'origine exacte de votre SPA, par exemple
https://app.votre-domaine.com. Un wildcard*est incompatible avec l'envoi de credentials, la spécification CORS l'interdit. - Access-Control-Allow-Methods : les méthodes réellement utilisées.
- Access-Control-Allow-Headers : vos en-têtes personnalisés,
Content-Typecompris. - Access-Control-Allow-Credentials : activé, pour que le cookie de session soit transmis.
Cloudflare répond alors lui-même aux requêtes OPTIONS, qui n'atteignent plus votre origine. Côté frontend, vos appels doivent inclure les credentials :
fetch('https://api.votre-domaine.com/v1/projets', {
credentials: 'include',
});
Notez enfin que ce mécanisme suppose que les deux applications sont derrière Access. Une SPA publique qui appelle une API protégée n'a aucune session à transmettre : dans ce cas, l'API ne doit pas être derrière Access, mais protégée par une authentification applicative classique et un WAF.
En développement local
Sur votre poste, il n'y a pas de Cloudflare devant Nest : l'en-tête Cf-Access-Jwt-Assertion est absent, et le guard rejette tout. La tentation est de le désactiver avec un if (process.env.NODE_ENV !== 'production') return true.
Évitez-le. Cette ligne est exactement le genre de raccourci qui finit en production le jour où une variable d'environnement est mal renseignée, et elle désactive alors toute votre sécurité en silence. Préférez cloudflared en local pour traverser le vrai chemin d'authentification, ou un service token de développement à durée de vie courte et à politique séparée.
Ce que ça change concrètement face aux attaques automatisées
| Scénario d'attaque 2026 | Sans Access | Avec Access |
|---|---|---|
| Credential stuffing avec identifiants fuités | Votre code teste chaque paire, votre CPU hache, votre base est interrogée | Aucun mot de passe n'existe. Le bot ne dépasse pas le réseau Cloudflare |
| Bot IA qui cartographie vos endpoints | Chaque réponse HTTP renseigne l'attaquant sur votre stack | Toutes les URL renvoient la même page de connexion. Aucune information |
| CVE critique publiée sur votre framework | Exploitable dans l'heure par des scanners automatisés | L'exploit n'atteint jamais le code vulnérable. Vous patchez sans urgence |
| Vol de jeton de session | Le voleur arrive après le MFA, il est dedans | Access revérifie le jeton à chaque requête HTTP, avec sa propre durée de session |
| Panneau d'admin oublié en ligne | Découvert par scan, exploité | Protégé par la même politique que le reste |
Le point le plus sous-estimé est le troisième. Quand une CVE critique sort sur Django, WordPress ou une dépendance transitive, le délai entre publication et exploitation de masse se compte désormais en heures. Si votre application est derrière un Identity-Aware Proxy, vous n'êtes pas dans la course. Vous patchez le lendemain matin, calmement.
Les limites, honnêtement
Aucun outil n'est magique, et présenter Access sans ses angles morts serait malhonnête.
Ce n'est pas une protection contre un compte compromis. Si la boîte mail d'un utilisateur autorisé est piratée, l'attaquant reçoit le code et entre. One-Time PIN déplace le risque vers la sécurité de la messagerie. Pour un accès réellement critique, exigez en plus une clé de sécurité matérielle FIDO2 via une règle Require.
One-Time PIN ne transporte pas les groupes. Un utilisateur authentifié par OTP n'apporte aucune appartenance de groupe issue d'un fournisseur d'identité. Si vos politiques reposent sur des groupes, il faut un vrai IdP.
Les boîtes mail partagées cassent le modèle. Une adresse contact@ ou info@ consultée par cinq personnes signifie cinq accès pour une identité. La traçabilité disparaît.
Les outils de sécurité e-mail peuvent consommer les liens. Certaines passerelles de protection cliquent automatiquement les liens des e-mails entrants, ce qui peut faire apparaître un code comme déjà utilisé. C'est une cause classique de tickets « le code ne marche pas ».
Le plan gratuit s'arrête à 50 utilisateurs. Au-delà, il faut passer au pay-as-you-go, facturé environ 7 $ par utilisateur et par mois, sans plafond. Surveillez aussi le quota moyen de requêtes DNS par siège si vous utilisez le Gateway DNS en parallèle.
Access ne remplace pas le WAF. Access protège ce qui doit rester privé. Pour vos pages publiques — un site, une boutique, une API ouverte — il ne peut rien : par définition, tout le monde a le droit d'y accéder. C'est là que le WAF, le bot management et le rate-limiting reprennent la main. Les deux couches sont complémentaires, jamais interchangeables. J'ai détaillé cette architecture complète dans le guide Cloudflare pour DSI et CTO.
Combien ça coûte
Le plan Zero Trust Free couvre jusqu'à 50 utilisateurs, sans limite de durée, et inclut le ZTNA complet, le Secure Web Gateway, un DLP limité et 24 heures de rétention des logs DNS, HTTP et réseau. Le nombre d'applications auto-hébergées protégées n'est pas limité.
Pour la très grande majorité des développeurs indépendants, des startups et des PME ivoiriennes, cela signifie : gratuit, indéfiniment. Un développeur qui protège son back-office, ses outils de monitoring et son environnement de staging consomme trois sièges.
Le passage payant se déclenche au 51e utilisateur (environ 7 $ par utilisateur et par mois), ou si vous avez besoin de rétention de logs longue, de Logpush ou d'un SLA de disponibilité contractuel.
FAQ Cloudflare Access 2026
Cloudflare Access est-il vraiment gratuit ?
Oui, jusqu'à 50 utilisateurs, sans limite de durée et sans limite du nombre d'applications protégées. Au-delà, le tarif pay-as-you-go est d'environ 7 $ par utilisateur et par mois.
Faut-il obligatoirement un Cloudflare Tunnel ?
Non, mais c'est vivement recommandé. Sans tunnel, votre serveur reste joignable par son IP publique, et un attaquant qui la découvre contourne entièrement Access. Si vous ne pouvez pas utiliser de tunnel, restreignez le pare-feu de votre origine aux plages d'IP Cloudflare et validez le JWT Access dans votre application.
Quelle différence entre Cloudflare Access et un VPN ?
Un VPN donne accès à un réseau : une fois connecté, l'utilisateur peut souvent atteindre bien plus que ce dont il a besoin. Access donne accès à une application précise, avec une politique par application et une vérification à chaque requête HTTP. C'est exactement le déplacement conceptuel opéré par BeyondCorp après Operation Aurora.
Le One-Time PIN est-il aussi sûr qu'un mot de passe ?
Sur les scénarios d'attaque dominants en 2026, il est nettement plus sûr : il n'y a rien à voler dans une fuite de base de données, rien à rejouer, rien à brute-forcer. Sa faiblesse est la sécurité de la boîte mail de l'utilisateur. Pour un accès très sensible, combinez-le avec une règle exigeant une clé de sécurité matérielle.
Access protège-t-il mon site public ?
Non, et ce n'est pas son rôle. Un site public doit être accessible à tous : c'est le WAF, la protection DDoS et le bot management qui interviennent là. Access protège ce qui doit rester privé — admin, staging, outils internes, API partenaires.
Puis-je mettre une API NestJS derrière Access ?
Oui, et c'est un excellent cas d'usage pour une API interne ou partenaire. Les appelants machine s'authentifient avec un service token, les utilisateurs via leur session Access, et un CloudflareAccessGuard valide le JWT côté Nest. Attention au piège CORS si un frontend sur un autre hostname appelle l'API : la requête préliminaire OPTIONS renvoie 403 tant que vous n'avez pas configuré les CORS settings dans l'application Access. En revanche, une API publique destinée à des clients anonymes n'a rien à faire derrière Access.
Que se passe-t-il si Cloudflare tombe ?
Votre application devient inaccessible, comme toute application derrière un proxy. C'est le compromis assumé de l'architecture. Pour les cas critiques, gardez une procédure de secours documentée : accès SSH direct depuis une IP de confiance, désactivation temporaire de la politique.
Puis-je l'utiliser depuis la Côte d'Ivoire sans latence ajoutée ?
Oui. Cloudflare dispose de points de présence en Afrique de l'Ouest, et l'authentification n'a lieu qu'à l'ouverture de session, pas à chaque page. Le surcoût perçu est négligeable pour un back-office. Le trafic vers l'origine passe ensuite par le réseau Anycast, souvent plus rapide qu'une route internet directe.
Comment gérer les accès pour un prestataire externe temporaire ?
Ajoutez son adresse e-mail dans le bloc Include de la politique, avec le One-Time PIN comme méthode. Aucun compte à créer dans votre application, aucun mot de passe à transmettre. À la fin de la mission, retirez l'adresse : l'accès est révoqué immédiatement, sans intervention sur votre code.
Conclusion : le meilleur code est celui qui n'est jamais exécuté
En 2026, la question n'est plus « mon application est-elle bien codée ? ». Elle l'est peut-être, mais elle dépend de dépendances qui ne le sont pas toujours, et les CVE sortent plus vite que vous ne déployez. La question devient : qui a le droit d'exécuter mon code ?
Un Identity-Aware Proxy répond à cette question en amont. Face à des attaquants qui privilégient désormais l'identifiant volé au zero-day, et qui automatisent la reconnaissance avec des agents IA, une porte qui exige une identité vérifiable avant même de transmettre la requête est la défense la plus solide et la moins coûteuse à maintenir.
Le plus étonnant reste que cette capacité, réservée aux moyens de Google il y a quinze ans, tienne aujourd'hui dans une quinzaine de minutes de configuration et coûte zéro franc jusqu'à 50 utilisateurs.
Si vous avez un back-office, un Grafana, un phpMyAdmin, une API NestJS interne ou un /admin Django exposé quelque part avec un simple mot de passe, c'est probablement la meilleure heure que vous passerez sur votre infrastructure ce mois-ci.
Pour aller plus loin
- Cloudflare Tunnel : sécuriser son VPS sans ouvrir un seul port — le prérequis indispensable pour fermer l'origine
- Automatiser Cloudflare avec l'API et le CLI — créer applications, politiques et service tokens en infrastructure-as-code
- Cloudflare pour DSI et CTO : le socle sécurité complet — comment Access s'articule avec WAF, DDoS et bot management
- Qu'est-ce qu'un WAF (Web Application Firewall) ? — la couche complémentaire pour vos pages publiques
- Guide WAF 2026 : choisir et configurer
- Sécuriser un serveur Ubuntu avec fail2ban — la défense classique, et ses limites
- Les 10 fonctionnalités Cloudflare incontournables
- Accéder à son VPS Linode — le serveur sur lequel mettre tout cela en pratique
Vous avez mis Cloudflare Access devant une application et vous butez sur la validation du JWT ou sur une politique qui ne se déclenche pas ? Écrivez-moi sur LinkedIn, je réponds aux questions d'infrastructure chaque semaine.