#!/usr/bin/env bash # ============================================================================= # Signatures CABM — installation et mise à jour sur le serveur Debian DMZ # # 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. # 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. # ============================================================================= set -Eeuo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" 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" HOSTNAME_FQDN="signature.beziers-mediterranee.fr" c_info() { printf '\033[1;34m[i]\033[0m %s\n' "$*"; } c_ok() { printf '\033[1;32m[ok]\033[0m %s\n' "$*"; } c_warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*"; } c_err() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; } die() { c_err "$*"; exit 1; } trap 'c_err "Échec ligne $LINENO : $BASH_COMMAND"' ERR ask() { # ask "Question" "défaut" -> REPLY local q="$1" def="${2:-}" if [[ -n "$def" ]]; then read -r -p "$q [$def] : " REPLY; REPLY="${REPLY:-$def}" else read -r -p "$q : " REPLY; fi } ask_secret() { read -r -s -p "$1 : " REPLY; echo; } ask_required() { # boucle tant que vide while :; do ask "$1" "${2:-}"; [[ -n "$REPLY" ]] && break; c_warn "Valeur obligatoire."; done } is_guid() { [[ "$1" =~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ ]]; } ask_guid() { while :; do ask "$1" "${2:-}"; is_guid "$REPLY" && break; c_warn "Format attendu : GUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)."; done } # 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 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} END{ if(!done) print l }' "$file" > "$tmp" cat "$tmp" > "$file"; rm -f "$tmp" } get_env() { # get_env fichier CLE [[ -f "$1" ]] || return 0 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 local p="$1" ss -Hltn "sport = :$p" 2>/dev/null | grep -q . && return 0 docker ps --format '{{.Ports}}' | grep -Eq "(:|\])$p->" && return 0 return 1 } port_owned_by_us() { docker port signature_nginx 2>/dev/null | grep -Eq ":$1$"; } next_free_port() { local p="$1"; while port_used "$p" && ! port_owned_by_us "$p"; do p=$((p + 1)); done; echo "$p"; } 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 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)." FIRST_INSTALL=0 [[ -f "$DEPLOY_ENV" ]] || FIRST_INSTALL=1 # ----------------------------------------------------------------------------- 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 DB_PASSWORD="$(openssl rand -hex 24)" umask 077 cat > "$DEPLOY_ENV" </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' ' ')" # --- Identifiants Entra ----------------------------------------------------- c_info "Identifiants Entra (docs/01-entra-app-registrations.md)." ask_guid "ID du tenant (ENTRA_TENANT_ID)" "$(get_env "$BACKEND_ENV" ENTRA_TENANT_ID)"; TENANT_ID="$REPLY" ask_guid "ID client « Back-office » (GRAPH_CLIENT_ID)" "$(get_env "$BACKEND_ENV" GRAPH_CLIENT_ID)"; GRAPH_ID="$REPLY" ask_secret "Valeur du secret « Back-office » (GRAPH_CLIENT_SECRET, saisie masquée)"; GRAPH_SECRET="$REPLY" [[ -n "$GRAPH_SECRET" ]] || die "Secret vide." ask_guid "ID client « Add-in » (ADDIN_CLIENT_ID)" "$(get_env "$BACKEND_ENV" ADDIN_CLIENT_ID)"; ADDIN_ID="$REPLY" ask "Réseaux autorisés pour /admin (CIDR, séparés par des virgules)" "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"; ADMIN_CIDRS="$REPLY" # --- backend/.env ----------------------------------------------------------- [[ -f "$BACKEND_ENV" ]] || cp backend/.env.example "$BACKEND_ENV" chmod 600 "$BACKEND_ENV" APP_KEY="base64:$(openssl rand -base64 32)" [[ -n "$(get_env "$BACKEND_ENV" APP_KEY)" ]] && APP_KEY="$(get_env "$BACKEND_ENV" APP_KEY)" declare -A BE=( [APP_NAME]="Signatures CABM" [APP_ENV]=production [APP_DEBUG]=false [APP_KEY]="$APP_KEY" [APP_URL]="https://$HOSTNAME_FQDN" [APP_LOCALE]=fr [APP_TIMEZONE]=Europe/Paris [LOG_CHANNEL]=daily [LOG_LEVEL]=notice [DB_CONNECTION]=mariadb [DB_HOST]=db [DB_PORT]=3306 [DB_DATABASE]=signature [DB_USERNAME]=signature [DB_PASSWORD]="$DB_PASSWORD" [SESSION_DRIVER]=database [SESSION_SECURE_COOKIE]=true [CACHE_STORE]=database [ENTRA_TENANT_ID]="$TENANT_ID" [GRAPH_CLIENT_ID]="$GRAPH_ID" [GRAPH_CLIENT_SECRET]="$GRAPH_SECRET" [SIGNATURE_GROUP_PREFIX]=signature_ [ADDIN_CLIENT_ID]="$ADDIN_ID" [ADDIN_API_AUDIENCES]="$ADDIN_ID,api://$HOSTNAME_FQDN/$ADDIN_ID" [ADDIN_API_SCOPE_NAME]=access_as_user [SIGNATURE_ROLE_SUPERADMIN_GROUP]=signature_role_superadmin [SIGNATURE_ROLE_ADMIN_GROUP]=signature_role_admin [SIGNATURE_ROLE_EDITOR_GROUP]=signature_role_editeur [SIGNATURE_DEFAULT_REPLY_VARIANT]=short [SIGNATURE_ADMIN_ALLOWED_CIDRS]="$ADMIN_CIDRS" ) for k in "${!BE[@]}"; do set_env "$BACKEND_ENV" "$k" "${BE[$k]}"; done c_ok "backend/.env configuré (production)." # --- addin/.env (lu au build de l'image) ----------------------------------- cat > "$ADDIN_ENV" < 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 ciblé sur le groupe pilote (docs/01, §4) Exploitation : logs docker compose --env-file deploy.env logs -f app scheduler nginx artisan docker compose --env-file deploy.env exec -u www-data app php artisan mise à jour sudo ./deploy/deploy.sh ──────────────────────────────────────────────────────────────────── EOF