03 de nov. de 2025·8 min de leitura

Erros de SSL somente em produção: corriga incompatibilidades de modo SSL

Erros de SSL que aparecem apenas em produção costumam vir de modos SSL incompatíveis ou certificados ausentes. Aprenda os erros comuns em strings de conexão e como testar localmente.

Erros de SSL somente em produção: corriga incompatibilidades de modo SSL

Por que erros de SSL no banco aparecem só em produção

Erros de SSL que surgem apenas em produção geralmente significam que seu app não está se conectando ao mesmo tipo de ambiente de banco que você tem localmente. O código pode ser idêntico, mas a produção muda as regras de rede e segurança.

No laptop, o Postgres costuma rodar no localhost, às vezes dentro do Docker, e conexões TCP simples são permitidas. Em produção, um serviço Postgres gerenciado geralmente fica atrás de um load balancer, proxy ou pooler de conexão, força criptografia e espera um modo SSL específico.

Quando um banco hospedado diz “SSL required”, ele está dizendo que recusará conexões não criptografadas. Alguns provedores vão além e exigem validação do certificado, não apenas a criptografia. É aí que configurações como sslmode=require vs sslmode=verify-full importam: require criptografa a conexão, enquanto verify-full também checa o certificado do servidor e confirma que o hostname bate.

Mesmo sem mudanças no código, o comportamento pode diferir porque configurações e padrões mudam. Seu .env local pode não incluir parâmetros de SSL, enquanto variáveis de ambiente em produção os incluem (ou sua plataforma injeta-os). Alguns drivers defaultam para “prefer” (tenta SSL e cai para não-SSL), o que pode funcionar silenciosamente localmente, mas falhar quando a produção exige verificação rigorosa.

Detalhes de rede também mudam. Conexões em produção frequentemente passam por proxy, PgBouncer ou um endpoint privado. Isso pode alterar o hostname para o qual você conecta e o certificado apresentado. Uma connection string que usa um endereço IP pode funcionar com require mas falhar com verify-full, porque certificados raramente correspondem a IPs puros.

Sintomas típicos incluem erros como:

  • “SSL is required” ou “no pg_hba.conf entry for host ... SSL off”
  • “certificate verify failed”, “unable to get local issuer certificate”, ou “self signed certificate”
  • “hostname mismatch” ou “certificate does not match host”
  • Funciona localmente e em preview, mas falha ao ser implantado no banco de produção
  • Falhas intermitentes quando um pooler ou proxy está no caminho

Um exemplo realista: localmente você conecta a postgres://localhost:5432/app sem configurações SSL. Em produção, seu banco gerenciado fornece uma URL como postgres://user:[email protected]:5432/app?sslmode=verify-full. Se seu app descarta o parâmetro (ou usa sslmode=disable), a produção rejeita a conexão mesmo que as mesmas queries funcionassem em casa.

Primeiros 10 minutos: obtenha o erro exato e o contexto

O caminho mais rápido para corrigir é parar de adivinhar e capturar a falha exata. Pequenos detalhes na mensagem, no host e em onde o código roda costumam apontar direto para a incompatibilidade.

Copie o texto completo do erro, incluindo linhas “caused by”, e anote a janela de tempo exata. Se você tem logs centralizados, filtre ao redor desse timestamp para ver o que ocorreu imediatamente antes da falha.

Em seguida, anote onde a conexão é aberta. Uma conexão criada durante uma requisição web pode se comportar diferente de uma criada em um worker em background, função serverless ou job agendado. Verifique também se falha imediatamente ou só depois de um período. “Funciona por um tempo e depois morre” frequentemente indica pooling, timeouts ou algo rotacionando (certificados, rotas ou endpoints).

Capture estes básicos antes de mudar qualquer coisa:

  • Texto completo do erro e stack trace (o primeiro erro, não o último retry)
  • Janela de tempo e ID da requisição ou job (se disponível)
  • Local de execução (servidor web, serverless, cron, worker de fila)
  • Se falha imediatamente ou após algumas queries bem-sucedidas
  • O host do banco ao qual você está realmente se conectando

