From d687d7dc58053837bdbb53ee98bb7a2b1ba86306 Mon Sep 17 00:00:00 2001 From: Johan LEROY Date: Wed, 23 Sep 2026 11:38:44 +0200 Subject: [PATCH] =?UTF-8?q?fix(deploy):=20passe=20de=20DuckDNS=20=C3=A0=20?= =?UTF-8?q?deSEC,=20filtr=C3=A9=20par=20l'=C3=A9cole?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le filtrage du réseau de l'école bloque duckdns.org, site et API, depuis les postes comme depuis la VM : sans API, pas de défi DNS-01. deSEC (dedyn.io) répond depuis les deux. - Domaine enervision-g3.dedyn.io ; provision-host.sh publie par l'API deSEC l'enregistrement du domaine et son joker vers la VM. - make tls-desec remplace tls-duckdns. acme.sh recopie le jeton dans acme/account.conf : le dossier est retiré aux autres comptes. - deploy.yml ne demande un certificat qu'à un .env déjà réaligné sur le domaine deSEC, pour ne pas faire échouer un déploiement en cours de migration. --- .github/workflows/deploy.yml | 5 +-- Makefile | 25 ++++++------- ...desec-certificats-dns01-et-frontal-sni.md} | 36 +++++++++++-------- docs/architecture/10-infra.md | 8 ++--- infra/README.md | 8 ++--- infra/proxy/README.md | 6 ++-- .../environments/vm-eni/variables.tf | 4 +-- scripts/provision-host.sh | 30 ++++++++++++---- 8 files changed, 74 insertions(+), 48 deletions(-) rename docs/adr/{0018-noms-duckdns-certificats-dns01-et-frontal-sni.md => 0018-noms-desec-certificats-dns01-et-frontal-sni.md} (69%) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 14e392c..9f733d3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -28,7 +28,7 @@ jobs: timeout-minutes: 30 environment: name: ${{ github.ref_name == 'main' && 'prod' || github.ref_name == 'dev' && 'rec' || 'dev' }} - url: ${{ github.ref_name == 'main' && 'https://enervision-g3.duckdns.org' || github.ref_name == 'dev' && 'https://rec.enervision-g3.duckdns.org' || 'https://dev.enervision-g3.duckdns.org' }} + url: ${{ github.ref_name == 'main' && 'https://enervision-g3.dedyn.io' || github.ref_name == 'dev' && 'https://rec.enervision-g3.dedyn.io' || 'https://dev.enervision-g3.dedyn.io' }} env: ENVIRONNEMENT: ${{ github.ref_name == 'main' && 'prod' || github.ref_name == 'dev' && 'rec' || 'dev' }} PORT_HTTPS: ${{ github.ref_name == 'main' && '10443' || github.ref_name == 'dev' && '8443' || '9443' }} @@ -54,7 +54,8 @@ jobs: echo "::endgroup::" echo "::group::Reconstruit et redémarre la stack" - if [ -r /srv/enervision/duckdns.token ]; then make tls-duckdns; fi + # Un `.env` pas encore réaligné par provision-host.sh porte encore un nom en `.local`. + if [ -r ../desec.token ] && grep -q '^PUBLIC_HOST=.*\.dedyn\.io$' .env; then make tls-desec; fi make stack-up if [ "${ENVIRONNEMENT}" = prod ]; then make front-up; fi echo "::endgroup::" diff --git a/Makefile b/Makefile index dfd883d..a46b071 100644 --- a/Makefile +++ b/Makefile @@ -73,7 +73,7 @@ DEMO_NOW ?= 2024-12-31T00:00:00Z migrate migrate-test bootstrap-admin services-up demo-data demo-data-force \ ml-lint ml-typecheck ml-test ml-check ml-train ml-score mlflow-up detect-alerts recommendations \ airflow-lint airflow-test airflow-check airflow-up airflow-down airflow-logs \ - tls-selfsigned tls-acme tls-renew tls-duckdns front-up stack-up stack-down stack-logs \ + tls-selfsigned tls-acme tls-renew tls-desec front-up stack-up stack-down stack-logs \ e2e-install e2e-prepare e2e load-smoke load-test load-stress load-limits \ db-ensure-supervision monitoring-up monitoring-down monitoring-logs monitoring-check @@ -236,20 +236,21 @@ tls-renew: ## Renouvelle les certificats Let's Encrypt et recharge le proxy $(COMPOSE_PROD) exec proxy nginx -s reload # Pourquoi : la VM n'a qu'une IP privée, que Let's Encrypt ne joint pas ; le défi DNS-01 passe -# par l'API DuckDNS (ADR 0018). Le jeton transite par l'environnement, jamais par `argv`. +# par l'API deSEC (ADR 0018). Le jeton transite par l'environnement, jamais par `argv`. ACME_SH := neilpang/acme.sh:3.1.6 -DUCKDNS_TOKEN_FILE ?= $(abspath $(CURDIR)/../duckdns.token) -acme-sh = docker run --rm --user "$$(id -u):$$(id -g)" -e DuckDNS_Token -e AUTO_UPGRADE=0 \ +DESEC_TOKEN_FILE ?= $(abspath $(CURDIR)/../desec.token) +acme-sh = docker run --rm --user "$$(id -u):$$(id -g)" -e DEDYN_TOKEN -e AUTO_UPGRADE=0 \ -v "$(CURDIR)/infra/proxy/acme:/acme.sh" -v "$(CURDIR)/infra/proxy/tls:/tls" $(ACME_SH) -# acme.sh sort en 2 quand le certificat n'est pas encore à renouveler : rejouable à chaque déploiement. -tls-duckdns: ## Certificat Let's Encrypt par DNS-01 DuckDNS, renouvelé seulement à échéance - @case "$(PUBLIC_HOST)" in *.duckdns.org) ;; *) echo "PUBLIC_HOST=$(PUBLIC_HOST) n'est pas un nom DuckDNS"; exit 1 ;; esac - @test -r "$(DUCKDNS_TOKEN_FILE)" || { echo "Jeton DuckDNS illisible : $(DUCKDNS_TOKEN_FILE)"; exit 1; } - @mkdir -p infra/proxy/acme - @DuckDNS_Token="$$(cat "$(DUCKDNS_TOKEN_FILE)")"; export DuckDNS_Token; \ - $(acme-sh) --issue --server letsencrypt --dns dns_duckdns -d "$(PUBLIC_HOST)"; \ - code=$$?; [ $$code -eq 0 ] || [ $$code -eq 2 ] || exit $$code +# acme.sh sort en 2 quand le certificat n'est pas encore à renouveler, et recopie le jeton dans +# acme/account.conf : d'où le chmod, qui le soustrait aux autres comptes de la machine. +tls-desec: ## Certificat Let's Encrypt par DNS-01 deSEC, renouvelé seulement à échéance + @case "$(PUBLIC_HOST)" in *.dedyn.io) ;; *) echo "PUBLIC_HOST=$(PUBLIC_HOST) n'est pas un nom deSEC"; exit 1 ;; esac + @test -r "$(DESEC_TOKEN_FILE)" || { echo "Jeton deSEC illisible : $(DESEC_TOKEN_FILE)"; exit 1; } + @mkdir -p infra/proxy/acme && chmod 700 infra/proxy/acme + @DEDYN_TOKEN="$$(tr -d '[:space:]' < "$(DESEC_TOKEN_FILE)")"; export DEDYN_TOKEN; \ + $(acme-sh) --issue --server letsencrypt --dns dns_desec -d "$(PUBLIC_HOST)"; \ + code=$$?; chmod -R go-rwx infra/proxy/acme; [ $$code -eq 0 ] || [ $$code -eq 2 ] || exit $$code @$(acme-sh) --install-cert --ecc -d "$(PUBLIC_HOST)" \ --fullchain-file /tls/fullchain.pem --key-file /tls/privkey.pem @$(COMPOSE_PROD) exec -T proxy nginx -s reload 2>/dev/null \ diff --git a/docs/adr/0018-noms-duckdns-certificats-dns01-et-frontal-sni.md b/docs/adr/0018-noms-desec-certificats-dns01-et-frontal-sni.md similarity index 69% rename from docs/adr/0018-noms-duckdns-certificats-dns01-et-frontal-sni.md rename to docs/adr/0018-noms-desec-certificats-dns01-et-frontal-sni.md index 56f30a7..bf15e4f 100644 --- a/docs/adr/0018-noms-duckdns-certificats-dns01-et-frontal-sni.md +++ b/docs/adr/0018-noms-desec-certificats-dns01-et-frontal-sni.md @@ -1,4 +1,4 @@ -# 0018 - Noms DuckDNS, certificats Let's Encrypt par DNS-01 et frontal SNI sans port +# 0018 - Noms deSEC, certificats Let's Encrypt par DNS-01 et frontal SNI sans port - Statut : accepté - Date : 2026-09-23 @@ -13,21 +13,24 @@ rien de présentable à un jury, et rien d'utilisable par quelqu'un qui n'a pas poste. Contraintes : la VM n'a qu'une IP privée, `10.101.200.37`, que ni Internet ni Let's Encrypt ne -joignent, et le réseau de l'école ne doit pas être touché. Deux vérifications faites le 23/09 : -les résolveurs de l'école rendent bien une adresse privée pour un nom public, et la VM sort en -HTTPS vers Let's Encrypt. +joignent, et le réseau de l'école ne doit pas être touché. Vérifications faites le 23/09 : les +résolveurs de l'école rendent bien une adresse privée pour un nom public, la VM sort en HTTPS +vers Let's Encrypt et vers l'API de deSEC, mais le filtrage de l'école bloque duckdns.org, site +et API, depuis les postes comme depuis la VM. ## Décision -**Des noms publics qui visent l'IP privée.** `enervision-g3.duckdns.org` porte la prod, -`rec.` et `dev.` en sous-domaines, que DuckDNS résout d'office. Tout poste du réseau de l'école -les résout sans configuration ; hors de ce réseau, l'IP ne mène nulle part. +**Des noms publics qui visent l'IP privée.** `enervision-g3.dedyn.io`, zone gratuite de deSEC, +porte la prod, et un enregistrement joker `*` porte `rec.` et `dev.`. `provision-host.sh` publie +ces deux enregistrements par l'API deSEC : le DNS est décrit par le code comme le reste. Tout +poste du réseau de l'école les résout sans configuration ; hors de ce réseau, l'IP ne mène +nulle part. -**Des certificats Let's Encrypt par défi DNS-01.** Le défi passe par l'API DuckDNS, qui pose -l'enregistrement TXT : Let's Encrypt n'a jamais à joindre la VM. `make tls-duckdns` (acme.sh +**Des certificats Let's Encrypt par défi DNS-01.** Le défi passe par l'API deSEC, qui pose +l'enregistrement TXT : Let's Encrypt n'a jamais à joindre la VM. `make tls-desec` (acme.sh épinglé) le joue dans chaque stack ; il ne renouvelle qu'à échéance, d'où son rejeu à chaque -déploiement et chaque nuit par cron. Un certificat par environnement : DuckDNS ne tient qu'un -TXT à la fois, ce qui interdit un certificat unique pour le domaine et son joker. +déploiement et chaque nuit par cron. Un certificat par environnement plutôt qu'un joker : chaque +stack garde le sien, et la clé de la prod n'est pas lisible depuis le clone de dev. **Un frontal SNI sur 443, le seul composant exposé.** `infra/front`, un nginx sur le réseau de l'hôte, lit le nom demandé dans le ClientHello et relaie le flux TLS intact vers la stack visée, @@ -49,6 +52,8 @@ clone de la prod, en retard sur `main`, sans dépendre de son `.env.example`. - **Garder `/etc/hosts` et l'auto-signé** : trois manipulations par poste et trois avertissements, précisément ce qu'il fallait supprimer. +- **DuckDNS** : premier choix, inscription en un clic, mais bloqué par le filtrage de l'école : + sans son API, pas de défi DNS-01. - **nip.io ou sslip.io** : résolution sans compte, mais aucun moyen d'y obtenir un certificat. - **Services à certificat joker public (traefik.me, local-ip.co)** : leur clé privée est publiée par conception, n'importe qui peut usurper ces noms. @@ -67,9 +72,12 @@ clone de la prod, en retard sur `main`, sans dépendre de son `.env.example`. aux services homonymes, tombe : le frontal ne joint que des ports de la boucle locale. - Sans le frontal, plus rien n'est joignable sur la VM. `deploy.yml` le relance à chaque déploiement de la prod, et son `restart: unless-stopped` le ramène après un redémarrage. -- Le jeton DuckDNS vit dans `/srv/enervision/duckdns.token`, jamais dans git, GitHub ni le state - Terraform. Qui le détient peut repointer les trois noms. -- DuckDNS devient une dépendance : s'il tombe, les noms cessent de résoudre et les +- Le jeton deSEC vit dans `/srv/enervision/desec.token`, jamais dans git, GitHub ni le state + Terraform ; acme.sh en garde une copie dans `infra/proxy/acme/`, retirée à la lecture des + autres comptes. Qui le détient peut repointer les trois noms. +- deSEC devient une dépendance : s'il tombe, les noms cessent de résoudre et les renouvellements échouent. Les certificats valent 90 jours, la marge est large. +- Un filtrage de l'école qui viendrait à bloquer deSEC arrêterait les renouvellements, pas les + noms : la résolution passe par les serveurs DNS de l'école, pas par le site. - Les noms sont publics mais ne mènent qu'à une IP privée : ils révèlent l'existence de la VM, pas son contenu. diff --git a/docs/architecture/10-infra.md b/docs/architecture/10-infra.md index a5546d0..1fb5b6f 100644 --- a/docs/architecture/10-infra.md +++ b/docs/architecture/10-infra.md @@ -238,7 +238,7 @@ Deux conséquences se propagent jusqu'à l'application, et elles ne se devinent Statut : `En cours`. Décision et motifs dans l'[ADR 0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md), étendue à un troisième environnement par l'[ADR 0017](../adr/0017-environnement-dev-a-la-demande.md) ; noms, -certificats et frontal sans port dans l'[ADR 0018](../adr/0018-noms-duckdns-certificats-dns01-et-frontal-sni.md). +certificats et frontal sans port dans l'[ADR 0018](../adr/0018-noms-desec-certificats-dns01-et-frontal-sni.md). La VM `eadl-2025-nantes-g3` porte le développement, la recette et la production, chacun dans son clone du dépôt, son `.env` et son projet Compose. Le nom de projet préfixe volumes, réseau et conteneurs : rien n'est partagé. `scripts/provision-host.sh` prépare les trois dossiers, génère @@ -248,20 +248,20 @@ les secrets et les certificats, et ne démarre rien. |---|---|---|---| | Branche, environnement GitHub | toute branche lancée à la main, `dev` | `dev`, `rec` | `main`, `prod` | | Dossier, projet Compose | `/srv/enervision/dev`, `enervision-dev` | `/srv/enervision/rec`, `enervision-rec` | `/srv/enervision/prod`, `enervision-prod` | -| URL | `https://dev.enervision-g3.duckdns.org` | `https://rec.enervision-g3.duckdns.org` | `https://enervision-g3.duckdns.org` | +| URL | `https://dev.enervision-g3.dedyn.io` | `https://rec.enervision-g3.dedyn.io` | `https://enervision-g3.dedyn.io` | | Proxy HTTP, HTTPS, PROXY protocol, sur `127.0.0.1` | `8083`, `9443`, `9444` | `8081`, `8443`, `8444` | `10080`, `10443`, `10444` | | PostgreSQL, Mailpit, Airflow, sur `127.0.0.1` | `5435`, `8027`, `8084` | `5434`, `8026`, `8082` | `5433`, `8025`, `8080` | | Supervision (profil `monitoring`) | à la demande, `make monitoring-up` | à la demande, `make monitoring-up` | active, `COMPOSE_PROFILES=monitoring` | | Grafana, Prometheus, Alertmanager, sur `127.0.0.1` | `3003`, `9092`, `9095` | `3002`, `9091`, `9094` | `3001`, `9090`, `9093` | -Les trois noms sont publics chez DuckDNS et visent l'IP privée de la VM : rien à déclarer sur +Les trois noms sont publics chez deSEC et visent l'IP privée de la VM : rien à déclarer sur les postes du réseau de l'école, et rien n'est joignable hors de ce réseau. Trois noms distincts sont nécessaires : le cookie `__Secure-ev_refresh` est posé par hôte, pas par port. Aucune stack ne publie hors de la boucle locale. Le frontal `infra/front`, sur le réseau de l'hôte, écoute 80 et 443 : il redirige le premier, et aiguille le second d'après le nom demandé (SNI) vers l'écouteur PROXY protocol de la stack visée, sans déchiffrer le TLS. Chaque stack -garde son certificat Let's Encrypt, obtenu par défi DNS-01 (`make tls-duckdns`) et renouvelé à +garde son certificat Let's Encrypt, obtenu par défi DNS-01 (`make tls-desec`) et renouvelé à chaque déploiement ainsi que chaque nuit par `/etc/cron.d/enervision-tls`. Le déploiement est décrit dans [50-cicd.md](50-cicd.md) : un runner GitHub Actions installé sur diff --git a/infra/README.md b/infra/README.md index 7df569b..7d0ee54 100644 --- a/infra/README.md +++ b/infra/README.md @@ -38,10 +38,10 @@ Apres l'apply, la machine porte `/srv/enervision/dev`, `/srv/enervision/rec` et manuel, `make stack-up` dans chaque dossier ; les suivants sont joues par le runner a chaque push sur `dev` et sur `main`, et a chaque lancement manuel d'une autre branche pour `dev`. -Noms et certificats (ADR 0018) : avant l'apply, l'enregistrement DuckDNS de `domaine` doit viser -`ssh_host`, et son jeton se trouver dans `/duckdns.token` (600, proprietaire). L'apply -obtient alors un certificat Let's Encrypt par environnement et planifie leur renouvellement ; -sans jeton, chaque environnement garde un certificat auto-signe. Le frontal SNI (`infra/front`) +Noms et certificats (ADR 0018) : avant l'apply, le jeton deSEC de `domaine` doit se trouver dans +`/desec.token` (600, proprietaire). L'apply fait alors pointer le domaine et son joker +vers la machine, obtient un certificat Let's Encrypt par environnement et planifie leur +renouvellement ; sans jeton, chaque environnement garde un certificat auto-signe. Le frontal SNI (`infra/front`) se demarre une fois depuis le dossier de la prod, `make front-up`. Retirer le runner se fait a la main, depuis les parametres du depot : `terraform destroy` ne le diff --git a/infra/proxy/README.md b/infra/proxy/README.md index c865d51..b3ec6eb 100644 --- a/infra/proxy/README.md +++ b/infra/proxy/README.md @@ -78,12 +78,12 @@ Renouvellement, à passer en tâche planifiée sur la machine : ### Let's Encrypt par DNS-01, le mode de la VM -La VM n'a qu'une IP privée : le défi HTTP-01 y est impossible. Ses trois noms sont chez DuckDNS, +La VM n'a qu'une IP privée : le défi HTTP-01 y est impossible. Ses trois noms sont chez deSEC, dont l'API pose l'enregistrement TXT du défi DNS-01, et acme.sh le fait sans rien ouvrir -([ADR 0018](../../docs/adr/0018-noms-duckdns-certificats-dns01-et-frontal-sni.md)). +([ADR 0018](../../docs/adr/0018-noms-desec-certificats-dns01-et-frontal-sni.md)). ```bash -make tls-duckdns # PUBLIC_HOST lu dans .env, jeton dans ../duckdns.token (600) +make tls-desec # PUBLIC_HOST lu dans .env, jeton dans ../desec.token (600) ``` La cible est rejouable : acme.sh ne renouvelle qu'à trente jours de l'échéance, installe le diff --git a/infra/terraform/environments/vm-eni/variables.tf b/infra/terraform/environments/vm-eni/variables.tf index 55ebea2..d8479a9 100644 --- a/infra/terraform/environments/vm-eni/variables.tf +++ b/infra/terraform/environments/vm-eni/variables.tf @@ -45,8 +45,8 @@ variable "depot_url" { variable "domaine" { type = string - description = "Nom DuckDNS de la prod, rec. et dev. en sous-domaines (ADR 0018). Son enregistrement doit viser ssh_host, et son jeton se trouver dans /duckdns.token sur la machine." - default = "enervision-g3.duckdns.org" + description = "Domaine deSEC de la prod, rec. et dev. en sous-domaines (ADR 0018). Son jeton doit se trouver dans /desec.token sur la machine : provision-host.sh y fait pointer le domaine et son joker vers la machine." + default = "enervision-g3.dedyn.io" } variable "adresse_publique" { diff --git a/scripts/provision-host.sh b/scripts/provision-host.sh index 7808457..baae538 100755 --- a/scripts/provision-host.sh +++ b/scripts/provision-host.sh @@ -15,10 +15,10 @@ set -euo pipefail DEPOT="${REPO_URL:-https://github.com/ineszang/ProjetPiscine_EnerVision.git}" RACINE="${RACINE:-/srv/enervision}" -DOMAINE="${DOMAINE:-enervision-g3.duckdns.org}" +DOMAINE="${DOMAINE:-enervision-g3.dedyn.io}" ADRESSE="${PUBLIC_IP:-$(hostname -I | awk '{print $1}')}" PROPRIETAIRE="${PROPRIETAIRE:-${SUDO_USER:-}}" -JETON_DUCKDNS="$RACINE/duckdns.token" +JETON_DESEC="$RACINE/desec.token" COMPOSE_MINIMALE="2.24.4" erreur() { echo "erreur : $*" >&2; exit 1; } @@ -130,19 +130,34 @@ preparer() { | grep -q " does match"; then (cd "$dossier" && PUBLIC_HOST="$hote" PUBLIC_IP="$ADRESSE" ./scripts/tls-selfsigned.sh --force) fi - if [[ -r "$JETON_DUCKDNS" && "$hote" == *.duckdns.org ]]; then - make -C "$dossier" --no-print-directory tls-duckdns PUBLIC_HOST="$hote" \ + if [[ -r "$JETON_DESEC" && "$hote" == *.dedyn.io ]]; then + make -C "$dossier" --no-print-directory tls-desec PUBLIC_HOST="$hote" \ || echo "$env : pas de certificat Let's Encrypt, l'auto-signé reste en place" >&2 fi echo "$env : $dossier sur $branche, https://$hote" } +# Le jeton passe par `curl --config -`, donc par l'entrée standard et jamais par `argv`. +publier_dns() { + [[ -r "$JETON_DESEC" ]] || { echo "pas de jeton $JETON_DESEC : ni DNS ni Let's Encrypt"; return 0; } + local enregistrements + enregistrements="[{\"subname\": \"\", \"type\": \"A\", \"ttl\": 3600, \"records\": [\"$ADRESSE\"]}, + {\"subname\": \"*\", \"type\": \"A\", \"ttl\": 3600, \"records\": [\"$ADRESSE\"]}]" + if printf 'header = "Authorization: Token %s"\n' "$(tr -d '[:space:]' < "$JETON_DESEC")" \ + | curl -fsS --max-time 20 --config - -X PATCH -H "Content-Type: application/json" \ + --data "$enregistrements" "https://desec.io/api/v1/domains/$DOMAINE/rrsets/" >/dev/null; then + echo "DNS : $DOMAINE et *.$DOMAINE visent $ADRESSE" + else + echo "DNS : deSEC refuse la mise à jour de $DOMAINE, enregistrements inchangés" >&2 + fi +} + planifier_renouvellement() { [[ "$(id -u)" -eq 0 && -n "$PROPRIETAIRE" && -d /etc/cron.d ]] || return 0 cat > /etc/cron.d/enervision-tls <&1 | logger -t enervision-tls +23 4 * * * $PROPRIETAIRE for e in prod rec dev; do make -C $RACINE/\$e --no-print-directory tls-desec; done 2>&1 | logger -t enervision-tls CRON chmod 644 /etc/cron.d/enervision-tls echo "renouvellement planifié : /etc/cron.d/enervision-tls" @@ -150,6 +165,7 @@ CRON verifier_outils mkdir -p "$RACINE" +publier_dns # Supervision active en prod seulement (ADR 0016). Le frontal (infra/front) publie 80 et 443 et # relaie vers les ports `front` ; les stacks ne publient plus rien hors de la boucle locale. @@ -176,6 +192,6 @@ L'installer sous le propriétaire de $RACINE, sinon git refuse ces dépôts et l échappe : relancer au besoin ce script avec PROPRIETAIRE=. Données historiques : git ne porte pas data/raw, déposer les fichiers dans chaque dossier avant de déclencher le DAG historical_import. -Noms et certificats : l'enregistrement DuckDNS de $DOMAINE doit viser $ADRESSE, et son jeton -se trouver dans $JETON_DUCKDNS (600, propriétaire du runner). Sans jeton : auto-signé. +Noms et certificats : le jeton deSEC de $DOMAINE doit se trouver dans $JETON_DESEC (600, +propriétaire du runner). Sans lui, ni enregistrement DNS ni Let's Encrypt : auto-signé. FIN