config.json. Fields marked required have no default and must be set before verifyngo will start. All duration values use Go’s duration string format: "30s", "10m", "2h", "168h" (7 days).
Core
| Field | Type | Default | Description |
|---|---|---|---|
listen_addr | string | — | Address and port verifyngo listens on, e.g. "127.0.0.1:8080". |
upstream_url | string | required | Base URL of the backend that verifyngo proxies to, e.g. "http://127.0.0.1:8886". |
cookie_secret | string | required | Secret used to sign the verification cookie. Must stay constant across restarts or all existing cookies become invalid. Generate with openssl rand -hex 32. |
cookie_name | string | "cp_verified" | Name of the verification cookie set in the visitor’s browser. |
cookie_ttl | duration | "168h" | How long a solved CAPTCHA cookie is valid. Default is 7 days. |
default_action | string | "challenge" | What to do when no rule matches. Accepted values: "challenge", "allow", "deny". |
IP Trust
| Field | Type | Default | Description |
|---|---|---|---|
trust_real_ip | bool | false | When true, verifyngo reads the visitor IP from X-Forwarded-For instead of the TCP connection. |
trusted_proxies | []string | [] | List of IP addresses or CIDR ranges whose X-Forwarded-For headers are trusted. Only used when trust_real_ip is true. If omitted, only loopback and RFC-1918 ranges are auto-trusted. |
whitelist | []string | [] | IP addresses or CIDR ranges that always bypass all checks and are never challenged. |
Paths
| Field | Type | Default | Description |
|---|---|---|---|
bypass_paths | []string | See below | Regex patterns matched against the request path. Matching requests are never challenged. Defaults to a built-in list covering common static assets (see below). Set to null (omit the field) to restore the default list. |
always_pass_paths | []string | [] | Regex patterns for paths that are always proxied upstream unconditionally, even if the visitor is banned. Useful for health-check endpoints. |
static_dir | string | "" | Path prefix for verifyngo’s own built-in static assets (challenge page resources). |
rules_file | string | "" | Path to an external rules file. Hot-reloaded every ~10 seconds. |
policy_file | string | "" | Path to an external policy file (ASN-based rules). Hot-reloaded every ~10 seconds. If both rules_file and policy_file are set, policy_file takes priority and is the only file watched for changes. |
bypass_paths list
When bypass_paths is not set in config.json, verifyngo uses the following patterns:
^/favicon\.ico$
^/robots\.txt$
^/sitemap\.xml$
^/\.well-known/
^/(?:manifest\.json|site\.webmanifest|manifest\.webmanifest)$
\.webmanifest$
^/browserconfig\.xml$
^/apple-touch-icon(?:-precomposed)?(?:-\d+x\d+)?\.png$
\.(?:css|js|mjs|png|jpe?g|gif|svg|webp|avif|ico|woff2?|ttf|otf|eot|wasm|mp4|webm)$
CAPTCHA
Provider selection
| Field | Type | Default | Description |
|---|---|---|---|
provider | string | required | Which CAPTCHA provider to use. Accepted values: "cap", "turnstile", "hcaptcha". |
cap (self-hosted Cap)
| Field | Type | Default | Description |
|---|---|---|---|
cap.api_url | string | "" | Public URL of the Cap instance, shown to the visitor’s browser, e.g. "https://captcha.example.com". |
cap.verify_url | string | "" | Internal URL used by verifyngo to verify tokens server-side, e.g. "http://127.0.0.1:3000". Must be reachable by the verifyngo process, not the browser. |
cap.site_key | string | "" | Cap site key. |
cap.secret_key | string | "" | Cap secret key used for server-side verification. |
cap.widget_script_url | string | "" | Override URL for the Cap widget script. Leave empty to use the default from api_url. |
turnstile (Cloudflare Turnstile)
| Field | Type | Default | Description |
|---|---|---|---|
turnstile.site_key | string | "" | Turnstile site key from the Cloudflare dashboard. |
turnstile.secret_key | string | "" | Turnstile secret key used for server-side verification. |
hcaptcha
| Field | Type | Default | Description |
|---|---|---|---|
hcaptcha.site_key | string | "" | hCaptcha site key. |
hcaptcha.secret_key | string | "" | hCaptcha secret key used for server-side verification. |
Walk-away & Ban
Walk-away tracking counts how many times a visitor abandons the challenge page without solving it. When the threshold is exceeded the visitor is banned.| Field | Type | Default | Description |
|---|---|---|---|
walkaway.threshold | int | 10 | Number of walk-aways before the visitor’s IP is banned. |
walkaway.ttl | duration | "2h" | How long a walk-away counter entry lives before it is reset. |
ban.duration | duration | "24h" | How long a banned IP remains banned. |
Progressive
Progressive mode observes traffic passively on specified paths before deciding to challenge. Visitors who exceedmax_requests within request_window are challenged.
| Field | Type | Default | Description |
|---|---|---|---|
progressive.enabled | bool | false | Enable progressive challenge mode. |
progressive.passive_paths | []string | [] | Regex patterns for paths observed in passive mode. Requests to these paths increment the counter without immediately triggering a challenge. |
progressive.passive_ttl | duration | "30m" | How long a passive observation window entry lives. |
progressive.max_requests | int | 50 | Number of requests within request_window that triggers a challenge. |
progressive.request_window | duration | "10m" | Sliding window duration for counting requests. |
Store
The store holds ban state, walk-away counters, and progressive counters.| Field | Type | Default | Description |
|---|---|---|---|
store.backend | string | "memory" | Storage backend. Accepted values: "memory", "file", "redis". |
store.file (file backend)
| Field | Type | Default | Description |
|---|---|---|---|
store.file.path | string | "" | Path to the JSON file used to persist store data. |
store.file.save_interval | duration | "30s" | How often the in-memory state is flushed to disk. |
store.redis (Redis backend)
| Field | Type | Default | Description |
|---|---|---|---|
store.redis.addr | string | "127.0.0.1:6379" | Address of the Redis server. |
store.redis.password | string | "" | Redis password (leave empty if not set). |
store.redis.db | int | 0 | Redis database index. |
store.redis.key_prefix | string | "verifyngo" | Prefix applied to all keys written by verifyngo. |
ASN
ASN lookups power policy-file rules that match on autonomous system numbers or organisation names.| Field | Type | Default | Description |
|---|---|---|---|
asn.geoip_db_path | string | "" | Path to a MaxMind GeoLite2-ASN .mmdb file. Required for ASN-based rules when whois is disabled. |
asn.whois_enabled | bool | true | Use WHOIS to resolve ASN information at startup. |
asn.whois_addr | string | "whois.radb.net:43" | WHOIS server address used for lookups. |
asn.whois_timeout | duration | "30s" | Timeout for each WHOIS query. |
asn.whois_strict | bool | false | When true, a WHOIS timeout or error during startup is treated as fatal and verifyngo will exit. When false, a warning is logged and startup continues. |
AbuseIPDB
When enabled, verifyngo reports banned IPs to AbuseIPDB after they have been banned a configurable number of times.| Field | Type | Default | Description |
|---|---|---|---|
abuseipdb.enabled | bool | false | Enable AbuseIPDB reporting. |
abuseipdb.api_key | string | "" | Your AbuseIPDB API key. |
abuseipdb.categories | string | "" | Comma-separated AbuseIPDB category IDs to attach to reports, e.g. "19,21". |
abuseipdb.comment | string | "" | Comment included with each report. |
abuseipdb.report_after_bans | int | 3 | Number of bans an IP must accumulate before it is reported. |
Branding
All branding fields are optional. They customise the appearance of the challenge page shown to visitors.| Field | Type | Default | Description |
|---|---|---|---|
branding.logo_url | string | "" | URL of a logo image displayed on the challenge page. |
branding.css_url | string | "" | URL of a custom stylesheet that overrides the default challenge page styles. |
branding.accent_color | string | "#4A90D9" | Primary accent colour used for buttons and highlights. |
branding.background_color | string | "#000000" | Challenge page background colour. |
branding.text_color | string | "#f2f2f2" | Challenge page body text colour. |
branding.font_family | string | "sans-serif" | CSS font-family value applied to the challenge page. |
branding.font_url | string | "" | URL of a web font stylesheet to load (e.g. a Google Fonts URL). |
branding.title | string | "" | Site name shown in the challenge page heading. |
branding.details_text | string | See below | Explanatory paragraph shown beneath the CAPTCHA widget. |
branding.contact_url | string | "" | URL linked from a “contact us” prompt on the challenge page. |
branding.details_text
This page runs a quick, one-time check to confirm you're a real visitor and
not an automated bot. It only takes a moment, and once you've solved it you
won't see this again for a while.
Full example config.json
Full example config.json
{
"listen_addr": "127.0.0.1:8080",
"upstream_url": "http://127.0.0.1:8886",
"cookie_secret": "CHANGE_ME_openssl_rand_hex_32",
"cookie_name": "cp_verified",
"cookie_ttl": "168h",
"provider": "cap",
"cap": {
"api_url": "https://captcha.example.com",
"verify_url": "http://127.0.0.1:3000",
"site_key": "your-cap-site-key",
"secret_key": "your-cap-secret-key",
"widget_script_url": ""
},
"turnstile": {
"site_key": "",
"secret_key": ""
},
"hcaptcha": {
"site_key": "",
"secret_key": ""
},
"trust_real_ip": false,
"trusted_proxies": [],
"static_dir": "/static",
"rules_file": "/rules.txt",
"policy_file": "",
"always_pass_paths": ["^/health$", "^/healthz$", "^/status$"],
"asn": {
"geoip_db_path": "",
"whois_enabled": true,
"whois_addr": "whois.radb.net:43",
"whois_timeout": "30s",
"whois_strict": false
},
"store": {
"backend": "memory",
"file": {
"path": "/data/store.json",
"save_interval": "30s"
},
"redis": {
"addr": "redis:6379",
"password": "",
"db": 0,
"key_prefix": "verifyngo"
}
},
"progressive": {
"enabled": false,
"passive_paths": [
"^/$",
"/this-specific-path"
],
"passive_ttl": "30m",
"max_requests": 50,
"request_window": "10m"
},
"branding": {
"logo_url": "/static/logo.svg",
"css_url": "/static/custom.css",
"title": "My Website",
"accent_color": "#444444",
"background_color": "#000000",
"text_color": "#f2f2f2",
"font_family": "'My Font', sans-serif",
"font_url": "",
"details_text": "This page runs a quick, one-time check to confirm you're a real visitor and not an automated bot. It only takes a moment, and once you've solved it you won't see this again for a while.",
"contact_url": "mailto:contact@example.com"
},
"whitelist": [
"203.0.113.10",
"198.51.100.0/24"
],
"default_action": "challenge",
"rules": [
{
"match": { "path": "^/api/" },
"action": "allow"
},
{
"match": { "ua": "python-requests|curl/|Go-http-client" },
"action": "deny"
},
{
"match": { "cidr": ["185.220.0.0/16"] },
"action": "deny"
},
{
"match": { "path": "^/search" },
"action": "challenge"
}
],
"walkaway": {
"threshold": 10,
"ttl": "2h"
},
"ban": {
"duration": "24h"
},
"abuseipdb": {
"enabled": false,
"api_key": "",
"categories": "19,21",
"comment": "verifyngo: exceeded walk-away threshold",
"report_after_bans": 3
}
}