Selenith
Projets, mémos et reflexions personnelles

Authentification centralisée via certificat p12 et nginx

Publié le 21/06/2026

Ça fait pas mal d'années que je n'utilise plus de mots de passe pour m'authentifier sur la plupart des services que j’auto-héberge. J'utilise à la place des certificats p12. Le problème c'est que je commence à en avoir un paquet car j'en utilise un pour chaque service et pour chaque terminal. Je me posais donc la question de pouvoir centraliser l'authentification comme avec un LDAP, mais avec des certificats à la place des login/mot de passe. Et comme souvent, les arsouyes ont écrit un article qui tombait à pic pour m'aiguiller dans la bonne direction. Alors, met ton casque, prend une lanterne et fait un bisou à tes proche avant de lire la suite, parce qu'on va utiliser des normes obscures et faire un peu de magie noire pour pouvoir réussir à mettre en œuvre un système d'authentification centralisée via certificats p12 !

Clé dorée émettant un halo de lumière, entourée par quatre cadenas.

Ah oui, cette illustration est générée par IA.

Mais sur mon propre PC alimenté 100% par du solaire !

Le contexte

Il y a quelques temps j'ai fini par briquer mon téléphone[1] à force de le bidouiller.

Évidemment j'avais tout sauvegardé !

...sauf mes flux RSS.

Je me suis alors dit qu'il me fallait un système qui me permettait de centraliser mes flux RSS hors de mon téléphone et que ce serait aussi l'occasion d'avoir quelque chose que je pourrais lire avec le confort de mon PC fixe. Mais après avoir longuement cherché, rien n'était satisfaisant et surtout je n'en ai trouvé aucun qui fonctionnait bien avec une authentification par certificat.

Et c'est la que les arsouyes ont publiés un article sur l'authentification LDAP via apache ! Et la illumination ! Je me suis dit que si on pouvait utiliser cette méthode avec LDAP on pouvait le faire avec à peu près n'importe quoi[2] , y compris un système qui centraliserait l'authentification via des certificats !

J'ai donc construit une preuve du concept que j'ai étendu en une vrai application de gestion des comptes centralisées par certificats. Et j'ai trouvé ça tellement bien que j'ai eu envi d'en partager le principe. Du coup on va voir comment faire ça comme dans l'article d'origine, avec une installation de freshRSS, mais au lieu du LDAP et de Apache on va utiliser des certificats et Nginx parce que j'aime bien faire jamais comme les autres 😜.

1. D’ailleurs je me demande encore pourquoi sur ces foutus trucs on ne peut pas déconnecter le disque eMMC comme sur un odroid pour pouvoir le brancher à un pc et le reflasher à l'infini 😣.

2. En vrai c'est aussi la conclusion de leur article.

Nomenclature

Afin d'éviter les périphrases et les répétitions on va utiliser les termes ci dessous pour le reste de l'article.

  • SYSAUTH : le système d'authentification centralisé, il va gérer les requêtes d'authentification envoyées par un ou plusieurs sites externes. Il s'agit ici d'un programme écrit en PHP accessible en https.
  • SITEWEB : Un exemple de site web dans lequel on va protéger un ou plusieurs chemin d'accès (URL) grâce à SYSAUTH. Il s'agira ici d'une installation de freshRSS.

Le schéma

Voila ce qu'on va faire en terme de séquence pour que l'authentification fonctionne :

Schéma de la séquence

Le schéma de la séquence. Cliquer pour agrandir.

La config de SITEWEB

La récupération des informations des certificats doit se faire par SITEWEB qui est le point de terminaison de la connexion TLS contenant le certificat client. Il va nous falloir trouver un moyen d'envoyer ces information à SYSAUTH.

Récupération des informations du certificat client

On indique que le serveur doit faire une demande optionnelle du certificat au client si celui ci en dispose d'un correspondant à l’autorité de certification qu'on présente (ici le certificat de SYSAUTH)

Dans la section server en HTTPS :

ssl_client_certificate /etc/nginx/certs/ca_SYSAUTH.pem;
ssl_verify_client optional;

Il faudra récupérer le certificat publique de SYSAUTH qui à permis de signer les certificats utilisateurs et le placer a l'endroit indiqué par ssl_client_certificate On indique la valeur optional car on va nous même gérer l'authentification. Avec cette option nginx se chargera de demander le certificat client, uniquement s'il est disponible, et en extraira le contenu sans bloquer les connexions non authentifié avec un message générique moche.

Si le navigateur a bien envoyé un certificat, les variables suivante seront renseignées dans nginx :

  • $ssl_client_verify -> indique si un certificat est bien présenté et s'il est valide au regard de la CA de SYSAUTH (true ou false)
  • $ssl_client_serial -> indique le serial, non obligatoire, mais dans le code qu'on utilise il est necessaire.
  • $ssl_client_i_dn -> indique la valeur Distinguished Name du certificat.
  • $ssl_client_s_dn -> indique la valeur Common Name du certificat.

Sous-requête d'authentification

