Vaultwarden é seguro? Guia de hardening
O Vaultwarden cifra os itens no cliente, mas o token de admin e o arquivo de backup continuam críticos. Veja os riscos reais e como endurecer a instalação.
O Vaultwarden é seguro? A resposta curta
O Vaultwarden é seguro no ponto mais importante, porque cada item do cofre é cifrado no seu dispositivo antes de chegar ao servidor. O servidor armazena blobs que não consegue ler. Mesmo que alguém copie a base de dados inteira, ainda precisa da palavra-passe mestra para obter algo útil.
Essa resposta depende de muitos fatores, e os pontos que falham são os que você configura. Um painel de administração protegido por um token fácil de adivinhar. Uma porta do contentor publicada para toda a Internet. Um config.json em texto simples. Um arquivo tar de backup armazenado num diretório pessoal no mesmo servidor. Nenhum desses problemas é de criptografia. Todos podem fazer com que os cofres autoalojados sejam esvaziados.
Tudo abaixo pressupõe que a instalação já está funcionando. Se ainda não tiver uma, configure-a primeiro usando o guia de instalação do Vaultwarden para um VPS e depois siga esta lista pela ordem indicada.
O que o servidor realmente armazena
O Vaultwarden implementa o modelo de dados do Bitwarden. O nome, o nome de utilizador, a palavra-passe, as notas e os URIs de um item do cofre são cifrados com uma chave derivada da sua palavra-passe principal, no cliente, antes do envio de qualquer pedido. O conteúdo dos ficheiros anexados é cifrado da mesma forma. O servidor recebe dados opacos aos quais está associado um UUID (identificador universalmente único).
Alguns dados não são texto cifrado. Deve saber exatamente quais:
- O endereço de e-mail da conta, em texto simples.
- As definições e o salt da sua KDF (função de derivação de chaves), porque o cliente precisa deles para reconstruir a chave no início da sessão seguinte.
- Um hash no servidor do hash da palavra-passe principal enviado pelo cliente, usado para autenticar o próprio início de sessão.
- Metadados: pertença a organizações, nomes dos dispositivos e horas do último início de sessão.
- O segredo do método de autenticação de dois fatores que protege o início de sessão no Vaultwarden. Fica na tabela
twofactorsem cifragem, porque o servidor precisa de calcular o código esperado para o comparar com o seu. Este não é o mesmo que um segredo TOTP (palavra-passe de utilização única baseada no tempo) que armazena dentro de um item do cofre, pois esse segredo é cifrado como qualquer outro campo.
A pasta de dados é pequena. Numa instalação Docker, corresponde ao caminho que montou em /data.
sudo ls -l /vw-data/db.sqlite3 contém quase todo o estado. attachments/ contém os ficheiros carregados, um por UUID, e é a única classe importante de dados que não fica em tabelas da base de dados. sends/ contém anexos do Send e destina-se a ser temporário. icon_cache/ pode ser eliminado. rsa_key.pem e os ficheiros associados assinam os JWTs (JSON Web Tokens) dos utilizadores com sessão iniciada. Por isso, uma cópia dessa chave privada pode ser usada para falsificar uma sessão de início de sessão no cofre. config.json só existe depois de ativar a página de administração. O projeto é explícito sobre o seu conteúdo: guarda o token de administração e as suas credenciais SMTP em texto simples.
Na prática, o modelo de ameaças centra-se no acesso ao sistema de ficheiros, não na criptografia da rede. O acesso de leitura a esse único diretório fornece o endereço de e-mail de todos os utilizadores, os respetivos segredos de autenticação de dois fatores, uma chave que permite falsificar sessões e uma cópia offline de todos os cofres para ataques posteriores. Cada passo abaixo existe para impedir o acesso de terceiros a esse diretório.
Corrija primeiro o token de administração
/admin é um painel de controlo completo: lista de utilizadores, convites, eliminação e todas as definições de execução. Está protegido apenas por um segredo partilhado. Não existe nome de utilizador nem autenticação de dois fatores por utilizador.
Os guias mais antigos indicam gerar ADMIN_TOKEN com openssl rand -base64 48. Isto funciona e grava o segredo em texto simples em config.json e no ficheiro de composição. O Vaultwarden também aceita uma string PHC do Argon2 (competição de hashing de palavras-passe), pelo que o valor armazenado é um hash. Gere uma contra um contentor em execução:
docker exec -it vaultwarden /vaultwarden hashOu sem tocar no contentor em execução:
docker run --rm -it vaultwarden/server /vaultwarden hashÉ solicitada uma palavra-passe duas vezes. Em seguida, é apresentada uma linha que começa por $argon2id$. Numa instalação bare metal, execute ./vaultwarden hash. Se preferir usar diretamente a CLI argon2, o projeto documenta os parâmetros mínimos da OWASP:
echo -n 'MySecretPassword' | argon2 "$(openssl rand -base64 32)" -e -id -k 19456 -t 2 -p 1Agora, o problema que pode consumir uma hora. Uma string PHC contém muitos caracteres $, e o Docker Compose trata $ como interpolação de variáveis. Se a colar sem escape num bloco environment:, o valor recebido pelo contentor fica corrompido. Por isso, /admin rejeita um token que sabe estar correto. Existem duas formas seguras. Em docker-compose.yml, duplique cada $:
environment:
ADMIN_TOKEN: $$argon2id$$v=19$$m=19456,t=2,p=1$$UUZxK1FZMkZoRHFQRlVrTXZvS0E3bHpNQW55c2dBN2NORzdsa0Nxd1JhND0$$cUoId+JBUsJutlG4rfDZayExfjq4TCt48aBc9qsc3UINum ficheiro .env, não é necessário fazer escape, mas use aspas simples:
ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$MmeKRnGK5RW5mJS7h3TOL89GrpLPXJPAtTK8FTqj9HM$DqsstvoSAETl9YhnsXbf43WeaUwJC6JhViIvuPoig78'Em seguida, limite a taxa de pedidos do painel e reduza a duração da sessão:
ADMIN_RATELIMIT_SECONDS=300
ADMIN_RATELIMIT_MAX_BURST=3
ADMIN_SESSION_LIFETIME=20Após três tentativas incorretas em cinco minutos, o painel deixa de responder a esse cliente. A sessão de administração expira após 20 minutos de inatividade.
Melhor do que qualquer uma destas opções: desative a página. A maioria das instâncias precisa dela uma vez, para configurar o SMTP e convidar os primeiros utilizadores, e nunca mais. Para a desativar, não defina ADMIN_TOKEN nem DISABLE_ADMIN_TOKEN, remova qualquer chave "admin_token" de config.json e recrie o contentor. É importante remover a chave do ficheiro porque a página de administração grava as definições nesse local, e o valor em config.json tem precedência sobre o ambiente. Remover apenas a variável deixa a página acessível.
Feche o registo antes que alguém encontre o domínio
SIGNUPS_ALLOWED=false
SIGNUPS_VERIFY=true
SHOW_PASSWORD_HINT=falseSIGNUPS_ALLOWED está definido como true por predefinição. Deixe-o assim e qualquer pessoa que aceda ao seu domínio poderá criar uma conta, ficando os respetivos dados no mesmo db.sqlite3 que os seus. Defina-o como false e adicione pessoas através de convites enviados pela página de administração, que precisa de um SMTP funcional. INVITATIONS_ALLOWED também está definido como true por predefinição e permite que os proprietários de organizações convidem outras pessoas. Isto é adequado quando confia nos seus utilizadores e deve estar definido como false numa instância de utilizador único. Se apenas determinados domínios puderem efetuar registos, SIGNUPS_DOMAINS_WHITELIST=example.com é mais restritivo do que o registo aberto e muito menos seguro do que os convites.
SHOW_PASSWORD_HINT está definido como false por predefinição e deve permanecer assim. Quando está ativado, introduzir um endereço de email válido no formulário de início de sessão devolve a dica da palavra-passe principal dessa conta. Isto expõe a dica e confirma que o endereço existe.
Se a sua instância tiver permitido registos abertos durante algum tempo, abra a página de administração e consulte a lista de utilizadores antes de assumir que é a única conta existente.
A porta que você não pretendia publicar
A imagem Docker escuta na porta 80 dentro do contentor. Uma instalação bare-metal usa ROCKET_PORT=8000 por predefinição. O comando de execução documentado publica-a desta forma:
--publish 127.0.0.1:8000:80O prefixo 127.0.0.1: é o ponto principal. Se escrever -p 8000:80, o Docker associa 0.0.0.0 e faz isso escrevendo regras DNAT (tradução de endereços de rede de destino) na tabela nat. Essas regras são avaliadas antes das cadeias filter que o ufw gere. Por isso, ufw status indica que a porta está negada, enquanto a porta continua a responder à Internet. Vale a pena consultar o mecanismo completo no guia sobre portas Docker que contornam o ufw.
Verifique o que está realmente a escutar:
sudo ss -tlnp | grep 8000Um resultado correto contém uma única linha associada a 127.0.0.1:8000. Uma linha associada a 0.0.0.0:8000 significa que o vault está exposto diretamente. Corrija o mapeamento e recrie o contentor. A associação de uma porta é definida quando o contentor é criado, e docker compose restart não a altera:
docker compose up -d --force-recreateHá outra porta que aparece em guias antigos: 3012, a porta WebSocket separada. O suporte para essa porta foi removido no Vaultwarden 1.31.0, porque o tráfego de notificações passou para a porta HTTP principal. WEBSOCKET_ENABLED e WEBSOCKET_PORT são ignorados desde a versão 1.29.0. A opção atual é ENABLE_WEBSOCKET, que usa true por predefinição. Se o firewall ou o ficheiro compose ainda abrir a porta 3012, feche-a.
Termine o TLS num reverse proxy, não no Rocket
O Vaultwarden pode fornecer TLS (transport layer security) diretamente através do Rocket, o seu framework web, mas o projeto recomenda que não faça isso em produção. O TLS integrado do Rocket não tem suporte rigoroso para SNI (server name indication). Por isso, as recomendações de hardening também indicam que deve aceder à sua instância pelo hostname e nunca por um endereço IP isolado. Os intervalos de IP públicos são analisados continuamente. Um vault que responde num endereço IP é um vault que pode ser encontrado.
As partes de um server block do nginx que importam:
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;
}Por predefinição, o nginx client_max_body_size para 1 MB. Sem essa linha, o carregamento de um anexo falha com 413 Request Entity Too Large no log de erros do nginx, enquanto o Vaultwarden não regista nada. Os cabeçalhos Upgrade e Connection transportam o handshake WebSocket para /notifications/hub. Se os remover, o vault continua a funcionar, mas as alterações deixam de aparecer nos outros dispositivos até recarregar manualmente a página.
O Caddy é mais curto e obtém o certificado por sua conta:
vw.example.com {
reverse_proxy 127.0.0.1:8000 {
header_up X-Real-IP {remote_host}
}
}Depois, informe o Vaultwarden:
DOMAIN=https://vw.example.com
IP_HEADER=X-Real-IPIP_HEADER já tem X-Real-IP como valor predefinido. Por isso, é necessário garantir que o proxy define efetivamente esse cabeçalho. Se não o fizer, todas as linhas do log e todos os limites de taxa de início de sessão veem 127.0.0.1, o próprio proxy. Isto faz com que as falhas de um atacante sejam contabilizadas contra todos os utilizadores da instância. Defina também DOMAIN para o URL https real, porque o Vaultwarden cria a partir dele as ligações de convite e de reposição de palavra-passe. As chaves de segurança WebAuthn também ficam associadas a essa origem.
Há um detalhe que muitas pessoas não consideram: a ligação WebSocket envia o token de sessão na query string, como /notifications/hub?access_token=[JWT]. Esse valor aparece sem proteção no access log do proxy. Redija o parâmetro access_token no formato do log ou garanta que esses logs não são enviados para nenhum local que não controle.
Bloqueie tentativas de brute force no endpoint de login
Os limites de pedidos estão ativados por predefinição (LOGIN_RATELIMIT_SECONDS=60, LOGIN_RATELIMIT_MAX_BURST=10). Eles atrasam um atacante, mas não o impedem. O fail2ban impede-o, mas o Vaultwarden precisa primeiro de escrever num ficheiro de log, e isso não acontece por predefinição:
LOG_FILE=/data/vaultwarden.log
LOG_LEVEL=info
EXTENDED_LOGGING=trueUm login falhado produz então exatamente uma linha. Esta é a string que o seu filtro tem de corresponder:
[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.Escreva o filtro em /etc/fail2ban/filter.d/vaultwarden.local:
[INCLUDES]
before = common.conf
[Definition]
failregex = ^.*?Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =E a jail em /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 = 14400Se manteve a página de administração, adicione uma segunda jail cujo failregex seja ^.*Invalid admin token\. IP: <ADDR>.*$, porque as falhas de administração são registadas com uma mensagem diferente e o filtro de login nunca as detetará. Depois, verifique o resultado:
sudo systemctl restart fail2ban
sudo fail2ban-client status vaultwardenUma jail funcional lista o seu ficheiro de log em File list e indica Currently failed: 0. Introduza uma palavra-passe errada três vezes a partir de outra rede. Esse contador aumenta e o endereço aparece em Banned IP list. Se o contador nunca mudar, a causa habitual é logpath: tem de ser o caminho do ficheiro no host, não o caminho /data/... dentro do contentor. A segunda causa habitual é a ausência de X-Real-IP, o que faz com que todos os bloqueios tenham como destino o seu próprio proxy. O restante da configuração, incluindo a jail SSH que já deveria estar a executar, está no guia do fail2ban para Ubuntu 24.04.
A senha mestra continua a ser o sistema inteiro
A encriptação do lado do cliente significa que a senha mestra é a chave. Uma senha mestra curta numa instância cuja base de dados foi copiada por um atacante não fica protegida por nada do que é referido neste artigo, porque o atacante pode atacar essa cópia offline à velocidade permitida pelo próprio hardware. Nenhuma configuração do servidor chega à máquina do atacante.
PASSWORD_ITERATIONS=600000 é o número de iterações do KDF fornecido aos clientes quando criam uma conta nova. As contas existentes mantêm o valor usado na sua criação. Por isso, aumentá-lo não altera nada para os utilizadores que se registaram no ano passado. Esses utilizadores têm de alterar o valor nas definições de segurança do cofre web. Essa alteração volta a encriptar a chave. Informe-os, porque a interface não mostra essa necessidade.
Em seguida, ative a autenticação de dois fatores para cada conta. Ela não protege o texto cifrado, porque a chave do cofre deriva apenas da senha mestra. No entanto, impede que uma senha roubada seja suficiente para iniciar sessão e sincronizar uma cópia. REQUIRE_DEVICE_EMAIL=true adiciona uma confirmação por e-mail na primeira vez que uma conta inicia sessão a partir de um dispositivo não reconhecido.
Backups são onde os cofres autoalojados falham
Uma tar czf da pasta de dados, deixada num diretório pessoal no mesmo VPS, anula todos os passos anteriores. Esse arquivo contém db.sqlite3 com o texto cifrado de todos os utilizadores, rsa_key.pem que falsifica sessões de início de sessão e config.json com o token de administrador e a palavra-passe SMTP em texto simples. O acesso de leitura a esse único ficheiro equivale a acesso de leitura ao cofre.
Duas regras resolvem o problema. Retire o arquivo do servidor. Cifre-o antes de o transferir.
Existe também um problema de integridade. Copiar db.sqlite3 com cp enquanto o serviço está em execução pode produzir um ficheiro que está a ser escrito e que não será aberto. Só descobrirá isso durante a restauração. Utilize o snapshot nativo do SQLite:
sqlite3 /vw-data/db.sqlite3 ".backup '/tmp/vw-db-backup.sqlite3'"O processo de restauração, que é a parte que quase ninguém testa, está descrito no guia de backup e restauração do Vaultwarden.
O que você abdica em comparação com o Bitwarden alojado
Contas claras. O serviço alojado do Bitwarden é operado por pessoas cuja função a tempo inteiro é mantê-lo em funcionamento, com auditorias de terceiros publicadas e alguém de prevenção às 3 da manhã. O self-hosting substitui isso pelo seu próprio ritmo de aplicação de patches.
O Vaultwarden distribui correções de segurança em releases normais. A versão 1.37.0, lançada em 24 July 2026, é a atual em August 2026, e as respetivas notas pedem aos utilizadores que atualizem assim que possível. Uma instância que configurou há um ano e esqueceu está a executar código com um ano. A etiqueta latest não resolve o problema por si só: um container em execução mantém a imagem com que arrancou até executar docker compose pull e o recriar. Configure atualizações automáticas no Ubuntu para os pacotes do host e coloque a atualização do container num lembrete de calendário que irá realmente ler.
A conclusão que um leitor deve tirar é simples: a criptografia usada aqui é a do design do Bitwarden e continua sólida, mas o risco operacional passa inteiramente para si. Se aplicar os patches e fizer cópias de segurança noutro local, uma instância do Vaultwarden num VPS que controla é um local razoável para guardar as suas palavras-passe. Se esses dois hábitos não forem viáveis, pague pelo serviço alojado e dedique a sua atenção a outra coisa. A comparação funcional detalhada está em Vaultwarden comparado com o Bitwarden self-hosted.
Reforce a segurança do host subjacente ao contentor
O Vaultwarden é um processo numa máquina Linux, e o root dessa máquina pode ler /vw-data, independentemente da configuração da aplicação. Execute o contentor como um utilizador sem privilégios com user: "1000:1000" no seu ficheiro compose, atribua a propriedade da pasta de dados de acordo com esse utilizador e monte como somente leitura, com :ro, tudo aquilo que o contentor não precisa de escrever. Depois feche a porta de entrada: Reforço da segurança do SSH num VPS aborda o início de sessão apenas com chaves e a desativação da autenticação por palavra-passe. É isso que impede o ataque básico que ultrapassa todas as medidas anteriores.
FAQ
Alguém pode ler as minhas palavras-passe se roubar a base de dados do Vaultwarden?
Não diretamente. Cada item do cofre é cifrado no cliente com uma chave derivada da palavra-passe principal, pelo que db.sqlite3 contém texto cifrado. O atacante obtém imediatamente o endereço de e-mail de cada conta, as definições do KDF, os metadados de início de sessão e do dispositivo, além dos segredos de autenticação de dois fatores na tabela twofactor, armazenados sem cifragem porque o servidor tem de calcular o código esperado. Também pode atacar o texto cifrado do cofre offline durante o tempo que quiser. Por isso, o comprimento da palavra-passe principal é o fator que determina o resultado.
Devo usar ADMIN_TOKEN ou desativar completamente a página de administração?
Desative-a se puder, porque a maioria das instâncias precisa dela uma vez para configurar o SMTP e convidar utilizadores, mas nunca mais. Para a desativar, não defina ADMIN_TOKEN nem DISABLE_ADMIN_TOKEN, remova qualquer chave "admin_token" de config.json e recrie o contentor. Remover apenas a variável de ambiente não é suficiente, porque as definições gravadas pela página de administração ficam em config.json e têm precedência. Se mantiver a página, armazene o token como um hash Argon2 produzido por vaultwarden hash, em vez de uma cadeia aleatória em texto simples, e defina ADMIN_RATELIMIT_MAX_BURST=3.
O meu ADMIN_TOKEN está correto, mas /admin rejeita-o. Qual é o problema?
Quase sempre é a interpolação de $. Uma cadeia PHC Argon2 contém vários caracteres $, e o Docker Compose expande-os como variáveis dentro de um bloco docker-compose.yml environment:. Assim, o contentor recebe um valor alterado, embora o ficheiro pareça correto. Duplique cada $ para $$ no ficheiro compose ou mova o valor para um ficheiro .env delimitado por aspas simples, onde não é necessário fazer escape. Recrie o contentor depois, porque as alterações de ambiente não são aplicadas por um simples reinício.
Ainda preciso de abrir a porta 3012 para as notificações?
Não. O suporte para tráfego WebSocket na porta 3012 foi removido no Vaultwarden 1.31.0 porque as notificações passaram para a porta HTTP principal, e WEBSOCKET_ENABLED e WEBSOCKET_PORT são ignorados desde a versão 1.29.0. A definição atual é ENABLE_WEBSOCKET, que é true por predefinição. Feche a porta 3012 na firewall e remova-a do ficheiro compose. Depois, confirme que o reverse proxy encaminha os cabeçalhos Upgrade e Connection, porque a sincronização em tempo real depende atualmente deles.