Aller au contenu
Ouvrir la console

Sauvegarder Vaultwarden dans le stockage objet avec restic

restic est un programme de sauvegarde qui chiffre et signe chaque octet sur la machine sauvegardée, avant que quoi que ce soit atteigne le stockage. Son guide Amazon S3 est une référence connue. Comme le stockage objet ZCP utilise l’API S3, la même configuration fonctionne ici avec un point de terminaison différent.

Ce tutoriel réalise toute la configuration de bout en bout. Terraform ou OpenTofu provisionne une VM de gestionnaire de mots de passe Vaultwarden, un réseau, une IP publique et un compartiment de stockage objet. Un script Bash sauvegarde ensuite la base de données SQLite dans ce compartiment selon une minuterie nocturne.

À la fin, vous disposez de :

  • Vaultwarden exécuté sur une VM accessible en HTTPS
  • Un compartiment de stockage objet contenant un dépôt restic
  • Un script de sauvegarde sur une minuterie systemd, avec rétention et contrôles d’intégrité
  • La preuve que les données déposées dans le compartiment sont chiffrées, ainsi qu’une restauration qui les remet dans un coffre fonctionnel

Prévoyez environ 45 minutes.

  • Un compte ZSoftly Public Cloud. Créez-en un si vous n’en avez pas.
  • Terraform 1.0 ou version ultérieure, ou OpenTofu 1.6 ou version ultérieure.
  • Le CLI zcp, pour rechercher les slugs et inspecter le compartiment.
  • Une paire de clés SSH. Générez-en une avec ssh-keygen -t ed25519 si nécessaire.

Dans la console, allez à Compte → Clés d’API, créez un jeton et exportez-le. Le fournisseur et la CLI lisent la même variable :

export ZCP_BEARER_TOKEN="<your-token>"

Le jeton reste hors de votre configuration et de votre fichier d’état.

Le calcul et le stockage objet se trouvent dans des régions distinctes. Les machines virtuelles s’exécutent dans yul-1 (Montréal) ou yow-1 (Ottawa), et les stockages objet se trouvent dans os-yul ou os-yow. Choisissez la paire dans la même ville pour réduire la latence et les frais de transfert, ou l’autre ville pour conserver la sauvegarde hors du domaine de défaillance de la VM qu’elle protège.

zcp region list # regions, including the os- ones
zcp project list # your project slug
zcp plan vm --region yul-1 # compute plans
zcp plan network --region yul-1 # network plans
zcp plan ip --region yul-1 # public IP plans
zcp storage-category list --region yul-1 # VM disk categories
zcp template list --region yul-1 | grep -i vaultwarden
zcp plan object-storage --region os-yul # object storage sizes
zcp storage-category list --region os-yul # object storage categories

L’image Vaultwarden est une application Marketplace. Elle fournit Vaultwarden dans Docker Compose sur Ubuntu 24.04, génère un certificat autosigné et un jeton administrateur au premier démarrage, puis sert le coffre web sur le port 8000.

Créez un répertoire vide avec un fichier, main.tf. Le stockage objet utilise sa propre source de données de région, afin qu’aucune valeur ne soit codée en dur deux fois :

