Credit : Logo officiel
Creer un certificat SSL wildcard avec Let's Encrypt
Creer un certificat SSL wildcard avec Let's Encrypt
Un client m'a appele la semaine derniere paniquer parce qu'il avait sept sous-domaines a couvrir : monsite.fr, www.monsite.fr, api.monsite.fr, app.monsite.fr, staging.monsite.fr, mail.monsite.fr et admin.monsite.fr. A chaque deploiement de nouveau service, il devait relancer un Certbot avec une nouvelle entree -d, modifier le vhost, recharger Nginx... et il commencait a en rajouter trois nouveaux pour un projet SaaS multi-tenant. Sept entrees etait deja penible, soixante-dix l'aurait tue. La solution : un certificat wildcard *.monsite.fr qui couvre n'importe quel sous-domaine en une seule emission.
Je vais te montrer comment generer un wildcard Let's Encrypt avec Certbot 2.x et le challenge DNS-01, comment automatiser le renouvellement via l'API DNS de IONOS (ou Cloudflare), et comment configurer Nginx 1.24 sur Debian 12 pour exploiter ce certificat sur tous tes vhosts.
Comprendre le wildcard et le challenge DNS
Un certificat wildcard couvre tous les sous-domaines d'un niveau d'une zone DNS. *.monsite.fr matche api.monsite.fr, staging.monsite.fr, client42.monsite.fr, mais pas monsite.fr (le domaine racine doit etre ajoute explicitement) et pas client.api.monsite.fr (deux niveaux profonds).
Let's Encrypt distribue ces certificats gratuitement, mais avec une contrainte technique : la validation ne peut pas se faire en HTTP-01 (le challenge classique qui pose un fichier sur le serveur), elle doit se faire en DNS-01. Le but : prouver que tu controles la zone DNS du domaine, parce qu'un wildcard peut etre utilise pour intercepter n'importe quel sous-domaine, donc Let's Encrypt veut une preuve plus forte.
Concretement, Certbot va te demander de creer un enregistrement TXT _acme-challenge.monsite.fr avec une valeur unique. Let's Encrypt resout ce TXT et compare. Si ca matche, le certificat est emis.
C'est la seule difference, mais c'est aussi ce qui rend l'automatisation un peu plus subtile : il faut une API DNS, ou alors faire la procedure a la main tous les 90 jours (ce qui est insupportable a moyen terme).
Wildcard ou multi-SAN : que choisir ?
Deux ecoles. Multi-SAN (Subject Alternative Names) liste explicitement chaque sous-domaine dans le certificat : monsite.fr, api.monsite.fr, mail.monsite.fr, etc. Avantage : tu vois exactement ce qui est couvert, plus securise sur un audit. Inconvenient : tu dois reemettre a chaque ajout, et Let's Encrypt limite a 100 SAN par certificat.
Wildcard te couvre tous les sous-domaines d'un coup, sans avoir a reemettre. Pratique pour les SaaS multi-tenant ou les environnements de test ephemerique. Inconvenient : si la cle privee est compromise, l'attaquant peut usurper n'importe quel sous-domaine. Donc wildcard demande encore plus de soin sur la conservation des cles.
Ma regle perso : SAN explicite si je sais a l'avance ce que je veux couvrir, wildcard si la liste est dynamique ou trop longue. Et pour les zones critiques (banque, sante), je reste en SAN.
Installer Certbot
Sur Debian 12 / Ubuntu 24.04 :
apt update
apt install certbot python3-certbot-nginx -y
certbot --version
A partir de Certbot 2.x, le snap n'est plus indispensable, le paquet apt est a jour. Si tu veux la toute derniere version :
apt install snapd -y
snap install --classic certbot
ln -sf /snap/bin/certbot /usr/bin/certbot
Premiere generation manuelle
Pour comprendre le flux et tester, on commence en mode manuel. Le challenge DNS te demandera de creer un TXT a la main :
certbot certonly \
--manual \
--preferred-challenges dns \
--email admin@monsite.fr \
--agree-tos \
--no-eff-email \
-d "monsite.fr" \
-d "*.monsite.fr"
Certbot affiche :
Please deploy a DNS TXT record under the name:
_acme-challenge.monsite.fr.
with the following value:
gH7dKj2mN8pQ4rT6wX9yB1cE3fA5iL0oZsXvU2nM
Tu vas dans l'interface DNS IONOS (ou OVH, Cloudflare, etc.), tu crees l'enregistrement :
- Type : TXT
- Nom :
_acme-challenge(le suffixe.monsite.frest ajoute automatiquement) - Valeur : la chaine fournie par Certbot, sans guillemets
- TTL : 60 secondes (pour pouvoir invalider rapidement apres)
Avant de valider dans Certbot, toujours verifier la propagation :
dig _acme-challenge.monsite.fr TXT +short @8.8.8.8
dig _acme-challenge.monsite.fr TXT +short @1.1.1.1
Des que les resolveurs publics renvoient ta valeur, retourne dans Certbot et appuie sur Entree. Si tu valides trop tot, le challenge echoue et tu dois recommencer (Let's Encrypt impose un rate limit, donc evite les ratees a la chaine).
Automatiser avec l'API IONOS
Le vrai interet du wildcard, c'est qu'il se renouvelle automatiquement comme n'importe quel autre certificat. Pour ca, il faut un hook qui cree et supprime l'enregistrement TXT via l'API du fournisseur DNS.
IONOS expose une API DNS gratuite via le portail Developer. Cree une cle API depuis https://developer.hosting.ionos.fr/, et note le format prefix.secret.
Hook auth (creation du TXT)
mkdir -p /etc/certbot/hooks
nano /etc/certbot/hooks/ionos-auth.sh
#!/bin/bash
set -euo pipefail
IONOS_API_KEY="PREFIX.SECRET"
DOMAIN="monsite.fr"
ZONE_ID=$(curl -s -H "X-API-Key: ${IONOS_API_KEY}" \
"https://api.hosting.ionos.com/dns/v1/zones" | \
jq -r --arg d "$DOMAIN" '.[] | select(.name==$d) | .id')
curl -s -X POST \
-H "X-API-Key: ${IONOS_API_KEY}" \
-H "Content-Type: application/json" \
-d "[{\"name\": \"_acme-challenge.${CERTBOT_DOMAIN}\", \"type\": \"TXT\", \"content\": \"${CERTBOT_VALIDATION}\", \"ttl\": 60, \"prio\": 0, \"disabled\": false}]" \
"https://api.hosting.ionos.com/dns/v1/zones/${ZONE_ID}/records"
sleep 30
Le sleep 30 laisse a la modification le temps de se propager avant que Let's Encrypt n'interroge le DNS. Selon le fournisseur, on peut monter a 60 voire 120 secondes.
Hook cleanup (suppression du TXT)
nano /etc/certbot/hooks/ionos-cleanup.sh
#!/bin/bash
set -euo pipefail
IONOS_API_KEY="PREFIX.SECRET"
DOMAIN="monsite.fr"
ZONE_ID=$(curl -s -H "X-API-Key: ${IONOS_API_KEY}" \
"https://api.hosting.ionos.com/dns/v1/zones" | \
jq -r --arg d "$DOMAIN" '.[] | select(.name==$d) | .id')
RECORD_IDS=$(curl -s -H "X-API-Key: ${IONOS_API_KEY}" \
"https://api.hosting.ionos.com/dns/v1/zones/${ZONE_ID}?recordName=_acme-challenge.${CERTBOT_DOMAIN}&recordType=TXT" | \
jq -r '.records[].id')
for RECORD_ID in $RECORD_IDS; do
curl -s -X DELETE \
-H "X-API-Key: ${IONOS_API_KEY}" \
"https://api.hosting.ionos.com/dns/v1/zones/${ZONE_ID}/records/${RECORD_ID}"
done
Protege les fichiers :
chmod 700 /etc/certbot/hooks/*.sh
chown root:root /etc/certbot/hooks/*.sh
apt install jq -y
Ces scripts contiennent des secrets, ils ne doivent etre lisibles que par root.
Generation initiale automatisee
certbot certonly \
--manual \
--preferred-challenges dns \
--manual-auth-hook /etc/certbot/hooks/ionos-auth.sh \
--manual-cleanup-hook /etc/certbot/hooks/ionos-cleanup.sh \
--manual-public-ip-logging-ok \
--email admin@monsite.fr \
--agree-tos \
--no-eff-email \
-d "monsite.fr" \
-d "*.monsite.fr"
A partir de la, le certificat se renouvelle tout seul tous les 90 jours sans intervention.
Variante Cloudflare (encore plus simple)
Si tu utilises Cloudflare, le plugin officiel evite d'ecrire les hooks :
apt install python3-certbot-dns-cloudflare -y
mkdir -p /root/.secrets/certbot
echo "dns_cloudflare_api_token = ton_token_avec_droit_dns_edit" > /root/.secrets/certbot/cloudflare.ini
chmod 600 /root/.secrets/certbot/cloudflare.ini
certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /root/.secrets/certbot/cloudflare.ini \
-d "monsite.fr" \
-d "*.monsite.fr"
Variante OVH
Sur OVH, le plugin Certbot officiel existe aussi. Cree des credentials API depuis https://api.ovh.com/createToken/ avec les droits GET /domain/zone/*, POST /domain/zone/*, DELETE /domain/zone/*.
apt install python3-certbot-dns-ovh -y
cat > /root/.secrets/certbot/ovh.ini <<EOF
dns_ovh_endpoint = ovh-eu
dns_ovh_application_key = TON_APPLICATION_KEY
dns_ovh_application_secret = TON_APPLICATION_SECRET
dns_ovh_consumer_key = TON_CONSUMER_KEY
EOF
chmod 600 /root/.secrets/certbot/ovh.ini
certbot certonly --dns-ovh --dns-ovh-credentials /root/.secrets/certbot/ovh.ini \
-d "monsite.fr" -d "*.monsite.fr"
Le meme principe s'applique a Gandi, Hetzner, DigitalOcean : un plugin existe pour quasiment tous les registrars majeurs. Si ton DNS n'a pas de plugin, tu peux deleguer la zone _acme-challenge vers un DNS qui en a un (acme-dns par exemple) et automatiser via lui sans toucher a ta zone principale.
Configuration Nginx avec wildcard
Un seul certificat couvre maintenant tous les vhosts. Exemple pour api.monsite.fr :
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name api.monsite.fr;
ssl_certificate /etc/letsencrypt/live/monsite.fr/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/monsite.fr/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/monsite.fr/chain.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header Referrer-Policy strict-origin-when-cross-origin;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto https;
}
}
server {
listen 80;
server_name api.monsite.fr;
return 301 https://$host$request_uri;
}
Factorise les directives SSL communes dans /etc/nginx/snippets/ssl-monsite.conf et inclus-le dans chaque vhost. Plus de DRY, plus de coherence.
Teste avec SSL Labs : un score A+ doit etre la norme avec cette config.
Renouvellement automatique
Certbot installe un timer systemd qui tourne deux fois par jour :
systemctl list-timers | grep certbot
Test a blanc :
certbot renew --dry-run
Si tu vois Congratulations, all renewals succeeded c'est tout bon.
Pour que Nginx recharge automatiquement le certificat apres renouvellement :
mkdir -p /etc/letsencrypt/renewal-hooks/deploy
nano /etc/letsencrypt/renewal-hooks/deploy/reload-services.sh
#!/bin/bash
nginx -t && systemctl reload nginx
# si tu utilises aussi postfix/dovecot/haproxy
systemctl reload postfix 2>/dev/null || true
systemctl reload dovecot 2>/dev/null || true
chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-services.sh
Surveiller l'expiration avec un script de garde-fou
Meme avec le timer Certbot, j'aime bien avoir une alerte si jamais un renouvellement plante silencieusement. Un script en cron quotidien dans /etc/cron.daily/check-cert-expiry :
#!/bin/bash
DOMAIN="monsite.fr"
DAYS_LEFT=$(echo | openssl s_client -servername "$DOMAIN" -connect "$DOMAIN:443" 2>/dev/null | \
openssl x509 -noout -enddate | cut -d= -f2 | xargs -I{} date -d "{}" +%s)
NOW=$(date +%s)
DIFF=$(( (DAYS_LEFT - NOW) / 86400 ))
if [ "$DIFF" -lt 20 ]; then
echo "Cert pour $DOMAIN expire dans $DIFF jours" | mail -s "Cert expiry warning" admin@monsite.fr
fi
Let's Encrypt renouvelle a J-30. Si a J-20 le cert n'est pas renouvele, c'est que quelque chose a cale (souvent un hook DNS qui timeout). L'alerte te previent avant que le certificat soit reellement expire.
Erreurs courantes et leur fix
DNS problem: NXDOMAIN looking up TXT for _acme-challenge.monsite.fr: la zone DNS n'est pas a jour ou tu valides trop tot. Augmente lesleepdans le hook auth a 60 ou 120 secondes.- Rate limit Let's Encrypt depasse : 5 echecs par heure, 50 certificats par semaine et par domaine. Si tu testes, utilise
--stagingqui pointe sur l'environnement d'integration sans limite stricte. Cannot accept the policy: must be one of:lors de la generation API IONOS : les permissions de la cle API sont insuffisantes. Verifie que la cle a bien le scopeDNS write.- Certificat genere mais Nginx affiche encore l'ancien : tu n'as pas recharge Nginx.
systemctl reload nginx. Verifie aussi que tu pointes surlive/monsite.fr/fullchain.pem(le symlink) et pas surarchive/.../fullchain1.pem(le fichier reel qui change a chaque renouvellement). *.monsite.frne couvre pasmonsite.fr: c'est attendu. Wildcard couvre uniquement les sous-domaines. Il faut declarer les deux :-d "monsite.fr" -d "*.monsite.fr".- TXT residuel apres une emission ratee : le hook cleanup n'a pas tourne. Va manuellement supprimer dans l'interface DNS sinon le challenge suivant peut s'emmeler.
- Plugins Cloudflare :
Unauthorized: le token API n'a pas le scopeZone.DNS.Editsur la bonne zone. Recree avec les bons droits dans le dashboard Cloudflare. - CAA record bloque Let's Encrypt : si ta zone a un enregistrement CAA qui ne mentionne pas
letsencrypt.org, l'AC refuse d'emettre. Verifie avecdig CAA monsite.fr +short. Ajoute0 issue "letsencrypt.org"et0 issuewild "letsencrypt.org"si necessaire.
Securiser le stockage des cles privees
Un wildcard a un pouvoir non negligeable : il signe pour tous les sous-domaines. Si la cle privee fuite, l'attaquant peut emettre des proxies qui usurpent api.monsite.fr, mail.monsite.fr, etc. Donc le stockage merite quelques regles strictes.
Les permissions Unix sont la base. Le repertoire /etc/letsencrypt/archive/ doit etre lisible uniquement par root :
chmod 700 /etc/letsencrypt/archive
chmod 600 /etc/letsencrypt/archive/monsite.fr/privkey*.pem
ls -la /etc/letsencrypt/live/monsite.fr/
Verifie aussi que les live/ symlinks sont bien crees avec les bons droits. Si tu as plusieurs services qui doivent lire le certificat (Nginx en www-data, Postfix en postfix, Dovecot en dovecot), evite de mettre la cle en 644. Au lieu de ca, cree un groupe dedie ssl-cert :
groupadd ssl-cert
usermod -aG ssl-cert www-data
usermod -aG ssl-cert postfix
usermod -aG ssl-cert dovecot
chgrp -R ssl-cert /etc/letsencrypt/archive /etc/letsencrypt/live
chmod 750 /etc/letsencrypt/archive
chmod 640 /etc/letsencrypt/archive/monsite.fr/privkey*.pem
Sur les serveurs sensibles, je vais plus loin avec un audit auditd qui logue chaque acces a la cle privee :
echo '-w /etc/letsencrypt/archive/monsite.fr/privkey1.pem -p rwa -k privkey-access' >> /etc/audit/rules.d/ssl.rules
systemctl restart auditd
ausearch -k privkey-access
Si un jour quelqu'un autre que les services attendus lit cette cle, c'est dans les logs.
Verifier le certificat genere
Une fois le certificat emis, je verifie systematiquement qu'il couvre bien ce que je veux et qu'il a les bonnes proprietes :
openssl x509 -in /etc/letsencrypt/live/monsite.fr/fullchain.pem -noout -text | head -40
Les points a controler :
- Subject : doit contenir
CN=monsite.fr(le premier domaine de la liste). - X509v3 Subject Alternative Name : doit lister
DNS:monsite.fretDNS:*.monsite.fr(et tout autre SAN demande). - Validity : 90 jours pour Let's Encrypt, c'est la norme.
- Public Key Algorithm : ECDSA recommande pour la performance, RSA 2048 par defaut. Tu peux forcer ECDSA avec
--key-type ecdsa --elliptic-curve secp384r1. - Issuer : doit etre
Let's Encrypt(production) ou(STAGING) Pretend Pear(staging, ne pas mettre en prod).
Test rapide depuis l'exterieur sur tous les sous-domaines :
for SUB in api app staging mail admin; do
echo "=== ${SUB}.monsite.fr ==="
echo | openssl s_client -servername "${SUB}.monsite.fr" -connect "${SUB}.monsite.fr:443" 2>/dev/null \
| openssl x509 -noout -subject -dates
done
Ca donne une vue d'ensemble en une commande, et tu verifies que le wildcard est bien servi par tous les vhosts.
CI/CD : injecter le certificat dans tes pipelines
Dans une infra ou tu deploies des conteneurs ou tu utilises un load balancer applicatif (HAProxy, Traefik, Nginx central), le certificat doit etre distribue a plusieurs machines. Plutot que de regenerer sur chaque host, je centralise sur un host "cert-master" et je distribue via Ansible ou rsync apres chaque renouvellement :
#!/bin/bash
# /etc/letsencrypt/renewal-hooks/deploy/sync-edges.sh
for HOST in edge1.monsite.fr edge2.monsite.fr; do
rsync -az /etc/letsencrypt/live/monsite.fr/ deploy@${HOST}:/etc/ssl/wildcard/
ssh deploy@${HOST} 'sudo systemctl reload nginx'
done
Pour Kubernetes, le pattern recommande est plutot cert-manager qui pilote Let's Encrypt directement depuis le cluster et stocke le cert dans un Secret. Mais sur une infra non-K8s, le rsync apres hook reste la solution la plus simple et la plus fiable.
Pour aller plus loin
Un wildcard est un point de depart. Pour bien securiser ta stack :
- Configurer HTTPS partout, guide SSL/TLS — tour complet des bonnes pratiques au-dela d'un simple cert.
- Resoudre les boucles de redirection SSL avec Cloudflare — piege classique quand on combine wildcard et CDN.
- Reverse proxy Nginx avec SSL — pour utiliser le wildcard sur des backends multiples.
- Hardening Linux : securiser son serveur — la base avant tout cert.
- Mettre en place un CDN gratuit avec Cloudflare — strategie edge qui complete le SSL serveur.
- Configurer un pare-feu applicatif WAF avec Nginx — pour proteger les services exposes derriere ton wildcard.
Un seul certificat pour les gouverner tous
Le wildcard, une fois automatise, c'est l'outil ideal pour gerer une infrastructure avec beaucoup de sous-domaines : SaaS multi-tenant, environnements de staging, microservices, sous-domaines techniques. Un seul certificat a renouveler, une config SSL factorisee dans un snippet Nginx, et une compatibilite totale avec les pipelines CI/CD ou Ansible. Couple ca a un bon timer systemd et un hook deploy, et tu n'as plus a y penser pendant des annees. La meilleure ops c'est celle qui ne se voit pas.
EDIT 2026 : les agents ACME alternatifs comme lego ou acme.sh sont aussi tres bons et plus legers que Certbot pour des serveurs avec peu de ressources. acme.sh notamment peut tourner sans Python, ce qui est appreciable sur Alpine ou les conteneurs minimalistes. Les commandes changent un peu mais la philosophie reste identique : challenge DNS-01, hooks API, deploy automatique apres renouvellement.