Pular para o conteúdo

Proxy do Qlyne no seu servidor

Para site que não usa o Cloudflare: um container pequeno que roda no seu servidor, entre o seu servidor web (nginx, Caddy ou Traefik) e a sua aplicação, e aplica as mesmas regras que você define no painel do Qlyne:

  • sala de espera nas páginas escolhidas, e limite de quantas pessoas ficam no site ao mesmo tempo;
  • desafio próprio (proof-of-work invisível), modo “sob ataque” e recusa de navegador controlado por ferramenta de automação;
  • limite de requisições por página, contado por IP (IPv6 pelo /64) ou por chave de API, token ou sessão;
  • IPs bloqueados e de confiança, e uma regra para as saídas da rede Tor;
  • robôs de IA barrados por categoria, robots.txt incluído;
  • proteção de formulário sem captcha;
  • modo “só observar”, que registra o que seria barrado sem barrar ninguém.

O seu tráfego não passa pelo Qlyne. O proxy busca as regras a cada minuto e manda as contagens a cada 10 segundos; com limite de pessoas no site, ele também pede uma vaga ao Qlyne quando chega alguém sem passe. Se o Qlyne estiver fora, o proxy usa as últimas regras que tinha ou deixa tudo passar.

  • O plano grátis do Qlyne cobre modo observar, IPs, robôs de IA e limite por IP. Sala de espera, limite de pessoas no site, desafio, modo sob ataque, formulários e Tor pedem o plano Pro.
  • Um servidor Linux com Docker e o plugin compose, onde o seu site roda.
  • Um servidor web na frente da aplicação cuidando do HTTPS: nginx, Caddy ou Traefik.
  • O segredo do conector: no painel, no seu site, Connector, “Generate secret”. O mesmo segredo vale para o conector do Cloudflare.
  1. Em Connector, clique em “Download docker-compose.yml” e ponha o arquivo numa pasta do seu servidor. Ele já sai com o id da sua conta e o endereço do Qlyne.
  2. Ao lado dele, crie um arquivo .env com uma linha: QLYNE_SECRET=<seu segredo do conector>.
  3. No arquivo, ajuste QLYNE_UPSTREAM para a sua aplicação vista de dentro do Docker. Aplicação na porta 3000 da mesma máquina é http://host.docker.internal:3000; aplicação no mesmo projeto compose é http://<serviço>:<porta>.
  4. Rode docker compose up -d. O proxy escuta em 127.0.0.1:8080, alcançável só pela própria máquina.

Aponte o servidor web para o proxy e deixe a aplicação de reserva: se o proxy parar, as requisições vão direto para a aplicação e o site continua no ar, só sem a proteção. Troque 3000 pela porta da sua aplicação.

nginx:

map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream app_com_qlyne {
server 127.0.0.1:8080 max_fails=1 fail_timeout=10s; # proxy do Qlyne
server 127.0.0.1:3000 backup; # sua aplicação
}
server {
listen 443 ssl;
server_name loja.exemplo.com;
# o seu ssl_certificate e ssl_certificate_key
location / {
proxy_pass http://app_com_qlyne;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_connect_timeout 2s;
proxy_next_upstream error timeout;
}
}

Caddy (ele já grava os headers X-Forwarded-* sozinho):

loja.exemplo.com {
reverse_proxy 127.0.0.1:8080 127.0.0.1:3000 {
lb_policy first
lb_try_duration 2s
fail_duration 10s
}
}

Traefik (provider de arquivo), com o health check do proxy dizendo ao Traefik quando usar a reserva:

http:
services:
loja-com-qlyne:
failover:
service: qlyne
fallback: app
qlyne:
loadBalancer:
healthCheck: { path: /__qlyne/health, interval: 5s, timeout: 2s }
servers: [{ url: "http://127.0.0.1:8080" }]
app:
loadBalancer:
servers: [{ url: "http://127.0.0.1:3000" }]

Confira: curl http://127.0.0.1:8080/__qlyne/health responde ok, e o site continua abrindo pelo seu servidor web.