Esse último ponto importa muito. Muitos apps têm múltiplas URLs de banco espalhadas (vars de ambiente, gerenciadores de segredos, defaults hardcoded, ambientes de preview). Logue o host resolvido em runtime (sem logar credenciais). Se a produção estiver se conectando a um host diferente do que você imagina, os requisitos de SSL e os certificados também podem ser diferentes.

Também verifique se falha em todas as regiões de produção ou apenas em uma. Uma falha regional pode significar um endpoint diferente, um caminho de rede diferente ou uma cadeia de certificados distinta.

Modos de SSL em linguagem simples (e por que importam)

A maioria das falhas de SSL que aparecem só em produção se resume a um desacordo sobre quão estrita a conexão deve ser. Bancos locais costumam permitir não-SSL ou SSL “melhor esforço”. Postgres gerenciado normalmente exige SSL.

SSL faz duas coisas diferentes, e ajuda separar:

  • Criptografia: mantém o tráfego privado para que terceiros não leiam senhas ou dados em trânsito.
  • Verificação de identidade: prova que você está conversando com o servidor de banco de dados real.

O “modo SSL” controla quão rigoroso você é sobre essas duas funções.

Modos de SSL comuns

Entre drivers e ferramentas do Postgres, você geralmente encontra:

  • disable: nunca usa SSL
  • prefer: tenta SSL primeiro, mas cai para não-SSL se o SSL falhar
  • require: sempre usa criptografia SSL, mas não prova estritamente a identidade do servidor
  • verify-ca: usa SSL e confirma que o certificado tem cadeia até uma CA confiável
  • verify-full: usa SSL, valida a CA e verifica que o hostname bate com o certificado

Se você define um modo estrito como verify-full sem a cadeia de certificados ou hostname corretos, ainda assim pode obter erros de SSL mesmo que “esteja criptografado”.

Validação de hostname: a quebra típica em produção

Validação de hostname significa que o certificado do servidor deve corresponder ao host ao qual você se conecta. Se sua connection string usa um endereço interno, um IP ou um DNS diferente daquele para o qual o certificado foi emitido, verify-full falha. Isso acontece frequentemente quando a produção roteia seu tráfego por um proxy ou endpoint privado.

Defaults diferentes por driver (e isso importa)

Os defaults variam por driver. Um pode se comportar como prefer por padrão, outro como require, e seu provedor de nuvem pode rejeitar não-SSL sem ressalvas.

Antes de mudar qualquer coisa, responda:

  • A produção exige SSL ou permite não-SSL?
  • Você está usando require ou verify-full?
  • Se usar verify-full, o host na connection string bate com o certificado?
  • De onde vem o certificado CA em produção (arquivo, variável de ambiente, store de confiança do sistema)?
  • Seu setup local está realmente testando o mesmo modo, ou está caindo silenciosamente para outro modo?

Erros comuns na connection string que causam falhas

Problemas de SSL em produção geralmente são pequenas diferenças entre o que seu banco espera e o que seu app realmente envia.

1) Nome de parâmetro errado para seu driver

Drivers diferentes leem configurações SSL diferentes. Alguns olham sslmode, outros leem ssl=true, e alguns esperam um objeto em vez de uma string. Se você usa o nome errado, o driver pode ignorar a opção e cair em um default.

Isso é especialmente comum em projetos gerados por IA porque o código mistura exemplos de ecossistemas distintos. Você pode ver sslMode em um lugar e sslmode em outro. Um é respeitado, o outro é ignorado.

2) Escolher a rigidez errada (require vs verify-full)

Dois padrões de falha aparecem com frequência:

  • Você configura sslmode=require, mas o provedor espera validação de certificado (verify-ca ou verify-full).
  • Você configura sslmode=verify-full, mas o nome do host ou a cadeia de certificados não batem, então a produção falha enquanto o local parecia tolerar configurações mais fracas.

3) CA ausente ao usar verify-ca ou verify-full

