yum upgrades for production use, this is the repository for you.
Active subscription is required.
The NGINX captcha module lets you generate and validate CAPTCHA images directly at the web server level. Instead of relying on external services like Google reCAPTCHA or hCaptcha, this module creates CAPTCHA challenges server-side using the GD graphics library. It validates user responses through MD5-hashed cookies — all within NGINX, with zero external API calls.
Most web applications depend on third-party CAPTCHA providers to stop bots. However, these services introduce external dependencies, privacy concerns, and additional latency. The NGINX captcha module eliminates all three by keeping everything local.
Why Use the NGINX Captcha Module?
There are several compelling reasons to generate CAPTCHA images directly in NGINX rather than calling an external service:
- No external dependencies — Your CAPTCHA system works even when third-party services are down or unreachable.
- Privacy-preserving — No user data leaves your server. This matters for GDPR compliance.
- Lower latency — The image is generated locally. There is no round-trip to an external API.
- Full control — You control the difficulty, appearance, expiration, and character set.
- Lightweight — Image generation takes microseconds thanks to the GD library.
The module is particularly useful for protecting login forms, registration pages, and other endpoints targeted by automated attacks. If you are already using NGINX for security headers, this module adds another layer of defense.
How the NGINX Captcha Module Works
The module operates in two phases: generation and validation.
Generation Phase
When a user visits the CAPTCHA endpoint (e.g., /captcha), the module performs these steps:
- Generates a random text string from a configurable character set
- Computes an MD5 hash:
MD5(secret + code + csrf_token) - Sets the hash as an HTTP cookie (e.g.,
Set-Cookie: Captcha=<hash>; Max-Age=600) - Renders the text as a PNG image with visual distortion
- Returns the PNG image to the browser
The module uses the GD graphics library with FreeType font rendering. It supports fontconfig, so you can reference fonts by name (e.g., "DejaVu Sans") or by full file path.
Validation Phase
When the user submits a form with their CAPTCHA answer, NGINX validates it:
- Extracts the user’s CAPTCHA input and CSRF token from the form data
- Computes the same MD5 hash:
MD5(secret + user_input + csrf_token) - Compares this hash against the cookie value set during generation
- If the hashes match, the user entered the correct code
This approach is stateless. NGINX does not store CAPTCHA codes in memory or a database. The cookie itself serves as the verification token.
CSRF Protection
The CSRF token prevents replay attacks. Without it, an attacker could reuse a valid CAPTCHA cookie across sessions. The module reads the CSRF token from either a cookie named csrf or a query parameter named csrf.
Installation
The NGINX captcha module requires two companion modules:
- nginx-module-form-input — Parses POST form data into NGINX variables
- nginx-module-set-misc — Provides
set_md5for hash computation
All dependencies install automatically.
RHEL, CentOS, AlmaLinux, Rocky Linux
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-captcha
After installation, load all required modules in /etc/nginx/nginx.conf. Add these lines before the events block. The order matters — NDK must load first:
load_module modules/ndk_http_module.so;
load_module modules/ngx_http_set_misc_module.so;
load_module modules/ngx_http_form_input_module.so;
load_module modules/ngx_http_captcha_module.so;
Verify the configuration:
sudo nginx -t
Debian and Ubuntu
First, set up the GetPageSpeed APT repository, then install:
sudo apt-get update
sudo apt-get install nginx-module-captcha
On Debian/Ubuntu, the package handles module loading automatically. No
load_moduledirective is needed.
Module Package Pages
- RPM packages: nginx-module-captcha
- APT packages: nginx-module-captcha
Configuration
Complete Working Example
Here is a complete configuration for CAPTCHA generation and validation on a login form:
server {
listen 80;
server_name example.com;
# CAPTCHA image generation endpoint
location = /captcha {
captcha;
captcha_secret "change_this_to_a_strong_random_string";
captcha_font "/usr/share/fonts/dejavu-sans-fonts/DejaVuSans.ttf";
captcha_width 200;
captcha_height 60;
captcha_length 5;
captcha_size 30;
captcha_expire 600;
captcha_case off;
captcha_line 15;
captcha_star 120;
}
# Login form validation
location = /login {
# Extract form fields
set_form_input $csrf_form csrf;
set_unescape_uri $csrf_unescape $csrf_form;
set_form_input $captcha_form captcha;
set_unescape_uri $captcha_unescape $captcha_form;
# Compute expected hash
set_md5 $captcha_md5 "change_this_to_a_strong_random_string${captcha_unescape}${csrf_unescape}";
# Compare with cookie set during generation
if ($captcha_md5 != $cookie_Captcha) {
return 403;
}
# CAPTCHA verified — pass to backend
proxy_pass http://backend;
}
}
Important: The captcha_secret value must match the secret string used in set_md5. If they differ, validation always fails.
Minimal Configuration
The simplest CAPTCHA setup requires only the captcha directive and a secret:
location = /captcha {
captcha;
captcha_secret "your_secret_here";
captcha_font "/usr/share/fonts/dejavu-sans-fonts/DejaVuSans.ttf";
}
You must set captcha_font to a font file that exists on your system. The compiled-in default path may not exist on your distribution. Since the module enables fontconfig, you can also use a font name:
captcha_font "DejaVu Sans";
Directive Reference
All directives except captcha can appear in http, server, or location context. The captcha directive is only valid in location context.
captcha
Enables CAPTCHA image generation for the current location.
Syntax: captcha;
Context: location
This directive takes no arguments. It registers the content handler, which responds to GET and HEAD requests with a CAPTCHA PNG image. POST requests return HTTP 405.
captcha_secret
Sets the secret key used in the MD5 hash computation.
Syntax: captcha_secret <string>;
Default: none
Context: http, server, location
This is the most critical security parameter. The directive supports NGINX variables for dynamic secrets:
captcha_secret "${server_name}_my_secret_key";
Security warning: If no secret is configured, the module still generates images but uses an empty string as the secret. This makes the CAPTCHA hash trivially forgeable — an attacker only needs to compute MD5("" + code + csrf). Always configure a strong, unique secret.
captcha_font
Specifies the TrueType font for rendering CAPTCHA text.
Syntax: captcha_font <path_or_name>;
Default: /usr/local/share/fonts/NimbusSans-Regular.ttf
Context: http, server, location
You can specify a full file path or a fontconfig name:
captcha_font "/usr/share/fonts/dejavu-sans-fonts/DejaVuSans.ttf";
captcha_font "DejaVu Sans";
Common font paths on RHEL-based systems:
| Font Package | Path |
|---|---|
| dejavu-sans-fonts | /usr/share/fonts/dejavu-sans-fonts/DejaVuSans.ttf |
| dejavu-sans-mono-fonts | /usr/share/fonts/dejavu-sans-mono-fonts/DejaVuSansMono.ttf |
captcha_width
Sets the width of the CAPTCHA image in pixels.
Syntax: captcha_width <pixels>;
Default: 130
Context: http, server, location
captcha_height
Sets the height of the CAPTCHA image in pixels.
Syntax: captcha_height <pixels>;
Default: 30
Context: http, server, location
captcha_length
Sets the number of characters in the CAPTCHA text.
Syntax: captcha_length <number>;
Default: 4
Context: http, server, location
A length of 4-6 characters balances security and usability.
captcha_size
Sets the font size in pixels.
Syntax: captcha_size <pixels>;
Default: 20
Context: http, server, location
The font size must not exceed the image height. NGINX rejects the config with captcha size is too large if it does.
captcha_expire
Sets the CAPTCHA cookie expiration in seconds.
Syntax: captcha_expire <seconds>;
Default: 300
Context: http, server, location
Shorter expirations limit the window for brute-force attempts. The default of 300 seconds (5 minutes) gives users enough time to complete a form.
captcha_case
Controls case sensitivity of the CAPTCHA validation.
Syntax: captcha_case on|off;
Default: on (case-insensitive)
Context: http, server, location
When on, the module lowercases the code before hashing. This means users can enter text in any case.
Important caveat: Case conversion only happens on the generation side. Your validation config must also lowercase the user’s input. Since set-misc lacks a lowercase function, use captcha_case off for simpler deployments:
captcha_case off;
captcha_charset
Defines the characters used to generate CAPTCHA text.
Syntax: captcha_charset <string>;
Default: abcdefghkmnprstuvwxyzABCDEFGHKMNPRSTUVWXYZ23456789
Context: http, server, location
The default charset excludes easily confused characters: l, i, j, o, q, 0, and 1. This reduces user frustration when reading distorted text.
For digits-only CAPTCHA:
captcha_charset "0123456789";
For lowercase-only (pair with captcha_case off):
captcha_charset "abcdefghkmnprstuvwxyz23456789";
captcha_case off;
captcha_name
Sets the CAPTCHA cookie name.
Syntax: captcha_name <string>;
Default: Captcha
Context: http, server, location
If changed, update the validation config accordingly. For example, captcha_name "MyCaptcha"; requires comparing against $cookie_MyCaptcha.
captcha_csrf
Sets the CSRF parameter name.
Syntax: captcha_csrf <string>;
Default: csrf
Context: http, server, location
The module looks for this token first in a cookie, then in a query parameter. Keep the default value csrf — custom values have a known issue in version 0.0.1.
captcha_line
Sets the number of random distortion lines.
Syntax: captcha_line <number>;
Default: 10
Context: http, server, location
More lines increase OCR difficulty but reduce human readability. Values between 10 and 20 work well.
captcha_star
Sets the number of background asterisk decorations.
Syntax: captcha_star <number>;
Default: 100
Context: http, server, location
Asterisks add visual noise that confuses automated solvers. They render in light colors to avoid obscuring the main text.
captcha_level
Sets the PNG compression level.
Syntax: captcha_level <number>;
Default: -1 (GD library default)
Context: http, server, location
Valid range: -1 to 9. Higher values reduce bandwidth at the cost of CPU time.
HTML Form Integration
To use the NGINX captcha module with a real application, you need an HTML form. The form displays the CAPTCHA image, collects the user’s answer, and includes a CSRF token.
Here is an example login form:
<form action="/login" method="POST">
<input type="hidden" name="csrf" id="csrf-token" />
<label for="username">Username:</label>
<input type="text" name="username" required />
<label for="password">Password:</label>
<input type="password" name="password" required />
<label for="captcha">Enter the code shown below:</label>
<img src="/captcha" id="captcha-image" alt="CAPTCHA" />
<button type="button" onclick="refreshCaptcha()">Refresh</button>
<input type="text" name="captcha" required autocomplete="off" />
<button type="submit">Login</button>
</form>
<script>
function refreshCaptcha() {
var csrf = Math.random().toString(36).substring(2);
document.getElementById('csrf-token').value = csrf;
document.cookie = 'csrf=' + csrf + '; path=/; SameSite=Strict';
var img = document.getElementById('captcha-image');
img.src = '/captcha?csrf=' + csrf + '&t=' + Date.now();
}
refreshCaptcha();
</script>
The JavaScript generates a random CSRF token on page load. It stores the token in three places: a cookie, a hidden form field, and a query parameter for the CAPTCHA image request. The cache-busting t parameter ensures a fresh image on each refresh.
Testing Your CAPTCHA Setup
After configuring the module, verify it works correctly.
Step 1: Generate a CAPTCHA Image
curl -v --cookie "csrf=test_token" http://localhost/captcha -o captcha.png
You should see HTTP 200, Content-Type image/png, and a Set-Cookie header with the hash.
Step 2: Verify the Image
Open captcha.png to confirm it contains readable but distorted text.
Step 3: Test Validation
Simulate a correct submission by computing the expected hash:
HASH=$(echo -n "your_secretabcdetest_token" | md5sum | cut -d' ' -f1)
curl -X POST --cookie "Captcha=$HASH" \
-d "csrf=test_token&captcha=abcde" http://localhost/login
A successful response confirms the validation logic works.
Step 4: Test Rejection
Submit an intentionally wrong code:
curl -X POST --cookie "Captcha=invalid_hash" \
-d "csrf=test_token&captcha=wrong" http://localhost/login
This should return HTTP 403.
Performance Considerations
The NGINX captcha module is lightweight. Key characteristics:
- Image generation typically takes under a millisecond per request.
- Memory usage is minimal — each image is generated in a temporary buffer and freed immediately.
- CPU impact is negligible. GD library operations are computationally inexpensive.
- Bandwidth per image is 6-12 KB depending on dimensions. Use
captcha_level 9to reduce this further.
For high-traffic sites, pair the CAPTCHA endpoint with NGINX’s built-in rate limiting:
limit_req_zone $binary_remote_addr zone=captcha:10m rate=5r/s;
location = /captcha {
limit_req zone=captcha burst=10 nodelay;
captcha;
captcha_secret "your_secret";
captcha_font "/usr/share/fonts/dejavu-sans-fonts/DejaVuSans.ttf";
}
Security Best Practices
Use a Strong Secret
The captcha_secret is the foundation of the validation mechanism. Generate a strong random value:
openssl rand -hex 32
Keep Expiration Short
Set captcha_expire to the minimum viable duration. Five minutes (300 seconds) is usually sufficient:
captcha_expire 300;
Enable Rate Limiting on the Login Endpoint
With a 4-character alphanumeric CAPTCHA, there are about 1.5 million combinations. Rate limiting makes brute-force impractical:
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
location = /login {
limit_req zone=login burst=5 nodelay;
# ... validation config ...
}
Use HTTPS
Always serve CAPTCHA over HTTPS. Without TLS, the cookie and hash travel in plaintext. This makes them vulnerable to interception and replay attacks.
Rotate Secrets Periodically
Change your captcha_secret on a regular schedule. When you rotate the secret, existing CAPTCHA cookies become invalid. Users simply request a new image. You can validate your NGINX configuration after each change to catch syntax errors.
Troubleshooting
CAPTCHA Returns 404
The module requires a CSRF token. If no csrf cookie or query parameter is present, it returns 404 and logs:
captcha: no "csrf" cookie specified, trying arg...
captcha: no "csrf" arg specified
Solution: Send a CSRF token as a cookie or query parameter:
curl http://localhost/captcha?csrf=my_token
curl --cookie "csrf=my_token" http://localhost/captcha
CAPTCHA Returns 500
This typically means the font file cannot be found or is unreadable. Verify your font path:
ls -la /usr/share/fonts/dejavu-sans-fonts/DejaVuSans.ttf
If the file does not exist, install a font package:
# RHEL/CentOS/Rocky Linux
sudo dnf install dejavu-sans-fonts
# Debian/Ubuntu
sudo apt-get install fonts-dejavu
Also check NGINX’s error log for details:
tail -f /var/log/nginx/error.log
CAPTCHA Returns 405
The module only accepts GET and HEAD requests. POST returns 405.
Validation Always Fails
Common causes:
- Mismatched secrets — The
captcha_secretvalue must exactly match the string inset_md5. - Wrong cookie name — Default is
$cookie_Captcha(capital C). Update if you changedcaptcha_name. - Case mismatch — With default
captcha_case on, the module lowercases before hashing. Usecaptcha_case offfor simpler validation. - Missing URL decoding — Do not remove the
set_unescape_uridirectives.
Font Not Found
Check available fonts and install one if needed:
fc-list | head -20
# RHEL/CentOS/Rocky Linux
sudo dnf install dejavu-sans-fonts
# Debian/Ubuntu
sudo apt-get install fonts-dejavu
Comparison with Third-Party CAPTCHA Services
| Feature | NGINX Captcha Module | reCAPTCHA | hCaptcha |
|---|---|---|---|
| External dependency | None | Google servers | hCaptcha servers |
| Privacy | Full control | Data sent to Google | Data sent to hCaptcha |
| Latency | Sub-millisecond | 100-500ms | 100-500ms |
| Bot detection | Visual distortion | Behavioral analysis | Behavioral analysis |
| Cost | Free (MIT license) | Free tier available | Free tier available |
The NGINX captcha module suits scenarios where privacy, latency, and independence matter most. For advanced behavioral bot detection, third-party services may be more appropriate.
Conclusion
The NGINX captcha module provides a straightforward way to add CAPTCHA protection at the web server level. By generating and validating challenges within NGINX, you eliminate external dependencies and maintain full control.
The module is valuable for protecting login forms, registration endpoints, and high-value targets against automated attacks — all without sending user data to any third party.
Source code: GitHub (MIT license).
