Bloquer l’accès à un site web pour tout le monde sauf…

Lors du développement d’un site web, il est pratique à partir d’un certain moment de le publier et pouvoir le rendre accessible à une minorité de personnes (du genre le client pour commencer à avoir du feedback).

Je décris dans ce billet différentes méthodes permettant de répondre à ce besoin.

Filtrage par IP

Une solution naïve serait de bloquer l’accès au site sauf à une poignée d’IPs, qui serait les IPs des personnes autorisées à consulter le site en construction. Cela cependant ne fonctionne pas si l’IP n’est pas connu à l’avance, comme par exemple lorsque le FAI fournit des IPs d’un pool dynamiques ou tout simplement lors d’une présentation (Orange et Bouygues notamment).

Filtrage par Cookie

Une seconde solution, et c’est celle que je veux présenter ici, se base sur un autre mécanisme. Je trouve cette solution particulièrement élégante car simple tout en répondant parfaitement au problème.
Dans cette solution, la clé n’est plus l’IP comme dans la première solution mais le cookie. La présence d’un cookie détermine l’accès au site.

L’implémentation la plus simple se fait en deux phases:

  • la première phase est la création d’une page-à-la-con dont le seul objectif sera d’insérer le cookie au navigateur puis d’effectuer une redirection par JavaScript vers le point d’entrée du site.
  • la seconde phase est l’implémentation de la restriction, en général au sein du serveur web car alors c’est de la configuration pur et il n’est pas nécessaire de bidouiller le code source du site.

La restriction doit répondre aux règles suivantes :

  1. L’url permettant de poser le cookie doit être en accès libre. Il faut la garder secrète.
  2. L’accès au site doit être autorisée si la requête provient d’une IP autorisée ou si le cookie d’accès est présent
  3. Poser un fichier robots.txt pour éviter que l’url du premier point soit crawlé par les principaux moteurs de recherche.

Ci-dessous un exemple d’implémentation de la restriction façon Apache

SetEnvIf Request_URI "youhou_cookie\.php$" let_me_in
SetEnvIfNoCase Cookie "euphor=tissu" let_me_in

order deny,allow
allow from 82.247.154.152
allow from 193.248.37.205
allow from 2001:470:1f13:129f::/64
allow from env=let_me_in
deny from all

Et voilà une version pour IIS

<configuration>
    <system.webServer>
        <rewrite>
            <rules>
                <clear />
                <rule name="cookie-redirect" enabled="true" patternSyntax="ExactMatch" stopProcessing="true">
                    <match url="entry-url" />
                    <conditions logicalGrouping="MatchAll" trackAllCaptures="false" />
                    <action type="Redirect" url="/" appendQueryString="false" redirectType="Found" />
                </rule>
                <rule name="cookie-checker" patternSyntax="ECMAScript" stopProcessing="true">
                    <match url=".*" negate="false" />
                    <conditions logicalGrouping="MatchAll" trackAllCaptures="false">
                        <add input="{REQUEST_URI}" pattern="^entry-url$" negate="true" />
                        <add input="{HTTP_COOKIE}" pattern="pass=euphor" negate="true" />
                    </conditions>
                    <action type="AbortRequest" />
                </rule>
            </rules>
            <outboundRules>
                <rule name="set-cookie" preCondition="url-checker" patternSyntax="ECMAScript" stopProcessing="false">
                    <match serverVariable="RESPONSE_Set_Cookie" pattern=".*" negate="false" />
                    <action type="Rewrite" value="pass=euphor; Max-Age=86400" replace="true" />
                </rule>
                <preConditions>
                    <preCondition name="url-checker">
                        <add input="{REQUEST_URI}" pattern="entry-url" />
                    </preCondition>
                </preConditions>
            </outboundRules>
        </rewrite>
        <httpErrors errorMode="Detailed" />
    </system.webServer>
</configuration>

 

Il sera préférable d’effectuer la redirection en javascript plutôt qu’avec un code HTTP 302, on évite quelques cas à priori bizarre (je ne me suis pas penché dessus) où le cookie n’est pas pris en compte par le navigateur.

