Skip to content

Sauvegarde et restauration

Guide complet pour sauvegarder et restaurer une instance Repod en production.


Vue d'ensemble

Repod stocke ses données dans 5 emplacements critiques :

Élément Chemin (défaut) Criticité Notes
Base utilisateurs & tokens repos/auth/users.db 🔴 Critique SQLite WAL — comptes, rôles, tokens API
Configuration repos/settings.json 🔴 Critique LDAP, OIDC, politique CVE
Manifestes paquets repos/manifests/ 🟠 Important Métadonnées de chaque paquet
Logs d'audit repos/audit/ 🟠 Important Traçabilité réglementaire
Clés GPG repos/gnupg/ 🔴 Critique Perte = re-signature de tous les paquets
Pool .deb repos/pool/ 🟡 Optionnel Peut être reconstitué depuis les sources

Script de sauvegarde (backup.sh)

Le script backup.sh fourni à la racine du projet automatise la sauvegarde complète.

Utilisation rapide

# Sauvegarde dans ./backups/ (répertoire par défaut)
./backup.sh

# Sauvegarde vers un NAS ou volume externe
BACKUP_DIR=/mnt/nas/repod ./backup.sh

# Mode simulation (aucune écriture)
./backup.sh --dry-run

# Rétention personnalisée (défaut : 30 jours)
BACKUP_RETENTION_DAYS=90 BACKUP_DIR=/mnt/nas/repod ./backup.sh

Ce que le script sauvegarde

repod_backup_YYYYMMDD_HHMMSS.tar.gz
├── users.db           ← sqlite3 .backup (cohérent même en prod active)
├── settings.json      ← configuration complète
├── audit/             ← tous les fichiers *.jsonl
│   └── audit_*.jsonl
├── security/          ← tokens révoqués, fichiers de sécurité
├── manifests/         ← métadonnées de chaque paquet uploadé
│   └── *_*.manifest.json
└── gnupg/             ← trousseau GPG (permissions 700 préservées)
    ├── pubring.kbx
    └── private-keys-v1.d/

Le pool .deb n'est pas inclus par défaut

Les fichiers .deb dans repos/pool/ ne sont pas sauvegardés par backup.sh car ils peuvent représenter plusieurs gigaoctets. Si vous souhaitez les inclure, ajoutez une étape rsync vers votre destination de backup :

rsync -av --progress repos/pool/ /mnt/nas/repod/pool/

Planification avec cron

# Sauvegarde quotidienne à 2h00 avec rétention 90 jours
0 2 * * * cd /opt/repod && BACKUP_DIR=/mnt/nas/repod BACKUP_RETENTION_DAYS=90 ./backup.sh >> /var/log/repod-backup.log 2>&1

Chiffrement GPG du backup (recommandé en production)

# Générer une clé de backup dédiée (à faire une seule fois)
gpg --batch --generate-key <<EOF
%no-protection
Key-Type: RSA
Key-Length: 4096
Name-Real: Repod Backup Key
Name-Email: [email protected]
Expire-Date: 2y
EOF

# Chiffrer l'archive après backup
ARCHIVE=$(ls -t backups/repod_backup_*.tar.gz | head -1)
gpg --recipient [email protected] --encrypt "$ARCHIVE"
# Résultat : repod_backup_TIMESTAMP.tar.gz.gpg
# Supprimer l'archive non chiffrée
rm "$ARCHIVE"

# Stocker la clé privée GPG séparément du backup (coffre-fort, KMS…)
gpg --export-secret-keys [email protected] | gpg --symmetric --output backup-key.gpg.enc

Procédure de restauration

Arrêtez Repod avant toute restauration

Une restauration sur un service actif peut corrompre la base SQLite. Arrêtez toujours les conteneurs avant de restaurer.

Étape 1 — Arrêter les services

cd /opt/repod
docker compose down

Étape 2 — Localiser l'archive de backup

# Lister les backups disponibles (du plus récent au plus ancien)
ls -lt /mnt/nas/repod/repod_backup_*.tar.gz

# Si chiffré GPG, déchiffrer d'abord
gpg --decrypt repod_backup_20260601_020000.tar.gz.gpg > repod_backup_20260601_020000.tar.gz

Étape 3 — Extraire l'archive

