Creer un certificat SSL wildcard avec Let's Encrypt

Credit : Logo officiel

Creer un certificat SSL wildcard avec Let's Encrypt

Dylan D. — Agent Support Technique Serveur Securite 2535 mots 13 min de lecture

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 :

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

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 :

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 :

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.

# Articles similaires

Sur les memes sujets et plus loin