terraform {
required_providers {
zcp = {
source = "zsoftly/zcp"
version = "~> 0.2"
}
}
}
provider "zcp" {
default_project = "default-9"
}
variable "admin_cidr" {
description = "CIDR allowed to reach SSH and the web vault. Deliberately has no default."
type = string
}
data "zcp_region" "compute" {
slug = "yul-1"
}
data "zcp_region" "objects" {
slug = "os-yul"
}
resource "zcp_ssh_key" "vault" {
name = "vault-key"
region = data.zcp_region.compute.slug
public_key = file("~/.ssh/id_ed25519.pub")
}
resource "zcp_network" "vault" {
name = "vault-network"
cloud_provider = data.zcp_region.compute.cloud_provider
region = data.zcp_region.compute.slug
network_plan = "pnet-yul"
billing_cycle = "hourly"
}
resource "zcp_instance" "vault" {
name = "vault-01"
cloud_provider = data.zcp_region.compute.cloud_provider
region = data.zcp_region.compute.slug
template = "zmi-vaultwarden-1360-ubuntu2404-100-1"
plan = "ca2sm"
billing_cycle = "hourly"
network = zcp_network.vault.id
storage_category = "premium-ssd"
ssh_key = zcp_ssh_key.vault.name
assign_public_ip = false
}
resource "zcp_ip_address" "vault" {
plan = "ipv4-yul"
billing_cycle = "hourly"
network = zcp_network.vault.id
}
resource "zcp_egress_rule" "https_out" {
network = zcp_network.vault.id
protocol = "tcp"
cidr = "0.0.0.0/0"
start_port = "443"
end_port = "443"
}
resource "zcp_egress_rule" "http_out" {
network = zcp_network.vault.id
protocol = "tcp"
cidr = "0.0.0.0/0"
start_port = "80"
end_port = "80"
}
resource "zcp_egress_rule" "dns_out" {
network = zcp_network.vault.id
protocol = "udp"
cidr = "0.0.0.0/0"
start_port = "53"
end_port = "53"
}
resource "zcp_firewall_rule" "ssh" {
ip_address = zcp_ip_address.vault.id
protocol = "tcp"
cidr_list = var.admin_cidr
start_port = "22"
end_port = "22"
}
resource "zcp_firewall_rule" "vault_web" {
ip_address = zcp_ip_address.vault.id
protocol = "tcp"
cidr_list = var.admin_cidr
start_port = "8000"
end_port = "8000"
}
resource "zcp_port_forward" "ssh" {
ip_address = zcp_ip_address.vault.id
protocol = "tcp"
public_start_port = "22"
public_end_port = "22"
private_start_port = "22"
private_end_port = "22"
virtual_machine = zcp_instance.vault.id
}
resource "zcp_port_forward" "vault_web" {
ip_address = zcp_ip_address.vault.id
protocol = "tcp"
public_start_port = "8000"
public_end_port = "8000"
private_start_port = "8000"
private_end_port = "8000"
virtual_machine = zcp_instance.vault.id
}
resource "zcp_object_storage" "backups" {
name = "vault-backups"
cloud_provider = data.zcp_region.objects.cloud_provider
region = data.zcp_region.objects.slug
billing_cycle = "hourly"
storage_category = "ssd-storage"
plan = "o210g"
}
resource "zcp_object_storage_bucket" "restic" {
object_storage = zcp_object_storage.backups.id
name = "vaultwarden-restic"
}
output "vault_ip" {
value = zcp_ip_address.vault.ip_address
}
output "object_storage_slug" {
value = zcp_object_storage.backups.id
}
# The platform assigns the bucket a suffixed identifier when it is created, and that
# value is both the bucket slug and the S3 bucket name. The `name` argument above is
# only the name requested, so never use it in an endpoint URL.
output "bucket_name" {
value = zcp_object_storage_bucket.restic.id
}
output "s3_access_key" {
value = zcp_object_storage.backups.api_key
sensitive = true
}
output "s3_secret_key" {
value = zcp_object_storage.backups.api_secret
sensitive = true
}

Plusieurs choix de ce fichier ne sont pas évidents. Une erreur peut vous laisser avec une VM inaccessible ou impossible à sauvegarder.

Les règles de sortie sont obligatoires ici. Un réseau déclaré comme ressource distincte commence sans règle de sortie et refuse tout trafic sortant. Sans ces trois règles, la VM ne peut atteindre ni le dépôt de paquets, ni GitHub, ni le point de terminaison du stockage objet. Chaque commande des étapes suivantes expire. Les règles ci-dessus autorisent le trafic HTTPS, HTTP et DNS vers toute destination. Restreignez les CIDR de destination lorsque vous connaissez les hôtes nécessaires.

Cela concerne la gestion directe de votre réseau. Un réseau créé automatiquement par la VM, via network_plan ou l’option --network-plan de la CLI, est provisionné avec un accès sortant fonctionnel. Prendre la responsabilité du réseau pour un destroy propre implique aussi de prendre la responsabilité de ses règles de sortie.

Le trafic entrant passe par des redirections de port, pas par un NAT statique. La première IP publique allouée à un nouveau réseau devient son adresse source-NAT et la plateforme refuse d’y placer un NAT statique. Un zcp_ip_association sur cette IP indique une réussite sans rien changer, donc le trafic n’arrive jamais. Utilisez plutôt zcp_port_forward. Chaque redirection nécessite une règle de pare-feu sur la même IP pour laisser entrer le trafic.