BACKUP_ARCHIVE="repod_backup_20260601_020000.tar.gz"
BACKUP_NAME="${BACKUP_ARCHIVE%.tar.gz}"
RESTORE_TMP="/tmp/repod_restore"

mkdir -p "$RESTORE_TMP"
tar -xzf "$BACKUP_ARCHIVE" -C "$RESTORE_TMP"
ls "$RESTORE_TMP/$BACKUP_NAME/"

Étape 4 — Restaurer la base utilisateurs

REPOS_DIR="/opt/repod/repos"
BACKUP_DIR="$RESTORE_TMP/$BACKUP_NAME"

# Sauvegarder la base actuelle (précaution)
cp "$REPOS_DIR/auth/users.db" "$REPOS_DIR/auth/users.db.before-restore"

# Restaurer avec sqlite3 (méthode cohérente WAL)
if command -v sqlite3 &>/dev/null; then
    sqlite3 "$BACKUP_DIR/users.db" ".backup '$REPOS_DIR/auth/users.db'"
    echo "✅ users.db restaurée"
else
    cp "$BACKUP_DIR/users.db" "$REPOS_DIR/auth/users.db"
    echo "⚠️  users.db copiée directement (sqlite3 absent)"
fi

Étape 5 — Restaurer la configuration

# Sauvegarder la config actuelle
cp "$REPOS_DIR/settings.json" "$REPOS_DIR/settings.json.before-restore" 2>/dev/null || true

# Restaurer
cp "$BACKUP_DIR/settings.json" "$REPOS_DIR/settings.json"
echo "✅ settings.json restauré"

Étape 6 — Restaurer les logs d'audit

# Fusionner (ne pas écraser — pour conserver les logs post-incident)
if [ -d "$BACKUP_DIR/audit" ]; then
    cp -rn "$BACKUP_DIR/audit/." "$REPOS_DIR/audit/"
    echo "✅ Audit logs restaurés (fichiers manquants uniquement)"
fi

Étape 7 — Restaurer les manifestes

if [ -d "$BACKUP_DIR/manifests" ]; then
    cp -r "$BACKUP_DIR/manifests/." "$REPOS_DIR/manifests/"
    echo "✅ Manifestes restaurés"
fi

Étape 8 — Restaurer les clés GPG

