From e133c741b891a47b6a4d23f670cda0afecddfc1a Mon Sep 17 00:00:00 2001 From: mrKamoo Date: Wed, 30 Sep 2026 17:06:16 +0200 Subject: [PATCH] 50 --- .gitignore | 2 + backend/bootstrap/app.php | 7 +- deploy/deploy.sh | 155 +++++++++++++++++---------- docker-compose.yml | 15 +-- docker/nginx/signature.conf.template | 59 ++++++++++ docs/01-entra-app-registrations.md | 5 +- docs/02-deploiement.md | 60 +++++++---- 7 files changed, 215 insertions(+), 88 deletions(-) create mode 100644 docker/nginx/signature.conf.template diff --git a/.gitignore b/.gitignore index 6532fa7..845d9ee 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ backend/.env docker/certs/* !docker/certs/README.txt deploy.env +docker-compose.override.yml +docker/nginx/admin-allow.inc diff --git a/backend/bootstrap/app.php b/backend/bootstrap/app.php index 14e3834..bb17f61 100644 --- a/backend/bootstrap/app.php +++ b/backend/bootstrap/app.php @@ -17,8 +17,11 @@ return Application::configure(basePath: dirname(__DIR__)) 'role' => \App\Http\Middleware\EnsureRole::class, 'admin.network' => \App\Http\Middleware\RestrictAdminNetwork::class, ]); - // Si un reverse proxy s'intercale devant Nginx, déclarer son IP ici (ex. at: ['10.10.0.5']). - // $middleware->trustProxies(at: []); + // Derrière Nginx Proxy Manager : on fait confiance aux en-têtes X-Forwarded-* (IP client, HTTPS) + // uniquement quand ils viennent des réseaux listés dans TRUSTED_PROXIES (CIDR séparés par des virgules). + if ($proxies = array_filter(array_map('trim', explode(',', (string) env('TRUSTED_PROXIES', ''))))) { + $middleware->trustProxies(at: $proxies); + } }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/deploy/deploy.sh b/deploy/deploy.sh index 3b09f6e..56456bb 100644 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -5,9 +5,10 @@ # Usage (depuis la racine du dépôt cloné, ex. /opt/signature) : # sudo ./deploy/deploy.sh installation (1re fois) ou mise à jour # -# 1re exécution : pose les questions, génère deploy.env, backend/.env, addin/.env, -# installe le certificat, construit et démarre la stack, initialise -# la base et lance une synchro annuaire complète. +# 1re exécution : détecte Nginx Proxy Manager, pose les questions, génère deploy.env, +# docker-compose.override.yml, backend/.env, addin/.env, construit et +# démarre la stack, initialise la base et lance une synchro annuaire complète. +# TLS : terminé par Nginx Proxy Manager (certificat wildcard déjà présent), stack en HTTP interne. # Exécutions suivantes : git pull, rebuild, redémarrage, contrôles. # Le serveur héberge d'autres applications : les ports sont détectés et jamais écrasés. # ============================================================================= @@ -19,9 +20,8 @@ cd "$ROOT" DEPLOY_ENV="$ROOT/deploy.env" BACKEND_ENV="$ROOT/backend/.env" ADDIN_ENV="$ROOT/addin/.env" -CERT_DIR="$ROOT/docker/certs" -CERT_CRT="$CERT_DIR/wildcard.beziers-mediterranee.fr.crt" -CERT_KEY="$CERT_DIR/wildcard.beziers-mediterranee.fr.key" +OVERRIDE="$ROOT/docker-compose.override.yml" +ADMIN_ALLOW="$ROOT/docker/nginx/admin-allow.inc" HOSTNAME_FQDN="signature.beziers-mediterranee.fr" c_info() { printf '\033[1;34m[i]\033[0m %s\n' "$*"; } @@ -48,7 +48,10 @@ ask_guid() { # Remplace ou ajoute KEY=VALUE dans un fichier .env (valeur entre guillemets si nécessaire) set_env() { local file="$1" key="$2" val="$3" line tmp - if [[ "$val" =~ [[:space:]\#\"\'\$] ]]; then line="$key=\"${val//\"/\\\"}\""; else line="$key=$val"; fi + # Guillemets simples = valeur littérale pour Laravel (phpdotenv) comme pour Docker Compose (pas d'interpolation de $) + if [[ "$val" =~ [[:space:]\#\"\$] && "$val" != *"'"* ]]; then line="$key='$val'" + elif [[ "$val" =~ [[:space:]\#\"\'\$] ]]; then line="$key=\"${val//\"/\\\"}\"" + else line="$key=$val"; fi tmp="$(mktemp)" KEY="$key" LINE="$line" awk 'BEGIN{k=ENVIRON["KEY"]; l=ENVIRON["LINE"]; done=0} $0 ~ "^"k"=" { if(!done){print l; done=1}; next } {print} @@ -57,7 +60,7 @@ set_env() { } get_env() { # get_env fichier CLE [[ -f "$1" ]] || return 0 - grep -E "^$2=" "$1" | tail -n1 | cut -d= -f2- | sed -e 's/^"//' -e 's/"$//' + grep -E "^$2=" "$1" | tail -n1 | cut -d= -f2- | sed -e "s/^[\"']//" -e "s/[\"']$//" } port_used() { # port occupé par le système ou publié par un conteneur Docker @@ -73,7 +76,7 @@ compose() { docker compose --env-file "$DEPLOY_ENV" "$@"; } # ----------------------------------------------------------------------------- [[ $EUID -eq 0 ]] || die "Lancer avec sudo (accès Docker et ports)." -for bin in docker git openssl ss curl awk; do command -v "$bin" >/dev/null || die "Commande manquante : $bin"; done +for bin in docker git openssl ss awk; do command -v "$bin" >/dev/null || die "Commande manquante : $bin"; done docker compose version >/dev/null 2>&1 || die "Plugin « docker compose » absent." [[ -f backend/composer.json && -f backend/composer.lock ]] \ || die "backend/composer.json ou composer.lock absent : le dépôt doit contenir le back-office complet (voir docs/02-deploiement.md, étape Git)." @@ -85,39 +88,66 @@ FIRST_INSTALL=0 if (( FIRST_INSTALL )); then c_info "Première installation de la stack « signature »." - # --- Ports ------------------------------------------------------------------ - def_https=443; port_used 443 && def_https="$(next_free_port 8443)" - def_http=80; port_used 80 && def_http="$(next_free_port 8080)" - (( def_https != 443 )) && c_warn "Le port 443 est déjà utilisé sur ce serveur : proposition $def_https (NAT Stormshield 443 public -> $def_https)." - while :; do ask "Port HTTPS de la stack sur ce serveur" "$def_https"; port_used "$REPLY" && { c_warn "Port $REPLY occupé."; continue; }; HTTPS_PORT="$REPLY"; break; done - while :; do ask "Port HTTP (redirection vers HTTPS)" "$def_http"; port_used "$REPLY" && { c_warn "Port $REPLY occupé."; continue; }; HTTP_PORT="$REPLY"; break; done + # --- Nginx Proxy Manager --------------------------------------------------- + NPM_CONTAINER="$(docker ps --format '{{.Names}} {{.Image}}' | awk 'tolower($2) ~ /nginx-proxy-manager/ {print $1; exit}')" + NPM_NETWORK=""; HTTP_PORT="" + if [[ -n "$NPM_CONTAINER" ]]; then + NPM_NETWORK="$(docker inspect -f '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}' "$NPM_CONTAINER" | tr ' ' '\n' | grep -vE '^(host|none)?$' | head -n1 || true)" + c_ok "Nginx Proxy Manager détecté : conteneur « $NPM_CONTAINER », réseau « ${NPM_NETWORK:-aucun} »." + else + c_warn "Aucun conteneur Nginx Proxy Manager détecté sur ce serveur." + fi + + if [[ -n "$NPM_NETWORK" ]]; then + ask "Raccorder la stack au réseau Docker « $NPM_NETWORK » de NPM (recommandé, aucun port exposé) ? (o/n)" "o" + [[ "$REPLY" =~ ^[oOyY] ]] || NPM_NETWORK="" + fi + + if [[ -n "$NPM_NETWORK" ]]; then + NGINX_REAL_IP_FROM="$(docker network inspect -f '{{range .IPAM.Config}}{{.Subnet}} {{end}}' "$NPM_NETWORK" | awk '{print $1}')" + [[ -n "$NGINX_REAL_IP_FROM" ]] || die "Sous-réseau du réseau $NPM_NETWORK introuvable." + # Seuls des conteneurs joignent la stack : confiance aux réseaux Docker privés pour X-Forwarded-*. + TRUSTED_PROXIES="10.0.0.0/8,172.16.0.0/12,192.168.0.0/16" + { + echo "# Généré par deploy/deploy.sh — raccordement au réseau de Nginx Proxy Manager" + echo "services:" + echo " nginx:" + echo " networks: [signature_net, npm]" + echo "networks:" + echo " npm:" + echo " external: true" + echo " name: $NPM_NETWORK" + } > "$OVERRIDE" + FORWARD_HOST="signature_nginx"; FORWARD_PORT=80 + else + c_warn "Mode port hôte : NPM joindra la stack par IP:port ; ce port est joignable par toute machine qui voit ce serveur." + def_http="$(next_free_port 8085)" + while :; do ask "Port HTTP interne de la stack" "$def_http"; port_used "$REPLY" && { c_warn "Port $REPLY occupé."; continue; }; HTTP_PORT="$REPLY"; break; done + ask_required "IP ou réseau (CIDR) d'où NPM joint ce port (ex. 172.18.0.1/32 si NPM tourne sur ce serveur)" + NGINX_REAL_IP_FROM="$REPLY"; TRUSTED_PROXIES="$REPLY,172.16.0.0/12" + { + echo "# Généré par deploy/deploy.sh — publication d'un port hôte pour Nginx Proxy Manager" + echo "services:" + echo " nginx:" + echo " ports:" + echo " - \"$HTTP_PORT:80\"" + } > "$OVERRIDE" + FORWARD_HOST="$(hostname -I | awk '{print $1}')"; FORWARD_PORT="$HTTP_PORT" + fi DB_PASSWORD="$(openssl rand -hex 24)" umask 077 - cat > "$DEPLOY_ENV" < "$DEPLOY_ENV" umask 022 - c_ok "deploy.env créé (ports $HTTP_PORT / $HTTPS_PORT)." - - # --- Certificat ------------------------------------------------------------- - mkdir -p "$CERT_DIR" - if [[ ! -f "$CERT_CRT" || ! -f "$CERT_KEY" ]]; then - c_info "Certificat wildcard *.beziers-mediterranee.fr (PEM, chaîne complète : certificat + intermédiaires)." - ask_required "Chemin du fichier certificat (.crt/.pem)"; src_crt="$REPLY" - ask_required "Chemin de la clé privée (.key)"; src_key="$REPLY" - [[ -f "$src_crt" && -f "$src_key" ]] || die "Fichier introuvable." - cp "$src_crt" "$CERT_CRT"; cp "$src_key" "$CERT_KEY"; chmod 644 "$CERT_CRT"; chmod 600 "$CERT_KEY" - fi - crt_mod="$(openssl x509 -noout -modulus -in "$CERT_CRT" | openssl md5)" - key_mod="$(openssl rsa -noout -modulus -in "$CERT_KEY" 2>/dev/null | openssl md5 || true)" - [[ -z "$key_mod" || "$crt_mod" == "$key_mod" ]] || die "Le certificat et la clé ne correspondent pas." - c_ok "Certificat : $(openssl x509 -noout -subject -enddate -in "$CERT_CRT" | tr '\n' ' ')" + c_ok "deploy.env et docker-compose.override.yml créés (cible NPM : http://$FORWARD_HOST:$FORWARD_PORT)." # --- Identifiants Entra ----------------------------------------------------- c_info "Identifiants Entra (docs/01-entra-app-registrations.md)." @@ -150,10 +180,17 @@ EOF [SIGNATURE_ROLE_EDITOR_GROUP]=signature_role_editeur [SIGNATURE_DEFAULT_REPLY_VARIANT]=short [SIGNATURE_ADMIN_ALLOWED_CIDRS]="$ADMIN_CIDRS" + [TRUSTED_PROXIES]="$TRUSTED_PROXIES" ) for k in "${!BE[@]}"; do set_env "$BACKEND_ENV" "$k" "${BE[$k]}"; done c_ok "backend/.env configuré (production)." + { + echo "# Généré par deploy/deploy.sh — réseaux autorisés pour /admin" + IFS=',' read -ra cidrs <<< "$ADMIN_CIDRS" + for c in "${cidrs[@]}"; do c="${c// /}"; if [[ -n "$c" ]]; then echo "allow $c;"; fi; done + } > "$ADMIN_ALLOW" + # --- addin/.env (lu au build de l'image) ----------------------------------- cat > "$ADDIN_ENV" < "$ADMIN_ALLOW" # ----------------------------------------------------------------------------- c_info "Construction de l'image (add-in + back-office)…" @@ -183,15 +220,16 @@ compose build --pull c_info "Démarrage…" compose up -d --remove-orphans +compose exec -T nginx nginx -t >/dev/null 2>&1 || { compose exec -T nginx nginx -t || true; die "Configuration Nginx de la stack invalide."; } + c_info "Attente de l'application…" ok=0 for _ in $(seq 1 30); do - code="$(curl -sk -o /dev/null -w '%{http_code}' --resolve "$HOSTNAME_FQDN:$HTTPS_PORT:127.0.0.1" "https://$HOSTNAME_FQDN:$HTTPS_PORT/up" || true)" - [[ "$code" == "200" ]] && { ok=1; break; } + compose exec -T nginx wget -q -O /dev/null http://127.0.0.1/up 2>/dev/null && { ok=1; break; } sleep 3 done -(( ok )) || { compose logs --tail=50 app nginx; die "L'application ne répond pas sur /up (dernier code : ${code:-aucun})."; } -c_ok "Application en ligne (https://$HOSTNAME_FQDN:$HTTPS_PORT/up)." +(( ok )) || { compose logs --tail=50 app nginx; die "L'application ne répond pas sur /up."; } +c_ok "Application en ligne (contrôle interne /up)." if (( FIRST_INSTALL )); then c_info "Données initiales (tenant, template par défaut)…" @@ -201,22 +239,31 @@ if (( FIRST_INSTALL )); then fi # Contrôles -addin_code="$(curl -sk -o /dev/null -w '%{http_code}' --resolve "$HOSTNAME_FQDN:$HTTPS_PORT:127.0.0.1" "https://$HOSTNAME_FQDN:$HTTPS_PORT/addin/manifest.xml")" -[[ "$addin_code" == "200" ]] && c_ok "Manifeste de l'add-in servi." || c_warn "manifest.xml : HTTP $addin_code" -xfo="$(curl -skI --resolve "$HOSTNAME_FQDN:$HTTPS_PORT:127.0.0.1" "https://$HOSTNAME_FQDN:$HTTPS_PORT/addin/commands.html" | grep -ci '^x-frame-options' || true)" -[[ "$xfo" == "0" ]] && c_ok "Add-in intégrable par Outlook (pas de X-Frame-Options)." || c_warn "X-Frame-Options présent sur /addin : Outlook refusera de charger l'add-in." +if compose exec -T nginx wget -q -O /dev/null http://127.0.0.1/addin/manifest.xml 2>/dev/null; then + c_ok "Manifeste de l'add-in servi." +else + c_warn "manifest.xml non servi par la stack." +fi +xfo="$(compose exec -T nginx wget -S -q -O /dev/null http://127.0.0.1/addin/commands.html 2>&1 | grep -ci 'x-frame-options' || true)" +if [[ "$xfo" == "0" ]]; then c_ok "Add-in intégrable par Outlook (pas de X-Frame-Options côté stack)." +else c_warn "X-Frame-Options présent sur /addin : Outlook refusera de charger l'add-in."; fi +FORWARD_HOST="$(get_env "$DEPLOY_ENV" FORWARD_HOST)"; FORWARD_PORT="$(get_env "$DEPLOY_ENV" FORWARD_PORT)" cat < - 2. Stormshield : NAT :443 -> $(hostname -I | awk '{print $1}'):$HTTPS_PORT - (+ 80 -> $(get_env "$DEPLOY_ENV" HTTP_PORT) pour la redirection, facultatif) - 3. Test externe : https://$HOSTNAME_FQDN/up puis /addin/manifest.xml - 4. Integrated Apps : téléverser https://$HOSTNAME_FQDN/addin/manifest.xml + Restant à faire (1re installation) : + 1. Nginx Proxy Manager > Proxy Hosts > Add Proxy Host : + Domain Names signature.beziers-mediterranee.fr + Scheme / Hostname / Port http / $FORWARD_HOST / $FORWARD_PORT + Block Common Exploits oui Websockets non + SSL certificat wildcard existant, Force SSL, HTTP/2, HSTS + Ne PAS ajouter d'en-tête X-Frame-Options (Outlook charge /addin en iframe) + 2. DNS public : signature.beziers-mediterranee.fr -> IP publique de NPM + 3. Test externe : https://signature.beziers-mediterranee.fr/up puis /addin/manifest.xml + 4. Integrated Apps : lien https://signature.beziers-mediterranee.fr/addin/manifest.xml ciblé sur le groupe pilote (docs/01, §4) Exploitation : diff --git a/docker-compose.yml b/docker-compose.yml index 679094b..a3a89f3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,6 @@ -# Stack « signature » — serveur Debian DMZ mutualisé (voir deploy/deploy.sh et docs/02-deploiement.md). -# Variables lues dans deploy.env (généré par deploy/deploy.sh) : HTTP_PORT, HTTPS_PORT, DB_*. +# Stack « signature » — serveur Debian DMZ mutualisé, derrière Nginx Proxy Manager. +# Variables lues dans deploy.env (généré par deploy/deploy.sh) : NGINX_REAL_IP_FROM, DB_*. +# docker-compose.override.yml (généré) : raccordement au réseau de NPM, ou port hôte en repli. name: signature services: @@ -33,12 +34,12 @@ services: nginx: image: nginx:1.28-alpine container_name: signature_nginx - ports: - - "${HTTP_PORT:-80}:80" - - "${HTTPS_PORT:-443}:443" + # HTTP uniquement : TLS terminé par Nginx Proxy Manager. Aucun port publié ici. + environment: + NGINX_REAL_IP_FROM: ${NGINX_REAL_IP_FROM:-172.16.0.0/12} volumes: - - ./docker/nginx/signature.conf:/etc/nginx/conf.d/default.conf:ro - - ./docker/certs:/etc/nginx/certs:ro + - ./docker/nginx/signature.conf.template:/etc/nginx/templates/default.conf.template:ro + - ./docker/nginx/admin-allow.inc:/etc/nginx/conf.d/admin-allow.inc:ro - addin:/srv/addin:ro - ./backend/public:/var/www/html/public:ro - images:/var/www/html/public/img:ro diff --git a/docker/nginx/signature.conf.template b/docker/nginx/signature.conf.template new file mode 100644 index 0000000..257ff02 --- /dev/null +++ b/docker/nginx/signature.conf.template @@ -0,0 +1,59 @@ +# signature.beziers-mediterranee.fr — derrière Nginx Proxy Manager (TLS terminé par NPM). +# Modèle traité au démarrage par l'image nginx officielle (envsubst des seules variables définies) : +# NGINX_REAL_IP_FROM : réseau d'où viennent les requêtes de NPM (ex. 172.18.0.0/16) +server { + listen 80; + server_name _; + + # IP réelle du client transmise par NPM (sinon /admin verrait l'IP de NPM) + set_real_ip_from ${NGINX_REAL_IP_FROM}; + real_ip_header X-Forwarded-For; + real_ip_recursive on; + + server_tokens off; + client_max_body_size 6m; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + root /var/www/html/public; + index index.php; + + # Add-in Outlook : chargé en iframe par Outlook, donc AUCUN X-Frame-Options ici. + location /addin/ { + alias /srv/addin/; + add_header X-Content-Type-Options "nosniff" always; + add_header Cache-Control "no-cache" always; + try_files $uri =404; + } + + # Logos et bannières : publics, mis en cache par les clients + location /img/ { + alias /var/www/html/public/img/; + expires 7d; + add_header Cache-Control "public" always; + add_header Access-Control-Allow-Origin "*" always; + try_files $uri =404; + } + + # Back-office : réseaux internes uniquement (IP réelle, voir set_real_ip_from) + location /admin { + include /etc/nginx/conf.d/admin-allow.inc; + deny all; + add_header X-Frame-Options "DENY" always; + try_files $uri $uri/ /index.php?$query_string; + } + + location / { + add_header X-Frame-Options "DENY" always; + try_files $uri $uri/ /index.php?$query_string; + } + + location ~ \.php$ { + fastcgi_pass signature_app:9000; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + include fastcgi_params; + fastcgi_hide_header X-Powered-By; + } + + location ~ /\.(?!well-known) { deny all; } +} diff --git a/docs/01-entra-app-registrations.md b/docs/01-entra-app-registrations.md index 11a3aeb..e6b58b2 100644 --- a/docs/01-entra-app-registrations.md +++ b/docs/01-entra-app-registrations.md @@ -38,9 +38,10 @@ Reporter : ## 2. Inscription « CABM Signatures - Back-office » 1. **Nom** : `CABM Signatures - Back-office` — mono-tenant. -2. **Authentification** > plateforme **Web**, URI de redirection : +2. **Authentification** > plateforme **Web** (surtout pas « Application monopage » : sinon erreur + AADSTS9002325, car Laravel échange le code côté serveur), URI de redirection : - `https://signature.beziers-mediterranee.fr/auth/callback` - - (dev Laragon) `https://addinoutlook.test/auth/callback` + - (dev) `http://localhost:8000/auth/callback` 3. **Certificats et secrets** > Nouveau secret client, durée 24 mois → `GRAPH_CLIENT_SECRET`. Créer un rappel GLPI deux mois avant l'expiration. 4. **Autorisations de l'API** (Microsoft Graph) : diff --git a/docs/02-deploiement.md b/docs/02-deploiement.md index 8a9150d..851b1b6 100644 --- a/docs/02-deploiement.md +++ b/docs/02-deploiement.md @@ -1,15 +1,22 @@ # 02 — Déploiement sur le serveur Debian DMZ -Cible : serveur Debian 13 existant en DMZ, déjà équipé de Docker et hébergeant d'autres applications. +Cible : serveur Debian 13 existant en DMZ, déjà équipé de Docker, de Nginx Proxy Manager et d'autres applications. La stack « signature » est isolée : projet Compose `signature`, conteneurs `signature_*`, réseau `signature_net`, volumes dédiés, ports choisis parmi les ports libres. Elle ne touche à aucune autre application. ``` -Internet ──443──► Stormshield (NAT) ──► serveur DMZ :HTTPS_PORT ──► signature_nginx ──► signature_app (PHP-FPM) - └► signature_db (MariaDB) +Internet ──443──► Stormshield ──► Nginx Proxy Manager (TLS wildcard) ──http──► signature_nginx ──► signature_app (PHP-FPM) + └► signature_db (MariaDB) signature_scheduler : synchro Graph toutes les 15 min + complète la nuit ``` +TLS est terminé par Nginx Proxy Manager (NPM), qui porte déjà le certificat wildcard. La stack ne parle qu'en HTTP interne : +- **mode recommandé** : `signature_nginx` est raccordé au réseau Docker de NPM, aucun port n'est publié sur l'hôte ; +- **mode de repli** (NPM sur un autre hôte ou sans réseau Docker) : un port HTTP hôte est publié et NPM y pointe. + +L'IP réelle des clients est transmise par NPM (`X-Forwarded-For`) et reprise par Nginx (`set_real_ip_from`) et par Laravel +(`TRUSTED_PROXIES`) : le filtrage de `/admin` s'applique donc à l'IP du poste, pas à celle de NPM. + ## 1. Pousser le projet dans le dépôt Git interne (poste de dev) Le dépôt doit contenir le back-office **complet** (après `install-backend.ps1`), sans `vendor`, sans `.env`. @@ -28,42 +35,49 @@ git push -u origin main | Élément | Valeur | | --- | --- | -| DNS public | `signature.beziers-mediterranee.fr` → IP publique **dédiée** (ou IP partagée dont le 443 est libre) | -| NAT Stormshield | IP publique:443 → serveur DMZ:`HTTPS_PORT` (proposé par le script, 443 si libre, sinon 8443…) | -| NAT facultatif | IP publique:80 → serveur DMZ:`HTTP_PORT` (redirection vers HTTPS) | -| Filtrage entrant | 443 depuis Internet (OWA et Outlook mobile sont utilisés hors réseau) | +| DNS public | `signature.beziers-mediterranee.fr` → IP publique déjà publiée vers NPM | +| Stormshield | aucun changement si le 443 public arrive déjà sur NPM | +| Filtrage entrant | 443 depuis Internet vers NPM (OWA et Outlook mobile sont utilisés hors réseau) | | Sortant serveur | 443 vers `login.microsoftonline.com`, `graph.microsoft.com` + Docker Hub, npm, Packagist, dépôts Debian (build) | | Vers le LAN | **aucun flux** | -Le NAT doit conserver l'IP source (pas de SNAT) : le filtrage de `/admin` repose sur l'IP du client. -Si le Stormshield fait du SNAT, filtrer `/admin` au niveau du pare-feu. - ## 3. Première installation (serveur) ```bash sudo git clone /opt/signature cd /opt/signature -# Copier le certificat wildcard (PEM chaîne complète + clé) quelque part sur le serveur, puis : sudo ./deploy/deploy.sh ``` Le script, interactif : -1. vérifie les prérequis et détecte les ports déjà pris (système et conteneurs existants) ; -2. propose les ports HTTPS/HTTP de la stack et génère `deploy.env` (mot de passe MariaDB aléatoire) ; -3. installe le certificat dans `docker/certs/` et vérifie que certificat et clé correspondent ; -4. demande les identifiants Entra (tenant, back-office, secret masqué, add-in) ; -5. génère `backend/.env` (production) et `addin/.env` (URL publiques) ; -6. construit l'image (add-in compilé + back-office), démarre la stack, attend `/up` ; -7. initialise la base (tenant, template par défaut) et lance une synchro annuaire complète ; -8. contrôle le manifeste et l'absence d'en-tête X-Frame-Options sur `/addin`. +1. vérifie les prérequis et détecte le conteneur Nginx Proxy Manager et son réseau Docker ; +2. propose de raccorder la stack à ce réseau (sinon, choisit un port HTTP libre sur l'hôte) et génère + `docker-compose.override.yml` et `deploy.env` (mot de passe MariaDB aléatoire) ; +3. demande les identifiants Entra (tenant, back-office, secret masqué, add-in) et les réseaux autorisés pour `/admin` ; +4. génère `backend/.env` (production, `TRUSTED_PROXIES`), `addin/.env` (URL publiques) et `docker/nginx/admin-allow.inc` ; +5. construit l'image (add-in compilé + back-office), démarre la stack, vérifie la configuration Nginx et attend `/up` ; +6. initialise la base (tenant, template par défaut) et lance une synchro annuaire complète ; +7. contrôle le manifeste et l'absence d'en-tête X-Frame-Options sur `/addin`, puis affiche la cible à saisir dans NPM. -Les fichiers `deploy.env`, `backend/.env`, `addin/.env` et la clé du certificat sont en `chmod 600` et ignorés par Git. +`deploy.env`, `docker-compose.override.yml`, `backend/.env`, `addin/.env` et `admin-allow.inc` sont propres au serveur et ignorés par Git. + +### Proxy Host dans Nginx Proxy Manager + +| Champ | Valeur | +| --- | --- | +| Domain Names | `signature.beziers-mediterranee.fr` | +| Scheme / Forward Hostname / Port | `http` / `signature_nginx` / `80` (mode recommandé) — ou l'IP et le port affichés par le script | +| Block Common Exploits | oui | +| Websockets Support | non | +| SSL | certificat wildcard existant, Force SSL, HTTP/2, HSTS | +| Advanced | rien ; surtout **pas** d'en-tête `X-Frame-Options` (Outlook charge `/addin` en iframe) | + +Pas besoin d'Access List NPM : `/admin` est déjà filtré par la stack (Nginx + Laravel) sur l'IP réelle du poste. Ensuite : - **Logo** : `sudo docker compose --env-file deploy.env cp logo-cabm.png app:/var/www/html/public/img/logo-cabm.png` - **Entra** : ajouter l'URI `https://signature.beziers-mediterranee.fr/auth/callback` (plateforme **Web**) à l'inscription Back-office. - Les URI localhost de dev peuvent rester pour le poste de développement. -- **Test externe** (4G) : `https://signature.beziers-mediterranee.fr/up` → 200, puis `/admin` depuis le LAN. +- **Test externe** (4G) : `https://signature.beziers-mediterranee.fr/up` → 200, `/addin/manifest.xml` → XML, puis `/admin` depuis le LAN. ## 4. Pilote DSI @@ -95,6 +109,6 @@ puis mettre à jour le complément dans Applications intégrées. Une modificati | Synchro manuelle | bouton du tableau de bord, ou `… exec -u www-data app php artisan signature:sync --full` | | Sauvegarde base | `sudo docker compose --env-file deploy.env exec -T db sh -c 'mariadb-dump -u"$MARIADB_USER" -p"$MARIADB_PASSWORD" "$MARIADB_DATABASE"' > signature-$(date +%F).sql` | | Supervision | LibreNMS : HTTP(S) sur `/up` et expiration du certificat | -| Renouvellement certificat | remplacer les fichiers de `docker/certs/` puis `sudo docker compose --env-file deploy.env restart nginx` | +| Certificat | géré par Nginx Proxy Manager (wildcard existant) | Sauvegarde : la VM est sauvegardée par Veeam ; ajouter le dump quotidien ci-dessus en tâche cron pour une restauration fine.