FAQ

  1. Pas besoin de s’embêter avec tout ça, il suffit simplement de ne pas laisser trainer l’url du site non ?
    Il suffit de surfer sur le site avec Google Chrome par exemple et le site se fait référencer dans la semaine.
  2. En plaçant un robots.txt, on évite que le site se fasse référencer par les moteurs de recherche, c’est bon là ?
    Oui Google et Bing! respectent le fichier robots.txt, mais ce n’est pas un simple fichier qui va bloquer, techniquement parlant, le référencement par tout moteur de recherche. Le crawler de la bibliothèque nationale de France par exemple ignore complètement ce fichier
  3. Obligé de suivre la solution 2 alors ?
    Sous l’angle de la sécurité, garder secret un nom de domaine ou poser un fichier robots.txt c’est de l’esbrouffe. Tant qu’il n’existe pas de barrière solide, on peut doit considérer que le site est en accès libre. La solution 1 peut suffire mais la solution 2 apporte de la souplesse.

pfSense sur APU

Ce poste rapide explique comment faire fonctionner pfSense lorsqu’il est installé sur un APU pcEngine avec une SDCard

Pré-requis

Avant de commencer, mettez en place un environnement permettant de monter une partition UFS (un FreeBSD ou un Linux avec le module qui va bien).
Le mode LiveCD de FreeBSD dans une VM fonctionne, mais le clavier qwerty et l’éditeur vi va probablement compliquer la tâche.

Manipulation

  1. Dans l’environnement, branchez-y la carte SD et montez la partition 1 de la manière suivante : mount -t ufs /dev/da1s1a /mnt/p1 où da1s1a est la partition 1 du disque 1 sous FreeBSD et /mnt/p1 un dossier où le montage va se faire.
  2. Editer le fichier /mnt/p1/boot/loader.conf en y ajoutant les lignes suivantes :
    1. comconsole_speed= »115200″
    2. kern.cam.boot_delay=10000
  3. Démontez les partitions, la carte SD peut-être replacée dans l’APU.

La première ligne sert à aligner la vitesse de communication du port série avec celle du BIOS, ce qui évite de reparamétrer le terminal et d’y perdre des lignes.
La seconde ligne ajoute un délai permettant au kernel de charger les modules permettant d’accéder à la carte SD.

Notes

L’image de pfSense sous format nanosd comporte 3 partitions. Les deux premières contiennent chacune une copie du système. La troisième partition est celle contenant la configuration.
La manipulation doit donc être effectuée deux fois, une fois pour chacune des deux premières partitions.

Retour d’expériences

Au bout de quelques heures d’utilisation, je me rend compte que pfSense, c’est cool, mais dès qu’on souhaite faire quelque chose d’avancé (multi-homing, IPSec avec chiffrage ECC et authentification par certificat, configuration poussé du pare-feu, dns menteur,…), ça pu pas mal. L’interface graphique étant très limitative, mon intérêt pour pfSense a disparu.

Le boitier tourne depuis sous Gentoo avec un petit ssd au format m.sata pour le système principal et une copie de sauvegarde plus ancienne sur la carte  SD depuis janvier 2015, et j’en suis très content. Ca fait son travail, et ça le fait très bien.

Sources

Restauration autoritaire de SYSVOL

J’aime bien commencer une belle et chaude semaine d’été par une restauration autoritaire de SYSVOL. Ou comment annoncer gentillement que bordel de merde, c’est pas le moment, j’avais vraiment autre chose à faire que de me prendre le choux avec ça. Ce billet détaille ma résolution pour remettre debout l’infra Active Directory.

État des lieux

Avant toute chose, il est de rigueur de décrire le parc et les versions utilisées. En effet Microsoft a introduit énormément de changements à partir de Windows Server 2008, et la procédure décrite ici ne pourra en conséquence pas fonctionner pour un contrôleur de domaine Windows Server 2003 ou plus ancien. Pour être plus précis, la procédure ne peut s’appliquer que lorsque le niveau fonctionnel du domaine est au minimum fixé à Windows Server 2008.

