La rapidité et la simplicité qu’apportent les fournisseurs de cloud public en terme de déploiement applicatif n’est plus à démontrer. Il s’agit bien souvent de l’argument principal à leur adoption. Cependant, il reste encore de nombreux cas où l’utilisation d’un cloud public n’est pas envisageable. Que ce soit pour des questions de souveraineté de la donnée (hébergement de données médicales, bancaires, etc...), ou tout autre contrainte propre à une entreprise. Dans ce genre de cas il est nécessaire de disposer et d’exploiter ses propres infrastructures. La question qui se pose alors est de savoir comment atteindre un niveau de réactivité et de rapidité de déploiement, comparable à ce que l’on peut avoir sur du cloud public.
Nous allons prendre le cas simple d’une application web et montrer comment déployer on-premise, avec ansible :
NB: les configurations présentées ici sont insuffisantes pour un déploiement en production, elles n’intègrent notamment aucun élément lié à la haute disponibilité. Il s’agit d’un exemple, qu’il convient d’étendre et d’adapter pour un déploiement sérieux.
Pour arriver à nos fins, nous allons utiliser les outils suivants:
Partons du postulat que notre application ne dispose même pas de nom de domaine. Comment automatiser également le processus de création de ce nom de domaine ? Si votre registrar est gandi.net, une API existe.
Pour commencer il vous faut générer une clef d’API. Dans votre interface d’administration Gandi, rendez-vous sur la fiche descriptive de votre compte, puis:
Notez qu’il est possible, via l’interface d’administration, de restreindre l’accès à l’API par adresse IP, pour un peu plus de sécurité.
Une fois votre clef récupérée, il est possible d’automatiser la réservation de votre nom de domaine en utilisant gandi cli.
Exemple:
gandi setup #saisissez votre clef d’API
gandi domain create --domain mydomain.com --duration 1
Pour permettre à notre serveur DNS d’être le nameserver principal du domaine, il est nécessaire d’éditer les glue records. Je n’ai pas trouvé comment le faire avec gandi cli, mais ceci est réalisable sur l’interface d’administration et le sera certainement prochainement par l’API.
Première étape: déployer notre serveur DNS et notre zone avec ansible. Pour ce faire, j’ai repris un rôle ansible existant, le résultat est disponible ici.
On appelle le rôle de cette manière, dans un playbook :
- hosts: dns
roles:
- knot
Et la configuration s’écrit comme suit dans les host_vars :
############################
## knot DNS configuration
knot_user: knot
knot_group: knot
knot_interfaces:
- 127.0.0.1
- ""
knot_zones:
- { name: 'mydomain.com', storage: "/etc/knot/zones", file: "mydomain.com.zone", src_file: 'files/mydomain.com.zone' }
La dernière ligne de configuration permet de déployer le fichier de zone attaché à notre nom de domaine. Voici à quoi ressemble le fichier de zone en question (remplacer l’adresse IP de l’entrée A par l’adresse IP publique par laquelle le proxy est accessible) :
$TTL 1
$ORIGIN mydomain.com.
mydomain.com. 600 IN SOA ns1.mydomain.com. (
hostmaster.mydomain.com.
2018082601
86400
7200
604800
86400
)
$TTL 86400
@ NS ns1.mydomain.com.
mysuperapp A 192.0.2.3
Une fois notre serveur dns et notre zone configurée, installons et configurons le proxy.
Nous allons également utiliser un role ansible pour configurer notre serveur proxy. Voici celui que j’ai utilisé: https://github.com/WeScale/Stouts.nginx. Ce rôle permet l’installation et la configuration de base de nginx, mais aussi de créer des vhosts basés sur des templates personnalisés. On écrit donc le template (externe au rôle) qui convient pour notre application et qui comprend la configuration nécessaire à la validation et à la récupération de notre futur certificat TLS (le dossier .well-known/acme-challenge) :
server {
listen 80;
listen [::]:80;
server_name {{item.name}} ;
access_log /var/log/nginx/{{item.name}}_access.log;
error_log /var/log/nginx/{{item.name}}_error.log;
root /var/www/{{item.name}};
location / {
rewrite ^/(.*)$ https://$host$request_uri;
}
location /.well-known/acme-challenge/ {
alias /var/www/{{item.name}}/.well-known/acme-challenge/;
allow all;
}
}
server {
listen 443;
listen [::]:443;
server_name {{item.name}};
access_log /var/log/nginx/{{item.name}}_access.log;
error_log /var/log/nginx/{{item.name}}_error.log;
ssl on;
ssl_certificate /{{item.name}}/{{item.name}}.crt;
ssl_certificate_key /{{item.name}}/{{item.name}}.key;
ssl_protocols TLSv1.1 TLSv1.2;
ssl_ciphers kEECDH+ECDSA:kEECDH:kEDH:HIGH:SHA256:!RC4:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!DSS:!PSK:!SRP:!CAMELLIA;
ssl_prefer_server_ciphers on;
location /.well-known/acme-challenge/ {
alias /var/www/{{item.name}}/.well-known/acme-challenge/;
allow all;
}
root /{{item.name}};
}
Et on configure le rôle comme suit, dans les host_vars :
domains:
- { "name": "mysuperapp.mydomain.com", "renew": True, "template": "templates/nginx_vhost.j2" }
nginx_templatized_vhosts: ""
Avant de faire appel à letsencrypt, il est préférable de fournir à nginx des certificats tls utilisables, pour valider le bon fonctionnement du vhost. De plus, puisque nous allons utiliser la validation par http pour letsencrypt, nous avons besoins que ce vhost soit fonctionnel pour permettre le processus de validation. Notez que, puisque nous avons la maîtrise des entrées DNS de notre domaine, nous pourrions également effectuer une validation par entrée DNS.
Voici donc le playbook de déploiement de nginx (toujours en utilisant les host_vars que nous avons définis), concentrons-nous sur les pre_tasks :
- hosts: proxy
pre_tasks:
- name: ensure web directories exist
file:
state: directory
path: "/{{item.name}}"
owner: www-data
group: www-data
with_items:
- ""
- name: ensure letsencrypt paths exists
file:
state: directory
path: "/{{item.name}}"
with_items:
- ""
- name: prepare tls private keys
shell: "! [ -e /{{item.name}}/{{item.name}}.key ] && openssl genrsa -out /{{item.name}}/{{item.name}}.key 4096"
with_items:
- ""
ignore_errors: True
- name: prepare account keys
shell: "! [ -e /{{item.name}}/{{item.name}}_account.key ] && openssl genrsa -out /{{item.name}}/{{item.name}}_account.key 4096"
with_items:
- ""
ignore_errors: True
- name: prepare tls CSRs
shell: "openssl req -new -key /{{item.name}}/{{item.name}}.key -subj \"/C=FR/ST=IDF/L=Paris/O=mydomain.com/OU=Hosting services/CN={{item.name}}\" -out /{{item.name}}/{{item.name}}.csr"
when: "item.renew is defined and item.renew"
with_items:
- ""
- name: generate auto-signed certificate if it doesn't exist
shell: "! [ -e /{{item.name}}/{{item.name}}.crt ] && openssl x509 -req -days 365 -in /{{item.name}}/{{item.name}}.csr -signkey /{{item.name}}/{{item.name}}.key -out /{{item.name}}/{{item.name}}.crt"
with_items:
- ""
ignore_errors: True
roles:
- nginx
post_tasks:
- service:
name: nginx
state: reloaded
Nous nous assurons ici, de créer un certificat CA et un certificat TLS auto signés, qui ne seront pas valables aux yeux des clients, mais permettront de faire fonctionner le vhost lors de son déploiement initial et ainsi servir en http les fichiers nécessaires à letsencrypt (voir le bloc location “location /.well-known/acme-challenge/” dans le template du vhost) pour la validation du vrai certificat.
Une fois le serveur DNS et le proxy configurés il nous faut récupérer le certificat de la part de letsencrypt. Voici un schéma du déroulement de la procédure :
Dans un premier temps nous enverrons une requête à Letsencrypt pour s’authentifier et demander un challenge pour notre nom de domaine. On copie les données du challenge dans un dossier desservi par notre proxy. Puisque l’entrée DNS de notre application est accessible, le service pourra alors accéder de nouveau à notre serveur pour vérifier la présence et le bon contenu du fichier de validation. Si le challenge est validé nous récupérerons le certificat TLS final.
Voici le rôle ansible utilisé: https://github.com/WeScale/ansible-role-letsencrypt.
Ce rôle utilise également le dictionnaire “domains” de notre fichier host_vars, en l’assignant de cette manière :
letsencrypt_domains: “”
Voyons un peu plus en détail son fonctionnement, en regardant le playbook generate.yml :
---
# tasks file for letsencrypt
- name: ensure well-known directory exists
file:
path: "/{{item.name}}/.well-known/acme-challenge"
state: directory
recurse: yes
owner: "www-data"
group: "www-data"
- letsencrypt:
account_key: "/{{item.name}}/{{item.name}}_account.key"
csr: "/{{item.name}}/{{item.name}}.csr"
dest: "/{{item.name}}/{{item.name}}.crt"
agreement: "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf"
account_email: ""
remaining_days: "{{item.remaining_days}}"
acme_directory: "https://acme-v01.api.letsencrypt.org/directory"
register: le_challenge
when: "item.renew is defined and item.renew"
- debug:
var: le_challenge
- copy:
dest: "/{{item.name}}/"
content: ""
owner: www-data
group: www-data
when: le_challenge|changed
- letsencrypt:
account_key: "/{{item.name}}/{{item.name}}_account.key"
csr: "/{{item.name}}/{{item.name}}.csr"
dest: "/{{item.name}}/{{item.name}}.crt"
data: " "
agreement: "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf"
account_email: ""
acme_directory: "https://acme-v01.api.letsencrypt.org/directory"
register: result
when: "item.renew is defined and item.renew and le_challenge|changed"
Nous utilisons ici le module ansible pour letsencrypt. La validation se déroule en trois phases :
Il est nécessaire, pour une bonne configuration https, y inclure le certificat intermédiaire et le root CA letsencrypt. Le playbook intermediate.yml est prévu à cet effet :
- name: get the intermediate certificate
get_url:
url: ""
dest: "/{{item.name}}/{{item.name}}.intermediate.crt"
- name: read the intermediate certificate file
shell: cat "/{{item.name}}/{{item.name}}.intermediate.crt"
register: file_content
- name: add intermediate certificate in the certificate file
blockinfile:
path: "/{{item.name}}/{{item.name}}.crt"
block: |
marker: ""
- name: get the root certificate
get_url:
url: ""
dest: "/{{item.name}}/{{item.name}}.root.crt"
- name: read the root certificate file
shell: cat "/{{item.name}}/{{item.name}}.root.crt"
register: file_content
- name: add root certificate in the certificate file
blockinfile:
path: "/{{item.name}}/{{item.name}}.crt"
block: |
marker: ""
Plusieurs éléments importants sont à prendre en compte concernant l’utilisation de ce module letsencrypt.
Premièrement, le paramètre acme_directory
désigne le service letsencrypt ciblé, dans le rôle il est mis à https://acme-v01.api.letsencrypt.org/directory qui est le service de production (qui délivre les certificats signés). Le nombre d’appel à ce service est limité à quelques demandes par mois pour un même domaine. Pour effectuer vos tests, je vous recommande donc de ne pas saisir ce paramètre et de laisser la valeur par défaut proposée par le module, qui cible le service de test de letsencrypt. Les certificats délivrés ne sont alors pas valides du point de vue du navigateur, car non signés par l’autorité de certification, mais vous pourrez effectuer autant d’appels et de processus de validation que souhaité pour tester votre configuration.
Deuxièmement, la valeur par défaut du paramètre agreement
pointe sur une version obsolète du document, celle-ci est mise à jour au niveau du rôle, mais attention si vous utilisez le module en dehors, les appels à l’API ne fonctionneront pas si vous n’en changez pas la valeur.
Pour gérer l’installation des rôles dans votre projet, il est possible de déclarer ces dépendances dans un fichier requirements.yml (à l’instar de pip) que ansible pourra interpréter. Voici un exemple correspondant à notre cas :
- src: https://github.com/bpetit/ansible-role-knotauth.git
name: knot
- src: https://github.com/bpetit/Stouts.nginx
name: nginx
- src: https://github.com/bpetit/ansible-role-letsencrypt
name: letsencrypt
Vous pourrez ainsi installer les dépendances via la commande :
ansible-galaxy install -r requirements.yml
Et appeler les rôles avec le nom définit par le champ name.
Nous avons montré comment permettre, avec ansible, le déploiement de l’environnement nécessaire à une application web destinée à être exposée sur Internet: proxy, configuration DNS et terminaison TLS. Cet exemple de configuration est fonctionnel mais nécessite d’être étendu pour gérer la haute disponibilité de l’application. Notez également que la validation letsencrypt par http fonctionne uniquement si l’application est exposée sur Internet. Pour une application interne à votre entreprise par exemple, il est nécessaire de passer par la validation à l’aide d’une entrée DNS prévue à cet effet.
La plus-value de cette configuration est visible une fois que tout est en place: on peut ajouter une entrée DNS, un vhost et un certificat pour une nouvelle application, simplement en éditant les host_vars et en relançant ansible-playbook.