Aller au contenu
Sylvain Joffraud l4w
Retour

OpenTofu ACME : `min_days_remaining` ne renouvelle rien

Avec OpenTofu et le provider ACME, min_days_remaining ne renouvelle rien tout seul : l’attribut dit seulement au prochain tofu apply de recréer le certificat s’il reste moins de N jours de validité. Entre deux apply, rien ne tourne. J’avais quatre certificats Let’s Encrypt dans un module OpenTofu, avec un commentaire rassurant en tête du fichier, « Auto-renewal : tofu apply recrée le cert quand < 30j ». Exact, mais rien ne lançait tofu apply. Fin juillet 2026, j’ai confié ces certificats à certbot.timer.

Le module qui promettait un renouvellement automatique

Le module certs utilisait OpenTofu 1.12.0 (installé via tenv) et le provider vancluever/acme 2.48.1, avec un challenge DNS-01 chez OVH. Chaque certificat suivait le même modèle :

resource "acme_certificate" "chat_example_com" {
  account_key_pem = acme_registration.main.account_key_pem
  common_name     = "chat.example.com"

  # Force re-create quand il reste < 30j de validité
  min_days_remaining = 30

  dns_challenge {
    provider = "ovh"
    config = {
      OVH_ENDPOINT           = var.ovh_endpoint
      OVH_APPLICATION_KEY    = var.ovh_application_key
      OVH_APPLICATION_SECRET = var.ovh_application_secret
      OVH_CONSUMER_KEY       = var.ovh_consumer_key
    }
  }

  recursive_nameservers = ["1.1.1.1:53", "8.8.8.8:53"]
}

Les PEM étaient écrits en local, puis une null_resource les copiait en SSH sur srv-a et rechargeait Apache. Le README du module prévoyait la suite : « Cron mensuel sur le PC qui apply », ou « à la main à chaque session ». Le message du commit qui a remplacé ce montage le résume : « pas automatisé ».

Pourquoi min_days_remaining ne renouvelle rien tout seul ?

OpenTofu n’est pas un démon. Le provider ACME compare la date d’expiration au seuil uniquement quand OpenTofu s’exécute ; le reste du temps, le certificat vit sa vie sur le serveur, sans surveillance.

min_days_remaining est un seuil évalué au moment d’un apply, pas un planificateur : sans apply, pas de renouvellement.

S’y ajoutait une dépendance au poste de travail : l’apply devait tourner sur le PC qui avait l’alias SSH vers srv-a, avec sudo sans mot de passe pour mv, chmod et systemctl reload apache2. Un renouvellement qui dépend d’un portable allumé n’est pas automatique. Et les clés privées dormaient dans le state.

Provider ACME (OpenTofu)certbot + certbot.timer
Déclencheurun tofu apply lancé par quelqu’untimer systemd, 2 passages par jour
Où ça tournele poste qui appliquele serveur lui-même
Déploiementscp + ssh depuis le posterien à copier, deploy-hook qui recharge Apache
Clé privéedans le state OpenTofusur le disque du serveur

La migration vers certbot.timer en DNS-01 OVH

Sur srv-a, certbot.timer gérait déjà cloud.example.com, coolify.example.com et le wildcard dev.example.com, avec le même challenge DNS-01 OVH. La migration a consisté à réémettre chaque nom avec certbot :

sudo certbot certonly --dns-ovh --dns-ovh-credentials /etc/letsencrypt/ovh.ini \
  --dns-ovh-propagation-seconds 60 \
  -d chat.example.com --cert-name chat.example.com

Le deploy-hook /etc/letsencrypt/renewal-hooks/deploy/reload-apache.sh recharge Apache après chaque renouvellement. Bilan du module :

  1. notes.example.com et chat.example.com passent sous certbot, ainsi que api.supabase.dev.example.com, d’abord émis par OpenTofu puis confié à certbot le jour même.
  2. rdv.example.org est supprimé du module : le domaine pointe désormais vers srv-b, où Traefik émet lui-même le certificat en HTTP-01.
  3. event.example.org reste sous OpenTofu, faute de mieux (voir ci-dessous).

⚠️ Passer à certbot-dns-ovh a un effet de bord : le plugin Debian ne supprime pas ses TXT de validation, et un résidu a cassé la résolution de deux noms couverts par un wildcard. C’est l’objet de l’article sur le _acme-challenge oublié qui fait taire le wildcard DNS, avec le hook qui purge la zone.

Le reste : un token OVH limité à une zone

Le token OVH de /etc/letsencrypt/ovh.ini est limité à la zone example.com et renvoie 403 sur example.org. Le certificat de event.example.org ne peut donc pas passer sous certbot avec ce token ; il reste le seul dans le module, dont l’en-tête annonce désormais un « périmètre réduit » et rappelle que la réémission n’a lieu qu’au tofu apply. Pour finir la migration, il faut un token OVH dont les règles couvrent aussi la seconde zone.

Lessons learned

Questions fréquentes

Le provider ACME de Terraform ou OpenTofu renouvelle-t-il les certificats automatiquement ?
Non. Le provider vancluever/acme ne recrée un certificat que pendant un tofu apply ou terraform apply, quand il reste moins de min_days_remaining jours de validité. Terraform et OpenTofu ne sont pas des démons : entre deux apply, rien ne tourne. Sans apply planifié, le certificat expire sans prévenir.
Faut-il gérer les certificats Let’s Encrypt avec Terraform ou avec certbot ?
Pour un serveur qui sert lui-même ses certificats, certbot avec certbot.timer est plus sûr : le renouvellement tourne sur le serveur, deux fois par jour, sans dépendre d’un poste de travail. Terraform reste utilisable pour un cas isolé, à condition de planifier un apply régulier sur une machine toujours disponible.
Pourquoi certbot dns-ovh renvoie 403 sur une deuxième zone DNS OVH ?
Un token OVH peut être restreint par ses règles d’accès à une seule zone, par exemple /domain/zone/example.com/*. Le plugin certbot-dns-ovh reçoit alors un 403 sur toute autre zone du compte. Il faut créer un token dont les règles couvrent aussi la seconde zone, ou garder un fichier ovh.ini par zone.
Où sont stockées les clés privées des certificats émis par le provider ACME Terraform ?
Les clés privées générées par acme_certificate sont stockées dans le state Terraform ou OpenTofu, comme la clé du compte ACME. Quiconque lit le state lit donc les clés. Il faut un backend à accès restreint, et c’est un argument de plus pour laisser certbot générer les clés directement sur le serveur.

Partager cet article :

Article précédent
Gemma 4 12B : QAT ou non QAT, même score au banc
Article suivant
Date métier en JS : `toISOString()` recule d’un jour