Se você verifica certificados, geralmente precisa de um certificado CA (via sslrootcert ou uma opção específica do driver). Sem ele, erros aparecem como “self signed certificate”, “unable to get local issuer certificate” ou “certificate verify failed”.

Um exemplo simples do que as pessoas costumam querer (nomes variam por driver):

... sslmode=verify-full sslrootcert=/path/to/ca.pem

4) Usar um endereço IP em vez de hostname

verify-full verifica que o certificado corresponde ao hostname. Se sua URL usa um IP como 10.0.0.12, mas o certificado foi emitido para algo como db.myprovider.com, a verificação falha.

5) Variáveis de ambiente sobrescrevendo o que você acha que configurou

É comum mudar o código, redeployar e ainda falhar porque a plataforma injeta DATABASE_URL (ou outra variável) que sobrescreve suas novas configurações. O app continua usando a URL antiga.

6) Copiar/colar a URL e quebrar a senha

Senhas com caracteres especiais (@, :, #, %) precisam ser codificadas na URL. Copiar/colar via dashboards, ferramentas de chat ou arquivos .env pode corromper a senha e gerar erros de autenticação que parecem problemas de SSL.

Se quiser checagens rápidas antes de se aprofundar:

  • Confirme que seu driver lê a opção de SSL que você usou (nome e casing).
  • Faça sslmode igual ao que a produção exige, não ao que o local tolera.
  • Se verificar, garanta que o CA esteja presente e acessível em runtime.
  • Use um hostname (não um IP) quando usar verify-full.
  • Verifique se nenhuma variável de ambiente está sobrescrevendo a connection string pretendida.

Passo a passo: construa as configurações de conexão de produção corretamente

Reconstruir em vez de corrigir
Se a base de código estiver muito embaralhada, podemos reconstruir limpo em cerca de 24 horas.

A correção começa por tornar as configurações finais, efetivas, óbvias e intencionais.

1) Liste todos os lugares de onde a config pode vir

Antes de mudar qualquer coisa, liste todas as fontes que podem influenciar a conexão: variáveis de ambiente em runtime, segredos da plataforma, arquivos de configuração do app, injeção em tempo de build e configurações do ORM.

Depois confirme qual fonte vence se a mesma configuração aparecer em dois lugares. Muitos bugs de “modo SSL” são na verdade “você editou o lugar errado”.

2) Crie uma fonte única de verdade para a connection string

Escolha uma representação canônica para produção, normalmente uma única DATABASE_URL. Remova outros ajustes ou derive-os estritamente dessa URL.

Uma boa URL de produção declara explicitamente a intenção de SSL:

postgres://USER:PASSWORD@HOST:5432/DBNAME?sslmode=verify-full

Se seu provedor exige criptografia mas não verificação estrita de identidade, use sslmode=require. Se exige verificação completa, use verify-full e configure certificados e checagens de hostname.

3) Decida o modo SSL baseado nos requisitos do provedor

Não adivinhe. Escolha um modo com base no que o provedor exige e anote o porquê (um comentário curto perto da variável é suficiente). Isso evita divergência futura.

4) Adicione CA e o nome de servidor esperado quando necessário

Para verify-ca e verify-full, normalmente você precisa que o certificado CA do provedor esteja disponível em runtime (como um caminho de arquivo ou passado diretamente, dependendo do driver).

Para verify-full, o nome do servidor deve corresponder ao certificado. Se você conectar por IP ou por um hostname alias, pode ter mismatch mesmo com credenciais corretas.

5) Logue uma versão segura e redigida da configuração final

Logue o que o app vai usar depois de todas as sobrescritas, mas nunca logue senhas, tokens ou conteúdo completo de certificados.

DB host=prod-db.example.com port=5432 db=app sslmode=verify-full sslrootcert=set user=app_user password=REDACTED

Essa única linha na inicialização muitas vezes basta para detectar um host errado, um caminho de CA ausente ou um modo SSL inesperado.

Passo a passo: teste o mesmo modo de SSL localmente

O objetivo é fazer seu laptop se comportar como a produção. Se a produção exige SSL e seu setup local conecta silenciosamente sem SSL, você vai enviar uma falha.

