PROJET AUTOBLOG


NullPointerException

Site original : NullPointerException

⇐ retour index

Docker et la tendance de la conteneurisation : a-t-elle vraiment été en production ?

dimanche 9 octobre 2016 à 00:00

Docker. Une technologie si jeune (2013) et pourtant dorénavant vendue un peu partout comme la technologie miracle qui va résoudre tous les problèmes de déploiement, vous rendre riche et vous ramener l’être aimé.

Et pourtant, en pratique cette technologie me pose pas mal de problèmes et de cas de conscience. Revue de détails.

Objectif : portabilité

Le but initial est un peu dans le prolongement de ce qu’a fait Java : construire une fois, déployer partout (« build once, deploy everywhere »). On ne peut nier que l’idée initiale soit bonne tellement le problème du déploiement est un véritable parcours du combattant en pratique.

Un logiciel est rarement un morceau totalement isolé. Il vient avec son interpréteur, ses bibliothèques, nécessite un moteur de base de données, des fichiers de configuration, un serveur web et tout un tas d’autres choses. Cet écosystème va être à gérer tout au long de la chaîne de fabrication, que ça soit en développement, en phase de validation/qualification, en recette ou encore en production.

Avant Docker, chaque étape nécessitait de remonter l’ensemble de l’environnement, d’y installer le logiciel (parfois à partir des sources via compilation), de le lancer… Et c’était un gros bordel, puisque la moindre déviation entre deux environnements pouvait conduire à des bugs non reproductibles en fonction de l’endroit où l’on lançait le logiciel, d’où l’apparition du trop fameux « Ça marche chez moi ™ ».

Avec Docker, point de tout cela. Les développeurs construisent une image du logiciel qui va contenir tout ce qui lui faut pour fonctionner, depuis l’OS jusqu’au logiciel lui-même en passant par toutes les autres briques. S’ils ont satisfaction de ce qu’ils ont obtenu, ils livreront à l’étape suivante cette image, qui n’aura pas bougé d’un iota et pourra être lancée à l’identique, et ainsi de suite jusqu’à la très attendue mise en production.

Ça c’est la théorie. Maintenant, la pratique :D

Besoins de dev ≠ besoin de la prod

Le premier rempart à l’usage de Docker est que les besoins entre la production et le développement sont assez différents.
Un développeur va vouloir accéder facilement aux journaux, si possible en mode debug, alors que la prod préférera les envoyer à un Logstash et en mode info voire warn ou error. Un développeur préférera utiliser directement le serveur d’application léger de son choix comme Jetty, Thin ou Gunicorn, alors que la production configurera un backend Nginx devant ou utilisera des serveurs d’application plus puissant tel que Tomcat ou Passenger. Un développeur préférera sûrement compiler en mode debug pour avoir des retours utilisables en cas de problème alors que la production insistera pour le faire en mode release. La production voudra mettre en place un pare-feu, ou ses outils de supervision de parc, dont le développement n’a même aucune idée de l’existence puisque ça ne fait pas partie de ses compétences ! J’évite même de parler d’intégrer ses outils de développement à un environnement Docker, par exemple lancer son projet Java présent sur son Eclipse local sur le Tomcat présent sur l’image Docker, ça ferait trop mal.
Bref, en pratique, c’est compliqué… On peut quand même s’en sortir pour certains morceaux, surtout grâce à certaines fonctionnalités de Docker, mais l’intérêt en devient limité.

Pour les environnements différents, il « suffit » que les devs travaillent hors Docker à l’ancienne, puis une fois parvenus à quelque chose de satisfaisant, s’attaquent à la construction d’une image Docker. Ça implique une espèce de mini-chaîne complète (dev/test/validation/qualification/production) faite uniquement par les développeurs, afin de s’assurer que l’image finale est à peu près conforme à ce qui est attendu (quel logiciel fournir, quelles dépendances disponibles…). Parce que du coup ça n’implique plus que si ça tourne sur leur environnement de dev, ça tourne sur l’environnement Docker. Pas forcément très folichon niveau processus sinon avoir ramené toutes les considérations des autres étapes sur celle de développement.

Pour les besoins propres à la production (pare-feu, monitoring…), Docker permet de créer une image à partir d’une autre. La production repartira donc de l’image fournie par les développeurs pour refaire sa propre image incluant tout le nécessaire. Ça casse aussi l’intérêt de Docker qui garantit que ça tournera en production, étant donné que la production peut elle aussi introduire des bugs. Par exemple l’installation d’un pare-feu va peut-être installer une bibliothèque utilisée par l’application mais dans une version différente. La production devra donc aussi repasser une bonne partie des étapes précédentes (test/validation/qualification) avant la mise en production réelle. Pas folichon non plus.

Dans les deux cas il aurait été plus intelligent que le dev et la prod travaillent ensemble dès le départ (qui a dit Devops ?) pour fournir une image Docker à la QA qui partira en production telle quelle une fois approuvée. Mais alors qu’on utilise Docker ou n’importe quelle autre technologie (virtualisation classique, automatisation d’installation via un outil comme Chef, Puppet, Ansible ou Salt) on aurait obtenu le même résultat.

Quid de la sécurité ?

LE gros point noir selon moi.

Docker repose sur le principe d’immuabilité des images. Une fois une image livrée, on n’y touche plus et au prochain changement nécessaire, on refait une image depuis zéro.

Ça pose un problème assez énorme en termes de sécurité. Le jour où vous avez une faille dans le logiciel livré, on comprend bien qu’on n’échappera de toute façon pas à un correctif, une regénération, un passage intégral de toute la chaîne de qualification et une nouvelle mise en production. Mais si c’est une bibliothèque utilisée par le logiciel, par exemple OpenSSL qui connaît au moins une faille critique de sécurité par jour ?

Dans une infrastructure sans Docker, les gentils administrateurs systèmes se seraient sagement connectés en SSH aux machines impactées, auraient simplement fait un apt update && apt upgrade et basta.
Un patch de sécurité n’introduisant pas de changement de fonctionnalités et les développeurs de OpenSSL faisant bien leur travail, ils livrent des versions patchées assurant la rétro-compatibilité avec les versions précédentes. Les admins peuvent donc appliquer le patch assez rapidement sans avoir besoin de consulter les développeurs. Et la faille est rapidement corrigée avec un risque de régression négligeable (qu’on a parfaitement su accepter et maîtriser pendant des décennies, et même Docker ne pourra jamais garantir le 0 bug). En prime, le logiciel final, lui, n’a pas changé de version.

