Jouons avec les Linux Pluggable Authentication Modules

Spécialité(s)


Résumé

Au cœur de la gestion des utilisateurs et de leurs permissions, le système GNU/Linux recèle un mécanisme modulaire et extensible pour faire face à tous les usages actuels et futurs, liés à la preuve d'identité. Intéressons-nous, à travers un cas pratique, à ces modules interchangeables d'authentification, utiles tant aux applicatifs qu'au système lui-même.


Body

Après une présentation simplifiée du mécanisme des Linux Pluggable Authentication Modules, nous appliquerons nos connaissances sur un cas pratique (vécu) qui, sans receler une grande complexité technique, nous suffira à illustrer le processus. Il s'agira d'exploiter l'architecture PAM en compilant un module personnalisé pour permettre l'exécution d'une action au moment d'une connexion FTP entrante.

1. Un peu de théorie

Le moyen le plus élémentaire de se connecter à un système consiste à s'identifier avec un username et à se justifier avec un mot de passe. À l'instar de l'ancêtre UNIX, l'annuaire des comptes est sur fichier (/etc/passwd et /etc/shadow pour les hachés de mots de passe).

Mais, pour démarrer une session, on peut imaginer un grand nombre de scénarios que ce mécanisme primitif ne permet pas de réaliser. Listons quelques exemples :

  • la présentation d'une clé privée au lieu d'un mot de passe ;
  • l'utilisation d'un moyen biométrique (empreinte digitale, etc.) ou d'une carte à puce ;
  • la centralisation de la gestion des comptes d'entreprise à l'extérieur de l'annuaire-fichier de la station locale ;
  • l'attribution de permissions particulières sur certaines fonctionnalités d'une application, sans relation avec les droits locaux d'un compte sur le système ;
  • la limitation du nombre de connexions d'un compte donné, par lieu, budget ou unité de temps ;
  • la délégation de connexion (ou SSO, Single Sign-On) ; c'est la possibilité d'utiliser des logiciels et ressources réseau requérant des comptes applicatifs, sans avoir besoin de s'y connecter séparément, mais en exploitant le fait que la session globale déjà ouverte puisse faire foi.

S'il incombe toujours à l'utilisateur d'indiquer qui il est au moyen d'un username (c'est ce qu'on appelle l'identification), nous venons de mettre en évidence trois aspects fondamentaux que le simple couple username+password du compte système ne permet pas de couvrir :

  1. La standardisation de l'authentification (c'est-à-dire des moyens de prouver son identité). Il importe peu, en principe, que la personne souhaite utiliser telle preuve d’identité plutôt que telle autre, du point de vue du programme auquel elle accède.
  2. La généralisation de l'autorisation (c'est-à-dire, des droits et habilitations). Une authentification réussie n'est pas suffisante pour déterminer finement les permissions de l'utilisateur sur l'ensemble des infrastructures et des fonctionnalités applicatives.
  3. Le tout, d'une façon mutualisée et modulaire, pour permettre aux applications d'obtenir ces services sans devoir réimplémenter les techniques correspondantes ni avoir directement accès aux annuaires du système ou de l'organisation.

Par exemple, en déléguant les services d'authentification et d'autorisation, un logiciel pourrait ouvrir une session grâce à la reconnaissance faciale plutôt que par mot de passe, sans devoir implémenter une lib de détection de visages, ni se préoccuper lui-même de l’expiration des permissions.

2. Pluggable Authentication Modules

Pour répondre à ces besoins, GNU/Linux déploie un mécanisme appelé PAM (Pluggable Authentication Modules) [1], qui est en fait une API offerte aux programmes pour leur fournir de façon transparente des services d'authentification, d'autorisation et de contrôle d'ouverture de sessions. De cette façon, PAM opère une séparation entre les applications d'une part, et de l'autre les standards et bonnes pratiques liés au contrôle d'identité.

2.1 Introduction

Dans la pratique, il s'agit d'une collection extensible de shared objects fédérés autour de libpam.so, qui répondent tous à une même interface exposant les fonctions suivantes :

  • pam_sm_authenticate
  • pam_sm_setcred
  • pam_sm_acct_mgmt
  • pam_sm_chauthtok
  • pam_sm_open_session
  • pam_sm_close_session

En s'adressant à un ou plusieurs modules PAM, une application peut poser les questions énoncées en figure 1 : l'utilisateur est-il réellement qui l'on croit ? Est-il autorisé à utiliser ce qu'il demande ? Peut-il le faire maintenant ?

