Passkeys natives dans Adobe ColdFusion 2025 - Partie 4 : Exécution derrière un équilibreur de charge

Le navigateur utilise HTTPS. ColdFusion aimerait un deuxième avis.

Dans la partie 1, nous avons enregistré une clé d'accès. Dans la partie 2, nous l'avons utilisée pour authentifier un utilisateur. Dans la partie 3, nous avons déplacé la cérémonie vers une origine d'authentification centrale et renvoyé un code d'autorisation à usage unique et de courte durée à l'application demanderesse.

Cela nous a donné une architecture utile :

app.example.com
    → auth.example.com
    → native passkey ceremony
    → app.example.com callbackß
    → application session

Nous devons maintenant l'exécuter derrière un AWS Application Load Balancer, avec plus d'une instance ColdFusion. C'est là qu'une implémentation fonctionnelle des clés d'accès peut devenir un outil de diagnostic d'infrastructure étonnamment efficace. Le navigateur sait quelle origine il visite. ColdFusion a sa propre vision de cette requête. Plusieurs noeuds ont leurs propres idées sur l'état qu'ils possèdent.

Tous doivent être d'accord.

L'origine publique doit survivre à la chaîne de proxy

Considérez ce déploiement :

Browser
    HTTPS :443
        ↓
AWS Application Load Balancer
    HTTP :80
        ↓
Apache
    Internal connector
        ↓
ColdFusion

Le TLS prend fin au niveau de l'ALB. La connexion interne utilise HTTP. Cette configuration peut fonctionner, mais ColdFusion doit quand même comprendre que la requête initiale du navigateur est arrivée via HTTPS. Pour notre service d'authentification, l'origine publique est :

https://auth.example.com

L'origine comprend le schéma, le nom d'hôte et le port effectif. Ce sont des origines différentes :

https://auth.example.com
http://auth.example.com
https://auth.example.com:8081

L'identifiant de la partie de confiance demeure :

auth.example.com

Il n'inclut ni schéma, ni port, ni chemin. Changer l'ID RP pour tenir compte d'un port de listener interne résoudrait le mauvais problème. Dans les déploiements décrits dans cet article, nous avons rencontré deux échecs distincts :

Origine du navigateur

Origine effective de ColdFusion

Problème

https://auth.example.com

http://auth.example.com

Le HTTPS externe a été perdu

https://auth.example.com

https://auth.example.com:8081

Le port du listener interne s'est infiltré dans la validation de l'origine

Les deux échecs se sont produits avant que nous puissions mener à bien une cérémonie de clé d'accès utile.

Les en-têtes transférés ne sont que la première étape

Un ALB fournit X-Forwarded-Proto et X-Forwarded-Port afin que les services en aval puissent identifier le protocole et le port de la connexion d'origine. AWS documente ces en-têtes ici.

Cependant, recevoir ceci :

X-Forwarded-Proto: https
X-Forwarded-Port: 443

ne prouve pas que la requête servlet, ou les valeurs CGI exposées par ColdFusion, reflète ces valeurs. Dans notre échec initial, les en-têtes étaient présents alors que ColdFusion signalait encore :

cgi.https       = off
cgi.server_port = 80

La configuration semblait plausible. La requête résultante était erronée.

Inspectez la requête via l'URL d'authentification publique réelle. Notez le schéma, le nom d'hôte et le port utilisés par le moteur, puis comparez-les à l'origine publique configurée. Gardez ce diagnostic protégé et limité au strict nécessaire. Il n'est pas nécessaire de publier un vidage CGI sans restriction pour découvrir que le port est incorrect. Établissez aussi la frontière de confiance du proxy. Les écouteurs backend ne devraient être accessibles que par l'infrastructure prévue, et les métadonnées transférées ne devraient être acceptées que depuis des proxies de confiance. Un en-tête de requête arbitraire ne doit pas devenir l'autorité de votre origine d'authentification.

Corrigez le runtime que vous avez réellement

Notre déploiement original utilisait le Tomcat intégré à ColdFusion via un connecteur AJP Apache.

Pour un connecteur servant exclusivement du trafic provenant d'origines HTTPS à partir de la chaîne de proxy de confiance, les attributs pertinents étaient :

scheme="https"
secure="true"
proxyPort="443"

Ceux-ci appartiennent au connecteur existant, avec son liaisonnement et ses paramètres de sécurité existants préservés. Tomcat documente leurs effets sur le schéma de la requête, l'état sécurisé et le port du serveur. Référence du connecteur AJP de Tomcat.