Dans une infrastructure dockerisée, c’est une autre histoire…
Les conteneurs devant être immuables, il est interdit aux administrateurs de se connecter aux machines pour les mettre à jour. Un changement de sécurité sur une bibliothèque réclame donc de redérouler l’intégralité de la chaîne : on met à jour la bibliothèque, on construit une nouvelle image Docker (qui change donc de version), on repasse toute la QA, on met en production. La mise en production nécessite un arrêt de l’ancienne image, la migration des données sur la nouvelle image et le redémarrage du système. Bref, vous allez tourner un long moment avec votre image trouée avant d’avoir pu fixer le problème…

Quand on évoque ce problème avec la communauté Docker, ils avancent alors leur recette miracle : l’intégration continue.
Certes, si vous réussissez à intégrer l’intégralité de la QA (tests unitaires, tests fonctionnels, tests d’intégration, tests de validation, tests de qualification, tests de recette) dans une suite de tests automatiques, c’est peut-être envisageable de faire une modification dans le code, de cliquer sur un bouton et d’avoir automagiquement la nouvelle version en production.
En pratique, on a déjà du mal à atteindre 100% de couverture sur les tests unitaires, et plus on monte dans les étages plus c’est compliqué. Les tests fonctionnels doivent simuler des humains presse-boutons, les tests d’intégration sont un enfer à réaliser vu le nombre de composants en jeu, les tests de qualification sont souvent longs (tests de performance, de tenue de charge…), etc.
Sauf à investir un budget colossal en automatisation de tests, l’intégration continue revient finalement à se restreindre à un sous-ensemble des tests (on se limite aux cas principaux et on laisse tomber les cas dégradés par exemple) et donc à potentiellement laisser passer des bugs lorsque les administrateurs pousseront le bouton « Déployer » après une modification rapide pour fixer une faille de sécurité. Seuls les grands comme Google ou Amazon peuvent se permettre l’outillage de détection d’un problème en production (du test A/B par exemple) et donc d’alléger ou de supprimer toutes les phases de test : on déploie plus ou moins à l’arrache, si ça merde, on revient en arrière immédiatement !

Les plus perspicaces d’entre vous auront noté que tout ce qui précède repose sur une hypothèse très forte : on doit être mainteneur de l’image Docker utilisée !
En pratique, beaucoup utilisent des images pré-construites qu’ils assemblent selon leurs besoins. Il existe des dépôts d’images, dont le plus connu est le Hub Docker. Du coup, en cas de faille, il va vous falloir attendre une mise-à-jour. L’unique mainteneur est passé sous un bus ? Vous êtes mal…
En termes de sécurité, c’est même encore plus gore dans ce cas, puisque vous n’avez pas beaucoup de moyens de vous assurer que l’image de 600Mo (n’oublions pas que ça intègre un OS complet) que vous allez utiliser n’intègre pas une porte dérobée ou une version obsolète d’une bibliothèque, surtout quand l’image est réalisée avec les pieds et ne permettent aucune vérification a posteriori. Les paquets Debian sont par exemple construits en compilation reproductible et vous pouvez facilement vous assurer que le nginx que vous utilisez est bien le même que celui du paquet Debian officiel alors que sous Docker, je vous souhaite bien du courage ne serait-ce que pour connaître le numéro de version utilisé. Sur chaque image Docker, il faudrait faire une revue assez poussée de ce qui se trouve sur l’image, au moins pour lister les différents composants et bibliothèques intégrés et ainsi connaître les impacts réels sur votre parc d’un correctif de sécurité.

Mono-processus : ma vie, mon œuvre

L’humanité s’est battue pendant des décennies pour mettre en place le multi-processus, puis le multi-thread… Mais ça c’était avant ! Avec Docker, vous ne pouvez lancer qu’un seul et unique processus dans une image. Pas plus.
Ça n’a l’air de rien, mais c’est très handicapant en pratique. Vous ne pouvez pas avoir cron à tourner régulièrement par exemple. Donc pas de logrotate. Vous ne pouvez pas avoir de serveur SSH pour vous connecter à distance sur l’image. Pas de serveur de mail non plus, par exemple pour les rapports journaliers de logwatch (de toute façon on n’a pas cron pour les lancer…). Rien. La seule et unique chose que vous allez pouvoir lancer sera donc votre application. Et c’est tout.

Le problème est que comme on l’a vu précédemment, une application se suffit généralement assez peu à elle-même. Elle nécessite par exemple une base de données, un frontal web, de pouvoir envoyer des courriels… Et donc de lancer plusieurs processus !

En méthode quick & dirty, vous pouvez contourner le problème en lançant un bête script bash qui lancera tout en arrière-plan, ou plus malin un gestionnaire de processus comme supervisord ou pups qui se chargera lui-même de lancer tout le reste de ce que vous avez besoin. C’est tout de même assez galère à faire puisque votre distribution adorée vous fournira des scripts de démarrage pour le système d’init habituel (sysv, systemd, upstart…) et non pour supervisord ou pups, il vous faudra donc faire un travail de portage pour chaque composant nécessaire.

La méthode recommandée par Docker pour gérer vos environnements est l’utilisation de Docker Compose. Vous allez créer autant de conteneurs que de composants de votre écosystème (un pour l’application, un pour la base de données, un pour le serveur de courriel…) et les assembler entre-eux pour qu’ils communiquent correctement.
Pour certains composants comme la base de données, je trouve ça intéressant de séparer du reste, exactement comme on l’aurait fait dans une infrastructure non virtualisée. Pour d’autres, comme un serveur de courriel dédié pour envoyer 3 courriels au mois, c’est du gaspillage de ressources flagrant. Et pour la majorité, c’est d’une prise de tête sans nom… Par exemple dans une application Ruby-on-Rails utilisant Sidekiq comme ordonnanceur, on va se retrouver à avoir 4 conteneurs :

Alors que tout mettre sur la même machine se justifie largement tant qu’on n’a pas une volumétrie délirante (je tiens les 200.000 vues quotidiennes sur Cryptcheck sans soucis avec 1 seul conteneur), on se retrouve avec 4 machines à gérer et à devoir mettre à jour (les 4 utilisent OpenSSL par exemple) et une duplication de l’environnement Ruby (RoR & Sidekiq). On risque aussi de rencontrer des dégradations de performances, puisqu’on passe de communications sur la boucle locale voire des sockets UNIX à une communication TCP/IP externe. Et la sécurité devient tout autant un enfer, avec de la gestion de pare-feu à mettre en œuvre.

Pour gérer autant de conteneurs, vous allez aussi devoir passer à des outils de gestion de déploiement et d’orchestration, comme Kubernetes. Toujours pareil, quand vous vous appelez Google, Wikimedia, SAP ou Ebay, c’est peut-être gérable. Si vous êtes une petite boîte, ça risque d’avoir un surcoût non négligeable et pas forcément rentable.

