Pular para o conteúdo

Integration recipes

Este conteúdo não está disponível em sua língua ainda.

Find your infrastructure below and paste the recipe. If you are still choosing between the ways to connect, start at Choose the integration that fits your infrastructure.

IntegrationHow it connectsStatus
NGINXauth_request in the server {} you already haveAvailable
OpenRestyThe NGINX recipe, unchangedAvailable
Nginx Proxy ManagerThe NGINX recipe in “Custom Nginx Configuration”Available
ingress-nginxFour annotations on your IngressAvailable
TraefikforwardAuth middleware, in a YAML file or in Docker labelsAvailable
DokployThe Traefik recipe in the app’s Traefik editorAvailable
CoolifyThe Traefik labels on your resourceAvailable
Caddyforward_auth inside a route blockAvailable
Envoyext_authzComing soon
Cloudflare WorkerA Worker on your domain’s routesAvailable
Next.jsMiddleware in your appAvailable
Hosted by QlyneYour DNS points to QlyneAvailable
Qlyne proxyA container between your web server and your appAvailable
JavaScript tagOne script line in your pagesComing soon

Use this when NGINX, Traefik or Caddy already runs your site: HTTPS, domains, routing. You keep all of it. The Qlyne proxy runs beside it in decision mode, and your web server asks it about each request by sub-request to /__qlyne/decide.

Qlyne answers with the verdict from your rules. A 200 means “go on”: your web server forwards the request to your app with the Qlyne-* headers. Anything else (the challenge page, a block, the waiting room redirect, a rate limit) goes to the visitor as Qlyne sent it. The proxy never forwards anything to your app in this mode.

What works in this mode, and what does not

Section titled “What works in this mode, and what does not”

The sub-request carries neither the request body nor your app’s response. That decides what applies:

WorksNeeds the Qlyne proxy in front
Blocked and trusted IP addresses, countries, Tor, shared reputationWAF on the request body (path and query are still inspected)
AI crawlers, verified bots and impostors, trap pathsForm stamps and the form decoy
Rate limits, the waiting room (redirect), locking a path behind an access gate, your own rulesLeaked password and login checks sent by POST, and opening an access gate with the password or the email
The challenge, in two steps on NGINX (the page, then the solution straight to the proxy)The sensor injected into your HTML, so the bot score only comes from a cookie already there
Qlyne-* headers to your app, with forged copies from the visitor removedThe consent banner, security headers on the response, the page cache and static file cache

The right column does not break your site: those protections do not run. Each rule still follows your plan: see Plans.

Start the proxy as in step 1 of Qlyne proxy, with the docker-compose.yml from Connector › Configuration. Skip step 2 of that page: here your web server asks the proxy instead of forwarding to it. Set these variables:

VariableValue
QLYNE_TENANT, QLYNE_CORE_URLAlready in the file you downloaded
QLYNE_SECRETYour connector secret, in .env
QLYNE_UPSTREAMYour app’s address. Still required, even though the proxy does not forward
QLYNE_DECIDE1 turns on /__qlyne/decide and the routes of the two-step challenge
QLYNE_TRUSTED_PROXIESThe IP or CIDR range your web server connects from. Empty: only loopback may ask. Anyone else gets 404
QLYNE_DECIDE_DENY_STATUSLeave it unset (403). Set 401 only for ingress-nginx

In every recipe, qlyne:8080 is the Qlyne proxy and app:3000 is your app. Replace both with the addresses your web server reaches.

Use it when NGINX already sits in front of your app. Inside the server {} you have:

location / {
auth_request /__qlyne/decide;
auth_request_set $qlyne_cookie $upstream_http_set_cookie;
auth_request_set $qlyne_challenge_passed $upstream_http_qlyne_challenge_passed; # one per Qlyne-* your app uses
add_header Set-Cookie $qlyne_cookie always;
error_page 403 500 = @qlyne_page;
proxy_set_header Qlyne-Challenge-Passed $qlyne_challenge_passed;
proxy_pass http://app:3000; # what you already had
}
location = /__qlyne/decide {
internal;
proxy_pass http://qlyne:8080/__qlyne/decide;
proxy_pass_request_body off; proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri; proxy_set_header X-Original-Method $request_method;
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;
}
location @qlyne_page {
rewrite ^ /__qlyne/page? break; proxy_pass http://qlyne:8080;
proxy_pass_request_body off; proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri; 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 Cookie "$http_cookie; $qlyne_cookie";
}
location /__qlyne/ { proxy_pass http://qlyne:8080; 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; }

What each part does:

  • auth_request /__qlyne/decide sends the sub-request, without a body, with the original request described in X-Original-URI and X-Original-Method. internal hides that route from the outside.
  • auth_request_set keeps one header from the verdict per variable. $upstream_http_set_cookie carries the waiting room pass back to the visitor. Add one line per Qlyne-* header your app reads, and pass it on with proxy_set_header: Qlyne-Bot-Score, Qlyne-Bot-Reasons, Qlyne-Challenge-Passed, Qlyne-Trusted-Ip, Qlyne-Verified-Bot, Qlyne-Reputation, Qlyne-Reputation-Score, Qlyne-Rule, Qlyne-Gate-User, Qlyne-Leaked-Password, Qlyne-Decoy. proxy_set_header replaces whatever the visitor sent under the same name, so nobody forges a Qlyne-* header.
  • error_page 403 500 = @qlyne_page is the first of the two steps. auth_request drops the response body and only understands 2xx, 401 and 403; any other status becomes 500. @qlyne_page asks the proxy for /__qlyne/page, which sends the real answer for the original address: the challenge or block page with 403, or the real 302, 429 or 303.
  • location /__qlyne/ without auth_request is the second step. The challenge page posts its solution to /__qlyne/verify with a body, which a sub-request cannot carry.
  • X-Forwarded-For is the same in the three routes, because the challenge is tied to the visitor’s IP.

Qlyne’s cookies are Secure, so the site has to be on HTTPS for browsers to keep them.

Use the NGINX recipe as it is. auth_request comes with the NGINX inside OpenResty; no Lua needed.

Open the host, tab Advanced, and paste into “Custom Nginx Configuration”. Nginx Proxy Manager writes location / on its own, so the verdict goes at server level and location / inherits it:

auth_request /__qlyne/decide;
auth_request_set $qlyne_cookie $upstream_http_set_cookie;
auth_request_set $qlyne_challenge_passed $upstream_http_qlyne_challenge_passed;
add_header Set-Cookie $qlyne_cookie always;
error_page 403 500 = @qlyne_page;

Below these lines, in the same field, add the three Qlyne routes from the NGINX recipe (= /__qlyne/decide, @qlyne_page and /__qlyne/), with auth_request off; inside /__qlyne/. Replace qlyne:8080 with an address the Nginx Proxy Manager container reaches: the host IP, or the container name on the same Docker network.

The location / that Nginx Proxy Manager writes has its own proxy_set_header lines, which override the ones at server level. To send Qlyne-* headers to your app, create the / location in Custom Locations, pointing to your app, and put the proxy_set_header Qlyne-... lines in its configuration field.

Add these annotations to your app’s Ingress:

nginx.ingress.kubernetes.io/auth-url: "http://qlyne.<namespace>.svc.cluster.local:8080/__qlyne/decide"
nginx.ingress.kubernetes.io/auth-method: "GET"
nginx.ingress.kubernetes.io/auth-response-headers: "Qlyne-Challenge-Passed, Qlyne-Bot-Score, Qlyne-Trusted-Ip"
nginx.ingress.kubernetes.io/auth-signin: "https://$host/__qlyne/page?to=$escaped_request_uri"

Then three more things:

  • Start the proxy with QLYNE_DECIDE_DENY_STATUS=401. ingress-nginx only sends the visitor to auth-signin on a 401, so every verdict that does not go on leaves /__qlyne/decide as 401. auth-signin takes the browser to /__qlyne/page, which answers with the real status: the challenge or block page, the waiting room 302, the 429.
  • Set QLYNE_TRUSTED_PROXIES to the CIDR of the controller pods.
  • Add a second Ingress for the path /__qlyne/, pointing to the Qlyne proxy Service, without the auth annotations.

ingress-nginx describes the original request in X-Original-URL, and the proxy reads path, query, host and scheme from it. No auth-snippet needed.

Use it with Traefik v3, with the file provider or the Docker provider. Two pieces make it work: the forwardAuth middleware on your app’s router, and a router for /__qlyne/ without the middleware, with a higher priority. The second one carries the challenge solution, which has a body.

In a dynamic configuration file (replace the host, the app’s port and the pass cookies):

http:
middlewares:
qlyne:
forwardAuth:
address: "http://qlyne:8080/__qlyne/decide"
trustForwardHeader: true
authResponseHeadersRegex: "^Qlyne-"
addAuthCookiesToResponse: [qlyne_pass_cap] # one qlyne_pass_<event_key> per queue event
routers:
qlyne-direct:
rule: "Host(`loja.exemplo.com`) && PathPrefix(`/__qlyne/`)"
priority: 1000
service: qlyne
app:
rule: "Host(`loja.exemplo.com`)"
middlewares: [qlyne]
service: app
services:
qlyne: { loadBalancer: { servers: [{ url: "http://qlyne:8080" }] } }
app: { loadBalancer: { servers: [{ url: "http://app:3000" }] } }

Your real routers already have entryPoints and tls; give qlyne-direct the same ones as app. addAuthCookiesToResponse lists the cookies the 200 must hand to the visitor: one qlyne_pass_<event_key> per waiting room queue. The challenge and clearance cookies need nothing, because Traefik passes those responses through whole.

In Docker labels, on your app’s service:

- traefik.http.routers.app.middlewares=qlyne
- traefik.http.middlewares.qlyne.forwardauth.address=http://qlyne:8080/__qlyne/decide
- traefik.http.middlewares.qlyne.forwardauth.trustforwardheader=true
- traefik.http.middlewares.qlyne.forwardauth.authresponseheadersregex=^Qlyne-
- traefik.http.middlewares.qlyne.forwardauth.addauthcookiestoresponse=qlyne_pass_cap

And on the qlyne service, which runs with QLYNE_DECIDE=1 and QLYNE_TRUSTED_PROXIES set to the range of Traefik’s Docker network:

- traefik.http.routers.qlyne-direct.rule=Host(`loja.exemplo.com`) && PathPrefix(`/__qlyne/`)
- traefik.http.routers.qlyne-direct.priority=1000
- traefik.http.services.qlyne.loadbalancer.server.port=8080

Traefik, your app and qlyne have to share a Docker network, because the middleware and the service are reached by name. Keep the entrypoints and tls.certresolver labels your routers already use on qlyne-direct as well.

The domain you add to an app becomes its own Traefik file, shown in Advanced, Traefik. Add the qlyne middleware there, put middlewares: [qlyne] on the router Dokploy wrote, and create the qlyne-direct router with the same entryPoints and tls as that router.

Run qlyne as a Compose service in the same project, or as a Docker app, on the dokploy-network network. QLYNE_TRUSTED_PROXIES is that network’s range (docker network inspect dokploy-network). In a Compose project, the labels above go straight into the file.

The labels Coolify writes are in the resource’s label editor. Add the middleware labels to your app’s service. Run qlyne as a Docker Compose resource on the coolify network, with the qlyne-direct router labels.

Use it with Caddy 2. The whole recipe:

loja.exemplo.com {
route {
reverse_proxy /__qlyne/* qlyne:8080
forward_auth qlyne:8080 {
uri /__qlyne/decide
copy_headers Qlyne-Bot-Score Qlyne-Bot-Reasons Qlyne-Challenge-Passed Qlyne-Trusted-Ip Qlyne-Verified-Bot Qlyne-Reputation Qlyne-Reputation-Score Qlyne-Rule Qlyne-Gate-User Qlyne-Leaked-Password Qlyne-Decoy
}
@pass expression `{http.reverse_proxy.header.Set-Cookie} != ""`
header @pass +Set-Cookie {http.reverse_proxy.header.Set-Cookie}
reverse_proxy app:3000
}
}

Why each line is there:

  • route keeps the order as written. Outside it, Caddy reorders the directives and forward_auth would run before the direct route.
  • reverse_proxy /__qlyne/* comes before forward_auth, because the challenge solution reaches /__qlyne/verify with a body.
  • copy_headers names each header. The wildcard Qlyne-* copies nothing. With the names, each one reaches your app with the verdict’s value, and a copy sent by the visitor is removed.
  • header @pass +Set-Cookie hands the waiting room pass to the visitor. forward_auth alone would only add it to the request your app receives.

On the proxy, set QLYNE_TRUSTED_PROXIES to Caddy’s IP or range. With a domain, Caddy serves HTTPS on its own, which Qlyne’s Secure cookies need.

Coming soon, through Envoy’s ext_authz filter.

WayWhen to use itWhat it covers
Cloudflare WorkerYour domain is on CloudflareAlmost everything (details on the Worker page); runs on Cloudflare’s edge
Next.jsA Next.js app, on Vercel or elsewhereWhat does not depend on your app’s response
Hosted by QlyneShared hosting, or nowhere to run anythingEverything, HTTPS included; traffic passes through Qlyne
Qlyne proxyYour own server, with the proxy between your web server and your appEverything, the right column of the table above included

Coming soon: one script line in your pages that scores each visitor. With decision mode, it brings back the bot score that the sub-request cannot inject.

Switch the site to “Enforce” in Connector › Configuration first. In “Observe only”, Qlyne counts what it would do and lets everyone through. Then, replacing loja.exemplo.com with your site:

Terminal window
# 1. From an address in your blocked list: 403
curl -s -o /dev/null -w '%{http_code}\n' https://loja.exemplo.com/produtos
# 2. A challenged route, with no cookie: the challenge page
curl -s https://loja.exemplo.com/checkout | grep -o '<title>[^<]*'
# 3. A page with no rule: 200 from your app
curl -s -o /dev/null -w '%{http_code}\n' https://loja.exemplo.com/produtos

The first one needs a machine whose IP is in IP rules, in the blocked list. The second prints <title>Checking your browser for a route in “Challenged routes” (Pro; see Bot Management). Open that route in a browser too: the page solves itself and opens yours.

Every visitor has the same IP, or the wrong one

Section titled “Every visitor has the same IP, or the wrong one”

Rate limits hit everyone at once, or the blocked list never matches. A CDN or load balancer sits in front of your web server, so the last item of X-Forwarded-For is its address.

Put its ranges in QLYNE_TRUSTED_PROXIES, next to your web server’s. The proxy skips those items from right to left and judges the first address outside the list. Traefik and Caddy also have to trust that CDN, or they drop the header: forwardedHeaders.trustedIPs on the Traefik entry point, trusted_proxies in Caddy. In NGINX, set_real_ip_from with real_ip_header still works; never with 0.0.0.0/0.

The solution is not reaching the proxy. /__qlyne/ has to go straight to the Qlyne proxy, outside the sub-request: NGINX location /__qlyne/ without auth_request (auth_request off; on Nginx Proxy Manager), the Traefik qlyne-direct router with priority 1000 and no middleware, Caddy’s reverse_proxy /__qlyne/* before forward_auth, the second Ingress on ingress-nginx. Without it, the solution gets 403 with DIRECT_ROUTE_REQUIRED.

Two other causes: on NGINX, a different X-Forwarded-For in one of the three routes (the challenge is tied to the IP); and a site on plain HTTP, where the browser drops Qlyne’s Secure cookies.

The waiting room or a rate limit answers 500 on NGINX

Section titled “The waiting room or a rate limit answers 500 on NGINX”

auth_request only passes 2xx, 401 and 403. The waiting room 302, the 429 and the gate’s 303 turn into 500, and the challenge 403 arrives empty. Add error_page 403 500 = @qlyne_page; and the @qlyne_page route from the recipe. On ingress-nginx, the fix is QLYNE_DECIDE_DENY_STATUS=401 with auth-signin.

The proxy only answers addresses in QLYNE_TRUSTED_PROXIES, or loopback when it is empty. Check that your web server’s address is in the list and that QLYNE_DECIDE=1 is set.