SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor

¿Es seguro Vaultwarden? Lista de hardening

Vaultwarden cifra cada elemento en el cliente y el servidor no ve texto plano. Revise el token de administración y el archivo de copia de seguridad.

¿Es seguro Vaultwarden? La respuesta breve

Vaultwarden es seguro en el punto más importante, porque cada elemento de la bóveda se cifra en el dispositivo antes de llegar al servidor. El servidor almacena blobs que no puede leer. Aunque alguien copie toda la base de datos, seguirá necesitando la contraseña maestra para obtener información útil.

Esta respuesta depende de muchos factores, y los problemas aparecen en los aspectos que usted configura. Un panel de administración protegido por un token fácil de adivinar. Un puerto del contenedor publicado en todo Internet. Un config.json en texto sin cifrar. Un archivo tar de copia de seguridad almacenado en el directorio personal del mismo equipo. Ninguno de esos casos es un problema de criptografía. Todos pueden provocar que se vacíen las bóvedas autohospedadas.

Todo lo siguiente presupone que la instalación funciona. Si todavía no tiene una, configúrela primero con la guía de instalación de Vaultwarden en un VPS y, después, siga esta lista en orden.

Qué almacena realmente el servidor

Vaultwarden implementa el modelo de datos de Bitwarden. El nombre, el nombre de usuario, la contraseña, las notas y las URI de un elemento de la bóveda se cifran con una clave derivada de la contraseña maestra, en el cliente, antes de enviar cualquier solicitud. El contenido de los archivos adjuntos se cifra de la misma forma. El servidor recibe datos opacos asociados a un UUID (identificador único universal).

Algunos datos no son texto cifrado. Debe saber exactamente cuáles:

  • La dirección de correo electrónico de la cuenta, en texto plano.
  • La configuración y la sal de la KDF (función de derivación de claves), porque el cliente las necesita para reconstruir la clave en el siguiente inicio de sesión.
  • Un hash del lado del servidor del hash de la contraseña maestra que envía el cliente, utilizado para autenticar el propio inicio de sesión.
  • Metadatos: pertenencia a organizaciones, nombres de dispositivos y horas del último inicio de sesión.
  • El secreto del método de dos factores que protege el inicio de sesión en Vaultwarden. Se almacena sin cifrar en la tabla twofactor porque el servidor debe calcular el código esperado y compararlo con el suyo. No es lo mismo que un secreto TOTP (contraseña de un solo uso basada en el tiempo) que almacena dentro de un elemento de la bóveda y que se cifra como cualquier otro campo.

La carpeta de datos es pequeña. En una instalación de Docker, es la ruta que montó en /data.

sudo ls -l /vw-data/

db.sqlite3 contiene casi todo el estado. attachments/ contiene los archivos subidos, uno por UUID, y es la única clase importante de datos que no se almacena en tablas de la base de datos. sends/ contiene los archivos adjuntos de Send y está pensada para ser temporal. icon_cache/ se puede eliminar sin consecuencias. rsa_key.pem y sus archivos asociados firman los JWT (tokens web JSON) de los usuarios que han iniciado sesión, por lo que una copia de esa clave privada puede utilizarse para falsificar una sesión de inicio de sesión en la bóveda. config.json sólo existe después de habilitar la página de administración. El proyecto lo indica claramente: contiene el token de administración y las credenciales SMTP en texto plano.

Por tanto, el modelo de amenazas práctico se centra en el acceso al sistema de archivos, no en la criptografía de red. El acceso de lectura a ese único directorio expone las direcciones de correo electrónico de todos los usuarios, sus secretos de 2FA de inicio de sesión, una clave para falsificar sesiones y una copia sin conexión de todas las bóvedas, que se puede atacar sin restricciones de tiempo. Cada paso que se muestra a continuación tiene como objetivo impedir el acceso a ese directorio.

Corrija primero el token de administración

