fix(deploy): passe à dynv6 et rend le défi DNS-01 indépendant du fournisseur
deSEC n'ouvre plus de nouveaux domaines dedyn.io, et duckdns.org est filtré par l'école. dynv6 répond depuis les postes et depuis la VM. - Zone enervision-g3.dynv6.net ; provision-host.sh pointe la zone, rec et dev vers la VM par l'API dynv6 (bloc Python, idempotent). - make tls-dns01 remplace tls-desec : DNS01_API et DNS01_JETON_VAR nomment le greffon acme.sh, le jeton vit dans ../dns.token quel que soit le fournisseur. Un domaine acheté ne demandera que ces variables. - deploy.yml ne demande un certificat qu'à un .env qui ne porte plus de nom en .local. - ADR 0018 renommé noms-publics : deSEC et DuckDNS en alternatives.
This commit is contained in:
@@ -28,7 +28,7 @@ jobs:
|
|||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
environment:
|
environment:
|
||||||
name: ${{ github.ref_name == 'main' && 'prod' || github.ref_name == 'dev' && 'rec' || 'dev' }}
|
name: ${{ github.ref_name == 'main' && 'prod' || github.ref_name == 'dev' && 'rec' || 'dev' }}
|
||||||
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' }}
|
url: ${{ github.ref_name == 'main' && 'https://enervision-g3.dynv6.net' || github.ref_name == 'dev' && 'https://rec.enervision-g3.dynv6.net' || 'https://dev.enervision-g3.dynv6.net' }}
|
||||||
env:
|
env:
|
||||||
ENVIRONNEMENT: ${{ github.ref_name == 'main' && 'prod' || github.ref_name == 'dev' && 'rec' || 'dev' }}
|
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' }}
|
PORT_HTTPS: ${{ github.ref_name == 'main' && '10443' || github.ref_name == 'dev' && '8443' || '9443' }}
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
|
|
||||||
echo "::group::Reconstruit et redémarre la stack"
|
echo "::group::Reconstruit et redémarre la stack"
|
||||||
# Un `.env` pas encore réaligné par provision-host.sh porte encore un nom en `.local`.
|
# 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
|
if [ -r ../dns.token ] && ! grep -q '^PUBLIC_HOST=.*\.local$' .env; then make tls-dns01; fi
|
||||||
make stack-up
|
make stack-up
|
||||||
if [ "${ENVIRONNEMENT}" = prod ]; then make front-up; fi
|
if [ "${ENVIRONNEMENT}" = prod ]; then make front-up; fi
|
||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ DEMO_NOW ?= 2024-12-31T00:00:00Z
|
|||||||
migrate migrate-test bootstrap-admin services-up demo-data demo-data-force \
|
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 \
|
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 \
|
airflow-lint airflow-test airflow-check airflow-up airflow-down airflow-logs \
|
||||||
tls-selfsigned tls-acme tls-renew tls-desec front-up stack-up stack-down stack-logs \
|
tls-selfsigned tls-acme tls-renew tls-dns01 front-up stack-up stack-down stack-logs \
|
||||||
e2e-install e2e-prepare e2e load-smoke load-test load-stress load-limits \
|
e2e-install e2e-prepare e2e load-smoke load-test load-stress load-limits \
|
||||||
db-ensure-supervision monitoring-up monitoring-down monitoring-logs monitoring-check
|
db-ensure-supervision monitoring-up monitoring-down monitoring-logs monitoring-check
|
||||||
|
|
||||||
@@ -236,20 +236,22 @@ tls-renew: ## Renouvelle les certificats Let's Encrypt et recharge le proxy
|
|||||||
$(COMPOSE_PROD) exec proxy nginx -s reload
|
$(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
|
# 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 deSEC (ADR 0018). Le jeton transite par l'environnement, jamais par `argv`.
|
# par l'API du fournisseur DNS, dynv6 par défaut (ADR 0018). Le jeton ne passe jamais par `argv`.
|
||||||
ACME_SH := neilpang/acme.sh:3.1.6
|
ACME_SH := neilpang/acme.sh:3.1.6
|
||||||
DESEC_TOKEN_FILE ?= $(abspath $(CURDIR)/../desec.token)
|
DNS01_API ?= dns_dynv6
|
||||||
acme-sh = docker run --rm --user "$$(id -u):$$(id -g)" -e DEDYN_TOKEN -e AUTO_UPGRADE=0 \
|
DNS01_JETON_VAR ?= DYNV6_TOKEN
|
||||||
|
DNS01_JETON_FICHIER ?= $(abspath $(CURDIR)/../dns.token)
|
||||||
|
acme-sh = docker run --rm --user "$$(id -u):$$(id -g)" -e $(DNS01_JETON_VAR) -e AUTO_UPGRADE=0 \
|
||||||
-v "$(CURDIR)/infra/proxy/acme:/acme.sh" -v "$(CURDIR)/infra/proxy/tls:/tls" $(ACME_SH)
|
-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, et recopie le jeton dans
|
# 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.
|
# 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
|
tls-dns01: ## Certificat Let's Encrypt par DNS-01, renouvelé seulement à échéance. Jeton : ../dns.token
|
||||||
@case "$(PUBLIC_HOST)" in *.dedyn.io) ;; *) echo "PUBLIC_HOST=$(PUBLIC_HOST) n'est pas un nom deSEC"; exit 1 ;; esac
|
@case "$(PUBLIC_HOST)" in *.local | localhost) echo "PUBLIC_HOST=$(PUBLIC_HOST) n'est pas un nom public"; exit 1 ;; esac
|
||||||
@test -r "$(DESEC_TOKEN_FILE)" || { echo "Jeton deSEC illisible : $(DESEC_TOKEN_FILE)"; exit 1; }
|
@test -r "$(DNS01_JETON_FICHIER)" || { echo "Jeton DNS illisible : $(DNS01_JETON_FICHIER)"; exit 1; }
|
||||||
@mkdir -p infra/proxy/acme && chmod 700 infra/proxy/acme
|
@mkdir -p infra/proxy/acme && chmod 700 infra/proxy/acme
|
||||||
@DEDYN_TOKEN="$$(tr -d '[:space:]' < "$(DESEC_TOKEN_FILE)")"; export DEDYN_TOKEN; \
|
@$(DNS01_JETON_VAR)="$$(tr -d '[:space:]' < "$(DNS01_JETON_FICHIER)")"; export $(DNS01_JETON_VAR); \
|
||||||
$(acme-sh) --issue --server letsencrypt --dns dns_desec -d "$(PUBLIC_HOST)"; \
|
$(acme-sh) --issue --server letsencrypt --dns $(DNS01_API) -d "$(PUBLIC_HOST)"; \
|
||||||
code=$$?; chmod -R go-rwx infra/proxy/acme; [ $$code -eq 0 ] || [ $$code -eq 2 ] || exit $$code
|
code=$$?; chmod -R go-rwx infra/proxy/acme; [ $$code -eq 0 ] || [ $$code -eq 2 ] || exit $$code
|
||||||
@$(acme-sh) --install-cert --ecc -d "$(PUBLIC_HOST)" \
|
@$(acme-sh) --install-cert --ecc -d "$(PUBLIC_HOST)" \
|
||||||
--fullchain-file /tls/fullchain.pem --key-file /tls/privkey.pem
|
--fullchain-file /tls/fullchain.pem --key-file /tls/privkey.pem
|
||||||
|
|||||||
+16
-10
@@ -1,4 +1,4 @@
|
|||||||
# 0018 - Noms deSEC, certificats Let's Encrypt par DNS-01 et frontal SNI sans port
|
# 0018 - Noms publics, certificats Let's Encrypt par DNS-01 et frontal SNI sans port
|
||||||
|
|
||||||
- Statut : accepté
|
- Statut : accepté
|
||||||
- Date : 2026-09-23
|
- Date : 2026-09-23
|
||||||
@@ -15,19 +15,19 @@ poste.
|
|||||||
Contraintes : la VM n'a qu'une IP privée, `10.101.200.37`, que ni Internet ni Let's Encrypt ne
|
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é. Vérifications faites le 23/09 : les
|
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
|
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
|
vers Let's Encrypt et vers l'API de dynv6, mais le filtrage de l'école bloque duckdns.org, site
|
||||||
et API, depuis les postes comme depuis la VM.
|
et API, depuis les postes comme depuis la VM.
|
||||||
|
|
||||||
## Décision
|
## Décision
|
||||||
|
|
||||||
**Des noms publics qui visent l'IP privée.** `enervision-g3.dedyn.io`, zone gratuite de deSEC,
|
**Des noms publics qui visent l'IP privée.** `enervision-g3.dynv6.net`, zone gratuite de dynv6,
|
||||||
porte la prod, et un enregistrement joker `*` porte `rec.` et `dev.`. `provision-host.sh` publie
|
porte la prod, et deux enregistrements A portent `rec.` et `dev.`. `provision-host.sh` les publie
|
||||||
ces deux enregistrements par l'API deSEC : le DNS est décrit par le code comme le reste. Tout
|
par l'API dynv6 : 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
|
poste du réseau de l'école les résout sans configuration ; hors de ce réseau, l'IP ne mène
|
||||||
nulle part.
|
nulle part.
|
||||||
|
|
||||||
**Des certificats Let's Encrypt par défi DNS-01.** Le défi passe par l'API deSEC, qui pose
|
**Des certificats Let's Encrypt par défi DNS-01.** Le défi passe par l'API dynv6, qui pose
|
||||||
l'enregistrement TXT : Let's Encrypt n'a jamais à joindre la VM. `make tls-desec` (acme.sh
|
l'enregistrement TXT : Let's Encrypt n'a jamais à joindre la VM. `make tls-dns01` (acme.sh
|
||||||
épinglé) le joue dans chaque stack ; il ne renouvelle qu'à échéance, d'où son rejeu à chaque
|
é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 plutôt qu'un joker : chaque
|
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.
|
stack garde le sien, et la clé de la prod n'est pas lisible depuis le clone de dev.
|
||||||
@@ -43,6 +43,10 @@ utilisateur bloquerait la connexion de tous. Chaque proxy de stack reçoit donc
|
|||||||
écouteur dédié, 4443, qui exige l'en-tête PROXY protocol et en tire l'IP du client. Le 443 de
|
écouteur dédié, 4443, qui exige l'en-tête PROXY protocol et en tire l'IP du client. Le 443 de
|
||||||
la stack reste sans PROXY protocol, pour les postes de développement et la sonde du déploiement.
|
la stack reste sans PROXY protocol, pour les postes de développement et la sonde du déploiement.
|
||||||
|
|
||||||
|
**Le fournisseur est un paramètre.** `DNS01_API` et `DNS01_JETON_VAR` nomment le greffon acme.sh,
|
||||||
|
le jeton vit dans `dns.token` quel que soit le fournisseur : passer à un domaine acheté chez
|
||||||
|
Cloudflare ou OVH ne demande que ces deux variables et `domaine`, plus `publier_dns()`.
|
||||||
|
|
||||||
**`scripts/provision-host.sh` fait foi pour l'adressage et les secrets.** Un `.env` existant
|
**`scripts/provision-host.sh` fait foi pour l'adressage et les secrets.** Un `.env` existant
|
||||||
garde ses secrets, reçoit ceux qui lui manquent et voit hôte, ports et profils réalignés sur le
|
garde ses secrets, reçoit ceux qui lui manquent et voit hôte, ports et profils réalignés sur le
|
||||||
tableau du script. C'est ce qui permet de migrer trois `.env` nés avant ce changement, et le
|
tableau du script. C'est ce qui permet de migrer trois `.env` nés avant ce changement, et le
|
||||||
@@ -54,6 +58,8 @@ clone de la prod, en retard sur `main`, sans dépendre de son `.env.example`.
|
|||||||
avertissements, précisément ce qu'il fallait supprimer.
|
avertissements, précisément ce qu'il fallait supprimer.
|
||||||
- **DuckDNS** : premier choix, inscription en un clic, mais bloqué par le filtrage de l'école :
|
- **DuckDNS** : premier choix, inscription en un clic, mais bloqué par le filtrage de l'école :
|
||||||
sans son API, pas de défi DNS-01.
|
sans son API, pas de défi DNS-01.
|
||||||
|
- **deSEC (`dedyn.io`)** : joignable et associatif, mais les inscriptions de nouveaux domaines
|
||||||
|
`dedyn.io` étaient fermées le 23/09 ; il reste le bon choix pour un domaine acheté.
|
||||||
- **nip.io ou sslip.io** : résolution sans compte, mais aucun moyen d'y obtenir un certificat.
|
- **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
|
- **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.
|
par conception, n'importe qui peut usurper ces noms.
|
||||||
@@ -72,12 +78,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.
|
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
|
- 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.
|
déploiement de la prod, et son `restart: unless-stopped` le ramène après un redémarrage.
|
||||||
- Le jeton deSEC vit dans `/srv/enervision/desec.token`, jamais dans git, GitHub ni le state
|
- Le jeton dynv6 vit dans `/srv/enervision/dns.token`, jamais dans git, GitHub ni le state
|
||||||
Terraform ; acme.sh en garde une copie dans `infra/proxy/acme/`, retirée à la lecture des
|
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.
|
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
|
- dynv6 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.
|
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
|
- Un filtrage de l'école qui viendrait à bloquer dynv6 arrêterait les renouvellements, pas les
|
||||||
noms : la résolution passe par les serveurs DNS de l'école, pas par le site.
|
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,
|
- 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.
|
pas son contenu.
|
||||||
@@ -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
|
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
|
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,
|
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-desec-certificats-dns01-et-frontal-sni.md).
|
certificats et frontal sans port dans l'[ADR 0018](../adr/0018-noms-publics-certificats-dns01-et-frontal-sni.md).
|
||||||
La VM `eadl-2025-nantes-g3` porte le développement, la recette et la production, chacun dans son
|
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
|
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
|
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` |
|
| 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` |
|
| Dossier, projet Compose | `/srv/enervision/dev`, `enervision-dev` | `/srv/enervision/rec`, `enervision-rec` | `/srv/enervision/prod`, `enervision-prod` |
|
||||||
| URL | `https://dev.enervision-g3.dedyn.io` | `https://rec.enervision-g3.dedyn.io` | `https://enervision-g3.dedyn.io` |
|
| URL | `https://dev.enervision-g3.dynv6.net` | `https://rec.enervision-g3.dynv6.net` | `https://enervision-g3.dynv6.net` |
|
||||||
| Proxy HTTP, HTTPS, PROXY protocol, sur `127.0.0.1` | `8083`, `9443`, `9444` | `8081`, `8443`, `8444` | `10080`, `10443`, `10444` |
|
| 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` |
|
| 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` |
|
| 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` |
|
| Grafana, Prometheus, Alertmanager, sur `127.0.0.1` | `3003`, `9092`, `9095` | `3002`, `9091`, `9094` | `3001`, `9090`, `9093` |
|
||||||
|
|
||||||
Les trois noms sont publics chez deSEC et visent l'IP privée de la VM : rien à déclarer sur
|
Les trois noms sont publics chez dynv6 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
|
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.
|
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
|
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é
|
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
|
(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-desec`) et renouvelé à
|
garde son certificat Let's Encrypt, obtenu par défi DNS-01 (`make tls-dns01`) et renouvelé à
|
||||||
chaque déploiement ainsi que chaque nuit par `/etc/cron.d/enervision-tls`.
|
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
|
Le déploiement est décrit dans [50-cicd.md](50-cicd.md) : un runner GitHub Actions installé sur
|
||||||
|
|||||||
+3
-3
@@ -38,9 +38,9 @@ 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
|
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`.
|
sur `dev` et sur `main`, et a chaque lancement manuel d'une autre branche pour `dev`.
|
||||||
|
|
||||||
Noms et certificats (ADR 0018) : avant l'apply, le jeton deSEC de `domaine` doit se trouver dans
|
Noms et certificats (ADR 0018) : avant l'apply, la zone `domaine` doit exister chez dynv6 et son
|
||||||
`<racine>/desec.token` (600, proprietaire). L'apply fait alors pointer le domaine et son joker
|
jeton se trouver dans `<racine>/dns.token` (600, proprietaire). L'apply fait alors pointer la
|
||||||
vers la machine, obtient un certificat Let's Encrypt par environnement et planifie leur
|
zone, `rec` et `dev` 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`)
|
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`.
|
se demarre une fois depuis le dossier de la prod, `make front-up`.
|
||||||
|
|
||||||
|
|||||||
@@ -78,15 +78,16 @@ Renouvellement, à passer en tâche planifiée sur la machine :
|
|||||||
|
|
||||||
### Let's Encrypt par DNS-01, le mode de la VM
|
### 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 deSEC,
|
La VM n'a qu'une IP privée : le défi HTTP-01 y est impossible. Ses trois noms sont chez dynv6,
|
||||||
dont l'API pose l'enregistrement TXT du défi DNS-01, et acme.sh le fait sans rien ouvrir
|
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-desec-certificats-dns01-et-frontal-sni.md)).
|
([ADR 0018](../../docs/adr/0018-noms-publics-certificats-dns01-et-frontal-sni.md)).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make tls-desec # PUBLIC_HOST lu dans .env, jeton dans ../desec.token (600)
|
make tls-dns01 # PUBLIC_HOST lu dans .env, jeton dans ../dns.token (600)
|
||||||
```
|
```
|
||||||
|
|
||||||
La cible est rejouable : acme.sh ne renouvelle qu'à trente jours de l'échéance, installe le
|
Autre fournisseur : `DNS01_API` et `DNS01_JETON_VAR` nomment le greffon acme.sh et sa variable
|
||||||
|
(`dns_cf` et `CF_Token` pour Cloudflare, par exemple). La cible est rejouable : acme.sh ne renouvelle qu'à trente jours de l'échéance, installe le
|
||||||
résultat dans `tls/` et recharge le proxy s'il tourne. Son état vit dans `acme/`, ignoré par git.
|
résultat dans `tls/` et recharge le proxy s'il tourne. Son état vit dans `acme/`, ignoré par git.
|
||||||
`deploy.yml` la rejoue avant chaque `make stack-up`, et `/etc/cron.d/enervision-tls` chaque nuit.
|
`deploy.yml` la rejoue avant chaque `make stack-up`, et `/etc/cron.d/enervision-tls` chaque nuit.
|
||||||
|
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ variable "depot_url" {
|
|||||||
|
|
||||||
variable "domaine" {
|
variable "domaine" {
|
||||||
type = string
|
type = string
|
||||||
description = "Domaine deSEC de la prod, rec. et dev. en sous-domaines (ADR 0018). Son jeton doit se trouver dans <racine>/desec.token sur la machine : provision-host.sh y fait pointer le domaine et son joker vers la machine."
|
description = "Zone dynv6 de la prod, rec. et dev. en sous-domaines (ADR 0018). Son jeton doit se trouver dans <racine>/dns.token sur la machine : provision-host.sh y fait pointer la zone et ses deux sous-domaines vers la machine."
|
||||||
default = "enervision-g3.dedyn.io"
|
default = "enervision-g3.dynv6.net"
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "adresse_publique" {
|
variable "adresse_publique" {
|
||||||
|
|||||||
+37
-18
@@ -15,10 +15,10 @@ set -euo pipefail
|
|||||||
|
|
||||||
DEPOT="${REPO_URL:-https://github.com/ineszang/ProjetPiscine_EnerVision.git}"
|
DEPOT="${REPO_URL:-https://github.com/ineszang/ProjetPiscine_EnerVision.git}"
|
||||||
RACINE="${RACINE:-/srv/enervision}"
|
RACINE="${RACINE:-/srv/enervision}"
|
||||||
DOMAINE="${DOMAINE:-enervision-g3.dedyn.io}"
|
DOMAINE="${DOMAINE:-enervision-g3.dynv6.net}"
|
||||||
ADRESSE="${PUBLIC_IP:-$(hostname -I | awk '{print $1}')}"
|
ADRESSE="${PUBLIC_IP:-$(hostname -I | awk '{print $1}')}"
|
||||||
PROPRIETAIRE="${PROPRIETAIRE:-${SUDO_USER:-}}"
|
PROPRIETAIRE="${PROPRIETAIRE:-${SUDO_USER:-}}"
|
||||||
JETON_DESEC="$RACINE/desec.token"
|
JETON_DNS="$RACINE/dns.token"
|
||||||
COMPOSE_MINIMALE="2.24.4"
|
COMPOSE_MINIMALE="2.24.4"
|
||||||
|
|
||||||
erreur() { echo "erreur : $*" >&2; exit 1; }
|
erreur() { echo "erreur : $*" >&2; exit 1; }
|
||||||
@@ -130,26 +130,45 @@ preparer() {
|
|||||||
| grep -q " does match"; then
|
| grep -q " does match"; then
|
||||||
(cd "$dossier" && PUBLIC_HOST="$hote" PUBLIC_IP="$ADRESSE" ./scripts/tls-selfsigned.sh --force)
|
(cd "$dossier" && PUBLIC_HOST="$hote" PUBLIC_IP="$ADRESSE" ./scripts/tls-selfsigned.sh --force)
|
||||||
fi
|
fi
|
||||||
if [[ -r "$JETON_DESEC" && "$hote" == *.dedyn.io ]]; then
|
if [[ -r "$JETON_DNS" && "$hote" != *.local ]]; then
|
||||||
make -C "$dossier" --no-print-directory tls-desec PUBLIC_HOST="$hote" \
|
make -C "$dossier" --no-print-directory tls-dns01 PUBLIC_HOST="$hote" \
|
||||||
|| echo "$env : pas de certificat Let's Encrypt, l'auto-signé reste en place" >&2
|
|| echo "$env : pas de certificat Let's Encrypt, l'auto-signé reste en place" >&2
|
||||||
fi
|
fi
|
||||||
echo "$env : $dossier sur $branche, https://$hote"
|
echo "$env : $dossier sur $branche, https://$hote"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Le jeton passe par `curl --config -`, donc par l'entrée standard et jamais par `argv`.
|
# Pourquoi : dynv6 est le fournisseur que le filtrage de l'école laisse passer (ADR 0018). Le
|
||||||
|
# jeton passe par l'environnement du seul processus Python, jamais par `argv`.
|
||||||
publier_dns() {
|
publier_dns() {
|
||||||
[[ -r "$JETON_DESEC" ]] || { echo "pas de jeton $JETON_DESEC : ni DNS ni Let's Encrypt"; return 0; }
|
[[ -r "$JETON_DNS" ]] || { echo "pas de jeton $JETON_DNS : ni DNS ni Let's Encrypt"; return 0; }
|
||||||
local enregistrements
|
DNS_TOKEN="$(tr -d '[:space:]' < "$JETON_DNS")" python3 - "$DOMAINE" "$ADRESSE" rec dev <<'PY' \
|
||||||
enregistrements="[{\"subname\": \"\", \"type\": \"A\", \"ttl\": 3600, \"records\": [\"$ADRESSE\"]},
|
|| echo "DNS : dynv6 refuse la mise à jour de $DOMAINE, enregistrements inchangés" >&2
|
||||||
{\"subname\": \"*\", \"type\": \"A\", \"ttl\": 3600, \"records\": [\"$ADRESSE\"]}]"
|
import json, os, sys, urllib.request
|
||||||
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" \
|
domaine, adresse, *sous_noms = sys.argv[1:]
|
||||||
--data "$enregistrements" "https://desec.io/api/v1/domains/$DOMAINE/rrsets/" >/dev/null; then
|
|
||||||
echo "DNS : $DOMAINE et *.$DOMAINE visent $ADRESSE"
|
def appel(methode, chemin, corps=None):
|
||||||
else
|
requete = urllib.request.Request(
|
||||||
echo "DNS : deSEC refuse la mise à jour de $DOMAINE, enregistrements inchangés" >&2
|
f"https://dynv6.com/api/v2/{chemin}", method=methode,
|
||||||
fi
|
data=None if corps is None else json.dumps(corps).encode(),
|
||||||
|
headers={"Authorization": f"Bearer {os.environ['DNS_TOKEN']}",
|
||||||
|
"Content-Type": "application/json", "Accept": "application/json"})
|
||||||
|
with urllib.request.urlopen(requete, timeout=20) as reponse:
|
||||||
|
contenu = reponse.read()
|
||||||
|
return json.loads(contenu) if contenu else None
|
||||||
|
|
||||||
|
zone = appel("GET", f"zones/by-name/{domaine}")
|
||||||
|
if zone.get("ipv4address") != adresse:
|
||||||
|
appel("PATCH", f"zones/{zone['id']}", {"ipv4address": adresse})
|
||||||
|
existants = {(r["type"], r["name"]): r for r in appel("GET", f"zones/{zone['id']}/records")}
|
||||||
|
for nom in sous_noms:
|
||||||
|
actuel = existants.get(("A", nom))
|
||||||
|
if actuel is None:
|
||||||
|
appel("POST", f"zones/{zone['id']}/records", {"type": "A", "name": nom, "data": adresse})
|
||||||
|
elif actuel["data"] != adresse:
|
||||||
|
appel("PATCH", f"zones/{zone['id']}/records/{actuel['id']}", {"data": adresse})
|
||||||
|
print(f"DNS : {domaine}, {', '.join(sous_noms)} visent {adresse}")
|
||||||
|
PY
|
||||||
}
|
}
|
||||||
|
|
||||||
planifier_renouvellement() {
|
planifier_renouvellement() {
|
||||||
@@ -157,7 +176,7 @@ planifier_renouvellement() {
|
|||||||
cat > /etc/cron.d/enervision-tls <<CRON
|
cat > /etc/cron.d/enervision-tls <<CRON
|
||||||
# Renouvellement Let's Encrypt des trois environnements (ADR 0018), écrit par provision-host.sh.
|
# Renouvellement Let's Encrypt des trois environnements (ADR 0018), écrit par provision-host.sh.
|
||||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
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
|
23 4 * * * $PROPRIETAIRE for e in prod rec dev; do make -C $RACINE/\$e --no-print-directory tls-dns01; done 2>&1 | logger -t enervision-tls
|
||||||
CRON
|
CRON
|
||||||
chmod 644 /etc/cron.d/enervision-tls
|
chmod 644 /etc/cron.d/enervision-tls
|
||||||
echo "renouvellement planifié : /etc/cron.d/enervision-tls"
|
echo "renouvellement planifié : /etc/cron.d/enervision-tls"
|
||||||
@@ -192,6 +211,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=<utilisateur du runner>.
|
échappe : relancer au besoin ce script avec PROPRIETAIRE=<utilisateur du runner>.
|
||||||
Données historiques : git ne porte pas data/raw, déposer les fichiers dans chaque dossier avant
|
Données historiques : git ne porte pas data/raw, déposer les fichiers dans chaque dossier avant
|
||||||
de déclencher le DAG historical_import.
|
de déclencher le DAG historical_import.
|
||||||
Noms et certificats : le jeton deSEC de $DOMAINE doit se trouver dans $JETON_DESEC (600,
|
Noms et certificats : le jeton dynv6 de la zone $DOMAINE doit se trouver dans $JETON_DNS (600,
|
||||||
propriétaire du runner). Sans lui, ni enregistrement DNS ni Let's Encrypt : auto-signé.
|
propriétaire du runner). Sans lui, ni enregistrement DNS ni Let's Encrypt : auto-signé.
|
||||||
FIN
|
FIN
|
||||||
|
|||||||
Reference in New Issue
Block a user