Configurar o HAProxy como proxy reverso no AlmaLinux 10 para receber webhooks de pagamento consiste em instalar o balanceador, realizar a SSL termination com certificado Let's Encrypt, editar o haproxy.cfg com frontends, backends e health checks, garantindo alta disponibilidade sem alterar o body usado na HMAC. Para ter o ambiente em produção, siga estes passos:
- Instale o HAProxy no AlmaLinux 10 e habilite o serviço no systemd.
- Libere 80/443 no firewalld e permita o HAProxy conectar aos backends no SELinux.
- Monte o frontend HTTPS, os backends e o health check HTTP.
- Preserve Host, X-Forwarded-For, Content-Type e o body original.
- Desative retry automático de POST e valide com curl e logs.
- Teste failover derrubando um worker e conferindo o segundo backend.
Pré-requisitos
- AlmaLinux 10 com acesso root ou sudo, conforme Acessando servidores VPS Linux da AviraHost.
- Pacote haproxy do AppStream, firewalld ativo e SELinux em enforcing.
- Certificado PEM (certificado + cadeia + chave) em arquivo único para seudominio.com.br.
- Dois ou mais workers HTTP na porta 8080 (PHP 8.4, Node ou similar) com rota POST de webhook e GET /health.
- DNS A de seudominio.com.br apontando para 203.0.113.10; veja o Guia de zona DNS: registros A, MX, CNAME e TXT do zero.
- Lista de IPs do provedor de pagamento, se for filtrar origem no frontend.
Instalação e configuração inicial do HAProxy
O proxy reverso HAProxy no AlmaLinux 10 escuta 443, escolhe o backend e devolve a resposta do worker sem reescrever o POST. Ao rodar a instalação pelo AppStream, o unit systemd já vem com User=haproxy e o arquivo principal em /etc/haproxy/haproxy.cfg. Não misture outro proxy na mesma porta: o bind exclusivo evita 502 e "address already in use".
Atualize o sistema e instale o pacote. Em seguida habilite o serviço, mas só inicie depois de validar a configuração, para o unit não falhar no primeiro boot.
dnf install -y haproxy
systemctl enable haproxy
haproxy -v
ss -tlnp | grep -E ':80|:443' || true
HAProxy version 3.x
Loaded plugins: ...
complete
Crie o diretório de certificados e junte cadeia e chave no formato que o bind ssl crt espera. O usuário haproxy precisa ler o PEM; permissão 640 e grupo haproxy bastam.
mkdir -p /etc/haproxy/certs
cat /etc/letsencrypt/live/seudominio.com.br/fullchain.pem /etc/letsencrypt/live/seudominio.com.br/privkey.pem > /etc/haproxy/certs/seudominio.com.br.pem
chown root:haproxy /etc/haproxy/certs/seudominio.com.br.pem
chmod 640 /etc/haproxy/certs/seudominio.com.br.pem
Libere HTTP e HTTPS no firewalld e recarregue. Sem isso o provedor de pagamento nunca alcança o frontend, mesmo com o processo no ar.
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
firewall-cmd --list-services
success
dhcpv6-client http https ssh
Com SELinux em enforcing, o daemon só conecta a portas típicas. Workers em 8080 exigem haproxy_connect_any ou porta rotulada. Sem isso o health check marca o servidor DOWN e o webhook some do log do aplicativo.
setsebool -P haproxy_connect_any on
getsebool haproxy_connect_any
haproxy_connect_any --> on
Faça backup do cfg padrão antes de substituir. Um bind inválido impede o start; o diagnóstico de unit falho está em Como solucionar problemas de inicialização de serviços no VPS Linux: Guia Completo.
Atenção: o próximo bloco sobrescreve /etc/haproxy/haproxy.cfg. Copie o arquivo atual para haproxy.cfg.bak.
cp -a /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg.bak
Terminação TLS no frontend e roteamento do path
A terminação TLS no HAProxy concentra um certificado, inspeciona o path /webhooks/pagamento e encaminha só o necessário aos workers. Passe TLS ponta a ponta somente se o gateway exigir o certificado no aplicativo; nesse caso o bind não usa crt e o backend fala HTTPS. Em ambos os modos use HTTP/1.1, timeouts curtos e encaminhe Host e X-Forwarded-For. Redirecionar 80 para 443 no frontend evita POST em claro; o fluxo HTTP para HTTPS também aparece em Como redirecionar um site http para https?.
Grave a configuração mínima. option forwardfor insere o IP do cliente. http-request del-header remove hop-by-hop. Não use http-request replace-path no POST do webhook: qualquer mudança de path ou query quebra a assinatura em vários provedores.
global
log stdout format raw local0
maxconn 4096
user haproxy
group haproxy
ssl-default-bind-options ssl-min-ver TLSv1.2
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5s
timeout client 30s
timeout server 30s
timeout http-request 10s
retries 0
frontend fe_http
bind 203.0.113.10:80
http-request redirect scheme https unless { ssl_fc }
frontend fe_webhooks
bind 203.0.113.10:443 ssl crt /etc/haproxy/certs/seudominio.com.br.pem alpn h2,http/1.1
option forwardfor
http-request set-header X-Forwarded-Proto https
acl is_hook path_beg /webhooks/pagamento
use_backend bk_webhooks if is_hook
default_backend bk_webhooks
O bloco unless com ssl_fc usa chaves do ACL do HAProxy; se o validador da sua política rejeitar chaves, troque o frontend 80 por um redirect incondicional: http-request redirect location https://seudominio.com.br. O essencial é não aceitar o POST do gateway em HTTP. ALPN com h2 no frontend é aceitável; nos backends de webhook prefira HTTP/1.1 para não fragmentar o body. timeout http-request de 10s corta clientes lentos sem esperar o retry do provedor. retries 0 no defaults impede reenvio de POST quando um worker recusa a conexão no meio do handshake.
Valide a sintaxe antes do reload. Ao rodar haproxy -c você deve ver a mensagem de sucesso; qualquer aviso de crt ou bind precisa ser corrigido antes do systemd.
haproxy -c -f /etc/haproxy/haproxy.cfg
Configuration file is valid
Configurando o backend bk_webhooks para alta disponibilidade
O backend completa o proxy reverso: round-robin ou leastconn, health check GET /health e servidores com check. Sticky sessions não cabem em evento idempotente. option httpchk usa o path de saúde da API, não o path do webhook, para não gerar evento falso.
backend bk_webhooks
balance roundrobin
option httpchk GET /health HTTP/1.1\r\nHost:\ seudominio.com.br
http-check expect status 200
http-reuse never
server app1 198.51.100.10:8080 check inter 3s fall 3 rise 2
server app2 198.51.100.11:8080 check inter 3s fall 3 rise 2
http-reuse never reduz o risco de misturar um POST de webhook em conexão keep-alive residual. inter 3s com fall 3 tira o worker do pool em cerca de 9s. Suba o serviço e confira a porta 443.
systemctl restart haproxy
systemctl is-active haproxy
ss -tlnp | grep haproxy
active
LISTEN 0 4096 203.0.113.10:443 0.0.0.0:* users:(("haproxy",pid=2148,fd=5))
LISTEN 0 4096 203.0.113.10:80 0.0.0.0:* users:(("haproxy",pid=2148,fd=6))
Balanceamento de carga, HMAC e teste do POST
O balanceamento de carga para webhooks de pagamento deve tratar cada POST como unidade isolada: round-robin se os workers forem iguais, leastconn se o processamento for lento. Não reescreva o body nem reordene cabeçalhos da HMAC. Encaminhe o header de assinatura intacto e o Content-Type original. Filtrar IP do provedor no ACL do frontend é mais seguro do que confiar só em X-Forwarded-For no PHP.
Simule o evento com curl contra HTTPS. O -d envia o JSON cru; o header de assinatura deve chegar igual no access log da aplicação. Troque o valor de assinatura pelo formato do seu gateway (Stripe, Mercado Pago ou similar).
curl -sS -o /tmp/hook.out -w "%{http_code}\n" \
--http1.1 \
-X POST "https://seudominio.com.br/webhooks/pagamento" \
-H "Content-Type: application/json" \
-H "X-Webhook-Signature: t=exemplo,v1=abc123" \
--data-binary '{"id":"evt_2030113","type":"payment.updated"}'
200
No worker, confira que o raw body bate com o JSON enviado e que X-Forwarded-For mostra o IP de origem do curl (ou o IP do provedor, em produção). Se a aplicação canonicalizar JSON antes da HMAC, a validação falha mesmo com proxy correto: a correção é ler o stream bruto, não o objeto já decodificado.
Force o failover: pare o app1 e dispare de novo o POST. O health check deve marcar app1 DOWN e o app2 responder 200. Suba o app1 e espere rise 2. Para ver o estado dos servers use o socket stats se você o habilitou, ou journalctl -u haproxy -e. Um worker que responde 200 em /health mas 500 no POST continua no pool: o check não substitui idempotência no aplicativo. Trate o id do evento como chave única e ignore duplicata quando o provedor retentar após timeout. No HAProxy, não ative option redispatch nesse backend e não use retry-on 503 para POST.
Se o provedor publicar faixas de IP, restrinja o ACL no frontend (src 198.51.100.0/24) e responda 403 ao restante. Isso evita que um POST forjado chegue ao validador HMAC. Documente a faixa e revise quando o gateway atualizar a lista. Ambiente isolado em Compreendendo o Servidor VPS: O que é e Como Funciona! facilita separar o proxy dos workers.
Problemas comuns e como resolver
Sintoma: health check DOWN e webhook 503
Causa: SELinux bloqueia a conexão com 8080, /health devolve 404 ou o Host do httpchk não bate com o vhost do worker.
Solução: confirme getsebool haproxy_connect_any, teste curl http://198.51.100.10:8080/health a partir do host do HAProxy e alinhe o header Host. Depois haproxy -c e systemctl reload haproxy.
Sintoma: assinatura HMAC inválida só atrás do proxy
Causa: replace-path, compressão, troca de Content-Type ou buffer que altera o body; às vezes HTTP/2 no backend fragmenta o POST.
Solução: remova rewrites no ACL do webhook, force HTTP/1.1 no server line se preciso, desative compressão nesse backend e compare o body hexdump no worker com o payload do curl.
Sintoma: eventos duplicados após timeout
Causa: retries maior que zero, option redispatch ou timeout server menor que o processamento do pagamento, levando o provedor a retentar.
Solução: retries 0, sem redispatch, timeout server alinhado ao SLA do gateway e idempotência por id de evento na aplicação. Não faça o HAProxy reenviar POST.
Perguntas frequentes sobre HAProxy e webhooks no AlmaLinux 10
Como o HAProxy recebe webhooks de pagamento com balanceamento de carga?
O HAProxy escuta HTTPS na frente, termina ou passa o TLS e distribui POSTs entre backends com round-robin ou leastconn. Health checks evitam enviar eventos a instâncias fora do ar. Preserve o corpo e cabeçalhos originais para a assinatura do provedor.
Preciso terminar SSL no HAProxy ou no backend para webhooks?
Termine TLS no HAProxy quando quiser um único certificado e inspeção de path. Passe TLS ponta a ponta se o gateway exigir o certificado no aplicativo. Em ambos os casos, use HTTP/1.1, timeouts curtos e encaminhe X-Forwarded-For e Host.
Como validar a assinatura do webhook atrás do proxy reverso?
Não altere o body nem reordene cabeçalhos usados na HMAC. Encaminhe o header de assinatura intacto e o Content-Type original. Confirme no backend o IP de origem real via X-Forwarded-For apenas se a lista de IPs do provedor for conferida no HAProxy.
Qual algoritmo de balanceamento usar para webhooks de pagamento?
Round-robin basta se os workers forem iguais. Leastconn ajuda se o processamento for lento. Evite sticky sessions: cada evento deve ser idempotente. Marque servidor down no health check HTTP no endpoint de saúde da API.
O que fazer se o provedor retentar o webhook e duplicar o pedido?
Trate o evento como idempotente no aplicativo com ID único. No HAProxy, não reescreva o path nem faça retry automático de POST. Ajuste timeout client e server para o provedor reenviar só após falha real de conexão.
Conclusão
- Valide haproxy.cfg com haproxy -c, recarregue o unit e confirme 443 em ss antes de cadastrar a URL no gateway.
- Mantenha retries 0, body intacto e health check em /health, não no path do webhook.
- Teste POST real, failover de um worker e HMAC no backend a cada mudança de certificado ou ACL.
Leia também
- SSL Wildcard: como configurar subdomínios no AlmaLinux
- Passo a passo para auditoria de segurança no AlmaLinux 9
- Tutorial: Redis consumindo muita RAM no AlmaLinux 9
Precisa de ajuda com HAProxy e proxy reverso no AlmaLinux 10?
Um VPS com AlmaLinux 10, IPv4 fixo e portas 80/443 livres reduz o tempo para colocar o balanceador e os workers em produção. A equipe pode orientar dimensionamento e isolamento de rede sem alterar o payload dos webhooks.