if [ -d "$BACKUP_DIR/gnupg" ]; then
    cp -rp "$BACKUP_DIR/gnupg/." "$REPOS_DIR/gnupg/"
    chmod 700 "$REPOS_DIR/gnupg"
    chmod 600 "$REPOS_DIR/gnupg"/* 2>/dev/null || true
    echo "✅ Trousseau GPG restauré"
fi

Étape 9 — Vérifier les permissions

# L'utilisateur qui fait tourner Docker doit pouvoir lire les fichiers
chown -R 1000:1000 "$REPOS_DIR/auth/" "$REPOS_DIR/manifests/" "$REPOS_DIR/audit/" 2>/dev/null || true
chmod 600 "$REPOS_DIR/auth/users.db"
echo "✅ Permissions corrigées"

Étape 10 — Redémarrer et vérifier

cd /opt/repod
docker compose up -d

# Attendre que l'API soit prête
sleep 10
curl -s http://localhost:8000/health | python3 -m json.tool

# Vérifier les utilisateurs
curl -s -H "Authorization: Bearer <token-admin>" http://localhost:8000/auth/users

Script de restauration complet (one-shot)

#!/bin/bash
# restore.sh — Restauration complète Repod
# Usage : ./restore.sh /chemin/vers/repod_backup_TIMESTAMP.tar.gz
set -euo pipefail

ARCHIVE="${1:?Usage: $0 <archive.tar.gz>}"
REPOS_DIR="${REPOS_DIR:-$(pwd)/repos}"
RESTORE_TMP=$(mktemp -d)
BACKUP_NAME=$(basename "$ARCHIVE" .tar.gz)

echo "[restore] Archive : $ARCHIVE"
echo "[restore] Destination : $REPOS_DIR"

# Arrêt des services
docker compose down || true

# Extraction
tar -xzf "$ARCHIVE" -C "$RESTORE_TMP"
BACKUP_DIR="$RESTORE_TMP/$BACKUP_NAME"

# Backups de précaution
ts=$(date +%Y%m%d_%H%M%S)
[ -f "$REPOS_DIR/auth/users.db"  ] && cp "$REPOS_DIR/auth/users.db"  "$REPOS_DIR/auth/users.db.pre-$ts"
[ -f "$REPOS_DIR/settings.json"  ] && cp "$REPOS_DIR/settings.json"  "$REPOS_DIR/settings.json.pre-$ts"

# Restauration
[ -f "$BACKUP_DIR/users.db"     ] && sqlite3 "$BACKUP_DIR/users.db" ".backup '$REPOS_DIR/auth/users.db'" && echo "✅ users.db"
[ -f "$BACKUP_DIR/settings.json"] && cp "$BACKUP_DIR/settings.json" "$REPOS_DIR/settings.json"           && echo "✅ settings.json"
[ -d "$BACKUP_DIR/manifests"    ] && cp -r "$BACKUP_DIR/manifests/." "$REPOS_DIR/manifests/"              && echo "✅ manifests"
[ -d "$BACKUP_DIR/audit"        ] && cp -rn "$BACKUP_DIR/audit/."    "$REPOS_DIR/audit/"                  && echo "✅ audit"
[ -d "$BACKUP_DIR/gnupg"        ] && cp -rp "$BACKUP_DIR/gnupg/."    "$REPOS_DIR/gnupg/" && chmod 700 "$REPOS_DIR/gnupg" && echo "✅ gnupg"

# Nettoyage
rm -rf "$RESTORE_TMP"

# Redémarrage
docker compose up -d
echo ""
echo "[restore] Restauration terminée. Vérifiez : curl http://localhost:8000/health"

Vérification de l'intégrité du backup

Vérifier qu'un backup est valide sans effectuer de restauration :

ARCHIVE="repod_backup_20260601_020000.tar.gz"
RESTORE_TMP=$(mktemp -d)

# Extraction en zone temporaire
tar -xzf "$ARCHIVE" -C "$RESTORE_TMP"
BACKUP_DIR="$RESTORE_TMP/$(basename "$ARCHIVE" .tar.gz)"

# Vérification users.db
if sqlite3 "$BACKUP_DIR/users.db" "PRAGMA integrity_check;" | grep -q "ok"; then
    echo "✅ users.db : intégrité OK"
else
    echo "❌ users.db : CORROMPU"
fi

# Vérification settings.json
if python3 -m json.tool "$BACKUP_DIR/settings.json" > /dev/null 2>&1; then
    echo "✅ settings.json : JSON valide"
else
    echo "❌ settings.json : JSON invalide"
fi

# Compter les manifestes
MANIFEST_COUNT=$(find "$BACKUP_DIR/manifests" -name "*.json" 2>/dev/null | wc -l)
echo "📦 Manifestes : $MANIFEST_COUNT fichiers"

# Compter les logs d'audit
AUDIT_COUNT=$(find "$BACKUP_DIR/audit" -name "*.jsonl" 2>/dev/null | wc -l)
echo "📋 Audit logs : $AUDIT_COUNT fichiers"

# Vérifier les clés GPG
if [ -d "$BACKUP_DIR/gnupg" ]; then
    echo "🔑 GPG : répertoire présent"
else
    echo "⚠️  GPG : absent du backup"
fi

rm -rf "$RESTORE_TMP"

Stratégie de backup recommandée (3-2-1)

La règle 3-2-1 est la référence de l'industrie :

Règle Description Implémentation recommandée
3 copies Au moins 3 copies des données Local + NAS + cloud
2 supports différents Sur 2 types de médias distincts Disque local + NAS ou bande
1 hors site Au moins 1 copie hors site S3 / cloud chiffré GPG
# Exemple : envoi vers S3 après backup
ARCHIVE=$(ls -t backups/repod_backup_*.tar.gz | head -1)
aws s3 cp "$ARCHIVE" s3://mon-bucket-backup/repod/

Plan de reprise d'activité (PRA)

Objectifs de reprise

Indicateur Valeur cible Notes
RPO (Recovery Point Objective) ≤ 24 heures Backup quotidien recommandé
RTO (Recovery Time Objective) ≤ 2 heures Temps de restauration complète

Scénarios couverts

Scénario 1 — Corruption de users.db

Symptôme : erreur database disk image is malformed dans les logs.

# Identifier le dernier backup valide
ls -lt backups/repod_backup_*.tar.gz

# Restaurer uniquement users.db (sans arrêter les autres services si possible)
ARCHIVE="repod_backup_20260601_020000.tar.gz"
tar -xzf "$ARCHIVE" --strip-components=1 -C /tmp/ "*/users.db"
docker compose stop backend
sqlite3 /tmp/users.db ".backup 'repos/auth/users.db'"
docker compose start backend