De la chasse aux méga-octets à la chasse aux bugs non reproductibles

On a vu juste avant que Docker incite à créer plein de conteneurs pour les assembler entre eux. En interne pour la fabrication de ses propres images, Docker se base aussi sur des images pré-construites qu’il empile les unes sur les autres au travers de OverlayFS. Pour l’image Sidekiq, c’est pas moins de 39 couches empilées.

Déjà, ça n’aide pas à la sécurité non plus, puisqu’une faille corrigée sur une couche réclame la reconstruction de toutes les images situées sur les couches supérieures. Ou alors vous corrigez violemment sur votre couche terminale, mais vous dupliquez alors vos bibliothèques (installées par une couche N mais masquées via overlayfs par la couche N+X) et vous avez le travail à faire sur toutes vos images utilisant la couche faillible.

Ensuite, si on utilisait des images standard, on se retrouverait à consommer plusieurs giga-octets pour pas grand-chose. Du coup, la communauté Docker a commencé à faire la chasse aux méga-octets, et s’est prise de passion pour une distribution présentée comme très légère : Alpine Linux.

Cette distribution vient avec un gros piège… Elle est conçue à la base pour aider à débugger des kernel ! Parce que quand vous développez ce genre de logiciel, vous êtes bien content d’avoir une image de 5Mo qui démarre en 2s, vu le nombre de redémarrages que vous allez faire. Et comme vous ne ferez presque rien côté utilisateur, on peut y mettre des trucs très légers comme musl et busybox au lieu des mastodontes que sont la glibc et les coreutils.

Sauf que musl n’est pas compatible avec la glibc disponible un peu partout.
Ni au niveau binaire, ce qui signifie que vous devez compiler explicitement vos logiciels avec cette libc et donc maintenir à la fois un livrable -glibc pour les gens hors de Alpine et un -musl pour Alpine.
Ni au niveau fonctionnalités, ce qui fait que vous pouvez rencontrer des bugs incompréhensibles et non reproductibles sur d’autres plate-formes plus standard. Ça peut aller jusqu’à l’impossibilité totale de compiler votre logiciel, comme c’est le cas actuellement avec OpenJDK 8 ou Phusion Passenger.

Bref, vous allez vous retrouver à soit utiliser des images du Hub Docker avec une chance non négligeable d’utiliser un conteneur Alpine dans votre chaîne et faire la chasse aux bugs vraiment chiants à comprendre, soit à devoir faire votre propre image personnelle sans Alpine… Le tout en croisant les doigts à chaque construction d’image pour ne pas tomber en plus sur une image contenant une faille de sécurité…

Au final, Docker passe en plus complètement à côté de la plaque en termes de consommation de ressources. À titre d’exemple, la stack précédente RoR/Redis/Sidekiq/Nginx ramène pour 60 overlays Docker et 3.1 Go d’espace disque, quand je m’en tire pour 1.8 Go pour Cryptcheck avec une stack dev/RoR/Redis/Sidekiq/Nginx/Elasticsearch/CryptCheck/données. Un beau gâchis d’espace

Une tendance qui se propage de plus en plus

Cette tendance du « je package tout dans un seul truc » est devenu à la mode et on la retrouve vraiment partout. Même si la complexité induite par ce type de systèmes peut être problématique, c’est vraiment le problème de la gestion de la sécurité qui est très dangereuse en pratique. On a déjà du mal à maintenir nos parcs plus ou moins à jour avec une infrastructure pas trop complexe, ça risque de devenir un véritable carnage une fois des outils comme Docker (mal) utilisé un peu partout…

Go, langage d’ailleurs utilisé par Docker lui-même, compile vos projets sous forme d’un exécutable statique qui embarque donc toutes vos bibliothèques. Ça a l’avantage de ne pas nécessiter leur installation, mais ça pose tous les problèmes de sécurité vus auparavant avec la recompilation nécessaire de tous vos binaires au moindre changement d’une bibliothèque.
Sachant en plus que la gestion des dépendances y est très mauvaise puisque se base par défaut sur les branches master de dépôts GitHub et non sur des tags, c’est une bombe à retardement dans vos systèmes. Par exemple vous êtes actuellement incapable de recompiler d’anciennes versions de pas mal de logiciels puisque des dépendances ont fait des modifications non rétro-compatibles avec les anciennes versions de Go et que les versions des dépendances utilisées à l’époque ne sont mentionnées nulle part. La situation devrait cependant s’améliorer avec l’introduction du vendoring depuis Go 1.5.

Snap, la nouvelle idée à la con le nouveau format de paquets d’Ubuntu/Canonical embarque aussi dans une image statique votre logiciel et toutes ses bibliothèques. La problématique de sécurité devient encore pire puisqu’ici, on parle d’une utilisation en tant qu’environnement de bureau.
Par exemple sur mon PC, je me retrouverais avec 60 versions de libssl, utilisée par postfix, openssl, bind9, gstreamer, virtualbox, isync, ntp, postgresql, nmap, tor, mumble, irssi, xca, openssh, apache2, telnet ou encore socat… Le jour où il faudra mettre à jour tout ça, ça va être une belle tranche de rigolade et on n’aura sûrement pas la réactivité qu’a pu avoir le projet Debian sur Heartbleed par exemple, corrigé en quelques heures et disponible tout aussi rapidement sur l’ensemble des dépôts. Quand je leur ai posé la question, ils ont uniquement réfléchi à la mise-à-jour d’un point de vue « binaires » et n’ont même pas pensé à la problématique des migrations de données.
Encore une fois, transformer les mainteneurs d’une application en mainteneurs d’un écosystème complet est loin d’être anodin ici, et le travail à abattre fera que la sécurité ne pourra plus être assurée.

Comment fait-on alors ?

Utiliser des conteneurs, ça peut quand même avoir du bon.

Je m’en sers beaucoup en développement pour monter un environnement stable et surtout pouvoir gérer des environnements difficiles à mixer proprement dans un même système (gcc 4.9 & 6.1 par exemple) ou avec des dépendances pouvant entrer en conflit avec celles du système (l’enfer ffmpeg/avidemux/audacity…).
Ça permet aussi de revenir rapidement à un état propre et maîtrisé. En phase de développement on a généralement tendance à installer des tonnes de choses à la truelle et à modifier violemment son système pour faciliter le debug ou pour chercher la cause d’un problème. Mais c’est toujours intéressant de recompiler sur une machine vierge pour vérifier qu’on n’a pas oublié un bout.
Et enfin ça peut faciliter la reproductibilité d’un bug si l’utilisateur qui le détecte parvient à vous fournir une image démontrant le problème plutôt que de faire face à des « mais ça ne marche pas chez moi ™ ».

