What is a brute force attack and how to defend against it

A brute force attack is an automated attempt to guess passwords, keys or tokens by trying many combinations until one works. Attempt limits, long passwords with slow hashing and MFA are the main defenses.

Updated 6 min read

Learning objectives

After reading this article you will be able to:

  • Understand how a brute force attack works and its variations
  • Tell online and offline attacks, password spraying and credential stuffing apart
  • Protect a login form and an SSH server with attempt limits, hashing and MFA
On this page

Put a login form or an SSH server on the internet and your logs will soon show sign-in attempts you never made. They are almost always bots testing username and password combinations, patiently and nonstop. It is the oldest and simplest attack there is, and it still works because plenty of doors are left unlocked.

Knowing how it works tells you where each defense goes: why some variants succeed in seconds while others never finish, and what you can apply to your login and your SSH server today.

123456 · qwerty · adminleaked password listAttackerbotPer-IP limitstops the excess from one addressPer-account limitstops attempts spread across many IPsSlow hashingslows anyone holding leaked hashesMFAthe right password is not enoughLogin formno attempt got in
No defense works alone: each layer stops part of the attempts, and together they close the login.

What is a brute force attack?#

It is an attempt to uncover a secret (a password, PIN, API key or token) by testing possibilities in bulk until one works. It does not exploit a bug in your code: it exploits the fact that the secret can be guessed in a feasible amount of time.

MITRE ATT&CK, the catalog of attack techniques used across the industry, lists brute force as technique T1110, used to access accounts when the password is unknown or when the attacker already has the hashes. It has four sub-techniques:

  • Password guessing (T1110.001): many passwords against one account, usually from lists of common passwords.
  • Password cracking (T1110.002): the attacker already has the hashes, for example from a leaked database, and tests passwords on their own machine.
  • Password spraying (T1110.003): a few very common passwords against many accounts, to avoid triggering per-account lockouts.
  • Credential stuffing (T1110.004): email and password pairs leaked from another site, tested on yours. It works because many people reuse passwords.

How does the attack work in practice?#

An online attack happens against your live system: the bot sends one login request per attempt and reads the response. Each attempt costs a network round trip, and you can count and block them.

An offline attack happens after a leak. With the hashes in hand, the attacker tests passwords on their own machine, with no attempt limit and without you seeing anything. The only barrier left is the hashing algorithm: the slower and more memory-hungry it is, the fewer passwords per second the attacker can test.

The math of the search space shows why length matters. A password of 8 lowercase letters has 26^8, about 209 billion combinations. One with 15 characters from the same alphabet has 26^15, about 1.7 x 10^21. Each extra character multiplies the work by 26. But attackers rarely test everything: they start with lists of leaked passwords, so a long password that has already leaked falls in seconds.

Where do brute force attacks happen?#

  • Login forms on websites, dashboards and APIs.
  • SSH, RDP and FTP exposed to the internet with password authentication.
  • Admin panels at well-known paths, such as /wp-admin or /admin.
  • Password reset endpoints and 6-digit code checks, which have only 1 million combinations.
  • Short or predictable keys and tokens. A long random API key cannot be guessed; one that leaked on GitHub does not even need to be, as API key exposed on GitHub shows.

How do you protect a login form?#

The defenses work in layers, and none of them is enough on its own.

  1. Attempt limits per IP and per account. OWASP recommends tying the counter to the account, not only to the IP, because attackers switch addresses. NIST SP 800-63B-4 requires limiting consecutive failures to no more than 100 per account.
  2. A generic message. Reply "invalid email or password" in both cases. Different messages, different HTTP status codes or different response times reveal which accounts exist.
  3. Slow hashing. Store passwords with Argon2id. OWASP recommends at least 19 MiB of memory, 2 iterations and a parallelism of 1; scrypt and bcrypt (work factor 10 or more) are alternatives.
  4. Long passwords, no composition rules. NIST asks for at least 15 characters for a password used as the only factor (8 with MFA), a blocklist of common and leaked passwords, and no forced periodic changes without evidence of compromise.
  5. MFA. It is the strongest defense: even with the right password, the attacker stops at the second factor.
  6. CAPTCHA after a few failures. OWASP treats it as defense in depth that makes the attack more expensive, not as a final barrier.

An example in Node.js with Express 5, express-rate-limit 8 and argon2, with per-IP and per-account limits:

js
// npm install express@5 express-rate-limit@8 argon2
import argon2 from "argon2";
import express from "express";
import { rateLimit } from "express-rate-limit";

const app = express();
app.set("trust proxy", 1); // behind a reverse proxy, use the client's real IP
app.use(express.json());

const tooMany = { error: "Too many attempts. Try again later." };

// Per IP: up to 20 failed attempts every 15 minutes
const ipLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  limit: 20,
  skipSuccessfulRequests: true,
  standardHeaders: "draft-8",
  legacyHeaders: false,
  message: tooMany,
});

