From 46f19ee87eab46122ca9129cefda9921f0c84f70 Mon Sep 17 00:00:00 2001 From: IliesAllali Date: Wed, 30 Sep 2026 17:41:15 +0200 Subject: [PATCH] Sauvegardes : dump avant migration, fichiers en 077, chiffrement age avant envoi MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit deploy.sh sauvegarde la base dans $BACKUP_ROOT/pre-migration quand prisma migrate status signale un écart, garde les 10 derniers dumps et s'arrête si pg_dump échoue. backup.sh crée ses fichiers en umask 077 et, si BACKUP_AGE_RECIPIENT est renseigné, n'envoie sur S3 que des copies chiffrées avec age. Commentaires sur les polices du frontend mis à jour (deploy.sh, ci.yml). Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 2 +- deploy/backup.sh | 46 ++++++++++++++++++++++++++++++++++++++-- deploy/deploy.sh | 41 ++++++++++++++++++++++++++++++++++- 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9bd7a61..fa7d392 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,7 +71,7 @@ jobs: - name: Tests run: npm test - # Le build lit les polices dans ../frontend/src/assets, présentes dans le dépôt. + # Le build ne lit que landing/ : aucune police n'est prise dans ../frontend. - name: Build run: npm run build diff --git a/deploy/backup.sh b/deploy/backup.sh index 94e2cc8..2685f85 100644 --- a/deploy/backup.sh +++ b/deploy/backup.sh @@ -5,7 +5,15 @@ # La base est sauvegardée à chaque passage. L'archive des médias n'est refaite que si les médias ont # changé (sinon une archive identique par jour), et seules les # BACKUP_MEDIA_KEEP dernières archives sont gardées. +# +# Envoi hors du serveur (facultatif, BACKUP_S3_BUCKET) : si BACKUP_AGE_RECIPIENT est renseigné (une ou +# plusieurs clés publiques séparées par des espaces, « age1... » ou « ssh-ed25519 AAAA... » sans commentaire), +# le dump et l'archive sont chiffrés avec age avant l'envoi et seules les copies chiffrées (.age) partent. +# Les fichiers gardés sur le serveur restent en clair, lisibles par root seulement. +# Restauration d'une copie chiffrée : age -d -i cle-privee.txt fichier.dump.age > fichier.dump set -euo pipefail +# Dumps, archives et empreintes lisibles par le seul propriétaire +umask 077 REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" BACKEND_ENV_FILE="$REPO_DIR/backend/.env" @@ -115,9 +123,43 @@ if [ -n "${BACKUP_S3_BUCKET:-}" ]; then S3_ARGS+=(--region "$BACKUP_S3_REGION") fi - aws s3 cp "$DB_BACKUP_FILE" "$S3_BASE_URI/db/$(basename "$DB_BACKUP_FILE")" "${S3_ARGS[@]}" + # Copies à envoyer : chiffrées si une clé age est configurée, sinon les fichiers tels quels (comportement historique) + UPLOAD_DB_FILE="$DB_BACKUP_FILE" + UPLOAD_MEDIA_FILE="$MEDIA_BACKUP_FILE" + if [ -n "${BACKUP_AGE_RECIPIENT:-}" ]; then + if ! command -v age >/dev/null 2>&1; then + echo "ERROR: BACKUP_AGE_RECIPIENT is set but age is not installed (apt install age)." + exit 1 + fi + AGE_ARGS=() + read -r -a AGE_KEYS <<< "$BACKUP_AGE_RECIPIENT" + # Une clé ssh s'écrit en deux mots (« ssh-ed25519 AAAA... ») : on recolle type et clé + i=0 + while [ "$i" -lt "${#AGE_KEYS[@]}" ]; do + key="${AGE_KEYS[$i]}" + if [[ "$key" == ssh-* ]]; then + i=$((i + 1)) + key="$key ${AGE_KEYS[$i]:-}" + fi + AGE_ARGS+=(-r "$key") + i=$((i + 1)) + done + ENCRYPT_DIR="$(mktemp -d "$BACKUP_ROOT/.offsite-XXXXXX")" + trap 'rm -rf "$ENCRYPT_DIR"' EXIT + UPLOAD_DB_FILE="$ENCRYPT_DIR/$(basename "$DB_BACKUP_FILE").age" + age "${AGE_ARGS[@]}" -o "$UPLOAD_DB_FILE" "$DB_BACKUP_FILE" + if [ "$MEDIA_IS_NEW" = "1" ]; then + UPLOAD_MEDIA_FILE="$ENCRYPT_DIR/$(basename "$MEDIA_BACKUP_FILE").age" + age "${AGE_ARGS[@]}" -o "$UPLOAD_MEDIA_FILE" "$MEDIA_BACKUP_FILE" + fi + echo "Copies chiffrées avec age avant envoi." + else + echo "WARNING: BACKUP_AGE_RECIPIENT absent, les sauvegardes partent en clair." + fi + + aws s3 cp "$UPLOAD_DB_FILE" "$S3_BASE_URI/db/$(basename "$UPLOAD_DB_FILE")" "${S3_ARGS[@]}" if [ "$MEDIA_IS_NEW" = "1" ]; then - aws s3 cp "$MEDIA_BACKUP_FILE" "$S3_BASE_URI/media/$(basename "$MEDIA_BACKUP_FILE")" "${S3_ARGS[@]}" + aws s3 cp "$UPLOAD_MEDIA_FILE" "$S3_BASE_URI/media/$(basename "$UPLOAD_MEDIA_FILE")" "${S3_ARGS[@]}" fi aws s3 cp "$CHECKSUM_FILE" "$S3_BASE_URI/meta/$(basename "$CHECKSUM_FILE")" "${S3_ARGS[@]}" else diff --git a/deploy/deploy.sh b/deploy/deploy.sh index eee5977..88cea52 100644 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -92,6 +92,43 @@ fix_perms() { find "$REPO_DIR/$1" -type f -exec chmod 644 {} + } +# Sauvegarde de la base avant migration, à côté de celles de deploy/backup.sh ($BACKUP_ROOT/pre-migration). +# « prisma migrate status » sort en 0 quand la base est à jour : pas de dump. Tout autre code (migration en +# attente, historique divergent, base injoignable) déclenche le dump, et un dump raté arrête le déploiement. +# Les PREMIGRATION_KEEP derniers dumps sont gardés (10 par défaut). +predeploy_dump() { + if (cd "$REPO_DIR/backend" && npx --no-install prisma migrate status >/dev/null 2>&1); then + log "Aucune migration en attente, pas de sauvegarde avant migration" + return 0 + fi + log "Migrations en attente : sauvegarde de la base avant de les appliquer" + ( + set -euo pipefail + umask 077 + # Sous-shell : DATABASE_URL et les réglages de sauvegarde ne fuient pas dans les builds qui suivent + set -a + # shellcheck disable=SC1090,SC1091 + . "$REPO_DIR/backend/.env" + if [ -f "${BACKUP_ENV_FILE:-/etc/genealogy/backup.env}" ]; then + # shellcheck disable=SC1090 + . "${BACKUP_ENV_FILE:-/etc/genealogy/backup.env}" + fi + set +a + command -v pg_dump >/dev/null 2>&1 || { echo "ERROR: pg_dump absent, déploiement arrêté avant migration"; exit 1; } + [ -n "${DATABASE_URL:-}" ] || { echo "ERROR: DATABASE_URL absent de backend/.env"; exit 1; } + dir="${BACKUP_ROOT:-/var/backups/genealogy}/pre-migration" + mkdir -p "$dir" + file="$dir/genealogy-premigration-$(date -u +%Y%m%dT%H%M%SZ)-${NEW_SHA:0:12}.dump" + if ! pg_dump "$DATABASE_URL" --format=custom --file "$file"; then + rm -f "$file" + echo "ERROR: pg_dump a échoué, déploiement arrêté avant migration" + exit 1 + fi + echo "[deploy] Base sauvegardée : $file" + ls -1t "$dir"/genealogy-premigration-*.dump 2>/dev/null | tail -n +"$((${PREMIGRATION_KEEP:-10} + 1))" | xargs -r rm -f + ) +} + # 4. Backend BACKEND_CHANGED=0 if changed backend ecosystem.config.cjs; then @@ -101,6 +138,7 @@ if changed backend ecosystem.config.cjs; then || { echo "ERROR: dépendances backend manquantes après installation"; exit 1; } log "Build backend" (cd "$REPO_DIR/backend" && NODE_ENV='' npm run prisma:generate && NODE_ENV='' npm run build) + predeploy_dump log "Migrations" (cd "$REPO_DIR/backend" && npm run prisma:deploy) else @@ -117,7 +155,8 @@ else log "Frontend inchangé" fi -# 6. Landing (elle utilise les polices du frontend) +# 6. Landing (autonome, plus aucune police lue dans le frontend ; frontend/src/assets reste dans le +# déclencheur par prudence, sans effet tant que rien n'y change) if changed landing frontend/src/assets; then install_deps landing log "Build landing"