assign_public_ip = false sur l’instance empêche la plateforme d’allouer une deuxième adresse. La ressource zcp_ip_address fournit l’unique adresse utilisée par le réseau.

zcp_ssh_key accepte un argument region. La documentation du registre pour cette ressource l’omet. L’API en a besoin pour déterminer le fournisseur cloud, donc son absence fait échouer la validation.

Donnez son propre compartiment au dépôt restic. Un dépôt possède exclusivement les chemins dans lesquels il écrit, donc ne pointez pas une deuxième charge de travail vers le même compartiment.

tofu init
tofu plan
tofu apply

L’application attend que la VM indique Running. La VM est l’élément lent, avec environ deux minutes et demie, et toute la pile est prête en trois à quatre minutes. Lisez les sorties :

tofu output vault_ip
tofu output object_storage_slug
tofu output bucket_name
tofu output -raw s3_access_key
tofu output -raw s3_secret_key

object_storage_slug est le slug du stockage, demandé comme premier argument par chaque commande zcp object-storage. bucket_name provient de l’id de la ressource de compartiment, que la plateforme définit à la valeur suffixée qu’elle a attribuée. Cette valeur suffixée est la chaîne à laquelle répond le point de terminaison S3. Confirmez-la dans la plateforme avant de vous y fier, car l’argument name de la ressource contient toujours le nom sans suffixe que vous avez saisi :

zcp object-storage bucket list <object_storage_slug> --region os-yul --project default-9

Terraform marque les deux sorties de clé comme sensibles, donc l’option -raw est nécessaire pour les afficher. Il les écrit tout de même en clair dans l’état. Tout fournisseur qui renvoie un identifiant se comporte de la même façon. Conservez le fichier d’état à un endroit où vous seriez prêt à conserver l’identifiant lui-même.

Étape 5 : Ouvrir le coffre et y ajouter un élément

Section intitulée « Étape 5 : Ouvrir le coffre et y ajouter un élément »

Le premier démarrage prend moins d’une minute. Connectez-vous et attendez la fin :

ssh ubuntu@<vault_ip>
journalctl -u vaultwarden-first-boot.service -f

Lisez le jeton administrateur généré :

sudo cat /root/.credentials/vaultwarden.txt

Ouvrez https://<vault_ip>:8000 dans un navigateur. Le certificat autosigné déclenche un avertissement, acceptez donc l’exception. L’auto-inscription est désactivée par défaut. Invitez-vous depuis le panneau d’administration à https://<vault_ip>:8000/admin, sous Users, connectez-vous puis enregistrez un mot de passe. Vous supprimerez et restaurerez cet élément plus tard. Il doit donc exister avant la première sauvegarde.

Les données du coffre se trouvent dans /data/vaultwarden/data : db.sqlite3, les répertoires des pièces jointes et des envois, config.json et la paire de clés RSA qui signe les jetons clients. Le fichier Compose et le fichier d’environnement se trouvent un niveau plus haut dans /data/vaultwarden.

Ubuntu fournit restic dans ses paquets, mais son archive accuse plusieurs versions de retard. Installez le binaire actuel et vérifiez-le avec les sommes de contrôle publiées :

sudo apt-get update
sudo apt-get install -y bzip2 rsync sqlite3
RESTIC_VERSION=0.19.1
cd /tmp
curl -fsSLO "https://github.com/restic/restic/releases/download/v${RESTIC_VERSION}/restic_${RESTIC_VERSION}_linux_amd64.bz2"
curl -fsSLO "https://github.com/restic/restic/releases/download/v${RESTIC_VERSION}/SHA256SUMS"
sha256sum --ignore-missing -c SHA256SUMS
bunzip2 "restic_${RESTIC_VERSION}_linux_amd64.bz2"
sudo install -m 755 "restic_${RESTIC_VERSION}_linux_amd64" /usr/local/bin/restic
restic version

sha256sum -c doit afficher OK pour l’archive. Si ce n’est pas le cas, arrêtez-vous et téléchargez-la de nouveau.

restic lit sa configuration dans l’environnement. Placez-la dans un fichier réservé à root plutôt que dans un profil shell afin que la minuterie systemd et votre shell interactif partagent une seule source de vérité.

