Introdução: “ontem funcionava, hoje o certificado é inválido”

Existe uma classe de incidentes que consegue se disfarçar melhor do que qualquer outra. Um script que durante meses passou por um proxy para dezenas de APIs de repente começa a falhar em todas as requisições. No log aparece certificate has expired ou certificate is not yet valid. O engenheiro abre o certificado do site de destino no navegador do próprio notebook: prazo de validade em ordem, cadeia de confiança verde. Ele reinicia o script sem proxy no notebook — funciona. Volta o proxy — falha de novo. A conclusão vem sozinha: a culpa é do proxy.

E essa conclusão quase sempre está errada. Não é que o proxy “substitui o certificado” ou “quebra o TLS”. O que acontece é que o script roda em outra máquina — em um servidor, em uma VM, em um contêiner — e nessa máquina o relógio do sistema saiu do lugar. Por alguns minutos, horas ou dias. O proxy só acabou sendo a variável mais visível da equação e levou a culpa.

Vamos destrinchar esse assunto por inteiro. Você vai entender em quais pontos exatos os protocolos modernos dependem da hora atual e por que um deslocamento de relógio em direções diferentes gera sintomas diferentes. Vai ver as mensagens de erro concretas no curl, Python e Node.js para reconhecer o problema logo na primeira linha do trace. Vai entender por que os relógios saem do lugar justamente em máquinas virtuais e contêineres e por que ter um daemon de sincronização instalado não garante que ele esteja funcionando. Vai receber um procedimento de diagnóstico de um minuto, configurações que funcionam de chrony e systemd-timesyncd, e um checklist para percorrer antes de escrever para o suporte do provedor de proxy.

Deixando claro os limites desde já. Este artigo não é sobre emissão de certificados, cadeias de confiança ou repositórios de raízes. Tratamos de uma única causa de falhas — hora do sistema errada — e de tudo o que dela decorre ao trabalhar via proxy. É um tema estreito, mas é justamente ele que queima horas de depuração em times que fazem integrações, monitoramento, coleta de dados abertos e automação através de infraestrutura de proxy como a Proxeon.

Fundamentos: onde o tempo é crítico e por que os protocolos olham para o relógio

Comecemos por uma pergunta simples. Para que os protocolos de rede precisam saber que horas são? A resposta é uma só: para limitar o tempo de vida da confiança. Qualquer segredo — certificado, token, assinatura, código de uso único — mais cedo ou mais tarde pode vazar. Se ele tem prazo de validade, o dano de um vazamento fica limitado. Mas a limitação do prazo só funciona quando a parte que verifica sabe a hora atual. E ela pega essa hora em um único lugar: no relógio do sistema da máquina onde a verificação acontece.

Daí vem uma consequência fundamental: cada verificação de tempo é local. O servidor não pergunta ao cliente que horas são. O cliente não pergunta ao servidor. Cada um olha para o próprio relógio e tira a própria conclusão. Se os relógios das duas partes divergem, elas enxergam realidades diferentes, e uma delas considera a outra infratora.

Verificação do prazo de validade do certificado no TLS

Todo certificado X.509 contém dois campos: notBefore (válido a partir de) e notAfter (válido até). No handshake TLS, o cliente compara a hora atual do próprio relógio com esse intervalo. O momento em que o certificado foi emitido pelo servidor não importa aqui — o que conta é apenas o que o cliente pensa sobre a hora atual.

A direção do deslocamento determina o sintoma. Se o relógio do cliente está atrasado, certificados recém-emitidos parecem ainda não ter sido emitidos: erro “certificate is not yet valid”. Se o relógio do cliente está adiantado, os certificados parecem vencidos: “certificate has expired”. Esse é o primeiro sinal de diagnóstico que vale memorizar.

Por que o tema se agravou agora? Porque os certificados estão ficando cada vez mais curtos. Muitas autoridades certificadoras públicas emitem certificados de 90 dias, já existem versões de 6 dias, e os requisitos do setor vêm reduzindo o prazo máximo de forma planejada: a partir de março de 2026, 200 dias, depois 100 dias e, até 2029, 47 dias. Antes, um relógio atrasado duas semanas quase nunca caía num certificado recém-emitido. Agora o site de destino pode reemitir o certificado a cada poucos dias, e uma máquina atrasada três dias vai rejeitá-lo como “ainda não válido”.

No mesmo grupo entram mecanismos associados: respostas OCSP e listas de revogação têm janelas próprias de validade, selos de Certificate Transparency contêm carimbos de tempo e tickets de sessão do TLS 1.3 duram tempo limitado. Tudo isso é verificado pelo relógio local.

JWT: os campos exp, nbf e iat

O JSON Web Token é o formato de token de acesso mais comum em APIs. No seu corpo existem três campos temporais no formato Unix epoch: iat (quando foi emitido), nbf (not before — a partir de quando é válido) e exp (quando expira). A parte que recebe o token compara isso com o próprio relógio, normalmente com uma pequena tolerância (leeway) de 30 a 60 segundos.

Surge aí uma assimetria interessante. Em geral o servidor emite o token pelo relógio dele, e o cliente o armazena em cache até o exp chegar — mas já pelo próprio relógio. Se o cliente está atrasado cinco minutos, ele vai considerar o token vivo mais cinco minutos depois de o servidor começar a rejeitá-lo. Você recebe 401 inexplicáveis no fim da vida de cada token. Já se o próprio cliente assina o JWT — por exemplo, para autenticação de conta de serviço — e seu relógio está adiantado, o servidor vê o iat no futuro e rejeita o token como ainda não válido.

Assinaturas de requisição com marcação de tempo

Muitas APIs exigem assinar a requisição com HMAC, incluindo a hora atual na string assinada. O exemplo mais conhecido é o esquema Signature Version 4, usado por storages de objetos compatíveis com S3. O servidor aceita a requisição se a marcação de tempo dela diferir do relógio do servidor em no máximo 15 minutos; caso contrário, devolve o erro RequestTimeTooSkewed. Gateways de pagamento, APIs de bolsas e corretoras, barramentos corporativos de dados usam esquemas parecidos com janelas de 30 segundos a alguns minutos. A autenticação Kerberos em redes corporativas tolera, por padrão, uma divergência de 5 minutos.

