SSD Nodes Learn 🎉 VPS desde $4.99/mes
Guías Matt ConnorPor Matt Connor

Copia de seguridad y restauración de Vaultwarden en VPS

Usa sqlite3 .backup para copiar Vaultwarden en ejecución y conserva attachments, config.json y rsa_key. Valida la restauración antes de necesitarla.

Qué debe contener una copia de seguridad de Vaultwarden

Una copia de seguridad de Vaultwarden es una copia de toda la carpeta de datos, y la base de datos que contiene debe copiarse correctamente. Ejecute sqlite3 db.sqlite3 ".backup out.sqlite3" en lugar de cp, porque una copia directa de una base de datos que está recibiendo escrituras puede generar un archivo que no se pueda abrir. Después, conserve los archivos que están junto a ella. Esta es la parte que suele olvidarse.

En una instalación de Docker, la carpeta de datos es la que haya montado en /data. Puede ser una ruta del host o un volumen con nombre, y la diferencia entre los montajes bind y los volúmenes con nombre determina dónde se almacenan realmente los datos del almacén. Esto es lo que contiene.

  • db.sqlite3: todas las cuentas, todos los elementos del almacén, todas las carpetas y todas las organizaciones. Si pierde este archivo, pierde el almacén.
  • db.sqlite3-wal y db.sqlite3-shm: el registro de escritura anticipada (WAL) y su índice de memoria compartida. Las escrituras recientes se almacenan aquí hasta que SQLite las integra en el archivo principal.
  • attachments/: los archivos que los usuarios adjuntaron a los elementos del almacén, cifrados y organizados en un directorio por elemento.
  • sends/: los archivos asociados a los enlaces de Bitwarden Send.
  • config.json: toda la configuración que guardó desde la página de administración.
  • rsa_key.pem, además de rsa_key.der y rsa_key.pub.der en instalaciones antiguas: la clave que firma los tokens de inicio de sesión.
  • icon_cache/: los iconos de sitios web descargados. Este es el único directorio que puede omitir, porque Vaultwarden los vuelve a descargar cuando los necesita.

¿Está segura mi base de datos de Vaultwarden? Qué contiene realmente el archivo

Dos comandos responden a esa pregunta, y puede ejecutar ambos ahora mismo.

sudo apt update && sudo apt install -y sqlite3
sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 "select email from users;"
sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 "select name from ciphers limit 1;"

El primero muestra las direcciones de correo electrónico de los usuarios en texto claro. El segundo muestra el nombre de un elemento, con este aspecto:

2.k9Qw1nQ0y7Yy2Xw==|E1r0J3l5s7d9f1g3h5j7k9==|Lm4nOp6qRs8tUv0wXy2zAb4cDe6fGh8i=

Los nombres de los elementos, los nombres de usuario, las contraseñas y las notas se cifran en el cliente antes de enviarse. Por tanto, el servidor almacena texto cifrado que no puede leer. El prefijo 2. indica el tipo de cifrado de Bitwarden. Le siguen un vector de inicialización (IV), el texto cifrado y un MAC (código de autenticación de mensajes), todos codificados en base64 y separados por |. La clave que lo descifra se deriva de la contraseña maestra de la cuenta, que nunca llega al servidor en una forma utilizable. Esto es igual tanto si ejecuta Vaultwarden como el servidor oficial, como se explica en la comparación entre Vaultwarden y Bitwarden autohospedado.

El resto de la base de datos no está cifrado. Las direcciones de correo electrónico, los nombres de las cuentas, las pistas de contraseña y los códigos de recuperación de la autenticación de dos factores se almacenan como texto sin formato, junto con metadatos como las fechas de creación y la organización propietaria de cada elemento. Por tanto, el archivo de copia de seguridad también es un secreto. Cualquiera que lo tenga puede saber quiénes son sus usuarios y atacar los datos cifrados sin conexión a la velocidad que permita su hardware. Este hecho determina las reglas de almacenamiento que se explican más adelante: la copia se cifra antes de salir del servidor.

