Copiar y restaurar Vaultwarden en un VPS
Usa sqlite3 .backup para copiar una bóveda activa de Vaultwarden y conserva attachments, config.json y rsa_key. Verifica 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. La base de datos que contiene debe copiarse de la forma correcta. 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 se suele olvidar.
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. La diferencia entre montajes bind y volúmenes con nombre determina dónde se encuentran realmente los datos de la bóveda en el disco. Esto es lo que contiene.
db.sqlite3: todas las cuentas, todos los elementos de la bóveda, todas las carpetas y todas las organizaciones. Si pierde este archivo, pierde la bóveda.db.sqlite3-walydb.sqlite3-shm: el registro de escritura anticipada (WAL) y su índice de memoria compartida. Las escrituras recientes permanecen aquí hasta que SQLite las integra en el archivo principal.attachments/: los archivos que los usuarios adjuntaron a los elementos de la bóveda, cifrados y distribuidos en un directorio por elemento.sends/: los archivos asociados a los enlaces de Bitwarden Send.config.json: toda la configuración guardada desde la página de administración.rsa_key.pem, además dersa_key.deryrsa_key.pub.deren 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 de sus 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 lo que el servidor almacena texto cifrado que no puede leer. El prefijo 2. indica el tipo de cifrado de Bitwarden, seguido de 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, los nombres de las cuentas, las sugerencias de contraseñas y los códigos de recuperación de la autenticación de dos factores se almacenan en texto claro, junto con metadatos como las fechas de creación y la organización propietaria de un elemento. Por tanto, el propio archivo de copia de seguridad es un secreto. Cualquiera que lo tenga sabrá quiénes son sus usuarios y podrá atacar los blobs cifrados sin conexión, a la velocidad que permita su hardware. Este hecho determina las reglas de almacenamiento que se describen más adelante: la copia se cifra antes de salir del servidor. El token de administrador es la otra parte del mismo problema, y el procedimiento de refuerzo de seguridad para un Vaultwarden autohospedado aborda ambos aspectos.
Por qué copiar db.sqlite3 mientras Vaultwarden se está ejecutando 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 sólo un checkpoint 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 faltar en el archivo sin que nada lo indique.
Copiar los tres archivos con cp tampoco lo soluciona. 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 entonces un archivo a partir del otro y el resultado es incorrecto. El problema se descubre mucho después:
Error: database disk image is malformed.backup evita este problema porque utiliza la Online Backup API de SQLite, que SQLite documenta 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 reinicia la operación si un proceso de escritura modifica el archivo durante la copia. Así, lo que se guarda en disco corresponde a un único estado coherente.
Tome la 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 usar. No la conserve ni elimine la anterior. Toda la secuencia se ejecuta contra un 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 lo que docker exec vaultwarden sqlite3 ... falla con:
exec: "sqlite3": executable file not found in $PATHEjecútela en el host contra 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 bajo /var/lib/docker/volumes/.
Vaultwarden también incluye su propio comando de copia de seguridad desde la versión 1.32.1. En su servidor:
docker exec -it vaultwarden /vaultwarden backupEjecuta 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 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 olvidan con facilidad
attachments/ contiene texto cifrado con nombres opacos. La fila de la base de datos correspondiente a cada archivo adjunto contiene su nombre cifrado y el material de clave que necesita un cliente para descifrarlo. Sin la base de datos, los archivos adjuntos son datos ilegibles. Sin los archivos adjuntos, la base de datos muestra elementos cuyas descargas fallan. Haga ambas copias 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 coincidentes. Esto tiene consecuencias 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 de SMTP y el token de administración. Almacene ese token como una cadena PHC de Argon2id (password hashing competition), no como texto sin formato. 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 de Vault sobrevive a este cambio 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 que están detrás de los enlaces de Send. Si faltan, esas descargas dejan de funcionar 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 operación importante: sqlite3 termina con el código 0 incluso cuando PRAGMA integrity_check informa de corrupción. Por eso, comparar la salida con ok convierte una copia incorrecta en un script fallido. Después, set -euo pipefail detiene todo 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 comprobar 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 journalctl y una unidad que informe de los fallos.
Verifique la copia de seguridad restaurándola en un directorio de prueba
Una copia de seguridad no verificada es sólo una suposición. Restaurarla en un directorio de prueba tarda un minuto y no modifica nada en producción.
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/attachmentsHay 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 parecido al valor del servicio activo que muestra sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 "select count(*) from ciphers;" y nunca es cero en un almacén en uso. 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 usando un registro que pertenece a otra copia. Esto puede dañar una base de datos que se restauró 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 del proceso.
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 vaultwardenchown debe indicar el usuario con el que se ejecuta el contenedor. La imagen estándar se ejecuta como root, por lo que root:root es correcto, salvo que haya establecido user: en el archivo de 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 muestran esta causa.
Un arranque correcto termina con la línea de Rocket:
[INFO] Rocket has launched from http://0.0.0.0:80Después, 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, el archivo contenía la base de datos, pero no attachments/. Conserve data.old.* hasta comprobar todo esto y después elimínelo. Para volver atrás, siga los mismos tres pasos e intercambie las carpetas en el sentido contrario.
Si las rutas no coinciden con las de este procedimiento, la guía de instalación de Vaultwarden en un VPS muestra el archivo de compose que dan por supuesto estos comandos.
Dónde no guardar la copia de seguridad
- No en el mismo disco que la carpeta de datos. Un volumen defectuoso elimina ambas copias, al igual que un
rm -rfen la ruta incorrecta. - No en el mismo servidor, ni siquiera en un segundo volumen. Un atacante que obtiene acceso a root también accede a las copias de seguridad durante la misma sesión.
- No en object storage sin cifrado, porque el archivo contiene direcciones de correo electrónico, indicios de contraseñas, códigos de recuperación y texto cifrado del almacén de credenciales que se puede atacar sin conexión.
- No únicamente en los snapshots del proveedor. Se restauran rápido, por lo que conviene tenerlos, pero están en la misma cuenta que el servidor. Un problema con la cuenta también los 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 --pruneIndique a restic el directorio del archivo y no la carpeta de datos activa. Así, cargará la copia coherente que ya ha comprobado. Guarde la contraseña del repositorio en un lugar distinto del servidor que protege. Si pierde esa contraseña, los snapshots no se podrán leer, por diseño. Cuando el almacenamiento lo permita, asigne al servidor credenciales que puedan escribir, pero no eliminar. Así, si el servidor sufre una intrusión, el atacante no podrá borrar su propio historial. Configurar copias de seguridad de restic en un VPS explica por completo el repositorio y la programación, y restic comparado con BorgBackup explica la elección si todavía no la ha hecho.
Pruebe la restauración con una periodicidad definida
Elija un día al mes. Extraiga la instantánea más reciente en un directorio temporal con restic restore latest --tag vaultwarden --target /tmp/vw-check, ejecute el mismo PRAGMA integrity_check y vuelva a contar las filas. Después, anote la fecha y los recuentos. Una copia de seguridad que nadie ha restaurado en seis meses tiene un estado desconocido. Su estado se descubre durante una interrupción del servicio, que es el peor momento para hacerlo.
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. Esto verifica de extremo a extremo el flujo de la contraseña maestra, algo que ningún recuento de filas puede demostrar. restic check --read-data-subset=10% con la misma periodicidad confirma 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. Una cp del archivo principal por sí sola las pierde silenciosamente, y copiar los dos archivos por separado puede dejar una pareja incoherente que más adelante se manifiesta como Error: database disk image is malformed. Use sqlite3 /path/db.sqlite3 ".backup '/path/out.sqlite3'". Utiliza la Online Backup API de SQLite y produce 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 en un 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 aparecer en el archivo de esa noche. En el peor de los casos, perdería un archivo adjunto. Si no le preocupa una interrupción de unos segundos, docker compose stop antes del script y docker compose start después elimina incluso ese riesgo.
¿Qué ocurre si restauro sin los archivos rsa_key?
Vaultwarden genera una clave nueva al iniciarse. 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 está cifrado con claves derivadas de la contraseña maestra de cada usuario, 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 a un almacenamiento de objetos tal como está?
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 las cuentas, las sugerencias 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 sacarlo del equipo. Un repositorio de restic lo hace por usted, y gpg --symmetric --cipher-algo AES256 vw-20260805-030000.tar.gz produce un único archivo cifrado que puede entregar a cualquier almacenamiento.
¿Cómo hago una copia de seguridad de Vaultwarden en PostgreSQL o MariaDB?
Los pasos para SQLite no se aplican, y el comando integrado se niega 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, obtenidos en la misma ejecución, cifrados y almacenados en un lugar distinto del servidor que los generó.