Repare: a assinatura é formada pelo seu cliente de acordo com o relógio dele. O proxy não participa desse processo — fisicamente não pode, ele apenas repassa bytes. Por isso, o erro “marcação de tempo inválida na assinatura” via proxy é sempre uma questão do relógio da sua máquina.

Códigos de uso único TOTP

Os códigos de autenticação de dois fatores pelo padrão TOTP são calculados a partir de um segredo compartilhado e do número do intervalo de 30 segundos. A parte que verifica costuma aceitar mais ou menos um intervalo. Se o seu relógio saiu do lugar em um minuto, os códigos deixam de ser aceitos por completo. Isso vale também para integrações próprias, em que uma conta de serviço gera TOTP por software para entrar em um portal corporativo.

Consumidores de tempo menos óbvios

  • Cache HTTP. Os cabeçalhos Date, Expires e max-age são comparados com o relógio local; com deslocamento, o cache ou não funciona ou entrega conteúdo desatualizado.
  • Cookies com Expires. O cliente com relógio atrasado os guarda por mais tempo; com relógio adiantado, descarta imediatamente.
  • HSTS e pinning. As diretivas max-age também são contadas localmente.
  • Rotação de certificados dentro de clusters. Componentes de orquestradores reemitem certificados por cronograma e, com deslocamento de relógio no nó, deixam de confiar uns nos outros.

Aprofundamento: de quem é o relógio que importa na cadeia cliente — proxy — servidor

Agora sobreponha esse cenário à topologia real. Ao trabalhar via proxy, há no mínimo três máquinas com três conjuntos de relógio: seu cliente, o servidor proxy e o servidor de destino. A pergunta-chave de diagnóstico é: em qual delas acontece a verificação que está falhando?

HTTP CONNECT e SOCKS5: o proxy não vê o TLS

Ao fazer uma requisição a um recurso HTTPS via proxy HTTP, o cliente envia o comando CONNECT com o nome do host e a porta. O proxy estabelece uma conexão TCP com o destino e daí em diante repassa bytes cegamente nos dois sentidos. O handshake TLS acontece através do proxy, entre seu cliente e o servidor de destino. O proxy não vê o certificado, não pode alterá-lo e não participa da verificação. O mesmo vale para SOCKS5.

Conclusão prática: o erro “certificate is not yet valid” ou “certificate has expired” ao trabalhar por um túnel transparente não pode, fisicamente, ser causado pelo proxy. A origem é ou o relógio da sua máquina, ou o repositório de raízes confiáveis dela, ou um certificado realmente vencido no destino. As duas primeiras hipóteses são locais. E a primeira delas se verifica em dez segundos.

Quando mais relógios são verificados: TLS até o próprio proxy

Se você se conecta ao proxy por canal protegido (esquema https:// para o próprio proxy), o cliente verifica dois certificados: o do servidor proxy e o do site de destino. As duas verificações acontecem pelo relógio do cliente. A sintomatologia é a mesma, mas na saída detalhada do curl vai dar para ver em qual etapa — “Proxy certificate” ou “Server certificate” — a falha ocorreu.

O que depende do relógio do próprio proxy

O relógio do servidor proxy influencia um conjunto limitado de coisas: a correção dos próprios logs dele, o funcionamento do listener TLS dele (por exemplo, obter respostas OCSP) e a validação de credenciais de autorização, caso o provedor use tokens com prazo limitado. A infraestrutura da Proxeon sincroniza o tempo nos próprios nós normalmente, e na esmagadora maioria dos incidentes o relógio do proxy não tem nada a ver: ele não participa nem da verificação do certificado de destino, nem da formação das suas assinaturas, nem da validação dos seus JWTs. Tudo isso é feito pelo cliente e pelo servidor de destino.

O relógio do servidor de destino

De vez em quando o relógio sai do lugar do outro lado. O sinal é: os erros aparecem só com um host, e o acesso aos demais está normal; o cabeçalho Date na resposta desse host difere bastante do Date de outros servidores. Você não pode fazer nada quanto a isso, além de avisar o dono e, se a API permitir, compensar o deslocamento nas suas requisições — algumas bibliotecas conseguem calcular a correção pelo cabeçalho Date da resposta de erro e assinar a próxima requisição já considerando o desvio.

Latência e “expiração na fila”

O proxy adiciona atraso — de dezenas de milissegundos a segundos em reconexões de canais móveis e rotação de IP. Para uma janela de 15 minutos, isso é insignificante. Mas há uma armadilha sutil na lógica de retentativas. Se você assinou a requisição, colocou na fila e só enviou vários minutos depois, após uma série de retries com espera, a marcação de tempo da assinatura fica desatualizada. Em canal lento ou sobrecarregado, isso acontece com mais frequência. A regra é: assine imediatamente antes de enviar e reassine a cada tentativa, em vez de reaproveitar um objeto já assinado.

Deslocamento: magnitude e direção

Vamos reunir a dependência do sintoma em relação ao deslocamento numa lista-tabela — ela vai ser útil na leitura dos logs:

  • Relógio do cliente atrasado: “certificate is not yet valid”, JWT com erro de nbf ou iat em autoassinatura, tokens em cache local parecem vivos mas o servidor responde 401 “expired”, assinaturas rejeitadas como antigas demais, TOTP não aceito.
  • Relógio do cliente adiantado: “certificate has expired”, JWTs próprios rejeitados com iat no futuro, assinaturas rejeitadas como “do futuro”, tokens descartados do cache antes do prazo, TOTP não aceito.
  • Deslocamento menor que um segundo: nenhum dos mecanismos acima nota.
  • Deslocamento de 30 segundos a 5 minutos: quebram TOTP, JWT com leeway pequeno e esquemas HMAC estritos.
  • Deslocamento acima de 15 minutos: quebram praticamente todas as assinaturas de requisição e o Kerberos.
  • Deslocamento em dias: começam a falhar conexões TLS com sites de certificados curtos.

Como o erro aparece em diferentes clientes: textos que você precisa reconhecer na primeira linha

Saber reconhecer o problema pelo texto do erro economiza mais tempo do que qualquer metodologia. Abaixo estão formulações típicas de clientes populares. Elas aparecem como as bibliotecas com OpenSSL por baixo imprimem; os números de linha entre parênteses variam de versão para versão.

curl

Mensagens básicas com relógio errado:

curl: (60) SSL certificate problem: certificate is not yet valid
curl: (60) SSL certificate problem: certificate has expired

O código 60 é CURLE_PEER_FAILED_VERIFICATION, comum a todos os problemas de verificação de certificado. Só dá para distinguir causa temporal de problema de raízes pelo texto depois dos dois pontos. Com a flag -v surge uma linha útil com o código numérico de verificação do OpenSSL: 9 corresponde a “not yet valid”, 10 a “has expired”:

* SSL certificate verify result: certificate is not yet valid (9), continuing anyway.

A linha “continuing anyway” só aparece com a flag -k; sem ela, a conexão é encerrada. No Windows com backend Schannel, o texto é outro:

curl: (60) schannel: SEC_E_CERT_EXPIRED (0x80090328) - The received certificate has expired.

Exemplo de verificação completa via proxy com saída detalhada, para ver tanto a etapa (proxy ou destino) quanto o resultado da verificação:

curl -v --proxy http://LOGIN:PASSWORD@PROXY_HOST:PORT https://example.com/ 2>&1 | grep -iE 'verify|expire|valid|date'

Python: ssl, requests, httpx, aiohttp

O módulo padrão ssl lança uma exceção com sufixo bem descritivo:

ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate is not yet valid (_ssl.c:1010)

A biblioteca requests o envolve em SSLError e acrescenta o wrapper do pool de conexões, fazendo as palavras-chave ficarem no final de uma string longa:

requests.exceptions.SSLError: HTTPSConnectionPool(host='example.com', port=443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1010)')))

Detalhe importante ao passar por proxy: em um túnel CONNECT, o erro de verificação do certificado de destino chega exatamente como SSLError, e não como ProxyError. ProxyError aparece quando não se conseguiu estabelecer conexão com o próprio proxy. O tipo da exceção já indica onde procurar.

O httpx formula mais curto:

httpx.ConnectError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1010)