Por qué copiar db.sqlite3 mientras Vaultwarden está en ejecución no es una copia de seguridad

Vaultwarden ejecuta SQLite en modo WAL de forma predeterminada (ENABLE_DB_WAL=true). Una escritura llega primero a db.sqlite3-wal y un checkpoint sólo la integra en db.sqlite3. Si copia db.sqlite3 por separado, obtiene la base de datos correspondiente al último checkpoint. Por tanto, una contraseña guardada hace diez minutos puede no aparecer en el archivo sin que haya ningún aviso.

Copiar los tres archivos con cp tampoco soluciona el problema. Las copias se crean en momentos ligeramente distintos. Por eso, el WAL guardado puede describir versiones de páginas que ya no coinciden con el archivo principal guardado. SQLite recupera un archivo a partir del otro y el resultado es incorrecto. El problema puede descubrirse mucho después:

Error: database disk image is malformed

.backup evita este problema porque usa la Online Backup API de SQLite. SQLite documenta esta API como el método para copiar una base de datos que puede estar en uso. Lee las páginas con un bloqueo de lectura y vuelve a empezar si un proceso de escritura modifica el archivo durante la operación. Así, lo que se guarda en disco representa un único estado coherente.

Realice una copia de la base de datos con sqlite3 .backup

sudo apt update && sudo apt install -y sqlite3
sudo install -d -m 700 /var/backups/vaultwarden
OUT=/var/backups/vaultwarden/db-$(date '+%Y%m%d-%H%M').sqlite3
sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 ".backup '$OUT'"
sudo sqlite3 "$OUT" "PRAGMA integrity_check;"

El último comando imprime ok en una línea independiente. Cualquier otro resultado significa que la copia no se puede utilizar. No la conserve ni elimine la copia anterior. Toda la secuencia se ejecuta con el servidor activo. Nadie cierra su sesión y ningún contenedor se reinicia.

La herramienta sqlite3 no está dentro del contenedor de Vaultwarden. La imagen se basa en debian:trixie-slim con ca-certificates, curl, libmariadb3, libpq5 y openssl. Por eso docker exec vaultwarden sqlite3 ... falla con:

exec: "sqlite3": executable file not found in $PATH

Ejecútela en el host sobre la ruta montada. Eso es lo que hacen los comandos anteriores. Si los datos están en un volumen con nombre, docker volume inspect <name> muestra la ruta del host en /var/lib/docker/volumes/.

Vaultwarden también incluye su propio comando de copia de seguridad desde la versión 1.32.1. En el servidor:

docker exec -it vaultwarden /vaultwarden backup

El comando ejecuta VACUUM INTO y escribe db_YYYYMMDD_HHMMSS.sqlite3 en la carpeta de datos. Esto tiene dos consecuencias. La copia queda junto al original en el mismo disco, por lo que es un paso de preparación y todavía no es una copia de seguridad. Además, sólo funciona con SQLite. En MariaDB o PostgreSQL se detiene con The database type is not SQLite. Backups only works for SQLite databases.

Los archivos que se suelen olvidar

attachments/ contiene el texto cifrado con nombres opacos. La fila de la base de datos de cada archivo adjunto incluye su nombre cifrado y el material criptográfico que necesita el cliente para descifrarlo. Los archivos adjuntos sin la base de datos son datos ilegibles, y una base de datos sin los archivos adjuntos muestra elementos cuyas descargas fallan. Incluya ambos en la misma ejecución.

config.json contiene todo lo que guardó desde la página de administración, y sus valores tienen prioridad sobre las variables de entorno correspondientes. Esto puede causar problemas en ambos sentidos: restaurar un config.json antiguo sobrescribe silenciosamente la configuración del archivo compose, y el propio archivo es sensible porque puede contener la contraseña SMTP y el token de administración. Almacene ese token como una cadena PHC de Argon2id (password hashing competition), no como texto sin cifrar. docker run --rm -it vaultwarden/server /vaultwarden hash genera una cadena.

