Installation d’un blog WordPress sur un VPS Gandi

Publié le 25 août 2010 par Nicolargo

Comme vous le savez, je me suis fait hacké mon site pendant les “grandes vacances”. Cette petite contrariété m’a fait faire dans l’urgence une chose que j’avais planifier depuis un certain moment: la migration du Blog de Nicolargo sur un serveur privé virtuel de Gandi.

Nous allons donc voir dans ce billet comment installer, sécuriser et optimiser une blog WordPress sur une serveur privé virtuel !

Commencons par le commencement

Il faut d’abord choisir les performances initiales du serveur virtuel. J’ai utilisé le serveur que j’avais créé il y a quelques mois. C’est l’entrée de gamme, c’est à dire 1 part correspondant aux spécifications suivantes:

  • 1/64ème des capacités globales du serveur, soit environ 1 GHz
  • 1/64ème des 16 Go de mémoire du serveur, soit 256 Mo de RAM garantie avec un burst possible à 2 Go (+512 Mo de Swap)
  • 1/64ème des capacités réseau, soit 5 Mbits dédiés
  • 1/64ème du disque réservé au serveur, soit 5 Go de disque data (+3 Go pour le système)

Comme nous allons le voir plus tard cette configuration n’est pas adapté pour un blog avec un trafic relativement important (environ 1 00 000 pages vues par mois) . Je suis donc rapidement passé à la configuration suivante:

Nous disposons donc d’un accès SSH (root) sur le serveur,. Après une mise à jour du système d’ecploitation (Ubuntu Server 9.04) nous pouvons enfin commencer les choses sérieuses…

Installation du serveur Web Apache

On commence par installer le serveur:

sudo aptitude install apache2

Puis on active les modules qui vont nous permettre d’optimiser les performances de WordPress:

sudo a2enmod deflate

sudo a2enmod headers

sudo a2enmod rewrite

sudo a2enmod expires

Il est également possible d’adapter la configuration des processus Apache (nombre de process, nombre maximal de client par process..) en modifiant le fichier /etc/apache2/apache2.conf:

<IfModule mpm_prefork_module>

StartServers 5

MinSpareServers 5

MaxSpareServers 10

MaxClients 40

MaxRequestsPerChild 2000

</IfModule>

StartServers permet de configurer le nombre de processus Apache lancés au démarrage.

MinSpareServers et MaxSpareServers donnent respectivement le nombre minimum et maximum de processus Apache selon les besoins.

MaxClients permet de configurer le nombre de clients (au sens session TCP) pouvant se connecter simultanément à un processus Apache. Pour des sites dynamiques (comme WordPress) il est plutôt conseillé de ne pas avoir une valeur trop élevée afin d’éviter les swap mémoire sur disque.

Une solution proposée sur ce blog pour calculer la valeur MaxClients est la suivantes: Il faut d’abord regarder combien de mémoire prend un processus apache2 (par exemple avec la commande top), puis diviser la mémoire système disponible par cette valeur. Par exemple, pour un serveur disposant de 1 Go de RAM (1000 Mo) et dont chaque processus Apache occupe 25 Mo, on a le calcul suivant:

MaxClients = 1000 / 25 = 40

Enfin MaxRequestsPerChild permet de définir le nombre maximal de requête qu’un processus peut prendre en charge avant d’être tué. Il est conseillé de mettre une valeur différente de 0 (le processus n’est alors jamais tué) pour éviter qu’une processus ne consomme trop de mémoire si un problème arrive.

On passe ensuite aux directives KeepAlive qui permette de configurer finement le temps de vie des sessions TCP. Pour un site dynamique comportant un nombre important d’images et de JS (comme mon blog), les valeurs suivants sont un bon point de départ:

# KeepAlive: Whether or not to allow persistent connectionsKeepAlive On# MaxKeepAliveRequests: The maximum number of requests to allowMaxKeepAliveRequests 200# KeepAliveTimeout: Number of seconds to wait for the next requestKeepAliveTimeout 5# Timeout: The number of seconds before receives and sends time out.Timeout 50

Enfin on relance le serveur pour que la configuration soit prise en compte:

sudo /etc/init.d/apache2 restart

Installation de MySQL

On commence par installer le serveur MySQL:

sudo aptitude install mysql-server

Puis on créé une base de donnée et un compte utilisateur associé qui va servir pour WordPress:

mysql -u adminusername -p

mysql> CREATE DATABASE databasename;

Query OK, 1 row affected (0.00 sec)

mysql> GRANT ALL PRIVILEGES ON databasename.* TO “wordpressusername”@”hostname”

-> IDENTIFIED BY “password”;

Query OK, 0 rows affected (0.00 sec)

mysql> FLUSH PRIVILEGES;

Query OK, 0 rows affected (0.01 sec)

mysql> EXIT

On peut optimiser la base de donnée en éditant le fichier /etc/mysql/my.cnf:

query_cache_type = 1

query_cache_limit = 2M

query_cache_size = 32M

Ne pas oublier de relancer le serveur MySQL pour prendre en compte les modifications.

Installation de WordPress

Depuis le hack de mon blog, j’ai décidé de travailler directement avec la version SVN de WordPress afin de disposer au plus vite des patchs de sécurité. Une fois de le répertoire racine de votre site Web (/home/nicolargo/web/blog dans mon cas), il faut saisir la commande suivante:

svn co http://core.svn.wordpress.org/trunk/ .

On ajoute ensuite la ligne suivante dans le fichier .htaccess afin de rendre invisible les sous répertoires .svn:

RewriteRule ^(.*/)?\.svn/ – [F,L]

Puis on finalise l’installation en suivant la fameuse installation de WordPress en 5 minutes.

Il est ensuite conseillé de supprimer le fichier wp-admin/install.php (qui ne servira plus une fois l’installation faite):

rm wp-admin/install.php

Sécurisation de WordPress

Cette sécurisation passe par deux étapes. Une première est de protéger le blog à l’aide du fichier .htaccess. La seconde est d’installer des plugins de sécurité qui vont nous permettre de faire régulièrement des audits sur notre serveur.

Pour le fichier .htaccess, j’utilise la base suivante:

# MAIN

RewriteEngine On

ServerSignature Off

Options All -Indexes

Options +FollowSymLinks

# SVN protect

RewriteRule ^(.*/)?\.svn/ – [F,L]

# Secure .htaccess

<Files .htaccess>

Order Allow,Deny

Deny from all

</Files>

# Secure wp-config.php

<Files wp-config.php>

Order Deny,Allow

Deny from all

</Files>

# FILTER REQUEST

<IfModule mod_rewrite.c>

RewriteBase /

RewriteCond %{REQUEST_FILENAME} !-f

RewriteCond %{REQUEST_FILENAME} !-d

RewriteRule . /index.php [L]

</IfModule>

# BEGIN Expire Header

<FilesMatch “\.(ico|jpg|jpeg|png|gif|js|css|swf)$”>

ExpiresDefault “access plus 2 hours”

</FilesMatch>

# BLACKLIST CANDIDATES

<Limit GET POST PUT>

Order Allow,Deny

Allow from all

Deny from 75.126.85.215 “# blacklist candidate 2008-01-02 = admin-ajax.php attack “

Deny from 128.111.48.138 “# blacklist candidate 2008-02-10 = cryptic character strings “

Deny from 87.248.163.54 “# blacklist candidate 2008-03-09 = block administrative attacks “

Deny from 84.122.143.99 “# blacklist candidate 2008-04-27 = block clam store loser “

Deny from 210.210.119.145 “# blacklist candidate 2008-05-31 = block _vpi.xml attacks “

Deny from 66.74.199.125 “# blacklist candidate 2008-10-19 = block mindless spider running “

Deny from 203.55.231.100 “# 1048 attacks in 60 minutes”

Deny from 24.19.202.10 “# 1629 attacks in 90 minutes”

</Limit>

# QUERY STRING EXPLOITS

<IfModule mod_rewrite.c>

RewriteCond %{QUERY_STRING} \.\.\/ [NC,OR]

RewriteCond %{QUERY_STRING} boot\.ini [NC,OR]

RewriteCond %{QUERY_STRING} tag\= [NC,OR]

RewriteCond %{QUERY_STRING} ftp\: [NC,OR]

RewriteCond %{QUERY_STRING} http\: [NC,OR]

RewriteCond %{QUERY_STRING} https\: [NC,OR]

RewriteCond %{QUERY_STRING} mosConfig [NC,OR]

RewriteCond %{QUERY_STRING} ^.*(\[|\]|\(|\)|<|>|’|”|;|\?|\*).* [NC,OR]

RewriteCond %{QUERY_STRING} ^.*(%22|%27|%3C|%3E|%5C|%7B|%7C).* [NC,OR]

RewriteCond %{QUERY_STRING} ^.*(%0|%A|%B|%C|%D|%E|%F|127\.0).* [NC,OR]

RewriteCond %{QUERY_STRING} ^.*(globals|encode|config|localhost|loopback).* [NC,OR]

RewriteCond %{QUERY_STRING} ^.*(request|select|insert|union|declare|drop).* [NC]

RewriteRule ^(.*)$ – [F,L]

</IfModule>

# CHARACTER STRINGS

<IfModule mod_alias.c>

# BASIC CHARACTERS

RedirectMatch 403 \,

RedirectMatch 403 \:

RedirectMatch 403 \;

RedirectMatch 403 \=

RedirectMatch 403 \@

RedirectMatch 403 \[

RedirectMatch 403 \]

RedirectMatch 403 \^

RedirectMatch 403 \`

RedirectMatch 403 \{

RedirectMatch 403 \}

RedirectMatch 403 \~

RedirectMatch 403 \”

RedirectMatch 403 \$

RedirectMatch 403 \<

RedirectMatch 403 \>

RedirectMatch 403 \|

RedirectMatch 403 \.\.

RedirectMatch 403 \/\/

RedirectMatch 403 \%0

RedirectMatch 403 \%A

RedirectMatch 403 \%B

RedirectMatch 403 \%C

RedirectMatch 403 \%D

RedirectMatch 403 \%E

RedirectMatch 403 \%F

RedirectMatch 403 \%22

RedirectMatch 403 \%27

RedirectMatch 403 \%28

RedirectMatch 403 \%29

RedirectMatch 403 \%3C

RedirectMatch 403 \%3E

RedirectMatch 403 \%3F

RedirectMatch 403 \%5B

RedirectMatch 403 \%5C

RedirectMatch 403 \%5D

RedirectMatch 403 \%7B

RedirectMatch 403 \%7C

RedirectMatch 403 \%7D

# COMMON PATTERNS

Redirectmatch 403 \_vpi

RedirectMatch 403 \.inc

Redirectmatch 403 xAou6

Redirectmatch 403 db\_name

Redirectmatch 403 select\(

Redirectmatch 403 convert\(

Redirectmatch 403 \/query\/

RedirectMatch 403 ImpEvData

Redirectmatch 403 \.XMLHTTP

Redirectmatch 403 proxydeny

RedirectMatch 403 function\.

Redirectmatch 403 remoteFile

Redirectmatch 403 servername

Redirectmatch 403 \&rptmode\=

Redirectmatch 403 sys\_cpanel

RedirectMatch 403 db\_connect

RedirectMatch 403 doeditconfig

RedirectMatch 403 check\_proxy

Redirectmatch 403 system\_user

Redirectmatch 403 \/\(null\)\/

Redirectmatch 403 clientrequest

Redirectmatch 403 option\_value

RedirectMatch 403 ref\.outcontrol

# SPECIFIC EXPLOITS

RedirectMatch 403 errors\.

RedirectMatch 403 config\.

RedirectMatch 403 include\.

RedirectMatch 403 display\.

RedirectMatch 403 register\.

Redirectmatch 403 password\.

RedirectMatch 403 maincore\.

RedirectMatch 403 authorize\.

Redirectmatch 403 macromates\.

RedirectMatch 403 head\_auth\.

RedirectMatch 403 submit\_links\.

RedirectMatch 403 change\_action\.

Redirectmatch 403 com\_facileforms\/

RedirectMatch 403 admin\_db\_utilities\.

RedirectMatch 403 admin\.webring\.docs\.

Redirectmatch 403 Table\/Latest\/index\.

</IfModule>

On passe ensuite à l’installation des plugins de sécurité suivants:

Le premier (Secure WordPress) permet d’effectuer simplement et automatiquement une liste d’actions essentielle à la sécurité de votre site:

Le second (WP Security Scan) effectue régulièrement un scan de l’arborescence de votre site et remonte dans l’interface d’administration de WordPress les fichiers modifiés.

Optimisation de WordPress

Pour l’optimisation des performance du blog, je me base sur l’indispensable plugin W3 Total Cache. Bien qu’il ne se limite pas qu’a cela, il permet de mettre facilement en place un système de cache ou les pages sont directement gérées en mémoire RAM plutôt qu’en lecture sur le disque dur. J’ai choisi de coupler W3 Total Cache avec Memcached (un cache mémoire sous licence libre).

L’installation de Memcached est des plus simple:

sudo aptitude install libcache-memcached-perl memcached

Si votre serveur est protégé par un Firewall (ce que je conseille évidemment…), il ne faut pas oublier d’ajouter la règle suivante:

iptables -A OUTPUT -p tcp –destination 127.0.0.1 –dport 11211 -j ACCEPT

La configuration de W3 Total Cache se fait via l’onglet Performance / General settings:

Une dernière optimisation consiste à installer le plugin WP-DBManager et de suivre régulièrement (tout les mois environ) les procédures d’optimisation (onglet Database).

Test et validation

Pour tester les performances de votre serveur, vous pouvez utiliser la commande suivante:

ab -t 30 -c 5 http://votreblog.com/

L’information intéressante se trouve sur la ligne:

Requests per second:    159.46 [#/sec] (mean)

Il est également important de surveiller les paramètres suivant: charge CPU, mémoire RAM disponible, accès disque et réseau. Gandi propose pour cela une interface d’administration Web avec un système de sonde permettant de remonter des alertes par mail ou même d’augmenter dynamiquement le nombre de part lors d’un pic de trafic.

Personnellement, je surveille mon serveur via Nagios mais cela sera le sujet d’un prochain billet…