Recentemente eu migrei minha gestão de senhas para uma instância própria do Vaultwarden em Docker. A instalação parecia simples, mas o container subia e a interface web abria, enquanto o envio de e-mail simplesmente não funcionava.
Neste guia, abro o capô de como solucionar problemas comuns de injeção de variáveis, tratamento de caracteres especiais nas senhas SMTP, configurações avançadas de segurança (hardening) e deploy com proxy reverso.
Arquitetura de deploy#
Antes de entrar nos problemas, é importante visualizar como os três componentes se relacionam em um deploy seguro:
Diagnóstico inicial#
O primeiro passo foi verificar se o serviço estava recebendo as variáveis SMTP. Executei:
docker exec -it vaultwarden env | grep SMTP
O resultado foi um retorno vazio. Isso mostrou claramente que o container não estava enxergando as variáveis de ambiente que eu acreditava ter definido.
1. Problema 1: Docker compose não leu o .env#
No meu caso, o docker-compose.yml estava na mesma pasta do .env, mas o Compose não importou as variáveis automaticamente devido a diferenças nas versões do utilitário e ambiente de execução.
O ajuste foi simples e definitivo: declarar env_file: .env no bloco services.vaultwarden. Sem isso, a configuração do Mail server dentro do container não era carregada.
2. Problema 2: senha SMTP com caracteres especiais#
Minha senha SMTP continha caracteres como (, £ e @. Sem proteção, o Linux e o parser do Docker podiam iniciar a expansão de variáveis ou interpretar caracteres especiais de maneira incorreta.
Para garantir que a credencial chegasse literal no Vaultwarden, usei aspas simples no .env:
SMTP_PASSWORD='sua_senha_smtp_aqui'
Isso é importante porque o .env do Docker é lido como texto plano, mas se a senha for passada para um shell intermediário ou um processo com parsing de string, caracteres especiais podem quebrar a autenticação.
3. Problema 3: porta exposta de forma insegura (0.0.0.0)#
A configuração comum de portas no formato - '16210:80' faz o bind por padrão na interface 0.0.0.0. Isso expõe a porta diretamente na internet pública, ignorando as tabelas de firewall do host em vários ambientes.
Como o Vaultwarden lida com gerenciamento de credenciais e senhas, expor a porta diretamente em HTTP puro é um risco crítico. A correção correta é forçar o bind no localhost (127.0.0.1) e utilizar um proxy reverso para tratar toda a comunicação sob HTTPS.
A configuração de hardening e produção#
1. .env de produção (/opt/vaultwarden/.env)#
Sempre proteja o arquivo .env com permissões restritivas (chmod 600 .env):
# Configurações de Identidade e Imagem
VERSION=latest
CONTAINER_NAME=vaultwarden
DOMAIN=https://vault.meudominio.com
# Recursos do Sistema (Deploy)
CPUS=0.5
MEMORY_LIMIT=512M
# Caminhos de Persistência
APP_PATH=/opt/vaultwarden
# Configuração SMTP
SMTP_HOST=smtp.provedor.com
[email protected]
SMTP_PORT=465
SMTP_SECURITY=force_tls # TLS Implícito obrigatório para porta 465
[email protected]
SMTP_PASSWORD='sua_senha_smtp_aqui'
# Geração Segura do ADMIN_TOKEN (Gerado via openssl rand -base64 48)
ADMIN_TOKEN=C8S2d3f4jK89FjK89FjK89FjK89FjK89FjK89FjK89FjK89...
# Parâmetros de Hardening e Segurança
SIGNUPS_ALLOWED=false # Bloqueia novos registros após criar sua conta
WEBSOCKET_ENABLED=true # Sincronização em tempo real nas extensões
EMERGENCY_ACCESS_ALLOWED=true # Acesso de emergência
2. docker-compose.yml seguro (sintaxe compose v2)#
Utilizamos a sintaxe padrão moderna do Docker Compose v2 (sem a chave obsoleta version):
services:
vaultwarden:
image: vaultwarden/server:${VERSION}
container_name: ${CONTAINER_NAME}
restart: unless-stopped
env_file: .env
deploy:
resources:
limits:
cpus: '${CPUS}'
memory: ${MEMORY_LIMIT}
ports:
- '127.0.0.1:16210:80' # Bind seguro no localhost
volumes:
- '${APP_PATH}/data:/data'
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80/alive"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
- vaultwarden_net
networks:
vaultwarden_net:
driver: bridge
HTTPS e configuração de proxy reverso (Nginx)#
Como a porta HTTP está vinculada apenas ao localhost, configuramos o Nginx para tratar as chaves SSL/TLS (Let's Encrypt) e repassar a conexão com segurança:
server {
listen 443 ssl http2;
server_name vault.meudominio.com;
ssl_certificate /etc/letsencrypt/live/vault.meudominio.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/vault.meudominio.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:16210;
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;
}
# Endpoint do WebSocket para notificações em tempo real (Push Sync)
location /notifications/hub {
proxy_pass http://127.0.0.1:16210;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Geração segura de token administrativo#
Não utilize chaves óbvias ou curtas para a variável ADMIN_TOKEN. Você pode gerá-las via terminal das seguintes formas:
- Token Aleatório Base64 (Recomendado):
# Execute este comando no HOST, não dentro do container.
# O objetivo é gerar o segredo no ambiente que controla o .env,
# nunca dentro de um processo contêinerizado que não tem acesso à entropia do host.
openssl rand -base64 48
- Hash Argon2 (Hardening Extremo):
Gere o hash a partir da imagem do Vaultwarden para ser colado no .env:
docker run --rm vaultwarden/server /vaultwarden/hash 'minha-senha-super-segura'
Nota: Em ambiente de produção estável, caso não use o painel admin, desative-o definindo ADMIN_TOKEN=disabled ou omitindo a variável.
Rotina de backup dos dados#
Os dados das credenciais residem no banco SQLite dentro do diretório /data.
sqlite3 .backup vs tar - consistência transacional#
O comando tar copia arquivos diretamente do disco. Se o Vaultwarden estiver em execução e com transações em andamento no momento do backup, o arquivo .tar.gz pode conter um banco em estado inconsistente - válido como arquivo, mas corrompido para o SQLite.
A abordagem corretamente transacional é usar sqlite3 .backup, que cria uma snapshot WAL-safe via API nativa do SQLite:
# Backup transacional (seguro mesmo com Vaultwarden rodando)
sqlite3 /opt/vaultwarden/data/db.sqlite3 ".backup '/opt/backups/vaultwarden-$(date +%Y%m%d).sqlite3'"
# Compressão pós-backup (agora o banco está em estado consistente garantido)
gzip /opt/backups/vaultwarden-$(date +%Y%m%d).sqlite3
O tar continua útil para fazer backup do diretório completo /data (incluindo attachments e outros arquivos), desde que o container esteja parado no momento da execução:
# Backup manual/automatizado do diretório de dados (com container parado)
tar czf /opt/backups/vaultwarden-$(date +%Y%m%d).tar.gz /opt/vaultwarden/data/
# Agendamento diário via Cron (executado às 02:00h da manhã)
# Adicione a linha abaixo executando 'crontab -e'
0 2 * * * tar czf /opt/backups/vaultwarden-$(date +\%Y\%m\%d).tar.gz /opt/vaultwarden/data/
Script de validação de instalação (validate-vaultwarden.sh)#
Para testar as dependências e o status da injeção de configurações em produção, você pode usar este script:
#!/bin/bash
# validate-vaultwarden.sh - Validação de integridade do container
set -euo pipefail
echo "=== Validação de Integridade do Vaultwarden ==="
# 1. Verificar se o container está rodando
if docker ps | grep -q "vaultwarden"; then
echo "✅ [STATUS]: Container está em execução."
else
echo "❌ [ERRO]: Container não está em execução." >&2
exit 1
fi
# 2. Verificar o carregamento das variáveis SMTP
SMTP_COUNT=$(docker exec vaultwarden env | grep -c "SMTP" || true)
if [ "$SMTP_COUNT" -gt 0 ]; then
echo "✅ [SMTP]: $SMTP_COUNT variáveis de e-mail foram injetadas com sucesso."
else
echo "❌ [ERRO]: Nenhuma variável SMTP encontrada no ambiente do container." >&2
fi
# 3. Testar a rota de status interno (Health Check)
if docker exec vaultwarden curl -sf "http://localhost:80/alive" >/dev/null 2>&1; then
echo "✅ [HEALTHCHECK]: A API do Vaultwarden está respondendo corretamente."
else
echo "❌ [ERRO]: Falha na comunicação com o healthcheck local." >&2
fi
# 4. Verificar arquivos de log por exceções
echo "[LOGS]: Últimos logs de erro/aviso:"
docker logs --tail 30 vaultwarden 2>&1 | grep -i "error\|warn\|fail" || echo " Nenhuma falha crítica registrada."
echo "=== Validação Concluída ==="
Como validei a correção#
- Reiniciei o ambiente com:
# Verifique a versão do Compose antes de qualquer operação:
# Requer Docker Compose v2.x (integrado ao Docker CLI como 'docker compose').
# Compose v1 (binário separado 'docker-compose') foi oficialmente descontinuado em julho de 2023.
docker compose version
docker compose down && docker compose up -d
- Confirmei a injeção dos valores no container:
docker exec -it vaultwarden env | grep SMTP
- Acompanhei os logs em tempo real ao disparar um e-mail de teste:
docker logs -f vaultwarden
O detalhe do TLS implícito#
Na prática operacional com Vaultwarden, a porta 465 exige SMTP_SECURITY=force_tls, não starttls. A porta 587 funciona com starttls, mas para meu ambiente de produção eu forcei TLS implícito, o que reduziu a superfície de falha do cliente SMTP.
Eu deixei claro no .env:
SMTP_PORT=465
SMTP_SECURITY=force_tls
Isso evita o erro clássico em que o serviço tenta negociar TLS em um canal que já deveria estar protegido desde o início.
Considerações práticas#
A lição prática é simples: no Docker, não considere o .env como magia invisível. Seja explícito no docker-compose.yml e trate senhas com caracteres especiais como strings literais. Além disso, garanta o bind de portas no localhost, use HTTPS via proxy reverso Nginx e aplique as diretivas de hardening para blindar as chaves de acesso.
Este artigo foi útil?
Deixe uma reação rápida para apoiar o conteúdo:
Este post está licenciado sob CC BY-NC.



Comentários
Participe da discussão abaixo.
0 comentários