rsa_key.pem firma los tokens web JSON (JWT) que mantienen las sesiones iniciadas de los clientes. Si falta el archivo al iniciar, Vaultwarden genera una clave nueva. Por tanto, todos los tokens firmados con la clave anterior dejan de validarse y se cierran las sesiones de todos los clientes. El contenido del almacén permanece intacto porque está cifrado con claves derivadas de la contraseña maestra. Restaurar el archivo de clave evita el cierre masivo de sesiones.

sends/ contiene los archivos asociados a los enlaces de Send. Si falta, esas descargas fallan y nada más se ve afectado.

Ponlo todo en un script

#!/bin/bash
set -euo pipefail

DATA=/opt/vaultwarden/data
DEST=/var/backups/vaultwarden
STAMP=$(date '+%Y%m%d-%H%M%S')
STAGE=$(mktemp -d /tmp/vw-stage.XXXXXX)

install -d -m 700 "$DEST"
sqlite3 "$DATA/db.sqlite3" ".backup '$STAGE/db.sqlite3'"
test "$(sqlite3 "$STAGE/db.sqlite3" 'PRAGMA integrity_check;')" = "ok"
cp -a "$DATA"/rsa_key* "$STAGE/"
for extra in config.json attachments sends; do
  if [ -e "$DATA/$extra" ]; then cp -a "$DATA/$extra" "$STAGE/"; fi
done
tar -C "$STAGE" -czf "$DEST/vw-$STAMP.tar.gz" .
chmod 600 "$DEST/vw-$STAMP.tar.gz"
rm -rf "$STAGE"
tar -tzf "$DEST/vw-$STAMP.tar.gz"

Guárdalo como /usr/local/sbin/vw-backup.sh, chmod 700 y ejecútalo como root. La línea test realiza una tarea importante: sqlite3 devuelve 0 incluso cuando PRAGMA integrity_check informa de corrupción, por lo que comparar la salida con ok convierte una copia defectuosa en un script fallido. set -euo pipefail detiene todo entonces, en lugar de permitir que tar cree un archivo correcto alrededor de una base de datos dañada.

El tar -tzf final muestra lo que realmente capturaste. Léelo la primera vez. Debes buscar ./db.sqlite3, ./rsa_key.pem, ./config.json y ./attachments/, y confirmar que no aparezca ./db.sqlite3-wal. Ejecútalo cada noche con un servicio y un temporizador de systemd en lugar de cron si quieres una salida de journalctl y una unidad que informe de los errores.

Verifique la copia de seguridad restaurándola en un directorio temporal

Una copia de seguridad que no se ha probado es sólo una suposición. Restaurarla en un directorio temporal tarda un minuto y no modifica nada activo.

sudo install -d -m 700 /tmp/vw-check
sudo tar -C /tmp/vw-check -xzf /var/backups/vaultwarden/vw-20260805-030000.tar.gz
ls -l /tmp/vw-check
sudo sqlite3 /tmp/vw-check/db.sqlite3 "PRAGMA integrity_check;"
sudo sqlite3 /tmp/vw-check/db.sqlite3 "select count(*) from users;"
sudo sqlite3 /tmp/vw-check/db.sqlite3 "select count(*) from ciphers;"
sudo du -sh /tmp/vw-check/attachments

Hay cuatro resultados importantes. integrity_check muestra ok. El número de usuarios coincide con el número de cuentas conocidas. El número de cifrados es similar al valor activo de sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 "select count(*) from ciphers;" y nunca es cero en un almacén que se utiliza. El directorio de archivos adjuntos tiene aproximadamente el tamaño esperado. Puede omitir esta comprobación si nadie carga archivos adjuntos. Después ejecute sudo rm -rf /tmp/vw-check, porque ese directorio contiene ahora una segunda copia de todo.