/admin es un panel de control completo: lista de usuarios, invitaciones, eliminación y todos los ajustes del entorno de ejecución. Está protegido únicamente por un secreto compartido. No hay nombre de usuario ni autenticación de dos factores por usuario.

Las guías antiguas indican generar ADMIN_TOKEN con openssl rand -base64 48. Funciona y escribe el secreto en texto plano en config.json y en el archivo de composición. Vaultwarden también acepta una cadena PHC de Argon2 (password hashing competition), por lo que el valor almacenado es un hash. Genere una contra un contenedor en ejecución:

docker exec -it vaultwarden /vaultwarden hash

O sin tocar el contenedor en ejecución:

docker run --rm -it vaultwarden/server /vaultwarden hash

Solicita una contraseña dos veces y después muestra una línea que comienza por $argon2id$. En una instalación bare metal, ejecute ./vaultwarden hash. Si prefiere usar directamente la CLI de argon2, el proyecto documenta los parámetros mínimos de OWASP:

echo -n 'MySecretPassword' | argon2 "$(openssl rand -base64 32)" -e -id -k 19456 -t 2 -p 1

Ahora viene el problema que hace perder una hora. Una cadena PHC contiene muchos caracteres $, y Docker Compose interpreta $ como interpolación de variables. Si la pega sin escapar en un bloque environment:, el valor que llega al contenedor queda alterado y /admin rechaza un token que sabe que es correcto. Hay dos formas seguras. En docker-compose.yml, duplique cada $:

environment:
  ADMIN_TOKEN: $$argon2id$$v=19$$m=19456,t=2,p=1$$UUZxK1FZMkZoRHFQRlVrTXZvS0E3bHpNQW55c2dBN2NORzdsa0Nxd1JhND0$$cUoId+JBUsJutlG4rfDZayExfjq4TCt48aBc9qsc3UI

En un archivo .env no es necesario escapar nada, pero use comillas simples:

ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$MmeKRnGK5RW5mJS7h3TOL89GrpLPXJPAtTK8FTqj9HM$DqsstvoSAETl9YhnsXbf43WeaUwJC6JhViIvuPoig78'

Después, limite la tasa de solicitudes del panel y reduzca la duración de su sesión:

ADMIN_RATELIMIT_SECONDS=300
ADMIN_RATELIMIT_MAX_BURST=3
ADMIN_SESSION_LIFETIME=20

Tras tres intentos incorrectos en cinco minutos, el panel deja de responder a ese cliente. La sesión de administración caduca después de 20 minutos de inactividad.

Mejor aún: desactive la página. La mayoría de las instancias sólo la necesitan una vez, para configurar SMTP e invitar a los primeros usuarios. Después, no vuelven a necesitarla. Para desactivarla, no establezca ADMIN_TOKEN ni DISABLE_ADMIN_TOKEN, elimine cualquier clave "admin_token" de config.json y vuelva a crear el contenedor. Eliminar la clave del archivo es importante porque la página de administración escribe allí los ajustes, y lo que aparece en config.json tiene prioridad sobre el entorno. Eliminar sólo la variable deja la página abierta.

Cierre el registro antes de que alguien encuentre el dominio

SIGNUPS_ALLOWED=false
SIGNUPS_VERIFY=true
SHOW_PASSWORD_HINT=false

SIGNUPS_ALLOWED tiene el valor predeterminado true. Déjelo así y cualquier persona que acceda al dominio podrá crear una cuenta. Sus datos quedarán en el mismo db.sqlite3 que los suyos. Establézcalo en false y añada usuarios mediante invitaciones desde la página de administración. Para ello, SMTP debe funcionar. INVITATIONS_ALLOWED también tiene el valor predeterminado true y permite que los propietarios de una organización inviten a otras personas. Esto es adecuado si confía en sus usuarios. En una instancia de un solo usuario, debería estar establecido en false. Si sólo determinados dominios deben poder registrarse, SIGNUPS_DOMAINS_WHITELIST=example.com es más restrictivo que el registro abierto, pero ofrece mucha menos seguridad que las invitaciones.

