EU Captcha configuration
EU Captcha is Myra's own GDPR-compliant captcha. It protects the registration form against automated sign-ups (epic AGF-2969). The configuration is database-backed (never environment variables) and has three parts:
| Setting | Where it is set | Exposure |
|---|---|---|
Enable flag (eu_captcha_enabled) |
Global settings (PUT /admin/v1/settings/eu_captcha_enabled) |
platform-admin |
Public sitekey (eu_captcha_sitekey) |
Global settings (PUT /admin/v1/settings/eu_captcha_sitekey) |
platform-admin; the sitekey is public (safe to embed on the registration page) |
| Server-only secret | this endpoint (PUT /admin/v1/eu-captcha/secret) |
platform-admin; write-only — never returned to anyone, never sent to the browser |
The enable flag and the public sitekey are ordinary global settings (see the Global settings API). The secret is a credential, so it is handled separately and never travels on the generic settings surface.
Until all three are set — and the registration widget and server-side verify flow ship (AGF-2986 / AGF-2987) — the feature is a no-op: registration behaves exactly as it does today. It ships dark; turning it on is a deliberate go-live act.
Base URL: https://<your-gateway-host>/admin/v1
Every endpoint requires the platform admin role (permission SETTINGS_MANAGE); a lower role is
403, an unauthenticated caller 401. Access is re-gated server-side — the client is never the
authorization boundary.
How the secret is protected
- Stored encrypted at rest. The secret is encrypted with the deployment's application master key before it is written to the database; only the ciphertext is ever persisted.
- Never returned. No endpoint — not this one, not the generic settings list — ever returns the
secret. The generic
GET /admin/v1/settingslists every settings row verbatim, so a platform admin will see an opaque ciphertext blob foreu_captcha_secretthere; the plaintext is never exposed. - Not a generic setting.
eu_captcha_secretis deliberately not an editable global-settings key, soPUT /admin/v1/settings/eu_captcha_secretis an unknown key →404. A plaintext credential can never be written through, or echoed by, the generic settings surface. - Audited without the value. Setting the secret writes an audit event recording only that it was set
(
secret_set: true) — never the value or its ciphertext.
GET /admin/v1/eu-captcha/secret
Reports only whether a secret is configured — never the value.
Response 200
secret_set is false when no secret has been stored.
PUT /admin/v1/eu-captcha/secret
Sets (or replaces) the server-only secret. The value is validated, encrypted, and stored; the response never echoes it.
Accepted body
The secret must be an opaque token: a string, non-empty, at most 4096 bytes, containing no
whitespace and no control characters (Base64 characters such as + / = are allowed). There is no
"clear" operation — to turn the feature off, set eu_captcha_enabled
to false; a stored secret can only be overwritten, not deleted, so a stale ciphertext row may
remain after the feature is disabled (harmless — it is never read while the feature is off).
Response 200
Rejected (fail closed — nothing is persisted)
- Not a platform admin →
403. - Missing
secret,null, or a non-string / empty / over-4096-byte value, or any value containing whitespace or a control character →400. - The application master key is unavailable (so the secret cannot be encrypted), or encryption fails
→
500. Nothing is written.
Fail-closed behaviour (contract for the verify flow)
The server-side read helper exposes a public shape to the registration page containing only
{ enabled, sitekey } — never the secret. "Enabled" is best-effort: it is true only when the flag
is on and a sitekey is set and a secret row is present. A present-but-undecryptable secret (for
example after a master-key rotation) keeps the feature enabled so the verify flow (AGF-2987)
rejects sign-ups, rather than silently disabling the captcha and letting them through. The verify
flow is therefore the authoritative gate: it must reject any request whose captcha response cannot be
verified, and must treat a missing/undecryptable secret as "reject all".