Vaultwarden e seguro? Como reforcar a instalacao
Entenda o que o Vaultwarden protege no cliente e confira os riscos reais: token de admin, porta exposta, SMTP em texto simples e backups.
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 completa, continua a precisar da palavra-passe principal para obter informação útil.
Essa resposta depende de muitos fatores, e os pontos que falham são os que 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 guardado num diretório pessoal no mesmo servidor. Nenhum desses problemas está relacionado com a criptografia. Todos eles explicam por que razão os cofres autoalojados acabam por ser esvaziados.
Tudo o que se segue pressupõe uma instalação funcional. Se ainda não tiver uma, configure-a primeiro com o guia de instalação do Vaultwarden para um VPS e depois siga esta lista pela ordem apresentada.
O que o servidor realmente armazena
O Vaultwarden implementa o modelo de dados do Bitwarden. O nome, nome de utilizador, palavra-passe, notas e URIs de um item do cofre são cifrados com uma chave derivada da sua palavra-passe principal, no cliente, antes de qualquer pedido ser enviado. O conteúdo dos ficheiros anexos é cifrado da mesma forma. O servidor recebe dados opacos associados a um UUID (identificador universalmente exclusivo).
Alguns elementos não são texto cifrado, e 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 do próximo início de sessão.
- 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 tem de calcular o código esperado para o comparar com o seu. Isto 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. 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 enviados, 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ária. icon_cache/ pode ser eliminada sem consequências. rsa_key.pem e os ficheiros associados assinam os JWTs (JSON Web Tokens) dos utilizadores com sessão iniciada, pelo que uma cópia dessa chave privada pode ser usada para forjar uma sessão de início de sessão no cofre. config.json só existe depois de ativar a página de administração, e o projeto é explícito sobre isso: contém o token de administração e as 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 cada utilizador, os respetivos segredos de autenticação de dois fatores, uma chave para forjar sessões e uma cópia offline de cada cofre, que pode ser atacada sem pressa. Cada passo abaixo existe para impedir o acesso 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 runtime. Está protegido apenas por um segredo partilhado. Não existe nome de utilizador. Não existe autenticação de dois fatores por utilizador.
Os guias mais antigos indicam gerar ADMIN_TOKEN com openssl rand -base64 48. Isso funciona e grava o segredo em texto simples em config.json e no ficheiro de composição. O Vaultwarden também aceita uma cadeia PHC do Argon2 (password hashing competition), 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É pedida 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 utilizar 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 custa uma hora a muitas pessoas. Uma cadeia 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 utilize aspas simples:
ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$MmeKRnGK5RW5mJS7h3TOL89GrpLPXJPAtTK8FTqj9HM$DqsstvoSAETl9YhnsXbf43WeaUwJC6JhViIvuPoig78'Em seguida, limite a taxa de pedidos ao 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 falhadas 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 aí as definições, e o valor existente em config.json tem precedência sobre o ambiente. Remover apenas a variável deixa a página aberta.
Feche o registo antes que alguém encontre o domínio
SIGNUPS_ALLOWED=false
SIGNUPS_VERIFY=true
SHOW_PASSWORD_HINT=falseSIGNUPS_ALLOWED assume true por predefinição. Mantenha essa definição 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 a partir da página de administração, o que requer um SMTP funcional. INVITATIONS_ALLOWED também fica 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 ficar false numa instância de utilizador único. Se apenas determinados domínios puderem criar contas, SIGNUPS_DOMAINS_WHITELIST=example.com é mais restrito do que o registo aberto e muito menos seguro do que os convites.
SHOW_PASSWORD_HINT fica como false por predefinição e deve permanecer assim. Quando esta opção está ativa, 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 esteve com o registo aberto durante algum tempo, abra a página de administração e consulte a lista de utilizadores antes de presumir que é a única conta existente.
A porta que 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. Escreva -p 8000:80 em vez disso e o Docker fará a ligação de 0.0.0.0, escrevendo regras DNAT (tradução de endereços de rede de destino) na tabela nat. Essas regras são avaliadas antes das cadeias filter geridas pelo ufw. Por isso, ufw status indica que a porta está bloqueada, enquanto a porta responde normalmente na 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 é uma única linha ligada a 127.0.0.1:8000. Uma linha ligada a 0.0.0.0:8000 significa que o vault está exposto diretamente. Corrija o mapeamento e recrie o contentor, porque uma ligação de porta fica definida quando o contentor é criado e docker compose restart não a altera:
docker compose up -d --force-recreateHá mais uma porta que aparece em guias antigos: 3012, a porta separada para WebSocket. 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, cujo valor predefinido é true. Se a 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 servir 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), que também é a razão pela qual as recomendações de hardening indicam aceder à instância pelo hostname e nunca por um endereço IP isolado. Os intervalos de IP públicos são analisados continuamente, e um vault que responde num endereço IP é um vault que 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 define client_max_body_size como 1 MB. Sem essa linha, o upload 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 automaticamente:
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, cada linha de log e cada limite de taxa de início de sessão verá 127.0.0.1, o próprio proxy. Isto significa que as falhas de um atacante são contabilizadas para todos os utilizadores da instância. Defina também DOMAIN como o URL https real, porque o Vaultwarden cria a partir dele os links de convite e de reposição de palavra-passe, e as chaves de segurança WebAuthn ficam associadas a essa origem.
Há um detalhe que muitas pessoas ignoram: a ligação WebSocket transmite o token de sessão na query string, como /notifications/hub?access_token=[JWT]. Esse valor aparece em texto simples no log de acesso 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.
Impedir ataques de força bruta no endpoint de login
Os limites de taxa estão ativados por predefinição (LOGIN_RATELIMIT_SECONDS=60, LOGIN_RATELIMIT_MAX_BURST=10). Eles tornam o ataque mais lento. Não impedem um ataque. O fail2ban impede-o, mas o Vaultwarden precisa primeiro de escrever um ficheiro de log, e não faz isso por predefinição:
LOG_FILE=/data/vaultwarden.log
LOG_LEVEL=info
EXTENDED_LOGGING=trueUma tentativa de login falhada gera 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 o 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 um segundo 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á. Em seguida, verifique a configuração:
sudo systemctl restart fail2ban
sudo fail2ban-client status vaultwardenUm 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 uma rede diferente. Esse contador deve aumentar e, em seguida, o endereço deve aparecer em Banned IP list. Se o contador nunca aumentar, 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 é um X-Real-IP em falta, que faz com que todos os bloqueios tenham como destino o seu próprio proxy. O resto da configuração, incluindo o jail SSH que já deveria estar a executar, está no guia do fail2ban para Ubuntu 24.04.
A senha-mestra continua a ser todo o sistema
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 neste artigo, porque o atacante pode atacar essa cópia offline à velocidade permitida pelo próprio hardware. Nenhuma configuração do servidor alcança a máquina do atacante.
PASSWORD_ITERATIONS=600000 é o número de iterações do KDF fornecido aos clientes quando criam uma conta. As contas existentes mantêm o valor com que foram criadas, por isso aumentá-lo não altera nada para os utilizadores que se registaram no ano passado. Eles têm de o alterar manualmente nas definições de segurança do cofre web, o que volta a encriptar a chave. Informe-os, porque a interface não o fará.
Depois, 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 email na primeira vez que uma conta inicia sessão a partir de um dispositivo não reconhecido.
Os backups são onde os vaults self-hosted falham
Uma tar czf da pasta de dados, deixada num diretório pessoal no mesmo VPS, anula todas as etapas anteriores. Esse arquivo contém db.sqlite3 com o ciphertext de todos os utilizadores, rsa_key.pem que forja sessões de login e config.json com o token de administrador e a palavra-passe SMTP em texto simples. O acesso de leitura a esse único ficheiro dá acesso de leitura ao vault.
Duas regras resolvem o problema. Retire o arquivo do servidor. Encripte-o antes de o transferir.
Também existe um problema de consistência. 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á o problema durante a reposição. Utilize o snapshot nativo do SQLite:
sqlite3 /vw-data/db.sqlite3 ".backup '/tmp/vw-db-backup.sqlite3'"A reposição, que é a parte que quase ninguém testa, está documentada em no guia de backup e reposição do Vaultwarden.
O que você abdica em comparação com o Bitwarden alojado
Análise honesta. O serviço alojado do Bitwarden é operado por pessoas cujo trabalho a tempo inteiro é geri-lo, com auditorias de terceiros publicadas e alguém de prevenção às 3 da manhã. O self-hosting substitui isso pelo seu próprio ciclo de aplicação de patches.
O Vaultwarden disponibiliza 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, por si só, não resolve o problema: um contentor em execução mantém a imagem com que arrancou até executar docker compose pull e o recriar. Configure atualizações não supervisionadas no Ubuntu para os pacotes do host e coloque a atualização do contentor num lembrete de calendário que irá realmente consultar.
A conclusão que um leitor deve tirar, de forma honesta, é esta: a criptografia é a do design do Bitwarden e mantém-se sólida, mas o risco operacional passa inteiramente para si. Se aplicar os patches e fizer cópias de segurança para outro 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 possíveis, pague pelo serviço alojado e dedique a sua atenção a outra coisa. A comparação funcional está em Vaultwarden em comparação com o Bitwarden self-hosted.
Fortaleça o host subjacente ao contentor
O Vaultwarden é um processo num sistema Linux, e o root desse sistema 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 forma correspondente e monte como somente leitura tudo o que o contentor não precisar de alterar, usando :ro. Depois feche a porta de entrada: Fortalecimento 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 comum 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, por isso 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 e os segredos de autenticação de dois fatores na tabela twofactor, que são 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 restart.
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 por predefinição é true. 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 é deles que a sincronização em tempo real depende atualmente.