Le parc est plutôt modeste et est, pour la partie qui nous concerne, composé de :

  • 1 contrôleur de domaine sous Windows Server 2012 (AZSHARA)
  • 1 contrôleur de domaine sous Windows Server 2012R2 (ALEXSTRASZA)
  • le niveau fonctionnel du domaine est fixé à Windows Server 2012
  • le niveau fonctionnel de la forêt est fixé à Windows Server 2012
  • un script PowerShell qui s’exécute au démarrage de chaque ordinateur via une GPO. Le script est enregistré quelque part dans le partage SYSVOL

sysvol_script_ps

Symptômes

J’avoue qu’il s’agit d’un problème qui aurait pu être évité, mais ne connaissant aucun indicateur fiable « OK, ton domaine fonctionne normalement, tout est bien synchronisé » (en revanche si vous en connaissez un, un script ou n’importe quoi qui puisse me donner l’état de santé du domaine, je suis preneur), j’ai du faire avec les petits moyens dont je dispose.
A priori, les contrôleurs de domaine fonctionnaient normalement, les utilisateurs pouvaient se logguer, les contrôleurs étaient redémarrés régulièrement pour appliquer les mises à jour Windows. Cependant il existait un phénomène assez curieux mais sur lequel je ne me suis penché qu’assez tardivement : de temps en temps, les changements que je faisais sur le script PowerShell n’étaient pas pris en compte lorsque je le testais par la suite sur une VM de test. Ce script est mis à jour assez régulièrement car il sert à déployer tout ce qui ne repose pas sur du MSI, c’est à dire Firefox, Filezilla, les grosses mises à jour Visual Studio etc…
Après une énième occurrence du phénomène, je décide de prendre 5 minutes pour regarder de plus près ce qu’il se passe vraiment. J’accède alors au partage SYSVOL où se trouve le script sur chaque contrôleur de domaine et là, c’est le drame : chaque serveur possède une version du script différente ; le partage n’est absolument plus synchronisé, dans un sens comme dans l’autre.

Diagnostic

Comme toujours dans ces cas là, on part à la chasse aux informations. Je trouve assez rapidement la raison pour laquelle il n’existe plus de réplication dans le journal d’événement d’un des contrôleurs de domaine.

event_bad-stop

C’est à partir d’ici où il est important de savoir dans quel niveau fonctionnel le domaine se trouve. Les contrôleurs de domaines sont tous maîtres, il n’y a pas (par défaut) de relation 1 maître – x esclaves comme c’est souvent le cas pour les serveurs de bases de données transactionnelles. Les données synchronisés entre les contrôleurs de domaine sont :

  • l’annuaire. Il contient entre autre tous les utilisateurs, groupes, unités organisationnelles, les enregistrements DNS, la topologie de réplication.
  • le partage SYSVOL, il contient mon script et des fichiers en relation directe avec les stratégies de groupe (les GPOs) mises en place.

Etant donné que le problème ne se situe pas sur l’annuaire, la commande repadmin.exe ne sera d’aucun secours. A la place, il faudra utiliser la commande dfsrdiag.exe.
Vu que cette dernière est bien barbare et demande un certain nombre de paramètre non évident, j’ai préféré examiner la réplication via la console de gestion du système de fichiers distributés DFS (dfsmgmt.msc).
Cette console permet de créer un rapport d’intégrité (première option), ce que j’ai fait ou de tester la propagation (seconde option pour lancer le test, troisième option pour suivre la progression du test)
Je vous conseille fortement d’ouvrir ce rapport exclusivement avec Internet Explorer sous peine d’avoir du hachis parmentier à l’écran avec un autre navigateur.

rapport-1 rapport-2 rapport-3 rapport-4

 

Résolution

Je suis donc la procédure décrite dans l’événement pour relancer la synchronisation, cela nous mène au second drame.

