ds https://www.vincentliefooghe.net/index.php/ fr Pass-Through Authentication et migration des mots de passe https://www.vincentliefooghe.net/index.php/content/pass-through-authentication-et-migration-des-mots-passe <span class="field field--name-title field--type-string field--label-hidden">Pass-Through Authentication et migration des mots de passe</span> <div class="clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item"><h2>Contexte</h2> <p>Dans un projet de migration d'un annuaire AD LDS vers un Forgerock DS, le principal problème est de pouvoir récupérer les mots de passe.</p> <p>En effet, AD LDS (et AD DS) ne permettent pas d'exporter le hash du mot de passe en LDIF, et il n'est pas possible non plus <em>simplement</em> de les récupérer, même en passant par des outils tels que <em>creddump</em> , <em>ntdsextract</em> ou autre.</p> <p>Du coup, l'idée peut être intéressante de paramétrer l'annuaire Forgerock DS en mode <em>pass through authentication</em>, d'activer le cache, et ensuite d'utiliser le mot de passe local, une fois la première authentification correcte réalisée sur l'AD LDS (ou n'importe quel annuaire).</p> <h2>Pré-requis : import du certificat</h2> <p>Dans un premier temps, il faut importer le certificat de l'annuaire d'authentification. On peut récupérer le certificat avec une commande <em>openssl</em> :</p> <pre> <code>openssl s_client -connect authserver:2636 </code></pre><p>Une fois le certificat sauvegardé, on va l'intégrer avec <em>keytool</em> :</p> <pre> <code>keytool -importcert -alias authserver -keystore /opt/opendj/config/keystore -file ~/authserver.pem </code></pre><h2>Création d'un "fournisseur de confiance"</h2> <p>Via la commande <em>dsconfig</em>, on va déclarer le serveur d'authentification distant comme "<em>fournisseur de confiance</em>"</p> <pre> <code>dsconfig \ create-trust-manager-provider \ --hostname localhost \ --port 4444 \ --bindDN uid=admin \ --bindPassword password \ --provider-name AuthServerPKCS12 \ --type file-based \ --set enabled:true \ --set trust-store-type:PKCS12 \ --set trust-store-file:config/keystore \ --set trust-store-pin:"&amp;{file:config/keystore.pin}" \ --usePkcs12TrustStore /opt/opendj/config/keystore \ --trustStorePassword:file /opt/opendj/config/keystore.pin \ --no-prompt </code></pre><h2>Paramétrage du mode PTA</h2> <p>Il faut ensuite créer une politique d'authentification</p> <pre> <code>dsconfig \ create-password-policy \ --hostname localhost \ --port 4444 \ --bindDN uid=admin \ --bindPassword password \ --policy-name "AuthServer_PTA_Policy" \ --type ldap-pass-through \ --set primary-remote-ldap-server:authserver:636 \ --set mapped-attribute:uid \ --set mapped-search-base-dn:"cn=Users,dc=example,dc=com" \ --set mapped-search-bind-dn:"uid=ad-bind-account,cn=admins,dc=example,dc=com" \ --set mapped-search-bind-password:ZmQ5OWUw-ADpassword \ --set trust-manager-provider:AuthServerPKCS12 \ --set mapping-policy:mapped-search \ --set use-password-caching:true \ --set cached-password-storage-scheme:PBKDF2-HMAC-SHA256 \ --set cache-password-ttl:24h \ --set use-ssl:true \ --usePkcs12TrustStore /opt/opendj/config/keystore \ --trustStorePassword:file /opt/opendj/config/keystore.pin \ --no-prompt </code></pre><p>On précise notamment :</p> <ul><li>le <em>hostname</em> et le port du serveur d'authentification (dans notre exemple, <strong>authserver</strong>, qui écoute sur le port <strong>636</strong>)</li> <li>la base de recherche pour les utilisateurs</li> <li>l'attribut commun entre le compte local et le compte sur le serveur d'authentification, dans l'exemple on utilise <strong>uid</strong> (il faut donc que l'attribut soit bien unique et identique sur les 2 serveurs)</li> <li>le DN du compte sur le serveur distant</li> <li>le mot de passe du compte distant</li> <li>Le trust manager (déclaré ci-avant)</li> </ul><p>En optionnel, on active le "<em>password caching</em>", qui permet, lors d'une première authentification réalisée avec succès, de cacher le mot de passe en local pour éviter les multiples accès sur le serveur d'authentification distant. Pour ceci, il faut également définir le schéma de stockage du mot de passe caché.</p> <p>On positionne la valeur du cache (TTL) à 24 heures.</p> <h2>Assignation de la password policy à un utilisateur</h2> <p>Pour que la politique de type "<em>pass through</em>" soit appliquée à l'utilisateur, il faut l'assigner de manière explicite, par exemple via un fichier ldif :</p> <pre> <code>dn: cn=user1,ou=Externes,ou=People,dc=example,dc=com changetype: modify add: ds-pwp-password-policy-dn ds-pwp-password-policy-dn: cn=AuthServer_PTA_Policy,cn=Password Policies,cn=config </code></pre><p>Si l'utilisateur tente ensuite de se connecter au serveur DS, on peut voir les logs sur le serveur d'authentification (ici il s'agit des logs d'une autre instance Forgerock DS, utilisée pour les tests):</p> <pre> <code>{"eventName":"DJ-LDAP","client":{"ip":"127.0.0.1","port":60984},"server":{"ip":"127.0.0.1","port":2636},"request":{"protocol":"LDAPS","operation":"SEARCH","connId":8,"msgId":9,"dn":"cn=Users,dc=example,dc=com","scope":"sub","filter":"(uid=1189670333702hawl)","attrs":["1.1"]},"transactionId":"0a31b37e-b7f3-4854-9154-5ebad695ff24-488","response":{"status":"SUCCESSFUL","statusCode":"0","elapsedTime":1,"elapsedTimeUnits":"MILLISECONDS","nentries":1},"userId":"uid=ad-bind-account,ou=admins,dc=example,dc=com","timestamp":"2022-04-04T14:15:36.803Z","_id":"0a31b37e-b7f3-4854-9154-5ebad695ff24-490"} {"eventName":"DJ-LDAP","client":{"ip":"127.0.0.1","port":60986},"server":{"ip":"127.0.0.1","port":2636},"request":{"protocol":"LDAPS","operation":"BIND","connId":9,"msgId":8,"version":"3","dn":"cn=user1,cn=Users,dc=example,dc=com","authType":"SIMPLE"},"transactionId":"0a31b37e-b7f3-4854-9154-5ebad695ff24-491","response":{"status":"SUCCESSFUL","statusCode":"0","elapsedTime":1,"elapsedTimeUnits":"MILLISECONDS","additionalItems":{"ssf":"256"}},"userId":"cn=user1,cn=Users,dc=example,dc=com","timestamp":"2022-04-04T14:15:36.805Z","_id":"0a31b37e-b7f3-4854-9154-5ebad695ff24-493"} </code></pre><p>Si on a activé le cache, on peut constater qu'après une première authentification correcte, l'annuaire distant n'est plus sollicité lors des authentifications suivantes, sauf si le mot de passe ne correspond plus à la valeur en cache.</p> <p>Au niveau local, le mot de passe est stocké dans un attribut opérationnel : <em>ds-pta-cached-password</em>. On y trouve également la date de mise en cache du mot de passe.</p> <p>Par exemple :</p> <pre> <code>dn: cn=user1,ou=Externes,ou=People,dc=example,dc=com ds-pta-cached-password: {SSHA512}CpdBFPCD2LrQpDOFK+hjOifS+hwRz79yWYjMFS543iyAmHZIliBgHnoKjY+HhERC61eQMqNmkgEvrlXYtkmhyK9HK+0gqQQBraFUUd2TViM= ds-pta-cached-password-time: 20220404141914Z ds-pwp-password-policy-dn: cn=AuthServer_PTA_Policy,cn=Password Policies,cn=config </code></pre><h2>Migration progressive des mots de passe</h2> <p>En utilisant le mécanisme de cache et l'authentification en mode <em>pass through</em>, on pourra donc envisager de migrer progressivement les mots de passe de la manière suivante :</p> <ul><li>pour tous les utilisateurs qui doivent être migrés, assigner la politique de mot de passe de type "PTA Policy"</li> <li>pour les autres, on peut se reposer sur les politiques de mots de passe par défaut, ou celles définies dans les données (de type <em>subentry</em>)</li> </ul><p>Il faudra ensuite avoir un traitement complémentaire, qui sera lancé à intervalle régulier, et avec une fréquence supérieure à la durée de cache, qui va réaliser le traitement suivant :</p> <ul><li>rechercher les comptes qui ont l'attribut <em>ds_pta-cached-password</em> positionné, avec la politique de mot de passe "PTA"</li> <li>Pour ces comptes :</li> <li>mettre à jour l'attribut <em>userPassword</em> avec la valeur de l'attribut de cache</li> <li>supprimer l'assignation de la politique de mot de passe, pour récupérer celle définie par défaut.</li> </ul><p>Afin d'améliorer les performances, on pourra créer un index sur l'attribut :</p> <pre> <code>create-backend-index --backend-name amIdentityStore --index-name ds-pta-cached-password --type generic --set index-type:presence </code></pre><p>Ceci peut donc permettre d'utiliser les mots de passe actuels, tout en déconnectant au fil de l'eau le lien avec l'ancien annuaire.</p> <p>Au bout d'un certain temps, on pourra considérer que les comptes qui ont encore l'ancienne politique de mots de passe (ou qui n'ont pas encore d'attribut <em>userPassword</em>) ne sont pas utilisés, et on pourra faire un peu de "ménage", ou faire une campagne de renouvellement de mots de passe.</p> <p> </p> <p> </p> </div> <span class="field field--name-uid field--type-entity-reference field--label-hidden"><span>vincentl</span></span> <span class="field field--name-created field--type-created field--label-hidden"><time datetime="2022-04-04T17:09:40+02:00" title="Lundi 4 avril 2022 - 17:09" class="datetime">lun 04/04/2022 - 17:09</time> </span> <div class="field field--name-field-categorie field--type-entity-reference field--label-above"> <div class="field__label">Catégorie</div> <div class="field__item"><a href="/index.php/cat%C3%A9gorie/iam" hreflang="fr">IAM</a></div> </div> <div class="field field--name-field-tags field--type-entity-reference field--label-above"> <div class="field__label">Tag</div> <div class="field__items"> <div class="field__item"><a href="/index.php/tags/forgerock" hreflang="fr">forgerock</a></div> <div class="field__item"><a href="/index.php/tags/ds" hreflang="fr">ds</a></div> <div class="field__item"><a href="/index.php/tags/opendj" hreflang="fr">opendj</a></div> </div> </div> <section class="field field--name-comment-node-book field--type-comment field--label-hidden comment-wrapper"> <h2 class="title comment-form__title">Ajouter un commentaire</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=215&amp;2=comment_node_book&amp;3=comment_node_book" token="5eq-zcWEYJm_Yk06U0kNvELXXhjQF7kEMPtT6Lez1yY"></drupal-render-placeholder> </section> Mon, 04 Apr 2022 15:09:40 +0000 vincentl 215 at https://www.vincentliefooghe.net Utilisation d'attributs Json dans Forgerock DS https://www.vincentliefooghe.net/index.php/content/utilisation-dattributs-json-dans-forgerock-ds <span class="field field--name-title field--type-string field--label-hidden">Utilisation d&#039;attributs Json dans Forgerock DS</span> <div class="clearfix text-formatted field field--name-body field--type-text-with-summary field--label-hidden field__item"><h2>Problématique</h2> <p>Il arrive des cas où l'utilisation des attributs "simples", qu'ils soient mono ou multi-valués, n'est pas satisfaisante par rapport aux besoins des utilisateurs.</p> <p>Prenons le cas suivant : on veut gérer des délégations pour X types de profils et chacun avec des rôles spécifiques.</p> <p>Soit :</p> <ul><li>délégation pour une société S1, avec les rôles RS1, RS2, RS3</li> <li>délégation pour une société S2, avec les rôles RS1, RS3</li> <li>délégation pour un utilisateur U1, avec les rôles RU1, RU2</li> </ul><p>Si on utilise un attribut de type chaîne de caractères, on peut s'en sortir avec des concaténations et des caractères de séparation. Par exemple :</p> <pre> <code>type|identifiant du type|role1-role2-role3 </code></pre><p>Dans ce cas on doit déjà gérer 2 types de séparateurs :le "|" pour les différents éléments, et le "-" pour les rôles.</p> <p>Dans notre exemple, ça donnerait :</p> <ul><li>societe|S1|RS1-RS2-RS3</li> <li>societe|S2|RS1-RS3</li> <li>utilisateur|U1|RU1-RU2</li> </ul><p>Par contre pour les recherches, ça devient plus compliqué, sauf à pré-calculer un filtre, en jouant avec les séparateurs.</p> <h2>Attribut Json</h2> <p>Depuis la version 5.5 (au moins), Forgerock permet d'utiliser une syntaxe Json dans les attributs, ce qui permet de gérer des données structurées à l'intérieur d'un attribut.</p> <h3>Déclaration dans le schéma</h3> <p>L'ajout d'un attribut de type JSON se fait de manière <em>traditionnelle</em>, en utilisant une syntaxe spécifique (<em>1.3.6.1.4.1.36733.2.1.3.1</em>).</p> <p>On ajoute ensuite les attributs dans le schéma :</p> <pre> <code>attributeTypes: ( 1.3.6.1.4.1.18472.2.101 NAME 'delegations' SYNTAX 1.3.6.1.4.1.36733.2.1.3.1 EQUALITY caseIgnoreJsonQueryMatch X-ORIGIN 'Mes tests json' ) attributeTypes: ( 1.3.6.1.4.1.18472.2.102 NAME 'jsonId' SYNTAX 1.3.6.1.4.1.36733.2.1.3.1 EQUALITY caseIgnoreJsonIdMatch SINGLE-VALUE X-ORIGIN 'Mes tests json' ) </code></pre><p>On doit également préciser le type de règle d'égalité qui sera utilisée. Il en existe 4 pré-définies :</p> <ul><li>caseIgnoreJsonQueryMatch : 2 objets sont identiques si tous leurs champs sont les mêmes, à la casse près</li> <li>caseExactJsonQueryMatch : 2 objets sont identiques si tous leurs champs sont les mêmes, avec la même casse de caractères</li> <li>caseIgnoreJsonIdMatch : 2 objets sont identiques si leur champ "_id" est identique, quelle que soit la casse de caractères</li> <li>caseExactJsonIdMatch : 2 objets sont identiques si leur champ "_id" est identique, avec la même casse de caractères.</li> </ul><p>Quelques exemples pour illustrer les cas. En supposant que j'ai 2 entrées dans l'annuaire LDAP, avec :</p> <ol><li>{"_id":"json","type":"user","value":"user123"}</li> <li>{"_id":"Json","type":"User","value":"USER123"}</li> <li>{"_id":"json","type":"user","value":"user345"}</li> <li>{"_id":"Json","type":"User","value":"USER345"}</li> </ol><p>Selon les cas :</p> <ul><li>avec la règle <strong>caseIgnoreJsonQueryMatch</strong>, les objets 1 et 2, et les objets 3 et 4 sont identiques</li> <li>avec la règle <strong>caseExactJsonQueryMatch</strong>, il n'y a pas de correspondance</li> <li>avec la règle <strong>caseIgnoreJsonIdMatch</strong>, les 4 objets sont identiques (ils ont le même _id)</li> <li>avec la règle <strong>caseExactJsonIdMatch</strong>, les objets 1 et 3 sont considérés identiques, ainsi que 2 et 4</li> </ul><h3>Indexation</h3> <p>Lorsqu'on indexe un attribut JSON, le comportement par défaut de Forgerock DS est de maintenir des clés pour chaque champ JSON. De ce fait, si l'attribut JSON contient de nombreux champs différents (et qui ne font pas l'objet de recherches) on peut choisir de n'indexer que ces champs. Dans ce cas, il faudra définir soi-même une règle (de type <em>json-equality-matching-rule</em>).</p> <p>Si on utilise une règle de comparaison basée sur l'ID uniquement, c'est ce champ qui sera indexé, et pas les autres.</p> <h3>Recherche</h3> <p>La syntaxe du filtre de recherche est la même que celle utilisée pour les API REST Forgerock (communément appelées CREST = Common REST).</p> <p>Quelques exemples de filtres, sur la base de nos attributs <em>delegations</em> et <em>jsonId</em> déclarés précédemment :</p> <pre> <code>delegations: {"type":"societe","attributions":[{"id":"societe1","roles":["role-soc1","role-soc2","role-soc3"]},{"id":"societe2","roles":["role-soc4"]}]} delegations: {"type":"utilisateur","attributions":[{"id":"user1","roles":["role-usr1","role-usr2"]},{"id":"user2","roles":["role-usr4"]}]} delegations: {"type":"org","attributions":[{"id":"org1","roles":["role-org1","role-org2","role-org4"]}] } jsonId: {"_id":"json","type":"user","value":"user123"} jsonId: {"_id":"Json","type":"User","value":"USER123"} jsonId: {"_id":"json","type":"user","value":"user345"} jsonId: {"_id":"Json","type":"User","value":"USER345"} </code></pre><ul><li>(delegations=*) : va renvoyer toutes les entrées qui ont un attribut <em>delegations</em></li> <li>(jsonid=_id eq 'json') va renvoyer les entrées qui ont la valeur 'json' dans le champ "_id" de l'attribut</li> <li>(delegations=type eq 'utilisateur' or type eq 'societe') : va renvoyer les objets ayant l'une ou l'autre valeur dans le champ type</li> </ul><p><strong>Limitations sur les filtres de recherche</strong></p> <p>Il n'est pas possible de faire une recherche dans un tableau qui contient plusieurs objets (attributions dans notre exemple).</p> <pre> <code>(delegations=id eq "user2") </code></pre><p>ne ramène rien (on n'y accède pas directement, il faudrait passer par attributions-&gt;id).</p> <p>Par contre, si on a une structure dont l'un des attributs est un tableau, qui ne contient que des valeurs, cela fonctionne.</p> <p>Si on a des attributs comme ceci :</p> <pre> <code>delegations: {"type":"utilisateur","id":"user1","roles":["role-usr1","role-usr2"]} delegations: {"type":"utilisateur","id":"user2","roles":["role-usr4","role-usr2"]} delegations: {"type":"societe","id":"societe1","roles":["role-soc1","role-soc2","role-soc3"]} delegations: {"type":"societe","id":"societe2","roles":["role-soc4"]} delegations: {"type":"org","id":"org2","roles":["role-org1","role-org2","role-org4"]} </code></pre><p>On peut bien faire des recherches de type :</p> <pre> <code>ldapsearch -b ou=people,dc=example,dc=com '(delegations=id eq "user2")' </code></pre><p>Si on veut éviter des collisions entre les différents types, on peut combiner plusieurs recherches. Si elles portent sur plusieurs éléments de l'attribut <em>json</em>, il faut utiliser le filtre adéquat. Par exemple :</p> <pre> <code>(delegations=id eq "2" and type eq "societe") </code></pre><p>La recherche dans un tableau fonctionne également. On peut ainsi rechercher tout les objets LDAP qui ont un role "role-usr4" :</p> <pre> <code>(delegations=roles eq "role-usr4") </code></pre><p>Idem pour les objets avec un type "societe" et un rôle "role-soc1" :</p> <pre> <code>(delegations=type eq "societe" and roles eq "role-soc1") </code></pre></div> <span class="field field--name-uid field--type-entity-reference field--label-hidden"><span>vincentl</span></span> <span class="field field--name-created field--type-created field--label-hidden"><time datetime="2022-03-04T14:55:34+01:00" title="Vendredi 4 March 2022 - 14:55" class="datetime">ven 04/03/2022 - 14:55</time> </span> <div class="field field--name-field-categorie field--type-entity-reference field--label-above"> <div class="field__label">Catégorie</div> <div class="field__item"><a href="/index.php/cat%C3%A9gorie/iam" hreflang="fr">IAM</a></div> </div> <div class="field field--name-field-tags field--type-entity-reference field--label-above"> <div class="field__label">Tag</div> <div class="field__items"> <div class="field__item"><a href="/index.php/tags/forgerock" hreflang="fr">forgerock</a></div> <div class="field__item"><a href="/index.php/tags/opendj" hreflang="fr">opendj</a></div> <div class="field__item"><a href="/index.php/tags/ds" hreflang="fr">ds</a></div> <div class="field__item"><a href="/index.php/tags/json" hreflang="fr">json</a></div> </div> </div> <section class="field field--name-comment-node-book field--type-comment field--label-hidden comment-wrapper"> <h2 class="title comment-form__title">Ajouter un commentaire</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=214&amp;2=comment_node_book&amp;3=comment_node_book" token="sYSuQMghlx0xCAUDqXvngNEvgVyPnyCe-RJ6u6n_6dw"></drupal-render-placeholder> </section> Fri, 04 Mar 2022 13:55:34 +0000 vincentl 214 at https://www.vincentliefooghe.net