On va ensuite ajouter la section suivante :

  location /remauth {
    internal;
    proxy_pass https://URL_SYSAUTH/remauth.php; # L'url vers le point d'entrée du script/programme d'authentification.
    proxy_ssl_server_name  on; # necessaire pour aller sur le bon vhost si le serveur web heberge plusieurs sites.
    # Suppression du body car non utilisé pour l'authentification
    fastcgi_pass_request_body off;
    proxy_set_header        Content-Length "";
    # ==== Attention magie noire à partir d'ici ====
    # On utilise ici les champs custom des header http. 
    # Les règles de nomage sont extrèmement strictes pour pouvoir être décodées.
    # Règle : X-Custom-Nom-De-La-Variable (mélange de kebab-case et de CamelCase avec le préfix X-Custom-)
    proxy_set_header X-Custom-Ssl-Client-Verify $ssl_client_verify;
    proxy_set_header X-Custom-Ssl-Client-M-Serial $ssl_client_serial;
    proxy_set_header X-Custom-Ssl-Client-I-DN $ssl_client_i_dn;
    proxy_set_header X-Custom-Ssl-Client-S-DN $ssl_client_s_dn;
  }

Il te faudra modifier URL_SYSAUTH par l'adresse de ton serveur SYSAUTH.

Puis ajouter dans la section à protéger :


  # Section pour appeler la sous requête d'authentification
  auth_request /remauth;
  auth_request_set $auth_username $upstream_http_x_custom_username;
  
  # Section qui permet de récuperer le nom d'utilisateur depuis le header de retour
  # du serveur d'authentification. Ici on choisi REMOTE_USER comme nom de variable,
  # mais on pourrait mettre ce qu'on veut
  fastcgi_param REMOTE_USER $auth_username;
  
  

Ce qui permet de récuperer le nom d'utilisateur dans l'index correspondant de la variable php $_SERVEUR :

echo($_SERVER['REMOTE_USER']);

Implémentation

Un exemple d'implémentation dans la configuration de freshRSS :


location ~ \.php$ {
  auth_request /remauth;
  auth_request_set $auth_username $upstream_http_x_custom_username;
  fastcgi_param REMOTE_USER $auth_username;

  include snippets/fastcgi-php.conf;
  fastcgi_pass unix:/var/run/php/php-fpm.sock;
}

Résumé du fonctionnement

Si une requête est faite vers un chemin d'un bloc location qui contient auth_request /remauth; alors nginx vérifie s'il y a bien un certificat qui à été envoyé par le navigateur[3] , récupère les informations et les envois via des customs header http à SYSAUTH. Si SYSAUTH renvoi un code HTTP 403 pour indiquer un echec d'authentification, alors l’accès est bloqué et SITEWEB renverra lui aussi un code http 403 forbidden/interdit au navigateur à la place de la ressource se trouvant la location indiqué (ici toutes les URLs vers des fichiers .php).

3. En vrai nginx le fait de toute façon pour chaque requêtes. C'est juste qu'a ce moment là on va enfin utiliser les infos.

La config de SYSAUTH

Il faut d'abord avoir mis en place une infrastructure similaire à celle décrite dans mon article sur l'authentification via certificat qui servira de système d'authentification centrale.

De base il s'agit d'un système qui permet l'authentification des utilisateurs qui s'y connectent directement. Il va donc falloir ajouter un script qui récupère les variables envoyée par SITEWEB via les headers custom et faire le nécessaire pour vérifier que ces infos correspondent à un utilisateur valide.

On peut faire cela très simplement avec PHP. Je simplifie le code et on va dire qu'on met tout dans remauth.php accessible via https://URL_SYSAUTH/remauth.php.


class Identifier{
	public static $USER_PERM_ADMIN = 3;
	public static $USER_PERM_SUPERVISOR = 2;
	public static $USER_PERM_BASE = 1;
	public static $USER_PERM_NOTHING = 0;




	public static function get_user_profile(){
		$user = [];

		if(self::is_user_certified()){

			$user_id = $_SERVER['HTTP_X_CUSTOM_SSL_CLIENT_M_SERIAL'];
			$dn_list = self::load_user_distinguished_name();

			if(!isset($dn_list['x500UniqueIdentifier']) OR $dn_list['x500UniqueIdentifier']==''){
				$user['connected']= false;
			}else{
				$user_found = self::get_user_from_database($user_id);

				if($user_found){
					$x500uid_list =  self::get_user_certificate_x500uid_list_from_database($user_id);
					if(in_array($dn_list['x500UniqueIdentifier'], $x500uid_list)){
						$user['connected']= true;
						$user['id']=(int)$user_id ;
						$user['name']=$dn_list['CN'];
						$user['x500uid']=$dn_list['x500UniqueIdentifier'];
						$user['permission_level']=$user_found->get('permission_level');
					}else{
						$user['connected']= false;
					}

				}else{
					$user['connected']= false;

				}
			}
		}else{
			$user['connected']= false;
		}

		return $user;
	}