Générez d’abord un mot de passe de dépôt. Toute personne qui obtient une copie du compartiment en a encore besoin pour lire votre coffre :

sudo install -d -m 700 /etc/restic
openssl rand -base64 32 | sudo tee /etc/restic/repo-password > /dev/null
sudo chmod 600 /etc/restic/repo-password
sudo cat /etc/restic/repo-password

Écrivez maintenant le fichier d’environnement en remplaçant le point de terminaison par celui de votre région, puis le nom du compartiment et les deux clés par les sorties Terraform :

sudo install -d -m 700 /var/cache/restic
sudo tee /etc/restic/vaultwarden.env > /dev/null <<'EOF'
RESTIC_REPOSITORY=s3:https://objects.yul.zcp.zsoftly.ca/<your-bucket-name>
RESTIC_PASSWORD_FILE=/etc/restic/repo-password
RESTIC_CACHE_DIR=/var/cache/restic
AWS_ACCESS_KEY_ID=<access-key>
AWS_SECRET_ACCESS_KEY=<secret-key>
EOF
sudo chmod 600 /etc/restic/vaultwarden.env

RESTIC_CACHE_DIR semble mineur, mais son absence casse la minuterie. systemd exécute les services sans $HOME, et restic refuse de démarrer lorsqu’il ne peut localiser un répertoire de cache. Sans cette variable, la minuterie nocturne de l’étape 9 échoue avec unable to locate cache directory: neither $XDG_CACHE_HOME nor $HOME are defined. La placer dans le fichier d’environnement partagé corrige à la fois la minuterie et votre shell interactif.

Les points de terminaison sont https://objects.yul.zcp.zsoftly.ca pour os-yul et https://objects.yow.zcp.zsoftly.ca pour os-yow. Le chemin après l’hôte est le nom du compartiment, qui correspond à la forme s3:https://server/bucket utilisée par restic pour tout serveur compatible S3.

Le fichier est réservé à root. Chargez-le donc dans un shell root plutôt que dans le vôtre. Chaque commande exécutée sur la VM d’ici à l’étape 11 utilise ce shell root et ne porte donc pas sudo. Les commandes zcp de l’étape 10 sont l’exception et s’exécutent sur votre poste de travail :

sudo -i
set -a; . /etc/restic/vaultwarden.env; set +a
restic init
created restic repository db62f25025 at s3:https://objects.yul.zcp.zsoftly.ca/vaultwarden-restic-001024
Please note that knowledge of your password is required to access
the repository. Losing your password means that your data is
irrecoverably lost.

Vaultwarden conserve ses données dans SQLite. Copier un fichier SQLite actif avec cp peut capturer une transaction incomplète. La commande .backup du client sqlite3 crée un instantané cohérent pendant que le conteneur continue de répondre. Aucune interruption n’est nécessaire.

Toujours dans le shell root, écrivez le script dans /root/vaultwarden-backup.sh :

#!/usr/bin/env bash
# Back up Vaultwarden to ZCP object storage with restic.
set -euo pipefail
VW_DIR=/data/vaultwarden
DATA_DIR="${VW_DIR}/data"
STAGE_ROOT=/var/backups/vaultwarden
STAGE_DIR="${STAGE_ROOT}/stage"
ENV_FILE=/etc/restic/vaultwarden.env
set -a
# shellcheck disable=SC1090
. "${ENV_FILE}"
set +a
install -d -m 700 "${STAGE_ROOT}"
install -d -m 700 "${STAGE_DIR}"
trap 'rm -rf "${STAGE_DIR:?}"' EXIT
# Copy the file-based data, skipping the live database and anything regenerable.
rsync -a --delete \
--exclude 'db.sqlite3*' \
--exclude 'icon_cache/' \
--exclude 'tmp/' \
--exclude 'ssl/' \
"${DATA_DIR}/" "${STAGE_DIR}/data/"
# Snapshot the running database without stopping the container.
sqlite3 "${DATA_DIR}/db.sqlite3" ".backup '${STAGE_DIR}/data/db.sqlite3'"
# Refuse to upload a copy that is already damaged.
integrity=$(sqlite3 "${STAGE_DIR}/data/db.sqlite3" 'PRAGMA integrity_check;')
if [ "${integrity}" != "ok" ]; then
echo "[ERROR] integrity check on the staged database returned: ${integrity}" >&2
exit 1
fi
# The env file carries the admin token hash, so a restore needs it too.
install -m 600 "${VW_DIR}/vaultwarden.env" "${STAGE_DIR}/vaultwarden.env"
restic backup --tag vaultwarden "${STAGE_DIR}"
restic forget --tag vaultwarden \
--keep-daily 7 --keep-weekly 4 --keep-monthly 6 \
--prune
restic check
echo "[OK] vaultwarden backup finished"