1) Igualar as entradas de produção, não apenas os valores

Comece rodando localmente com as mesmas entradas que a produção usa: a URL completa do banco, o modo SSL e quaisquer caminhos de certificado. Evite misturar “defaults locais” com “valores prod” na mesma execução.

Um padrão prático é um arquivo local dedicado, por exemplo .env.prodlike, e rodar o app apenas com esse arquivo carregado.

# Example (names vary by framework/driver)
DATABASE_URL=postgres://user:[email protected]:5432/appdb?sslmode=verify-full
PGSSLMODE=verify-full
PGSSLROOTCERT=./certs/prod-ca.pem

2) Traga a mesma cadeia de CA usada em produção

Exporte ou baixe o bundle de CA usado em produção e armazene localmente (por exemplo ./certs/prod-ca.pem). Aponte seu driver para esse arquivo. Sem ele, verify-ca e verify-full vão falhar mesmo que todo o resto esteja correto.

3) Force o modo SSL explícito e impeça “auto fallback”

Algumas bibliotecas tentam múltiplas opções de conexão ou caem para não-SSL quando SSL falha. Isso esconde o problema real. Torne o modo SSL explícito e observe logs que indiquem retries com diferentes configurações TLS. Se vir “retrying without TLS/SSL”, trate isso como um teste falho.

4) Faça o hostname bater com o que verify-full espera

Se você conecta localmente usando um IP, localhost ou um DNS diferente do que a produção usa, verify-full pode falhar mesmo com a CA correta.

Use o mesmo hostname DNS que a produção usa na connection string. Se precisar mapear localmente, ajuste hosts/DNS para manter o mesmo hostname.

5) Cheque fora do seu app

Antes de culpar seu código, teste a conexão com um cliente simples usando as mesmas configurações. Se isso falhar, seu app também vai falhar.

Prove a correção: testes rápidos antes de redeployar

Obtenha uma auditoria gratuita de código
Comece com uma auditoria gratuita de código para ver todos os problemas antes de se comprometer com a correção.

Depois de mudar as configurações de SSL, prove que a conexão funciona antes de enviar. Isole o problema do código do app para não ficar adivinhando se a mudança resolveu algo.

Comece testando conectividade fora do app, usando o mesmo host, porta, usuário e nome de banco. Um CLI simples ou um script pequeno remove o framework/ORM da equação. Se a conexão direta falhar, o foco é configuração SSL, certificados, DNS ou rede.

Uma rápida bateria de provas:

  • Conecte do mesmo ambiente que a produção (mesma imagem de container ou mesma VM) usando um cliente mínimo.
  • Imprima as configurações finais que o app usará em runtime (sanitizadas) e confirme que o sslmode é o esperado.
  • Confirme que o runtime consegue ler o arquivo de CA configurado (o arquivo existe e tem permissões corretas).
  • Compare versões do driver entre local e produção (defaults mudam entre versões).
  • Se usar containers, verifique se a imagem inclui bundles de CA do sistema (issue comum em imagens mínimas).

Depois de obter uma conexão bem-sucedida, quebre de propósito para confirmar o que está sendo validado:

  • Altere o hostname para algo errado e confirme que o erro vira DNS ou conexão.
  • Mude para um modo mais estrito (verify-full) sem a CA correta e confirme que há falhas de validação de certificado.
  • Aponte o caminho de CA para um arquivo ausente e confirme que dá erro de arquivo/permissão.

Se essas mudanças não afetarem o erro, talvez você não esteja exercitando o mesmo caminho de código da produção. Isso frequentemente significa que as configurações de SSL estão sendo ignoradas ou sobrescritas.

Cenário exemplo: um banco gerenciado exige SSL em produção

Um padrão comum: tudo funciona no laptop, você faz deploy e o app não consegue se conectar. Logs mostram “SSL handshake failed”, “certificate verify failed” ou “server does not support SSL, but SSL was required”.

