Automatisation: Gérer vos certificat SSL (1/4)
Écrire des scripts, c'est facile.
Une suite d'instructions, des conditions et hop ça tourne.
Si c'est ce que vous cherchez, alors il y a des chaînes y0utub€ pour cela et les scripts font le job dans des vidéos d'une vingtaine de minutes,
Mais c'est de l'amateurisme.
J'assume pleinement ce que je viens d'écrire.
Le job de SysOps, ou d'autre en Ops, c'est aussi de voir plus loin que la simple exécution réussie de son script.
Un script, ça vit, ça évolue, comme une documentation.
Ce qui va suivre est long et pénible comme une documentation à lire ou écrire, mais nous allons aborder (jusqu'au dernier article) des concepts (ce ne sont pas des cours) de temps, de sécurité, d'automatisation, et même de logique, mais là, c'est la mienne par contre.
Bien, commençons.
Les certificats SSL sont devenus monnaies courantes depuis quelques années pour le plus grand bien de tous et si comme moi, vous gérez plusieurs domaines/sous-domaines, cela peut très vite devenir excessivement chronophage.
Attention : un certificat SSL n'est pas un gage de sécurité absolue !
Il y a plusieurs façons de générer un certificat SSL (OpenSSL, Let's Encrypt).
Nous verrons une méthode avec OpenSSL et une autre avec Let's Encrypt.
Tout ne sera pas vu dans cet article, par exemple, OpenSSL sera survolé dans un autre article, juste pour montrer que ce qui sera fait via Let's Encrypt fonctionne aussi.
Cela va être une petite série d'articles, 3 ou 4 maximum, pour arriver au résultat d'automatisation qui ne sera traité que dans le dernier article.
Pour commencer, un petit rappel :
Seules 4 erreurs sont possibles avec les certificats au-delà du fait que le certificat existe.
- La correspondance de nom
- la provenance du certificat
- la date d’expiration
- la révocation
un certificat SSL est composé de deux fichiers:
- une clé privée qui sert à déchiffrer, à vérifier/confirmer la signature
- une clé publique qui sert à chiffrer, à signer.
Si certificat il y a, service il y a.
Dans notre cas, un serveur web apache.
Pour générer un certificat Let's Encrypt, ou le renouveler tous les 3 mois via certbot renew, le port 80 doit être ouvert, mais aussi le 443 .
En déléguant les certificats à une machine à part nous réduisons la surface d'attaque de notre serveur web dont seuls les ports SSH et HTTPS seront ouverts.
La contrepartie est d'ouvrir les ports 80 & 443 sur notre machine de gestion des certificats, mais nous allons y remédier, d’où une distribution Alpine, à jour, dont l'empreinte en matière de sécurité est plus que raisonnable contrairement à un serveur web (Debian, Red Hat, etc...) ou tout un tas de choses sont présente, là nous avons le strict minimum.
Cette distribution se trouve derrière un pare-feu donc il y a déjà une première couche sécurité.
Installons dans notre distribution alpine les packages dont nous aurons besoin.
apk add certbot bind-tools openssl
De mon côté, j'ai également installé le package iptables, iptables qui sera initié selon l'article: Sécurité: initier proprement iptables.
Jj'ai masqué le port ssh que j'utilise parce que bon quand même hein !
Les IP ce n'est pas grave, c'est du local.
Oui, le port 53 est dans iptables, mais pas sur le pare-feu.
Vous noterez également que concernant ssh, il y a des règles ACCEPT et REJECT y compris sur le 22 qui n'est pas utilisé pourtant.
Donc on autorise 2 IP et ensuite tout le reste => go away
Les ports 80 et 443 seront gérés dans les scripts bash au moment opportun, concernant l'ouverture et la fermeture.
Enfin, quand nous aborderons la question de l'automatisation nous feront de même via Gitlab/Ansible comme ceci :
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" }
Nous ferons également en sorte de fermer les ports une fois que la CI n'en aura plus besoin.
Une fois ceci exposé, nous allons pouvoir nous intéresser à la première étape :
Vérifier l'enregistrement DNS du domaine ou du sous-domaine.
Là encore il y a plusieurs écoles pour faire cela, mais en ce qui me concerne, j'utilise dig
Je vais me concentrer que sur les enregistrement DNS A => IPV4
Pour l'IPV6, enregistrement de type AAAA c'est la même chose.
Dans la capture ci-dessous, nous avons bien un enregistrement IPV4 pour un sous-domaine pointant sur le domaine g00gl€.fr, ce qui signifie que ma zone DNS contient bien cet enregistrement. (l'exploitabilité n'est pas nécessaire ici)
Il va nous falloir exploiter ceci dans un script réduisons l’information au strict nécessaire afin de vérifier la correspondance de nom avec une IP et exploiter cela dans un script.
À notre commande, nous allons cacher l'IP de la sortie console, nous n'en avons pas besoin, en ajoutant | grep -q . (le point est important) et dans un premier script que j'ai nommé tmp, auquel j'ai ajouté un peu de couleur pour la sortie console pour que cela soit plus visuel et lisible rapidement.
Attention : notez bien que le sheebang est ash.
Avec une petite boucle for, nous allons également prendre en compte le teste de plusieurs domaines/sous-domaines en même temps et afficher le résultat avec une condition.
#!/bin/ash
source colors
# Ask domain
read -p "Entrez le nom de domaine ou sous-domaine à vérifier : " DOMAINS
echo ""
for DOMAIN in $DOMAINS; do
# Check DNS
if dig +short A "$DOMAIN" | grep -q .; then
# show result
echo -e "${GREEN}OK${NC} - L'enregistrement A pour ${GREEN}$DOMAIN${NC} existe."
echo ""
else
echo -e "${RED}NOK${NC} - Aucun enregistrement A trouvé pour ${RED}$DOMAIN${NC}."
fi
done
Le script colors, avec un panel qui peut éventuellement vous servir et qui fonctionne sous Alpine Linux.
#!/bin/ash
RED='\033[1;31m'
GREEN='\033[1;32m'
YELLOW='\033[1;33m'
BLUE='\033[1;34m'
MAGENTA='\033[1;35m'
CYAN='\033[1;36m'
WHITE='\033[1;37m'
BLACK='\033[1;30m'
GRAY='\033[1;90m'
LIGHT_RED='\033[1;91m'
LIGHT_GREEN='\033[1;92m'
LIGHT_YELLOW='\033[1;93m'
LIGHT_BLUE='\033[1;94m'
LIGHT_MAGENTA='\033[1;95m'
LIGHT_CYAN='\033[1;96m'
BOLD='\033[1m'
ITALIC='\033[3m'
UNDERLINE='\033[4m'
# Reset colors
NC='\033[0m'
Pour voir le rendu, ajoutez ceci à la fin et exécutez-le.
COLORS="$RED $GREEN $YELLOW $BLUE $MAGENTA $CYAN $WHITE $BLACK $GRAY $LIGHT_RED $LIGHT_GREEN $LIGHT_YELLOW $LIGHT_BLUE $LIGHT_MAGENTA $LIGHT_CYAN"
for color in $COLORS; do
echo -e "${color}Ceci est un test pour cette couleur${NC}"
done
echo -e "${BOLD}Ceci est du texte en gras${NC}"
echo -e "${ITALIC}Ceci est du texte en italique${NC}"
echo -e "${UNDERLINE}Ceci est du texte souligné${NC}"
Bref,
Si vous avez besoin de gérer les enregistrements DNS IPV6 la commande, pour rajouter une condition et un test, est la suivante :
dig +short AAAA "$DOMAIN"
Exécutons notre script et regardons ce qui se passe si l'on renseigne 3 sous-domaines dont un qui n'existe pas.
Le résultat attendu est le bon, pas besoin de plus, nous allons voir pour générer le certificat ou les certificats.
Pour générer un certificat dans notre cas, c'est la commande suivante avec les deux variables nécessaires déjà implémentées. (je ne vais pas les détailler, elles sont suffisamment explicite)
certbot certonly --standalone -d "$domain" --agree-tos --non-interactive --email "$EMAIL"
Dans le script final, j'ai inséré la demande de générer ou non le certificat, je convertis par la même occasion la réponse y en minuscule si elle est tapée en majuscule et enfin la commande qui génère notre certificat.
J'ai également inclus les règles iptables concernant le port 80 open et close quand on en a plus besoin.
N'oubliez pas de modifier la variable MAIL ligne 3.
Ce qui nous donne :
#!/bin/ash
EMAIL="mon-super@mail.amoi"
source colors
# Ask domain
read -p "Entrez le nom de domaine ou sous-domaine à vérifier : " DOMAINS
echo ""
for DOMAIN in $DOMAINS; do
# Check DNS
if dig +short A "$DOMAIN" | grep -q .; then
# Show result
echo -e "${GREEN}OK${NC} - L'enregistrement A pour ${GREEN}$DOMAIN${NC} existe."
echo ""
read -p "Voulez-vous générer le ou les certifats() dont l'enregistrement est vérifié ? yes (y) or no (n) " response
# Convert Uppercase to Lowercase
response=$(echo "$response" | tr '[:upper:]' '[:lower:]')
if [ "$response" == "y" ]; then
# Open port 80
iptables -I INPUT -p tcp --dport 80 -j ACCEPT && iptables -I INPUT -p tcp --dport 443 -j ACCEPT
iptables -I OUTPUT -p tcp --dport 80 -j ACCEPT && iptables -I OUTPUT -p tcp --dport 443 -j ACCEPT
# Generate ssl certificat
certbot certonly --standalone -d "$DOMAIN" --agree-tos --non-interactive --email "$EMAIL"
# Close port 80
iptables -D INPUT -p tcp --dport 80 -j ACCEPT && iptables -D INPUT -p tcp --dport 443 -j ACCEPT
iptables -D OUTPUT -p tcp --dport 80 -j ACCEPT && iptables -D OUTPUT -p tcp --dport 443 -j ACCEPT
echo ""
else
exit 0
fi
else
echo -e "${RED}NOK${NC} - Aucun enregistrement A trouvé pour ${RED}$DOMAIN${NC}."
fi
done
On enlèvera tous les commentaires d'ici la fin.
Vous noterez que je génère certificat par certificat, dans le cas où je renseigne plusieurs domaines/sous-domaines, et non pas l'ensemble des entrée testé et valide, ce qui serait possible, à vous de voir si vous souhaitez modifier le script en ce sens.
Également, je n'ai pas mis de > /dev/null dans ma commande certbot (mais cela viendra) afin de voir la sortie console, là encore, à vous de voir ce que vous souhaitez.
Exécutons le script avec 3 sous-domaines dont un qui n'existe pas et testons la touche majuscule par la même occasion.
Le script a eu le comportement attendu, tester les enregistrements DNS, générer les certificats demandés, gérer la majuscule et ignoré le domaine inexistant.
Tout cela en 32 lignes de commentaires compris.
Et dans une distribution Alpine Linux persistante dans un container Proxmox.
Dans l'article suivant, nous intégrerons la gestion des dates d'expiration de nos certificats.