Deixe o modo em “Observe only”, navegue pelo site e confira as últimas 24 horas na Overview do site. Com os números fazendo sentido, mude para “Enforce”.

  • O IP do visitante vem do último item do X-Forwarded-For, que o seu servidor web grava. Se o proxy ficar exposto direto, sem servidor web na frente, use QLYNE_CLIENT_IP: "peer".
  • Os limites são contados pelo próprio proxy, por IP ou por header ou cookie, como a regra pedir (o valor fica só como hash). Com mais de um proxy (vários servidores), cada um conta a sua parte.
  • WebSocket e uploads e downloads grandes passam direto.
  • Arquivos estáticos (scripts, estilos, imagens, fontes) não recebem sala de espera, desafio nem checagem de formulário.

Em Access, tranque um caminho como /admin* ou um staging inteiro sem mexer no app. Dois jeitos de entrar:

  • Senha: uma senha compartilhada, guardada só como hash. As tentativas têm limite por IP.
  • Link por e-mail: o visitante digita o endereço; se estiver na sua lista (um endereço, ou @empresa.com.br para todo mundo de lá), chega por e-mail um link que vale uma vez, por 15 minutos. Todo mundo vê a mesma mensagem de “confira seu e-mail”, então a página não revela quem tem acesso.

Quem entra fica dentro pelo tempo que você escolher, por um cookie assinado pelo conector, e o app recebe Qlyne-Gate-User com o e-mail. O portão vale também no modo observar, porque é uma tranca que você pôs, não um palpite. Endereços de confiança passam direto.

Em Logins and forms, “Login protection”, liste as rotas para onde o formulário de login (ou o app) envia e os nomes dos campos de usuário e senha (user.email entra em JSON). Formulário, multipart e JSON são lidos.

  • Senha vazada: o conector calcula o SHA-1 da senha e manda ao Have I Been Pwned só os 5 primeiros caracteres, que valem para centenas de senhas; a senha nunca sai do conector. Se ela aparece em vazamentos, o login chega ao seu app com Qlyne-Leaked-Password: 1, e o app decide, por exemplo pedindo uma senha nova. Se a consulta demora ou cai, o login segue sem o header.
  • Tentativas por conta: contadas pelo usuário, de qualquer IP, para uma conta não ser adivinhada de muitos endereços. Passou do limite, o visitante recebe 429.

Em Firewall, “Managed rules”, o Qlyne procura assinaturas de ataques comuns no caminho, na query string e em corpos de formulário ou JSON de até 16 KB, depois de decodificar (duas vezes, para quem codifica duas vezes para escapar) e sem os comentários de SQL: SQL injection (UNION SELECT, condições 1=1, comandos empilhados, atrasos de tempo, leitura do esquema), cross-site scripting (tag script, handlers de evento, links javascript:, iframes), subida de pasta (../, arquivos do sistema, byte nulo) e injeção de comando ($(…) com comandos do sistema, curl ou bash encadeados, Log4Shell). Corpo em outros formatos (envio de arquivo) e corpo sem tamanho declarado não são lidos.

Todo site começa em “Observe only”: o que casa aparece em Events como “Would block by managed rules”, com a assinatura, e o seu app recebe no Qlyne-Rule. Quando lá só aparecer ataque, mude para “Block” (Pro; no grátis as regras continuam observando). Página que manda HTML ou código de propósito, como um editor de texto rico, vai em “Routes the managed rules skip”. As regras gerenciadas rodam depois das suas, então uma regra de liberar também pula elas.

As assinaturas pegam os ataques comuns e automatizados; não substituem validar a entrada e usar consulta parametrizada no app.

Em Bots, “Bot score”, o proxy põe um script pequeno, /__qlyne/s.js, nas suas páginas HTML, servido do seu próprio domínio. Alguns segundos depois de a página abrir, depois de um clique ou tecla, e quando o visitante envia um formulário ou sai, ele manda um resumo para /__qlyne/s: se o navegador diz que é automatizado, quantos idiomas, plugins, núcleos de processador e pontos de toque ele informa, o tamanho da tela, se tem fuso horário, e quantas vezes o ponteiro mexeu, o visitante clicou, digitou, rolou ou tocou, e quantos desses eventos um script disparou. Ele manda só contagens: nunca teclas, texto ou onde o ponteiro estava.