// Per account: up to 5 failed attempts every 15 minutes, wherever they come from
const accountLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  limit: 5,
  skipSuccessfulRequests: true,
  keyGenerator: (req) => String(req.body?.email ?? "").toLowerCase(),
  standardHeaders: "draft-8",
  legacyHeaders: false,
  message: tooMany,
});

// Example only: in production, hashes come from your database
const users = new Map([
  ["ana@example.com", await argon2.hash("a long example passphrase")],
]);
// Hash used when the email does not exist, so response time does not reveal accounts
const dummyHash = await argon2.hash("no-such-account");

app.post("/login", ipLimiter, accountLimiter, async (req, res) => {
  const { email, password } = req.body ?? {};
  if (typeof email !== "string" || typeof password !== "string") {
    return res.status(400).json({ error: "Invalid request" });
  }
  const hash = users.get(email.toLowerCase());
  const valid = await argon2.verify(hash ?? dummyHash, password);
  if (!hash || !valid) {
    // same message for an unknown email and a wrong password
    return res.status(401).json({ error: "Invalid email or password" });
  }
  res.json({ ok: true });
});

app.listen(Number(process.env.PORT) || 3000);

skipSuccessfulRequests makes only failures count. After five failures for the same account, the next attempt gets 429 Too Many Requests until the 15-minute window ends. argon2.hash uses Argon2id by default, with parameters above the OWASP minimum. On a server with several instances, swap the limiter's in-memory store for a shared one, such as Redis.

How do you protect an SSH server?#

The most effective defense is removing the password. With SSH keys there is nothing to guess; see what SSH keys are. In /etc/ssh/sshd_config, after confirming that key login works:

PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no

Restart the SSH service and test from a second session before closing the current one. To cut the bot noise, fail2ban reads the logs and blocks IPs with repeated failures. The jail.conf defaults are 5 failures (maxretry) within 10 minutes (findtime) for a 10-minute ban (bantime). An /etc/fail2ban/jail.local that extends the ban:

ini
[sshd]
enabled = true
maxretry = 5
findtime = 10m
bantime = 1h

What are the limits of these defenses?#

Per-account lockout has a side effect: an attacker can enter wrong passwords on purpose to lock someone else out. OWASP suggests short, escalating lockouts and keeping the "forgot password" flow available even during a lockout.

A per-IP limit alone does not stop distributed attacks, and CAPTCHAs can be solved by paid services. None of these defenses helps if the password has already leaked and the user reuses it on other sites; against that, only MFA and blocklists of leaked passwords work.

Brute force is also not the same as DDoS. Both generate many requests, but DDoS wants to take the service down, and brute force wants to get in. A WAF can help filter automated traffic, but how many login attempts each account gets is a rule that lives in your code. For the rest of your protection, use the deploy security checklist.

How to do this on Square Cloud#

On Square Cloud, the SSH half of the problem drops out: an application only receives inbound traffic on port 80, published over HTTPS, so there is no SSH server of yours exposed for bots to guess passwords against. What remains is the login form, and there the layers split. The network in front of your applications has a WAF and DDoS mitigation at layers L3, L4 and L7 on every plan, which filters network traffic; attempt limits, slow hashing and MFA stay in your code, as in the example above.

If you serve one domain with load balancing (from the Standard plan), each application would keep its own in-memory counter, and the attacker would get extra attempts per instance. Since there are no sticky sessions, switch express-rate-limit to a shared store: Redis hosting, also from Standard, runs in the same datacenter as your applications. Keep the Redis URL and session secrets in the application's environment variables, never in code; a change takes effect on the next restart.

To see the attack, the web analytics included in every plan shows routes and status codes: a jump in 401 and 429 responses is the signal. See the protection on the network page, set your secrets in environment variables and explore Redis hosting.

Infrastructure

L3/L4/L7 DDoS mitigation and a WAF on every plan

Learn more

FAQs

How long does a brute force attack take?

It depends on the size of the password space and the testing speed. Online, with attempt limits, a long password that is not on any leaked list is out of reach. Offline, against a leaked hash database, a slow hash (Argon2id, scrypt, bcrypt) is what holds the attacker back.

Is brute forcing illegal?

Trying to access someone else's system or account without authorization is a crime in many countries. Only test systems you own or have written permission to test.

Does blocking the IP after a few failures solve it?

It helps, but it is not enough. Distributed attacks use thousands of IPs, so OWASP recommends counting failures per account as well. The catch is not letting an attacker lock out legitimate accounts on purpose.

What is the most effective defense against brute force?

Multi-factor authentication (MFA). OWASP cites a Microsoft analysis suggesting MFA would have stopped 99.9% of account compromises. Combine it with attempt limits and long passwords.

Next articleMy app was hacked: what to do nowIf your app was hacked, contain access, rotate every secret and preserve the logs before cleaning up. See the right order, how to find the cause and how to recover.

More in Security

See all 10 articles