Et impose un choix à faire parmi deux propositions :

  • on suit les recommandations de l’événement et on considère que la copie du partage SYSVOL est foutue car trop vieille. La bonne copie est donc celle se trouvant sur l’autre contrôleur de domaine. C’est souvent le choix à faire dans le cas où l’on a au moins 3 contrôleurs de domaines dont 2 au minimum ont une réplication fonctionnelle du partage SYSVOL.
  • on fait sa tête de mule et l’on décide que finalement la vieille copie est la bonne et devrait écraser l’autre copie.

Restauration autoritaire / non-autoritaire

  • Si vous avez un contrôleur de domaine avec un SYSVOL propre, alors vous pouvez (et même devriez) effectuer une restauration dite non-autoritaire, c’est à dire que l’on va invalider la copie du SYSVOL foutue, afin que le contrôleur de domaine qui héberge la copie la resynchronise à partir des autres contrôleurs de domaine. Dans mon cas, cela se traduit par AZSHARA qui va synchroniser son partage SYSVOL à partir de ALEXSTRASZA
  • Si vous avez opté pour le second choix ou bien si aucun contrôleur de domaine n’a un SYSVOL propre, alors il est nécessaire d’effectuer une restauration dite autoritaire, c’est-à-dire que l’on va explicitement déclarer qu’un des contrôleurs de domaine a la « bonne » copie du SYSVOL, et tous les autres contrôleurs de domaines vont effectuer une resynchronisation sur cette copie. La restauration autoritaire est à considérer comme la solution de la dernière chance, il est préférable tant que faire se peut d’effectuer en priorité une restauration non-autoritaire.

Sources

Authentification SSH par AD

J’essaie lorsque c’est possible d’éviter la sur-multiplication des mots de passe à gérer. Dans un environnement principalement Microsoft, Active Directory permet de résoudre assez bien ce problème, c’est cool. Un annuaire pour les unir tous (et dans les ténèbres les lier).

Qu’en est-il des distributions Linux ? Je gère un petit nombre de serveurs sous Linux, pour lesquels l’intérêt de les intégrer dans un domaine Active Directory est proche du néant (sans parler du foutoir à mettre en place côté Linux). En effet, il n’y a pas de GPO, l’authentification par mot de passe est désactivé sur ces serveurs, et seul quelques personnes doivent pouvoir y avoir accès et de manière limitée.

Je me suis donc orienté vers une solution où la base utilisateurs des serveurs Linux reste locale (via /etc/passwd et /etc/shadow donc) mais en cherchant à centraliser la clé SSH de ces utilisateurs avec Active Directory.

David Gwynne (voir source) s’est déjà penché sur ce problème et a exhibé un paramètre contenu dans OpenSSH permettant de contrôler l’étape d’authentification via clé publique de manière très flexible, ce paramètre acceptant un script en entrée.

Le principe est alors très simple :

  1. un utilisateur tente de se connecter au serveur linux en SSH
  2. le serveur SSH récupère le nom d’utilisateur et exécute le script indiqué par le paramètre AuthorizedKeysCommand
    1. par mesure de sécurité, OpenSSH propose d’exécuter ce script sous une autre identité, ce qui est fortement préférable vu qu’OpenSSH est root
    2. ce script va se connecter à un serveur LDAP à l’aide d’identifiants dédié
    3. il va récupérer l’objet utilisateur correspondant au nom d’utilisateur, et s’il existe retourner sa ou ses clés publique SSH

L’intérêt consiste à éviter de devoir manipuler le fichier /home/{user}/.ssh/authorized_keys2 sur chaque serveur pour chaque utilisateur, la création d’un accès se faisant alors en deux temps :

  1. création du compte sur le serveur linux avec la commande useradd
  2. si ce n’est pas déjà fait, ajout de sa clé dans Active Directory

Mise en place de la solution

  1. Dans le fichier /etc/ssh/sshd_config, on ajoute/modifie les lignes suivantes:
AuthorizedKeysCommand /etc/ssh/authorized_keys.pl
AuthorizedKeysCommandUser _sshkeys

# si ce n'est pas déjà fait, ci-dessous les paramètres à modifier pour désactiver l'identification par mot de passe
PasswordAuthentication no
ChallengeResponseAuthentication no

