228 lines
12 KiB
Bash
228 lines
12 KiB
Bash
#!/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" <<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
|
|
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' ' ')"
|
|
|
|
# --- 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" <<EOF
|
|
ADDIN_BASE_URL=https://$HOSTNAME_FQDN/addin
|
|
API_BASE_URL=https://$HOSTNAME_FQDN
|
|
PORTAL_URL=https://$HOSTNAME_FQDN/me
|
|
SUPPORT_URL=https://$HOSTNAME_FQDN/me
|
|
ENTRA_TENANT_ID=$TENANT_ID
|
|
ADDIN_CLIENT_ID=$ADDIN_ID
|
|
API_SCOPE=api://$HOSTNAME_FQDN/$ADDIN_ID/access_as_user
|
|
EOF
|
|
c_ok "addin/.env configuré (production)."
|
|
else
|
|
c_info "Mise à jour de la stack existante."
|
|
if [[ -d .git ]]; then
|
|
if git diff --quiet && git diff --cached --quiet; then
|
|
git pull --ff-only
|
|
else
|
|
c_warn "Modifications locales dans le dépôt : git pull ignoré."
|
|
fi
|
|
fi
|
|
HTTPS_PORT="$(get_env "$DEPLOY_ENV" HTTPS_PORT)"
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------------
|
|
c_info "Construction de l'image (add-in + back-office)…"
|
|
compose build --pull
|
|
c_info "Démarrage…"
|
|
compose up -d --remove-orphans
|
|
|
|
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; }
|
|
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)."
|
|
|
|
if (( FIRST_INSTALL )); then
|
|
c_info "Données initiales (tenant, template par défaut)…"
|
|
compose exec -T app php artisan db:seed --force
|
|
c_info "Synchro annuaire complète…"
|
|
compose exec -T -u www-data app php artisan signature:sync --full || c_warn "Synchro en échec : vérifier les autorisations Graph (docs/01) puis relancer depuis le tableau de bord."
|
|
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."
|
|
|
|
cat <<EOF
|
|
|
|
────────────────────────────────────────────────────────────────────
|
|
Stack « signature » opérationnelle sur ce serveur : HTTPS $HTTPS_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
|
|
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 <commande>
|
|
mise à jour sudo ./deploy/deploy.sh
|
|
────────────────────────────────────────────────────────────────────
|
|
EOF
|