Desse resumo, e de os cabeçalhos do pedido baterem ou não com o navegador do user agent, o conector dá ao visitante uma nota de 0 a 100: baixa abaixo de 30, média de 30 a 59, alta de 60 para cima. A nota fica 30 minutos no cookie qlyne_bs, assinado pelo conector e amarrado ao IP e ao navegador. O seu app recebe a nota em Qlyne-Bot-Score, com os motivos em Qlyne-Bot-Reasons (como automation,no_interaction), e a tela Events lista as notas média e alta com os motivos. A primeira página vista ainda não tem nota: as ações valem a partir do pedido seguinte.

No Pro, escolha o que a nota média e a alta recebem: nada, o pedido atrasado em 3 segundos, o desafio, o desafio com clique, 403, ou a página-isca. A isca é uma página do seu site que o robô recebe no lugar da que pediu, no mesmo endereço, e ele segue raspando algo que não vale nada. No modo observar isso só é contado.

“Routes that need the score” recusam formulário ou chamada de API (tudo que não é GET) de um navegador que não rodou o script, com 403 e {"code": "SENSOR_REQUIRED"}. Use nas rotas que robô chama direto, como a API de checkout ou de cadastro. Quem resolveu o desafio passa.

“Click to confirm”, em “Challenge”, define como a página do desafio e o captcha de formulário se comportam: nunca (o navegador faz a conta sozinho), na dúvida (um clique em “Sou uma pessoa” só quando a nota não é baixa ou o endereço pediu muitos desafios) ou sempre. Clique disparado por script, ou no computador sem o ponteiro ter mexido antes, é recusado.

Em Firewall, “Shared reputation”, o Qlyne lista os endereços que sites de outras contas viram atacando nas últimas 24 horas: pegos pela armadilha para scanner ou pelas regras gerenciadas, com nota de robô alta, usando navegador automatizado ou recusados pelo captcha de formulário. Um endereço só entra na lista quando pelo menos duas contas o relatam, então nenhuma conta sozinha põe alguém nela. No IPv6 a lista guarda o /64.

Por padrão, o endereço da lista só chega ao seu app com Qlyne-Reputation: 1. No Pro, desafie as visitas às páginas dele (chamadas de API passam, como nas saídas Tor) ou bloqueie com 403; Googlebot e Bingbot nunca são bloqueados, e os IPs de confiança passam direto. A lista vem junto com as regras, em até um minuto.

“Report attacks on this site” (ligado por padrão) manda ao Qlyne só o endereço desses ataques, guardado por 24 horas, nunca o caminho, o user agent ou qual site foi. Desligar não impede você de usar a lista. Como esses dados são tratados está nos termos e na política de privacidade.