SHOW_PASSWORD_HINT tiene el valor predeterminado false y debe mantenerse así. Cuando está habilitado, al introducir una dirección de correo válida en el formulario de inicio de sesión se muestra la pista de la contraseña maestra de esa cuenta. Esto expone la pista y confirma que la dirección existe.

Si su instancia tuvo el registro abierto durante algún tiempo, abra la página de administración y revise la lista de usuarios antes de asumir que sólo existe su cuenta.

El puerto que no pretendía publicar

La imagen de Docker escucha en el puerto 80 dentro del contenedor. Una instalación en el sistema anfitrión usa ROCKET_PORT=8000 de forma predeterminada. El comando de ejecución documentado lo publica así:

--publish 127.0.0.1:8000:80

El prefijo 127.0.0.1: es el elemento fundamental. Si escribe -p 8000:80 en su lugar, Docker enlaza 0.0.0.0 y lo hace escribiendo reglas DNAT (traducción de direcciones de red de destino) en la tabla nat. Esas reglas se evalúan antes de las cadenas filter que administra ufw. Por eso ufw status informa de que el puerto está denegado, aunque el puerto responde normalmente desde Internet. El mecanismo completo se explica en la guía sobre cómo Docker omite ufw al publicar puertos.

Compruebe qué está escuchando realmente:

sudo ss -tlnp | grep 8000

El resultado correcto es una sola línea enlazada a 127.0.0.1:8000. Una línea enlazada a 0.0.0.0:8000 significa que vault está expuesto directamente. Corrija la asignación y vuelva a crear el contenedor, porque la vinculación de puertos queda fijada al crear el contenedor y docker compose restart no la cambiará:

docker compose up -d --force-recreate

En las guías antiguas aparece otro puerto: 3012, el puerto independiente para WebSocket. La compatibilidad con este puerto se eliminó en Vaultwarden 1.31.0 porque el tráfico de notificaciones pasó al puerto HTTP principal. WEBSOCKET_ENABLED y WEBSOCKET_PORT se ignoran desde 1.29.0. La opción actual es ENABLE_WEBSOCKET y su valor predeterminado es true. Si el firewall o el archivo de compose todavía abre 3012, ciérrelo.

Termina TLS en un proxy inverso, no en Rocket

Vaultwarden puede gestionar TLS (seguridad de la capa de transporte) mediante Rocket, su framework web, pero el proyecto recomienda no hacerlo en producción. La implementación TLS integrada de Rocket no admite SNI estricto (indicación del nombre de servidor). Por eso las recomendaciones de endurecimiento también indican acceder a la instancia mediante el nombre de host y nunca mediante una dirección IP sin más. Los rangos de IP públicas se analizan constantemente. Un almacén que responde en una dirección IP acaba siendo localizado.

Estas son las partes relevantes de un bloque de servidor de nginx:

client_max_body_size 525M;

location / {
  proxy_pass http://127.0.0.1:8000;
  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-Proto $scheme;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection $connection_upgrade;
}

nginx establece client_max_body_size en 1 MB de forma predeterminada. Sin esa línea, la carga de un archivo adjunto falla con 413 Request Entity Too Large en el registro de errores de nginx, mientras Vaultwarden no registra nada. Las cabeceras Upgrade y Connection transportan el handshake de WebSocket hasta /notifications/hub. Si las omite, el almacén seguirá funcionando, pero los cambios dejarán de aparecer en los demás dispositivos hasta que vuelva a cargar la página manualmente.

Caddy requiere menos configuración y obtiene el certificado por sí solo:

vw.example.com {
  reverse_proxy 127.0.0.1:8000 {
    header_up X-Real-IP {remote_host}
  }
}

A continuación, informe a Vaultwarden de esa configuración:

DOMAIN=https://vw.example.com
IP_HEADER=X-Real-IP