2. Contenu du fichier /etc/ssh/authorized_keys.pl. Il faudra renseigner le nom du domaine, le nom d’utilisateur et mot de passe d’un compte pour se connecter à l’annuaire.

#!/usr/bin/perl -w

use strict;
use Net::DNS;
use Net::LDAP;
use Net::LDAP::Constant qw(LDAP_SUCCESS LDAP_INVALID_CREDENTIALS);
use Net::LDAP::Util qw(escape_filter_value);

sub ad_bind($$$)
{
	my $domain = shift;
	my $user = shift;
	my $pass = shift;

	my $res = Net::DNS::Resolver->new;
	my $query;
	my $answer;
	my $ldap_error;

	$query = $res->query("_ldap._tcp." . $domain, 'SRV');
	if (!$query) {
		die "unable to query SRV records for $domain";
	}

	my @answers = $query->answer;
	foreach $answer (@answers) {
		# next if ($answer->port != 389)

		my $ldap = new Net::LDAP($answer->target, timeout => 2);

		$ldap_error = $@;
		next unless ($ldap);

		my $mesg = $ldap->bind(sprintf("%s@%s", $user, uc($domain)),
			password => $pass);
		return ($ldap) if ($mesg->code == LDAP_SUCCESS);

		if ($mesg->code == LDAP_INVALID_CREDENTIALS) {
			return (undef, $mesg->error);
		}

		$ldap_error = $mesg->error;
	}

	return (undef, $ldap_error);
}

if (scalar @ARGV != 1) {
	die "username not specified";
}

my $username = $ARGV[0];
my $ad_filter = sprintf('(&(sAMAccountName=%s)(objectClass=user))', escape_filter_value($username));

my %d = (
	'kveer.fr' => {
		'user' => 'sshkeys',
		'pass' => 'ton_mot_de_passe',
		'base' => 'DC=kveer,DC=fr',
		'filter' => $ad_filter
	}
);

foreach my $domain (keys %d) {
	my $param = $d{$domain};

	my ($ds, $err) = ad_bind($domain, $param->{'user'}, $param->{'pass'});
	next unless ($ds);

	my $mesg = $ds->search(
		base	=> $param->{'base'},
		filter	=> $param->{'filter'},
		attrs	=> ['altSecurityIdentities']
	);
	next unless ($mesg->code == LDAP_SUCCESS);

	for (my $i = 0; $i < $mesg->count; $i++) {
		my $entry = $mesg->entry($i);

		my @ids = $entry->get_value('altSecurityIdentities');
		foreach my $id (@ids) {
			my ($type, $value) = split(/:/, $id, 2);
			print "$value\n" if ($type eq 'SSHKey');
		}
	}
}

3. puis enfin quelques commandes pour terminer la configuration (à adapter en fonction de la distribution)

#!/bin/bash

USERNAME=_sshkeys
SCRIPT_FILE=/etc/ssh/authorized_keys.pl

useradd -s /sbin/nologin -d /var/empty/ $USERNAME
chmod 750 "$SCRIPT_FILE"
chown root:"$USERNAME" "$SCRIPT_FILE"

Ajouter la clé SSH

Pour chaque utilisateur devant avoir un accès nunux, il suffit de déclarer leur clé publique au sein d’Active Directory, dans l’attribut altSecurityIdentities. L’avantage de cet attribut est qu’il existe déjà, et fonctionne à l’aide d’un préfixe sur les valeurs, de sorte à ce qu’il puisse être utilisé par un acteur différent (ici OpenSSH) sans impacter l’usage initial qui en est fait de cet attribut. Ci-dessous à travers l’outil de gestion graphiques de l’annuaire dsa.msc.

ssh_ad_dsa

Remarques

  • Le script n’est pas parfait et peut être améliorer. Il ne vérifie pas par exemple si le compte en question est désactivé ou non.
  • Le script utilise l’attribut altSecurityIdentities. On aurait très bien pu créer un attribut dédié, mais cette solution a l’avantage d’éviter de modifier manuellement le schéma AD.

Source