Al restaurar cualquier directorio de datos copiado manualmente, siga una regla: elimine db.sqlite3-wal y db.sqlite3-shm antes de iniciar el servidor. De lo contrario, SQLite intentará recuperar la base de datos restaurada mediante un registro que pertenece a otra copia. Esto puede dañar una base de datos que se había restaurado intacta. Los archivos generados por el script anterior nunca contienen esos archivos, porque .backup escribe una base de datos completa.

Restaurar en el servidor

Ejecute estos comandos en su propio servidor, con el contenedor detenido. Vaultwarden no debe escribir mientras la carpeta de datos cambia por debajo de él.

cd /opt/vaultwarden
docker compose stop vaultwarden
sudo mv data data.old.$(date '+%Y%m%d-%H%M%S')
sudo install -d -m 700 data
sudo tar -C data -xzf /var/backups/vaultwarden/vw-20260805-030000.tar.gz
sudo chown -R root:root data
docker compose start vaultwarden
docker compose logs --tail 20 vaultwarden

chown debe indicar el usuario con el que se ejecuta el contenedor. La imagen predeterminada se ejecuta como root, por lo que root:root es correcto, salvo que haya definido user: en el archivo compose. En ese caso, use ese uid y gid. Si el servidor no puede escribir en la carpeta de datos, la página de inicio de sesión falla en cada solicitud. Los registros lo indican.

Un arranque correcto termina con la línea de Rocket:

[INFO] Rocket has launched from http://0.0.0.0:80

A continuación, inicie sesión desde un navegador, abra un elemento y descargue un archivo adjunto. Si el inicio de sesión funciona pero las descargas de archivos adjuntos fallan, significa que el archivo contenía la base de datos, pero no attachments/. Mantenga data.old.* hasta comprobar todo esto y elimínelo después. La restauración a un estado anterior consta de los mismos tres pasos, pero intercambiando las carpetas.

Si las rutas no coinciden con las indicadas aquí, la guía de instalación de Vaultwarden para un VPS muestra el archivo compose que presuponen estos comandos.

Dónde no guardar la copia de seguridad

  • No en el mismo disco que la carpeta de datos. Un volumen averiado elimina ambas copias, igual que un rm -rf en la ruta incorrecta.
  • No en el mismo servidor, ni siquiera en un segundo volumen. Un atacante que obtiene acceso a root también llega a las copias de seguridad en la misma sesión.
  • No en almacenamiento de objetos sin cifrado, porque el archivo contiene direcciones de correo electrónico, pistas de contraseñas, códigos de recuperación y texto cifrado del almacén de credenciales que se pueden atacar sin conexión.
  • No sólo en las instantáneas del proveedor. Se restauran rápido, por lo que conviene tenerlas, pero están en la misma cuenta que el servidor. Un problema con la cuenta también las afecta.

Una copia externa es el caso de uso de restic, porque un repositorio de restic se cifra en la máquina antes de cargar cualquier dato. En el servidor:

sudo apt install -y restic
export RESTIC_REPOSITORY=s3:https://s3.example.com/vaultwarden-backups
export RESTIC_PASSWORD_FILE=/root/.restic-password
restic init
restic backup /var/backups/vaultwarden --tag vaultwarden
restic snapshots --tag vaultwarden
restic forget --tag vaultwarden --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune

Configure restic para usar el directorio del archivo y no la carpeta de datos activa. Así, lo que carga es la copia coherente que ya comprobó. Guarde la contraseña del repositorio en un lugar distinto del servidor que protege. Si pierde esa contraseña, las instantáneas serán ilegibles por diseño. Cuando el almacenamiento lo permita, asigne al servidor credenciales que puedan escribir, pero no eliminar. Así, una intrusión en el servidor no podrá borrar su propio historial. Configurar copias de seguridad de restic en un VPS explica completamente el repositorio y la programación. restic frente a BorgBackup explica cómo elegir entre ellos si aún no ha tomado una decisión.

Pruebe la restauración con una periodicidad establecida

