Atomic Edge Captcha challenge protection for your website
November 28, 2025
By: admin

How we built our own self-hosted CAPTCHA for Atomic Edge (and how you can use it today)

At Atomic Edge, customers ask for one thing over and over: block bad bots without punishing real users or shipping data to third-party CAPTCHA providers. Many of you run login portals, API backends, and form-heavy apps that sit behind our WAF. You want a challenge you can trust, that lives inside our stack, and that adds almost no latency.

So we built it.

Our new ALTCHA challenge runs entirely inside the Atomic Edge platform. It pairs a lightweight, browser-solved proof-of-work with constant-time HMAC checks at the edge. No external calls. No image puzzles. Typical browser solve time is around 200 ms at 1,000,000 difficulty. Verification on our edge nodes is under 10 ms.

You can turn it on today. It’s included on all plans (free and paid), with additional controls for paid tiers. Click here to register your account.


Why we chose a self-hosted challenge

  • Zero external dependency. No reCAPTCHA, hCaptcha, or Turnstile round-trips. That removes a failure mode and a privacy concern.
  • Predictable performance. The challenge is issued and verified in the same Caddy + Coraza pipeline that powers our WAF.
  • Policy control. We can challenge only the traffic that needs friction: login POSTs, suspicious API calls, or requests flagged by specific WAF rules.
  • User experience. Proof-of-work runs in JavaScript quietly. No “select all traffic lights.”

The core of the system is ALTCHA (altcha.org). The server issues a signed puzzle. The browser computes a small answer. The server verifies the answer by recomputing an HMAC. That’s it.


How it works in Atomic Edge

Request flow

  1. A request hits your protected route.
  2. Our Coraza rules and heuristics evaluate it. If policy says “challenge,” we save the original request (including POST body up to the configured size), and redirect to a short challenge page.
  3. The page loads a tiny widget, fetches a puzzle from our /api/altcha/challenge endpoint, solves it in the browser, and posts the solution back.
  4. We verify in constant time, set a signed cookie, restore the original request, and continue to your app. The user does not need to retype a form.

Where it runs

  • Edge nodes: Caddy HTTP layer with our ALTCHA middleware
  • State: memory (free plan), Redis for paid multi-region deployments
  • Policy engine: Coraza WAF + our rule routing

What we had to solve to ship this in production

Widget protocol compatibility

Small protocol details matter. We hit three early issues that caused silent failures:

  • The browser widget expects JSON fields in camelCase. Use maxNumber, not maxnumber.
  • The puzzle hash is exactly salt+number with no separator. Our first cut used salt:number, which breaks verification.
  • The HMAC must sign the challenge hash only. If you sign additional fields, nothing verifies.

We rebuilt our challenge generator and verifier to match the reference implementation byte-for-byte and added automated tests that compare our server values with the widget script.

Return URI correctness

Users who solved the challenge sometimes landed on / instead of the page they started on.

  • Fix: we attach the original target to the challenge link as ?return=.... The widget reads that parameter and sends the browser back to the correct path.
  • Security: the edge validates return so it cannot point off-site. We strip scheme and host and allow only local paths.

Security hardening after audit

We put the feature through an internal security review and hardened several areas:

  • Session IDs are now 256-bit random hex.
  • CORS is origin-restricted by policy; no wildcard.
  • File persistence (for single-node plans) uses atomic write-rename to avoid races.
  • Errors are sanitized. Verbose detail is available only in debug.
  • Rate limiting prevents puzzle spam per IP and per site.
  • HMAC compare is constant-time.
  • Payload caps: challenge and verify payloads limited to 4 KB by default.

Build and deployment stability

We pin our Caddy toolchain to a known version for reproducible builds:

xcaddy build v2.8.4 --with github.com/stardothosting/caddy-altcha

This keeps the edge image stable across regions and avoids unexpected Go toolchain breaks.

Route matching edge cases

Customers sometimes serve the challenge at /captcha and link to /captcha/. We updated our matchers to accept an optional trailing slash so both paths work cleanly.


What you configure (and what we handle)

All plans can enable ALTCHA from your Atomic Edge dashboard:

  • Protected routes: pick exact paths, path prefixes, or rule-driven conditions (for example “challenge when WAF rule X fires”).
  • Difficulty: set maxNumber per route. A reasonable default is 1,000,000 (about 200 ms browser work).
  • POST preservation: on by default for login and form routes.
  • Cookie scope: path and TTL for the verification cookie.
  • Rate limits: per IP and per site caps for challenge generation.