Ao abrir a conexão, o navegador diz ao servidor quais versões do TLS, cifras e extensões aceita. Essa lista, resumida numa impressão digital JA4 como t13d1516h2_8daaf6152771_d8a2da3f94cd, depende do programa e não do user agent que ele diz ter: um script que copia o user agent do Chrome continua conectando como script. O proxy só vê isso se a conexão HTTPS terminar nele. Atrás de nginx, Caddy ou Traefik ela não termina, então há dois caminhos:

  • HTTPS no próprio proxy: defina QLYNE_TLS_LISTEN (0.0.0.0:8443, publicado como porta 443), QLYNE_TLS_CERT e QLYNE_TLS_KEY, e monte os arquivos do certificado (o docker-compose.yml baixado traz as linhas, comentadas). Com o certbot, copie os arquivos a cada renovação para uma pasta que o proxy consiga ler, porque a imagem roda como o usuário 65532: certbot renew --deploy-hook 'install -m 0644 $RENEWED_LINEAGE/fullchain.pem /srv/qlyne/certs/ && install -m 0640 -g 65532 $RENEWED_LINEAGE/privkey.pem /srv/qlyne/certs/'. O proxy relê os arquivos quando eles mudam. Nesse modo o proxy fala HTTP/2 com os navegadores que o oferecem (HTTP/1.1 com o resto), pega o IP do visitante da conexão (um X-Forwarded-For mandado pelo visitante é ignorado ali) e continua respondendo em QLYNE_LISTEN; mantenha a porta 80 no seu servidor web para redirecionar ao HTTPS.
  • HTTPS no proxy com certificado sob demanda: defina QLYNE_TLS_LISTEN, QLYNE_ACME_DIR (uma pasta montada em que o proxy escreve, onde ficam a conta no Let’s Encrypt e os certificados) e QLYNE_ACME_EMAIL, e liste os seus hosts em QLYNE_HOSTS (loja.exemplo,www.loja.exemplo; com QLYNE_SITES, os hosts de cada site). Publique a porta 80 em QLYNE_LISTEN também: é ali que o proxy prova que o host é seu, respondendo /.well-known/acme-challenge/. Na primeira visita a cada host o certificado é pedido com aquela conexão esperando (alguns segundos), depois fica guardado e é renovado aos 60 dias. Host que não é seu não ganha certificado nem gera pedido, então ninguém gasta a sua cota do Let’s Encrypt apontando um DNS para você. Com QLYNE_ACME_URL=staging dá para testar contra o servidor de testes do Let’s Encrypt antes (os navegadores não confiam nesses certificados, mas o fluxo é o mesmo).
  • O seu servidor HTTPS calcula: com o módulo de JA4 do nginx ou um plugin de JA4 do Caddy, mande o valor num cabeçalho e ponha o nome dele em QLYNE_JA4_HEADER. Só faça isso quando o proxy for alcançável apenas por esse servidor; senão qualquer um manda o cabeçalho.

Com o JA4, o seu app recebe Qlyne-JA4, a tela Events mostra o valor ao lado do user agent, as suas regras podem usá-lo (“TLS fingerprint (JA4)”, como starts with t13d1812h1), e a nota de robô soma tls quando o user agent diz ser um Chrome, Firefox ou Safari recente mas a conexão não usa TLS 1.3 ou não oferece HTTP/2, como os scripts. Proxy de empresa que abre o HTTPS para inspecionar também troca a impressão digital, então isso só leva a nota à faixa média e nunca bloqueia sozinho.

Em Firewall, “Your rules”, cada regra é uma lista de condições que precisam valer todas, e o que fazer: bloquear, desafiar, liberar (pulando todas as outras regras, sala de espera inclusive) ou só registrar. As condições olham o caminho, o host, o método, o país, a rede (ASN), o IP ou faixa, o user agent, a impressão digital do TLS (JA4, veja acima) e se o visitante é um robô de busca verificado. Para “ou”, ponha vários valores na mesma condição (BR, PT) ou escreva duas regras. As regras rodam em ordem, depois dos endereços bloqueados e da armadilha para scanner; a primeira que bloqueia, desafia ou libera decide.

Exemplos: bloquear o login para redes de empresas de hospedagem (path starts with /login e ASN is any of AS14061, AS16509); desafiar o checkout fora dos seus países (path matches /checkout* e country is none of BR, PT); bloquear chamada à API sem user agent. O proxy ainda não sabe país e rede: condição sobre eles nunca casa, então country is none of BR não bloqueia ninguém.

Scanners pedem caminhos que nenhum visitante pede: /.env, /.git/config, /phpmyadmin, /wp-login.php num site sem WordPress. Em Bots, “Scanner trap”, escolha os grupos de caminhos e acrescente os seus (um endereço de admin antigo, um link que só robô segue). Quem pede um deles leva um 404 comum, sem sinal de que era armadilha, e fica bloqueado em todas as rotas por 15 minutos, 1 hora ou 24 horas. No IPv6, o bloqueio vale para o /64. Com vários proxies, os outros ficam sabendo em até um minuto. Googlebot, Bingbot e endereços de confiança nunca são bloqueados, e no modo observar a armadilha só conta. Deixe o grupo WordPress desligado se o seu site usa WordPress.

Cabeçalhos de segurança e Content Security Policy

Seção intitulada “Cabeçalhos de segurança e Content Security Policy”

