Desvendando o vaultwarden: como configurar SMTP e Docker sem erros
Voltar para blog

Desvendando o vaultwarden: como configurar SMTP e Docker sem erros

07/06/2026 · 4 min · Infraestrutura

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:

flowchart LR INET(["🌐 Internet\nHTTPS :443"]) NGX["Nginx\nProxy Reverso\n+ TLS (Let's Encrypt)"] VW["Vaultwarden\n127.0.0.1:16210\n(HTTP interno)"] SMTP["Servidor SMTP\n:465 (force_tls)"] INET -->|"HTTPS"| NGX NGX -->|"HTTP local\nproxy_pass"| VW VW -->|"SMTP TLS Implícito\nnotificações + 2FA"| SMTP style INET fill:#1e3a5f,color:#93c5fd style NGX fill:#14532d,color:#86efac style VW fill:#0e2a3a,stroke:#4fd8ff,color:#4fd8ff style SMTP fill:#78350f,color:#fde68a

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:

  1. 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
  1. 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#

  1. 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
  1. Confirmei a injeção dos valores no container:
   docker exec -it vaultwarden env | grep SMTP
  1. 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:

CC BY-NC

Este post está licenciado sob CC BY-NC.

Comentários

Participe da discussão abaixo.

0 comentários