This commit is contained in:
mrKamoo
2026-09-30 17:06:16 +02:00
parent 310724708f
commit e133c741b8
7 changed files with 215 additions and 88 deletions
+101 -54
View File
@@ -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" <<EOF
# Généré par deploy/deploy.sh le $(date '+%d/%m/%Y %H:%M') — ne pas versionner
HTTP_PORT=$HTTP_PORT
HTTPS_PORT=$HTTPS_PORT
DB_DATABASE=signature
DB_USERNAME=signature
DB_PASSWORD=$DB_PASSWORD
EOF
{
echo "# Généré par deploy/deploy.sh le $(date '+%d/%m/%Y %H:%M') — ne pas versionner"
echo "NGINX_REAL_IP_FROM=$NGINX_REAL_IP_FROM"
echo "FORWARD_HOST=$FORWARD_HOST"
echo "FORWARD_PORT=$FORWARD_PORT"
echo "DB_DATABASE=signature"
echo "DB_USERNAME=signature"
echo "DB_PASSWORD=$DB_PASSWORD"
} > "$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" <<EOF
ADDIN_BASE_URL=https://$HOSTNAME_FQDN/addin
@@ -174,8 +211,8 @@ else
c_warn "Modifications locales dans le dépôt : git pull ignoré."
fi
fi
HTTPS_PORT="$(get_env "$DEPLOY_ENV" HTTPS_PORT)"
fi
[[ -f "$ADMIN_ALLOW" ]] || printf 'allow 10.0.0.0/8;\nallow 172.16.0.0/12;\nallow 192.168.0.0/16;\n' > "$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 <<EOF
────────────────────────────────────────────────────────────────────
Stack « signature » opérationnelle sur ce serveur : HTTPS $HTTPS_PORT
Stack « signature » opérationnelle — cible NPM : http://$FORWARD_HOST:$FORWARD_PORT
────────────────────────────────────────────────────────────────────
Restant à faire côté réseau (1re installation) :
1. DNS public : $HOSTNAME_FQDN A <IP publique dédiée>
2. Stormshield : NAT <IP publique>: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 :