IP_HEADER ya tiene X-Real-IP como valor predeterminado. Por tanto, debe asegurarse de que el proxy establezca realmente esa cabecera. Si no lo hace, todas las líneas del registro y todos los límites de frecuencia de inicio de sesión verán 127.0.0.1, es decir, el propio proxy. Como resultado, los fallos de un atacante se contabilizan contra todos los usuarios de la instancia. Establezca también DOMAIN en la URL https real, porque Vaultwarden genera a partir de ella los enlaces de invitación y restablecimiento de contraseña, y las claves de seguridad de WebAuthn están vinculadas a ese origen.

Hay un detalle que suele pasarse por alto: la conexión WebSocket envía el token de sesión en la cadena de consulta, como /notifications/hub?access_token=[JWT]. Ese valor aparece sin cifrar en el registro de acceso del proxy. Redacte el parámetro access_token en el formato del registro o asegúrese de que esos registros no se envíen a ningún lugar que no controle.

Bloquee los ataques de fuerza bruta en el endpoint de inicio de sesión

Los límites de tasa están activados de forma predeterminada (LOGIN_RATELIMIT_SECONDS=60, LOGIN_RATELIMIT_MAX_BURST=10). Ralentizan al atacante. No lo detienen. fail2ban sí lo hace, pero Vaultwarden primero debe escribir un archivo de registro y no lo hace de forma predeterminada:

LOG_FILE=/data/vaultwarden.log
LOG_LEVEL=info
EXTENDED_LOGGING=true

Un inicio de sesión fallido genera exactamente una línea, y esta es la cadena que debe coincidir con el filtro:

[2026-08-07 09:14:22][vaultwarden::api::identity][ERROR] Username or password is incorrect. Try again. IP: 203.0.113.10. Username: user@example.com.

Escriba el filtro en /etc/fail2ban/filter.d/vaultwarden.local:

[INCLUDES]
before = common.conf

[Definition]
failregex = ^.*?Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =

Y la jail en /etc/fail2ban/jail.d/vaultwarden.local:

[vaultwarden]
enabled = true
port = 80,443
filter = vaultwarden
banaction = %(banaction_allports)s
logpath = /vw-data/vaultwarden.log
maxretry = 3
bantime = 14400
findtime = 14400

Si conservó la página de administración, añada una segunda jail cuyo failregex sea ^.*Invalid admin token\. IP: <ADDR>.*$, porque los fallos de administración se registran con un mensaje diferente y el filtro de inicio de sesión nunca los detectará. Después, compruebe el resultado:

sudo systemctl restart fail2ban
sudo fail2ban-client status vaultwarden

Una jail operativa muestra el archivo de registro en File list e informa de Currently failed: 0. Escriba una contraseña incorrecta tres veces desde otra red y el contador aumentará; después, la dirección aparecerá en Banned IP list. Si el contador nunca cambia, la causa habitual es logpath: debe ser la ruta del archivo en el host, no la ruta /data/... dentro del contenedor. La segunda causa habitual es la falta de X-Real-IP, que hace que todos los bloqueos se apliquen al propio proxy. El resto de la configuración, incluida la jail de SSH que ya debería estar ejecutándose, se encuentra en la guía de fail2ban para Ubuntu 24.04.

La contraseña maestra sigue siendo todo el sistema

El cifrado del lado del cliente significa que la contraseña maestra es la clave. Una contraseña maestra corta en una instancia cuya base de datos ha copiado un atacante no está protegida por nada de este artículo, porque el atacante puede atacar esa copia sin conexión a la velocidad que permita su hardware. Ningún ajuste del servidor afecta a la máquina del atacante.

PASSWORD_ITERATIONS=600000 se puede interpretar fácilmente de forma incorrecta. Establece las iteraciones del hash del lado del servidor aplicado al hash de inicio de sesión que envía el cliente, y la plantilla de configuración del proyecto indica que un valor modificado se aplica a los usuarios existentes en su siguiente inicio de sesión. No afecta al KDF del lado del cliente, que es el ajuste que determina el coste de un ataque sin conexión contra el almacén. Ese ajuste pertenece a cada cuenta: alguien que se registró hace años puede seguir usando un valor de KDF antiguo y menor hasta que lo aumente en la configuración de seguridad del almacén web, lo que vuelve a cifrar su clave. Debe informarles de ello, porque el servidor no puede hacerlo en su nombre.