Installez-le et exécutez-le une première fois manuellement :

install -m 700 /root/vaultwarden-backup.sh /usr/local/sbin/vaultwarden-backup.sh
/usr/local/sbin/vaultwarden-backup.sh

La première exécution téléverse tout. Les suivantes n’envoient que les blocs modifiés. Un coffre qui ajoute quelques mots de passe par jour ne consomme donc que quelques kilooctets par nuit. Un nouveau coffre produit un premier instantané très petit :

Files: 3 new, 0 changed, 0 unmodified
Dirs: 5 new, 0 changed, 0 unmodified
Added to the repository: 276.673 KiB (9.721 KiB stored)
processed 3 files, 274.046 KiB in 0:00
snapshot f5eb9b2f saved

Vérifiez ce qui a été envoyé :

restic snapshots
ID Time Host Tags Paths Size
f5eb9b2f 2026-09-13 21:39:33 vwtest-01 vaultwarden /var/backups/vaultwarden/stage 274.046 KiB

La ligne de rétention conserve 7 instantanés quotidiens, 4 hebdomadaires et 6 mensuels, puis supprime ce qui ne relève d’aucune de ces trois catégories. restic check vérifie que tous les paquets et index référencés existent. Sur un petit dépôt, cela prend quelques secondes. Lorsque le dépôt dépasse quelques dizaines de gigaoctets, placez cette vérification dans un minuteur hebdomadaire séparé et ajoutez-y --read-data-subset=1/7, qui lit et vérifie un septième des données de paquets à chaque exécution.

Créez l’unité de service dans /etc/systemd/system/vaultwarden-backup.service :

[Unit]
Description=Back up Vaultwarden to ZCP object storage with restic
After=network-online.target docker.service
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/vaultwarden-backup.sh
Nice=10
IOSchedulingClass=idle

Puis le minuteur dans /etc/systemd/system/vaultwarden-backup.timer :

[Unit]
Description=Daily Vaultwarden restic backup
[Timer]
OnCalendar=*-*-* 02:30:00
RandomizedDelaySec=20m
Persistent=true
[Install]
WantedBy=timers.target

Activez-le :

systemctl daemon-reload
systemctl enable --now vaultwarden-backup.timer
systemctl list-timers vaultwarden-backup.timer

Confirmez son exécution sous systemd sans attendre le lendemain :

systemctl start vaultwarden-backup.service
systemctl show vaultwarden-backup.service -p Result -p ExecMainStatus
Result=success
ExecMainStatus=0

OnCalendar suit l’horloge de la VM, en UTC sauf modification de votre part. Persistent=true exécute au prochain démarrage une sauvegarde manquée. Une VM éteinte pendant la nuit rattrape donc son retard. Consultez la dernière exécution avec journalctl -u vaultwarden-backup.service -n 50.

Étape 10 : Confirmer que le compartiment contient du texte chiffré

Section intitulée « Étape 10 : Confirmer que le compartiment contient du texte chiffré »

Cette étape répond à la question laissée ouverte par l’absence de chiffrement côté serveur. Chaque dépôt restic contient à sa racine un petit objet nommé config. Sur la VM, restic le déchiffre :

restic cat config
{
"version": 2,
"id": "db62f2502541f42cc6af484cc6b9fc04b24498e3dc6430441a4156bcea6553de",
"chunker_polynomial": "395c3db449e5db"
}

Récupérez maintenant le même objet directement du compartiment. Utilisez les identifiants de gestion de votre poste de travail plutôt que restic :

