Se construire un serveur de tuile OpenStreetMap

Le résultat final

Après un discution avec Jean-Yvon Landrac (Orolia SAS) au State of the Map France 2017, à Avignon, j’ai eu envie d’implémenter mon propre serveur de cartographie. Comme je ne suis ni un debianeux ni un ubuntiste, le défi est de l’installer sur CentOS 7 ou Fedora.

Pour construire mon propre serveur de tuiles OpenStreetMap (OSM), l’utilisation de RPM facilite grandement la tâche.

Il sera basé sur:

  • postgresql avec les extensions postgis et hstore qui hébergeront les données OSM
  • Apache et mod_tile pour servir les tuiles au navigateur
  • renderd, le chef d’orchestre du rendu
  • mapnik, la bibliothèque qui construira les images en fonction des données stockées en base.
  • osm-carto, le style officiel d’OSM
  • carto le compilateur de feuille de style
  • osmctools qui manipulera les données OSM (notamment pour fusionner deux extraits de données OSM
  • osm2pgsql qui intégrera en base les extraits de données OSM

Architecture d’un serveur de tuiles

Il faudra prévoir quand même pas mal d’espace disque: la taille de la base de données sera de 500Go environ pour l’Europe, 85Go environ pour la France entière, ou de 11Go pour les deux seules régions utilisées.

Le VPS utilisé ici possède 16Go de RAM et 2 vCPUs.

Installation

Seulement sur CentOS 7

  • Beaucoup de paquets utilisés proviennent du dépôt epel, on commence donc par s’assurer qu’il est installé.
    sudo yum install epel-release
  • Les paquets RPMs contenant les outils OSM proviennent de mes dépôts COPR, on va donc créer le fichier de dépôt /etc/yum.repos.d/tartare.repo
    sudo cat << eOF > /etc/yum.repos.d/tartare.repo
    [tartare-mapnik]
    name=Copr repo for mapnik owned by tartare
    baseurl=https://copr-be.cloud.fedoraproject.org/results/tartare/mapnik/epel-7-$basearch/
    type=rpm-md
    skip_if_unavailable=True
    gpgcheck=1
    gpgkey=https://copr-be.cloud.fedoraproject.org/results/tartare/mapnik/pubkey.gpg
    repo_gpgcheck=0
    enabled=1
    enabled_metadata=1
    
    [tartare-python-mapnik]
    name=Copr repo for python-mapnik owned by tartare
    baseurl=https://copr-be.cloud.fedoraproject.org/results/tartare/python-mapnik/epel-7-$basearch/
    type=rpm-md
    skip_if_unavailable=True
    gpgcheck=1
    gpgkey=https://copr-be.cloud.fedoraproject.org/results/tartare/python-mapnik/pubkey.gpg
    repo_gpgcheck=0
    enabled=1
    enabled_metadata=1
    
    [tartare-mod_tile]
    name=Copr repo for mod_tile owned by tartare
    baseurl=https://copr-be.cloud.fedoraproject.org/results/tartare/mod_tile/epel-7-$basearch/
    type=rpm-md
    skip_if_unavailable=True
    gpgcheck=1
    gpgkey=https://copr-be.cloud.fedoraproject.org/results/tartare/mod_tile/pubkey.gpg
    repo_gpgcheck=0
    enabled=1
    enabled_metadata=1
    
    [tartare-osmosis-bin]
    name=Copr repo for osmosis-bin owned by tartare
    baseurl=https://copr-be.cloud.fedoraproject.org/results/tartare/osmosis-bin/epel-7-$basearch/
    type=rpm-md
    skip_if_unavailable=True
    gpgcheck=1
    gpgkey=https://copr-be.cloud.fedoraproject.org/results/tartare/osmosis-bin/pubkey.gpg
    repo_gpgcheck=0
    enabled=1
    enabled_metadata=1
    
    [tartare-osmctools]
    name=Copr repo for osmctools owned by tartare
    baseurl=https://copr-be.cloud.fedoraproject.org/results/tartare/osmctools/epel-7-$basearch/
    type=rpm-md
    skip_if_unavailable=True
    gpgcheck=1
    gpgkey=https://copr-be.cloud.fedoraproject.org/results/tartare/osmctools/pubkey.gpg
    repo_gpgcheck=0
    enabled=1
    enabled_metadata=1
    
    [tartare-openstreetmap-carto]
    name=Copr repo for openstreetmap-carto owned by tartare
    baseurl=https://copr-be.cloud.fedoraproject.org/results/tartare/openstreetmap-carto/epel-7-$basearch/
    type=rpm-md
    skip_if_unavailable=True
    gpgcheck=1
    gpgkey=https://copr-be.cloud.fedoraproject.org/results/tartare/openstreetmap-carto/pubkey.gpg
    repo_gpgcheck=0
    enabled=1
    enabled_metadata=1
    EOF
    

Seulement sur Fedora

    • On installe mes dépôts COPR
sudo dnf copr enable tartare/mod_tile 
sudo dnf copr enable tartare/osmosis-bin
sudo dnf copr enable tartare/openstreetmap-carto 

Pour les deux distributions
Si on souhaite faire l’installation dans des partitions séparées, il faut préparer le terrain. La partition /var/lib/pgsql sera, de préférence, sur un disque SSD.

Partition 2 régions France Europe
/var/lib/mod_tile 50Go
/var/lib/openstreetmap-carto 3Go
/var/lib/pgsql 20Go 100Go 500Go
  1. Création des 3 partitions et formatage en xfs (ou en ext4)
  2. création des points de montages
    sudo mkdir /var/lib/pgsql /var/lib/mod_tile /var/lib/openstreetmap-carto
  3. Modification de la table des partitions en éditant le fichier /etc/fstab
  4. Montage de nos partions
    sudo mount -a

On installe la base de données postgresql avec l’extension postgis

sudo yum -y install postgresql-server postgresql-contrib postgis postgis-utils

Puis les outils OSM et quelques dépendances

sudo yum install mapnik mapnik-utils python2-mapnik mod_tile osm2pgsql \
                 osmctools openstreetmap-carto PyYAML
sudo yum install npm wget git unzip java

On installe maintenant l’utilitaire carto, via npm

sudo npm -g install carto

Le paquet mod_tile créé un utilisateur système osm mais celui-ci n’a pas de shell (compte système = shell /sbin/nologin). Par flemme simplicité, on lui attribue un shell et on lui ajoute le groupe wheel, et on lui affecte un mot de passe, le temps de l’installation seulement.

usermod -s /bin/bash osm
usermod -aG wheel osm
passwd osm

On créé le répertoire volatile d’accueil de la socket renderd

sudo systemd-tmpfiles --create

Initialisation de la base de données

L’initialisation de postgresql est triviale

sudo postgresql-setup initdb
sudo systemctl enable postgresql httpd
sudo systemctl start postgresql httpd

On va maintenant configurer finement notre base de données en modifiant le fichier /var/lib/pgsql/data/postgresql.conf:

  • shared_buffers = 128MB
  • maintenance_work_mem = 256MB

De manière temporaire, afin d’optimiser le temps d’import de notre fichier français, on peut aussi modifier ces valeurs. Toutefois ces valeurs doivent être remis à on une fois l’import terminé.

  • autovacuum = off

On applique les changements

sudo systemctl restart postgresql

On initialise la base de données

sudo -u postgres createuser -s osm
sudo -u postgres createuser apache
sudo -u postgres createdb -E UTF8 -O osm gis
sudo -u postgres psql
\c gis
CREATE EXTENSION postgis;
CREATE EXTENSION hstore;
ALTER TABLE geometry_columns OWNER TO osm;
ALTER TABLE spatial_ref_sys OWNER TO osm;
\q

Si la création de la base échoue avec l’erreur (ref http://stackoverflow.com)
createdb: database creation failed: ERROR: new encoding (UTF8) is incompatible with the encoding of the template database (SQL_ASCII)

sudo -u postgres psql 
psql (9.2.18)
Type "help" for help.

postgres=# UPDATE pg_database SET datistemplate = FALSE WHERE datname = 'template1';
UPDATE 1
postgres=# DROP DATABASE template1;
DROP DATABASE
postgres=# CREATE DATABASE template1 WITH TEMPLATE = template0 ENCODING = 'UNICODE';
CREATE DATABASE
postgres=# UPDATE pg_database SET datistemplate = TRUE WHERE datname = 'template1';
UPDATE 1
postgres=# \c template1
You are now connected to database "template1" as user "postgres".
template1=# VACUUM FREEZE;
VACUUM
template1=# \q

Style osm-carto

A partir d’ici, nous utiliserons le compte utilisateur osm exclusivement (pour toutes les commandes du reste de ce tutorial).

sudo su - osm

Pour pouvoir générer des tuiles, il faudra utiliser une feuille de style (un peu comme le css des pages web). Le style par défaut d’OSM est utilisé.

Il faut environ 2Go de libre dans /var pour les fichiers shapefile

cd /usr/share/openstreetmap-carto/
scripts/get-shapefiles.py
sudo ln -s /var/lib/openstreetmap-carto/style.xml /usr/share/openstreetmap-carto/style.xml
carto -a "3.0.0" project.mml > style.xml

On modifie le fichier /etc/renderd.conf pour spécifier notre feuille de style

[default]
...
XML=/usr/share/openstreetmap-carto/style.xml

Import des données

Pour complexifier un peu la tâche, on souhaite avoir deux régions (c’est cool d’être limitrophe):

  • PACA
  • Languedoc-Roussillon

osmconvert fera le travail de fusion.

On créé le répertoire d’accueil de nos extraits de planet et on s’assure que la partition est assez grande pour contenir le(s) fichier(s)

sudo mkdir /home/osm
sudo chown osm:osm /home/osm
cd /home/osm

On télécharge nos deux extraits de planet, en prenant soin de télécharger d’abord le checksum md5 et le fichier state.txt pour avoir le timestamp de l’extrait. Ce timestamp sera utilisé par l’utilitaire osmosis lors de la mise à jour de la base de données. Par sécurité, on applique le timestamp du site geofabrik sur nos extraits. On aura un backup gratuit d’une information essentielle au processus de mise à jour.

cd /home/osm
mkdir provence-alpes-cote-d-azur
pushd provence-alpes-cote-d-azur
wget http://download.geofabrik.de/europe/france/provence-alpes-cote-d-azur-latest.osm.pbf.md5
wget http://download.geofabrik.de/europe/france/provence-alpes-cote-d-azur-updates/state.txt
wget http://download.geofabrik.de/europe/france/provence-alpes-cote-d-azur-latest.osm.pbf
popd

mkdir languedoc-roussillon
pushd languedoc-roussillon
wget http://download.geofabrik.de/europe/france/languedoc-roussillon-latest.osm.pbf.md5
wget http://download.geofabrik.de/europe/france/languedoc-roussillon-updates/state.txt
wget http://download.geofabrik.de/europe/france/languedoc-roussillon-latest.osm.pbf
popd

On les vérifie

pushd /home/osm/provence-alpes-cote-d-azur
md5sum -c provence-alpes-cote-d-azur-latest.osm.pbf.md5
popd
pushd /home/osm/languedoc-roussillon
md5sum -c languedoc-roussillon-latest.osm.pbf.md5
popd

et on les fusionne

osmconvert languedoc-roussillon/languedoc-roussillon-latest.osm.pbf --out-o5m | osmconvert - provence-alpes-cote-d-azur/provence-alpes-cote-d-azur-latest.osm.pbf -o=sud-est.osm.pbf

Nous allons utiliser la commande osm2pgsql pour importer les données

  • La valeur du paramètre C est d’environ 2/3 de la RAM
  • l’option –slim est obligatoire si des mises à jour sont prévues
  • on spécifie notre feuille de style avec l’option –style
  • on spécifie le nom de notre base de données avec l’option -d
  • on spécifie le nombre de tâches à paralléliser (lorsque c’est possible), à adapter en fonction du nombre de CPU disponible.
  • on importe notre fusion d’extrait du planet

Il est préférable de lancer cette commande avec nohup, ce qui évitera d’arrêter le processus en cas de déconnexion intempestive.

osm2pgsql --slim --hstore --style /usr/share/openstreetmap-carto/openstreetmap-carto.style --tag-transform-script /usr/share/openstreetmap-carto/openstreetmap-carto.lua -d gis -C 10000 --number-processes 2 /home/osm/sud-est.osm.pbf

osm2pgsql version 0.92.0 (64 bit id space)

Using lua based tag processing pipeline with script /usr/share/openstreetmap-carto/openstreetmap-carto.lua
Using projection SRS 3857 (Spherical Mercator)
Setting up table: planet_osm_point
Setting up table: planet_osm_line
Setting up table: planet_osm_polygon
Setting up table: planet_osm_roads
Allocating memory for dense node cache
Allocating dense node cache in one big chunk
Allocating memory for sparse node cache
Sharing dense sparse
Node-cache: cache=10000MB, maxblocks=160000*65536, allocation method=11
Mid: pgsql, scale=100 cache=10000
Setting up table: planet_osm_nodes
Setting up table: planet_osm_ways
Setting up table: planet_osm_rels

Reading in file: /home/osm/sud-est.osm.pbf
Using PBF parser.
Processing: Node(46593k 6.4k/s) Way(6822k 1.91k/s) Relation(39430 79.66/s)  parse time: 11438s
Node stats: total(46593329), max(4919729897) in 7336s
Way stats: total(6822494), max(501069714) in 3577s
Relation stats: total(39438), max(7336004) in 495s
Committing transaction for planet_osm_point
Committing transaction for planet_osm_line
Committing transaction for planet_osm_polygon
Committing transaction for planet_osm_roads
Setting up table: planet_osm_nodes
Setting up table: planet_osm_ways
Setting up table: planet_osm_rels
Using lua based tag processing pipeline with script /usr/share/openstreetmap-carto/openstreetmap-carto.lua
Setting up table: planet_osm_nodes
Setting up table: planet_osm_ways
Setting up table: planet_osm_rels
Using lua based tag processing pipeline with script /usr/share/openstreetmap-carto/openstreetmap-carto.lua

Going over pending ways...
        5962327 ways are pending

Using 2 helper-processes
Finished processing 5962327 ways in 6957 s

5962327 Pending ways took 6957s at a rate of 857.03/s
Committing transaction for planet_osm_point
Committing transaction for planet_osm_line
Committing transaction for planet_osm_polygon
Committing transaction for planet_osm_roads
Committing transaction for planet_osm_point
Committing transaction for planet_osm_line
Committing transaction for planet_osm_polygon
Committing transaction for planet_osm_roads

Going over pending relations...
        0 relations are pending

Using 2 helper-processes
Finished processing 0 relations in 0 s

Committing transaction for planet_osm_point
WARNING:  there is no transaction in progress
Committing transaction for planet_osm_line
WARNING:  there is no transaction in progress
Committing transaction for planet_osm_polygon
WARNING:  there is no transaction in progress
Committing transaction for planet_osm_roads
WARNING:  there is no transaction in progress
Committing transaction for planet_osm_point
WARNING:  there is no transaction in progress
Committing transaction for planet_osm_line
WARNING:  there is no transaction in progress
Committing transaction for planet_osm_polygon
WARNING:  there is no transaction in progress
Committing transaction for planet_osm_roads
WARNING:  there is no transaction in progress
Sorting data and creating indexes for planet_osm_roads
Sorting data and creating indexes for planet_osm_polygon
Sorting data and creating indexes for planet_osm_line
Sorting data and creating indexes for planet_osm_point
Copying planet_osm_roads to cluster by geometry finished
Creating geometry index on planet_osm_roads
Creating osm_id index on planet_osm_roads
Creating indexes on planet_osm_roads finished
All indexes on planet_osm_roads created in 476s
Completed planet_osm_roads
Copying planet_osm_point to cluster by geometry finished
Creating geometry index on planet_osm_point
Copying planet_osm_line to cluster by geometry finished
Creating geometry index on planet_osm_line
Creating osm_id index on planet_osm_point
Creating indexes on planet_osm_point finished
All indexes on planet_osm_point created in 1624s
Completed planet_osm_point
Creating osm_id index on planet_osm_line
Creating indexes on planet_osm_line finished
All indexes on planet_osm_line created in 2291s
Completed planet_osm_line
Copying planet_osm_polygon to cluster by geometry finished
Creating geometry index on planet_osm_polygon
Creating osm_id index on planet_osm_polygon
Creating indexes on planet_osm_polygon finished
All indexes on planet_osm_polygon created in 5913s
Completed planet_osm_polygon
Stopping table: planet_osm_nodes
Stopped table: planet_osm_nodes in 0s
Stopping table: planet_osm_ways
Building index on table: planet_osm_ways
Stopped table: planet_osm_ways in 10042s
Stopping table: planet_osm_rels
Building index on table: planet_osm_rels
Stopped table: planet_osm_rels in 97s
node cache: stored: 46593329(100.00%), storage efficiency: 52.06% (dense blocks: 1698, sparse nodes: 37792830), hit rate: 100.00%

Osm2pgsql took 34458s overall

Durée environ 9 heures et 35 minutes

On génère les index

cd /usr/share/openstreetmap-carto/
scripts/indexes.py | psql -d gis

Le chef d’orchestre du rendu: renderd

On vérifie d’abord la configuration: fichier /etc/renderd.conf

[renderd]
num_threads=2

[mapnik]
plugins_dir=/usr/lib64/mapnik/input
font_dir=/usr/share/fonts

[default]
XML=/usr/share/openstreetmap-carto/style.xml

On adapte le paramètre num_threads de la section [renderd] pour refléter le nombre de vCPUs.

Puis on tente de lancer le démon à la main, avec l’utilisateur osm, afin de vérifier que tout fonctionne comme attendu.

/usr/sbin/renderd -f -c /etc/renderd.conf

On essaie de télécharger une tuile pour vérifier que tout fonctionne: localhost/mod_tiles/0/0/0.png

Si tout est correct, on obtient cette image.

On arrête notre instance de renderd (CTRL+C) et on lance le démon

sudo systemctl start renderd
sudo systemctl enable renderd

Pré-génération des tuiles (optionnel)

Afin d’avoir une bonne réactivité de la carte, il est nécessaire de pré-générer la génération des tuiles. Un niveau de zoom 10 devrait suffir, les niveaux supérieurs seront générer à la demande. Bien évidemment, ça aussi, ça prend énormement du temps.

Le démon renderd doit être démarré et opérationnel
Il est préférable de lancer cette commande avec nohup

render_list -a -n 2 -Z 10

Zoom 01: min: 25.0 avg: 25.0 max: 25.0  over a total of    25.0s in    1 requests
Zoom 02: min: 36.2 avg: 36.2 max: 36.2  over a total of    36.2s in    1 requests
Zoom 03: min: 42.1 avg: 42.1 max: 42.1  over a total of    42.1s in    1 requests
Zoom 04: min:  3.4 avg: 47.9 max: 92.4  over a total of   191.6s in    4 requests
Zoom 05: min:  2.5 avg: 11.8 max: 84.1  over a total of   189.6s in   16 requests
Zoom 06: min:  1.6 avg:  6.7 max: 129.3 over a total of   426.7s in   64 requests
Zoom 07: min:  1.8 avg:  4.0 max: 222.0 over a total of  1025.7s in  256 requests
Zoom 08: min:  1.7 avg:  3.2 max: 12.4  over a total of  3313.9s in 1023 requests
Zoom 09: min:  1.0 avg:  3.7 max: 193.0 over a total of 15224.3s in 4090 requests
Zoom 10: min:  0.9 avg:  3.0 max: 123.8 over a total of 49596.2s in 16373 requests

La page web magique

Si l’installation a été faite en RPM

sudo cp /usr/share/doc/mod_tile-0.5/slippymap.html /var/www/html/index.html

Sinon, la page web est à copier depuis le répertoire des sources de mod_tile (/home/osm dans ce tutorial)

Le rendu est visible sur map.tartarefr.eu
On modifie quelques variables:

  • var lat Mettre la lattitude correspondante au centre des extraits de planet utilisés (43.81 pour mes extraits de planet)
  • var lon Mettre la longitude correspondante au centre des extraits de planet utilisés (4.64 pour mes extraits de planet)
  • var newLayer On modifie l’URL car par défaut, il faut ajouter l’alias /osm_tiles pour pouvoir être servi en tuile par notre serveur (/osm_tiles/${z}/${x}/${y}.png)

On peut aussi rajouter la ligne avant la fonction init(), afin d’éviter les tuiles roses pales (tuile par défaut correspondant à un fichier non trouvé). Le javascript du navigateur fera 5 tentatives avant de déclarer que le fichier n’existe pas.

OpenLayers.IMAGE_RELOAD_ATTEMPTS = 5;

Mise à jour de la base Postgis

L’utilitaire osmosis s’occupera de télécharger les mises à jour et osm2pgsql les intégrera en base. Toujours avec l’utilisateur osm, on initialisera le processus et on le lancera de manière périodique (tâche cron)

  • On initialise la mise à jour pour le premier extrait de planet (l’ordre de traitement des extraits importe peu)
    export WORKDIR_OSM=/var/lib/mod_tile/.osmosis/provence-alpes-cote-d-azur
    mkdir -p ${WORKDIR_OSM}
    cd ${WORKDIR_OSM}
    osmosis --read-replication-interval-init workingDirectory=${WORKDIR_OSM}

    Même chose pour le deuxième

    export WORKDIR_OSM=/var/lib/mod_tile/.osmosis/languedoc-roussillon
    mkdir -p ${WORKDIR_OSM}
    cd ${WORKDIR_OSM}
    osmosis --read-replication-interval-init workingDirectory=${WORKDIR_OSM}
  • On modifie les URL de mise à jour (on utilise geofabrik)
    sed -i -e "/^baseUrl/ s;=.*$;=http://download.geofabrik.de/europe/france/provence-alpes-cote-d-azur-updates;" /var/lib/mod_tile/.osmosis/provence-alpes-cote-d-azur/configuration.txt
    sed -i -e "/^baseUrl/ s;=.*$;=http://download.geofabrik.de/europe/france/languedoc-roussillon-updates;" /var/lib/mod_tile/.osmosis/languedoc-roussillon/configuration.txt
  • On copie le fichier state.txt pour qu’osmosis connaisse le point de départ de la mise à jour
    cp /home/osm/provence-alpes-cote-d-azur/state.txt /var/lib/mod_tile/.osmosis/provence-alpes-cote-d-azur/
    cp /home/osm/languedoc-roussillon/state.txt /var/lib/mod_tile/.osmosis/languedoc-roussillon/
    
  • On lance la première tâche de mise à jour manuellement pour s’assurer que tout fonctionne correctement
    export WORKDIR_OSM=/var/lib/mod_tile/.osmosis/provence-alpes-cote-d-azur
    cd ${WORKDIR_OSM}
    osmosis --read-replication-interval workingDirectory=${WORKDIR_OSM} \
            --write-xml-change /tmp/change.osm.gz
    osm2pgsql --append --slim --hstore \
              --style /usr/share/openstreetmap-carto/openstreetmap-carto.style \
              --tag-transform-script /usr/share/openstreetmap-carto/openstreetmap-carto.lua \
              -d gis -C 10000 --number-processes 2 \
              -e 10-20 -o /tmp/expire.list
              /tmp/change.osm.gz
    render_expired --min-zoom=10 --delete-from=15 < /tmp/expire.list
    rm -f /tmp/change.osm.gz
    rm -f /tmp/expire.list
    
    export WORKDIR_OSM=/var/lib/mod_tile/.osmosis/languedoc-roussillon
    cd ${WORKDIR_OSM}
    osmosis --read-replication-interval workingDirectory=${WORKDIR_OSM} \
            --write-xml-change /tmp/change.osm.gz
    osm2pgsql --append --slim --hstore \
              --style /usr/share/openstreetmap-carto/openstreetmap-carto.style \
              --tag-transform-script /usr/share/openstreetmap-carto/openstreetmap-carto.lua \
              -d gis -C 10000 --number-processes 2 \
              -e 10-20 -o /tmp/expire.list
              /tmp/change.osm.gz
    render_expired --min-zoom=10 --delete-from=15 < /tmp/expire.list
    rm -f /tmp/change.osm.gz
    rm -f /tmp/expire.list

Le lancement des commandes (script de mise à jour osm-update.sh) en tâche planifiée du dernier point est laissé en exercice, juste quelques conseils:

  • il faut absolument lancer les tâches planifiées avec l'utilisateur osm.
  • Je péfère écrire les fichiers temporaires dans /tmp, car c'est un système de fichier en RAM (plus rapide), mais on peut tout à fait l'écrire dans n'importe quel répertoire où l'utilisateur osm a des droits lecture/écriture
  • Le lancement des commandes ci-dessus ne met en base de données que des modifications d'une seule journée. Si plusieurs jours se sont écoulés depuis l'installation, il faudra lancer ces commandes autant de fois que de jours en retard pour rattraper le lag. Le plus simple est d'obtenir le numéro de la dernière mise à jour sur le site de geofabrik et de comparer ce chiffre à celui du fichier state.txt du répertoire de travail d'osmosis (qui est incrémenté de 1 à chaque commande de mise à jour. Si beaucoup de jour ce sont écoulés, il vaut mieux télécharger toutes les mises à jour et les fusionner dans un seule fichier (osmctools: osmconvert).
  • On peut tout à fait appliquer une mise à jour qui a déjà été intégrée. C'est même conseiller dans le cas d'utilisation des minutely changes officielles.
  • On peut utiliser un pipe entre les tâche osmosis et osm2pgsql afin d'éviter de passer par un fichier temporaire mais je profite de ce fichier temporaire pour faire la mise à jour de nominatim (hors sujet de l'installation d'un serveur de tuiles)
  • On régénère les tuiles qui ont été modifiées par la mise à jour si le zoom est compris entre 10 et 14 et on les supprime simplement si le zoom est supérieur ou égal à 15 (elles seront reconstruites à la demande)

Post installation

Maintenant que tout est installé, configuré et opérationnel, on va enlever le shell à l'utilisateur osm, le retirer du groupe wheel, lui affecter un mot de passe vide et on réactive l'autovacuum de postgresql. Ces commandes sont à lancer avec notre utilisateur normal (ayant servi à l'installation des RPMs).

sudo usermod -s /sbin/nologin osm
sudo passwd -d osm
sudo gpasswd -d osm wheel
sudo sed -i -e '/^autovacuum/ s/off/on/' /var/lib/pgsql/data/postgresql.conf
sudo systemctl restart postgresql

Références

Sécuriser son VPS centos/redhat/fedora en 10 étapes.

Pré-requis

Depuis notre poste client, on s’assure qu’une paire de clés (rsa c’est mieux que dsa) existe pour notre utilisateur courant. Celle-ci servira à se connecter à notre VPS sans mot de passe, mais de manière sécurisée. Si ce n’est pas le cas, on n’en créé une et on lui affecte un mot de passe.

ssh-keygen -t rsa

On se sert ensuite de l’agent SSH pour ne renseigner le mot de passe de notre clé privée qu’une seule fois, mais si on préfère le taper à chaque fois ….

ssh-add

Sécurisation

On peut maintenant s’occuper de notre VPS:

  1. Dès réception du mail de confirmation d’installation, on se connecte en SSH sur notre VPS, avec le login root et le mot de passe fourni.
    Cette session ne doit pas être fermée avant de pouvoir se connecter sans mot de passe (par clé donc) avec l’utilisateur qui sera créé (point 7).
  2. On modifie tout de suite le mot de passe root
    passwd
  3. On ajoute un utilisateur standard et on lui affecte un mot de passe
    useradd -m -s /bin/bash -c "<Nom> <Prenom>" <mon-user>
    passwd <mon-user>
  4. Depuis notre poste client, on autorise notre clé sur le serveur
    ssh-copy-id <mon-user>@<ip-de-mon-vps>
  5. On Modifie la configuration du service SSH (typiquement le fichier /etc/ssh/sshd_config):
    • ne plus autoriser le super-utilisateur root
      PermitRootLogin no
    • ne plus autoriser les connexions par mot de passe (n’accepter que les connexions par clé)
      PasswordAuthentication no
    • On redémarre le service ssh (fedora, centos7 ou rhel7)
      systemctl restart sshd

      ou sur les distributions ne prenant pas en charge systemd (centos6 ou rhel6)

      service sshd restart
  6. Dans un autre shell, on s’assure que notre utilisateur peut se connecter au VPS sans mot de passe
    ssh <mon-user>@<ip-de-mon-vps>
  7. Maintenant que la porte d’entrée a été changée, que notre VPS est bien accessible depuis notre poste client avec un utilisateur normal, on peut fermer le shell ouvert en début de procédure, mais cela reste optionnel
  8. On met à jour son système
    yum update
  9. On met en place un firewall
    Si le service firewalld est disponible, il est normalement déjà installé:

    1. On obtient la zone par défaut
      firewall-cmd --get-default-zone
      public
      

      Notre zone par défaut s’appelle public

    2. On vérifie que le service ssh est autorisé sur le firewall
      firewall-cmd --zone=public --list-services
      dhcpv6-client ssh
    3. Si le service ssh n’est pas dans la liste, on l’ajoute (de manière permanente) et on recharge le firewall
      firewall-cmd --zone=public --permanent --add-service=ssh
      firewall-cmd --reload

    Sinon, on utilise le service iptables

    1. Si firewalld est installé mais qu’il ne fonctionne pas (merci à l’hébergeur qui ne sait pas configurer la technologie proxmox correctement), on le supprime
      yum remove firewalld
    2. On installe le service iptables
      yum install iptables-services
    3. On le démarre et on l’active
      systemctl start iptables
      systemctl enable iptables

      Ou sur les distributions ne prenant pas en charge systemd (centos6 ou rhel6)

      service iptables start
      chkconfig iptables on
      
    4. On met en place une configuration minimale dans le fichier /etc/sysconfig/iptables: on autorise le trafic entrant sur la boucle locale (localhost), les pings (protocole icmp), ainsi que le trafic entrant sur les connexions déjà établies ( state RELATED,ESTABLISHED ) plus le trafic entrant sur le port 22 (ssh) et surtout on ignore tout le reste avec la politique par défaut à DROP. Par contre, on autorise le trafic sortant.
      # sample configuration for iptables service
      # you can edit this manually or use system-config-firewall
      # please do not ask us to add additional ports/services to this default configuration                                                                                             
      *filter 
      :INPUT DROP [0:0]
      :FORWARD DROP [0:0]
      :OUTPUT ACCEPT [0:0]
      -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
      -A INPUT -p icmp -j ACCEPT
      -A INPUT -i lo -j ACCEPT
      -A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT
      COMMIT
      
    5. On redémarre le service iptables
      systemctl restart iptables

      Ou sur les distributions ne prenant pas en charge systemd (centos6 ou rhel6)

      service iptables restart
  10. On met en place le service fail2ban sur le service ssh
    • Installation
      yum install fail2ban
    • On met en place une configuration minimale: on surcharge le fichier jail.conf dans le fichier jail.local pour bannir les gros lourds 48 heures directement, car il ne peut y avoir de mots de passe mal renseigné, vu qu’on accepte uniquement les connexions par clés.
      [DEFAULT]
      ignoreip = 127.0.0.1/8 <ip-de-mon-vps>
      bantime = 3600
      banaction = firewallcmd-ipset
      banaction_allports = firewallcmd-ipset
      backend = systemd
      sender = fail2ban@<mon-domaine>
      destemail = root
      action = %(action_mwl)s
      
      [sshd]
      enabled  = true
      maxretry = 5
      bantime  = 172800
      findtime = 172800
      

      ou si firewalld n’est pas disponible

      [DEFAULT]
      ignoreip = 127.0.0.1/8 <ip-de-mon-vps>
      bantime = 3600
      banaction = iptables-multiport
      banaction_allports = iptables-allports
      backend = systemd
      sender = fail2ban@<mon-domaine>
      destemail = root
      action = %(action_mwl)s
      
      [sshd]
      enabled  = true
      maxretry = 5
      bantime  = 172800
      findtime = 172800
      
    • On démarre et on active le service
      systemctl start fail2ban
      systemctl enable fail2ban

      Ou sur les distributions ne prenant pas en charge systemd (centos6 ou rhel6)

      service fail2ban start
      chkconfig fail2ban on
      

Voilà, le VPS est maintenant sécurisé et les premiers courriels de bannissement des gros relous ne devraient pas tarder…

WordPress et CSP: unsafe-inline et unsafe-eval (script-src)

Ou comment supprimer unsafe-inline et unsafe-eval de script-src dans l’en-tête HTTP Content-Security-Policy avec WordPress et obtenir un A+ sur securityheaders.io.

Déjà, autant prévenir, ça va être long et pénible… Cela se fera à coup de petite modification, test, re petite modification. Oui modification est au singulier, car on ne change qu’un paramètre à la fois entre chaque test.

WordPress seul est dans l’ensemble assez propre, dans le sens où il n’embarque que très peu de javascript inline. Il n’y a donc que quatre problèmes à résoudre

  1. Désactiver les emojis, avec un plugin pour ne pas toucher au code
  2. Choisir un thème n’utilisant pas de javascript inline
  3. N’utiliser ques des plugins sans javascript inline
  4. Ajouter une exception pour la partie admin

Ça parait simple dit comme ça, mais dans la pratique, le moindre plugin (surtout ceux de galerie d’images) peut venir violer la politique de sécurité du contenu (Content-Security-Policy). De plus, pour avoir les rapports de violation des CSP, il va falloir un script qui enregistre tout ça.

Prérequis

  • On s’écrit un petit script PHP perfectible qui enregistrera les violations. Vu le peu de sécurité offert par ce script, il est préférable de le durcir et de mutualiser l’enregistrement des rapports: Expect-Certificate-Transparency (ect) et Public-Key-Pins (pkp). Ou bien de simplment le supprimer une fois nos tests effectués.
    Fichier /usr/share/wordpress/report.php
    <?php
    date_default_timezone_set('UTC');
    $LOGPATH = "/var/log/httpd";
    $ROWS = array(
      'violated-directive',
      'effective-directive',
      'blocked-uri',
      'document-uri',
      'line-number',
      'status-code',
      'referrer',
      'disposition',
      'original-policy',
      'script-sample'
    );
    
    $header = date("Y-m-d H:i:s");
    $raw = file_get_contents('php://input');
    
    if ( ! isset( $raw ) or empty ( $raw ) or strlen( $raw ) >= 2048 ) {
      exit( 1 );
    }
    
    $rows = json_decode( $raw );
    $message = $header . ' csp-report' . PHP_EOL;
    foreach( $ROWS as $row ) {
      $message .= "  $row: " . $rows->{'csp-report'}->{$row} . PHP_EOL;
    }
    $message .= PHP_EOL;
    
    $file = file_put_contents( $LOGPATH . "/report.log", $message, FILE_APPEND | LOCK_EX );
    
    echo "For reporting Content-Security-Policy violation";
    exit( 0 );
    
  • On va commencer en mettant une définition très restrictive dans la configuration apache.
    Header always set Content-Security-Policy "default-src 'self' data: ; script-src 'self' ; style-src 'self' 'unsafe-inline' ; font-src 'self' data: ; img-src 'self' data: ; report-uri https://blog.example.com/report.php"
    
  • On désactive tous les plugins WordPress

On verra apparaitre au rechargement de la page, les premiers rapports, dans le fichier /var/log/httpd/report.log

Premier test

Rapports de violation

  • C’est le seul problème du cœur de WordPress (hors partie admin) concernant la politique de sécurité du contenu: les emojis.
    2017-11-12 12:02:29 csp-report
      violated-directive: script-src https://www.tartarefr.eu
      effective-directive: 
      blocked-uri: self
      document-uri: https://www.tartarefr.eu/
      line-number: 12
      status-code: 
      referrer: 
      disposition: 
      original-policy: default-src https://www.tartarefr.eu data:; script-src https://www.tartarefr.eu; style-src https://www.tartarefr.eu 'unsafe-inline'; font-src https://www.tartarefr.eu data:; img-src https://www.tartarefr.eu data:; report-uri https://report.tartarefr.eu/report.php?type=csp
      script-sample: 
                            window._wpemojiSettings = {"baseUrl"...
    
  • Dans le CSS de notre thème, il y a une dépendance à https://fonts.googleapis.com
    2017-11-12 12:02:29 csp-report
      violated-directive: style-src https://www.tartarefr.eu 'unsafe-inline'
      effective-directive: 
      blocked-uri: https://fonts.googleapis.com
      document-uri: https://www.tartarefr.eu/
      line-number: 
      status-code: 
      referrer: 
      disposition: 
      original-policy: default-src https://www.tartarefr.eu data:; script-src https://www.tartarefr.eu; style-src https://www.tartarefr.eu 'unsafe-inline'; font-src https://www.tartarefr.eu data:; img-src https://www.tartarefr.eu data:; report-uri https://report.tartarefr.eu/report.php?type=csp
      script-sample:
    

Résolution

  • On ré-active (installe et active) le module Disable Emojis
  • On ajoute https://fonts.googleapis.com à notre en-tête Content-Security-Policy, dans la section style-src et on recharge Apache.
    Header always set Content-Security-Policy "default-src 'self' data: ; script-src 'self' ; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com ; font-src 'self' data: ; img-src 'self' data: ; report-uri https://report.example.com/report.php?type=csp"

Deuxième test

Rapports de violation

    • C’est la dépendance pour la police de caractère
      2017-11-12 12:06:52 csp-report
        violated-directive: font-src https://www.tartarefr.eu data:
        effective-directive: 
        blocked-uri: https://fonts.gstatic.com
        document-uri: https://www.tartarefr.eu/
        line-number: 
        status-code: 
        referrer: 
        disposition: 
        original-policy: default-src https://www.tartarefr.eu data:; script-src https://www.tartarefr.eu; style-src https://www.tartarefr.eu 'unsafe-inline' https://fonts.googleapis.com; font-src https://www.tartarefr.eu data:; img-src https://www.tartarefr.eu data:; report-uri https://report.tartarefr.eu/report.php?type=csp
        script-sample: 
      

Résolution

      • On ajoute https://fonts.gstatic.com à notre en-tête Content-Security-Policy, dans la section font-src et on recharge Apache.
        Header always set Content-Security-Policy "default-src 'self' data: ; script-src 'self' ; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com ; font-src 'self' data: https://fonts.gstatic.com ; img-src 'self' data: ; report-uri https://report.example.com/report.php?type=csp"

À partir d’ici, on reçoit plus de rapport de violation, sauf si on essaie la partie administrateur ou un plugin de galerie.

La partie administrateur

La partie administrateur du site ne fonctionne pas bien avec notre définition de Content-Security-Policy. On va encore l’adapter mais à la fin il faudra tricher pour la section script-src et modifier l’en-tête HTTP Content-Security-Policy pour la partie admin.
On va donc ajouter à notre définition Content-Security-Policy

  • https://code.jquery.com à notre en-tête Content-Security-Policy, dans la section style-src
  • https://code.jquery.com à notre en-tête Content-Security-Policy, dans la section img-src
  • https://secure.gravatar.com à notre en-tête Content-Security-Policy, dans la section img-src

Pour le reste, pas de recette miracle, hélas. La définition dans Apache devient ceci:

<Location "/wp-admin">
  ...
  Header always set Content-Security-Policy "default-src 'self' data: ; script-src 'self' 'unsafe-inline' 'unsafe-eval' ; style-src 'self' 'unsafe-inline' fonts.googleapis.com ; font-src 'self' fonts.gstatic.com data: ; img-src 'self' data: secure.gravatar.com ; report-uri https://report.example.com/report.php"
</Location>
Header always set Content-Security-Policy "default-src 'self' data: ; script-src 'self' ; style-src 'self' 'unsafe-inline' fonts.googleapis.com https://code.jquery.com ; font-src 'self' fonts.gstatic.com data: ; img-src 'self' data: https://code.jquery.com https://secure.gravatar.com ; report-uri https://report.example.com/report.php"

Test du site sur securityheaders.io

Bien évidemment, on ne teste pas la partie admin …

Rediriger les mails locaux sur son adresse email

Il suffit d’avoir un serveur de courriel (serveur de mail) installé, avec la configuration par défaut, si ce n’est pas le cas, on l’installe.

sudo yum install postfix

Bien évidemment, on s’assure que le service est démarré et activé.

sudo systemctl start postfix
sudo systemctl enable postfix

Ou sur les distributions ne prenant pas en charge systemd (centos6 ou rhel6)

sudo service postfix start
sudo chkconfig postfix on

On modifie le fichier /etc/aliases pour rediriger les messages locaux vers son adresse de courriel. Exemple de redirection des messages locaux de l’utilisateur root vers l’adresse de courriel mon-user@mon-domain

root:         <mon-user>@<mon-domain>

On lance la reconstruction du cache des redirections

sudo newaliases

Il n’y a plus qu’à tester

echo 'Test depuis la ligne de commande' | \
mail -s 'test redirection' root

On devrait avoir un courriel avec le sujet test redirection

Et la sécurité dans tout ça ?

Par défaut, le service de mail se branche sur localhost et n’est pas accessible depuis l’extérieur. Donc aucun soucis.

Découverte de docker swarm

Pour découvrir le swarm (la nuée ou l’essaim) de docker, on va partir de notre poste Fedora, avec une installation de libvirt fonctionnelle.

Installation

On va créer deux nouvelles machines virtuelles que l’on nommera worker1 et worker2: 2 vCPUs et 4Go de RAM.

Sur l’hôte servant de manager, on initialise le swarm (aka le poste Fedora), en précisant l’adresse IP sur la patte libvirt

docker swarm init --advertise-addr 192.168.122.1
Swarm initialized: current node (2kdz664ro2rpjh5pn1743gsez) is now a manager.
    
To add a worker to this swarm, run the following command:

docker swarm join \
       --token SWMTKN-1-5od9m6ccyazf6uqnopqv6f1p2xqu4saiul7yvbqvjkw5ryczyz-8mjxk1qv26isjnzzyzpf02w5e \
       192.168.122.1:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

Sur les deux hôtes servant de workers: worker1 et worker2

  1. On installe la version la plus récente de docker
    yum install docker docker-latest
  2. On lance le daemon docker
    systemctl start docker
  3. On se joint au swarm en précisant le token fourni à l’initialisation du swarm sur le manager.
    docker swarm join --token SWMTKN-1-54yhh9qjmwcsxb4jhp7p6a0isc4in0vykqu3ir4y06aq6xtpbb-8mspc4bbxh32nx8obni897fvn 192.168.122.1:2377
    This node joined a swarm as a worker.

Le swarm est en place, il ne reste plus qu’à l’utiliser.

Visualiseur

Pour faciliter la vision des choses, on va mettre en place un visualiseur, qui sera encore un conteneur docker.

docker run -it -d \
-p 5000:8080 \
-v /var/run/docker.sock:/var/run/docker.sock \
--name swarm_visualizer \
dockersamples/visualizer

On accède maintenant au visualizer en allant sur l’url http://192.168.122.1:5000/

Gestion des services

Docker swarm gère des services qui seront associés en interne à un ou plusieurs conteneurs. Dans cette exemple (phpmyadmin), on va le découper en deux services:

  • la base de données
  • le serveur web avec le module php

On aurait pu découper en trois, en n’installant pas le module php avec le serveur web et utiliser un service php-fpm, mais cela aurait complexifié inutilement l’exemple.

Afin que nos conteneurs puisse se parler entre eux, on va créer un réseau dédié (overlay)

docker network create --driver overlay --subnet 172.255.0.0/24 swarmnet

On démarre le conteneur embarquant la base de données. Quelques remarques sur la commande à lancer:

  • On fixe la contrainte d’exécution sur le manager, car le conteneur a un volume attaché et docker n’a pas de solution de synchonisation des volumes en natif. En effet si le conteneur s’exécutait sur une autre hôte (worker), il aurait un nouveau volume vierge et on oublie la persistance des données. Toutefois, on pourrait fixer la contrainte d’exécution sur un worker particulier avec --constraint node.hostname=worker1 afin de toujours se servir du même volume de données.
  • On monte le volume docker s’appelant pma-db-data en lieu et place du répertoire de travail mariadb.
  • On fixe le nom de la base de données, les identifiants utilisateur et le mot de passe du superutilisateur (root) via des variables d’environnement.
  • On précise que le conteneur est accessible via son nom d’hôte (accès par rotation DNS) et comme on n’a qu’un seul conteneur base de données, on est sûr de tomber toujours sur le même). Sans ce paramètre (accès par vip) il aurait fallut brancher le port 3306 sur l’hôte exécutant docker (le poste Fedora) et de connecter le conteneur web à l’adresse IP de l’hôte.
  • Évidemment, on le branche sur notre réseau dédié
docker service create --name pma-db \
--constraint node.role==manager \
--mount type=volume,src=pma-db-data,dst=/var/lib/mysql \
-e DB_NAME=pmadb \
-e DB_USER=pma \
-e DB_PASS=pmapasswd \
-e DB_ROOT_PASS=rootpasswd \
--endpoint-mode dnsrr \
--network swarmnet \
docker.io/didier13150/mariadb

On démarre maintenant le second conteneur qui contient le serveur web (avec le module php) et phpmyadmin sur un des workers

  • On fixe la contrainte d’exécution sur un worker
  • On lui communique le nom d’hôte, le nom de la base de données, ainsi que les identifiants utilisateur via des variables d’environnement.
  • On connecte le port 80 du conteneur sur le port 8080 du manager, même si le(s) conteneur(s) s’exécute(nt) sur le(s) worker(s)
  • On le branche également sur notre réseau dédié (pour la résolution du nom d’hôte pma-db)
docker service create --name pma-web \
--constraint node.role==worker \
-e DB_NAME=pmadb \
-e DB_USER=pma \
-e DB_PASS=pmapasswd \
-e DB_HOST=pma-db \
-p 8080:80 \
--network swarmnet \
docker.io/didier13150/phpmyadmin

On accède maintenant à phpmyadmin en allant sur l’url http://192.168.122.1:8080/

Rien de bien compliqué jusqu’à présent.

Mais imaginons qu’un seul serveur web ne puisse tenir la charge et qu’il faudrait le cloner (même plusieurs fois) pour absorber la charge, disons 4 instances de ce serveur web serait pas mal. La magie de swarm, c’est que c’est faisable à chaud, quasiment instantanément et en une seule petite commande:

docker service scale pma-web=4

Et encore plus fort, imaginons que le pic de charge est passé et que les clones ne servent plus à rien, et bien on peut les supprimer:

docker service scale pma-web=1

Pour arrêter les services, on les supprime

docker service rm pma-web
docker service rm pma-db

On arrête aussi notre réseau

docker network rm swarmnet

Gestion des nœuds

Étudions maintenant la gestion des nœuds au sein de notre essaim

  • Lister les nœuds du swarm
    docker node ls
    ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
    2kdz664ro2rpjh5pn1743gsez * manager1 Ready Active Leader
    0xj7x2kxmjuvyqj19ekdubs99 worker1 Ready Active
    7xqkaxxbzi4260b6drujhis1q worker2 Ready Active
  • Supprimer un nœud
    docker node rm 7xqkaxxbzi4260b6drujhis1q

Un petit point sur les options SSL et les en-têtes HTTP

Apache Logo

Options générales

  • ServerTokens définit ce qu’apache peut diffuser comme information à propos de lui-même. Le fanfaronnage est incompatible avec la sécurité.
    • Prod: c’est la valeur la plus sécurisée et le serveur n’enverra que son nom:
      Server: Apache
    • Major: le serveur n’enverra que son nom et son numéro de version majeur:
      Server: Apache/2
    • Minor: le serveur n’enverra que son nom et son numéro de version complet:
      Server: Apache/2.4.25
    • Os: le serveur n’enverra que son nom, son numéro de version complet et le nom du système d’exploitation:
      Server: Apache/2.4.25 (Fedora)
    • Full: le serveur enverra son nom, son numéro de version, le nom du système d’exploitation et la liste des modules actifs avec leur numéro de version:
      Server: Apache/2.4.25 (Fedora) OpenSSL/1.0.2k-fips mod_auth_kerb/5.4 mod_wsgi/4.4.23 Python/2.7.13 PHP/7.0.18 mod_perl/2.0.10 Perl/v5.24.1
  • ServerSignature définit la signature (pied de page) apposée par apache sur les page générées par lui-même (typiquement les pages d’erreurs)
    • Off: aucune signature sur les page générées
    • On: signature présente avec les mêmes informations défini par la directive ServerTokens, le domaine et le port
      <address>Apache/2.4.25 (Fedora) OpenSSL/1.0.2k-fips mod_auth_kerb/5.4 mod_wsgi/4.4.23 Python/2.7.13 PHP/7.0.18 mod_perl/2.0.10 Perl/v5.24.1 Server at www.tartarefr.eu Port 80</address>
    • Email: signature présente avec les mêmes informations défini par la directive ServerTokens, le domaine, le port et l’email de l’administrateur du domaine
      <address>Apache/2.4.25 (Fedora) OpenSSL/1.0.2k-fips mod_auth_kerb/5.4 mod_wsgi/4.4.23 Python/2.7.13 PHP/7.0.18 mod_perl/2.0.10 Perl/v5.24.1 Server at <a href="mailto:fake@tartarefr.eu">www.tartarefr.eu</a> Port 80</address>
      
  • TraceEnable définit si la méthode HTTP Trace est autorisée. Cette méthode sert surtout pour des tests ou des diagnostiques et n’a pas sa place sur un serveur en production. Elle peut prendre deux valeurs: On (la méthode est permise) ou Off (méthode désactivée)

Options SSL

Hormis les directives habituelles (SSLEngine, SSLCertificateFile, SSLCertificateKeyFile, SSLCertificateChainFile) pour le paramétrage SSL, il y en a quelques unes qui méritent une petite explication.

  • SSLCipherSuite définit la liste des ciphers autorisés. Actuellement, pour obtenir un A+ sur SSLlabs, il faut désactiver certains ciphers medium ou high.
    HIGH:MEDIUM:!aNULL:!eNULL:!MD5:!RC4:!EXP:!3DES:!LOW:!SEED:!IDEA:!CBC
  • SSLHonorCipherOrder définit si l’ordre des ciphers de la directive SSLCipherSuite doit être suivi. Il est recommandé de suivre l’ordre défini en mettant la valeur On. Typiquement ici, le serveur essaiera d’abord tous les ciphers HIGH avant d’essayer les MEDIUM.
  • SSLProtocol définit les protocoles autorisés: ici on accepte tous les protocoles sauf SSL version 2 et version 3. On peut commencer à envisager d’exclure aussi TLSv1 (TLSv1.0)
    SSLProtocol all -SSLv2 -SSLv3
  • SSLCompression active ou désactive la compression sur SSL. Comme la faille CRIME exploite une faille de la compression, on désactive cette fonctionnalité en mettant le paramètre à off.

En-têtes HTTP concernant la sécurité

  • Set-Cookie permet de sécuriser les cookies en ajoutant deux paramètres qui rendent les cookies non accessibles aux scripts du client (HttpOnly) et ils ne sont transmis que sur une connexion sécurisée (Secure), c’est à dire en HTTPS.
    Header always edit Set-Cookie ^(.*)$ $1;HttpOnly;Secure
  • X-Frame-Options autorise le navigateur à encapsuler ou non la page web dans une frame. En empêchant la page d’être encapsulée par un site externe, on se protège du clickjacking.
    Il y a 3 valeurs possibles:

    • DENY: aucune encapsulation possible
    • SAMEORIGIN: encapsulation uniquement en provenance du même domaine et du même protocole (HTTP ou HTTPS)
    • ALLOW-FROM: encapsulation uniquement en provenance de l’URI passée en argument
  • X-XSS-Protection active les filtres cross-site scripting embarqués dans la plupart des navigateurs. La meilleure configuration est d’activer la protection des navigateurs: “X-XSS-Protection: 1; mode=block“.
  • X-Content-Type-Options désactive la détection automatique du type MIME par le navigateur et force celui-ci à utiliser uniquement le type déclaré avec Content-Type. La seule valeur valide est nosniff.
  • Referrer-Policy définit la politique d’envoi d’information de navigation dans l’en-tête Referer
    • no-referrer: L’en-tête sera absente de la réponse à la requête.
    • no-referrer-when-downgrade: L’en-tête sera absente s’il y a une diminution de la sécurité (HTTPS->HTTP). Sinon elle sera envoyée.
    • same-origin: L’en-tête sera présente que si le domaine de destination est identique à celui d’origine.
    • origin: L’en-tête sera présente mais ne comprendra que le domaine de la page d’origine.
    • strict-origin: L’en-tête sera absente s’il y a une diminution de la sécurité (HTTPS->HTTP). Sinon elle ne comprendra que le domaine de la page d’origine.
    • origin-when-cross-origin: L’en-tête sera présente et l’URI sera complète si le domaine de destination est identique à celui d’origine, mais ne comprendra que le domaine de la page d’origine si le domaine de destination diffère de celui d’origine.
    • strict-origin-when-cross-origin: L’en-tête sera absente s’il y a une diminution de la sécurité (HTTPS->HTTP). Sinon elle sera complète si le domaine de destination est identique à celui d’origine, mais ne comprendra que le domaine de la page d’origine si le domaine de destination diffère de celui d’origine.
    • unsafe-url: L’en-tête sera présente et l’URI sera complète
  • Content-Security-Policy regroupe les sources autorisées à être incluses dans la page web. En listant uniquement les sources nécessaires, on empêche le téléchargement de sources malicieuses par le navigateur. Le mot clé self représente le domaine appelé.
    • default-src : Définit les sources autorisées par défaut de tous les types de ressources
    • script-src : Définit les sources autorisées pour les scripts
    • object-src : Définit les sources autorisées pour les objets
    • style-src : Définit les sources autorisées pour les feuilles de styles
    • img-src : Définit les sources autorisées pour les images
    • media-src : Définit les sources autorisées pour les médias (vidéo et audio)
    • frame-src : Définit les sources autorisées pour les frames
    • font-src : Définit les sources autorisées pour les polices de caractères
    • connect-src : Définit les sources autorisées à être chargée par les scripts
    • form-action : Définit les sources autorisées pour l’action d’un formulaire
    • plugin-types : Définit les sources autorisées pour les plugins
    • script-nonce : Définit les sources autorisées pour les scripts ayant le même argument nonce
    • sandbox : Définit la politique de bac-à-sable
    • reflected-xss : Active ou désactive les filtres des navigateurs pour la protection XSS
    • report-uri : Définit une URI vers laquelle envoyer un rapport en cas de violation de politique
  • HTTP-Strict-Transport-Security force le navigateur à modifier tous les liens non sécurisés par des liens sécurisés (HTTP->HTTPS) durant le temps indiqué par le paramètre max-age. Celui-ci doit donc être configuré pour être supérieur à la durée de navigation. La page ne sera pas affichée si le certificat SSL n’est pas valide.
  • Public-Key-Pins protège contre les attaques man-in-the-middle avec des certificats X.509 volés (mais valides). En spécifiant l’empreinte des certificats du site au navigateur, celui-ci ne fera pas
    confiance aux autres certificats valides non listés pour le site.
  • Expect-CT annonce le statut du site en rapport aux futurs pré-requis de Chrome. L’en-tête comporte le mot enforce s’il est prêt. Mais dans un premier temps il vaut mieux le mettre en test avec la directive max-age=0 et éventuellement le paramètre report-uri

Vérification

Exemple de configuration Apache

On peut mettre la définition de ces en-têtes dans le fichier /etc/httpd/conf.d/common.conf (On le créé s’il n’existe pas), il sera inclut par la directive IncludeOptional conf.d/*.conf

ServerTokens    Prod
ServerSignature Off

# Disable Trace
# Otherwise host is vulnerable to XST
TraceEnable Off

# Secure cookie with HttpOnly
Header always edit Set-Cookie ^(.*)$ $1;HttpOnly;Secure

Header always set X-Frame-Options SAMEORIGIN
Header always set X-XSS-Protection: "1;mode=block"
Header always set X-Content-Type-Options nosniff
Header always set Referrer-Policy same-origin
Header always set Content-Security-Policy "default-src 'self' ; script-src 'self' report.tartarefr.eu https://s.w.org ; style-src 'self' fonts.googleapis.com fonts.gstatic.com ; report-uri https://report.tartarefr.eu/"
Header always set Strict-Transport-Security "max-age=31536000;includeSubDomains"
Header always set Expect-CT 'max-age=0; report-uri="https://report.tartarefr.eu/"'
Header always set Public-Key-Pins 'pin-sha256="YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=";pin-sha256="1B/6/luv+TW+JQWmX4Qb8mcm4uFrNUwgNzmiCcDDpyY=";max-age=2592000;includeSubdomains; report-uri="https://report.tartarefr.eu/"'

Remplacement de sectool par openscap-content-sectool

Security Audit Icon
L’outil d’audit de sécurité sectool n’étant plus mis à jour, il ne sera plus disponible à partir de Fedora 25. Il est temps de changer d’outil. Une implémentation des tests a été faite dans un plugin open-scap et la migration est triviale.

dnf install openscap-content-sectool

Il ne reste plus qu’à lancer l’outil

/usr/bin/oscap xccdf eval --profile Server --results-arf sectool-arf.xml --report sectool.html /usr/share/openscap/sectool-sce/sectool-xccdf.xml

et de visualiser le fichier sectool.html dans un navigateur.

Certificats SSL avec letsencrypt

 

HTTPS Logo L’avantage avec letsencrypt, c’est que le seul pré-requis est d’être propriétaire du domaine (ou du sous domaine). Le nom DNS doit correspondre à l’hôte qui a lancé le processus de certification.

dnf install letsencrypt

On fait notre demande de certificat

letsencrypt --text --email admin@example.com --domains www.example.com,example.com,apache.example.com --agree-tos --webroot --webroot-path /var/www/html certonly

Il ne reste plus qu’à déclarer notre clé privée et notre certificat dans le fichier de configuration d’Apache: /etc/httpd/conf.d/ssl.conf

SSLCertificateFile /etc/letsencrypt/live/www.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/www.example.com/privkey.pem

Maintenant il nous faut encore mettre en place le renouvellement automatique, cr ce certificat n’est valable que 90 jours (et des discussions sont en cours pour abaisser cette période).
On va se servir des timers de systemd:

  • /etc/systemd/system/letsencrypt-renewal.service
    [Unit]
    Description=Automatically renew the letsencrypt certificate
    
    [Service]
    Type=oneshot
    ExecStart=/usr/bin/letsencrypt renew --agree-tos --email admin@example.com --renew-by-default
    ExecStart=/usr/bin/systemctl reload httpd
  • /etc/systemd/system/letsencrypt-renewal.timer
    [Unit]
    Description=Trigger an automatic renewal every month
    
    [Timer]
    OnCalendar=monthly
    Persistent=true
    
    [Install]
    WantedBy=multi-user.target

On recharge systemd

systemctl daemon-reload

on active le timer

systemctl enable letsencrypt-renewal.timer

on le démarre

systemctl start letsencrypt-renewal.timer

et le tour est joué

Attention la modification de la configuration apache n’est valable que pour apache > 2.4.8
Pour les versions antérieures, il faut utiliser

SSLCertificateFile /etc/letsencrypt/live/www.example.com/cert.pem 
SSLCertificateKeyFile /etc/letsencrypt/live/www.example.com/privkey.pem
SSLCertificateChainFile /etc/letsencrypt/live/www.example.com/chain.pem

 

Logguer l’IP/hôte distant quand on utilise mod_proxy

Par défaut, en passant par un proxy http, Apache loggue l’adresse IP du proxy et non celle à l’origine de la connexion dans les fichiers access.
Pour résoudre ce problème, on va utiliser le module apache mod_remoteip.
On active ce module en créant le fichier /etc/httpd/conf.modules.d/00-remoteip.conf

LoadModule remoteip_module modules/mod_remoteip.so

et on le configure dans le fichier /etc/httpd/conf.d/remoteip.conf

<IfModule mod_remoteip.c>
RemoteIPHeader X-Forwarded-For
RemoteIPInternalProxy 127.0.0.1
LogFormat "%a %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
</IfModule>

On redémarre Apache et le tour est joué

systemctl restart httpd

On ne peut pas déclarer notre proxy de manière générique avec la directive

RemoteIPProxiesHeader X-Forwarded-By

car notre proxy fait partie d’un block IP privé: dixit la page de documentation officielle du module

Tous les blocs d’adresses internes 10/8, 172.16/12, 192.168/16, 169.254/16 and 127/8 (ainsi que les adresses IPv6 en dehors du bloc public 2000::/3 block) ne sont évaluées par mod_remoteip que lorsque des mandataires internes (intranet) RemoteIPInternalProxy sont enregistrés.

Partage NFS

Pour partager facilement des dossiers entre plusieurs ordinateurs *nix, on peut se servir de NFS. Il est vraiment très triviale à mettre en place

yum install nfs-utils

d’exporter les dossiers dans le fichier /etc/exports

/data/save 192.168.0.x(rw,sync,no_root_squash)

et de démarrer le server NFS

systemctl start nfs-server
systemctl enable nfs-server

Mais c’est surtout la partie configuration du firewall qui n’est pas précisement détaillée.
Celle-ci est grandement simplifiée avec firewalld car elle se résume à

firewall-cmd --permanent --add-service mountd
firewall-cmd --permanent --add-service rpc-bind
firewall-cmd --permanent --add-service nfs
firewall-cmd --reload

On peut maintenant lister les services autorisés par le firewall

firewall-cmd --list-all
FedoraServer (default, active)
interfaces: eth0
sources:
services: mountd nfs rpc-bind ssh
ports:
protocols:
masquerade: no
forward-ports:
icmp-blocks:
rich rules:

Cette simplicité est rendue possible grâce aux fichiers situés dans /usr/lib/firewalld/services/:

  • Fichier nfs.xml
    <?xml version="1.0" encoding="utf-8"?>
    <service>
    <short>NFS4</short>
    <description>The NFS4 protocol is used to share files via TCP networking. You will need to have the NFS tools installed and properly configure your NFS server for this option to be useful.</description>
    <port protocol="tcp" port="2049"/>
    </service>
  • Fichier mountd.xml
    <?xml version="1.0" encoding="utf-8"?>
    <service>
    <short>mountd</short>
    <description>NFS Mount Lock Daemon</description>
    <port protocol="tcp" port="20048"/>
    <port protocol="udp" port="20048"/>
    </service>
  • Fichier rpc-bind.xml
    <?xml version="1.0" encoding="utf-8"?>
    <service>
    <short>rpc-bind</short>
    <description>Remote Procedure Call Bind</description>
    <port protocol="tcp" port="111"/>
    <port protocol="udp" port="111"/>
    </service>