Normalmente, o que acontece com um serviço Postgres gerenciado é: localmente você roda Postgres sem SSL, ou seu cliente local tolera configurações fracas. Em produção, o banco gerenciado exige SSL e seu app roda num container que não tem os certificados CA corretos.

A causa raiz costuma ser um desalinhamento entre três coisas: o hostname que você usa para conectar, o modo SSL configurado e se o runtime consegue verificar a cadeia de certificados.

Por exemplo, você pode fazer deploy com:

DATABASE_URL=postgres://app:[email protected]:5432/prod?sslmode=verify-full

Isso pode falhar mesmo com credenciais corretas. verify-full exige que o certificado seja confiável e que ele corresponda ao hostname. Um endereço IP (ou um alias) frequentemente não bate com o DNS do certificado.

Uma correção mínima é conectar usando o hostname que o certificado espera e garantir que o runtime consiga validá-lo:

DATABASE_URL=postgres://app:[email protected]:5432/prod?sslmode=verify-full\u0026sslrootcert=/etc/ssl/certs/ca-certificates.crt

Se você não consegue fornecer um caminho para o bundle de CA (ou sua imagem não o inclui), um workaround temporário é mudar para sslmode=require para que o tráfego seja criptografado sem verificação estrita. Isso pode desbloquear a situação, mas é menos seguro que a verificação completa.

Para evitar repetição:

  • Use o mesmo formato de hostname localmente que usará em produção (nome DNS, não IP).
  • Escolha o modo SSL de forma intencional e documente o motivo.
  • Garanta que sua imagem de runtime inclua certificados CA e saiba onde o bundle está.
  • Adicione um smoke test que rode dentro da mesma imagem de container que você deploya.

Armadilhas comuns em apps gerados por IA (Lovable, Bolt, v0, Cursor, Replit)

Resgate um app gerado por IA
Se Lovable, Bolt, v0, Cursor ou Replit geraram seu código, tornamos ele pronto para produção.

Apps gerados por ferramentas como Lovable, Bolt, v0, Cursor e Replit frequentemente falham de um jeito específico: as configurações de banco parecem plausíveis, mas foram chutadas. Por isso erros de SSL aparecem depois do deploy.

Um padrão é um campo de connection string inventado que seu driver não usa. Um app pode definir ssl=true ou tls=true e assumir que isso força SSL, enquanto o driver só respeita sslmode=require (ou um ssl estruturado). Localmente, seu banco aceita TCP simples, então você não percebe.

Outro padrão é sobrescritas ocultas. Esses projetos costumam definir config de banco em vários lugares; você conserta só um. Localmente, um arquivo .env vence. Em produção, a plataforma usa um nome de variável diferente, injeta seu próprio valor ou um default de build prevalece.

O refactor mais simples que previne quedas repetidas é chato, porém eficaz: escolha uma fonte de verdade (geralmente DATABASE_URL), faça o parse uma vez e passe as mesmas configurações para todo lugar que tocar o banco (runtime, migrations, jobs em background).

Checklist rápido e próximos passos

Quando um erro de SSL aparece só em produção, raramente é aleatório. Normalmente é uma pequena incompatibilidade entre o que seu app pede e o que o banco espera.

Checklist:

  • Confirme host e porta correspondem ao endpoint do banco gerenciado (sem host dev antigo, sem porta faltando).
  • Confirme o modo SSL pretendido está definido (por exemplo, require vs verify-full) e que seu driver realmente o lê.
  • Se você verifica certificados, verifique se o CA está presente no runtime (VM, container, serverless) e se o caminho está correto.
  • Verifique que o hostname na connection string corresponde ao hostname do certificado (falha comum com verify-full).
  • Garanta que exista uma única fonte de verdade para a config, não múltiplas sobrescritas espalhadas pelo código e dashboards.

Se você continuar ouvindo “works on my machine”, compare versões de driver e stores de confiança. Uma imagem de container pode não incluir o bundle de CA, ou a produção pode rodar uma biblioteca cliente diferente com defaults distintos.

Um passo leve é um preflight de startup que falhe rápido com mensagem clara:

