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 :
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¶
É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)¶
- Provisionner un nouveau serveur (voir guide de déploiement)
- Installer Docker + Docker Compose
- Cloner le dépôt Repod
- Créer le fichier
backend.envdepuisbackend.env.example - Exécuter le script de restauration :
- Vérifier le health check et les comptes administrateurs
- Mettre à jour le DNS / reverse proxy si l'IP a changé
- 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_DIRvers 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_DAYSselon 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)