O aiohttp, com classe própria:

aiohttp.client_exceptions.ClientConnectorCertificateError: Cannot connect to host example.com:443 ssl:True [SSLCertVerificationError: (1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate is not yet valid (_ssl.c:1010)')]

Erros de JWT no PyJWT ao validar o token do seu lado:

jwt.exceptions.ExpiredSignatureError: Signature has expired
jwt.exceptions.ImmatureSignatureError: The token is not yet valid (nbf)

E aqui um exemplo mínimo reproduzível via proxy, que vale manter à mão como caso de teste:

import requests, ssl, sys
PROXY = "http://LOGIN:PASSWORD@PROXY_HOST:PORT"
try:
r = requests.get("https://example.com/", proxies={"https": PROXY}, timeout=15)
print(r.status_code, r.headers.get("Date"))
except requests.exceptions.SSLError as e:
msg = str(e)
if "not yet valid" in msg or "has expired" in msg:
print("Похоже на сдвиг системных часов, проверьте date -u")
print(msg)

Uma pequena explicação: o caractere de quebra de linha neste e nos próximos exemplos aparece como entidade HTML para que a marcação permaneça em uma única linha; ao copiar no editor ele vira uma quebra de linha comum.

Node.js: https, fetch, undici

O Node imprime mensagens curtas e, o que é valioso, fornece um campo code no erro:

Error: certificate is not yet valid
code: 'CERT_NOT_YET_VALID'
Error: certificate has expired
code: 'CERT_HAS_EXPIRED'

Armadilha do fetch nativo: por fora você só vê TypeError: fetch failed, e a causa real fica escondida na propriedade cause. Registre-a explicitamente:

try {
 const res = await fetch("https://example.com/");
 console.log(res.status, res.headers.get("date"));
} catch (e) {
 console.error(e.message, e.cause && e.cause.code, e.cause && e.cause.message);
}

Para rotear o fetch por proxy no Node, usa-se o ProxyAgent do pacote undici. O erro de certificado de destino nesse caso chega com o mesmo código CERT_HAS_EXPIRED ou CERT_NOT_YET_VALID em cause. Para o módulo clássico https e bibliotecas como axios, usa-se o agente do pacote https-proxy-agent — os códigos de erro são os mesmos, pois vêm do mesmo OpenSSL dentro do Node.

A biblioteca jsonwebtoken, ao validar tokens, distingue dois casos temporais:

TokenExpiredError: jwt expired
NotBeforeError: jwt not active

Erros do lado do servidor em assinaturas

Aqui o texto é definido pela API, mas as formulações típicas são reconhecíveis. Nos storages compatíveis com S3:

<Code>RequestTimeTooSkewed</Code>
<Message>The difference between the request time and the current time is too large.</Message>

Em servidores OAuth — cabeçalho WWW-Authenticate com error="invalid_token" e descrição do tipo “The token expired” ou “Token used before issued”. Em APIs financeiras — mensagens sobre timestamp outside of recvWindow ou “Timestamp for this request is outside of the allowed window”. Se você vê qualquer uma dessas frases, o relógio é a primeira coisa a checar.

Por que os relógios saem do lugar: virtualização, contêineres, ausência de daemon, ajuste manual

O oscilador de quartzo da placa-mãe não é referência. A imprecisão típica é de 10 a 100 ppm, ou seja, até 1 a 9 segundos de deriva por dia sem correção. Em um mês sem sincronização, acumulam-se minutos. Esse é o pano de fundo de qualquer máquina. Mas os incidentes principais não vêm da deriva lenta, e sim de saltos. Vejamos as origens.

Máquinas virtuais: pausa, snapshot, migração

A máquina virtual não é dona do relógio físico. Ela recebe o tempo por uma fonte paravirtualizada (kvm-clock, Hyper-V reference time e equivalentes em outros hipervisores). Quando o guest é suspenso, salvo em snapshot ou migra entre hosts, o relógio interno dele “congela”. Após a retomada, o kernel ou recebe uma correção do hipervisor ou não — depende das configurações dos agentes de guest. Cenário clássico: o time reverteu um servidor de teste para um snapshot de duas semanas atrás, e ele acordou com o relógio 14 dias atrasado. Todos os certificados emitidos nessas duas semanas são “ainda não válidos” para ele.

Um problema à parte é o conflito entre duas fontes de verdade. O agente de guest do hipervisor periodicamente “puxa” o tempo para o do host, e dentro do guest roda um daemon NTP próprio. Se o host não está sincronizado, o guest recebe dois sinais contraditórios e o relógio pode pular para frente e para trás. Recomendação da maioria dos guias de virtualização: ou sincronização pelo hipervisor, ou NTP interno, mas nunca os dois ao mesmo tempo.

Notebooks de desenvolvedores: suspensão e VMs de desktop

O Docker Desktop e o WSL2 rodam dentro de uma máquina virtual leve. Depois de suspender ou hibernar o notebook, o relógio dessa VM frequentemente fica atrasado exatamente o tempo da suspensão — uma hora, uma noite, um fim de semana. O SO host puxa o tempo, mas a VM nem sempre. Resultado: na manhã de segunda-feira todos os scripts em contêineres falham em TLS, e no terminal do host está tudo certo. É justamente essa situação que mais gera a reclamação “funciona sem proxy no notebook, não funciona via proxy no contêiner” — mesmo quando o proxy, nos dois casos, não participava da verificação.

Contêineres

O contêiner não tem relógio de tempo real próprio — ele usa o relógio do host. Vamos detalhar isso numa seção específica; por ora, fixemos o principal: se o host saiu do lugar, todos os contêineres saíram junto, e não dá para consertar de dentro do contêiner.

Ausência de daemon de sincronização ou sua inoperância

Imagens mínimas, instalações “peladas” e sistemas montados à mão muitas vezes vêm sem cliente NTP algum. E onde ele existe, pode não funcionar por uma dezena de motivos: UDP de saída para a porta 123 bloqueado pelo firewall; nomes de pools que não resolvem; serviço instalado mas mascarado; chrony e timesyncd instalados ao mesmo tempo atrapalhando um ao outro; deslocamento na inicialização acima do limite em que o ntpd clássico se recusa a corrigir e encerra. Especialmente vulneráveis são os servidores cuja única saída para a internet é via proxy HTTP corporativo: NTP por UDP através desse proxy não passa, e o relógio fica à própria sorte.

Ausência de RTC e bateria descarregada

Computadores de placa única como o Raspberry Pi e muitos mini-PCs não têm relógio de hardware com bateria. Ao iniciar, eles arrancam da época Unix ou da data da última gravação e vivem com isso até a primeira sincronização bem-sucedida. Em servidores antigos, a bateria da CMOS acaba, e depois de cada desligamento a BIOS reseta a data para a de fábrica. Se a sincronização estiver inacessível, a máquina trabalha com data de dez anos atrás, e qualquer conexão TLS está condenada.

Ajuste manual e confusão de fuso horário

Um comando date -s executado “para testar a hipótese” e esquecido é um enredo comum. Outro é o dual boot Windows e Linux na mesma máquina: o Windows guarda a hora local no relógio de hardware, o Linux por padrão espera UTC, e depois de cada reinicialização no outro sistema o relógio desloca pelo valor do fuso, três horas no horário de Brasília. E, por fim, a confusão entre hora e fuso horário: mudar a variável TZ ou o arquivo /etc/localtime muda a exibição, mas não a hora do sistema. A comparação deve ser sempre feita em UTC.

Diagnóstico em um minuto: comparação com fonte externa e desvio aceitável

O objetivo deste procedimento é responder em 60 segundos a uma única pergunta: o relógio da máquina de onde saem as requisições via proxy está certo? Todos os comandos rodam naquela mesma máquina ou naquele mesmo contêiner onde o cliente falha, e não no seu notebook.

Passo 1. Veja o UTC e compare com qualquer fonte confiável

date -u

Compare com o relógio do celular, sincronizado pela operadora. Divergência em minutos aparece na hora. Para segundos, são necessários os passos abaixo.

Passo 2. Pegue a hora do cabeçalho Date do HTTP pelo mesmo proxy

Qualquer servidor HTTP correto devolve o cabeçalho Date pelo próprio relógio (normalmente sincronizado). O truque: peça por HTTP comum, não HTTPS, porque com deslocamento grande o TLS não vai estabelecer e você não recebe cabeçalho algum:

date -u; curl -sI --proxy http://LOGIN:PASSWORD@PROXY_HOST:PORT http://example.com/ | grep -i '^date'

As duas linhas devem bater com precisão de um ou dois segundos (o cabeçalho tem granularidade de segundos, mais a latência do canal). Se viu um minuto ou mais, o diagnóstico está feito. Esse truque vale sobretudo porque não exige UDP, funciona por qualquer proxy e não depende de TLS.

O mesmo truque em Python — útil para embutir no healthcheck de um serviço que passa pela Proxeon ou por qualquer outro proxy:

import email.utils, time, requests
PROXY = "http://LOGIN:PASSWORD@PROXY_HOST:PORT"
r = requests.head("http://example.com/", proxies={"http": PROXY}, timeout=10)
server_ts = email.utils.parsedate_to_datetime(r.headers["Date"]).timestamp()
skew = time.time() - server_ts
print(f"Смещение локальных часов: {skew:+.1f} с")
if abs(skew) > 30:
raise SystemExit("Часы уехали, синхронизация обязательна")

Passo 3. Pergunte ao sistema se ele se considera sincronizado

timedatectl

Procure duas linhas: “System clock synchronized: yes” e “NTP service: active”. Qualquer “no” ou “inactive” já é motivo para ir à seção de configuração. Se for chrony:

chronyc tracking

Olhe “System time” (magnitude e sinal do deslocamento), “Stratum” (não pode ser 0) e “Leap status” (deve ser Normal, e não “Not synchronised”).

Passo 4. Consulta pontual a um servidor NTP sem alterar o relógio

Se a UDP de saída estiver aberta, dá para medir o deslocamento com precisão, sem mudar nada:

chronyd -Q 'pool pool.ntp.org iburst'

Ou, se ntpdate estiver instalado:

ntpdate -q pool.ntp.org

A saída mostra o offset em segundos com sinal.

Passo 5. Confirme que é o tempo que está quebrando o TLS

A ferramenta openssl sabe passar por proxy HTTP e imprime o resultado da verificação com código numérico:

openssl s_client -connect example.com:443 -proxy PROXY_HOST:PORT -servername example.com </dev/null 2>/dev/null | grep 'Verify return code'

“Verify return code: 9 (certificate is not yet valid)” ou “10 (certificate has expired)” com certificado vivo é prova cabal. Lembre que o s_client, na maioria das compilações, não suporta autenticação por login e senha no proxy; para essa verificação o prático é usar autenticação por IP, que a Proxeon oferece. As datas do próprio certificado saem assim:

openssl s_client -connect example.com:443 -proxy PROXY_HOST:PORT -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -dates

Qual desvio é considerado normal

  • Até 100 milissegundos — máquina saudável com NTP funcionando. Não precisa fazer nada.
  • 100 ms — 1 segundo — aceitável para todos os protocolos em questão, mas indica fonte ruim ou consulta rara. Vale conferir a configuração.
  • 1 — 30 segundos — zona amarela. TLS e assinaturas com janela larga ainda funcionam, TOTP e JWT com leeway pequeno já podem tropeçar. Consertar hoje.
  • Mais de 30 segundos — zona vermelha. Considere que qualquer erro de autenticação ou TLS foi causado pelo relógio, até prova em contrário.

Como reproduzir o problema de propósito

Para confirmar que o seu tratamento de erros reconhece corretamente uma falha temporal, não é preciso mexer no relógio do sistema. A biblioteca libfaketime substitui o tempo para um único processo:

faketime '2024-01-01 12:00:00' curl -sS https://example.com/ -o /dev/null

Você receberá “certificate is not yet valid” para qualquer site com certificado emitido depois da data indicada. Para verificar certificado sem rede, o OpenSSL tem a flag -attime com hora Unix, útil para testes em CI.

Configuração da sincronização: systemd-timesyncd e chrony, e como confirmar que está de fato funcionando

Instalar o pacote não é sincronizar. Abaixo estão dois esquemas que funcionam e, mais importante, a metodologia de verificação.

Escolha: timesyncd ou chrony

systemd-timesyncd — cliente SNTP minimalista, que faz parte do systemd. Consulta um servidor da lista, sabe corrigir o relógio com salto e de forma suave, não atua como servidor, não suporta fontes de hardware. É suficiente para estações de trabalho e servidores não críticos, onde a precisão necessária é da ordem de dezenas de milissegundos.

chrony — implementação completa de NTP. Combina várias fontes, converge rapidamente após longa inatividade, comporta-se bem em rede intermitente, suporta NTS (NTP com autenticação via TLS) e pode distribuir tempo na rede interna. É a escolha padrão para servidores, máquinas virtuais e qualquer nó por onde passam requisições assinadas e trabalho com proxy.

Importante: dois daemons ao mesmo tempo são fonte de caos. Ao instalar o chrony, as distribuições costumam desabilitar o timesyncd, mas confira isso explicitamente.

Variante A: systemd-timesyncd

Configuração no arquivo /etc/systemd/timesyncd.conf:

[Time]
NTP=ntp.your-provider.internal 0.pool.ntp.org 1.pool.ntp.org
FallbackNTP=2.pool.ntp.org 3.pool.ntp.org
PollIntervalMinSec=32
PollIntervalMaxSec=2048

Ativação e verificação:

sudo timedatectl set-ntp true
sudo systemctl restart systemd-timesyncd
timedatectl timesync-status

Na saída de timesync-status, o campo Server deve estar preenchido, o Stratum diferente de 0 e o campo Offset em milissegundos. Se o servidor não aparecer ou vier “Packet count: 0”, as respostas NTP não estão chegando — quase certamente a UDP 123 está fechada.

Variante B: chrony

Arquivo /etc/chrony/chrony.conf (em alguns sistemas /etc/chrony.conf):

server ntp.your-provider.internal iburst prefer
pool pool.ntp.org iburst maxsources 4
driftfile /var/lib/chrony/drift
makestep 1.0 3
rtcsync
logdir /var/log/chrony

Vamos destrinchar as diretivas-chave, porque são elas que definem o comportamento em caso de falha:

  • iburst — na inicialização, envia uma série de requisições seguidas e sincroniza em segundos, não em minutos.
  • makestep 1.0 3 — se o deslocamento passar de 1 segundo, nas três primeiras atualizações é permitido um salto; depois, só correção suave. Sem essa diretiva, após um snapshot ou suspensão o chrony vai “alcançar” o relógio por semanas. Em servidores que dormem ou migram com frequência, alguns usam makestep 1.0 -1, permitindo o salto sempre.
  • rtcsync — grava periodicamente a hora no relógio de hardware, para iniciar perto do valor correto após reiniciar.
  • Servidor interno do provedor em primeiro lugar — as nuvens costumam oferecer um endereço NTP dentro da rede, acessível mesmo com UDP externa bloqueada e com latência mínima. Pegue o endereço na documentação do seu provedor.

Inicialização:

sudo systemctl enable --now chrony
sudo chronyc makestep
chronyc tracking
chronyc sources -v

Como saber que a sincronização está de fato funcionando

Esta é a parte central da seção. Sinais de sincronização funcionando no chrony:

  1. Em chronyc tracking, o campo Reference ID não é zero e o Stratum fica entre 2 e 4 num servidor típico. Stratum 0 ou 10 com Reference ID vazio significa “estou funcionando sozinho comigo mesmo”.
  2. Leap status: Normal. O valor “Not synchronised” significa que nenhum relógio está sendo corrigido por ninguém.
  3. System time mostra deslocamento em milissegundos ou microssegundos, não em segundos.
  4. Em chronyc sources, ao menos uma fonte está marcada com asterisco (a atual selecionada) ou com mais (participa da combinação). Ponto de interrogação em todas as fontes indica que as respostas não chegam — procure firewall ou DNS.
  5. Last offset e RMS offset diminuem com o tempo, e não aumentam.

Para o timesyncd, a verificação equivalente é “System clock synchronized: yes” no timedatectl e Packet count diferente de zero no timesync-status.

Quando o NTP está bloqueado pelo firewall

Se no servidor só está liberado HTTP(S) de saída via proxy, há três caminhos. O primeiro é pedir a liberação da UDP 123 ao menos para o servidor NTP interno; é um pedido padrão e seguro para os administradores de rede. O segundo é sincronizar pelo hipervisor ou pelo host, se for VM ou contêiner. O terceiro é um meio-termo com o utilitário htpdate, que acerta o relógio pelo cabeçalho Date das respostas HTTP; a precisão é da ordem de um segundo, o suficiente para TLS, JWT e assinaturas de requisição, e ele consegue operar por proxy HTTP. Não é a solução mais elegante, mas funciona para segmentos isolados.

Windows

Em hosts Windows, o serviço de hora se confere assim:

w32tm /query /status
w32tm /resync

Em ambiente de domínio, a fonte é o controlador de domínio; fora do domínio, é o servidor NTP configurado. O campo Last Successful Sync Time deve estar recente.

Contêineres e relógio: o que se herda do host e o que não

Um contêiner é um conjunto de namespaces isolados sobre um kernel compartilhado. O isolamento abrange processos, rede, sistema de arquivos, pontos de montagem. Mas o relógio de tempo real (CLOCK_REALTIME) não é isolado. Os namespaces de tempo, que surgiram no Linux 5.6, permitem deslocar apenas relógios monotônicos e o tempo desde o boot — isso serve para migração de contêineres e não afeta a hora do calendário. Daí decorrem algumas consequências rígidas.

O que se herda

  • Hora do calendário. Todos os contêineres veem exatamente a mesma hora que o host. Se o host saiu do lugar, todos saíram.
  • Sincronização. Um chrony funcionando no host “conserta” automaticamente a hora em todos os contêineres.
  • Saltos. Se o host corrigiu o relógio com salto, as aplicações nos contêineres veem esse salto na hora.

O que não se herda

  • Fuso horário. O arquivo /etc/localtime e a variável TZ vivem dentro da imagem. Em imagens mínimas Alpine, o pacote tzdata pode faltar, e o contêiner sempre mostra UTC. Não é erro de hora, é erro de exibição, mas confunde regularmente na leitura de logs.
  • Repositório de certificados raiz. Não é tema deste artigo, mas vale lembrar: ele também fica dentro da imagem.
  • Permissão para mudar a hora. A chamada de sistema que define a hora exige CAP_SYS_TIME, que o contêiner por padrão não tem. O comando date -s dentro do contêiner termina com o erro “Operation not permitted”. Rodar um daemon NTP dentro de um contêiner comum é inútil, e dar CAP_SYS_TIME a ele é má prática: ele vai passar a controlar o relógio de todo o host.

Consequências práticas

O diagnóstico da seção anterior pode e deve ser executado dentro do contêiner — date -u e a consulta ao cabeçalho Date via proxy mostram o quadro real. Mas a correção sempre acontece no host. Se o contêiner roda em Kubernetes, o host relevante é o nó específico onde o pod está agendado; confirme que o chrony faz parte da imagem base dos nós e que o monitoramento coleta a métrica de deslocamento de cada nó.

Docker Desktop e WSL2 em notebooks são uma história à parte, porque o “host” para os contêineres ali é uma máquina virtual oculta, propensa a atrasar após suspensão. Para o WSL2, a correção rápida é puxar a hora do relógio de hardware, que o hipervisor mantém em ordem:

sudo hwclock -s

Para o Docker Desktop, o modo radical mas confiável é reiniciar o aplicativo; após o start, a VM se sincroniza com o host. Em versões recentes o problema é mais raro, mas não desapareceu de vez, então um healthcheck de deslocamento de relógio em ambiente de dev não é paranóia, é economia de uma hora pela manhã.

Exemplo: healthcheck em arquivo compose

A ideia é não subir o contêiner de trabalho se o relógio do host saiu do lugar além do tolerável. A verificação usa o mesmo truque do cabeçalho Date por HTTP comum via proxy, para não depender de TLS:

healthcheck:
 test: ["CMD", "python", "/app/clock_check.py"]
 interval: 5m
 timeout: 20s
 retries: 1

O script clock_check.py é aquele que escrevemos na seção de diagnóstico, com limite de 30 segundos. O orquestrador marca o contêiner como não saudável e você vê a causa no status, não numa pilha de erros TLS nos logs da aplicação.

Erros típicos e casos da prática

A seção sobre o que não fazer é mais útil que a seção sobre o que fazer. Aqui estão reunidas decisões que parecem lógicas no momento do incidente e custam caro depois, além de algumas histórias características com números.

Erro 1: desativar a verificação do certificado

Flag -k, verify=False, NODE_TLS_REJECT_UNAUTHORIZED=0. O script volta a “funcionar”, tarefa encerrada. Mas as assinaturas de requisição e os JWTs continuam não batendo, o TOTP não é aceito, e você desligou o único mecanismo que protegia o canal. O relógio continua torto e em uma semana vai quebrar outra coisa. Desativar a verificação é aceitável exatamente em um comando no contexto do diagnóstico, e nunca no código.

Erro 2: aumentar leeway ou a janela de assinatura “com folga”

Aumentar a tolerância de verificação do JWT para 10 minutos significa multiplicar por 20 a janela para reutilização de um token interceptado. O problema real (o relógio) continua lá. A tolerância correta para JWT é de 30 a 60 segundos; para assinaturas HMAC, é o que o provedor da API define.

Erro 3: ajustar a hora na mão em vez de sincronizar

date -s corrige o sintoma por algumas horas. A deriva e o próximo snapshot trazem tudo de volta, só que agora ninguém vai lembrar que o relógio foi ajustado à mão.

Erro 4: dois daemons de hora ao mesmo tempo

chrony e timesyncd, ou chrony e o agente de guest do hipervisor com sincronização ativa, puxam o relógio em direções opostas. Nos logs do chrony isso aparece como correções grandes constantes. Deixe uma única fonte de verdade.

Erro 5: comparar hora local com UTC

Diferença exata de 3, 5 ou 7 horas quase sempre não é falha de relógio, e sim fuso horário. Compare sempre date -u com uma fonte UTC e registre nos logs a hora com a zona explícita.

Erro 6: confiar no status “installed”

A role do Ansible instalou o chrony, tarefa verde. Mas a UDP 123 está fechada e o chrony está sincronizado “consigo mesmo”. A verificação na configuração da infraestrutura precisa olhar para o Leap status e o deslocamento, não para o fato de o pacote existir.

Erro 7: reaproveitar a requisição assinada em retries

Principalmente via proxy com reconexões. Cada tentativa deve receber nova marcação de tempo e nova assinatura.

Caso A: snapshot do ambiente de teste

Um time de coleta de dados abertos de preços mantinha um pool de workers em máquinas virtuais e revertia o ambiente uma vez por semana para um snapshot “limpo”. Após uma das reversões, todos os workers que passavam por proxy começaram a falhar com “certificate is not yet valid” em um terço dos sites de destino — justamente os que usavam certificados de 90 dias e tinham sido reemitidos ao longo dos 12 dias de vida do snapshot. No notebook do engenheiro, sem proxy, os sites abriam. Levou uma hora e meia alternando pools e planos do proxy até alguém rodar date -u e ver os 12 dias de atraso. Correção: chrony com makestep na imagem base do ambiente e verificação de deslocamento no script de pós-restauração. Não houve repetição.

Caso B: segmento isolado e mini-PC

Um mini-PC sem relógio de hardware estava em um segmento de rede onde só era permitido HTTP de saída via proxy corporativo. Após uma queda de energia programada, ele iniciou com a data de build da imagem — 14 meses no passado. Nenhuma conexão TLS se estabelecia, e isso parecia uma falha total do proxy. Solução: liberação da UDP 123 para o servidor NTP interno da empresa, diretiva makestep 1.0 -1 no chrony e, para o caso de ausência de NTP, execução do htpdate via proxy na inicialização.

Caso C: upload para storage de objetos a partir do Docker Desktop

Um desenvolvedor depurava o envio de arquivos para um storage compatível com S3 via proxy, a partir de um contêiner no notebook. Depois do almoço, todas as requisições passaram a devolver RequestTimeTooSkewed. Deslocamento do relógio da VM do Docker Desktop: 23 minutos, pouco acima da janela de 15 minutos. Reiniciar o Docker Desktop resolveu em um minuto; foi adicionado ao projeto um healthcheck de deslocamento que mostra a causa de imediato.

Caso D: 50 segundos e TOTP

Um serviço automatizava o login no próprio portal corporativo com uma conta de serviço e autenticação de dois fatores por TOTP. Os códigos deixaram de ser aceitos: o servidor respondia que o código estava errado. Deslocamento do relógio do nó: 50 segundos, porque o timesyncd estava instalado mas mascarado na migração para chrony, que nunca foi ativado. Com tolerância de um intervalo de 30 segundos, isso bastou. Zero mudanças no código, apenas iniciar o chrony.

Ferramentas e checklist “antes de culpar o proxy”

Vamos reunir tudo em um formato que pode ser anexado a um ticket ou embutido num runbook. Primeiro as ferramentas, depois a sequência.

Ferramentas

  • date -u, timedatectl, chronyc, hwclock — estado do relógio e da sincronização.
  • curl -v --proxy, openssl s_client -proxy — teste de TLS via proxy com código de verificação.
  • chronyd -Q, ntpdate -q — medição pontual do deslocamento sem alterar o relógio.
  • Cabeçalho Date por HTTP — fonte universal de hora, que funciona por qualquer proxy.
  • libfaketime, openssl -attime — reprodução da falha e teste do tratamento de erros.
  • htpdate — sincronização de emergência em segmentos com UDP fechada.
  • Métricas — exportadores para chrony ou timesyncd entregam o deslocamento ao sistema de monitoramento; limiar de alerta em 1 segundo, crítico em 30 segundos.

Checklist

  1. Onde exatamente o código que falha é executado? Notebook, servidor, VM, contêiner, pod. Todas as verificações são feitas ali.
  2. date -u confere com o celular? Divergência em minutos — pare e conserte o relógio.
  3. O cabeçalho Date por http:// via proxy confere com o UTC local em até dois segundos? Se não, é o relógio.
  4. O timedatectl diz synchronized: yes e o chronyc tracking mostra Leap status Normal? Se não, a sincronização não está funcionando, independentemente do que esteja instalado.
  5. O texto do erro contém not yet valid, has expired, RequestTimeTooSkewed, jwt expired, jwt not active, timestamp? Cada uma dessas palavras é sobre tempo.
  6. Sem proxy, na mesma máquina, o erro é o mesmo? Se sim, o proxy não tem nada a ver.
  7. Via proxy, a partir de outra máquina, não há erro? Se sim, o problema é na primeira máquina.
  8. O openssl s_client via proxy mostra Verify return code 9 ou 10 com as datas do certificado válidas? Diagnóstico confirmado.
  9. Os erros são só com um host de destino? Compare o Date dele com os outros — pode ser que o relógio do lado dele tenha saído do lugar.
  10. As assinaturas são refeitas a cada tentativa? Se não, conserte a lógica de retries.
  11. Houve snapshot, suspensão, migração ou reinício após longo desligamento recentemente? Qualquer “sim” é quase certamente a causa.
  12. Só depois de todos os “não” — contato com o provedor de proxy com as saídas de date -u, chronyc tracking e curl -v anexadas. Assim o suporte da Proxeon ou de qualquer outro serviço resolve em uma rodada de mensagens, não em cinco.

FAQ: perguntas frequentes sobre tempo, proxy e falhas

O proxy pode distorcer a hora nas minhas requisições ou no TLS?

Ao tunelar via CONNECT ou SOCKS5 — não. O handshake TLS acontece entre o seu cliente e o servidor de destino; o proxy só vê bytes criptografados. As marcações de tempo nas assinaturas e nos JWTs são formadas pelo seu código na sua máquina. A única coisa que o proxy pode adicionar é latência, e ela é medida em milissegundos, não em minutos.

Por que funciona no notebook sem proxy, mas não no servidor com proxy?

Porque são duas máquinas diferentes com dois conjuntos de relógio diferentes. A comparação é incorreta por construção: mudaram duas variáveis de uma vez. Para isolar o efeito do proxy, rode a requisição no servidor sem proxy. Para isolar o efeito da máquina, rode via proxy no notebook. Quase sempre vai dar que o erro está atrelado à máquina.

Qual precisão de hora é necessária para trabalhar via proxy?

Para TLS, basta precisão em horas se os certificados forem de longa duração, e em dias para os de curta duração. Para assinaturas de requisição, a janela típica é de 5 a 15 minutos. Para JWT, 30 a 60 segundos de tolerância. Para TOTP, mais ou menos 30 segundos. Um NTP funcionando dá milissegundos, então o padrão prático é deslocamento inferior a 1 segundo, com alerta acima de 30 segundos.

Posso rodar um cliente NTP dentro do contêiner?

Tecnicamente, só com CAP_SYS_TIME, e aí ele vai controlar o relógio de todo o host, violando o isolamento e entrando em conflito com o daemon do host. A abordagem correta é a sincronização no host ou no nó do cluster. Dentro do contêiner, deixe apenas a verificação de deslocamento.

O que fazer se a UDP 123 está totalmente bloqueada e a saída só existe via proxy HTTP?

Peça a liberação da UDP ao menos para o NTP interno da empresa ou do provedor de nuvem — é uma necessidade de infraestrutura padrão. Enquanto isso, use a sincronização do hipervisor ou htpdate pelos cabeçalhos Date via proxy. Lembre que o NTS protege o NTP via TLS na porta TCP 4460, mas os pacotes de tempo em si continuam indo por UDP, então o NTS não contorna o bloqueio de UDP.

O relógio está adiantado alguns minutos, mas o TLS funciona. Por que então as requisições à API falham?

Porque no TLS a janela é de dias, e nas assinaturas e tokens é de segundos e minutos. É uma situação normal: os certificados são verificados com sucesso, mas uma assinatura HMAC com marcação de tempo três minutos no futuro é rejeitada pelo servidor com janela rígida. Olhe o texto do erro da API, e não só o sucesso da conexão.

Como saber se o relógio que saiu do lugar é o do servidor de destino, e não o meu?

Compare o cabeçalho Date da resposta do host problemático com o Date de dois ou três outros hosts e com o seu date -u. Se o seu relógio bate com a maioria e um host se desvia, o problema está do lado dele. Alguns SDKs de cliente conseguem compensar automaticamente o deslocamento do servidor, calculando-o a partir da resposta de erro de tempo.

É preciso reassinar a requisição em cada retentativa via proxy?

Sim. A assinatura inclui a marcação de tempo e é válida dentro de uma janela. Se entre a primeira tentativa e o retry passaram minutos de espera e reconexões, a assinatura antiga pode sair da janela. Forme a assinatura no momento do envio de cada tentativa.

Como incluir a verificação de relógio no monitoramento?

Colete o deslocamento do chrony ou do timesyncd no sistema de métricas e defina dois limiares: aviso em 1 segundo, crítico em 30 segundos. Adicionalmente, um healthcheck do serviço pelo cabeçalho Date através do proxy em uso; ele pega situações em que o daemon de hora está formalmente vivo mas o relógio saiu do lugar mesmo assim, por exemplo após restauração de snapshot.

Mudou alguma coisa em 2026?

A principal mudança é a redução dos prazos de validade dos certificados públicos e a disseminação em massa da reemissão automática. Isso faz com que até pequenos atrasos de relógio, de alguns dias, sejam causa real de falhas TLS, quando antes eram raridade. Ao mesmo tempo, cresce a fatia de JWTs curtos e janelas rígidas de assinatura em APIs. Em outras palavras, a exigência de precisão do relógio nas máquinas cliente está aumentando, e a sincronização de horário deixa de ser “uma configuração padrão em que ninguém pensa”.

Conclusão: o tempo como primeiro item de diagnóstico

Fazendo o resumo. A verificação do prazo de validade do certificado, dos campos exp e nbf do JWT, da marcação de tempo na assinatura da requisição e do código de uso único — tudo isso são comparações locais com o relógio do sistema. Ao trabalhar via proxy, aparece mais uma máquina na cadeia, e a atenção se desloca naturalmente para ela, embora nem o handshake TLS, nem a assinatura, nem a validação do token passem pelo túnel do proxy. O proxy, nesses cenários, é apenas transporte. Já os relógios saem do lugar por motivos bem terrenos: snapshots e pausa de máquinas virtuais, suspensão de notebooks com Docker Desktop e WSL2, ausência ou inoperância do daemon de sincronização, UDP fechada, bateria descarregada, ajuste manual.

O programa prático é simples e cabe em três passos. O primeiro é se acostumar a reconhecer os erros temporais pelo texto: not yet valid, has expired, RequestTimeTooSkewed, jwt expired, jwt not active. O segundo é colocar a verificação de deslocamento de relógio como primeiro item de qualquer análise de incidente com proxy: date -u, cabeçalho Date por HTTP via proxy, timedatectl ou chronyc tracking. É um minuto que economiza horas. O terceiro é fazer com que o problema não volte: chrony com iburst e makestep nas imagens base, uma única fonte de hora por máquina, sincronização no host para os contêineres, métrica de deslocamento no monitoramento e healthcheck nos serviços que passam por proxy.

Quando tudo isso está feito, o contato com o provedor de proxy passa a fazer sentido: você chega com a hipótese descartada e com as saídas dos comandos prontas, e a análise vai direto ao ponto. E, na maioria dos casos, o contato nem vai ser necessário — porque o relógio vai ser a primeira coisa que você vai checar, e a última que vai te surpreender.