testcookie-nginx-module challenges bots with a cookie they must bring back
simple robot mitigation module using cookie based challenge/response technique. Not supported any more.
At a glance
- What is it?
- testcookie-nginx-module is an nginx module that sets a challenge cookie, redirects the client, and only lets the request through when the cookie comes back correctly, a technique that filters simple robots and is no longer supported by its author.
- Who is it for?
- testcookie-nginx-module fits an operator who runs nginx in front of an origin that is being crawled by unsophisticated robots and wants to drop the trivial ones at the edge with a cookie challenge, and it is cheap to try because the whole mechanism is a handful of directives. It is not a defence against a distributed denial of service, since it filters by client behaviour rather than by volume, and the project's own description says it is not supported any more.
- Can I use it commercially?
- Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
- Is it still maintained?
- Yes. The repository last received commits 159 days ago.
- What is it written in?
- Mainly C, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A cookie challenge, and the three ways to serve it
testcookie-nginx-module is a simple robot mitigation module built on a cookie based challenge and response technique, and the entire mechanism is a request cycle you can describe in three steps. The server sets a challenge cookie, sends the client somewhere else, and on the following request checks whether the cookie came back with the value the server expected. A client that handles cookies and follows the redirect passes. A simple script that fetches a URL once does not.
The challenge cookie can be set in three ways, and the choice is mostly about how much work the client has to do. The first is a Set-Cookie header plus a 302 or 307 HTTP Location redirect. The second is a Set-Cookie header plus an HTML meta refresh redirect. The third is a custom template where JavaScript can be used.
The optional encryption path is the interesting part. The challenge cookie value can be encrypted with AES-128 in CBC mode using a custom or random key and initialisation vector, and then decrypted on the client side with JavaScript. The stated purpose is to prevent automatic parsing of the cookie, so a scraper that has learned to read a predictable cookie format cannot simply copy the value and reuse it.
The three directives you have to set
The module needs three required directives and a lot of optional ones, and the required three are worth isolating because the rest of the configuration is decoration.
The testcookie directive enables the module and takes on, off or var, with off as the default. The on and off values are the obvious ones, and var is the interesting third: it does not intercept requests at all, it only sets the module variables. That is what you use when you want to build the logic yourself in an if block rather than let the module redirect.
The testcookie_secret directive is required, and it is the secret string used in the challenge cookie computation. It should be 32 bytes or more. The documentation is explicit about one trap: setting it to the word random causes a new secret to be generated on every server restart, which is not recommended, because all cookies signed with the previous key become invalid and every legitimate user is challenged again. A long static secret is what you want.
The testcookie_session directive is also required, and it sets the input to the challenge generation function. The choices are the client IP address, or the IP address combined with the User-Agent. The second is stronger, since it ties the challenge to the client fingerprint, but it also means a user agent change invalidates the cookie.
Redirect limits, fallbacks and the 403 default
The module has to decide what to do with a client that keeps failing the challenge, and here the directives are unusually concrete.
The testcookie_max_attempts directive sets the maximum number of redirects before the user is sent to a fallback URL, and the documentation notes the constraint that comes from RFC1945, which says it cannot be more than 5. The default is 5. Setting it to 0 means the server will try to set the cookie infinitely, and the comment notes that in practice the browser will show an error page.
The testcookie_fallback directive sets that fallback URL, and nginx scripting variables can be used in it. If no fallback is configured, the client gets a 403 after the maximum attempts are reached. That default is the sane one for a security module, since a silent 403 tells a scraper nothing about why it was stopped.
The testcookie_arg directive sets the name of a GET parameter used for the cookie setting attempt computation, and it is also required in practice even though the documented default is none. The note is blunt: if it is not set, the server will try to set the cookie infinitely, since there is no way to vary the attempt count without a parameter to read.
The testcookie_whitelist directive takes a network list and skips the test for those addresses, and the documentation says to add search engine networks there. The current implementation is IPv4 CIDR only, which is a real limitation for an operator whose traffic is mostly over IPv6.
Cookie attributes, including the SameSite default that allows everything
The cookie the module sets is configurable in the usual ways, and one default deserves attention.
The name is set by testcookie_name and defaults to TCK. The domain is set by testcookie_domain, with the default being none, set by the browser. The path is set by testcookie_path and defaults to /, and the documentation notes it is useful if you plan to use different keys for different locations. The expiry is set by testcookie_expires and defaults to 31 Dec 2037 23:55:55 GMT, which is a long-lived cookie by design, since the point is to challenge a client once rather than on every request.
The SameSite attribute is the one to read twice. The testcookie_samesite directive accepts Lax, Strict or None, and the default is None, which the documentation spells out as cookies being sent in all contexts, that is, cross-origin sending is allowed. That is the permissive choice, and it is a deliberate one for a module whose whole job is to have the cookie come back. The consequence is that if you place the module behind a site you do not control the cookie namespace, you are setting a permissive cookie on your own domain, so scope the domain and path accordingly.
There is also a P3P policy directive, testcookie_p3p, which sets a legacy policy header and defaults to none.
The custom template path, and the variables it exposes
The template route is where the module gets flexible, and the variables it exposes are what you build on.
The testcookie_refresh_template directive replaces the simple meta refresh with custom HTML, and you have to set the cookie yourself from the template. The testcookie_redirect_via_refresh directive has to be enabled for that to work, and the testcookie_refresh_status directive controls the HTTP status code used when the HTML is served, defaulting to 200.
The variables available in the template are the interesting part, because they are the module's state. The documentation lists them like this:
$testcookie_nexturl - URL the client should be redirected to, if max_attempts exceeded *testcookie_fallback* value will be here
$testcookie_got - cookie value received from client, empty if no cookie or it does not match format
$testcookie_set - correct cookie value we're expecting from client
$testcookie_ok - user passed test (1 - passed, 0 - not passed) Note: changed from "yes"/"no" in v1.10$testcookie_nexturl is the URL the client should be redirected to, and if the maximum attempts are exceeded the testcookie_fallback value is placed there. $testcookie_got is the cookie value received from the client, empty if there is no cookie or it does not match the format. $testcookie_set is the correct cookie value the module is expecting. And $testcookie_ok says whether the user passed the test, with 1 for passed and 0 for not passed, and the documentation notes that this changed from yes and no in version 1.10.
When the encryption option is enabled there are three more. $testcookie_enc_key is the encryption key, $testcookie_enc_iv the initialisation vector, and $testcookie_enc_sec the encrypted cookie value, each given as 32 hex digits.
Having the expected value and the received value side by side is what makes a custom template workable, since a template can compare them in JavaScript and decide what to do without a round trip to the server.
Bypasses and the request-shaping switches
A module that challenges every request needs ways to step out of the way, and there are two of them.
The testcookie_whitelist directive handles the network side, skipping the test for listed addresses, and the documentation points at search engine networks as the case to cover. It is a blunt instrument, since a whole CIDR is trusted or not trusted.
The testcookie_pass directive is the finer-grained one. It sets the name of a variable to test when the cookie check should be bypassed, and if that variable's value is set to 1 during the request the cookie check is not performed. The documentation says it can be used for more complex whitelisting, and in practice that means you can write your own condition in nginx, for example a check on a header your API clients send, and set the variable in a map or an if block before the module runs.
Three more switches shape which requests are touched. testcookie_get_only processes only GET requests, and POST requests are bypassed, which is a sensible default for a module protecting a site rather than an API. testcookie_deny_keepalive closes the connection right after setting the cookie, and the reasoning given is that there is no reason to keep connections with bots. And testcookie_https_location redirects the client to the https protocol after the cookie is set, which also affects the nexturl variable, and it is described as useful when SSL is terminated by a third party.
Where this module stops, and why it is not a DDoS defence
The honest limitations are the same ones the project states.
First, the description says it is not supported any more. The last push to the repository was on 2026-05-18, there are no GitHub releases, and the repository is not archived, so the code is there to read and build, but nobody is promising to fix it against a new nginx.
Second, and more important, this filters by behaviour, not by volume. A bot that stores cookies and follows redirects passes. A distributed flood from many hosts that behave like browsers passes. The module reduces the number of trivial requests that reach your origin, and that is genuinely useful, but it is a filter for scrapers rather than a mitigation for a denial of service, which is what the repository topics suggest at a glance and what the design cannot deliver.
Third, the challenge costs real browsers something. Every new visitor is redirected at least once before they see the page, and a client that does not follow a redirect, does not store cookies, or strips them will be challenged repeatedly until it hits the attempt limit and receives a 403. That is a real cost on a site with an audience of scripts, which is most modern front-end stacks.
Fourth, the IPv4-only whitelist and the permissive SameSite default are the two configuration details most likely to cause a surprise, and both are documented, so read them before you turn the module on for a site that matters.
Editorial conclusion
testcookie-nginx-module fits an operator who runs nginx in front of an origin that is being crawled by unsophisticated robots and wants to drop the trivial ones at the edge with a cookie challenge, and it is cheap to try because the whole mechanism is a handful of directives. It is not a defence against a distributed denial of service, since it filters by client behaviour rather than by volume, and the project's own description says it is not supported any more. Before you deploy it, check three things: that you have a stable secret of at least 32 bytes, because a random secret is regenerated on every restart and invalidates every cookie, that the whitelist contains the search engine networks you actually want, since the current implementation is IPv4 CIDR only, and that your client population can execute the redirect, because a headless client that does not follow the challenge will look exactly like a bot. The last push to the repository was on 2026-05-18 and the project publishes no GitHub releases.
Frequently asked questions
How does testcookie-nginx-module filter robots?
It sets a challenge cookie and redirects the client, then checks on the next request that the cookie came back with the value the server expected. A client that handles cookies and follows the redirect passes, and a simple script that fetches a URL once does not.
What configuration does testcookie-nginx-module require?
Three directives are required: testcookie to enable the module, testcookie_secret with a secret of 32 bytes or more, and testcookie_session to set the challenge input, either the client IP or the IP combined with the User-Agent. Setting the secret to the word random regenerates it on every restart and invalidates all cookies.
What happens when a client fails the cookie challenge repeatedly?
After the number of redirects set by testcookie_max_attempts, which defaults to 5 and cannot be more than 5 under RFC1945, the client is sent to the URL in testcookie_fallback, or receives a 403 if no fallback is configured.
Can the challenge cookie be encrypted?
Yes. The cookie value can be encrypted with AES-128 in CBC mode using a custom or random key and initialisation vector, and then decrypted on the client side with JavaScript, which the README says prevents automatic parsing of the cookie.
Is testcookie-nginx-module still supported?
The project description states that it is not supported any more. The last push to the repository was on 2026-05-18, the repository is not archived, and there are no GitHub releases, so the code is available to build but is not being maintained.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/kyprizel-testcookie-nginx-module)Was this useful?