Un déploiement gérant des protocoles externes mixtes a besoin d'une configuration qui dérive ces valeurs à partir des informations de proxy de confiance. Le RemoteIpValve de Tomcat fournit ce mécanisme, avec une configuration explicite de confiance envers les proxies. Référence des valves Tomcat.

Le test important vient après la configuration : une requête via https://auth.example.com atteint-elle maintenant ColdFusion avec l'origine effective correcte ? Refaites cette vérification après les mises à jour du moteur. Un fichier de connecteur déjà corrigé peut être remplacé lors d'une mise à niveau.

Le problème de port de CommandBox

Plus tard, dans un déploiement ARM utilisant Adobe ColdFusion 2025.0.11 sous CommandBox et Undertow, nous avons rencontré un problème différent. Le schéma et le nom d'hôte étaient corrects. Le port du serveur est demeuré le port du répartiteur HTTP interne :

cgi.https       = on
cgi.server_name = auth.example.com
cgi.server_port = 8081

Le traitement natif des passkeys de ColdFusion s'attendait donc à :

https://auth.example.com:8081

Nous avons essayé des paramètres de port transféré et des changements d'en-tête d'hôte. Dans cet environnement d'exécution, ces changements n'ont pas corrigé la valeur du port du serveur utilisée par la vérification d'origine de l'application. La solution de contournement qui a modifié la valeur observée a consisté à déplacer le répartiteur HTTP de boucle locale du moteur vers le port 443 :

{
  "web": {
    "http": {
      "host": "127.0.0.1",
      "port": 443
    }
  }
}

Apache a ensuite transmis vers :

http://127.0.0.1:443/

Oui, c'est HTTP sur le port 443. C'est bancal, non?

L'ALB a quand même terminé le TLS public. Le numéro de port du répartiteur interne n'activait pas TLS, et l'URL du proxy devait conserver http://. Le traitement du proxy de confiance fournissait toujours le bon schéma externe. Il s'agissait d'une solution de contournement pour le comportement d'exécution précis que nous avons mesuré. Ce n'est pas une exigence générale voulant que les applications ColdFusion écoutent sur le port 443. Préférez une configuration de proxy prise en charge qui produit les bonnes valeurs de requête lorsque votre environnement d'exécution le prend en charge.

La solution de contournement a aussi amené deux détails Linux ordinaires dans la discussion :

  • Un service non privilégié avait besoin de l'autorisation de lier un port bas. Nous avons accordé au service CAP_NET_BIND_SERVICE plutôt que de l'exécuter comme root.
  • Apache avait déjà un écouteur sur le port 443 à partir de sa configuration SSL. Nous avons dû concilier la propriété du port pour cette topologie terminée par l'ALB avant que le moteur puisse se lier avec succès.

Un message de démarrage n'était pas une preuve suffisante. Nous avons rencontré un message « le serveur est en marche » en même temps qu'un échec de liaison. Vérifiez le processus d'écoute, faites une requête HTTP et inspectez les valeurs de requête ColdFusion obtenues. L'optimisme du journal n'est pas un contrôle d'état.

Conserver le service de ColdFusion à l'intérieur de l'application d'authentification

La partie 1 a présenté une route locale à l'application vers le service natif de passkeys de ColdFusion :

/__cf_passkey/DatabasePasskey.cfc

Cela demeure important derrière un équilibreur de charge. Dans le déploiement CommandBox, le mappage /CFIDE pouvait atteindre la racine Web du moteur en dehors de l'application à l'origine de la requête. Le navigateur accédait au service, mais le service ne participait pas au contexte d'application et de session attendu.

Le résultat était CSRF_INVALID. Un routage réussi et un contexte d'application correct sont deux exigences distinctes. L'alias local de l'application d'authentification doit résoudre vers les fichiers de passkey natifs appartenant au moteur qui exécute cette application. Chaque cible a besoin de la même configuration.

Gardez la surface plus large /CFIDE bloquée, n'exposez que le point de terminaison du service requis et vérifiez le véritable POST du navigateur via cette route. Évitez de copier le CFC de ColdFusion dans votre application : une implémentation copiée peut discrètement diverger du moteur installé après une mise à jour. Désactiver la validation CSRF retirerait la protection qui a mis en évidence le problème de routage. Corrigez le routage.

Provisionner chaque moteur

Le dépôt de l'application ne contient pas toutes les parties d'un déploiement de passkeys natifs. Chaque moteur ColdFusion a besoin de sa configuration passkey : la source de données des identifiants, la sélection du magasin de défis et la durée de vie du défi. Dans notre flux de déploiement, ces paramètres se trouvaient dans la configuration de sécurité du moteur et n'étaient pas automatiquement repris par le processus d'exportation/importation CFConfig que nous utilisions. Le déploiement de la même application sur deux nœuds ne prouvait donc pas que les deux moteurs étaient configurés.

