Automatisation: Gérer vos certificat SSL (4/4)
Bon, Nous y sommes.
Nous allons pousser plus loin cette automatisation.
Je garde tout de même la main sur quand je vais déployer mon playbook, puisque je reçois par mail, 30 jours avant, une alerte.
Faisons le bilan.
Nous avons 2 scripts (renommons-les)
tmp2 => check-cert, qui vérifie la date d'expiration des certificats tous les 5 jours via une tache cron et envoi un mail pour les certificats concernés.
tmp3 => manage, qui permet de :
1 - Vérifier l'enregistrement d'un domaine ou d'un sous domaine
2 - Vérifier la date d'expiration des certificats
3 - Générer un certificat Let's encrypt
4 - Générer un certificat OpenSSL
5 - Révoquer un certificat
6 - Supprimer un certificat OpenSSL
Ce deuxième script est surtout pour de l'administration, de l'urgence (révoquer un certificat), ou en créer de nouveau dans le cas d'un nouveau service à mettre en place (le playbook à mettre en place fait partie de cette étape)
Nous ne verrons pas la mise en place d'un nouveau service web mais le playbook sera sensiblement le même, il suffit d'utiliser exclusivement ceci :
---
- name: Fetching toto certificat from cert server
ansible.builtin.fetch:
run_once: true
src: "{{ item.src }}"
dest: "{{ item.dest }}"
flat: true
with_items:
- { src: "/etc/letsencrypt/archive/toto.fast-reload.com/fullchain1.pem", dest: "/tmp/toto/fullchain1.pem" }
- { src: "/etc/letsencrypt/archive/toto.fast-reload.com/privkey1.pem", dest: "/tmp/toto/privkey1.pem" }
when: inventory_hostname == 'cert'
- name: Copying new certificat
ansible.builtin.copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
owner: root
group: root
mode: 0777
with_items:
- { src: "/tmp/toto/fullchain1.pem", dest: "/etc/letsencrypt/live/toto.fast-reload.com/fullchain.pem" }
- { src: "/tmp/toto/privkey1.pem", dest: "/etc/letsencrypt/live/toto.fast-reload.com/privkey.pem" }
when: inventory_hostname == 'toto'
# Not required
- name: Restart service apache2, in all cases
ansible.builtin.service:
name: apache2
state: restarted
when: inventory_hostname == 'toto'
Le dérouler de la CI (mise à jour du certificat) est simple, elle reprend en grande partie ce que nous avons fait dans nos script bash.
- Ouvrir les ports 80 & 443
- Révoquer le certificat
- Générer un nouveau certificat
- Fermer les ports 80 & 443
- Sur le serveur cible, supprimer le vieux certificat
- Sur le serveur de certificat, récupérer le nouveau certificat
- Copier le certificat sur le serveur cible
- Et (non requis) redémarrer le service apache (dans l'exemple)
Ci-dessous une CI simple, qui reprend tout ce que l'on viens de lister.
CI à laquelle il faudra sans doute créer des variables car mettre les domaines en dur cela risque d'être pénible si vous avez plusieurs certificats à gérer.
Ceci s’exécute en quelques secondes, donc la coupure du service est extrêmement limité, un visiteur sur votre site en train de lire une page ne s'en rendrait même pas compte.
Vous noterez la première tâche et les 2 dernières.
Pour faire simple on récupère les dates d'expiration avant et après mais sur le serveur web
sachant que le certificat sera renouvelé sur le serveur cert et,
en dernière tâche on affiche les deux variables stockées.
---
- name: Fetch cert date Before ...
ansible.builtin.shell: openssl x509 -enddate -noout -in /etc/letsencrypt/live/toto.fast-reload.com/cert.pem | sed 's/notAfter=//'
register: cert_before
when: inventory_hostname == 'toto'
- name: Allow IPTABLES connections ...
ansible.builtin.iptables:
chain: "{{ item.chain }}"
protocol: "{{ item.proto }}"
destination_port: "{{ item.port }}"
jump: ACCEPT
with_items:
- { chain: "INPUT", proto: "tcp", port: "80" }
- { chain: "INPUT", proto: "tcp", port: "443" }
- { chain: "OUTPUT", proto: "tcp", port: "80" }
- { chain: "OUTPUT", proto: "tcp", port: "443" }
when: inventory_hostname == 'cert'
- name: Revoke old certificat ...
ansible.builtin.shell: certbot revoke --cert-name "toto.fast-reload.com" --non-interactive
when: inventory_hostname == 'cert'
- name: Generate new certificat
ansible.builtin.shell: certbot certonly --standalone -d toto.fast-reload.com --agree-tos --non-interactive --email mon.super@mail.amoi > /dev/null
when: inventory_hostname == 'cert'
- name: Remove IPTABLES connections ...
ansible.builtin.iptables:
chain: "{{ item.chain }}"
protocol: "{{ item.proto }}"
destination_port: "{{ item.port }}"
jump: ACCEPT
state: absent
with_items:
- { chain: "INPUT", proto: "tcp", port: "80" }
- { chain: "INPUT", proto: "tcp", port: "443" }
- { chain: "OUTPUT", proto: "tcp", port: "80" }
- { chain: "OUTPUT", proto: "tcp", port: "443" }
when: inventory_hostname == 'cert'
- name: Remove old certificat ...
ansible.builtin.file:
path: "{{ item.dest }}"
state: absent
with_items:
- { dest: "/etc/letsencrypt/live/toto.fast-reload.com/fullchain.pem" }
- { dest: "/etc/letsencrypt/live/toto.fast-reload.com/privkey.pem" }
when: inventory_hostname == 'toto'
- name: Fetching toto certificat from cert server ...
ansible.builtin.fetch:
run_once: true
src: "{{ item.src }}"
dest: "{{ item.dest }}"
flat: true
with_items:
- { src: "/etc/letsencrypt/archive/toto.fast-reload.com/fullchain1.pem", dest: "/tmp/toto/fullchain1.pem" }
- { src: "/etc/letsencrypt/archive/toto.fast-reload.com/privkey1.pem", dest: "/tmp/toto/privkey1.pem" }
when: inventory_hostname == 'cert'
- name: Copying new certificat ...
ansible.builtin.copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
owner: root
group: root
mode: 0777
with_items:
- { src: "/tmp/toto/fullchain1.pem", dest: "/etc/letsencrypt/live/toto.fast-reload.com/fullchain.pem" }
- { src: "/tmp/toto/privkey1.pem", dest: "/etc/letsencrypt/live/toto.fast-reload.com/privkey.pem" }
when: inventory_hostname == 'toto'
# Not required
- name: Restart service apache2, in all cases ...
ansible.builtin.service:
name: apache2
state: restarted
when: inventory_hostname == 'toto'
- name: Fetch cert date after ...
ansible.builtin.shell: openssl x509 -enddate -noout -in /etc/letsencrypt/live/toto.fast-reload.com/cert.pem | sed 's/notAfter=//'
register: cert_after
when: inventory_hostname == 'toto'
- name: Show Result cert date before & after ...
ansible.builtin.debug:
msg:
- "Expire date before: {{ cert_before.stdout }}"
- "Expire date after: {{ cert_after.stdout }}"
when: inventory_hostname == 'toto'
il ne vous reste plus qu'à "variabiliser" vos path et domaines,
Préparer vos job .gitlab-ci.yml, par exemple :
variables:
LIMIT: "--limit lab_install"
stages:
- certificat
certificat_update:
stage: certificat
image: $CI_REGISTRY_IMAGE/ansible:$VERSION # mon runner est une image
script: |
ansible-playbook -i env/inventory $LIMIT certificat.yml
when: manual
C'est pas plus compliqué que cela pour la base.
A vous de jouer si vous souhaitez rajouter des rules par exemple.
Ci-dessous le résultat de notre labeur.
(le certificat venait juste d'être générer, ce qui explique la faible différence entre before et after sur la dernière tâche)
PLAY [Lab install] *************************************************************
TASK [Gathering Facts] *********************************************************
[WARNING]: Platform linux on host cert is using the discovered Python
interpreter at /usr/bin/python3.11, but future installation of another Python
interpreter could change the meaning of that path. See
https://docs.ansible.com/ansible-
core/2.16/reference_appendices/interpreter_discovery.html for more information.
ok: [toto]
ok: [cert]
TASK [certificat : Fetch cert date Before ...] *********************************
skipping: [cert]
changed: [toto]
TASK [certificat : Allow IPTABLES connections ...] *****************************
skipping: [toto] => (item={'chain': 'INPUT', 'proto': 'tcp', 'port': '80'})
skipping: [toto] => (item={'chain': 'INPUT', 'proto': 'tcp', 'port': '443'})
skipping: [toto] => (item={'chain': 'OUTPUT', 'proto': 'tcp', 'port': '80'})
skipping: [toto] => (item={'chain': 'OUTPUT', 'proto': 'tcp', 'port': '443'})
skipping: [toto]
changed: [cert] => (item={'chain': 'INPUT', 'proto': 'tcp', 'port': '80'})
changed: [cert] => (item={'chain': 'INPUT', 'proto': 'tcp', 'port': '443'})
changed: [cert] => (item={'chain': 'OUTPUT', 'proto': 'tcp', 'port': '80'})
changed: [cert] => (item={'chain': 'OUTPUT', 'proto': 'tcp', 'port': '443'})
TASK [certificat : Revoke old certificat ...] **********************************
skipping: [toto]
changed: [cert]
TASK [certificat : Generate new certificat] ************************************
skipping: [toto]
changed: [cert]
TASK [certificat : Remove IPTABLES connections ...] ****************************
skipping: [toto] => (item={'chain': 'INPUT', 'proto': 'tcp', 'port': '80'})
skipping: [toto] => (item={'chain': 'INPUT', 'proto': 'tcp', 'port': '443'})
skipping: [toto] => (item={'chain': 'OUTPUT', 'proto': 'tcp', 'port': '80'})
skipping: [toto] => (item={'chain': 'OUTPUT', 'proto': 'tcp', 'port': '443'})
skipping: [toto]
changed: [cert] => (item={'chain': 'INPUT', 'proto': 'tcp', 'port': '80'})
changed: [cert] => (item={'chain': 'INPUT', 'proto': 'tcp', 'port': '443'})
changed: [cert] => (item={'chain': 'OUTPUT', 'proto': 'tcp', 'port': '80'})
changed: [cert] => (item={'chain': 'OUTPUT', 'proto': 'tcp', 'port': '443'})
TASK [certificat : Remove old certificat ...] **********************************
skipping: [cert] => (item={'dest': '/etc/letsencrypt/live/toto.fast-reload.com/fullchain.pem'})
skipping: [cert] => (item={'dest': '/etc/letsencrypt/live/toto.fast-reload.com/privkey.pem'})
skipping: [cert]
changed: [toto] => (item={'dest': '/etc/letsencrypt/live/toto.fast-reload.com/fullchain.pem'})
changed: [toto] => (item={'dest': '/etc/letsencrypt/live/toto.fast-reload.com/privkey.pem'})
TASK [certificat : Fetching toto certificat from cert server ...] **************
skipping: [toto] => (item={'src': '/etc/letsencrypt/archive/toto.fast-reload.com/fullchain1.pem', 'dest': '/tmp/toto/fullchain1.pem'})
skipping: [toto] => (item={'src': '/etc/letsencrypt/archive/toto.fast-reload.com/privkey1.pem', 'dest': '/tmp/toto/privkey1.pem'})
skipping: [toto]
changed: [cert] => (item={'src': '/etc/letsencrypt/archive/toto.fast-reload.com/fullchain1.pem', 'dest': '/tmp/toto/fullchain1.pem'})
changed: [cert] => (item={'src': '/etc/letsencrypt/archive/toto.fast-reload.com/privkey1.pem', 'dest': '/tmp/toto/privkey1.pem'})
TASK [certificat : Copying new certificat ...] *********************************
skipping: [cert] => (item={'src': '/tmp/toto/fullchain1.pem', 'dest': '/etc/letsencrypt/live/toto.fast-reload.com/fullchain.pem'})
skipping: [cert] => (item={'src': '/tmp/toto/privkey1.pem', 'dest': '/etc/letsencrypt/live/toto.fast-reload.com/privkey.pem'})
skipping: [cert]
changed: [toto] => (item={'src': '/tmp/toto/fullchain1.pem', 'dest': '/etc/letsencrypt/live/toto.fast-reload.com/fullchain.pem'})
changed: [toto] => (item={'src': '/tmp/toto/privkey1.pem', 'dest': '/etc/letsencrypt/live/toto.fast-reload.com/privkey.pem'})
TASK [certificat : Restart service apache2, in all cases ...] ******************
skipping: [cert]
changed: [toto]
TASK [certificat : Fetch cert date after ...] **********************************
skipping: [cert]
changed: [toto]
TASK [certificat : Show Result cert date before & after ...] *******************
ok: [toto] => {
"msg": [
"Expire date before: Jun 25 15:53:52 2024 GMT",
"Expire date after: Jun 25 16:00:15 2024 GMT"
]
}
skipping: [cert]
PLAY RECAP *********************************************************************
cert : ok=6 changed=5 unreachable=0 failed=0 skipped=6 rescued=0 ignored=0
toto : ok=7 changed=5 unreachable=0 failed=0 skipped=5 rescued=0 ignored=0
Cleaning up project directory and file based variables
Job succeeded
Fait intéressant, comme je vous l'avais dit, cela ne prend que quelques secondes, un utilisateur en train de lire un article ne le verra même pas ^_^
conclusion:
En quelques scripts bash nous avons pu :
- vérifier l’enregistrement DNS d'un domaine ou d'un sous-domaines.
- Générer un certificat SSL.
- surveiller la date d'expiration d'un ou plusieurs certificat SSL et recevoir un mail en conséquence.
Dans tous cela, nous avons également pris en compte le point de sécurité, en gérant les règles iptables nécessaires à la volée dans les script
Enfin, Nous également ajouté une touche d'automatisation via Gitlab et un playbook ansible plutôt minimaliste.
Cela nous permet de gagner un temps précieux sur nos tâches d'administration tout en conservant un œil (intervention humaine minimal) sur toute la chaîne et avec un minimum de sécurité.
So, what else ?