🛡️ Sign-in protection
Updated
ZuloOne counts failed sign-ins and slows down anyone who keeps guessing passwords. Optionally, it adds a Cloudflare Turnstile check on the sign-in page.
User path: Open a stand.
How the process works#
Every wrong password is counted three ways for 15 minutes:
- by account name — from any address;
- by client address — for any account names;
- by the pair of address and account name.
The counters decide two things:
- Captcha. After 3 wrong passwords for one account, or 10 from one address, the sign-in page shows a Cloudflare Turnstile widget under the password field. Sign-in works again once the widget has passed the check. This needs Turnstile keys. Without them there is no captcha, and only the lockout below applies.
- Lockout. The fifth wrong password from one address for one account stops sign-in for that pair for 30 seconds. Every further failure doubles the pause, up to 15 minutes. While the pause lasts, the server does not check the password at all, so guessing cannot continue. The Sign in button shows a countdown.
An account name alone never locks anything. Otherwise anyone could keep a colleague out by typing their name wrong on purpose. The owner signing in from their own address is not affected by someone else's lockout.
One public address that fails 50 times in 15 minutes, across any names, is locked as a whole for 15 minutes. A private address, such as a proxy or an office network, is never locked as a whole, because everyone behind it would be locked out. It still gets the captcha.
A correct password clears the account and pair counters. The address counter stays. One valid sign-in from an attacking address does not erase what that address did to other accounts.
The self-service password change counts wrong old passwords the same way and has the same lockout. It does not show a captcha.
Settings#
All settings are server configuration: appsettings.json or environment variables on the tenant container. There is no settings card in the application. A change needs a container restart.
| Setting | Values | Default | Effect |
|---|---|---|---|
Turnstile:SiteKey / Turnstile__SiteKey |
Cloudflare site key | empty | Public key for the widget. Empty = no captcha. |
Turnstile:Secret / Turnstile__Secret |
Cloudflare secret | empty | Checks a solved widget on the server. Empty = no captcha. |
Turnstile:ExpectedHostname |
host name | empty | The token must be solved on this host. The control panel sets it to the tenant host. |
Security:Login:CaptchaMode |
Off, AfterFailures, Always |
AfterFailures |
Off: never ask. AfterFailures: ask after the thresholds below. Always: every sign-in passes the check (the widget stays invisible unless Cloudflare wants a click). |
Security:Login:CaptchaAfterAccountFailures |
number, 0 = off |
3 |
Wrong passwords for one account before the captcha. |
Security:Login:CaptchaAfterAddressFailures |
number, 0 = off |
10 |
Wrong passwords from one address before the captcha. |
Security:Login:LockoutAfterFailures |
number, 0 = off |
5 |
Wrong passwords of one address for one account before the pause. |
Security:Login:LockoutBaseSeconds |
seconds | 30 |
First pause. It doubles on every further failure. |
Security:Login:LockoutMaxSeconds |
seconds | 900 |
Longest pause. |
Security:Login:AddressLockoutAfterFailures |
number, 0 = off |
50 |
Failures from one public address before the whole address pauses. |
Security:Login:AddressLockoutSeconds |
seconds | 900 |
Length of an address pause. |
Security:Login:WindowMinutes |
minutes | 15 |
How long a failure is remembered. |
In environment variables, : becomes __, for example Security__Login__CaptchaMode=Always.
Provisioned tenants get the Turnstile keys and the client-address header from the control panel. The operator steps are in the deployment repository, prod/INSTALL.md, section "Tenant sign-in captcha".
The client address must be real. Behind Cloudflare the tenant needs ZuloOne:ForwardedForHeader=CF-Connecting-IP. Without it every caller appears as the proxy's address. The address counters then add up everyone's mistakes, and after 10 typos anywhere every user sees the captcha.
Example with numbers#
Defaults, Turnstile keys set. Anna's account is anna. An attacker works from 203.0.113.7.
| # | From | Account | Password | Answer |
|---|---|---|---|---|
| 1–2 | 203.0.113.7 |
anna |
wrong | Invalid username or password |
| 3 | 203.0.113.7 |
anna |
wrong | Invalid username or password; the widget appears |
| 4 | 203.0.113.7 |
anna |
wrong, widget passed | Invalid username or password |
| 5 | 203.0.113.7 |
anna |
wrong, widget passed | Pause of 30 s: the button shows Try again in 0:30 |
| 6 | 203.0.113.7 |
anna |
any, after 30 s | If wrong: pause of 60 s |
| — | Anna's office | anna |
correct, widget passed | Signed in. The pause applies only to 203.0.113.7 + anna |
The same attacker then tries 50 different names within 15 minutes. 203.0.113.7 is a public address, so it is locked for 15 minutes for every account.
Boundaries and edge cases#
- Counters live in the memory of the tenant process. A restart forgets them. This is deliberate: one tenant is one process, and a restart costs an attacker seconds, not the account.
- If Cloudflare cannot be reached, the check fails closed. The person sees "The check did not pass" and cannot sign in until the account's failures expire.
- A failed Turnstile check counts as a failed attempt.
- Clients that cannot draw the widget, such as the mobile app and scripts, keep working until the captcha threshold. After it they are refused with the code
captcha_required(HTTP 428) until the failures expire, or until the person signs in once in a browser. - The administrator lock (Locked on the user card) is separate. It refuses a correct password and never expires by itself.
- Directory sign-in through login.getzulo.com has its own Turnstile and is not counted here.
What's not there yet#
- No list of locked accounts or addresses on screen, and no button to lift a pause early. Restarting the tenant clears all counters.
- No e-mail or notification to the account owner after repeated failures.
- No settings card: thresholds change only in configuration.
- No two-factor confirmation for tenant users; the Two-factor fields on the user card are not enforced at sign-in.