Scénario 2 — Perte des clés GPG

Situation critique

La perte des clés GPG rend les paquets déjà signés non vérifiables par les clients apt. La restauration des clés depuis le backup est la seule solution sans re-signer tous les paquets.

# Restaurer les clés GPG
tar -xzf "$ARCHIVE" --strip-components=1 -C /tmp/ "*/gnupg"
cp -rp /tmp/gnupg/. repos/gnupg/
chmod 700 repos/gnupg
docker compose restart backend

Scénario 3 — Sinistre total (serveur perdu)

  1. Provisionner un nouveau serveur (voir guide de déploiement)
  2. Installer Docker + Docker Compose
  3. Cloner le dépôt Repod
  4. Créer le fichier backend.env depuis backend.env.example
  5. Exécuter le script de restauration :
    ./restore.sh /mnt/nas/repod/repod_backup_TIMESTAMP.tar.gz
    
  6. Vérifier le health check et les comptes administrateurs
  7. Mettre à jour le DNS / reverse proxy si l'IP a changé
  8. Notifier les équipes (RSSI, ops) et documenter l'incident

Scénario 4 — Rollback de configuration

# Restaurer uniquement settings.json depuis le backup
ARCHIVE="repod_backup_20260601_020000.tar.gz"
tar -xzf "$ARCHIVE" --strip-components=1 -C /tmp/ "*/settings.json"
cp repos/settings.json repos/settings.json.rollback-$(date +%Y%m%d)
cp /tmp/settings.json repos/settings.json
docker compose restart backend

Tests de restauration

Testez votre backup au moins une fois par trimestre

Un backup non testé est un backup dont on ne connaît pas l'état réel.

# Procédure de test sur un serveur de staging
# 1. Copier le dernier backup sur le serveur de staging
scp /mnt/nas/repod/repod_backup_latest.tar.gz staging:/tmp/

# 2. Sur le staging, exécuter la restauration
ssh staging "cd /opt/repod-staging && ./restore.sh /tmp/repod_backup_latest.tar.gz"

# 3. Vérifier que le service est opérationnel
ssh staging "curl -s http://localhost:8000/health"

# 4. Vérifier qu'un login admin fonctionne
# 5. Vérifier qu'un paquet est visible dans l'interface
# 6. Documenter le test (date, durée, résultat)

Variables d'environnement du backup

Variable Défaut Description
BACKUP_DIR ./backups Répertoire de destination des archives
REPOS_DIR ./repos Répertoire source des données Repod
BACKUP_RETENTION_DAYS 30 Durée de rétention des archives locales (0 = pas de purge)

Checklist opérationnelle

Mise en place initiale

  • Configurer un cron daily pour backup.sh
  • Définir BACKUP_DIR vers un volume externe (NAS, S3...)
  • Configurer le chiffrement GPG du backup
  • Documenter l'emplacement de la clé GPG de backup (coffre-fort, KMS)
  • Définir BACKUP_RETENTION_DAYS selon la politique de rétention RGPD
  • Tester une restauration complète sur environnement de staging
  • Documenter le RTO/RPO réel mesuré lors du test

Contrôle mensuel

  • Vérifier que le cron s'est bien exécuté (logs)
  • Vérifier l'intégrité d'au moins 1 archive (script de vérification ci-dessus)
  • Vérifier l'espace disque disponible sur la destination backup
  • Vérifier que la rotation des anciennes archives fonctionne

En cas d'incident

  • Identifier le dernier backup valide avant l'incident
  • Évaluer le RPO (données perdues depuis ce backup)
  • Suivre la procédure de restauration adaptée au scénario
  • Documenter l'incident et la restauration dans le registre des incidents
  • Si données personnelles impactées : notifier la CNIL dans les 72h (Art. 33 RGPD)