Em Page security, ligue HSTS, X-Content-Type-Options, Referrer-Policy, proteção contra iframe e Permissions-Policy. Eles entram nas suas páginas só quando o app ainda não manda. A nota (de A+ a F) sai das páginas que passaram pelo conector, então conta os cabeçalhos do próprio app também.

“Content Security Policy” (Pro) recebe a sua política e um modo. Em “Report only”, o navegador manda o que a política bloquearia para a própria página com ?__qlyne=csp; o conector recebe e lista em Page security por 7 dias, e nada quebra. Quando a lista só mostrar o esperado, mude para “Enforce”. O conector troca qualquer report-uri da sua política pelo dele.

As requisições que chegam ao site levam headers que o seu código pode usar (desligue em Connector, “Signals to your app”):

HeaderValor
Qlyne-CountryPaís, em duas letras (só no conector Cloudflare; o proxy não manda)
Qlyne-ASNNúmero da rede do visitante (só no conector Cloudflare; o proxy não manda)
Qlyne-Verified-Botgooglebot ou bingbot, confirmado pelo IP
Qlyne-Challenge-Passed1 quando o visitante resolveu o desafio, em qualquer página
Qlyne-Trusted-IP1 para um endereço da lista de confiança
Qlyne-RuleNo modo observar, o que teria acontecido, como would_challenge=/checkout*, would_rate_limit=/api/*
Qlyne-Leaked-Password1 num login cuja senha aparece em vazamentos (proteção de login; vai mesmo com os outros sinais desligados)
Qlyne-Gate-UserO e-mail de quem entrou por um portão de acesso (vai mesmo com os outros sinais desligados)
Qlyne-Bot-ScoreCom a nota de robô ligada, a nota do visitante de 0 a 100
Qlyne-Bot-ReasonsO que subiu a nota, como automation,no_interaction
Qlyne-Reputation1 para endereço que outras contas do Qlyne viram atacando (reputação entre sites)
Qlyne-JA4A impressão digital do TLS, quando o proxy tem (HTTPS no proxy, ou QLYNE_JA4_HEADER)

Headers com esses nomes mandados pelo visitante são sempre apagados. Confie neles só se a aplicação só puder ser alcançada pelo proxy.

Todas as regras de um site também são um arquivo YAML (ou JSON) para guardar no git, autenticado com a chave de API da conta, de Plan and API key (uma chave para todos os sites da conta; o site vai em X-Tenant-ID):

Janela do terminal
# baixar
curl -H "Authorization: Bearer $QLYNE_API_KEY" -H "X-Tenant-ID: sua-conta" \
"https://app.qlyne.com/api/protection?format=yaml" > qlyne.yaml
# conferir sem salvar e depois aplicar (sem o ?dry_run=1)
curl -X PUT -H "Authorization: Bearer $QLYNE_API_KEY" -H "X-Tenant-ID: sua-conta" \
-H "Content-Type: application/yaml" --data-binary @qlyne.yaml \
"https://app.qlyne.com/api/protection?dry_run=1"

O PUT troca as regras: seção que falta no arquivo volta ao padrão, e lista no arquivo substitui a lista inteira. Erro volta como 422 com o campo que falhou (challenge.routes, rules.0.conditions.1.values), e configuração desconhecida também é erro, para um erro de digitação não passar calado. Num CI, rode a chamada com ?dry_run=1 no pull request e a de verdade no merge.

GET /api/metrics, com a mesma chave, devolve as métricas Prometheus só da sua conta (decisões por ação, tamanho das filas e o resto), para o seu Grafana. A configuração de coleta está em Connector, “Rules as code”.

VariávelPadrãoO quê
QLYNE_TENANT, QLYNE_CORE_URLno arquivo baixadoSua conta e o servidor do Qlyne
QLYNE_SECRETnenhumO segredo do conector, do arquivo .env
QLYNE_UPSTREAMhttp://host.docker.internal:3000Sua aplicação, http://host:porta
QLYNE_CLIENT_IPx-forwarded-forDe onde vem o IP do visitante: x-forwarded-for, x-real-ip, cf-connecting-ip ou peer
QLYNE_LISTEN0.0.0.0:8080Endereço dentro do container
QLYNE_TLS_LISTENnenhumHTTPS no próprio proxy, como 0.0.0.0:8443; precisa dos dois abaixo
QLYNE_TLS_CERT, QLYNE_TLS_KEYnenhumCadeia do certificado e chave privada, em PEM; relidos quando mudam
QLYNE_JA4_HEADERnenhumCabeçalho em que o seu servidor HTTPS manda o JA4, como X-JA4
QLYNE_ACME_DIRnenhumCertificado sob demanda: pasta da conta no Let’s Encrypt e dos certificados; com ela, QLYNE_TLS_CERT e QLYNE_TLS_KEY não são necessários
QLYNE_ACME_EMAILnenhumE-mail da conta no Let’s Encrypt (avisos de vencimento); obrigatório com QLYNE_ACME_DIR
QLYNE_ACME_URLLet’s Encryptstaging para o servidor de testes do Let’s Encrypt, ou outra URL de diretório ACME
QLYNE_HOSTSnenhumHosts que ganham certificado, separados por vírgula (com um site só; com QLYNE_SITES, os hosts de cada site)
QLYNE_COMPRESSdesligado1: comprime respostas de texto (gzip, brotli, zstd) que a origem mandou sem comprimir, para quem aceita
QLYNE_HTTP_REDIRECTdesligado1: pedidos na porta HTTP recebem 308 para o HTTPS (o desafio do certificado e /__qlyne/health seguem respondendo ali)
QLYNE_SITESnenhumVários sites num proxy: caminho de um arquivo JSON (abaixo). Com ele, QLYNE_TENANT, QLYNE_SECRET e QLYNE_UPSTREAM são por site
LOG_FORMATjsonjson ou text

Um proxy na frente de várias aplicações, como um Traefik: cada pedido vai para o site cujo host ele traz, com a conta, o segredo, as regras e a origem daquele site. Monte um arquivo JSON e aponte QLYNE_SITES para ele:

[
{"tenant": "loja", "secret": "…", "upstream": "http://loja:3000", "hosts": ["loja.exemplo", "www.loja.exemplo"]},
{"tenant": "blog", "secret": "…", "upstream": "http://blog:8080", "hosts": ["blog.exemplo"]},
{"upstream": "http://api:9000", "hosts": ["api.exemplo"], "deny": ["/internal/*"],
"headers": {"strict-transport-security": "max-age=31536000"}, "remove": ["server"]},
{"redirect": "https://loja.exemplo", "hosts": ["exemplo.com", "www.exemplo.com"]}
]

Qualquer entrada pode levar também deny (caminhos que respondem 404 de fora, * casa qualquer coisa), headers (postos em toda resposta que ainda não os tem) e remove (tirados de toda resposta, como o Server da origem). Três tipos de entrada: com tenant e secret, o site é protegido com as regras daquela conta; só com upstream, o proxy apenas repassa (uma API, uma página de status); com redirect no lugar de upstream, todo pedido recebe 308 para aquele endereço com o mesmo caminho e query (o domínio sem subdomínio, um domínio antigo). O arquivo é relido quando muda, sem reiniciar; um site que não mudou mantém as regras e as contagens. Um host que não está em nenhum site recebe 421 Misdirected Request. Arquivo inválido vai para o log e a lista anterior continua.

docker compose logs qlyne-proxy mostra.

MensagemO que é
the config is not signed by the Qlyne serverO QLYNE_CORE_URL não aponta para o servidor do Qlyne. Nada é barrado até corrigir.
the Qlyne server refused the config (401)O QLYNE_SECRET não é o segredo atual do conector. Nada é barrado até corrigir.
QLYNE_TENANT, QLYNE_CORE_URL or QLYNE_SECRET is not setToda requisição vai direto para a aplicação.
could not reach the application at ...O QLYNE_UPSTREAM está errado ou a aplicação está fora: o proxy responde 502.
could not reach the Qlyne serverO proxy usa as últimas regras que tinha; sem nenhuma, deixa passar.

Para tirar o proxy, aponte o servidor web de volta para a aplicação e rode docker compose down.