Monthly Archives: November 2017

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 que 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 simplement 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 …