feat(deploy): URL sans port et certificats Let's Encrypt sur la VM ENI
Les trois environnements passent sur enervision-g3.duckdns.org, rec. et dev. : noms publics qui visent l'IP privée de la VM, donc résolus sans /etc/hosts sur le réseau de l'école et injoignables ailleurs (ADR 0018). - infra/front : nginx sur le réseau de l'hôte, seul exposé en 80 et 443. Aiguille par SNI vers la stack visée sans déchiffrer le TLS, et lui transmet l'IP du client en PROXY protocol. - Proxy de stack : écouteur 4443 en PROXY protocol, real_ip_header ; sans lui, limit_req et get_client_ip() compteraient tous les postes comme un seul. PROXY_FRONT_PORT le publie sur 127.0.0.1. - make tls-duckdns : Let's Encrypt par défi DNS-01 via l'API DuckDNS (acme.sh 3.1.6), rejouable, rejoué à chaque déploiement et chaque nuit. - provision-host.sh fait foi pour l'adressage et les secrets : un .env existant garde ses secrets, reçoit ceux qui manquent (supervision) et voit hôte et ports réalignés. Planifie le renouvellement des certificats. - deploy.yml : nouvelles URL, sonde prod sur 10443, front-up en prod. - Terraform : variable domaine. CI : validation du frontal.
This commit is contained in:
@@ -38,6 +38,12 @@ 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 `<racine>/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`)
|
||||
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
|
||||
desinscrit pas.
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Pourquoi : le réseau de l'hôte, parce que les trois stacks publient leur écouteur PROXY protocol
|
||||
# sur 127.0.0.1 et que seul un conteneur sur l'hôte joint cette boucle locale (ADR 0018).
|
||||
|
||||
name: enervision-front
|
||||
|
||||
services:
|
||||
front:
|
||||
image: nginx:1.31-alpine
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,46 @@
|
||||
# Pourquoi : trois environnements sur une seule IP, des URL sans port (ADR 0018). Ce frontal lit
|
||||
# le nom demandé dans le ClientHello (SNI) et relaie le flux TLS intact vers le proxy de la
|
||||
# stack visée : il ne détient aucun certificat, chaque stack garde le sien et ses en-têtes.
|
||||
# Piège : relayé tel quel, le flux arriverait avec l'IP du frontal, et les limitations de débit
|
||||
# de nginx et du backend deviendraient globales. D'où `proxy_protocol on`, reçu sur l'écouteur
|
||||
# 4443 de chaque stack (infra/proxy/conf.d/enervision.conf), qui y restaure l'IP du client.
|
||||
# Contrainte : ces ports sont ceux que `scripts/provision-host.sh` donne à PROXY_FRONT_PORT.
|
||||
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
stream {
|
||||
log_format aiguillage '$remote_addr [$time_local] $ssl_preread_server_name '
|
||||
'-> $upstream_addr $status $session_time';
|
||||
access_log /var/log/nginx/access.log aiguillage;
|
||||
|
||||
map $ssl_preread_server_name $stack {
|
||||
~^rec\. 127.0.0.1:8444;
|
||||
~^dev\. 127.0.0.1:9444;
|
||||
default 127.0.0.1:10444;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443;
|
||||
ssl_preread on;
|
||||
proxy_pass $stack;
|
||||
proxy_protocol on;
|
||||
proxy_connect_timeout 5s;
|
||||
}
|
||||
}
|
||||
|
||||
http {
|
||||
server_tokens off;
|
||||
access_log off;
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
+21
-2
@@ -76,8 +76,27 @@ Renouvellement, à passer en tâche planifiée sur la machine :
|
||||
17 3 * * * cd /srv/enervision && make tls-renew >> /var/log/enervision-tls.log 2>&1
|
||||
```
|
||||
|
||||
Pour un domaine sans port 80 entrant, le défi DNS-01 est l'alternative : elle demande un
|
||||
greffon certbot propre au fournisseur DNS et un jeton d'API, hors périmètre à ce jour.
|
||||
### 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,
|
||||
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)).
|
||||
|
||||
```bash
|
||||
make tls-duckdns # PUBLIC_HOST lu dans .env, jeton dans ../duckdns.token (600)
|
||||
```
|
||||
|
||||
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.
|
||||
`deploy.yml` la rejoue avant chaque `make stack-up`, et `/etc/cron.d/enervision-tls` chaque nuit.
|
||||
|
||||
## Écouteur PROXY protocol
|
||||
|
||||
Sur la VM, le frontal `infra/front` relaie les connexions TLS sans les déchiffrer. Reçues sur
|
||||
443, elles porteraient son adresse, et `limit_req` comme `get_client_ip()` compteraient tous les
|
||||
postes comme un seul. Le port 4443 ne les accepte qu'avec l'en-tête PROXY protocol, d'où
|
||||
`real_ip_header proxy_protocol` tire l'IP du client ; seules les adresses des réseaux Docker ont
|
||||
le droit de l'annoncer, et le port n'est publié que sur `127.0.0.1` (`PROXY_FRONT_PORT`).
|
||||
|
||||
## Vérifier la configuration sans démarrer la stack
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
# variable et le résolveur interne de Docker : la résolution redevient dynamique.
|
||||
# Pourquoi : la redirection 80 vers 443 conserve `$host` plutôt qu'un nom canonique, faute de
|
||||
# quoi l'accès par IP cesserait de fonctionner sur la cible. Risque acté dans l'ADR 0007.
|
||||
# Piège : 4443 n'accepte que le PROXY protocol du frontal (infra/front, ADR 0018), qui y porte
|
||||
# l'IP du client. Seules les adresses des réseaux Docker ont le droit de l'annoncer.
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
@@ -23,9 +25,14 @@ server {
|
||||
|
||||
server {
|
||||
listen 443 ssl default_server;
|
||||
listen 4443 ssl proxy_protocol default_server;
|
||||
http2 on;
|
||||
server_name _;
|
||||
|
||||
set_real_ip_from 172.16.0.0/12;
|
||||
set_real_ip_from 192.168.0.0/16;
|
||||
real_ip_header proxy_protocol;
|
||||
|
||||
resolver 127.0.0.11 valid=10s ipv6=off;
|
||||
|
||||
ssl_certificate /etc/nginx/tls/fullchain.pem;
|
||||
|
||||
@@ -57,9 +57,10 @@ resource "null_resource" "environnements" {
|
||||
depends_on = [null_resource.docker_engine]
|
||||
|
||||
triggers = {
|
||||
script = filesha256(local.provisionneur)
|
||||
racine = var.racine
|
||||
depot = var.depot_url
|
||||
script = filesha256(local.provisionneur)
|
||||
racine = var.racine
|
||||
depot = var.depot_url
|
||||
domaine = var.domaine
|
||||
}
|
||||
|
||||
connection {
|
||||
@@ -83,6 +84,7 @@ resource "null_resource" "environnements" {
|
||||
${local.sudo}env RACINE='${var.racine}' \
|
||||
REPO_URL='${var.depot_url}' \
|
||||
PROPRIETAIRE='${var.proprietaire}' \
|
||||
DOMAINE='${var.domaine}' \
|
||||
PUBLIC_IP='${var.adresse_publique}' \
|
||||
bash /tmp/provision-host.sh
|
||||
rm -f /tmp/provision-host.sh
|
||||
|
||||
@@ -43,6 +43,12 @@ variable "depot_url" {
|
||||
default = "https://github.com/ineszang/ProjetPiscine_EnerVision.git"
|
||||
}
|
||||
|
||||
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 <racine>/duckdns.token sur la machine."
|
||||
default = "enervision-g3.duckdns.org"
|
||||
}
|
||||
|
||||
variable "adresse_publique" {
|
||||
type = string
|
||||
description = "Adresse annoncee dans les certificats auto-signes. Vide : la premiere adresse de la VM."
|
||||
|
||||
Reference in New Issue
Block a user