Mais par pitié, arrêtez de vouloir mettre en production des blobs galères à gérer, en particulier en termes de sécurité, qui compliquent le moindre audit pour savoir ce qui tourne là-dedans et qui transforment les infrastructures en plat de spaghetti…

Un petit conteneur LXC tout simple, construit à partir de debootstrap générant une image de maximum 200Mo toute mouillée et dans laquelle vous allez installer vos logiciels au mieux avec du apt/dpkg standard et au pire quelques scripts automatisant l’installation (du bon vieux bash over chroot ou du ansible/salt), ça fonctionne simplement et c’est facile à gérer côté sécurité. Et vous découpez vos conteneurs non plus par logiciel comme sous Docker, mais par besoin : 1 conteneur pour toute la stack Discourse, 1 pour votre serveur de courriel entrant, 1 pour votre serveur DNS faisant autorité, etc.

Installer Discourse sans Docker

vendredi 30 septembre 2016 à 00:00

Discourse est un moteur de forum nouvelle génération, vraiment excellent comparé aux autres moteurs vieillissants comme PHPBB ou Simple Machines Forum.

Son principal et gros problème est qu’il n’est livré officiellement que via un conteneur Docker.
(Vous aurez d’ailleurs le loisir de savoir tout le mal que je pense de cette technologie et de cette tendance dans un prochain billet ici-même.)

Comme je n’aime pas Docker et que Ruby on Rails, le framework web utilisé par Discourse, est pourtant très bien fait pour un déploiement simple et efficace, voici un tuto sur comment installer ce moteur de forum en se passant totalement de toute solution de virtualisation !
Je me sers de cette procédure pour ma propre instance Discourse.

Prérequis : Ruby

Ruby est disponible nativement dans les paquets Debian, mais son installation via apt, au même titre que Python, est assez dangereuse pour votre système.
En effet, un projet vient généralement avec trop plein de dépendances qui vont aller s’installer un peu partout sur votre système et risquent d’entrer en conflit avec votre gestionnaire de paquets.
Du coup, il vaut mieux utiliser RBEnv qui va vous permettre d’avoir plusieurs versions de Ruby sur la même machine facilement, mais en plus isolera vos dépendances proprement.

Pour installer tout ça :

apt install git-core
apt install rbenv
apt install autoconf bison build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev libgdbm3 libgdbm-dev

export RBENV_ROOT=/usr/local/rbenv
eval "$(rbenv init -)"

git clone --depth 1 https://github.com/rbenv/ruby-build $RBENV_ROOT/plugins/ruby-build -b v20160913
rbenv install 2.3.1
rbenv global 2.3.1

echo "gem: --no-test --no-document" > ~/.gemrc
gem install bundler
rbenv rehash

Discourse

L’installation n’est guère compliquée pour qui a déjà fait du Ruby on Rails, puisque c’est du 100% standard :

export RBENV_ROOT=/usr/local/rbenv
eval "$(rbenv init -)"

export RAILS_ENV=production
export DISCOURSE_DB_HOST=localhost
export DISCOURSE_DB_PASSWORD=un-bon-gros-mot-de-passe
export DISCOURSE_HOSTNAME=le.futur.domaine.de.votre.forum

apt install redis-server postgresql postgresql-contrib libpq-dev nginx-light
su postgres -s /bin/bash <<-EOF
	psql -c "CREATE USER discourse WITH PASSWORD '${DISCOURSE_DB_PASSWORD}';"
	createdb -O discourse discourse
EOF

mkdir -p /srv/www

git clone https://github.com/discourse/discourse /srv/www/discourse --depth 1 -b v1.6.4
cd /srv/www/discourse
mkdir -p public/uploads
chown -R www-data:www-data log tmp public/uploads
bundle install

su postgres -s /bin/bash -c 'psql -c "ALTER USER discourse WITH SUPERUSER;"'
bundle exec rake db:migrate
su postgres -s /bin/bash -c 'psql -c "ALTER USER discourse WITH NOSUPERUSER;"'

bundle exec rake assets:precompile

Services divers

Discourse nécessite un démon Sidekiq pour ses taches d’arrière-plan. Et j’ai tenté d’utiliser Passenger comme serveur d’application Ruby on Rails, mais il ne tenait pas la charge et je me suis donc rabattu sur Unicorn.
Et il faut donc démarrer ces deux services pour avoir un forum fonctionnel. Voici les services SystemD correspondants, à éditer en fonction de vos besoins !

wget https://gist.github.com/aeris/24862f2d0c34e831d80cb616e995957b/raw/7d8b9f7519d5ad09c14ea17a9d5408cbe6f39ab0/discourse-sidekiq.service -O /etc/systemd/system/discourse-sidekiq.service
wget https://gist.github.com/aeris/24862f2d0c34e831d80cb616e995957b/raw/7d8b9f7519d5ad09c14ea17a9d5408cbe6f39ab0/discourse-unicorn.service -O /etc/systemd/system/discourse-unicorn.service
# Éditer ici les fichiers (mot de passe, domaine, répertoire…)

systemctl enable discourse-sidekiq discourse-unicorn
systemctl start discourse-sidekiq discourse-unicorn

Pour votre configuration Nginx, vous trouverez un exemple dans le fichier config/nginx.sample.conf de Discourse. La seule véritable subtilité est que certaines URL (/assets/, /uploads/…) doivent être servies directement depuis le disque et ne doivent pas être transmises au serveur d’application et doivent donc être exclues de la directive proxy_pass.

Mise-à-jour

Pour la mise-à-jour du moteur, c’est aussi une procédure tout à fait classique pour du Ruby on Rails :

export RBENV_ROOT=/usr/local/rbenv
eval "$(rbenv init -)"

export RAILS_ENV=production
export DISCOURSE_DB_HOST=localhost
export DISCOURSE_DB_PASSWORD=un-bon-gros-mot-de-passe
export DISCOURSE_HOSTNAME=le.futur.domaine.de.votre.forum

cd /srv/www/discourse
git fetch https://github.com/discourse/discourse
git checkout vX.Y.Z

systemctl stop discourse-sidekiq discourse-unicorn

bundle install

su postgres -s /bin/bash -c 'psql -c "ALTER USER discourse WITH SUPERUSER;"'
bundle exec rake db:migrate
su postgres -s /bin/bash -c 'psql -c "ALTER USER discourse WITH NOSUPERUSER;"'

bundle exec rake assets:precompile

systemctl start discourse-sidekiq discourse-unicorn