Paid plans add:

  • Redis state across regions.
  • Per-tenant keys and scheduled rotation.
  • Per-route difficulty and analytics.
  • API access to update policies on the fly.

Performance profile

  • Solve time (browser): ~20 ms at 100k, ~200 ms at 1M, ~2 s at 10M.
  • Verify time (edge): typically < 10 ms, since we recompute an HMAC and compare.
  • Overhead when already verified: none. A signed cookie lets the request pass without re-challenge for the configured TTL.

Our recommendation: use 1M difficulty for login and API routes, then increase for high-risk paths if you see automated abuse.


Implementation notes for those who care

Below are simplified examples that mirror what runs inside our edge. They are not required to use the feature, but they help explain behavior when you read your logs.

Challenge generation

// shape the widget expects
{
  "salt": "hex…",
  "maxNumber": 1000000,
  "signature": "hmac_sha256(salt+number, server_key)",
  "expiresAt": 1735689600
}

Verification hot path (pseudo)

// Recreate the hash the browser solved: salt + number
hash := salt + strconv.Itoa(number)
// HMAC with the server key
mac := hmac.New(sha256.New, key)
mac.Write([]byte(hash))
expected := mac.Sum(nil)

// constant-time compare with provided signature
if subtle.ConstantTimeCompare(expected, provided) != 1 {
    return deny()
}
return allow()

Caddyfile example (conceptual)

# Challenge API
route /api/altcha/challenge {
  altcha_challenge {
    key env:ALTCHA_HMAC_KEY
    difficulty 1000000
    ttl 120s
    backend redis { addr 10.0.0.12:6379 prefix altcha: }
    cors_origins https://yourapp.example
    rate_limit 30 /1m
  }
}

# Challenge page
handle_path ^/captcha/?$ {
  root * /var/www/altcha
  file_server
}

# Protect routes with POST preservation
@protect path /login* /admin* /api/private/*
altcha_verify @protect {
  challenge_url /api/altcha/challenge
  challenge_page /captcha
  preserve_post on
  cookie_name ae_altcha
  backend redis { addr 10.0.0.12:6379 prefix altcha: }
  return_param return
}

In Atomic Edge you won’t edit Caddyfiles directly. Our control plane renders the JSON config and pushes it to the edge when you change a setting.


Preserving POST data

A challenge in the middle of a form is annoying if it forces users to retype credentials or payloads. We cache the original request body up to a safe limit, then replay it after verification. This is on by default for login and form routes. If your app posts large files, challenge before the upload endpoint rather than during it.


WAF integration

ALTCHA is a policy action inside our WAF:

  • Route-based: “always challenge /wp-login.php.”
  • Signal-based: “if rule 932100 (SQLi) fires twice in a minute, challenge next request.”
  • Burst-based: “if this IP requests /api/token more than N times per minute, challenge for 10 minutes.”

Because verification is local, you keep full control over thresholds without worrying about external API quotas.


Troubleshooting quick hits

  • Widget is blank: check JSON field names. Use maxNumber, not maxnumber.
  • Redirect to home after solve: ensure your challenge page reads ?return= and that the configured challenge page URL matches the path you serve.
  • Verification fails: the only string signed is salt+number. Extra fields break the HMAC.
  • High solve time: reduce difficulty or exclude heavy client devices from challenges using your own logic before the WAF rule.

If you get stuck, our dashboard logs include per-site diagnostics with timestamps, route names, and concise error reasons.


Try it on your site

  1. Create a free Atomic Edge account
  2. Add your domain and point DNS to the provided edge endpoint.
  3. In Bot Protection, enable ALTCHA Challenge on your login or API routes.
  4. Keep the default difficulty, verify flow, then tune policies.

That’s it. You get a self-hosted, privacy-friendly challenge that lives inside your WAF. If you need Redis-backed sessions across regions, per-route analytics, and API automation, upgrade to a paid plan inside the dashboard.

Questions or need help migrating from a third-party CAPTCHA? Drop a note through the support widget in the dashboard. We’ll help you set policy and tune difficulty for your traffic profile.

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School