Sauvegarde et restauration¶
Guide complet pour sauvegarder et restaurer une instance Repod en production.
Vue d'ensemble¶
Depuis la migration vers PostgreSQL (commit 6a711dc), toutes les données
relationnelles (utilisateurs, index des manifestes, inventaire, jobs
d'installation, recherche de paquets, empreintes SSH ssh_known_hosts) vivent
dans une seule base PostgreSQL, accessible via DATABASE_URL. Le
système de fichiers /repos/ ne contient plus que les données non
relationnelles.
| Élément | Emplacement | Criticité | Notes |
|---|---|---|---|
| Base applicative (utilisateurs, manifestes, inventaire, CVE, jobs) | PostgreSQL (DATABASE_URL, volume postgres_data) |
🔴 Critique | Sauvegardée via pg_dump (format custom, restaurable avec pg_restore) |
| Configuration | repos/settings.json |
🔴 Critique | LDAP, OIDC, politique CVE, mirror, backup |
| Manifestes paquets | repos/manifests/ |
🟠 Important | Métadonnées de chaque paquet (cache — reconstructible depuis pool/) |
| Logs d'audit | repos/audit/ |
🟠 Important | Traçabilité réglementaire |
| Clés GPG | repos/gnupg/ |
🔴 Critique | Perte = re-signature de tous les paquets |
| Pool de paquets | repos/pool/ |
🟡 Optionnel | .deb/.rpm/.apk — peut être reconstitué depuis les sources, mais coûteux |
| Décisions CVE / cache KEV/EPSS | repos/security/ |
🟠 Important | Décisions de revue RSSI et caches CVE |
Deux mécanismes de sauvegarde sont disponibles et produisent le même format
d'archive (repod_backup_TIMESTAMP.tar.gz) :
- Sauvegarde intégrée (recommandée) — déclenchable par un admin via l'API
ou l'interface (Paramètres → Sauvegardes), et planifiable via le
scheduler (
backup_daily, cron jobservices/backup.py). - Script
backup.sh— à la racine du projet, pour les sauvegardes externes (cron host, NAS).
Sauvegarde intégrée (admin / scheduler)¶
services/backup.py:create_backup() produit une archive .tar.gz dans
BACKUP_DIR (défaut /repos/backups), contenant :
repod_backup_YYYYMMDD_HHMMSS/
├── postgres.dump ← pg_dump -F c (format custom, restaurable par pg_restore)
├── settings.json
├── audit/
├── security/
├── manifests/
├── gnupg/ ← permissions préservées
└── pool/
create_backup() lève une erreur explicite si DATABASE_URL ou pg_dump est
absent — il n'y a jamais d'archive "incomplète" silencieuse.
Déclencher une sauvegarde manuelle¶
TOKEN=$(curl -s -X POST http://localhost:8000/api/v1/auth/token \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"VotreMotDePasse"}' | jq -r .access_token)
# Créer une sauvegarde
curl -X POST -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/v1/backup/
# Lister les sauvegardes existantes
curl -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/v1/backup/
# Télécharger une archive
curl -H "Authorization: Bearer $TOKEN" \
-o repod_backup_20260601_020000.tar.gz \
http://localhost:8000/api/v1/backup/repod_backup_20260601_020000.tar.gz
# Supprimer une archive
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
http://localhost:8000/api/v1/backup/repod_backup_20260601_020000.tar.gz
Tous ces endpoints exigent le rôle admin.
Planification automatique¶
Dans Paramètres → Sauvegardes :
backup.enabled— active le jobbackup_daily(APScheduler)backup.hour/backup.minute— heure d'exécution (défaut 04:30)backup.retention_count— nombre d'archives conservées (apply_retention()supprime les plus anciennes au-delà de ce nombre)
Le job est journalisé dans l'audit log (BACKUP_CREATE, SUCCESS/FAILURE).
Script backup.sh (sauvegarde externe)¶
Pour les sauvegardes déclenchées depuis l'hôte (cron, NAS), backup.sh à la
racine du projet sauvegarde les mêmes éléments via pg_dump :
# Sauvegarde dans ./backups/ (répertoire par défaut)
DATABASE_URL=postgresql://repod:CHANGE_MOI@localhost:5432/repod ./backup.sh
# Sauvegarde vers un NAS ou volume externe
DATABASE_URL=postgresql://... BACKUP_DIR=/mnt/nas/repod ./backup.sh
# Mode simulation (aucune écriture)
./backup.sh --dry-run
# Rétention personnalisée (défaut : 30 jours)
DATABASE_URL=postgresql://... BACKUP_RETENTION_DAYS=90 BACKUP_DIR=/mnt/nas/repod ./backup.sh
backup.sh requiert DATABASE_URL et pg_dump — il échoue explicitement
(fail) si l'un des deux est absent plutôt que de produire une archive sans
base de données.
Ce que le script sauvegarde¶
repod_backup_YYYYMMDD_HHMMSS.tar.gz
├── postgres.dump ← pg_dump -F c (toutes les données relationnelles)
├── pool/ ← .deb / .rpm / .apk
├── settings.json
├── audit/ ← tous les fichiers *.jsonl
├── security/ ← décisions CVE, caches KEV/EPSS
├── manifests/ ← métadonnées de chaque paquet
└── gnupg/ ← trousseau GPG (permissions 700 préservées)
Installations antérieures à la migration PostgreSQL
Si repos/auth/users.db existe encore (installation pré-migration non
nettoyée), backup.sh le sauvegarde également (users.db, via
sqlite3 .backup si disponible) pour ne pas perdre de données legacy. Les
installations actuelles n'ont pas ce fichier.
Planification avec cron¶
# Sauvegarde quotidienne à 2h00 avec rétention 90 jours
0 2 * * * cd /opt/repod && DATABASE_URL=postgresql://repod:CHANGE_MOI@localhost:5432/repod \
BACKUP_DIR=/mnt/nas/repod BACKUP_RETENTION_DAYS=90 ./backup.sh >> /var/log/repod-backup.log 2>&1
Connexion à PostgreSQL depuis l'hôte
Le service db (PostgreSQL) n'est pas publié sur l'hôte par défaut. Pour
que backup.sh exécuté côté hôte puisse se connecter, soit publiez
temporairement le port 5432 du conteneur db, soit exécutez pg_dump
depuis l'intérieur du conteneur repod-db :
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
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 le backend avant toute restauration
pg_restore --clean supprime et recrée les objets de la base. Arrêtez le
conteneur backend-api (ou la pile entière) avant de restaurer pour
éviter les écritures concurrentes.
Étape 1 — Arrêter les services¶
Étape 2 — Extraire l'archive¶
ARCHIVE="repod_backup_20260601_020000.tar.gz"
BACKUP_NAME="${ARCHIVE%.tar.gz}"
RESTORE_TMP="/tmp/repod_restore"
# Si chiffré GPG, déchiffrer d'abord
# gpg --decrypt "$ARCHIVE.gpg" > "$ARCHIVE"
mkdir -p "$RESTORE_TMP"
tar -xzf "$ARCHIVE" -C "$RESTORE_TMP"
ls "$RESTORE_TMP/$BACKUP_NAME/"
Étape 3 — Démarrer uniquement PostgreSQL et restaurer la base¶
cd /opt/repod
docker compose up -d db
sleep 5 # attendre que PostgreSQL accepte les connexions
# pg_restore --clean recrée les objets ; --if-exists évite les erreurs si la base est vide
docker exec -i repod-db pg_restore -U repod -d repod --clean --if-exists \
< "$RESTORE_TMP/$BACKUP_NAME/postgres.dump"
Étape 4 — Restaurer la configuration et les données /repos/¶
REPOS_DIR="/opt/repod/repos"
RESTORE_DIR="$RESTORE_TMP/$BACKUP_NAME"
# settings.json — sauvegarder l'actuel par précaution
cp "$REPOS_DIR/settings.json" "$REPOS_DIR/settings.json.before-restore" 2>/dev/null || true
cp "$RESTORE_DIR/settings.json" "$REPOS_DIR/settings.json"
# Audit logs — fusionner sans écraser (conserver les logs post-incident)
[ -d "$RESTORE_DIR/audit" ] && cp -rn "$RESTORE_DIR/audit/." "$REPOS_DIR/audit/"
# Manifestes
[ -d "$RESTORE_DIR/manifests" ] && cp -r "$RESTORE_DIR/manifests/." "$REPOS_DIR/manifests/"
# Décisions CVE / caches sécurité
[ -d "$RESTORE_DIR/security" ] && cp -r "$RESTORE_DIR/security/." "$REPOS_DIR/security/"
# Pool de paquets
[ -d "$RESTORE_DIR/pool" ] && cp -r "$RESTORE_DIR/pool/." "$REPOS_DIR/pool/"
# Clés GPG — préserver les permissions
if [ -d "$RESTORE_DIR/gnupg" ]; then
cp -rp "$RESTORE_DIR/gnupg/." "$REPOS_DIR/gnupg/"
chmod 700 "$REPOS_DIR/gnupg"
fi
Étape 5 — Régénérer les arborescences de dépôt¶
Les arborescences dists/ (APT), rpm/ (RPM) et apk/ (Alpine) ne sont pas
incluses dans la sauvegarde — elles sont régénérées depuis pool/ et la base
PostgreSQL restaurée :
cd /opt/repod
docker compose up -d
TOKEN=$(curl -s -X POST http://localhost:8000/api/v1/auth/token \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"VotreMotDePasse"}' | jq -r .access_token)
curl -X POST http://localhost:8000/api/v1/distributions/init \
-H "Authorization: Bearer $TOKEN"
Étape 6 — Vérifier¶
curl -s http://localhost:8000/health | python3 -m json.tool
# Vérifier la connexion admin
curl -s -X POST http://localhost:8000/api/v1/auth/token \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"VotreMotDePasse"}'
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)
tar -xzf "$ARCHIVE" -C "$RESTORE_TMP"
BACKUP_DIR="$RESTORE_TMP/$(basename "$ARCHIVE" .tar.gz)"
# Vérifier le dump PostgreSQL (liste le contenu sans restaurer)
if pg_restore --list "$BACKUP_DIR/postgres.dump" > /dev/null 2>&1; then
echo "✅ postgres.dump : archive valide"
else
echo "❌ postgres.dump : CORROMPU"
fi
# 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
# Manifestes
MANIFEST_COUNT=$(find "$BACKUP_DIR/manifests" -name "*.json" 2>/dev/null | wc -l)
echo "📦 Manifestes : $MANIFEST_COUNT fichiers"
# Audit logs
AUDIT_COUNT=$(find "$BACKUP_DIR/audit" -name "*.jsonl" 2>/dev/null | wc -l)
echo "📋 Audit logs : $AUDIT_COUNT fichiers"
# 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 la base PostgreSQL¶
Symptôme : backend-api échoue au démarrage avec une erreur SQLAlchemy/Alembic,
ou GET /health renvoie checks.critical.auth_db / manifest_db en échec.
ls -lt backups/repod_backup_*.tar.gz
# Suivre les étapes 1 à 3 de la procédure de restauration ci-dessus
# (arrêt des services, extraction, pg_restore --clean --if-exists)
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/dnf/zypper/apk. La restauration des clés depuis le
backup est la seule solution sans re-signer tous les paquets.
ARCHIVE="repod_backup_20260601_020000.tar.gz"
tar -xzf "$ARCHIVE" --strip-components=1 -C /tmp/ "*/gnupg"
cp -rp /tmp/gnupg/. repos/gnupg/
chmod 700 repos/gnupg
docker compose restart backend-api
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
.envetbackend.envdepuis les fichiers.example(DATABASE_URL,POSTGRES_PASSWORD,JWT_SECRET_KEY,SETTINGS_ENCRYPTION_KEY,WEBHOOK_SECRET,REPO_FORMAT) - Démarrer uniquement
db, puis suivre les étapes 2 à 5 de la procédure de restauration ci-dessus - 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¶
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-api
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, suivre la procédure de restauration ci-dessus
# 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 |
|---|---|---|
DATABASE_URL |
— | Requis par backup.sh et la sauvegarde intégrée — chaîne de connexion PostgreSQL |
BACKUP_DIR |
/repos/backups (intégré) / ./backups (backup.sh) |
Répertoire de destination des archives |
REPOS_DIR |
/repos (intégré) / ./repos (backup.sh) |
Répertoire source des données non relationnelles |
BACKUP_RETENTION_DAYS |
30 (backup.sh) |
Durée de rétention des archives locales (0 = pas de purge) |
settings.json["backup"]["retention_count"] |
7 (sauvegarde intégrée) |
Nombre d'archives conservées par apply_retention() |
Checklist opérationnelle¶
Mise en place initiale¶
- Activer
backup.enableddans Paramètres → Sauvegardes (sauvegarde intégrée quotidienne) - Et/ou configurer un cron daily pour
backup.shavecDATABASE_URLdéfini - Définir
BACKUP_DIRvers un volume externe (NAS, S3...) pourbackup.sh - Configurer le chiffrement GPG du backup
- Documenter l'emplacement de la clé GPG de backup (coffre-fort, KMS)
- Définir la rétention selon la politique 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 les sauvegardes (intégrées et/ou cron) se sont bien exécutées
- Vérifier l'intégrité d'au moins 1 archive (
pg_restore --list) - 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)