Preflight idea: on startup, connect with the same connection string.
If it fails, log: sslmode, host, and whether a CA file was found.
Exit so the deploy fails early instead of timing out later.

Se você herdou um protótipo gerado por IA e a lógica de SSL/config está espalhada, FixMyMess (fixmymess.ai) foca em diagnosticar e reparar essas quebras só em produção. Uma auditoria rápida pode identificar connection strings conflitantes, manuseio de CA ausente e defaults inseguros para que o app se comporte de forma previsível em produção.

Perguntas Frequentes

Por que erros de SSL aparecem só depois que eu faço deploy para produção?

Geralmente significa que, em produção, você está se conectando através de um endpoint gerenciado que exige SSL e possivelmente validação de certificado, enquanto seu banco local aceita TCP simples ou faz fallback silencioso para não-SSL. O código pode ser igual, mas os padrões do ambiente e o caminho de rede são diferentes.

Qual a diferença entre sslmode=require e sslmode=verify-full?

require criptografa a conexão, mas não verifica estritamente a identidade do servidor. verify-full criptografa a conexão e também checa a cadeia de certificados e se o hostname que você usa bate com o certificado, o que costuma falhar se você usar um IP, um hostname de proxy ou um nome DNS incorreto.

Por que dá “hostname mismatch” quando uso um endereço IP?

Porque verify-full espera que o certificado corresponda a um hostname real, e certificados raramente são emitidos para endereços IP puros. Quando precisar de verificação estrita, use o hostname DNS que o provedor emitiu para o certificado, não um IP privado ou um alias interno.

O que significa “unable to get local issuer certificate” em produção?

Normalmente significa que o runtime não consegue verificar a cadeia de certificados. Causas comuns: CAs ausentes na imagem/container/VM, caminho errado em sslrootcert, ou uma imagem base mínima sem o bundle de CA do sistema.

O que devo registrar para diagnosticar isso sem vazar segredos?

No startup, registre o host resolvido do DB, a porta, o nome do banco e o sslmode, com credenciais redigidas. Também capture o primeiro erro e qualquer linha “caused by”, porque tentativas e retries podem esconder a falha original.

Por que ainda falha depois que eu “ativei SSL” na minha configuração?

Frequentemente é porque o driver está ignorando sua configuração de SSL por causa de um nome de opção incorreto ou casing errado, ou porque uma variável de ambiente como DATABASE_URL está sobrescrevendo o que você mudou no código. Outro motivo comum é um proxy/pooler em produção apresentando um certificado diferente do esperado.

Como faço para que meu ambiente local se comporte como produção em relação ao SSL?

Execute localmente usando exatamente a mesma connection string de produção e o mesmo modo de SSL, incluindo os paths de certificados. O objetivo é evitar que comportamentos como prefer ou fallback escondam problemas até o deploy.

Isso pode ser causado pelo meu DATABASE_URL ou por um erro de copiar/colar?

Primeiro confirme qual DATABASE_URL o app realmente usa em runtime, já que plataformas frequentemente injetam ou sobrescrevem esse valor. Depois verifique se o driver lê o parâmetro que você está configurando e se caracteres especiais na senha estão codificados na URL, pois URLs malformadas podem parecer erros de SSL.

PgBouncer ou um proxy podem causar falhas intermitentes de SSL?

Sim. Se a produção roteia tráfego por PgBouncer, um load balancer ou um endpoint privado, o hostname e o certificado apresentados podem ser diferentes do que você testou. Isso pode causar falhas intermitentes de handshake ou erros de verificação se o pooler rotacionar endpoints ou usar uma cadeia de certificados distinta.

Quando devo pedir para a FixMyMess intervir?

Se o projeto for gerado por IA ou a configuração estiver espalhada em muitos lugares, o caminho mais rápido é uma auditoria focada para descobrir a configuração efetiva de conexão e corrigir as incompatibilidades. FixMyMess pode rodar uma auditoria gratuita de código e então reparar a lógica de conexão, o manuseio de SSL e defaults inseguros; a maioria das correções é feita em 48–72 horas com opção de reconstrução limpa em ~24 horas quando necessário.