Elija un día al mes. Extraiga la instantánea más reciente en un directorio de trabajo con restic restore latest --tag vaultwarden --target /tmp/vw-check, ejecute el mismo PRAGMA integrity_check, vuelva a contar las filas y anote la fecha y los recuentos. Una copia de seguridad que nadie ha restaurado en seis meses tiene un estado desconocido. Ese estado se comprueba durante una interrupción, que es el peor momento para descubrirlo.

Una vez al año, realice la prueba completa. Inicie un segundo contenedor de Vaultwarden en un puerto libre con la carpeta de datos restaurada e inicie sesión con una cuenta real. Así se comprueba de extremo a extremo el acceso mediante la contraseña maestra, algo que ningún recuento de filas puede demostrar. restic check --read-data-subset=10% con la misma periodicidad verifica que los datos almacenados se pueden leer y no sólo enumerar.

FAQ

¿Puedo copiar db.sqlite3 con cp mientras Vaultwarden está en ejecución?

No. Vaultwarden ejecuta SQLite en modo WAL, por lo que las escrituras recientes permanecen en db.sqlite3-wal y todavía no están en db.sqlite3. Un cp del archivo principal por sí solo las pierde silenciosamente, y copiar los dos archivos por separado puede dejar un par incoherente que más adelante provoque Error: database disk image is malformed. Use sqlite3 /path/db.sqlite3 ".backup '/path/out.sqlite3'" en su lugar. Usa la Online Backup API de SQLite y genera un archivo coherente mientras el servidor sigue atendiendo solicitudes.

¿Tengo que detener el contenedor de Vaultwarden para hacer una copia de seguridad?

No, y ese es precisamente el objetivo de .backup. La copia de la base de datos es segura con el servidor en ejecución. Los archivos adjuntos y los archivos de Send se escriben cuando un usuario carga uno, por lo que un archivo añadido entre la copia de la base de datos y tar podría no estar en el archivo de esa noche. Como máximo, perdería un archivo adjunto. Si unos segundos de indisponibilidad no son un problema, docker compose stop antes del script y docker compose start después de este elimina incluso esa posibilidad.

¿Qué ocurre si restauro sin los archivos rsa_key?

Vaultwarden genera una clave nueva al iniciar. Esa clave firma los JSON web tokens (JWT) que mantienen activas las sesiones, por lo que todos los tokens existentes dejan de validarse y todos los clientes cierran sesión y deben autenticarse de nuevo. El contenido de la bóveda no se ve afectado, porque se cifra con claves derivadas de la contraseña maestra de cada usuario y no con la clave RSA. Restaure rsa_key.pem junto con el resto de la carpeta de datos y nadie notará la restauración.

¿Es seguro subir el archivo de copia de seguridad al almacenamiento de objetos tal cual?

No. Los nombres de los elementos, las contraseñas y las notas son texto cifrado, pero las direcciones de correo electrónico, los nombres de cuenta, las pistas de contraseña y los códigos de recuperación de la autenticación de dos factores están en texto plano en la base de datos. Además, un atacante sin conexión puede probar el texto cifrado a su propio ritmo. Cifre el archivo antes de que salga de la máquina. Un repositorio de restic lo hace por usted, y gpg --symmetric --cipher-algo AES256 vw-20260805-030000.tar.gz genera un único archivo cifrado que puede entregar a cualquier sistema de almacenamiento.

¿Cómo hago una copia de seguridad de Vaultwarden con PostgreSQL o MariaDB?

Los pasos para SQLite no se aplican, y el comando integrado falla con The database type is not SQLite. Backups only works for SQLite databases. Vuelque la base de datos con su herramienta nativa, pg_dump o mysqldump, y mantenga las demás reglas sin cambios. El volcado debe estar en un único archivo de copia de seguridad junto con attachments/, sends/, config.json y los archivos rsa_key. Todos deben obtenerse en la misma ejecución, cifrarse y almacenarse en un lugar distinto del servidor que los generó.