zcp object-storage object list <object_storage_slug> <your-bucket-name> \
--region os-yul --project default-9
zcp object-storage object download <object_storage_slug> <your-bucket-name> config \
--dest /tmp/restic-config --region os-yul --project default-9
file /tmp/restic-config
head -c 32 /tmp/restic-config | xxd

La liste présente la structure standard du dépôt :

KEY SIZE
config 155
data/00/00d100ce161bbecdd894e17051b75e431d81d8178c3f25c665e27373e1b167ff 1945
data/a3/a31a963e4c25da5fc0a8446cb32840830b6ecf1aa70f26e5145863d6ed13d91c 8081
index/d17710e043e7ba09fa77ab93681d82dc4ca1bfe7a7ad915f0e45c0046987e07e 638
keys/78fd2a6413aa925c0f0fe801933cc68f18568ae82cb7dcede38121e288f25bb4 446
snapshots/f5eb9b2f32fd48703d1f1b4487d33195099250bdeffe625a94149653c66a46bf 379

Le même objet config, extrait directement du compartiment, est illisible :

/tmp/restic-config: data
00000000: 6498 2898 76cb 76fd 3a6a e81f 1dd5 b9db d.(.v.v.:j......
00000010: eac5 6875 a1b5 b406 59c3 7b4c b264 c344 ..hu....Y.{L.d.D

file indique data et le vidage hexadécimal est du bruit. Il en va de même pour les paquets sous data/, qui contiennent le coffre. Rien dans le compartiment n’est lisible sans le mot de passe du dépôt. Ce mot de passe ne quitte jamais la VM ni votre copie hors ligne. restic chiffre avec AES-256 en mode compteur et authentifie avec Poly1305-AES. Un objet modifié échoue donc à la vérification MAC au lieu de se déchiffrer en données plausibles.

Une sauvegarde que vous n’avez jamais restaurée reste une hypothèse. Vérifiez-la maintenant, quand la perte des données n’a pas de conséquence.

Supprimez du coffre web l’élément de mot de passe enregistré à l’étape 5. Restaurez ensuite l’instantané :

restic snapshots --tag vaultwarden
restic restore latest --target /tmp/vault-restore
restoring snapshot f5eb9b2f of [/var/backups/vaultwarden/stage] to /tmp/vault-restore
Summary: Restored 8 files/dirs (274.046 KiB) in 0:00

restore écrit l’arborescence dans /tmp/vault-restore/var/backups/vaultwarden/stage. Remettez la base de données avec le conteneur arrêté afin qu’aucune écriture ne survienne pendant l’opération :

cd /data/vaultwarden
docker compose stop
rsync -a --delete \
--exclude 'ssl/' \
--exclude 'icon_cache/' \
--exclude 'tmp/' \
/tmp/vault-restore/var/backups/vaultwarden/stage/data/ ./data/
chown -R root:root ./data
docker compose start

--delete en fait une restauration ponctuelle plutôt qu’une fusion. Les pièces jointes, envois et config.json reviennent donc à leur état au moment de l’instantané, et tout élément créé depuis est supprimé. Copier seulement la base de données les désynchroniserait.

Les trois exclusions reprennent celles du script de sauvegarde et rendent ici --delete sûr. rsync ne supprime pas un chemin exclu sauf avec --delete-excluded, donc le répertoire actif ssl/ survit. C’est essentiel : ROCKET_TLS dans vaultwarden.env pointe sur /data/ssl/cert.pem, le certificat est généré sur la VM et n’est jamais préparé, et sa suppression empêche le conteneur de démarrer. Vaultwarden régénère icon_cache/ et tmp/.

vaultwarden.env figure aussi dans l’instantané, mais laissez-le intact lors d’un retour en arrière sur la même VM. Une restauration de base ne le modifie pas et l’écraser supprimerait toute modification ultérieure de DOMAIN ou du jeton administrateur. Restaurez-le seulement lors d’une reconstruction sur une autre VM, comme ci-dessous.

Rechargez le coffre web et connectez-vous. L’élément supprimé est revenu. Pour vérifier sans navigateur, lisez directement la base avant et après :

sqlite3 /data/vaultwarden/data/db.sqlite3 'select email from users;'

Pour reconstruire plutôt sur une nouvelle VM, déployez la même image Marketplace et installez restic. Créez le même /etc/restic/vaultwarden.env et le même mot de passe de dépôt, puis restaurez depuis le dépôt existant. Copiez aussi rsa_key.pem et config.json. La clé RSA signe les jetons clients et son remplacement déconnecte tous les clients.

restic restore latest --target /tmp/vault-restore
cp /tmp/vault-restore/var/backups/vaultwarden/stage/vaultwarden.env \
/data/vaultwarden/vaultwarden.env

Détruisez toute la pile après les essais :

tofu destroy

La destruction ne se termine pas en une passe. Attendez-vous aux deux échecs suivants.

Videz d’abord le compartiment. Un compartiment qui contient un dépôt restic refuse d’être supprimé, et la destruction s’arrête avec API error 403 sur zcp_object_storage_bucket. Videz-le puis relancez :

zcp object-storage bucket empty <object_storage_slug> <your-bucket-name> \
--region os-yul --project default-9
Removed 9 object/version entries from bucket "vaultwarden-restic-001024".

L’IP publique est supprimée avec son réseau, pas seule. Comme l’adresse est l’IP source-NAT du réseau, sa libération directe échoue avec API error 403: Source Nat IP cannot be deleted from the associated network. Laissez la destruction du réseau s’en charger :

tofu state rm zcp_ip_address.vault
tofu destroy

Supprimer le réseau libère son adresse. Confirmez qu’il ne reste rien de facturable :

zcp instance list --region yul-1 --project default-9
zcp network list --region yul-1 --project default-9
zcp ip list --region yul-1 --project default-9
zcp object-storage list --region os-yul --project default-9

Tout sur la VM expire, y compris apt-get update. Le réseau ne possède aucune règle de sortie. La VM ne peut donc joindre ni l’archive de paquets, ni GitHub, ni le point de terminaison du stockage d’objets. Vérifiez avec zcp egress list --network <network-slug> --region yul-1. Une liste vide est le problème. Appliquez les trois ressources zcp_egress_rule de l’étape 3. Une VM sur un réseau créé automatiquement ne rencontre pas ce cas, d’où l’absence de sortie dans les tutoriels CLI.

SSH expire juste après l’application. Les règles de pare-feu et de redirection s’activent de façon asynchrone et prennent environ trente secondes. Confirmez qu’elles sont toutes deux Active :

zcp firewall list --ip <ip-slug> --region yul-1 --project default-9
zcp portforward list --ip <ip-slug> --region yul-1 --project default-9

Une liste de pare-feu vide juste après la création d’une règle signifie généralement qu’elle n’est pas encore active. Cela ne signifie pas forcément qu’elle a échoué.

SSH expire toujours et les règles sont Active. Vérifiez les redirections de ports. Le NAT statique sur l’IP source-NAT du réseau ne fait silencieusement rien. Une configuration qui utilise zcp_ip_association au lieu de zcp_port_forward laisse la VM inaccessible tout en indiquant un succès.

API error 500: Private end port is required. Il manque public_end_port ou private_end_port à un zcp_port_forward. Définissez-les à la même valeur que les ports de début.

Le minuteur nocturne échoue mais le script fonctionne manuellement. RESTIC_CACHE_DIR manque dans /etc/restic/vaultwarden.env. systemd exécute les services sans $HOME.

Fatal: unable to open config file ou une erreur de compartiment introuvable. L’URL du dépôt désigne un compartiment inexistant, souvent parce que le suffixe numérique de la plateforme a été omis. Listez les noms réels avec zcp object-storage bucket list <object_storage_slug> --region os-yul.

wrong password or no key found. Le mot de passe du dépôt ne correspond pas. Vérifiez que RESTIC_PASSWORD_FILE pointe vers le fichier généré et que ce fichier n’a pas été modifié depuis.

Les téléversements échouent avec InvalidArgument. Le chiffrement par défaut est activé sur le compartiment. Désactivez-le avec zcp object-storage bucket encryption disable <object_storage_slug> <your-bucket-name> et vérifiez le réglage actuel avec encryption status.

Interroger la documentation ZCP

Posez une question et choisissez un assistant. Il s’ouvre dans un nouvel onglet et lit cette page et toute la documentation ZCP, pour répondre à partir de nos docs.

Exemples de questions

Demander avec

Votre question est envoyée à l’assistant choisi, selon ses propres conditions.