	private static function is_user_certified(){


		if(	isset($_SERVER['HTTP_X_CUSTOM_SSL_CLIENT_VERIFY']) &&
			isset($_SERVER['HTTP_X_CUSTOM_SSL_CLIENT_M_SERIAL']) &&
			isset($_SERVER['HTTP_X_CUSTOM_SSL_CLIENT_S_DN']) &&
			$_SERVER['HTTP_X_CUSTOM_SSL_CLIENT_VERIFY'] == "SUCCESS"
		){
			return true;
		}

		return false;
	}

	private static function load_user_distinguished_name(){
		$DN = $_SERVER['HTTP_X_CUSTOM_SSL_CLIENT_S_DN'];

		$dn_pairs = explode(',', $DN);
		$dn_list = [];
		foreach ($dn_pairs as $pair){
			$key_value = explode('=', $pair);
			$dn_list[$key_value[0]] = $key_value[1];
		}

		return $dn_list;
	}



	/**
	* Return the user found in database.
	*
	* @return Archivable user or false if not found
    * @param mixed $user_id
 	*/
	private static function get_user_from_database($user_id){
		$arch = new Archivist();
		$user = new User();
		$user->set('id',  (int)$user_id );

		$user_found = $arch->restore_first($user);

		return $user_found;

	}
    /**
     * @param mixed $user_id
     */
    private static function get_user_certificate_x500uid_list_from_database($user_id){

		$x500uid_list = [];
		$arch = new Archivist();
		$certif_to_find = new Certificate();
		$certif_to_find->set('id_user',  (int)$user_id );

		$certifs_found = $arch->restore($certif_to_find);

		foreach($certifs_found as $current_certif){
			$x500uid_list[] = $current_certif->get('x500uid');
		}

		return $x500uid_list;
	}
}


class Remauth{

	public function start(){



		$PLASMIDE_USER =Identifier::get_user_profile();

		if($PLASMIDE_USER['connected'] == false){
			http_response_code(403);
			return;
		}

		header('X-Custom-Username: '.$PLASMIDE_USER['name']);


	}
}


# Si non authentifié, retourne header http status code 403 : forbidden
# Si authentifié, retourne header http status code 200 (celui par défaut) et valeur X-Custom-Username dans le header de la réponse.
(new Remauth())->start();

Les portions de code relatives aux classes Archivist et Archivable sont des abstractions perso que j'utilise pour accéder à la base de donnée. Je ne donne pas le code de ces classes ici pour éviter d'alourdir cet article déjà très long. A toi de remplacer ces classes par ton propre système d'interrogation de base de donnée (PDO, mysql, etc).

On voit ici que selon si on veut lire des variables contenues dans des custom header http ou en ecrire, la syntaxe change. C'est donc un peu fourbe.

Pour lire une variable dans un custom header tel que X-Custom-Ssl-Client-Verify par exemple, on utilisera :

$_SERVER['HTTP_X_CUSTOM_SSL_CLIENT_VERIFY']

Alors que pour écrire une variable dans un custom header comme 'X-Custom-Username on utilisera :

header('X-Custom-Username: valeur_à_envoyer');

Un petit test

Comme décrit plus haut, en cas d'authentification invalide, tel qu'une non présentation du certificat (car pas dans le magasin de l'OS ou du navigateur), d'un certificat non reconnu ou non validé par le système central, un message d'erreur HTTP 403 (accès Interdit) est envoyé.

Et forcément, il est possible de personnaliser la page d'erreur pour avoir un truc un peu sympa. Tu peux le tester en essayant de te connecter sur mon instance freshrss : https://news.madyweb.net 🫣.

Points à prendre en compte

Même si ça marche du tonnerre, il faut quand même noter quelques points.

L'authentification nécessitant une sous requête vers SYSAUTH pour chaque requête vers SITEWEB protégée, il faut ajouter le temps de réponse de SYSAUTH à chacune de ces dernières. Idéalement il faudrait faire en sorte que les deux applications se trouvent sur le même réseau local, voir même sur la même machine s'il y a besoin d'optimiser le temps de réponse au maximum.

L'exemple de SYSAUTH que j'ai donné dans cet article ne vérifie pas si la source de la demande est de confiance. On pourrait donc ajouter un vérification de la source de la requête afin d'éviter de faire fuiter un nom d'utilisateur si quelqu'un s'amusait à bruteforcer les couple ID+x500uid via des requêtes automatisées. Même si on ne pourrait pas s'authentifier avec, c'est toujours embêtant[4] .

4. Et ça évite de faire comme tous les vilains qui se trouvent sur https://bonjourlafuite.eu.org/.

Conclusion

Avec ce type de système, il donc est possible d'ajouter une authentification par certificat à des applications qui ne sont pas forcement prévues pour, centraliser le tout pour ne pas avoir 50 certificats par device et en plus on peut même imaginer d'avoir des logs de connexions sur SYSAUTH afin de voir si un compte ne serait pas compromis.

L'avantage d'un tel système c'est qu'étant donné que chaque utilisateur dispose d'un certificat, il serait même possible de créer des applications web permettant de signer numériquement des documents, par exemple, ou de faire pleins d'autres trucs amusants à faire grâce à des fonctions cryptographiques.