SSH-Ident : gérez proprement vos agents SSH

samedi 23 juillet 2016 à 00:00

Du (mauvais) usage de l’agent SSH et du transfert d’agent

Agent SSH et transfert d’agent

Si comme moi vous êtes admin sys, vous utilisez forcément SSH au quotidien pour gérer votre parc (sauf s’il est sous Windows, auquel cas recevez toutes mes condoléances…). Et si vous êtes un vrai admin sys, vous utilisez aussi forcément une clef SSH pour vous connecter plutôt qu’un mot de passe. Sauf qu’il est plus que pénible d’avoir à saisir sa phrase de passe avant chaque connexion à une machine, et donc il est plus que probable que vous utilisiez aussi un agent SSH pour charger vos clefs une bonne fois pour toutes à la première connexion.

Pour les personnes utilisant juste SSH pour le fun ou 2-3 trucs persos, cette solution est relativement sécurisée et ne posera pas les soucis exposés ci-après. Par contre, sur des parcs plus complexes, il y a fort à parier que vous utilisiez une autre fonctionnalité de l’agent SSH : le transfert d’agent (agent forwarding dans le texte). En effet, on a bien souvent certaines machines qui ne sont pas directement accessibles sur Internet (les machines virtuelles généralement), mais nécessitent une machine de rebond (bounce) pour y parvenir. Dans les grands parcs, il n’est même pas rare d’avoir une unique machine exposée dans la zone démilitarisée (DMZ), toutes les autres étant bien à l’abri derrière les pare-feux. Avec l’agent forward, notre agent SSH va se propager au travers du réseau via les machines auxquelles on se connecte, permettant ainsi de se connecter aux machines finales sans mot de passe et sans avoir à copier notre clef SSH sur chaque machine intermédiaire. La plupart du temps, l’admin sys étant un fainéant notoire, il a tendance à activer l’agent-forwarding par défaut pour ne pas avoir à se préoccuper du chemin à suivre pour joindre une machine et ainsi pouvoir accéder à n’importe quelle machine du parc depuis n’importe quelle autre.

Une grosse faille de sécurité si on cumule les deux

Les deux fonctionnalités (ssh-agent + agent-forward) cumulées peuvent avoir un impact très important sur la sécurité de vos clefs SSH. Votre agent SSH local charge toutes vos clefs SSH au démarrage, et se chargera de les proposer à votre client SSH quand il en aura besoin. Pour la communication avec le client SSH, l’agent met à disposition un socket UNIX de la forme /tmp/ssh-<truc random>/agent.<pid de l’agent> et le publie dans la variable d’environnement SSH_AUTH_SOCK. Lorsqu’on fait du transfert d’agent, ce socket va se propager sur chaque machine intermédiaire pour assurer la communication avec le client SSH, toujours via la variable SSH_AUTH_SOCK.

Et c’est là que quelque chose de terrible peut se produire… Le transfert d’agent crée aussi un socket UNIX sur la machine intermédiaire, socket qui est un simple fichier quelque part sur le disque. Et donc tout utilisateur capable de lire ce fichier peut communiquer avec votre agent SSH transféré ! Toute personne connectée sous le même utilisateur SSH que vous peut le faire. L’utilisateur root peut le faire. L’administrateur de l’hôte physique hébergeant votre machine virtuelle peut le faire (cas des VPS, mutualisés, etc). Pour exploiter ce socket, il suffit à un attaquant de trouver le socket (un simple ls /tmp/ssh-* lui les listera tous) et de définir manuellement la variable d’environnement SSH_AUTH_SOCK pour utiliser votre agent SSH…

Là où ça pique vraiment, c’est que votre agent SSH de départ connaît toutes vos clefs. Pas seulement celle que vous avez réellement utilisée pour votre chaîne de connexion. Toutes. Si vous avez par exemple une clef A pour vos machines persos et une clef B pour vos machines pros, que les deux sont chargées dans l’agent que vous avez exporté, votre collègue connecté en même temps peut alors se connecter à toutes vos machines perso ! On pousse encore un peu la parano ? Vous utilisez GitHub très certainement, et avec une clef SSH pour éviter d’avoir à saisir votre mot de passe à chaque push/fetch. Vous avez un agent SSH et vous avez activé le transfert d’agent par défaut ? Bingo, à chacune de vos connexions, GitHub peut se connecter à l’ensemble de vos machines, y déployer sa propre clef SSH (dans /root/.ssh/authorized_keys2, il est aussi pris en compte par SSH et peu d’admin penseront à vérifier ce fichier :P) et ainsi monter un botnet géant constitué de l’intégralité du parc informatique de tous ses utilisateurs.

Une petite démo de l’utilisation possible :

alice@home  $ eval $(ssh-agent)
alice@home  $ ssh-add ~/.ssh/id_ed25519_perso
alice@home  $ ssh-add ~/.ssh/id_ed25519_pro

alice@home  $ ssh alice@perso
alice@perso $ logout 

alice@home  $ ssh alice@pro -A
alice@pro   $ echo $SSH_AUTH_SOCK
SSH_AUTH_SOCK=/tmp/ssh-HVo7YpeUUH/agent.4245
alice@pro   $ (do a long stuff)

(Meanwhile)
malory@home $ ssh root@pro
root@pro    # echo $SSH_AUTH_SOCK

root@pro    # ssh alice@perso
Permission denied (publickey).

root@pro    # find /tmp -path /tmp/ssh-*/agent.*
/tmp/ssh-HVo7YpeUUH/agent.4245
root@pro    # ls -al /tmp/ssh-HVo7YpeUUH/agent.4245
srwxr-xr-x 1 alice alice 0 Jul 22 20:08 /tmp/ssh-HVo7YpeUUH/agent.4245
root@pro    # export SSH_AUTH_SOCK=/tmp/ssh-HVo7YpeUUH/agent.4245

root@pro    # ssh alice@perso
alice@perso $ echo Powned | write alice

Du (bon) usage de l’agent SSH et du transfert d’agent

Maintenant qu’on a vu comment on pouvait exploiter l’agent SSH des autres, voyons comment on peut se prémunir de ce genre de problème.

Ne pas utiliser de transfert d’agent SSH

La solution la plus simple est de se passer tout simplement du transfert d’agent.

On peut conserver les possibilités de rebond via l’option ProxyCommand et -W de SSH. Par exemple pour joindre bar via foo, vous pouvez mettre dans votre fichier ~/.ssh/config :

Host bar
	ProxyCommand ssh -W %h:%p foo