Provisionnez ces paramètres au moyen d'un processus administratif protégé, en utilisant l'API Admin présentée plus tôt dans la série. Vérifiez la configuration effective sur chaque cible et confirmez qu'elle survit à un redémarrage. ColdFusion prend en charge plusieurs choix de magasin de défis par l'entremise de setPasskeyConfig(). La sélection d'un magasin et le provisionnement de l'infrastructure derrière ce magasin sont deux étapes distinctes. Documentation native des passkeys d'Adobe.

Une base de données d'identifiants partagée est nécessaire pour que plusieurs moteurs reconnaissent les mêmes identifiants enregistrés. Elle ne partage pas automatiquement l'état temporaire d'une cérémonie d'authentification. Cette distinction est importante pour la suite.

« Nos sessions sont dans Redis » est une réponse incomplète

Une connexion native par passkey implique plusieurs types d'état :

État

Objet

Question de déploiement

Identifiants enregistrés

Associer un utilisateur à des identifiants de clé publique

Tous les moteurs utilisent-ils la base de données partagée prévue?

Défis natifs

Lier les réponses à une cérémonie particulière

Le noeud de validation peut-il récupérer le défi?

État CSRF natif

Valider les requêtes au service de ColdFusion

La validation survit-elle à un changement de noeud?

État natif du jeton de résultat

Permettre au rappel d'obtenir le résultat terminé

Le rappel peut-il résoudre le jeton sur sa cible?

Session de l'application d'authentification

Préserver la transaction centrale en attente

Survit-elle aux changements de routage?

Session de l'application de confiance

Préserver l'état de la partie 3

Est-il disponible lorsque l'utilisateur revient?

Enregistrements de la transaction centrale et du code

Lier et échanger la remise

Sont-ils partagés et consommés de façon atomique?

Déplacer la portée de session de ColdFusion vers Redis règle une partie de ce tableau. Cela ne démontre pas que le cache des défis d'un moteur, l'état natif de validation CSRF ou la gestion des jetons de résultat sont aussi distribués. Nous avons observé un état de validation CSRF natif local au noeud, même avec des sessions prises en charge par Redis. Nous avons aussi constaté que le cache de serveur ColdFusion configuré était local à un moteur. Par conséquent, changer ce paramètre:

challengeStore: "servercache"

n'a pas, à lui seul, rendu les défis disponibles à travers les noeuds. Le cache derrière ce nom doit réellement être partagé. Modifier la configuration globale du cache de serveur peut aussi affecter la mise en cache d'autres applications, alors considérez cela comme un travail d'infrastructure avec des conséquences au-delà des clés d'accès. Même après avoir partagé les défis, testez la cérémonie complète sur plusieurs noeuds, y compris le POST du service natif et le rappel du résultat. N'en déduisez pas que tous les autres magasins d'état natif suivent le paramètre du magasin de défis.

La partie 3 montre que le transfert soutenu par base de données règle l'échange central du code d'autorisation. Il ne peut pas réparer un état natif qui disparaît avant que l'origine d'authentification ne produise ce code.

Le déploiement pratique utilisait la persistance

Notre déploiement enregistré utilisait :

Native challenge store: memory
ALB stickiness type:    lb_cookie
Stickiness duration:   300 seconds

Pour chaque groupe cible applicable, les attributs pertinents étaient :

stickiness.enabled = true
stickiness.type = lb_cookie
stickiness.lb_cookie.duration_seconds = 300

La persistance ALB basée sur la durée utilise un témoin de répartition pour acheminer les requêtes subséquentes vers la même cible. Si cette cible devient non saine ou est supprimée, l'ALB peut en sélectionner une autre. Documentation AWS sur les sessions collantes.

Le fait de garder le navigateur sur un seul moteur sain a permis à la cérémonie d'utiliser l'état local de ce moteur. Il s'agissait d'un choix opérationnel avec une limite claire : la persistance ne réplique pas l'état. Si le moteur redémarre ou si la cible change pendant l'authentification, la cérémonie en cours peut être perdue. L'application devrait signaler que l'authentification n'a pas pu se terminer et permettre une nouvelle tentative. N'étendez pas indéfiniment un défi et ne sautez pas la validation pour récupérer une connexion interrompue.