Después, habilite la autenticación de dos factores para cada cuenta. No protege el texto cifrado, porque la clave del almacén procede únicamente de la contraseña maestra. Sí evita que una contraseña robada sea suficiente para iniciar sesión y sincronizar una copia. REQUIRE_DEVICE_EMAIL=true obliga a enviar el correo electrónico de inicio de sesión desde un dispositivo nuevo: si Vaultwarden no puede enviarlo, el inicio de sesión falla, por lo que un inicio de sesión desde un dispositivo no reconocido no pasa inadvertido. Necesita un SMTP operativo; de lo contrario, se rechazará cualquier inicio de sesión desde un dispositivo nuevo.

Los problemas de las bóvedas autoalojadas suelen estar en las copias de seguridad

Un tar czf de la carpeta de datos, dejado en un directorio personal del mismo VPS, anula todos los pasos anteriores. Ese archivo contiene db.sqlite3 con el texto cifrado de todos los usuarios, rsa_key.pem que permite falsificar sesiones de inicio de sesión y config.json con el token de administrador y la contraseña SMTP en texto plano. Tener acceso de lectura a ese único archivo equivale a tener acceso de lectura a la bóveda.

Bastan dos reglas. Saque el archivo del equipo. Cífrelo antes de transferirlo.

También existe un problema de integridad. Copiar db.sqlite3 con cp mientras el servicio está en ejecución puede generar un archivo a medio escribir que no se abrirá, y no lo descubrirá hasta la restauración. Use la instantánea propia de SQLite:

sqlite3 /vw-data/db.sqlite3 ".backup '/tmp/vw-db-backup.sqlite3'"

La restauración, que es la parte que casi nadie prueba, se explica en la guía de copias de seguridad y restauración de Vaultwarden.

Lo que asume al elegir Bitwarden alojado

Un análisis honesto. El servicio alojado de Bitwarden lo administra personal cuyo trabajo a tiempo completo es operar el servicio. Cuenta con auditorías externas publicadas y con alguien de guardia a las 3 de la madrugada. El autoalojamiento sustituye todo eso por su propio ritmo de aplicación de parches.

Vaultwarden publica correcciones de seguridad como versiones normales, y lo hace con frecuencia. Tras comprobarlo el 7 de octubre de 2026 en el feed de versiones del proyecto, la versión actual es la 1.37.4, publicada el 5 de octubre de 2026. Sus notas incluyen siete avisos de seguridad. El más grave tiene una clasificación High y recomiendan actualizar lo antes posible. Es la cuarta versión desde la 1.37.0, publicada el 24 de julio de 2026: la 1.37.1 llegó el 29 de julio, la 1.37.2 el 22 de agosto (necesaria para clientes con la versión 2026.8.0 y posteriores) y la 1.37.3 el 13 de septiembre. Esta última revoca los tokens de recuerdo de 2FA cuando cambian las credenciales o 2FA, y limita la tasa de las solicitudes a los endpoints de prelogin y autenticación. Si ejecuta organizaciones con administradores en los que no confía plenamente, las notas de la 1.37.4 también recomiendan rotar la clave de API de cada organización después de actualizar. Una instancia que configuró hace un año y olvidó sigue ejecutando código de hace un año. La etiqueta latest no basta por sí sola: un contenedor en ejecución conserva la imagen con la que se inició hasta que ejecuta docker compose pull y lo vuelve a crear. Configure actualizaciones desatendidas en Ubuntu para los paquetes del host y añada la actualización del contenedor a un recordatorio del calendario que realmente vaya a leer.