L’idée est d’utiliser les possibilités de transfert de connexion TCP de SSH pour se connecter d’abord à foo, puis de raccrocher un tunnel TCP depuis bar vers votre machine locale via foo et de se connecter localement en SSH à ce tunnel, donc en utilisant l’agent local.

En pratique, sur des infrastructures un peu complexe, cette possibilité est très vite limitante puisque ça devient très vite l’enfer si on doit passer par plus d’une machine intermédiaire et qu’en plus on ne peut plus joindre les machines finales à partir de n’importe quelle autre machine (il faut obligatoirement revenir à la machine locale).

Limiter le transfert d’agent à la seule clef utilisée

L’option IdentitiesOnly existe dans SSH pour limiter les clefs possibles pour un agent SSH. Si vous voulez donc l’activer par défaut, il faut ajouter à votre ~/.ssh/config :

Host *
	IdentitiesOnly yes

Vos chaînes de connexion doivent du coup utiliser la même clef SSH à chaque rebond, ce qui peut parfois être problématique (par exemple vos frontaux sécurisés n’acceptent que les clefs ED22519 quand vos vieilles machines finales en sont restées à RSA-4096).

Cloisonner les clefs SSH via des agents multiples

L’un des gros problèmes de la « faille » de l’agent SSH est surtout que l’agent expose l’ensemble de vos clefs SSH, et non pas uniquement celles concernées par la connexion établie. À la limite, si GitHub ne pouvait avoir accès qu’à ses propres machines, vous aux vôtres et vos collègues à celles de votre parc professionnel, il n’y aurait plus vraiment de problème.

L’idéal serait donc d’avoir un agent SSH par groupe de connexion (pro, perso, dev, git…), chacun chargé uniquement des clefs qui vont bien. Ça aurait en plus l’énorme avantage d’alléger le nombre de clefs par agent, et donc d’accélérer les connexions, l’agent testant les clefs disponibles les unes après les autres jusqu’à parvenir à se connecter (ce qui peut en plus causer des problèmes si vous avez trop de clefs qui échouent ou un fail2ban en face).

De manière générale, le cloisement d’identité n’est jamais une mauvaise idée en termes de sécurité, et permet par exemple d’éviter ceci.

SSH ne gère nativement pas cette séparation, il va donc falloir ruser un peu.

SSH-Ident a été développé initialement par Carlo Contavalli et j’ai corrigé 2 ou 3 trucs dessus (support de SSH_ASKPASS, suppression des invocations bash…). Il remplace SSH et se base sur les options passées à SSH pour déterminer à quelle identité raccrocher la connexion. Chaque identité se trouve isoler dans un sous-répertoire de ~/.ssh/identities/, avec son fichier de configuration, ses clefs SSH propres, son agent SSH…

Pour l’installer et l’utiliser, suivez la doc.

Personnellement, j’utilise 4 identités :

rsync et autres outils liés à SSH ne posent pas de soucis particuliers avec SSH-Ident. Je rencontre uniquement des problèmes avec scp, qui cherche à invoquer ssh via un chemin en dur et non via le mécanisme du PATH, ainsi que sur la machine distante ce qui pose des problèmes si SSH-Ident est aussi déployé là-bas. Investigations en cours !

Bonus : Concierge

Avec SSH-Ident, vous allez perdre la complétion automatique sur votre invocation SSH, puisqu’elle ne tient compte que du fichier ~/.ssh/config, qui n’est plus utilisé ici. Et au passage, c’est déjà assez chiant à rédiger un fichier de config SSH, mais là en plus, on se retrouve avec plusieurs…

Vous pouvez donc utiliser Concierge, un moteur de template de fichiers SSH, qui permet d’en écrire des plus concis et compréhensible que ceux par défaut (par exemple la gestion horrible du paramétrage par défaut qui nécessite des Host * en fin de fichier…).

Et pour restaurer la complétion automatique, générez automatiquement votre ~/.ssh/config avec tous les hôtes trouvés dans vos fichiers d’identité.

Pour automatiser le tout, faites un ~/.ssh/Makefile que vous invoquerez avec un petit make -C ~/.ssh !

.DEFAULT_GOAL := config
MAKEFLAGS += --no-builtin-rules

