Skip to content

Qlyne for Cloudflare

A connector that sits in front of your site, inside your own Cloudflare account, and applies the rules you set in the Qlyne dashboard:

  • a waiting room on the pages you pick (/tickets/*, /checkout);
  • a cap on how many people are on the site at once: visitors go straight in while there is room and wait in line when it is full;
  • its own challenge (invisible proof-of-work, no Google or Cloudflare Turnstile) on the pages you pick, plus an “under attack” mode that challenges every page view;
  • rate limits per page, counted per IP (IPv6 by its /64) or per API key, token or session;
  • blocked and trusted IP addresses, and a rule for Tor exit nodes (challenge or block);
  • AI crawlers blocked by category (training, AI search, assistants), in your robots.txt too;
  • form protection with no captcha: an invisible trap field and a minimum fill time;
  • browsers driven by automation tools (Selenium, Puppeteer, Playwright) refused at the challenge;
  • an “observe only” mode that records what would be blocked without blocking anyone;
  • counters in the dashboard for each of these.

The connector does not call Qlyne on every request. It keeps your rules in memory for 60 seconds, and the waiting room pass and the challenge clearance are signed with a secret that only your connector and Qlyne know, so it checks them on its own. With a cap on people at once, it also asks Qlyne for a slot when someone without a pass arrives. If Qlyne goes down, the connector lets traffic through: protection may fail, your site does not.

This is HTTP-layer (L7) protection. Volumetric network attacks are Cloudflare’s job.

  • The Qlyne free plan covers observe mode, IP addresses, AI crawlers and rate limits. The waiting room, the cap on people at once, the challenge, under attack mode, forms and Tor need the Pro plan.
  • Your site must go through Cloudflare (DNS record proxied, orange cloud).
  • The connector is a Cloudflare Worker. The Workers free plan allows 100,000 requests a day and bursts of up to 1,000 a minute, and every request on the Worker’s routes counts. Route only the pages that need protection, not images or scripts. Busier sites need the Workers paid plan. When the daily limit runs out, each route does what you set for it in Cloudflare: go to your site without the Worker (fail open) or return error 1027 (fail closed).
  1. Open the signup page and pick an account ID, your site’s name and your e-mail.
  2. Open the confirmation link from your e-mail and click “Activate and generate my key”. Save the API key: it is shown only once.
  3. On the same page, click “Set my password” and choose your dashboard password. The link lasts 60 minutes; after that, use “Forgot your password?” on the login page.

Log in and open your site. Each item below is in the site menu:

  1. In Connector, click “Generate secret” and save it. It is shown only once.
  2. Add the pages. * matches anything: /checkout* covers /checkout and /checkout/pay.
    • Bots, challenged routes: pages bots go after, such as login and checkout.
    • Waiting room, “Routes on your site”: the page and the queue (event) it uses. Create the queue under Waiting room first. Its “Target URL” must be on the same site as the protected pages, so people come back to the exact page they were on. To cap how many people are on the site at once, see the section with that name below.
    • Firewall, rate limits: strict (10 per minute), normal (60) or loose (300), counted per IP by default. On an API, or anywhere many people share one address (mobile carriers, offices), count per header (Authorization, X-API-Key) or per session cookie instead. Requests without that header or cookie are counted per IP, and one IP never goes above 10 times the limit, so making up a new key on every request does not get around it.
    • Logins and forms, protected forms: the page with the form and the address it posts to. Use it on forms people type into, such as contact, signup and comments; a one-click form (add to cart) is often sent within 2 seconds and would be refused.
  3. Optionally, list blocked and trusted IP addresses and pick a rule for Tor exit nodes (Firewall), and the AI crawler categories to block (Bots). Trusted addresses (your office, uptime monitors, partners calling your API) skip every rule.
  4. Leave the mode in Connector on “Observe only” for now.

Under Connector, “Install on Cloudflare”, click Download connector. The file already carries your account ID and the Qlyne server address.

From the Cloudflare dashboard, no terminal:

  1. Workers & Pages, Create, Worker: deploy the “Hello World” example.
  2. Edit code, replace everything with the downloaded connector, Deploy.
  3. Settings, Variables and Secrets: add a Secret named QLYNE_SECRET with your connector secret.
  4. Settings, Domains & Routes: add a route for each page to protect, for example shop.example.com/checkout*.

This covers the waiting room, the challenge and under attack mode. Rate limits need Wrangler, because Cloudflare does not let you add them in the dashboard (download wrangler.toml again after updating the connector: version 0.6.0 adds the per-IP ceiling for limits counted per header or cookie):

  1. Also click Download wrangler.toml and put both files in one folder.
  2. Set your routes in wrangler.toml.
  3. Run:
Terminal window
npx wrangler login
npx wrangler secret put QLYNE_SECRET
npx wrangler deploy

Visit a protected page, then check the last 24 hours in the site Overview. Many “would be challenged” on ordinary pages usually means a route is too broad; many “would be rate limited” calls for a looser tier. When the numbers make sense, switch the mode to “Enforce” in Connector.

During an attack, the “Under attack” button challenges every page view without a clearance cookie (the connector picks it up within a minute). API calls are not challenged, so apps and integrations keep working.

Cap how many people are on the site at once

Section titled “Cap how many people are on the site at once”

If your server can only take so many people, open the queue under Waiting room and fill in “Max users on the site at once”, then add a route for that queue in Waiting room, “Routes on your site”. While there are free slots and nobody is waiting, visitors go straight to your site without seeing the waiting room. When the site is full, new visitors wait in line and get in as slots free up, in the order they arrived.

Someone who makes no request for “Free a slot after” seconds (5 minutes by default) gives up the slot, and their pass expires with it. The dashboard shows “X / Y on the site” for the queue.

  • It only works in “Enforce” mode, through the connector.
  • Pick a number your server handles with room to spare. The count is approximate (activity reaches Qlyne every 10 seconds or so), and the release rate only paces people leaving the queue: when the site is empty, a burst of visitors can take every free slot within the same second.
  • One IP address (a /64 for IPv6) gets at most 10 slots this way; anyone else from it waits in line.
  • If Qlyne does not answer within 1.5 seconds, the connector lets visitors in and asks again 10 seconds later.

For each request on the connector’s routes:

  1. Someone coming back from the waiting room with ?qlyne_pass=: the connector checks the pass, keeps it in a cookie for that event and redirects to the same address without the parameter. An invalid pass shows an error page instead of sending the person back to the queue, so a wrong secret never turns into a redirect loop.
  2. Trusted IP address: goes to your site with no rule applied. Blocked IP address, or Tor exit node with the “block” rule: 403 (an “Access blocked” page, or {"code": "ACCESS_BLOCKED"} for API calls).
  3. Address on the shared reputation list, with “Block” (Pro): 403, with the same page or {"code": "ACCESS_BLOCKED"}. With “Challenge”, its page views get the challenge in step 8.
  4. Request matching a managed rule (WAF) in “Block” mode: 403, with the same “Access blocked” page or {"code": "ACCESS_BLOCKED"}. In “Observe only” it goes on and is counted.
  5. With the bot score on (Pro), a medium or high score gets the action chosen for it: slowed down, challenged, 403 or the decoy page. A form or API call to a route that needs the score, from a browser that has not run the script, gets 403 with {"code": "SENSOR_REQUIRED"}.
  6. AI crawler from a blocked category, by user agent: 403. Your /robots.txt is served with a Disallow: / group for those crawlers added at the end.
  7. Page with a rate limit, over the limit: 429.
  8. Challenged page (or under attack mode, or a Tor exit node with the “challenge” rule; page views only), no clearance cookie: the challenge page. API calls get 403 with {"code": "CHALLENGE_REQUIRED"}. Googlebot and Bingbot pass when their IP is in the ranges Google and Bing publish. A browser controlled by automation software does not get the clearance. The challenge comes before the waiting room, so a bot that fails it never takes a place in line or a slot on the site.
  9. Protected form: the page gets the trap field and the time stamp in each POST form. A submission with the trap filled, or sent less than 2 seconds after the page loaded, gets 403 (a “Form not sent” page, or {"code": "FORM_REJECTED"}). Submissions without the stamp, such as forms built by JavaScript or sent as JSON, go through. These pages are sent with Cache-Control: no-store, so every visit gets a fresh stamp.
  10. Page with a waiting room, event active and not over, no valid pass: redirect to the waiting room. API calls (not asking for HTML) get 403 with {"code": "QUEUE_REQUIRED", "waiting_room_url": ...}. With a cap on people at once, the connector first asks Qlyne for a slot; with one, the request goes to your site and the pass is set right away.
  11. Everything else goes to your site.

Static files (scripts, styles, images, fonts) get neither the waiting room, the challenge nor the form check; blocked addresses, AI crawlers and rate limits still apply.

To block AI crawlers, the connector has to run on your whole site (a route like example.com/*), robots.txt included: crawlers fetch every page. On the Workers free plan that uses more of the daily allowance.

Cookies, all HttpOnly; Secure; SameSite=Lax:

CookieWhatLifetime
qlyne_pass_<event>waiting room pass, signed by Qlynethe event’s access token TTL (“Free a slot after” when the queue caps people at once), renewed while the person keeps browsing, up to 2 hours after it was issued
qlyne_clrchallenge clearance, tied to the IP (the /64 for IPv6) and the browser“Clearance” in the dashboard (30 minutes by default)
qlyne_bsbot score, signed by the connector and tied to the IP and the browser (with the bot score on)30 minutes

Under Access, lock a path like /admin* or a whole staging site without touching your app. Two ways in:

  • Password: one shared password, stored only as a hash. Tries are limited per IP.
  • Link by e-mail: the visitor types an address; if it is on your list (an address, or @company.com for everyone there), a link that works once, for 15 minutes, arrives by e-mail. Everyone sees the same “check your e-mail” message, so the page does not tell who has access.

Whoever gets in stays in for the time you choose, through a cookie signed by the connector, and your app gets Qlyne-Gate-User with the e-mail. The gate applies in observe mode too, since it is a lock you set, not a guess. Trusted addresses skip it.

Under Logins and forms, “Login protection”, list the routes your login form (or app) posts to and the names of the username and password fields (user.email reaches into JSON). Forms, multipart and JSON are read.

  • Leaked passwords: the connector computes the password’s SHA-1 and sends Have I Been Pwned only the first 5 characters, shared by hundreds of passwords; the password never leaves the connector. If it is in a known breach, the login reaches your app with Qlyne-Leaked-Password: 1, and your app decides, for example by asking for a new password. If the check is slow or down, the login goes through without the header.
  • Tries per account: counted by the username, from any IP, so one account cannot be guessed from many addresses. Over the limit, the visitor gets a 429.

Under Firewall, “Managed rules”, Qlyne looks for signatures of common attacks in the path, the query string and form or JSON bodies up to 16 KB, after decoding them (twice, for whoever encodes twice to slip through) and without SQL comments: SQL injection (UNION SELECT, 1=1 conditions, stacked statements, time delays, schema reads), cross-site scripting (script tags, event handlers, javascript: links, iframes), path traversal (../, system files, null bytes) and command injection ($(…) with system commands, chained curl or bash, Log4Shell lookups). Bodies in other formats (file uploads) and bodies without a declared size are not read.

Every site starts in “Observe only”: the matches show up in Events as “Would block by managed rules”, with the signature, and your app gets them in Qlyne-Rule. When only attacks show up there, switch to “Block” (Pro; on Free the rules keep observing). A page that sends HTML or code on purpose, like a rich text editor, goes in “Routes the managed rules skip”. The managed rules run after your rules, so a “let through” rule skips them too.

Signatures catch the common, automated attacks; they do not replace validating input and using parameterized queries in your app.

Under Bots, “Bot score”, the connector adds a small script, /__qlyne/s.js, to your HTML pages, served from your own domain. A few seconds after the page opens, after a click or a key, and when the visitor sends a form or leaves, it posts a summary to /__qlyne/s: whether the browser says it is automated, how many languages, plugins, processor cores and touch points it reports, the screen size, whether it has a time zone, and how many times the pointer moved, the visitor clicked, typed, scrolled or touched, and how many of those events a script fired. It sends counts only: never keys, text or where the pointer was.

From that summary, and from whether the request headers match the browser named in the user agent, the connector gives the visitor a score from 0 to 100: low below 30, medium from 30 to 59, high from 60. The score stays 30 minutes in the qlyne_bs cookie, signed by the connector and tied to the IP and the browser. Your app gets it in Qlyne-Bot-Score, with the reasons in Qlyne-Bot-Reasons (like automation,no_interaction), and Events lists medium and high scores with the reasons. A first page view has no score yet: the actions start with the next request.

On Pro, choose what a medium and a high score get: nothing, the request slowed down by 3 seconds, the challenge, the challenge with a click, 403, or the decoy page. The decoy is a page of your site that the bot gets in place of the one it asked for, under the same address, so it keeps scraping something worth nothing. In observe mode these only count.

“Routes that need the score” refuse a form or API call (anything but GET) from a browser that has not run the script, with 403 and {"code": "SENSOR_REQUIRED"}. Use it on what bots call directly, like a checkout or signup API. A visitor with a solved challenge goes through.

“Click to confirm”, under “Challenge”, sets how the challenge page and the form captcha behave: never (the browser does the work alone), when in doubt (a click on “I’m a person” only when the score is not low or the address asked for many challenges) or always. A click fired by a script, or on a computer without the pointer moving first, is refused.

Under Firewall, “Shared reputation”, Qlyne lists the addresses that sites of other accounts saw attacking in the last 24 hours: caught by the scanner trap or the managed rules, with a high bot score, driving an automated browser or refused by the form captcha. An address goes on the list only when at least two accounts report it, so no single account can put someone there. For IPv6 the list holds the /64.

By default a listed address only reaches your app with Qlyne-Reputation: 1. On Pro, challenge its page views (API calls go through, as with Tor exits) or block it with 403; Googlebot and Bingbot are never blocked, and trusted addresses skip it. The list comes with the rules, within a minute.

“Report attacks on this site” (on by default) sends Qlyne only the address of those attacks, kept 24 hours, and never the path, the user agent or which site it was. Turning it off does not stop you from using the list. How this data is handled is in the terms and the privacy policy.

Under Firewall, “Your rules”, each rule is a list of conditions that must all hold, and what to do: block, challenge, let through (skipping every other rule, the waiting room included) or only log. Conditions look at the path, host, method, country, network (ASN), IP or range, user agent, TLS fingerprint (JA4, only with Bot Management), and whether the visitor is a verified search bot. For “or”, put several values in one condition (BR, PT) or write two rules. Rules run in order, after blocked addresses and the scanner trap; the first one that blocks, challenges or lets through decides.

Examples: block the login for networks of hosting companies (path starts with /login and ASN is any of AS14061, AS16509); challenge the checkout outside your countries (path matches /checkout* and country is none of BR, PT); block API calls with no user agent. Country and network come from Cloudflare.

Scanners ask for paths no visitor does: /.env, /.git/config, /phpmyadmin, /wp-login.php on a site without WordPress. Under Bots, “Scanner trap”, pick the groups of paths and add your own (an old admin address, a link only bots follow). Whoever requests one gets a plain 404, with no hint it was a trap, and is blocked on every route for 15 minutes, 1 hour or 24 hours. An IPv6 visitor is blocked by its /64. The Worker that caught it blocks at once; the others learn within a minute. Googlebot, Bingbot and trusted addresses are never blocked, and in observe mode the trap only counts. Leave the WordPress group off if your site runs WordPress.

Security headers and Content Security Policy

Section titled “Security headers and Content Security Policy”

Under Page security, turn on HSTS, X-Content-Type-Options, Referrer-Policy, framing protection and Permissions-Policy. They are added to your pages only when your app does not send them already. The grade (A+ to F) comes from the pages that went through the connector, so it counts your own headers too.

“Content Security Policy” (Pro) takes your policy and a mode. In “Report only”, browsers send what the policy would block to the page itself with ?__qlyne=csp, which the connector receives and lists under Page security for 7 days; nothing breaks. When the list only shows what you expect, switch to “Enforce”. The connector replaces any report-uri in your policy with its own.

Requests that reach your site carry headers your code can use (turn them off in Connector, “Signals to your app”):

HeaderValue
Qlyne-CountryTwo-letter country code
Qlyne-ASNThe visitor’s network number
Qlyne-Verified-Botgooglebot or bingbot, confirmed by IP
Qlyne-Challenge-Passed1 when the visitor solved the challenge, on any page
Qlyne-Trusted-IP1 for an address in your trusted list
Qlyne-RuleIn observe mode, what would have happened, like would_challenge=/checkout*, would_rate_limit=/api/*
Qlyne-Leaked-Password1 on a login whose password is in a known breach (login protection, sent even with the other signals off)
Qlyne-Gate-UserThe e-mail of who signed in at an access gate (sent even with the other signals off)
Qlyne-Bot-ScoreWith the bot score on, the visitor’s score from 0 to 100
Qlyne-Bot-ReasonsWhat raised the score, like automation,no_interaction
Qlyne-Reputation1 for an address other Qlyne accounts saw attacking (shared reputation)
Qlyne-JA4The TLS fingerprint, only on Cloudflare plans with Bot Management (Enterprise)

Headers with these names sent by the visitor are always removed. Trust them only if your origin accepts only Cloudflare’s IP ranges; otherwise someone can reach the origin directly and send the headers themselves.

Every rule of a site is also a YAML (or JSON) file you can keep in git, authenticated with the account API key from Plan and API key (one key for every site of the account; the site goes in X-Tenant-ID):

Terminal window
# download
curl -H "Authorization: Bearer $QLYNE_API_KEY" -H "X-Tenant-ID: your-account" \
"https://app.qlyne.com/api/protection?format=yaml" > qlyne.yaml
# check without saving, then apply (drop ?dry_run=1)
curl -X PUT -H "Authorization: Bearer $QLYNE_API_KEY" -H "X-Tenant-ID: your-account" \
-H "Content-Type: application/yaml" --data-binary @qlyne.yaml \
"https://app.qlyne.com/api/protection?dry_run=1"

PUT replaces the rules: a section missing from the file goes back to its default, and a list in the file replaces the whole list. Errors come back as 422 with the field that failed (challenge.routes, rules.0.conditions.1.values), and an unknown setting is an error too, so a typo does not pass silently. In a CI, run the ?dry_run=1 call on pull requests and the real one on merge.

GET /api/metrics, with the same key, returns the Prometheus metrics of your account only (decisions by action, queue sizes and the rest), for your own Grafana. The scrape config is under Connector, “Rules as code”.

  • The challenge makes bots pay (proof-of-work); it does not stop a determined one. It gets harder in under attack mode and for an address that asks for many in 10 minutes, and a solution sent by a client that only copies a browser’s user agent (without the headers that browser always sends over HTTPS) is refused. The automation check catches browsers driven by Selenium, Puppeteer and Playwright as they come, but a real browser that hides those marks gets through after solving the proof-of-work. There is no browser fingerprinting or behavior analysis yet, and no TLS fingerprinting (on Cloudflare, that is Enterprise only).
  • The pass is not tied to the browser: a copied pass cookie works elsewhere until it expires (at most 2 hours after it was issued). With a cap on people at once, everyone using the copy counts as one person.
  • The cap on people at once can be filled by someone with many IP addresses. A challenge on the same pages makes each slot cost a proof-of-work.
  • AI crawlers are recognized by the user agent they declare. One that pretends to be a browser is treated as a browser, and meets the other rules.
  • Form protection stops the bots that fill every field or submit at once. A bot that loads the page, waits and leaves the trap empty gets through; a challenge on the same page raises the cost.
MessageMeaning
the config is not signed by the Qlyne serverThe connector file or QLYNE_CORE_URL does not match the Qlyne server. Download the connector again; nothing is blocked meanwhile.
the Qlyne server refused the config (401)QLYNE_SECRET is not the current connector secret. Nothing is blocked until you fix it.
QLYNE_SECRET is not set, or this is not the connector file downloaded from the dashboardAdd the secret, or download the connector again. Nothing is blocked.
waiting room pass refusedSomeone came back with an invalid or expired pass. If it happens to everyone, check the secret.
could not reach the Qlyne serverThe connector keeps the last rules it had; with none, it lets traffic through.
could not reach the Qlyne server for an admission, the Qlyne server answered ... to an admission requestQlyne did not give a slot in time. The connector lets visitors in and asks again 10 seconds later.