Skip to main content

NGINX / Security

NGINX Captcha Module: Server-Side CAPTCHA Guide

by ,


We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth 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:

  1. Generates a random text string from a configurable character set
  2. Computes an MD5 hash: MD5(secret + code + csrf_token)
  3. Sets the hash as an HTTP cookie (e.g., Set-Cookie: Captcha=<hash>; Max-Age=600)
  4. Renders the text as a PNG image with visual distortion
  5. 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:

  1. Extracts the user’s CAPTCHA input and CSRF token from the form data
  2. Computes the same MD5 hash: MD5(secret + user_input + csrf_token)
  3. Compares this hash against the cookie value set during generation
  4. 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_md5 for 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_module directive is needed.

Module Package Pages

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 9 to 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:

  1. Mismatched secrets — The captcha_secret value must exactly match the string in set_md5.
  2. Wrong cookie name — Default is $cookie_Captcha (capital C). Update if you changed captcha_name.
  3. Case mismatch — With default captcha_case on, the module lowercases before hashing. Use captcha_case off for simpler validation.
  4. Missing URL decoding — Do not remove the set_unescape_uri directives.

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).

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.