TEMPLATES := $(wildcard $(HOME)/.ssh/identities/*/config.tpl)
CONFIGS := $(subst .tpl,,$(TEMPLATES))

%: %.tpl
	"$(WORKON_HOME)/concierge/bin/concierge-check" -u mako -s "$<" -o "$@"

$(HOME)/.ssh/config: $(CONFIGS)
	grep -h "^Host" $^ | grep -v "*" > $@
config: $(HOME)/.ssh/config

clean:
	rm -f $(HOME)/.ssh/config $(CONFIGS)

No more SSII

jeudi 21 juillet 2016 à 00:00

Note préliminaire :

Cet article avait initialement été rédigé au tout début du mois de février 2016, durant un burn-out qui a conduit à mon départ de la société en question, puisque je n’y était plus du tout à ma place comme vous allez pouvoir le constater. Je n’ai quasiment pas touché au texte, hormis retirer des passages trop « violents » après relecture à froid.

Cet article n’a pas vocation à jeter la pierre à mon ancienne société (malgré les quelques mois difficiles sur la fin, dus aux conditions de travail décrites ci-dessous et au burn-out qui se mettait en place). Elle m’a quand même apporté beaucoup de choses au final et reste très loin des SSII « pures et dures » qui peuvent défrayer l’actualité du domaine.

Ce texte a plutôt pour vocation à éclairer les futurs candidats sur les conditions qu’on peut rencontrer dans une SSII. Comme beaucoup maintenant, je considère qu’une SSII est un bon tremplin pour un jeune diplômé, mais qu’il faut aussi savoir tourner la page quand on sent que ça peut déraper.
La diversité des domaines permet de monter en compétence rapidement et de toucher à plein de sujets et de technos différentes, chose qu’il est beaucoup plus difficile de réaliser sur un poste fixe dans une société standard où votre poste n’évoluera que très peu. Mais les conditions de travail n’y sont pas toujours très roses, et il faut en avoir conscience, surtout si comme moi on a des difficultés à ne pas pouvoir faire son travail correctement (au sens de l’état de l’art).


Il était une fois la vie… d’un petit développeur !

2008. Encore en école d’ingénieur, je dois trouver un stage pour valider mon diplôme. Déjà à cette époque les SSII (devenues ESN par la suite) n’ont pas la cote, mais je fais quand même le choix d’en intégrer une petite de 200 personnes, très familiale, très axé industrie (ferroviaire, aéronautique, énergie…). L’expérience est plus que concluante et positive, et j’enchaîne donc avec un CDI avant même d’avoir terminé mon stage.

J’enchaîne immédiatement sur des projets intéressants, et cette société s’avère même être plutôt en avance technologique et technique sur son époque. La méthode Scrum n’est connue que depuis 4 ans, mais on a déjà en place en interne des méthodes agiles (daily meeting, peer review, backlog…), alors même qu’on continue à vendre du cycle en V aux clients en externe. Hudson (devenu Jenkins) date de 2005 mais est déjà déployé sur pas mal de projets, alors même qu’il n’est pas facturé explicitement aux clients.

On a de la relecture de code plus ou moins obligatoire, des tests unitaires avec une bonne couverture de code. On travaille sur des projets de grands envergures (équipes de 10 personnes et plus, 300, 500, 1000 voire 1200 jours-hommes…) et des technos intéressantes. On fait confiance aux équipes (fraîchement sorti de mon stage, ma hiérarchie a osé me faire confiance et j’ai pris le lead sur certains morceaux d’architecture de projets critiques), on a du matériel correct pour travailler.

Ça n’a certes pas empècher quelques projets de déraper (j’ai souvenir d’un soft qui avait régressé à 99% juste pour avoir chercher à inverser 2 couleurs dans les fichiers de sortie générés) et d’autres d’être difficile au niveau de la relation client, mais globalement les équipes techniques ont les moyens et le temps de faire leur travail correctement.

2012. L’année charnière. La crise est passée par là (même si elle a à mon avis bon dos), les relations clients deviennent plus difficiles. Les budgets ne sont plus là, les délais non plus. Les projets continuent à rentrer mais la concurrence entre SSII fait que les prix sont tirés vers le bas, chacun voulant rentrer des projets et les volent aux autres en massacrant les prix. Les clients n’ont plus les yeux en face des trous et en tout cas des moyens totalement décalés par rapport à leurs besoins.

Les clients ont commencé à inventer de nouveaux modes de gestion de projet, avec des notions de « POC (preuve de concept) » (comprendre du code kleenex que si ça marche on garde, si ça ne marche pas on jette, et que dans les 2 cas, on cherche à ne pas payer le fournisseur quand même), de « challenge » (comprendre qu’on sait que c’est infaisable ce qu’on demande mais que tu pourrais quand même faire un effort quoi).

Les méthodes agiles ont aussi eu le malheur de devenir le buzz-word du moment, et du coup sont apparues dans les plaquettes et propositions commerciales. Sauf qu’elles sont devenues la justification pour faire n’importe quoi, comme ne plus du tout faire de spécification, supprimer les tests unitaires ou la revue de code, ou encore livrer n’importe quoi très vite.

Les développeurs se retrouvent du coup avec des projets sous-vendus (50 à 100 jours max) et une qualité technique massacrée. L’intégration continue a disparu des écrans radars, tout comme la revue de code. Les équipes ne sont plus formées, le niveau technique global chute drastiquement, les « têtes » qui tenaient les projets à bouts de bras partent. Les chefs de projet senior sont remplacés par des juniors qui n’ont de chef ni la formation ni la volonté ni le charisme nécessaire pour tenir tête aux commerciaux et à la direction. Il n’y a même plus vraiment d’équipe projet, ou alors se résumant à un développeur débutant encadré par un développeur à peine plus senior propulsé chef de projet, tout étant dorénavant géré par les commerciaux en pratique.

2015. Le coup de grâce. Les buzz-words sont de retour. « Big-data », « cloud », « IoT », « cyber-sécurité ». Plus aucun appel d’offre ne sort sans en contenir plusieurs sinon tous. On fait n’importe quoi, et cette fois non seulement d’un point de vue commercial et technique, mais aussi, et c’est encore plus grave, d’un point de vue éthique. Les projets qui dépassent les 10 jours-hommes se comptent sur les doigts d’une main et ceux avec plus de 2 personnes affectées dessus aussi.

Trop c’est trop

Vous l’aurez compris, je ne me sens dorénavant plus à ma place. Je n’ai plus le temps ni les ressources pour faire correctement ce que j’estime être mon travail.

Techniquement, je n’ai plus le temps de faire de revue de code, de former les nouveaux arrivants ou de mettre en place les outils pour faire du code de qualité, parce que les commerciaux gardent la main sur l’ensemble des manettes et ont transformé les ingénieurs en techniciens voire en ouvriers. Pas étonnant que les Chinois ou les Indiens grignotent de plus en plus le marché, ils coûtent 10× moins chers pour une médiocrité équivalente.

Éthiquement, les projets réalisés sont de plus en plus en contradiction avec mes idéaux, avec des objets connectés où la sécurité est bien le dernier des soucis et où la vie privée n’existe plus, même en option. Parce que le discours commercial ambiant est en contradiction totale avec les nécessités techniques, surtout dans une ère post-Snowden.

Humainement parlant, les délais sont intenables, le stress élevé, la pression aussi. Les moyens ne sont plus suffisants pour travailler, j’en arrive même à devoir héberger des projets sur mes propres machines parce que ceux qui devaient faire cette partie ne veulent/peuvent/savent pas le faire, faute de moyens et de temps. Parce que la rentabilité ne peut être maintenue qu’en sur-exploitant le personnel.

J’ai déjà plusieurs fois alerté ma hiérarchie sur les problèmes rencontrés, ça a conduit au mieux à tenter de me retourner les neurones à coups de justification bullshitto-marketo-commercialo-financier, en général à aucune action sérieuse qui suit et au pire à des prises de bec de plus en plus violente.

J’ai aussi eu le droit à quelques remarques, du style que j’étais trop pessimiste ou trop utopiste. Pour le côté pessimiste, comme dit le proverbe, les pessimistes sont des optimistes avec de l’expérience, et malheureusement ma vie personnelle aussi bien que professionnelle n’a que trop rarement contredit ce fait. Pour le côté utopiste, le fait que je développe beaucoup de logiciel libre sur mon temps libre montre qu’il est parfaitement possible de faire du code propre avec des méthodes propres sans que ça ne coûte plus cher ou nécessite de délai supplémentaire, voire parfois le contraire. De plus, je ne demande pas non plus la perfection, juste de pouvoir faire ce que l’état de l’art actuel réclame : un peu de revue de code, de l’intégration continue, des tests unitaires, utiliser des bibliothèques, réfléchir a minima à une architecture, assurer une formation minimale des équipes… Mais c’est effectivement souvent en contradiction avec les impératifs commerciaux ou financiers…

Le travail en SSII est aussi un coup de roulette russe permanent, une épée de Damoclès. Si vous avez la chance de réussir à rester plus ou moins sédentaire au sein du bureau d’études pour réaliser les projets forfaitisés, vous pouvez contrôler a minima l’impact de votre vie professionnelle sur votre vie personnelle.
Mais si vous avez le malheur d’être envoyé en mission, parfois à l’autre bout de l’Île-de-France, vous pouvez être baladé tous les X temps (X variant de 3 mois à 10 ans) d’un client à l’autre, d’un lieu à l’autre, dans des conditions qui en plus sont théoriquement illégales au vu du Code du Travail (délit de marchandage et prêt de main d’œuvre illicite).
Si vous êtes parmi les chanceux du premier groupe, vous n’avez en plus pas intérêt à devenir le petit caillou dans la chaussure du service, sinon le second groupe vous tend rapidement les bras. Comme on dit, en SSII, le licenciement n’existe pas, on appelle ça « partir en mission »… :)
Dans mon cas, mon dernier départ en mission a aussi été synonyme du sacrifice de ma vie associative et collaborative. Il est en effet bien difficile de trouver du temps libre et de la motivation pour travailler sur des projets personnels quand vous faites 8h-20h « effectif » tous les jours, dont 3h de transport dans les bouchons (non comptés dans les temps de travail, merci la SYNTEC !). C’est à mon avis ce sacrifice qui a accéléré ma décision de partir.

Le temps du changement ?

Aujourd’hui, je n’ai plus réellement de points d’attache en lien direct avec cette société. J’y reste parce que c’est près de chez moi, parce que les collègues sont sympas, parce que j’ai le temps de travailler à l’extérieur sur des projets qui eux me tiennent à cœur et que je n’ai pas honte de publier. Alors que je préférerais y rester parce que les technos utilisés sont intéressantes, parce qu’on y fait de la qualité logicielle ou de la gestion de projet réellement agile. Et dans tous les cas, la situation tire fortement sur un truc où je n’y aurais plus du tout ma place, un projet vraiment pas éthique ou un gros projet bien bordélique de passage achèverait de m’achever.

Le temps pour moi d’aller découvrir le reste du monde ?

OVH - Installer une machine FreeBSD avec root ZFS raidZ

mardi 19 juillet 2016 à 00:00

Un petit billet rapide pour expliquer comment installer un serveur dédié sous FreeBSD avec votre partition racine sur un raidZ ZFS. En effet, par défaut, l’installeur automatique proposé par OVH permet un root ZFS, mais le raid est en raid1, ce qui réduit drastiquement l’espace disque final (avec 3 disques de 4To, vous obtenez 4To utilisables en raid1, contre 8To en raidz).

Pour installer votre système en raidZ, il faut d’abord faire l’installation « à l’arrache » via l’installeur OVH, puis rebooter le serveur sur le mode rescue, et refaire l’installation à la main avec la procédure suivante.

On commence par dézinguer tous les anciens pools ZFS :

zpool import -fR /mnt zroot
zpool destroy zroot

On crée ensuite les partitions qui vont bien aller :

On en profite pour installe le bootloader GPT au passage.
(Le script suivant considère que vous utilisez 3 disques, adaptez-le en conséquence)

foreach n ( 0 1 2 )
    gpart destroy -F ada${n}
    gpart create -s gpt ada${n}

    gpart add -a 4k -s 512k -t freebsd-boot -l boot${n} ada${n}
    gpart add -a 4k -s 2g -t freebsd-swap -l swap${n} ada${n}
    gpart add -a 4k -t freebsd-zfs -l disk${n} ada${n}

    gpart bootcode -b /boot/pmbr -p /boot/gptzfsboot -i 1 ada${n}

    gnop create -S 4096 /dev/gpt/disk${n}
end

On créé ensuite le pool ZFS en raidZ, avec les 3 disques formatés :

zpool create -o altroot=/mnt -O canmount=off -o compression=lz4 -o checksum=fletcher4 -o atime=off -m none zroot raidz /dev/gpt/disk0 /dev/gpt/disk1 /dev/gpt/disk2

zfs create -o mountpoint=none -o quota=10G zroot/ROOT
zfs create -o mountpoint=/ zroot/ROOT/default
zfs create -o mountpoint=/tmp -o quota=2G -o setuid=off zroot/tmp

Ensuite, on installe le futur système sur le pool nouvellement créé :

chmod 1777 /mnt/tmp

zpool set bootfs=zroot/ROOT/default zroot

set FREEBSD_VERSION = 10.3-RELEASE
foreach file ( base kernel lib32 )
wget -O - ftp://ftp.freebsd.org/pub/FreeBSD/releases/amd64/${FREEBSD_VERSION}/${file}.txz | tar -Upxf - -C /mnt
end

cat > /mnt/etc/fstab <<EOF
/dev/gpt/swap0                 none                    swap    sw              0       0
/dev/gpt/swap1                 none                    swap    sw              0       0
/dev/gpt/swap2                 none                    swap    sw              0       0
EOF

echo zfs_load="YES" >> /mnt/boot/loader.conf

sed -i "" "s/^#PermitRootLogin .*/PermitRootLogin prohibit-password/" /mnt/etc/ssh/sshd_config

cat > /mnt/etc/rc.conf <<EOF
hostname="pony.example.org"

ifconfig_igb0="inet X.X.X.X netmask 255.255.255.0 broadcast X.X.X.255"
defaultrouter="X.X.X.254"

ifconfig_igb0_ipv6="inet6 XXX:XXX:XXX:XXXX:: prefixlen 64 accept_rtadv no_radr"
ipv6_network_interfaces="igb0"
ipv6_default_interface="igb0"
ipv6_defaultrouter="XXXX:XXXX:XXXX:XXff:ff:ff:ff:ff"
ipv6_route_ovhgw="XXXX:XXXX:XXXX:XXff:ff:ff:ff:ff -prefixlen 128 -interface igb0"
ipv6_static_routes="ovhgw"

dumpdev="AUTO"
clear_tmp_enable="YES"
accounting_enable="YES"

unbound_enable="NO"
sshd_enable="YES"
ntpd_enable="YES"
postfix_enable="YES"
zfs_enable="YES"
EOF

mkdir -p /mnt/root/.ssh/
cat > /mnt/root/.ssh/authorized_keys <<EOF
<vos clefs SSH qui va bien>
EOF

cat > /mnt/etc/resolv.conf <<EOF
domain example.org
search example.org
nameserver 213.186.33.99
EOF

chroot /mnt passwd

On repasse le serveur en boot sur le disque, et on lance un reboot. Normalement, votre nouvelle machine devrait prendre vie rapidement !

En cas de soucis, vous pouvez toujours repasser en mode rescue, et remonter votre pool pour travailler dessus :

zpool import -fR /mnt zroot