Toutes ces fonctions reçoivent en paramètre des informations sur le candidat qui se présente et ses pièces d'identité, à travers une structure opaque pam_handle_t interrogeable par des fonctions utilitaires comme pam_get_user [2] fournies par le framework. En retour, ces points d’entrée répondent soit la valeur PAM_SUCCESS soit un code d'échec (PAM_USER_UNKNOWN, PAM_AUTH_ERR, PAM_SERVICE_ERR…).

pam figure 01-s

Fig. 1 : Schéma général des principales fonctions mises en œuvre dans un module PAM, et séquence d'interaction avec l'application ou le système.

Les systèmes GNU/Linux apportent toute une famille de modules PAM standard avec le projet Linux-PAM [3] et il est bien sûr possible, par définition, d'en construire de nouveaux pour répondre à des exigences spécifiques. Les shared objects sont à placer dans /lib/x86_64-linux-gnu/security/ (selon les distributions).

Tous les modules PAM ne sont pas obligés d'intervenir à tous ces niveaux : en fait, certains modules se spécialisent dans l'une ou l'autre de ces tâches. D'autres agissent à tous les étages et traitent tous les aspects liés à une certaine technologie. On trouvera par exemple le module pam_krb5 [4], qui prend en charge le protocole Kerberos [5; le module pam_ldap [6] gère quant à lui la détermination d'habilitations auprès d'un serveur LDAP [7; la lib pam_google_authenticator, de la société éponyme, pour un code à usage unique OTP [8] [14] ; ou encore pam_mysql [9] pour placer l'annuaire des utilisateurs dans une table MySQL et laisser un programme (ou le système) contrôler les accès depuis cette source plutôt que dans le fichier /etc/passwd.

En outre, un même programme est libre d'utiliser autant de modules que nécessaire, et il peut les assembler de façon optionnelle ou requise, afin de construire des stratégies sur mesure. Dans la terminologie PAM, un assemblage donné (matérialisé dans un fichier, voir 2.2) s'appelle un « service ».

Certains modules sont strictement utilitaires, pour élaborer des enchaînements évolués : c'est le cas de pam_succeed_if, très pratique comme agent de liaison pour évaluer des conditions passées en paramètre, au sein d'un scénario complexe de contrôle à l'ouverture de session.

Ainsi, il est possible d'obtenir délibérément certains effets secondaires (désirables), par exemple en déclenchant systématiquement telle couche de tel module dès que l'authentification par une autre méthode est réussie – comme nous le verrons dans la suite.

2.2 Configuration

Une application qui souhaite utiliser l'architecture PAM doit indiquer quels modules elle veut exécuter, dans quel ordre, et sur quel niveau d'exigence.

Cela se réalise en groupant les règles à mettre en œuvre dans un ficher texte, à placer dans /etc/pam.d/. Le fichier de configuration est au format PAM [10], c'est-à-dire une succession de directives de cette forme :

[type] [control] [module-path] [module-arguments]

dont voici la signification des champs :

  • type indique la phase : entre autres, auth, account, session ;
  • control : parmi required, sufficient, optional, et encore d'autres valeurs permettant de construire des enchaînements conditionnels et complexes ;
  • module-path : le chemin du fichier .so du module invoqué. Le binaire se trouve dans /lib/x86_64-linux-gnu/security/ (selon les distributions) ;
  • module-arguments : une liste optionnelle d'arguments à passer au module, généralement de la forme nom1=valeur1 nom2=valeur2.

On peut même utiliser la directive @include <autre-fichier> pour organiser les règles.

Comme dit plus haut, un même fichier peut contenir plusieurs directives pour un même type, qui se combinent selon les règles assemblées grâce au champ control.

Ainsi, une application n'aura qu'à spécifier au framework PAM un nom de « service » (c'est-à-dire un nom de fichier PAM dans le répertoire standard) pour obtenir de façon transparente une réponse de succès/rejet pour chacune des phases (c'est-à-dire chaque type).

Nous nous en tiendrons à ce survol pour ce qui est de la théorie. Étudions maintenant un cas pratique auquel je me suis trouvé confronté.

3. Atelier

Une petite entreprise propose un service en ligne à des clients dont elle gère les comptes utilisateurs dans une base de données. Elle veut leur fournir la possibilité de téléverser des rapports par FTP. Certes, le protocole est vieux et présente beaucoup d'inconvénients, mais hélas, le secteur l'exige. En effet, ces clients (dont l'activité est le commerce de gros d'une marchandise fortement réglementée) utilisent un certain logiciel métier très largement dominant (pour ne pas dire totalement monopoliste) qui ne sait transmettre ses états que par FTP (et dont l'interface n'est pas sans rappeler l'âge de Bronze).

Les fichiers ne présentent aucun caractère confidentiel, la seule contrainte est que chaque utilisateur dépose bien son fichier dans son bon dossier à lui.

3.1 Le besoin

Les clients de cette entreprise sont déclarés et gérés dans une base MySQL. Chaque utilisateur doit pouvoir se connecter à son propre répertoire FTP ; la création d'un utilisateur, l'invalidation ou le changement de mot de passe se font au niveau MySQL, mais la conséquence doit être immédiate quant à l'acceptation de connexions sur le serveur FTP.

Le principe d'un répertoire FTP par compte utilisateur est extrêmement classique et fait penser bien sûr aux bons vieux hébergeurs de « pages web perso ». Dans le cas qui nous occupe, il ne s'agit pas de stockage. Les fichiers ne sont là qu'en transit et doivent être pris en charge dès leur téléversement, de façon entièrement automatisée, à travers une chaîne de traitement qui se trouve dans le cloud d'AWS. C'est donc tout naturellement que mon conseil s'est d'abord porté sur la mise en œuvre du produit AWS Transfer Family, dont c'est précisément la finalité (mais qui est cher, et nécessite l'ajout d'ingrédients supplémentaires, notamment AWS Lambda, pour pouvoir s'interconnecter avec une base de données). Cependant, j'ai profité de l'occasion pour proposer une solution plus artisanale, mais peu coûteuse (surtout pour la faible volumétrie envisagée), et qui a eu le mérite de me permettre de décortiquer les PAM.

Dans ce qui suit, nous ne nous intéresserons pas à la partie qui concerne le déclenchement automatique d'un traitement à l'arrivée d'un fichier sur le disque dur, qui dépasse le cadre de cet article. Nous nous pencherons sur le branchement du service FTP sur la base de données et l’acceptation transparente des connexions.

3.2 Les briques existantes

Nous utiliserons un serveur vsFTPd [11] en version 3.0.3. Il permet d'avoir des comptes utilisateurs virtuels à partir d'une liste arbitraire, plutôt que de réels comptes GNU/Linux locaux. Chaque utilisateur se voit attribuer un répertoire racine en toute sécurité ; vsFTPd sait utiliser le framework PAM, ce qui le rend indirectement capable de puiser son annuaire de comptes dans une base de données MySQL distante, comme nous le verrons dans la suite.

Ce montage, qui semble donc avoir tout pour lui, souffre pourtant d'un inconvénient majeur : pour fonctionner, il faut que le répertoire racine du compte existe déjà au moment de la connexion d'un arrivant, sans quoi celle-ci échoue. Or, la gestion des utilisateurs est centralisée au niveau de la base de données, et lorsqu'un nouveau compte est créé, il est exclu de devoir aller se connecter sur la(les) machine(s) FTP pour y créer un nouveau répertoire au nom dudit utilisateur. Il faut absolument que les nouveaux répertoires soient créés à la volée sur l'instance FTP lors de la connexion.

Nous allons apporter une solution à ce problème grâce à la mécanique du PAM.

3.3 vsFTPd et PAM

En premier lieu, voici pour installer le serveur FTP :

# apt install vsftpd

Le daemon se configure [12] au moyen du fichier /etc/vsftpd.conf, aux paramètres abscons, il faut bien le dire. Nous y précisons les options suivantes (celles déjà présentes par défaut dans le fichier, et qui ne sont pas mentionnées ici, peuvent rester inchangées) :

anonymous_enable=NO
local_enable=YES
guest_enable=YES
virtual_use_local_privs=YES
 
local_root=/chemin/vers/racine_ftp/USERDIR
user_sub_token=USERDIR
chroot_local_user=YES
allow_writeable_chroot=YES
write_enable=YES
 
pam_service_name=vsftpd_mysql

Nous interdisons l'accès anonyme, mais nous autorisons les connexions nominatives avec local_enable (le terme est mal choisi, à mon avis) et nous acceptons par guest_enable les utilisateurs qui ne sont pas des comptes GNU/Linux physiques. Il faut également conférer à ces « faux » comptes les permissions du compte de service vsFTPd (qui est par défaut le compte système ftp, contrôlable avec l'option guest_username), ce que nous faisons en activant virtual_use_local_privs.

Ici, local_root représente la convention de nommage des répertoires racines, et user_sub_token indique l'élément textuel qui, dans le pattern précédent, sera à remplacer par le nom du compte. Puis, nous « chrootons » les arrivants dans leur propre répertoire racine (ils ne peuvent pas remonter plus haut avec la commande FTP cd ..).

Enfin, concernant la gestion des comptes, vsFTPd délègue l'authentification au moteur PAM via une configuration arbitraire à préciser dans l'option pam_service_name. La chaîne spécifiée est le nom d'un fichier que nous allons créer dans le répertoire /etc/pam.d/ et qui sera au format vu en 2.2.

Pour notre atelier, nous avons besoin d’un module PAM spécialisé dans l’interrogation d’une table MySQL. C’est le travail du projet pam-mysql [9], qui n’est pas un module standard, et que nous devons soit installer depuis les dépôts (libpam-mysql) soit compiler à partir des sources.

Pour la compilation sur Debian 11, les paquets à installer sont :

# apt install build-essential libpam-dev \
    libssl-dev meson libmariadb-dev \
    libmariadb-dev-compat gcc-10 \
    openssl pkg-config

Après quoi, une fois cloné le repository Git, le build s’obtient par :

pam-MySQL # mkdir build && meson build
...
pam-MySQL # cd build
pam-MySQL/build # ninja
...
[31/31] Linking target pam_mysql.so

Puis, déplaçons le fichier pam_mysql.so obtenu vers le répertoire /lib/x86_64-linux-gnu/security/.

3.4 La table SQL

Nous supposerons que nous disposons d'un serveur MySQL, à l'adresse db_host, servant une base de données nommée db_name, qui contient une table tbl_ftp.

Cette table présente au moins les deux colonnes suivantes :

mysql> select database();
+------------+
| database() |
+------------+
| db_name    |
+------------+
1 row in set (0.00 sec)mysql> select ftp_user, ftp_pass from tbl_ftp;
+----------+----------------------------------+
| ftp_user | ftp_pass                         |
+----------+----------------------------------+
| anne     | 37b51d194a7513e45b56f6524f2d51f2 |
| denis    | acbd18db4cc2f85cedef654fccc4a4d8 |
+----------+----------------------------------+
2 rows in set (0.00 sec)

Les noms de la table et des champs ne sont pas imposés ; nous renseignerons les nôtres dans la configuration en 3.5.

J'ai choisi ici un hachage MD5 pour les mots de passe FTP (plusieurs autres choix précâblés sont possibles ; se référer à la documentation du pam-mysql).

Naturellement, la table peut contenir d'autres colonnes, et l'on pourra même préférer une View si cela s'avère plus utile.

Il reste à dédier un compte MySQL spécifique sur cette table, à l'usage exclusif du serveur vsFTPd, afin de réduire la surface d’attaque de notre solution. Appelons-le ftp, avec un mot de passe secret (n'hésitez pas à en restreindre l'accès depuis le réseau sur lequel se trouve le serveur FTP).

mysql> create user 'ftp' identified by 'secret';
Query OK, 0 rows affected (0.02 sec)
 
mysql> grant select on db_name.tbl_ftp to 'ftp';
Query OK, 0 rows affected (0.01 sec)

3.5 PAM et MySQL

Nous pouvons à présent construire un fichier de « service » PAM pour vsFTPd.

Commençons par reporter dans un fichier texte les éléments de connexion MySQL, à un emplacement arbitraire, par exemple /svr/vsftpd_mysql_conn.conf :

users.host = db_host
users.database = db_name
users.db_user = ftp
users.db_passwd = secret
users.table = tbl_ftp
users.user_column = ftp_user
users.password_column = ftp_pass
users.password_crypt = md5

Notez l'option users.password_crypt, à faire correspondre au choix précédent pour le hachage.

On s’est contenté ici d’indiquer un nom de table et deux noms de champs, desquels le module déduira une simple requête SELECT WHERE. Mais il est également possible de spécifier une requête plus complexe, avec des jointures et une clause WHERE plus élaborée.

Créons à présent le fichier de « service PAM » /etc/pam.d/vsftpd_mysql comme ceci (le nom vsftpd_mysql correspond à ce que nous avons renseigné pour pam_service_name dans /etc/vsftpd.conf en 3.3) :

auth     sufficient pam_mysql.so config_file=/srv/vsftpd_mysql_conn.conf
account sufficient pam_permit.so

Détaillons ce que nous venons de faire.

Nous déclarons à vsFTPd que sa gestion de l'identité se fait par le framework PAM, et en particulier en déroulant les règles du fichier ci-dessus.

La règle auth se doit d'être présente, car elle indique à vsFTPd quoi faire lorsqu'un utilisateur FTP se présente au portail avec son username et son mot de passe. Nous indiquons ici via pam_mysql.so qu'il suffira qu'une ligne existe dans notre table, pour le couple <username, password> selon le paramétrage fourni dans vsftpd_mysql_conn.conf. Lors de cette phase, la libpam a invoqué la fonction pam_sm_authenticate du module pam_mysql.so.

La règle account est également nécessaire, car vsFTPd attend obligatoirement une réponse sur la validité du compte. En effet, comme évoqué plus haut, même si l'identification réussit, l'abonnement est peut-être expiré, ou la connexion n'est permise que les lundis et jeudis, etc. Nous pourrions utiliser pam-mysql là encore, car il sait gérer cette phase (il implémente pam_sm_acct_mgmt et permet de préciser une autre colonne de la table pour le calcul de validité du compte ; je laisserai le lecteur approfondir la documentation du projet).

Pour faire simple, nous allons ici mettre en œuvre le module standard pam_permit, qui est en quelque sorte le Yes-Module du PAM : du moment que notre auth est réussie, alors nous forçons un succès aussi pour la validité du compte FTP. Voici d’ailleurs ce qu’on peut lire dans le code source pam_permit.c :

int
pam_sm_acct_mgmt(pam_handle_t *pamh UNUSED,
            int flags UNUSED,
           int argc UNUSED,
           const char **argv UNUSED)
{
     return PAM_SUCCESS;
}

3.6 Connexion et répertoire maison

Relançons le serveur FTP pour qu’il prenne en compte la configuration :

root@serveur-ftp:~# service vsftpd restart
Stopping FTP server: vsftpd.
Starting FTP server: vsftpd.

et tentons la connexion :

user@client-ftp:~$ ftp serveur-ftp
Connected to serveur-ftp.
220 (vsFTPd 3.0.3)
Name (localhost:user): anne
331 Please specify the password.
Password: *****
500 OOPS: cannot change directory:/srv/ftp/anne
Login failed.
421 Service not available, remote server has closed connection

Le mot de passe est correct, pourtant c'est l'échec : comme alerté précédemment, le répertoire calculé pour un utilisateur entrant n'est pas automatiquement créé, or sa présence est nécessaire pour l'établissement de la connexion.

À présent, créons un répertoire pour un autre utilisateur de notre table :

root@serveur-ftp:~# mkdir /srv/ftp/denis
root@serveur-ftp:~# touch /srv/ftp/denis/fichier-denis
root@serveur-ftp:~# chown -R ftp:ftp /srv/ftp/denis

et tentons d'entrer :

user@client-ftp:~$ ftp serveur-ftp
Connected to serveur-ftp.
220 (vsFTPd 3.0.3)
Name (localhost:user): denis
331 Please specify the password.
Password:
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> ls
200 PORT command successful. Consider using PASV.
150 Here comes the directory listing.
-rw-------    1 102 104     0 Jun 19 10:15 fichier-denis
226 Directory send OK.
ftp>

Nous pouvons illustrer en figure 2 la mécanique en présence.

pam figure 02-s 1

Fig. 2 : Séquence de validation PAM pour vsFTPd avec MySQL pour l’authentification et pam_permit pour le droit d’entrée.

Nous voyons notamment qu'il est nécessaire de provoquer la création automatique du répertoire maison de l'utilisateur entrant, avant la phase session du mécanisme PAM.

C'est le moment de remplacer le pam_permit.so de notre règle account par un autre module, personnalisé.

3.7 D'autres briques ?

Avant de passer à l'écriture d'un module sur mesure dédié à la création d'un sous-dossier, disons quelques mots sur d'autres possibles candidats.

Le toolkit standard [3] contient un module a priori intéressant : pam_mkhomedir.so. En effet, il sert à créer si nécessaire le répertoire maison pour un compte listé dans un annuaire externe (par exemple LDAP) s'il ouvre pour la première fois une session GNU/Linux sur une station donnée. Cependant, cela ne fonctionnera pas pour nous ici, car le compte dont il est question est un compte système, ce qui n'est pas le cas de nos utilisateurs FTP virtuels.

L'idée est bonne, et en fin de compte tout ce qu'on souhaite, c'est effectuer un mkdir... Penchons-nous dès lors sur cet autre module du toolkit : pam_exec.so. Il a pour rôle d'exécuter une commande arbitraire, indiquée en argument dans la règle de service (à n'importe quelle phase), en passant par variables d'environnement les données de la structure pam_handle_t (voir 2.1.).

On aimerait pouvoir s'en servir, car alors il nous suffirait d'ajouter une petite règle comme ceci :

account required pam_exec.so /bin/bash -c 'mkdir -p /srv/ftp/$PAM_USER'

Mais il y a un problème : le vsftpd du package pour Debian a un bug, le daemon reste bloqué si un module PAM effectue un appel aux fonctions popen (et famille) pour lancer un sous-processus. Or, c'est sur cette distribution que je me trouve. Le bug n'est pas présent dans le package pour CentOS (a-t-il été patché ?), et une solution pourrait être de compiler les sources de vsFTPd de cette distribution [13].

Mais profitons de l'opportunité pour réaliser un module dédié, que nous appellerons : pam_mkvirtdir.so.

3.8 Le code

Pour ce faire, le plus simple est de s'inspirer des sources du module pam_permit, et un peu de celles de pam_mkhomedir.

Notre module se concentre sur la phase account. Nous laisserons les autres phases répondre la valeur PAM_USER_UNKNOWN.

Nous décidons que notre module s'invoquera dans une règle sous la forme suivante, mise en œuvre dans cette nouvelle mouture du fichier /etc/pam.d/vsftpd_mysql :

auth     sufficient pam_mysql.so config_file=/srv/vsftpd_mysql_conn.conf
account required pam_mkvirtdir.so owner=ftp group=ftp parent_dir=/srv/ftp

La séquence devient celle présentée en figure 3.

pam figure 03-s

Fig. 3 : Séquence de validation PAM avec utilisation de pam_mkvirtdir pour la phase account.

Voici donc le code de notre pam_mkvirtdir.c. Commençons par les #include requis pour travailler avec le framework Linux-PAM, et les quelques fonctions sur les permissions et répertoires avec lesquels nous travaillerons.

01: #include "config.h"
02:
03: #include "pam_inline.h"
04: #include <syslog.h>
05: #include <stdio.h>
06:
07: #include <security/_pam_macros.h>
08: #include <security/pam_modutil.h>
09: #include <security/pam_ext.h>
10: #include <security/pam_modules.h>
11:
12: #include <sys/types.h>
13: #include <sys/stat.h>
14:
15: #include <grp.h>
16: #include <pwd.h>

Débarrassons-nous tout de suite des fonctions de l'API que nous ne voulons pas implémenter, et qui répondront un simple code d'erreur.

17:
18: /* --- authentication management functions --- */
19: int
20: pam_sm_authenticate(pam_handle_t *pamh, int flags UNUSED,
21:         int argc UNUSED, const char **argv UNUSED)
22: {
23:     return PAM_USER_UNKNOWN;
24: }
25:
26: int
27: pam_sm_setcred(pam_handle_t *pamh UNUSED, int flags UNUSED,
28:            int argc UNUSED, const char **argv UNUSED)
29: {
30:     return PAM_USER_UNKNOWN;
31: }
32:
33: /* --- password management --- */
34:
35: int
36: pam_sm_chauthtok(pam_handle_t *pamh UNUSED, int flags UNUSED,
37:         int argc UNUSED, const char **argv UNUSED)
38: {
39:     return PAM_USER_UNKNOWN;
40: }
41:
42: /* --- session management --- */
43:
44: int
45: pam_sm_open_session(pam_handle_t *pamh UNUSED, int flags UNUSED,
46:             int argc UNUSED, const char **argv UNUSED)
47: {
48:     return PAM_USER_UNKNOWN;
49: }
50:
51: int
52: pam_sm_close_session(pam_handle_t *pamh UNUSED, int flags UNUSED,
53:          int argc UNUSED, const char **argv UNUSED)
54: {
55:     return PAM_USER_UNKNOWN;
56: }
57:

Laissons de la place pour quelques fonctions utilitaires que nous ajouterons ensuite, mais passons maintenant à la phase PAM qui nous intéresse : pam_sm_acct_mgmt.

Voici ce que nous pouvons programmer :

103:
104: int
105: pam_sm_acct_mgmt(pam_handle_t *pamh, int flags UNUSED,
106:             int argc, const char **argv)
107: {
108:
109:     int optargc;
110:     const char * use_owner_user = NULL;
111:     const char * use_owner_group = NULL;
112:     const char * use_parent_dir = NULL;
113:
114:     if (argc < 3) {
115:       pam_syslog (pamh, LOG_ERR,
116:         "This module needs at least 3 arguments");
117:       return PAM_SERVICE_ERR;
118:     }
119:
120:     for (optargc = 0; optargc < argc; optargc++)
121:     {
122:       const char *str;
123:
124:       if ((str = pam_str_skip_icase_prefix (argv[optargc], "owner=")) != NULL) {
125:         use_owner_user = str;
126:       } else if ((str = pam_str_skip_icase_prefix (argv[optargc], "group=")) != NULL) {
127:         use_owner_group = str;
128:       } else if ((str = pam_str_skip_icase_prefix (argv[optargc], "parent_dir=")) != NULL) {
129:         use_parent_dir = str;
130:       }
131:     }
132:
133:     if (NULL == use_owner_user) {
134:       pam_syslog (pamh, LOG_ERR,
135:         "pam_mkvirtdir: owner required");
136:       return PAM_SERVICE_ERR;
137:     }
138:     if (NULL == use_owner_group) {
139:       pam_syslog (pamh, LOG_ERR,
140:         "pam_mkvirtdir: group required");
141:       return PAM_SERVICE_ERR;
142:     }
143:     if (NULL == use_parent_dir) {
144:       pam_syslog (pamh, LOG_ERR,
145:         "pam_mkvirtdir: parent_dir required");
146:       return PAM_SERVICE_ERR;
147:     }
148:
149:     create_virt_dir(pamh, use_owner_user, use_owner_group, use_parent_dir);150:     return PAM_SUCCESS;
151: }
152:

La fonction create_virt_dir est celle qui effectue le travail. Elle ne pose pas de difficulté :

83:
84: void create_virt_dir(
85:         pam_handle_t *pamh,
86:         const char* use_owner_user,
87:         const char* use_owner_group,
88:         const char* use_parent_dir)
89: {
90:
91:     const char *user_name = NULL;
92:     pam_get_user(pamh, &user_name, NULL);
93:     char targetDir[250];
94:     strcpy(targetDir, use_parent_dir);
95:     if (targetDir[strlen(targetDir) - 1] != '/') {
96:         strcat(targetDir, "/");
97:     }
98:     strcat(targetDir, user_name);
99:     mkdir(targetDir, 0770);
100:     do_chown(targetDir, use_owner_user, use_owner_group);
101: }
102:

Il y a encore besoin d'une petite fonction utilitaire, do_chown, pour poser les permissions demandées sur ce nouveau répertoire :

58:
59: void do_chown (const char *file_path,
60:                const char *user_name,
61:                const char *group_name)
62: {
63:   uid_t          uid;
64:   gid_t          gid;
65:   struct passwd *pwd;
66:   struct group *grp;
67:
68:   pwd = getpwnam(user_name);
69:   if (pwd == NULL) {
70:       return;
71:   }
72:   uid = pwd->pw_uid;
73:
74:   grp = getgrnam(group_name);
75:   if (grp == NULL) {
76:       return;
77:   }
78:   gid = grp->gr_gid;
79:
80:   chown(file_path, uid, gid);
81: }

Il conviendrait bien sûr de traiter correctement tous les cas d'erreur, mais cela dépasse le cadre de cette expérimentation.

3.9 Compilation

Le lecteur attentif aura remarqué la présence d'un #include "config.h" au début du fichier pam_mkvirtdir.c : ce header n'existe pas encore, il s'agit d'un fichier généré que nous allons produire en procédant à la préconfiguration automatique du projet Linux-PAM.

Tout d'abord clonons, puis suivons les instructions du README [3] pour l'installation des dépendances et l'autoconfiguration :

~# git clone https://github.com/linux-pam/linux-pamCloning into 'linux-pam'...
remote: Enumerating objects: 20855, done.
remote: Counting objects: 100% (4859/4859), done.
remote: Compressing objects: 100% (1469/1469), done.
remote: Total 20855 (delta 3480), reused 3398 (delta 3390), pack-reused 15996
Receiving objects: 100% (20855/20855), 6.23 MiB | 7.34 MiB/s, done.
Resolving deltas: 100% (17220/17220), done.
 
~# cd linux-pam
~/linux-pam# ci/install-dependencies.sh
...
 
~/linux-pam# ./autogen.sh
+ umask 022
+ touch ChangeLog
+ autoreconf -fiv -Wall
autoreconf: Entering directory `.'
autoreconf: running: autopoint --force
Copying file ABOUT-NLS
Creating directory build-aux
Copying file build-aux/config.rpath
Copying file m4/codeset.m4
...
libtoolize: copying file 'm4/libtool.m4'
libtoolize: copying file 'm4/ltoptions.m4'
libtoolize: copying file 'm4/ltsugar.m4'
autoreconf: Leaving directory `.'
 
~/linux-pam# ./configure.sh
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... /bin/mkdir -p
checking for gawk... no
checking for mawk... mawk
checking whether make sets $(MAKE)... yes
checking whether make supports nested variables... yes
checking build system type... x86_64-pc-linux-gnu
...
config.status: creating config.h
config.status: executing depfiles commands
config.status: executing libtool commands
config.status: executing po-directories commands
config.status: creating po/POTFILES
config.status: creating po/Makefile

Après quoi, il n'est pas nécessaire de compiler tous les modules du toolkit, nous allons nous contenter du nôtre. Créons un répertoire modules/pam_mkvirtdir, puis plaçons-y notre source C, et enfin, compilons.

~/linux-pam/modules/pam_mkvirtdir# gcc -fPIC -shared \
   -o pam_mkvirtdir.so \
   -I../.. \
   -I../../libpam/include \
   -I ../../libpamc/include/security \
   pam_mkvirtdir.c

3.10 Mise en œuvre

Après avoir déplacé le binaire obtenu vers le répertoire des modules PAM /lib/x86_64-linux-gnu/security/, comme nous l’avons fait pour le module MySQL, il est temps de redémarrer le service vsftpd, et de ressayer la connexion.

user@client-ftp:~$ ftp serveur-ftp
Connected to serveur-ftp.
220 (vsFTPd 3.0.3)
Name (localhost:user): anne
331 Please specify the password.
Password:230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> ls
200 PORT command successful. Consider using PASV.
150 Here comes the directory listing.
226 Directory send OK.
ftp>

La connexion est acceptée ; contrôlons que le répertoire a bien été créé automatiquement :

root@serveur-ftp:~# ls -la /srv/ftp
drwxr-xr-x 2 ftp ftp 4096 Jun 19 11:42 anne
drwxr-xr-x 2 ftp ftp 4096 Jun 19 10:15 denis

Tout fonctionne.

Conclusion

L'avenir n'est sans doute pas dans le protocole FTP, mais parfois nous n'avons pas la maîtrise des contraintes imposées et il faut bien trouver des solutions.

Pour obtenir le résultat de cette expérimentation, à savoir s'assurer de la bonne création du répertoire racine d'un utilisateur FTP virtuel, plusieurs autres voies auraient pu faire l'affaire, comme modifier directement le code de vsFTPd ou mettre en place un polling avec cron. Mais tout compte fait, la manipulation des Linux PAM fut l'occasion pour moi de découvrir cet aspect fort intéressant de notre cher système, et sur lequel j'espère avoir réussi à attiser votre curiosité !

Références

[1] Le site du projet Linux-PAM : http://www.linux-pam.org/

[2] Manuel des structures et fonctions du framework Linux-PAM :
https://man7.org/linux/man-pages/man3/pam_get_user.3.html

[3] Les modules standard du toolkit Linux-PAM : https://github.com/linux-pam/linux-pam

[4] Le module PAM Kerberos : https://manpages.ubuntu.com/manpages/trusty/man5/pam_krb5.5.html

[5] Le protocole Kerberos : https://web.mit.edu/kerberos/

[6] Le module PAM LDAP : https://wiki.debian.org/LDAP/PAM

[7] Lightweight Directory Access Protocol : https://ldap.com/

[8] Le module OTP de Google : https://github.com/google/google-authenticator-libpam

[9] PAM et MySQL : https://github.com/NigelCunningham/pam-MySQL

[10] Format de configuration du fichier PAM :
http://www.linux-pam.org/Linux-PAM-html/sag-configuration-file.html

[11] Le serveur vsFTPd : https://security.appspot.com/vsftpd.html

[12] Les options de configuration de vsFTPd : https://security.appspot.com/vsftpd/vsftpd_conf.html

[13] Bug de vsFTPd sur Debian pour les modules PAM créant un sous-processus :
https://askubuntu.com/a/778448/863347

[14] « L’authentification par mot de passe unique » - Linux Pratique n°120 :
https://connect.ed-diamond.com/Linux-Pratique/lp-120/l-authentification-par-mot-de-passe-unique



Article rédigé par

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous