Configurer son serveur web

Un petit article sans prétentions pour l’installation d’un serveur web maison.

Les accès système se feront par SSH, un protocole sûr et reconnu qui équipe nativement les serveurs sous Linux.

Le mieux est la connexion via SSH + clé d’identification (plus robuste qu’un simple mot de passe). [Ajouter une clé SSH pour root]

Générer des mots de passes fort

Pour générer un mot de passe fort (ajustez le 32 pour la longueur de la chaine)

openssl rand -base64 32

Mettre à jour le système

Pour mettre à jour le système d’exploitation ET les logiciels installés

apt update && apt -y upgrade && apt -y autoremove

Le must est de faire une mise à jour hebdomadaire du système (pour limiter les failles de sécurité). On créé donc un fichier update-system.sh

On va créer un dossier pour stocker nos scripts pour les mises à jour

mkdir /root/update/
nano /root/update/update-system.sh
#!/bin/bash
apt update && apt -y upgrade && apt -y autoremove

On rend le script exécutable et on créé un lien pour une exécution hebdomadaire

chmod +x /root/update/update-system.sh
ln -s /root/update/update-system.sh /etc/cron.weekly/update-system

Installation de Fail2Ban

Fail2Ban va nous permettre de bloquer durant un certain temps les personnes qui tentent de s’infiltrer dans le serveur.

Cela ne résous pas tous les problèmes, mais cela peut limiter les tentatives. Il faut penser à regarder les journaux système afin éventuellement d’être plus sévère sur le temps de banissement.

apt -y install fail2ban

On rajoute une sécurisation sur le protocole SSH (pensez à modifier le 1234 en avec le port personnalisé utilisé)

nano /etc/fail2ban/jail.d/ssh.conf
[ssh]
enabled = true
port = ssh,sftp,1234
filter = sshd
logpath = /var/log/auth.log
maxretry = 4
bantime = 86400

On recharge le processus et on contrôle le bon fonctionnement

fail2ban-client reload
fail2ban-client status

Gérer les sauvegardes !

La gestion des sauvegardes est primordial pour un site web, surtout en cas de crash. Il est possible de remettre en route un nouveau serveur très rapidement, mais la restauration des données peut être problématique s’il n’y a pas de sauvegardes…

Dans une simple mesure, il faut externaliser les sauvegardes en dehors du serveur, sur un hébergement tiers :

  • Scaleway (S3 Buckets, C14, FTP pour Dedibox…)
  • OVH (Block Storage…)
  • Etc etc

Le backup doit se faire à intervalle régulier (2x par jour par exemple) et comporter tout ce qui est nécessaire au fonctionnement rapide du site (fichiers, base de données, configuration, certificats SSL…)

Je n’utilise pas le backup de type image disque, car je veux rapidement pouvoir revenir dans un dossier, sans avoir à remonter toute l’image du backup.

On peut donc découper notre script de backup en 2 parties :

  • Sauvegarde de la base de données (un export avec mysqldump)
  • Sauvegarder les fichiers (une simple copie peut suffire)

Installation de duplicity

duplicity quand à lui permet de faire des backups par incrément, ce qui limite la quantité de donnée transférée. Il est aussi possible de chiffrer les backups.

Pour l’installer (la partie python3-boto est pour l’envoi sur des serveurs S3)

apt -y install duplicity python3-boto

On créé le dossier qui va contenir les scripts

mkdir /root/backup

On créé le fichier qui va exécuter le backup (et envoyer les fichiers sur un bucket)

nano /root/backup/duplicity.sh
#!/bin/bash

# On récupère les paramètres spécifiques pour le backup
source /root/backup/.source

# On donne quelques informations
echo "Backup ${SITE}"

# On supprime les vielles versions
duplicity \
	remove-older-than ${KEEP_BACKUP_TIME} \
	--force \
	${S3_BUCKET}

# On déploie les nouvelles version
duplicity \
	incr \
	--exclude "${SOURCE}/logs" \
	--full-if-older-than ${FULL_BACKUP_TIME} \
	--no-encryption \
	${SOURCE} \
	${S3_BUCKET}

On rend le script exécutable

chmod +x /root/backup/duplicity.sh

Maintenant on va s’occuper du script principal : celui qui va faire les exports des bases puis exécuter le script de transfert

nano /root/backup/main.py
#!/usr/bin/env python3

import os
import sys

# Quelques configurations
# Mot de passe root pour MySQL pour faire les backup des bases
MYSQL_ROOT = "P@$$w0rd"

# Répertoires où sont les sites
SITES_PATH = "/opt/sites/"

# Identifiants pour le backup bucket S3
S3_HOST = "s3://s3.fr-par.scw.cloud/__bucketName__"
S3_ACCESS_ID = "__ACCESS-ID__"
S3_ACCESS_KEY = "__ACCESS-KEY__"

# Faire un backup SQL pour le site en question
def backupSQL(site):
  # Supprimer les caractères interdits
  base = site
  base = (base).replace(".", "_")
  base = (base).replace("-", "_")

  # On indique quel site va être traité
  print("SQL Base = " + base)

  # Contrôle que le répertoire de destination existe
  backPath = SITES_PATH + site + "/backup"

  try:
    os.makedirs(backPath)
  except:
    pass

  # La commande SQL de dump
  backCmd = "mysqldump -u root -p" + MYSQL_ROOT + " " + base + " | gzip -9 -c > " + backPath + "/export.sql.gz"

  try:
    if os.path.exists("/var/lib/mysql/" + base):
      os.system(backCmd)
  except:
    pass

def writeSource(site):

  with open("/root/backup/.source", "w") as config:
    config.write('export SITE="' + site + '"' + "\n")
    config.write('export AWS_ACCESS_KEY_ID="' + S3_ACCESS_ID + '"' + "\n")
    config.write('export AWS_SECRET_ACCESS_KEY="' + S3_ACCESS_KEY + '"' + "\n")
    config.write('export S3_BUCKET="' + S3_HOST + site + '"' + "\n")
    config.write('export SOURCE="' + SITES_PATH + site + '"' + "\n")
    config.write('export KEEP_BACKUP_TIME="1D"' + "\n")
    config.write('export FULL_BACKUP_TIME="1D"' + "\n")

# Faire une copie des fichiers entre le serveur et l'espace de sokage distant
def backupDuplicity():
  os.system("/root/backup/duplicity.sh")

#####
#
#  MAIN
#
#####

for site in os.listdir(SITES_PATH):
  if os.path.isdir(os.path.join(SITES_PATH, site)):
    print("Backup %s" % site)    
    backupSQL(site)
    writeSource(site)
    backupDuplicity()

On rend le script exécutable et on s’assure de l’exécution chaque jour

chmod +x /root/backup/main.py
ln -s /root/backup/main.py /etc/cron.daily/backup

Installation de MariaDB

On va maintenant installer le programme pour la gestion de base de données

apt -y install mariadb-server mariadb-client

Paramétrage

On va supprimer les utilisateurs qui ne servent à rien ET donner un mot de passe root en local uniquement.

mysql_secure_installation

On va modifier la façon dont notre utilsateur root peut se connecter

echo "UPDATE user SET plugin='' WHERE User='root'; FLUSH PRIVILEGES;" | mysql -u root -p mysql

Installation de Certbot

Pour faire des certificats SSL

apt -y install certbot

Installation de Apache + PHP

On installe Apache et PHP pour l’exécution des scripts.

apt -y install apache2 php libapache2-mod-php php-mysql

On installe également des extensions

apt -y install php-gd php-xml php-zip php-curl php-mbstring php-imagick

Après chaque modification sur Apache, penser à redémarrer le service Apache :

systemctl restart apache2.service

Paramétrage PHP :

Attention à la version (ici 7.4), le chemin peut changer.
Pour savoir quelle version vous avez sur votre serveur

ls /etc/php/

sed -i 's/post_max_size = .*/post_max_size = 128M/' /etc/php/7.4/apache2/php.ini
sed -i 's/upload_max_filesize = .*/upload_max_filesize = 128M/' /etc/php/7.4/apache2/php.ini
sed -i 's/expose_php = .*/expose_php = Off/' /etc/php/7.4/apache2/php.ini

Paramétrage Apache

sed -i 's/ServerSignature .*/ServerSignature Off/' /etc/apache2/conf-enabled/security.conf
sed -i 's/ServerTokens .*/ServerTokens Prod/' /etc/apache2/conf-enabled/security.conf

On active quelques modules pour Apache

a2enmod ssl headers rewrite proxy proxy_http

On supprime les fichiers par défaut et l’on créé un fichier vide à la place.

rm /var/www/html/index.html
touch /var/www/html/index.php

On modifie le site par défaut de Apache

nano /etc/apache2/sites-enabled/000-default.conf
<VirtualHost *:80>
  DocumentRoot /var/www/html
  <Directory /var/www/html>
    DefaultType application/x-httpd-php
    DirectoryIndex index.php
    AddType application/x-httpd-php .php
    Require all granted
    Options +FollowSymLinks +MultiViews
  </Directory>
</VirtualHost>

Installation de Adminer

Pour la gestion des bases de données, beaucoup plus léger que phpMyAdmin

wget "http://www.adminer.org/latest-mysql-en.php" -O /var/www/html/adminer.php

On peut optimiser la mise à jour de Adminer

nano /root/update/update-adminer.sh
#!/bin/bash
wget "http://www.adminer.org/latest-mysql-en.php" -O /var/www/html/adminer.php

On rend le script exécutable et on créé un lien pour une exécution hebdomadaire

chmod +x /root/update/update-adminer.sh
ln -s /root/update/update-adminer.sh /etc/cron.weekly/update-adminer

Ajout des différents sites

On part sur une configuration chaque site web aura sa propre arborescence

  • « backup » pour le backup de la base de données
  • « conf » qui vont contenir les fichiers de configuration
  • « logs » qui vont contenir les logs
  • « public » pour les pages qui seront affichées sur internet

On imagine que le site à configurer est « mon-site.com »

mkdir -p /opt/sites/mon-site.com/{backup,certificates,conf,logs,public}

Pensez à redonner les droits à l’utilisateur Apache sur ces répertoires

chown -R www-data:www-data /opt/sites/mon-site.com

On va donc rajouter un fichier de configuration pour Apache (prendre un fichier par site, ce qui limitera les problèmes). Attention au fait que le fichier doit finir par « .conf »

nano /etc/apache2/sites-enabled/zzz.mon-site.com.conf

Ou on rajoute un lien direct

ln -s /opt/sites/mon-site.com/conf/apache.conf /etc/apache2/sites-enabled/zzz.mon-site.com.conf

Configuration sans certificat (http standard)

Define vHost mon-site.com
<VirtualHost *:80>
  ServerName ${vHost}
  ServerAlias *.${vHost}
  DocumentRoot /opt/sites/${vHost}/public
  <Directory /opt/sites/${vHost}/public>
    DefaultType application/x-httpd-php
    DirectoryIndex index.php
    AddType application/x-httpd-php .php
    Require all granted
    AllowOverride All
    Options +FollowSymLinks +MultiViews
  </Directory>
  ErrorLog "|/usr/bin/rotatelogs -l -f   /opt/sites/${vHost}/logs/apache-error.%Y.%m.%d.log 86400"
  CustomLog "|/usr/bin/rotatelogs -l -f /opt/sites/${vHost}/logs/apache-access.%Y.%m.%d.log 86400" common
  LogLevel warn
</VirtualHost>
UnDefine vHost

Configuration avec certificat (https)

Define vHost mon-site.com

<VirtualHost *:80

  ServerName ${vHost}
  ServerAlias *.${vHost}

  Redirect permanent / https://${vHost}/

</VirtualHost>

<VirtualHost *:443>

  ServerName ${vHost}
  ServerAlias *.${vHost}

  DocumentRoot /opt/sites/${vHost}/public

  <Directory /opt/sites/${vHost}/public>

    DefaultType application/x-httpd-php
    DirectoryIndex index.php
    AddType application/x-httpd-php .php

    Require all granted
    AllowOverride All

    Options +FollowSymLinks +MultiViews

  </Directory>

  ErrorLog  "|/usr/bin/rotatelogs -l -f  /opt/sites/${vHost}/logs/apache-error.%Y.%m.%d.log 86400"
  CustomLog "|/usr/bin/rotatelogs -l -f /opt/sites/${vHost}/logs/apache-access.%Y.%m.%d.log 86400" common
  LogLevel warn

  SSLEngine on
  SSLCertificateFile      /etc/letsencrypt/live/${vHost}/cert.pem
  SSLCertificateKeyFile   /etc/letsencrypt/live/${vHost}/privkey.pem
  SSLCertificateChainFile /etc/letsencrypt/live/${vHost}/fullchain.pem
  SSLProtocol all -TLSv1.1 -TLSv1 -SSLv2 -SSLv3
  SSLHonorCipherOrder on
  SSLCompression off
  SSLOptions +StrictRequire
  SSLCipherSuite ALL:+HIGH:!ADH:!EXP:!SSLv2:!SSLv3:!MEDIUM:!LOW:!NULL:!aNULL

  Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"

</VirtualHost>

UnDefine vHost

Ajouter un utilisateur pour la base de données. Privilégiez 1 utilisateur par base de données avec un mot de passe fort.

  • dbName est le nom de la base de données
  • dbUser est le nom de l’utilisateur
  • dbPassword est le mot de passe pour cet utilisateur
CREATE DATABASE dbName;
GRANT ALL ON dbName.* TO 'dbUser'@'localhost' IDENTIFIED BY 'dbPassword';