Gardez aussi en vue les deux frontières d'application de la partie 3. L'affinité à auth.example.com ne préserve pas automatiquement l'état de session en attente à app.example.com. Chaque application a besoin d'une stratégie de session appropriée. La conception d'authentification centrale utilise toujours des sessions d'application distinctes. Il n'est pas nécessaire d'introduire un cookie de session pour le domaine parent.

Faire en sorte que la préparation décrive le comportement

Une vérification de déploiement utile devrait répondre à plus que « ce fichier contient-il l'hôte prévu? ». L'un de nos contrôles précédents a trouvé l'hôte dans une règle de réécriture d'hôte. Cette règle n'a pas corrigé la valeur du port du serveur. Le contrôle a réussi. L'authentification, non. Pour ce déploiement, la préparation doit couvrir :

  1. Fonctions natives : les BIF passkey requis existent.
  2. Aiguillage du service : l'alias local se résout vers les bons fichiers du moteur.
  3. Configuration du moteur : les paramètres passkey prévus sont présents.
  4. Origine publique : une requête passant par l'hôte externe produit le schéma, l'hôte et le port attendus.
  5. Stratégie d'état : la configuration prévue du cache, de la session et de l'affinité est active.

Gardez les échecs précis. Un origin_mismatch est plus utile qu'un simple « passkeys unavailable ». Journalisez l'étape en échec, l'identifiant du noeud et des valeurs diagnostiques étroitement sélectionnées. Évitez de journaliser les jetons de résultat natifs, les codes d'autorisation, les témoins de session ou les charges utiles complètes de la cérémonie. Pendant le déploiement, un script de préparation peut s'exécuter en mode rapport seulement pendant que vous établissez une ligne de base. Avant de vous y fier comme point de contrôle du déploiement, activez l'application des règles et vérifiez qu'un échec connu produit un code de sortie non nul.

Une configuration illisible donne un résultat inconnu. Un contrôle ignoré est un contrôle ignoré. Aucun des deux ne devrait devenir silencieusement la preuve que le déploiement est prêt.

Testez chaque cible par l'origine réelle

Une connexion réussie par l'entremise de l'ALB prouve qu'un chemin a fonctionné. Elle ne prouve pas que chaque nœud fonctionne. Utilisez un environnement de préparation contrôlé pour tester chaque cible tout en préservant :

https://auth.example.com

Accéder directement à l'adresse IP d'un nœud change l'origine et invalide la comparaison. Le plan de test final devrait inclure :

Test

Résultat attendu

Inscrire et authentifier par l'entremise de chaque cible

Les deux se terminent avec succès

Effectuer l'aller-retour de la partie 3 depuis chaque application de confiance

Le bon utilisateur local reçoit une session

Faire expirer une cérémonie avant son achèvement

L'authentification échoue proprement; une nouvelle tentative fonctionne

Rejouer un code de transmission déjà utilisé

Le rachat échoue

Soumettre une valeur d'état non correspondante

L'application de confiance rejette le rappel

Redémarrer une cible pendant une cérémonie

Aucune session non autorisée; la récupération commence une nouvelle cérémonie

Changer de nœud entre les étapes de la cérémonie

Réussit seulement si la conception d'état distribué revendiquée le prend en charge

Redémarrer ou mettre à jour le moteur

L'origine, le routage et la configuration des clés d'accès demeurent corrects

Exécutez le flux réel du navigateur ainsi que les vérifications passives de préparation. Un port correct, une source de données saine et un CFC existant sont des ingrédients nécessaires. Ils ne prouvent pas que le chemin d'authentification complet fonctionne.

Compléter la série

Les responsabilités établies dans la partie 1 demeurent. ColdFusion gère la cérémonie WebAuthn native et le traitement des identifiants. L'application contrôle l'identité de l'utilisateur, l'intention, l'autorisation et sa propre session authentifiée.

La partie 3 a ajouté une origine d'authentification centrale et une transmission étroitement contrôlée entre les applications.

L'équilibreur de charge ajoute une responsabilité de plus - préserver l'identité publique de la requête et l'acheminer de façon cohérente avec le modèle d'état que vous avez réellement déployé. Dans notre cas, bien faire cela a exigé de corriger des valeurs d'origine dérivées du proxy, de gérer un problème de port d'écoute propre à l'environnement d'exécution, de garder le point de terminaison de ColdFusion à l'intérieur de l'application d'authentification, d'approvisionner chaque moteur et d'utiliser l'affinité là où l'état natif demeurait local. Aucun de ces changements n'a nécessité d'affaiblir la validation de l'origine ou de contourner la protection CSRF.

Une fois que ces éléments s'accordent, une connexion par clé d'accès peut enfin devenir ce que nous voulions dès le départ - sans incident et sans tracas.