La conclusión que debería sacar un lector honesto es la siguiente: la criptografía es la del diseño de Bitwarden y sigue siendo sólida, pero el riesgo operativo recae por completo en usted. Si aplica los parches y guarda una copia de seguridad en otra ubicación, una instancia de Vaultwarden en un VPS que controle es un lugar razonable para almacenar sus contraseñas. Si no va a mantener esos dos hábitos, pague por el servicio alojado y dedique su atención a otra cosa. La comparación detallada de funciones está en Vaultwarden frente a Bitwarden autoalojado.

Refuerce el host subyacente al contenedor

Vaultwarden es un proceso más en un sistema Linux, y root en ese sistema puede leer /vw-data independientemente de la configuración de la aplicación. Ejecute el contenedor con un usuario sin privilegios mediante user: "1000:1000" en el archivo compose y asigne la propiedad de la carpeta de datos para que coincida. Monte como solo lectura todo lo que el contenedor no necesite modificar mediante :ro. Después cierre la puerta de entrada: Refuerzo de SSH en un VPS explica cómo usar el inicio de sesión exclusivamente con claves y deshabilitar la autenticación mediante contraseña. Esto evita el ataque básico que supera todas las medidas anteriores.

FAQ

¿Puede alguien leer mis contraseñas si roba la base de datos de Vaultwarden?

No directamente. Cada elemento de la bóveda se cifra en el cliente con una clave derivada de la contraseña maestra, por lo que db.sqlite3 contiene texto cifrado. Lo que obtiene de inmediato son la dirección de correo electrónico de cada cuenta, la configuración de KDF, los metadatos de inicio de sesión y del dispositivo, y los secretos de autenticación de dos factores de la tabla twofactor, que se almacenan sin cifrar porque el servidor debe calcular el código esperado. También puede atacar el texto cifrado de la bóveda sin conexión durante todo el tiempo que quiera. Por eso, la longitud de la contraseña maestra es el factor que determina el resultado.

¿Debo usar ADMIN_TOKEN o desactivar por completo la página de administración?

Desactívela si puede, porque la mayoría de las instancias sólo la necesitan una vez para configurar SMTP e invitar a usuarios. Después, no vuelven a necesitarla. Para desactivarla, no establezca ni ADMIN_TOKEN ni DISABLE_ADMIN_TOKEN, elimine cualquier clave "admin_token" de config.json y vuelva a crear el contenedor. Eliminar sólo la variable de entorno no es suficiente, porque la configuración escrita desde la página de administración se guarda en config.json y tiene prioridad. Si mantiene la página, almacene el token como un hash Argon2 generado por vaultwarden hash en lugar de una cadena aleatoria en texto plano y establezca ADMIN_RATELIMIT_MAX_BURST=3.

Mi ADMIN_TOKEN es correcto, pero /admin lo rechaza. ¿Cuál es el problema?

Casi siempre se debe a la interpolación de $. Una cadena PHC de Argon2 contiene varios caracteres $, y Docker Compose los expande como variables dentro de un bloque docker-compose.yml environment:. Por tanto, el contenedor recibe un valor alterado aunque el archivo parezca correcto. Duplique cada $ como $$ en el archivo compose o mueva el valor a un archivo .env entre comillas simples, donde no es necesario escapar caracteres. Después, vuelva a crear el contenedor, porque un reinicio no aplica los cambios de entorno.

¿Todavía tengo que abrir el puerto 3012 para las notificaciones?

No. La compatibilidad con el tráfico WebSocket en el puerto 3012 se eliminó en Vaultwarden 1.31.0 porque las notificaciones pasaron al puerto HTTP principal, y WEBSOCKET_ENABLED y WEBSOCKET_PORT se ignoran desde 1.29.0. La configuración actual es ENABLE_WEBSOCKET, que está establecida en true de forma predeterminada. Cierre 3012 en el firewall y elimínelo del archivo compose. Después, asegúrese de que el proxy inverso reenvíe las cabeceras Upgrade y Connection, porque la sincronización en tiempo real depende actualmente de ellas.