Migration Plesk 18 > ISPconfig 3

Pour l’association le Retzien.fr (dont je fais parti) j’ai effectué une migration de Plesk 18 vers ISPconfig 3. Plesk avait été mis en œuvre avant mon arrivé car l’administrateur en place connaissait le produit. Mais c’est un produit payant, de plus en plus cher, et pour une association qui a du mal à avoir un équilibre financier c’était difficile à assumer. Plesk est très « beau » et user friendly mais en tant qu’administrateur système je le trouve plutôt opaque (on ne sais pas exactement ce qu’il fait, pas trop de respect des « us et coutumes » de Linux (position des fichiers de config…), ET CE N’EST PAS LIBRE !!!

ISPconfig est libre et propose un script de migration (ISPConfig Migration Toolkit) mais ne supporte pas les dernières versions de Plesk, car le chiffrement des mots de passe n’est plus compatible. Du coup j’ai dû bricoler… Ce bricolage est l’objet de cet article.

Note : ici il n’est question que de la partie mail/DNS et compte client, car c’est la seule fonctionnalité du serveur, les fonctions web n’ont pas été testées.

Installation ISPconfig + Migration Toolkit

Je ne détaille pas ici la procédure d’installation d’ISPconfig 3 et l’usage du « Migration Toolkit » qui sont déjà bien documentés, dans mon cas :

Suite à l’usage du « Migration toolkit », même si le Plesk source n’est pas supporté, je n’ai pas d’erreur mais j’ai plusieurs problèmes que je vais essayer de régler par différents scripts.

Préambules : Les scripts suivants utilisent l’API d’ISPconfig (il faut donc créer un utilisateur distant, dans Système/utilisateur distant). ils doivent donc les lancer depuis le serveur source (Plesk).

Authentification IMAP

Le problème

Il est impossible de se connecter au serveur avec les comptes IMAP. Le log dit :

Error: sql(xxxxxxx@retzien.fr,::1,<MdFia/2k3toAAAAAAAAAAAAAAAAAAAAB>): Invalid password in passdb: crypt() failed: Invalid argument

Effectivement le chiffrement en base est différent :

Mais on va s’en sortir en rusant, en effet Plesk ne crypte/ »hash » pas les mots de passe mais les chiffre :

  • Crypter / « hasher » : c’est convertir définitivement un chaîne (ici un mot de passe) en un code. Pour savoir si le mot de passe proposé pour l’authentification est bon il faut crypter / « hasher » le mot de passe et le comparer.
  • Chiffrer : c’est convertir une chaîne (le mot de passe ici) avec une clé, cette fonction est réversible, et qui détient la clé peut donc déchiffrer la chaîne (le mot de passe)
  • Plus d’info sur la différence ici

Les mots de passe dans Plesk sont donc « lisibles » pour qui possède la clé de déchiffrement (l’administrateur par exemple ou un hacker qui obtiendrait l’accès au serveur…)

Pour vous en persuader, lancez la commande ci après et vous obtiendrez un beau tableau 1er colonne « émail », 2ème « mot de passe en clair »

/usr/local/psa/admin/sbin/mail_auth_view

Mais pourquoi les mots de passes ne sont ils pas cryptés ?

Plesk propose le support de l’authentification « mot de passe chiffré ». Et cette fonctionnalité est de fait contrainte à avoir le mot de passe en clair sur le serveur car il faut pouvoir le déchiffrer.

Exemple de méthode d’authentification sous Thunderbird

ISPconfig ne supporte pas cette fonctionnalité, car qu’il considère que ce serait un trop gros trou dans la sécurité du serveur, et qu’avec le chiffrement de la connexion SSL/TLS ou START/TLS ce n’est pas nécessaire d’en remettre une couche.

Effet de bord important : si vos utilisateurs ont configurés leur client de messagerie en « mot de passe chiffré » il faudra leur faire modifier ce paramétrage (IMAP & SMTP)

De ce que je comprends, cette méthode d’authentification chiffrée avait sa place quand les serveur IMAP/SMTP ne proposait pas de chiffrement de connexion SSL/TLS ou START/TLS.

Les sources de ce que j’avance :

La solution

La solution consiste, sur le serveur Plesk à lancer le script ci-après qui :

  • Lance la commande Plesk « magique » pour voir les mots de passe déchiffrés
  • « Parse » le résultat… Se connecte à ISPconfig et change le mot de chaque boîte émail
<?php
 
/* 
 * licence Beerware
 * By David Mercereau : http://david.mercereau.info
 */
 
// Plan B : https://www.besuchet.net/2016/06/plesk-11-encrypted-hashed-password-authentication-php-on-psa-database/
 
// ISPconfig Remote Config
$CONFIG['remoteUser'] = 'retzien';
$CONFIG['remotePassword'] = 'XXXXXXXXXX';
$CONFIG['remoteSoapLocation'] = 'https://192.168.1.64:8080/remote/index.php';
$CONFIG['remoteSoapUri'] = 'https://192.168.1.64:8080/remote/';
$CONFIG['logfile'] = '/tmp/migration-email-password.log';
 
function toLog($txt, $level) {
	file_put_contents($GLOBALS['CONFIG']['logfile'],date("[j/m/y H:i:s]")." - ".$level." - $txt \r\n",FILE_APPEND);
	echo "[$level] $txt\n";
}
 
// Connexion to ISPconfig
$client = new SoapClient(null, array('location' => $CONFIG['remoteSoapLocation'],
	'uri'      => $CONFIG['remoteSoapUri'],
	'stream_context'=> stream_context_create(array('ssl'=> array('verify_peer'=>false,'verify_peer_name'=>false))),
	'trace' => 1));
 
// Login
if($session_id = $client->login($CONFIG['remoteUser'], $CONFIG['remotePassword'])) {
	toLog('Login Ok. Session ID:'.$session_id, 'info');
}
 
# Launch plesk command for auth view in plain text
exec('/usr/local/psa/admin/sbin/mail_auth_view', $out, $return);
if ($return == 0 || $out[0] == null){
	foreach ($out as $line) {
		$lineExplode = explode('|', $line);
		if (isset($lineExplode[1]) &amp;&amp; isset($lineExplode[3])) {
			$email = str_replace(' ', '', $lineExplode[1]);
			$password = str_replace(' ', '', $lineExplode[3]);
			# Verification data
			if (filter_var($email, FILTER_VALIDATE_EMAIL) &amp;&amp; $password != '') {				
				# mail_user_get
				try {
					$mail_user_get = $client->mail_user_get($session_id, array('email' => $email));
					if (count($mail_user_get) > 0) {
						# Update data
						$mail_user_get['password']=$password;
						$mail_user_get['move_junk']='y';
						$affected_rows = $client->mail_user_update($session_id, 0, $mail_user_get[0]['mailuser_id'], $mail_user_get);
						if ($affected_rows == 0) {
							toLog('No change for '.$email, 'error');
						} else {
							toLog('Ok for '.$email, 'info');
						}
					} else {
						toLog('email '.$email.' not present in ISPconfig', 'warn');
					}
				} catch (SoapFault $e) {
					echo $client->__getLastResponse();
					die('SOAP Error: '.$e->getMessage());
				}
			} 
		}
	}
}
 
if($client->logout($session_id)) {
	toLog('Logged out.', 'info');
}
 
?>

IMAP subscriptions

La souscription au dossier IMAP (le fait de voir ou non certains dossiers et/ou d’en cacher d’autres) n’a pas migré, donc si l’utilisateur avait ajouté un dossier IMAP, il ne le voyez plus sur le nouveau serveur, pourtant le dossier est bien là.. c’est fâcheux. Pour corriger ça, lancez ce petit script sur le Plesk (source) :

#!/bin/bash
target='root@192.168.1.64'
find  /var/qmail/mailnames/ -name subscriptions | while IFS=$'\n' read f ; do
	domain=$(echo $f | cut -d/ -f5)
	user=$(echo $f | cut -d/ -f6)
	echo ${user}@${domain}
	scp /var/qmail/mailnames/${domain}/${user}/Maildir/subscriptions ${target}:/var/vmail/${domain}/${user}/Maildir/
done

DKIM & Spam policy

Ce script ajoute une politique de spam “normal” (modifiable) et récupère les anciennes clefs DKIM générées par Plesk pour les appliquer dans ISPconfig. Ceci pour éviter d’avoir à retoucher au DNS pendant la migration (toujours à lancer depuis le Plesk) :

<?php
 
/* 
 * licence Beerware
 * By David Mercereau : http://david.mercereau.info
 */
 
// ISPconfig Remote Config
$CONFIG['remoteUser'] = 'retzien';
$CONFIG['remotePassword'] = 'XXXXXXX';
$CONFIG['remoteSoapLocation'] = 'https://192.168.1.64:8080/remote/index.php';
$CONFIG['remoteSoapUri'] = 'https://192.168.1.64:8080/remote/';
$CONFIG['logfile'] = '/tmp/migration-email-password.log';
$CONFIG['DKIM-Selector'] = 'default';
$CONFIG['policy_id'] = 5;
$CONFIG['priority'] = 5;
 
function toLog($txt, $level) {
	file_put_contents($GLOBALS['CONFIG']['logfile'],date("[j/m/y H:i:s]")." - ".$level." - $txt \r\n",FILE_APPEND);
	echo "[$level] $txt\n";
}
 
// Connexion to ISPconfig
$client = new SoapClient(null, array('location' => $CONFIG['remoteSoapLocation'],
	'uri'      => $CONFIG['remoteSoapUri'],
	'stream_context'=> stream_context_create(array('ssl'=> array('verify_peer'=>false,'verify_peer_name'=>false))),
	'trace' => 1));
 
// Login
if($session_id = $client->login($CONFIG['remoteUser'], $CONFIG['remotePassword'])) {
	toLog('Login Ok. Session ID:'.$session_id, 'info');
}
 
$mail_domain_record = $client->mail_domain_get($session_id, array('active' => 'y'));
 
foreach ($mail_domain_record as $mail_domain) {
	# DKIM
	if (is_dir('/etc/domainkeys/'.$mail_domain['domain'])) {
		$getPrivateKey=file_get_contents('/etc/domainkeys/'.$mail_domain['domain'].'/'.$CONFIG['DKIM-Selector']);
		exec('openssl rsa -in /etc/domainkeys/'.$mail_domain['domain'].'/'.$CONFIG['DKIM-Selector'].' -pubout', $out, $return);
		if ($return == 0 || $out[0] == null){
			$getPublicKey='';
			foreach ($out as $line) {
				$getPublicKey.=$line."\n";
				//~ if (!preg_match('/^---/', $line))  {
					//~ $getPublicKeyDns.=$line;
				//~ }
			}
			$mail_domain['dkim'] = 'y';
			$mail_domain['dkim_selector'] = $CONFIG['DKIM-Selector'];
			$mail_domain['dkim_private'] = $getPrivateKey;
			$mail_domain['dkim_public'] = $getPublicKey;
			$affected_rows = $client->mail_domain_update($session_id, 0, $mail_domain['domain_id'], $mail_domain);
			if ($affected_rows == 0) {
				toLog('DKIM No change for '.$mail_domain['domain']. ' already exist ?', 'error');
			} else {
				toLog('DKIM Ok for '.$mail_domain['domain'], 'info');
			}
		}
	} else {
		toLog($mail_domain['domain'].' haven\'t got dkim key on plesk', 'warn');
	}		
	# Spam Policy add in domain 
	try {
		$client_id = 1;
		$params = array(
			'server_id' => 1,
			'priority' => $CONFIG['priority'],
			'policy_id' => $CONFIG['policy_id'],
			'email' => '@'.$mail_domain['domain'],
			'fullname' => '@'.$mail_domain['domain'],
			'local' => 'Y'
		);
		$affected_rows = $client->mail_spamfilter_user_add($session_id, $client_id, $params);
		toLog('Spam policy Ok for '.$mail_domain['domain'], 'info');
	} catch (SoapFault $e) {
		toLog('Spam policy no change for '.$mail_domain['domain']. ' already exist ?', 'error');
	}
}
 
if($client->logout($session_id)) {
	toLog('Logged out.', 'info');
}
 
?>

Les mailings lists

Les mailings lists ne sont pas migrées, pourtant c’est du « Mailman 2 » des 2 côtés donc ça ne devrait pas être très compliqué.

Ce script est à lancer depuis le Plesk en mode « CLI » (ligne de commande) et vous avez quelques paramètres à modifier au début.

Le script :

  • Liste les mailings lists avec la commande /var/lib/mailman/bin/list_lists
  • Les créer dans ispconfig
  • Donne les commandes rsync pour synchroniser les données (archives, paramètres…)
 <?php
 
/* 
 * licence Beerware
 * By David Mercereau : http://david.mercereau.info
 */
 
// ISPconfig Remote Config
$CONFIG['remoteUser'] = 'retzien';
$CONFIG['remotePassword'] = 'XXXXX';
$CONFIG['remoteSoapLocation'] = 'https://192.168.1.64:8080/remote/index.php';
$CONFIG['remoteSoapUri'] = 'https://192.168.1.64:8080/remote/';
$CONFIG['logfile'] = '/tmp/migration-email-password.log';
$CONFIG['clientId'] = 3;
$CONFIG['domain'] = 'retzien.fr';
 
function toLog($txt, $level) {
	file_put_contents($GLOBALS['CONFIG']['logfile'],date("[j/m/y H:i:s]")." - ".$level." - $txt \r\n",FILE_APPEND);
	echo "[$level] $txt\n";
}
 
// Connexion to ISPconfig
$client = new SoapClient(null, array('location' => $CONFIG['remoteSoapLocation'],
	'uri'      => $CONFIG['remoteSoapUri'],
	'stream_context'=> stream_context_create(array('ssl'=> array('verify_peer'=>false,'verify_peer_name'=>false))),
	'trace' => 1));
 
// Login
if($session_id = $client->login($CONFIG['remoteUser'], $CONFIG['remotePassword'])) {
	toLog('Login Ok. Session ID:'.$session_id, 'info');
}
 
# Launch plesk command for auth view in plain text
exec('/var/lib/mailman/bin/list_lists -b', $out, $return);
if ($return == 0 || $out[0] == null){
	foreach ($out as $line) {
	try {
		$params = array(
			'server_id' => 1,
			'domain' => $CONFIG['domain'],
			'listname' => $line,
			'email' => 'migration@migration.do',
			'password' => 'temp123!'
		);
		$mailinglist_id = $client->mail_mailinglist_add($session_id, $CONFIG['clientId'], $params);
		toLog('Ok for '.$line);
		} catch (SoapFault $e) {
			toLog('SOAP Error: '.$line. ' - '. $e->getMessage(), 'error');
		}
	}
}
 
if($client->logout($session_id)) {
	toLog('Logged out.', 'info');
}
 
echo "Patienteez 5min que Ispconfig est tout ajouté puis : \n";
echo "for list in \$(/var/lib/mailman/bin/list_lists -b); do\n
	rsync -az --progress  /var/lib/mailman/archives/public/\$list/ root@192.168.1.64:/var/lib/mailman/archives/public/\$list/\n
	rsync -az --progress  /var/lib/mailman/archives/private/\$list/ root@192.168.1.64:/var/lib/mailman/archives/private/\$list/\n
	rsync -az --progress  /var/lib/mailman/archives/private/\${list}.mbox root@192.168.1.64:/var/lib/mailman/archives/private/\n
	rsync -az --progress  /var/lib/mailman/lists/\$list/ root@192.168.1.64:/var/lib/mailman/lists/\$list/\n
done\n";
 
?>

En sortie le script donne quelques commandes « rsync » à exécuter par un copier / coller direct dans le terminal du serveur source (Plesk) pour synchroniser les données (archives, paramètres…)

Ensuite sur le serveur decdestination (ISPconfig) lancez la commande :

/var/lib/mailman/bin/check_perms -f

Conclusion

La migration c’est bien passée, c’est transparent pour l’utilisateur, à part le problème de « mot de passe chiffré » à « mot de passe normal », qu’il faut faire changer avant la migration de préférence.

Sauvegarde de serveur écologiquement soutenable (à froid)

Pour les besoins de mes activités pro et associatives, j’ai besoin de faire de la sauvegarde quotidienne de serveur. Les serveurs c’est déjà plutôt énergivore en électricité/climatisation (quoi que pas tant que ça si on s’auto-héberge – pas de climatisation par exemple). Je cherche à faire diminuer le coup énergétique tant que je peux, et les sauvegardes ne sont pas en reste.

Je suis donc parti sur une solution :

  • Froide : c’est-à-dire qui ne serait allumée qu’au besoin (au moment de la sauvegarde) et pas allumée 24h/24 comme à l’habitude dans un datacenter.
  • Faible consommation : Puisque composée d’un Raspberry Pi et d’un disque dur 2,5° = consommation ~3Wh
  • Auto-hébergée : ça veut dire « à la maison », ça évite les climatiseurs des datacenters (vitaux quand la concentration de serveurs est forte) et on garde le contrôle sur les données/le matériel.

En solution logiciel, j’utilise Dirvish, qui est une surcouche à Rsync, qui permet (en gros) de faire des sauvegardes « full » tous les jours en ne copiant que les différences. En gros il ne télécharge et enregistre que les fichiers différents de la veille et utilise l’adresse INODE sur le disque des fichiers qui n’ont pas changé. De cette façon, toute l’arborescence est complète tous les jours mais elle ne prend pas plus d’espace disque que la différence (un peu magique quand on ne connait pas le fonctionnement d’un partitionnement sous linux). De cette façon, ma sauvegarde quotidienne prend ~1h pour sauvegarder ~150Go sur 4 serveurs.

Le matériel utilisé

J’utilise donc pour mes sauvegardes à froid :

  • Un Raspberry Pi (4 ici mais pas vraiment d’importance – quoi que avec un Zero j’ai essayé c’est pas terrible en perf…)
  • Un disque dur 2,5° mécanique (ça serait encore plus économe en électricité avec un SSD) dans un boîtier USB 3.0

Consommation totale : 3Wh constaté au Wattmètre

Comparatif consommation

Pour comparaison (discutable j’en conviens) dans mon cas avec la solution choisie et une VM ou un serveur que j’aurais pu louer dans un datacenter :

RaspberryPi + DisqueDatacenter
Temps d’allumageLe temps de la sauvegarde (ici ~1h mais variable)24h/24h
Consommation électrique (W)38 à 170 (1)
Consommation journalière (Wh/j)3192 à 4080

(1) Consommation serveur nuancée entre une VM (8W) et un serveur physique dédié (170W) – chiffres de l’ADM (source). C’est tout relatif, ça dépend des cas, c’est une hypothèse de travail. Je n’ai pas non plus considéré les équipements actifs pour simplifier (chez moi juste une box, dans un datacenter beaucoup de switch/routeurs : même s’ils sont mutualisés, ça a un coût énergétique).

Avec cette méthode de sauvegarde à froid, dans mon cas, on est entre 64 et 1360 fois moins énergivore que dans le cas d’une sauvegarde ‘classique » en datacenter.

L’allumage automatique peut se faire de différente façon :

  • BIOS : certain bios permette un « réveil » à heure fixe mais ce n’est pas le cas du Raspberry Pi
  • WakeOnLan (fait par le serveur a sauvegarde par exemple)
  • Une prise programmable avec l’heure
  • PvMonit (dans mon cas) logiciel avec lequel je gère le surplus de mon énergie solaire

Dans tous les cas, le Raspberry Pi s’éteint de lui même après la sauvegarde (shutdown -h now dans le script à la fin de la sauvegarde).

Il y a quand même des désavantages à faire la sauvegarde « à la maison » :

  • Débit montant limité : dans le cas ou il faut remonter le serveur en entier (toute la sauvegarde) ça peut prendre un certain temps car le débit montant d’une box est souvent faible
    • Ce cas est très très rare
    • Ce défaut n’est plus si on est en situation auto-hébergée : on va brancher notre disque dur avec la sauvegarde directement sur le serveur à reconstruire
  • Restaurer à distance peut être problématique : si on est pas sur site et qu’on a besoin d’accéder aux sauvegardes, ça peut être problématique
    • Tout dépend de la conception pour l’allumage automatique. Dans mon cas, je peux allumer à distance au besoin avec PvMonit.

Après quelques tests de débits/écritures, voici ce que je constate sur ce « nas » de sauvegarde :

root@nas(ro):/mnt/nas# curl -4 -o ./test100M http://bouygues.testdebit.info/100M.iso
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 95.3M  100 95.3M    0     0  7145k      0  0:00:13  0:00:13 --:--:-- 7509k

En comparaison avec mon ordinateur :

david@monordinateurportable:/tmp$ curl -4 -o ./test100M http://bouygues.testdebit.info/100M.iso
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 95.3M  100 95.3M    0     0  8424k      0  0:00:11  0:00:11 --:--:-- 8689k

A ~100k de différence ça me semble pas significatif voir même plutôt bon ! (surpris même que ce soit si bon…)

Plus gros, plus redondant

Pour ceux qui ont de plus gros besoins en capacité / redondance de disque, il y a des HAT pour Raspberry Pi où il est possible de connecter plusieurs disques :

Héberger un WordPress : sécurité, optimisation, économie d’énergie

WordPress occupe une part très importante dans l’usage des CMS à l’heure ou j’écris ce billet (62%). Voilà ce qui peut l’expliquer à mon sens :

  • Simple d’utilisation côté utilisateur
  • Plutôt simple côté développeur
  • Un nombre incalculable de thèmes et de plugins sont à disposition (communauté conséquente).

Mais il a aussi les défaut de ses qualités :

  • Côté utilisation de ressources énergétiques (serveur), WordPress est une catastrophe… Surtout dès qu’on lui ajoute des plugins (qui n’en ajoute pas ?).
  • Côté sécurité, il est très attaqué (car très utilisé). Le sport préféré des hackers, c’est le brute force…

Je vais tâcher de balayer ici mes trucs pour transformer un wordpress énergivore en wordpress sobre et sécurisé 🙂

Transformer en site statique (parfait pour les sites vitrines)

Un site « statique » c’est quoi ? C’est un site ou le code est uniquement exécuté sur le client et le serveur ne fait rien d’autre que « servir la page ». Alors qu’un site dynamique par opposition (sous wordpress pour l’exemple) va générer des pages « à la volée » et donc utiliser des ressources sur le serveur.

Mon parti-pris, c’est de convertir le wordpress « visible pour les visiteurs » en site statique « html ». L’utilisateur continue d’utiliser / d’alimenter son site via l’interface (bien faite et connue) wordpress, mais le visiteur lui ce qu’il voit c’est un site « html », sans aucun code dynamique (php ici) exécuté. De cette façon :

  • Le site est inattaquable par les hackers/spameurs, il n’y a plus de code dynamique, donc plus de moyen de corrompre le site / le serveur
  • L’affichage pour le visiteur est beaucoup plus rapide.
  • Le serveur consomme beaucoup moins de ressources (moins d’électricité, plus petit serveur…).
  • Les utilisateurs qui connaissent déjà wordpress ne sont pas bouleversés / impactés par cette optimisation.

Pour cela j’utilise un plugin wordpress qui s’appelle wp2static. Voici la structure du répertoire « web » qui est la racine du site :

  • /wp/ : site wordpress avec le plugin wp2static. Adresse utilisée par l’administrateur du site pour faire ces modifications
  • /static/ : le site statique généré par le plugin
  • /.htaccess : contient les redirections pour que le visiteur ne voit pas de différence

Dans le wordpress installé dans /wp/ rendez -vous dans wp2statics et configuré :

  • Where will you host the optimized version of your site? : Subdirectory
  • Destination URL : http://votresite.fr/
  • Target Directory : /var/www/votreiste.fr/web/static/

Puis dans l’onglet Crawling indiqué :

  • Exclude certain URLs :
    • /wp/
    • /wp-content/uploads/
    • /wp-admin/

Quand c’est fait, vous pouvez cliquer sur Start Static Export.

Ajouter dans votre configuration apache ou dans un .htaccess à la racine du site :

RewriteEngine on
# Pas de ré-écriture pour le contenu uploadé (images, 
RewriteRule ^/wp-content/uploads/(.*)$          /wp/wp-content/uploads//$1 [L]
# Rediriger toutes les requêtes vers /static sauf quand l'accès est souhaité sur le site wordpress original (/wp)
RewriteCond %{REQUEST_URI} !^/wp/ 
RewriteRule ^(.*)$          /static/$1 [L]

Inconvénient majeur de ce type de « transformation » en site full static

  • A chaque modification l’utilisateur doit penser à re-générer le site statique
  • Le contenu dynamique (formulaire de contact / commentaire) est impossible :
    • Pour les commentaires, c’est possible avec des commentaires chargés en Javascript, typiquement HashOver Next (alternative auto-hébergable à DISQUS). J’ai fait un script de migration WordPress > HashOver : wp2hashover
    • Pour les commentaires, il est possible d’utiliser des formulaires en iframe ou formulaire javascript/ajax

Transformer en (presque) site statique (W3 Total Cache)

La solution wp2static n’était pas complètement satisfaisante et j’ai continué mes recherches. En testant le plugin W3 Total Cache, je me suis aperçu qu’il faisait exactement ce que je cherchais à faire et même plus !

Après paramétrage, j’ai trouvé ceci dans le fichier .htaccess :

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteBase /
[...]
    RewriteCond %{REQUEST_METHOD} !=POST
[...]
    RewriteCond "%{DOCUMENT_ROOT}/wp-content/cache/page_enhanced/%{HTTP_HOST}/%{REQUEST_URI}/_index%{ENV:W3TC_SSL}%{ENV:W3TC_PREVIEW}.html%{ENV:W3TC_ENC}" -f
    RewriteRule .* "/wp-content/cache/page_enhanced/%{HTTP_HOST}/%{REQUEST_URI}/_index%{ENV:W3TC_SSL}%{ENV:W3TC_PREVIEW}.html%{ENV:W3TC_ENC}" [L]
</IfModule>

Pour résumer, ce que font les règles c’est :

  • Si la page HTML static a été générée (le fichier existe) dans /var/www/decroissant-au-beurre.com/web/wp-content/cache/page_enhanced/decroissant-au-beurre.com/services/_index_ssl.html (pour l’appel à https://decroissant-au-beurre.com/services/ alors on l’appelle directement (pas d’exécution de code PHP… ) SINON on appelle la page « normale » dans wordpress et celui-ci génère la page de cache /static pour le prochain visiteur
  • Dès qu’il y a une requête POST, le cache n’est pas appelé. Donc tous les formulaires restent compatibles avec le cache et seules les pages de résultats ne seront pas tirées du cache.

Ca veut dire qu’a performance égale avec la solution précédente, ce plugin fait mieux car il gomme les défauts précédents et permet de continuer d’utiliser des formulaires/les commentaires.

C’est parfait ! Le cache peut aussi être stocké via memcache et non sur disque (ce qui rend l’accès / l’affichage encore plus rapide (d’autres technologies possibles : APC / Xcache…)

Les fonctionnalités supplémentaires :

Minifier : diminuer trafic réseau

W3 Total Cache propose une fonctionnalité de Minification

minifier signifie réduire la taille du code. C’est un processus très utilisé en programmation web pour réduire la taille d’un programme à télécharger depuis un serveur et ainsi réduire l’encombrement du réseau. Cela peut aussi être considéré comme une forme d’offuscation du code.

Pour cela on supprime tous les commentaires et les espaces qui ne gêneront pas le bon fonctionnement de l’application. On remplace aussi le nom des variables interne à l’application pour les réduire à un seul ou deux caractères. Il est aussi possible d’utiliser certaines écritures compactes propres aux langages (couleur en hexadécimal, raccourcis…)

https://fr.wikipedia.org/wiki/Minification

Si on analyse le code après avoir activé la Minification on observe :

<!DOCTYPE html><html lang=fr-FR><head><script>window.w3tc_lazyload=1,window.lazyLoadOptions={elements_selector:".lazy",callback_loaded:function(t){var e;try{e=new CustomEvent([...]

Alors qu’un site non-minifié donne plutôt :

<!DOCTYPE html>
<html lang="fr-FR">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    
<link rel="stylesheet" id="extend-builder-css-css" href="assets/static/css/theme.css" type="text/css" media="all">
<style id="extend-builder-css-inline-css" type="text/css">
/* page css */
/* part css : theme */

.h-y-container > *:not(:last-child), .h-x-

Vous pouvez minifier tout ou partie du code (HTML/ CSS / Javascript…). Certains plugins peuvent ne plus fonctionner à cause de la minification. Il est donc important de tester votre site après avoir activé ces options.

Lazy Loading : Afficher les images au besoin

Si vous avez un site très long, il est possible que des images soient présentes en bas de page. Il est pertinent de ne charger ces pages qu’au besoin (que si vous allez jusqu’en bas de la page). Et bien la fonction Lazy Loading permet cela.

Cette fonctionnalité permet aussi un gain non négligeable de bande passante.

Mise en cache navigateur

Vous pouvez améliorer les entêtes navigateurs en spécifiant à celui-ci des dates d’expiration de fichier. De cette façon il ne va pas retourner charger la/les pages sur le serveur en cas de nouvelles visites.

Mesures performance

Voilà un stress test (envoi de requêtes massives) du même site avec ou sans génération en site statique (fait avec W3 total cache ici) :

On constate que c’est 20 fois plus rapide à l’affichage et que c’est plutôt constant. Est-ce que c’est 20 fois moins énergivore ? ça c’est difficile à mesurer mais c’est forcément moins…

Voici ci-après le poids de chargement d’une même page sans optimisation et avec Lazy Loading + Minification

  • Sans optimisation : 21,5Mo
  • Avec Minification et Lazy : 3,9Mo

Soit 5,5 fois moins lourd…

Sécurité

Le minimum pour sécuriser un wordpress c’est :

  • Faire les mises à jour (de nombreux plugins existent pour automatiser cela)
  • Ne pas utiliser « admin » en nom d’utilisateur admin
  • Avoir des mots de passe forts
  • Bloquer les tentatives de brutes forces :

WordPress se fait pas mal attaquer du fait qu’il soit populaire. La principale attaque (hors SPAM sur les commentaires) est faite par brute-force sur la page wp-login.php et xmlrpc. J’ai consacré un article dédié pour intégrer les logs de WordPress dans Fail2ban, et ainsi pouvoir bloquer les tentatives au niveau firewall : WordPress & fail2ban : stopper la brute-force « POST /wp-login.php » (uniquement possible sur un hébergement dédié). C’est particulièrement efficace comme vous pouvez le voir sur ce graphique :

Graphique fail2ban sur 1 semaine

Gestion serveur mutualisé

Si vous gérez un serveur mutualisé, vous pouvez déployer le plugin de cache et faire les mise à jour de tous les wordpress installé sur votre serveur avec wp-cli-isp (taillé pour les serveurs sous ISPconfig, mais c’est adaptable)

file2link : Stockage de fichier temporaire contre lien

Depuis que Framasoft à annoncé sa Déframasoftisons Internet, en tant que membre des CHATONS (avec zici.fr & retzien.fr), je me suis dis que c’était l’heure de se retrousser les manches.

Faire un framadrop ça me semblait accessible, par contre Lufi (utilisé par framadrop) c’est codé en Perl et sur les serveurs de l’asso on est plus en mode PHP (et j’essaie de pas trop me disperser et pas héberger : un coup du perl, un coup du python, un coup du ruby, du node.js… c’est plus facile à maintenir) Sauf qu’en PHP je n’ai pas trouvé de logiciel maintenu similaire qui répondent à mes attentes, du coup j’en ai pondu un : file2link.

Bien sûr tout est libre de droit, file2link est sous licence beerware.

Petit tour en images :

Si vous l’utilisez, dites le moi, ça me parait pertinent qu’on fasse du lien avec toutes les instances…

Fonctionnalités

Dans sa version 0.1 le logiciel permet :

  • Uploader des fichier (plusieurs possible) et de recevoir des liens en échanges :
    • Lien direct
    • Lien de téléchargement forcé
    • Lien vers un zip contenant tout les fichiers
  • Images redimensionné à l’upload possible
  • Mémorisation des fichiers précédaient uploadé (via le storages local du navigateur
  • Se configure avec un fichier YAML
  • Configure with YAML file :
    • Limite dans le temps
    • Limite le type de fichier (via mime)
    • Limite le nombre de fichier envoyé simultanément
    • Limite de type de fichier
  • Bar de progression en HTML5
  • Multi-langue (français / anglais pour le moment)
  • Ménage des fichiers expiré par tâche cron (cron.php) ou par traffic sur la page d’accueil (web cron)
  • Personnalisation du contenu de la page avec la possibilité de créer des pages header-page.php, header.php, footer-page.php, footer.php, start-home.php, end-home.php
  • Ajout d’un menu vers des pages HTML pour créer des pages « contact » « mention légal »…

Installation

Pré-requis :

  • Un serveur http configuré compatible avec htaccess / url rewriting (apache2 typiquement
  • PHP 5.6 minimum
  • Lib php : GD & ZIP

Envoyer le contenu du dépôt sur votre serveur :

git clone https://framagit.org/kepon/file2link.git

Déplacer le fichier de conf distribué :

mv config.yaml.exemple config.yaml

Modifier le fichier config.yaml, changé le baseUrl et autres paramètres que vous souhaitez personnalisé. Assuré vous de la cohérence des paramètres :

  • maxUploadPerFile doit être similaire à la config PHP upload_max_filesize and post_max_size
  • maxUploadNbdoit doit être similaire à la config PHP config max_file_uploads

Assurez vous que le répertoire « files » soit accessible en écriture pour le servuer

Option : si vous voulez faire le ménage par tâche cron modifier la valeur expireCron par « cron » dans le fichier config.yaml et ajouter votre tâche cron :

* * * * * cd /var/www/file2link/ ; php cron.php

Et voilà, fin de chantier !

ISPConfig : Un mode maintenance avec SSL

By RRZEicons (Cc-sa)

Afin de réaliser des maintenances ou migration sur mon serveur qui héberge ~100 sites et qui tourne avec le panel ISPConfig j’ai mis en place un petit mot d’excuse qui dit en substance : « maintenance en cours, merci de repasser plus tard ». C’est mieux que « La connexion a échoué » ou « Can’t Connect to MySQL Server on nian nian nian » non ?. C’était plutôt simple jusqu’à l’arrivé massive du SSL. parce que quand on tente de joindre https://david.mercereau.info ou https://zici.fr et qu’on est redirigé vers la même page de maintenance, avec le SSL ça coince / ça affiche un message d’erreur, normal… hors c’est ce qu’on cherche à éviter.

La configuration de prod en temps normal  :

  • Apache sur le port 80 & 443
  • Let’sEncrypt délivre les certificat SSL
  • Le tout est motorisé par ISPConfig

La configuration de maintenance :

  • Apache sur le port 80 & 443 (toujours)
  • Lighttpd sur le port 81 & 444
  • Iptables redirige le trafic arrivant sur le port 80 vers le port 81 en PREROUTING sauf pour mon IP (ça permet donc de bricoler)

Installation de lighttpd

Apété, et ayé :

aptitude -y install lighttpd
update-rc.d lighttpd remove
mkdir /var/www/maintenance
echo "Serveur en maintenance" > /var/www/maintenance/index.html # Vous pouvez faire une belle page html c'est mieux !

La configuration de lighttpd : /etc/lighttpd/lighttpd.conf

server.modules = (
	"mod_access",
)

server.document-root        = "/var/www/maintenance"
server.upload-dirs          = ( "/var/cache/lighttpd/uploads" )
server.errorlog             = "/var/log/lighttpd/error.log"
server.pid-file             = "/var/run/lighttpd.pid"
server.username             = "www-data"
server.groupname            = "www-data"
server.port                 = 81
server.error-handler-404 = "/index.html"

index-file.names            = ( "index.html", "index.lighttpd.html" )
url.access-deny             = ( "~", ".inc" )
static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" )

include_shell "/usr/share/lighttpd/create-mime.assign.pl"
include_shell "/usr/share/lighttpd/include-conf-enabled.pl"

Récupérer la configuration SSL

J’ai fais un script qui récupère dans configuration apache (généré par ISPConfig chez moi), les chemins vers les certificats SSL Let’sEncrypt et qui (re)génère la conf lighttpd (qui crée le fichier /etc/lighttpd/conf-enabled/maintenance-mode-ssl.conf).

Préparation avant de lancer le script :

pear install config
mkdir /etc/lighttpd/certs/

C’est bon, on peut lancer le script :

<?php
require_once 'Config.php';

# Add include "maintenance-mode-ssl.conf" In lighttpd.conf
$lighttpdConfig='/etc/lighttpd/conf-enabled/maintenance-mode-ssl.conf';
# Content of SSL .pem
$ligttpdSslDir = '/etc/lighttpd/certs/';
# SSL port
$ligttpdSslPort = 444;

$sitesEnableds = scandir('/etc/apache2/sites-enabled/');
$lighttpdConfigContent='';
foreach($sitesEnableds as $sitesEnabled) {
    if ($sitesEnabled != '.' && $sitesEnabled != '..' && $sitesEnabled != '000-ispconfig.vhost' && $sitesEnabled !=  '000-apps.vhost' && $sitesEnabled != '000-default.conf' && $sitesEnabled != '000-ispconfig.conf') {
        $DocumentRoot=null;
        $ServerAlias=null;
        $ServerAliasMultiple=null;
        $ServerName=null;
        $conf = new Config();
        $root = $conf->parseConfig('/etc/apache2/sites-enabled/'.$sitesEnabled, 'apache');
        if (PEAR::isError($root)) {
            echo 'Error reading config: ' . $root->getMessage() . "\n";
            exit(1);
        }
        // Parse du fichier
        $i = 0;
        while ($item = $root->getItem('section', 'VirtualHost', null, null, $i++)) {
            foreach ($item->children as $child) {
                if ($child->name == 'ServerName') {
                    $ServerName = $child->content;
                }
                if ($child->name == 'ServerAlias') {
                    $ServerAliasMultiple=explode(" ", $child->content);
                    foreach ($ServerAliasMultiple as $ServerAliasOne) {
                        if (is_null($ServerAlias)) {
                            $ServerAlias[]=$ServerAliasOne;
                        } elseif (!in_array($ServerAliasOne, $ServerAlias))  {
                            $ServerAlias[]=$ServerAliasOne;
                        }
                    }
                }
                if ($child->name == 'DocumentRoot') {
                    $DocumentRoot = $child->content;
                }
            }
        }
        if ($ServerName != null && $DocumentRoot != null) {
            if (is_file(substr($DocumentRoot, 0, -3).'ssl/'.$ServerName.'-le.key')) {
                $key=file_get_contents(substr($DocumentRoot, 0, -3).'ssl/'.$ServerName.'-le.key');
                $crt=file_get_contents(substr($DocumentRoot, 0, -3).'ssl/'.$ServerName.'-le.crt');
                file_put_contents($ligttpdSslDir.'/'.$ServerName.'.pem', $key.$crt);
                $lighttpdConfigContent.='$HTTP["host"] == "'.$ServerName.'" {
  $SERVER["socket"] == ":'.$ligttpdSslPort.'" {
    ssl.engine = "enable" 
    ssl.pemfile = "'.$ligttpdSslDir.$ServerName.'.pem" 
  }
}
';
                foreach ($ServerAlias as $ServerAliasJustOne) {
                    $lighttpdConfigContent.= '$HTTP["host"] == "'.$ServerAliasJustOne.'" {
  $SERVER["socket"] == ":'.$ligttpdSslPort.'" {
    ssl.engine = "enable" 
    ssl.pemfile = "'.$ligttpdSslDir.$ServerName.'.pem" 
  }
}
';
                }
            }
        }
    }
}
file_put_contents($lighttpdConfig, $lighttpdConfigContent);
?>

ça doit générer quelque chose comme ça dans le fichier /etc/lighttpd/conf-enabled/maintenance-mode-ssl.conf :

$HTTP["host"] == "zici.fr" {
  $SERVER["socket"] == ":444" {
    ssl.engine = "enable" 
    ssl.pemfile = "/etc/lighttpd/certs/zici.fr.pem" 
  }
}
$HTTP["host"] == "david.mercereau.info" {
  $SERVER["socket"] == ":444" {
    ssl.engine = "enable" 
    ssl.pemfile = "/etc/lighttpd/certs/david.mercereau.info.pem" 
  }
}
$HTTP["host"] == "calcpv.net" {
  $SERVER["socket"] == ":444" {
    ssl.engine = "enable" 
    ssl.pemfile = "/etc/lighttpd/certs/calcpv.net.pem" 
  }
}
[...]

Maintenance On/Off

Pour se mettre en mode maintenance :

# Mon ip : 
IPMAISONDEMOI=X.X.X.X
# Lancer le script ci-dessus qui génère la conf SSL pour lighttpd 
php maintenance-ssl-lighttpd.php
# Démarrage du serveur d'excuse
service lighttpd start
# Redirection du port 80 vers 81 sauf pour mon IP
iptables -A INPUT -p tcp --dport 81 -j ACCEPT
iptables -A INPUT -p tcp --dport 444 -j ACCEPT
iptables -t nat -A PREROUTING \! -s ${IPMAISONDEMOI} -p tcp --dport 80 -j DNAT --to-destination ${IPDUSERVEUR}:81
iptables -t nat -A PREROUTING \! -s ${IPMAISONDEMOI} -p tcp --dport 443 -j DNAT --to-destination ${IPDUSERVEUR}:444
iptables -t nat -A POSTROUTING -j MASQUERADE

Et pour désactiver le mode maintenance :

# Si vous n'avez pas d'autres règles (c'est mal) :
iptables -F
iptables -t nat -F
# Sinon redémarrer votre service iptables 
# Lighttpd n'est plus utile
service lighttpd stop

Script API Ispconfig création utilisateur

Je m’occupe d’un service d’hébergement destiné aux actions politiques / citoyennes / artistiques… Le serveur était plein de chez plein, ça tournait sans que je m’en occupe trop… J’ai décroché de ces geekeries pour d’autres occupations… Mais voilà Debian 7 était en fin de support, j’ai migré, fais du ménage… du coup il y a de nouveau de la place sur le serveur donc je ré-ouvre les portes.

De ce fait j’ai automatisé une tâche qui me prenait du temps jadis : la création des comptes (même si avec Ispconfig, c’est quand même clique clique). J’ai donc accouché d’un script PHP qui utilise l’API d’ISPconfig, on y passe en paramètre :

  • Nom d’utilisateur
  • Email de l’utilisateur
  • Sous domaine souhaité (ici : cequejesouhait.zici.fr)

Et il nous génère :

  • Création de l’utilisateur « client » pour l’accès au panel Ispconfig
  • Création du site web
    • Création d’une base de donnée et de son utilisateur
    • Création d’un compte FTP et SFTP pour l’accès au site web
  • Création d’un transfère mail (ici cequejesouhait@zici.fr vers l’email de l’utilisateur)

Le script est téléchargeable ici.

Firewall : Mon script iptables

Je partage ici mon script de firewall iptable. C’est un script « à l’ancienne », dans du bash… ça fait le taf, mais rien de bien transsudant. En gros :

  • On ferme tout les ports sauf ceux qui nous intéresse (80, 25, icmp…)
  • Petite fonction pour ouvrir les ports mis en écoute sur Portsentry. Portsentry c’est un petit logiciel de sécurité en mode « pot de miel ». On met des ports en écoute mais il n’y a rien derrière. Dès que quelqu’un tente de s’y connecter (un robot ou quelqu’un de malveillant), ça bloque son IP dans le firewall pour un temps donnée. C’est radical si vous déplacez le port SSH du 22 vers autre chose et que vous mettez Portsentry à écouter (entre autre) sur le 22…
  • Mode maintenance du serveur web (lancé via ./iptables.sh maintenance). Il permet de mettre une page de maintenance pour tout le monde sauf pour vous (j’explique en détail dans cet article)
#!/bin/bash

## IP :
# Chez moi
MOI="A.A.A.A" 
# Mon serveur
SRV1="X.X.X.X"

IPT="/sbin/iptables"
PORTSENTRYCONF="/etc/portsentry/portsentry.conf"

export IPT PORTSENTRYCONF

function portsentryOpen() {
	. ${PORTSENTRYCONF}
	IFS=',' read -ra TCP_PORTS_SPLIT <<< "${TCP_PORTS}"
	for TCP_PORT in "${TCP_PORTS_SPLIT[@]}"; do 
		${IPT} -A INPUT -p tcp --dport ${TCP_PORT} -j ACCEPT
	done
	IFS=',' read -ra UDP_PORTS_SPLIT <<< "${UDP_PORTS}"
	for UDP_PORT in "${UDP_PORTS_SPLIT[@]}"; do 
		${IPT} -A INPUT -p udp --dport ${UDP_PORT} -j ACCEPT
	done
}

# Remise a 0
${IPT} -F
${IPT} -t nat -F

# Les connexions entrantes sont bloquées par défaut
${IPT} -P INPUT DROP
# Les connexions destinées à être routées sont acceptées par défaut
${IPT} -P FORWARD ACCEPT
# Les connexions sortantes sont acceptées par défaut
${IPT} -P OUTPUT ACCEPT


######################
# Règles de filtrage #
######################
# Nous précisons ici des règles spécifiques pour les paquets vérifiant
# certaines conditions.
 
# Pas de filtrage sur l'interface de "loopback"
${IPT} -A INPUT -i lo -j ACCEPT
 
# Accepter le protocole ICMP (notamment le ping)
${IPT} -A INPUT -p icmp -j ACCEPT
  
# Accepter les packets entrants relatifs à des connexions déjà
# établies : cela va plus vite que de devoir réexaminer toutes
# les règles pour chaque paquet.
${IPT} -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT

# ftp 
${IPT} -A INPUT -p tcp --dport 20 -j ACCEPT 
${IPT} -A INPUT -p tcp --dport 21 -j ACCEPT
# Préalabielemnt, pour pure-ftpd : echo "29700 29750" > /etc/pure-ftpd/conf/PassivePortRange ${IPT} -A INPUT -p tcp --dport 29700:29750 -j ACCEPT
# SSH
${IPT} -A INPUT -p tcp --dport 222 -j ACCEPT
# NTP
${IPT} -A INPUT -p udp --dport 123 -j ACCEPT
# smtp
${IPT} -A INPUT -p tcp --dport smtp -j ACCEPT
# Pour test bricolage smtp
${IPT} -A INPUT -p tcp --dport 587 -j ACCEPT
# imap(s)
${IPT} -A INPUT -p tcp --dport 143 -j ACCEPT
${IPT} -A INPUT -p tcp --dport 993 -j ACCEPT
# sieve
${IPT} -A INPUT -p tcp --dport 4190 -j ACCEPT
# dns
${IPT} -A INPUT -p tcp --dport domain -j ACCEPT
${IPT} -A INPUT -p udp --dport domain -j ACCEPT
# http
${IPT} -A INPUT -p tcp --dport http -j ACCEPT
# https
${IPT} -A INPUT -p tcp --dport https -j ACCEPT

# Maintenance 
if [ "$1" == "maintenance" ] ; then
	echo "Maintenance On"
	/usr/sbin/service lighttpd start
	${IPT} -A INPUT -p tcp --dport 81 -j ACCEPT
	${IPT} -t nat -A PREROUTING \! -s ${MOI} -p tcp --dport 80 -j DNAT --to-destination ${SRV1}:81
	${IPT} -t nat -A POSTROUTING -j MASQUERADE
elif [ -f "/var/run/lighttpd.pid" ] ; then
	echo "Maintenance Off"
	/usr/sbin/service lighttpd stop
fi

# Portsentry 
if [ -f ${PORTSENTRYCONF} ] ; then
	portsentryOpen ${IPT} ${PORTSENTRYCONF}
fi

# End
${IPT} -A INPUT -j LOG --log-prefix "iptables denied: "  --log-level 4
${IPT} -A INPUT -j REJECT

# Si vous utilisez fail2ban, relancé à la fin du script :
#/usr/sbin/service fail2ban restart