S
Home Identity ยท Updated 2026-07-08

Passwords

Password security sits at the intersection of two competing forces: security requirements that make passwords difficult to attack, and usability constraints that determine whether people follow the policy at all. A policy that is too burdensome gets worked around -- passwords written on sticky notes, reused across systems, incremented predictably (Password1! to Password2!). The goal is a policy grounded in how attacks actually work, not one based on theatre that makes auditors comfortable while providing little real protection.

This article follows NIST SP 800-63B (Digital Identity Guidelines) as the primary reference, which reversed several long-standing password requirements that research showed to be counterproductive.


How Password Attacks Work

Policy requirements should be designed around the attacks they defend against. The primary attack classes are:

Brute Force

The attacker systematically tries every possible combination of characters. Effectiveness depends entirely on password length and character space, and the cost of computing each guess (determined by the hashing algorithm protecting the stored password).

Password space = charset_size ^ length

8-char alphanumeric (62 chars):  62^8  = ~218 trillion combinations
12-char alphanumeric:            62^12 = ~3.2 quadrillion combinations
16-char alphanumeric:            62^16 = ~47 quintillion combinations

At 100 billion guesses per second (modern GPU against MD5): an 8-character password falls in ~25 minutes. A 12-character password takes ~370 days. A 16-character password takes millions of years. Length is the single most effective defence against brute force.

Dictionary and Rule-Based Attacks

Rather than exhaustive enumeration, attackers apply transformation rules to wordlists derived from breach data, common phrases, keyboard patterns, and cultural references.

Hashcat rules like best64.rule apply substitutions (a->@, e->3, o->0), capitalisations, and suffix appending to millions of base words. correct horse battery staple as a passphrase is far stronger than P@ssw0rd! despite meeting complexity requirements.

Implications for policy:
- Requiring !@#$ substitutions provides weak protection -- attackers know the rules too
- Banning common passwords and known breached passwords is more effective than mandating complexity patterns

Credential Stuffing

Attackers take email:password pairs from one breach and replay them against other services. Success relies entirely on password reuse across accounts.

Implication: Users who reuse passwords are vulnerable regardless of the password's inherent strength. The defence is detection (see Advanced WAF) and checking passwords against breach databases at authentication time.

Password Spraying

Rather than brute-forcing a single account (which triggers lockout), attackers try a small set of common passwords (Password1, Welcome1, Summer2024) across a large number of accounts. Each account receives only one or two attempts -- staying under lockout thresholds.

Implication: Banning the most common passwords and enforcing MFA are the primary defences. Lockout policies alone are insufficient.

Phishing and Social Engineering

The technically strongest password provides zero protection if the user is deceived into entering it on a fake login page. Phishing-resistant MFA (FIDO2/passkeys) is the only complete defence.


Password Requirements

Length

Length is the most important password characteristic. Minimum 12 characters for user accounts; 16+ for privileged accounts.

Account Type Minimum Length Recommended
Standard user 12 characters 16+
Privileged / admin account 16 characters 20+
Service account / API key 32 characters Use a secrets manager
System-to-system secrets 32+ characters Randomly generated; never human-typed

NIST SP 800-63B requires support for passwords up to at least 64 characters. Many organisations still cap passwords at 16 or 20 characters -- this is a legacy constraint with no security justification. Remove length maximums or raise them to 256+.

Complexity

NIST SP 800-63B explicitly recommends against mandatory complexity rules (requiring uppercase, numbers, and special characters). Research showed these requirements cause predictable behaviour:

  • Users satisfy rules minimally: Password1!
  • Complexity is concentrated at predictable positions: capital at start, number and symbol at end
  • Users rotate predictably: Password1! to Password2!

What to do instead:
- Enforce minimum length
- Check against a banned password list
- Check against known breached passwords
- Allow any printable Unicode character including spaces

If your organisation or a compliance requirement mandates complexity rules, configure them as:
- At least one character from two or more character classes (not all four)
- Never mandate specific positions (first character uppercase, last character a digit)

Banned Password List

Block the most commonly chosen passwords regardless of whether they meet length and complexity requirements. At minimum, ban:

  • The top 10,000-1,000,000 most common passwords (e.g. from the SecLists project)
  • Common patterns: password, letmein, welcome, qwerty, abc123, 111111
  • The organisation's own name, domain, product names, and obvious variations
  • Keyboard walks: qwerty, asdfgh, zxcvbn, 123456, qwerty123
  • Seasonal/year patterns: Summer2024, Winter2024, Jan2024
  • Dictionary words with simple substitutions: P@ssw0rd, S3cur1ty
# Check password against banned list at registration/change
def is_banned_password(password: str, banned_list: set) -> bool:
    normalised = password.lower().strip()
    return normalised in banned_list

# At authentication endpoint:
if is_banned_password(new_password, BANNED_PASSWORDS):
    return error("This password is too common. Please choose a less predictable password.")

Breached Password Detection

Check passwords against known breach databases at account creation and password change. If a user's chosen password appears in a breach, reject it -- even if it meets all other requirements.

HaveIBeenPwned Pwned Passwords (k-anonymity API): The full database of 900 million+ breached passwords, queryable without sending the plain-text password:

import hashlib
import requests

def is_breached_password(password: str) -> tuple[bool, int]:
    # Uses k-anonymity -- password is never sent to the API
    sha1 = hashlib.sha1(password.encode("utf-8")).hexdigest().upper()
    prefix, suffix = sha1[:5], sha1[5:]

    response = requests.get(f"https://api.pwnedpasswords.com/range/{prefix}", timeout=5)
    response.raise_for_status()

    for line in response.text.splitlines():
        hash_suffix, count = line.split(":")
        if hash_suffix == suffix:
            return True, int(count)

    return False, 0

# Usage at password change:
breached, count = is_breached_password(new_password)
if breached:
    return error(f"This password has appeared in data breaches {count:,} times.")

Self-hosted alternative: Download the full HIBP SHA-1 hash list (~40 GB) and run checks locally -- no third-party dependency and no network call during authentication.

Passphrases

Encourage passphrases -- sequences of random words that are long, memorable, and resistant to dictionary attacks:

correct-horse-battery-staple       (4 words, ~44 bits of entropy)
purple-monkey-dishwasher-friday    (4 words, ~44 bits of entropy)
coffee-lamp-river-seven-moon       (5 words, ~55 bits of entropy)

A 4-word passphrase from a 7,776-word wordlist (Diceware) has ~51 bits of entropy -- stronger than P@ssw0rd1! while being far more memorable. Train users that length beats complexity.


Password Rotation

The Old Guidance (Now Superseded)

Mandatory periodic rotation -- changing passwords every 30, 60, or 90 days -- was standard practice for decades. It was based on intuition: if a password is compromised, rotating it limits exposure.

Research and real-world experience showed the opposite: mandatory rotation makes passwords weaker, not stronger. When forced to change passwords regularly, users:
- Make minimal changes: Password1! to Password2! to Password3!
- Choose weaker base passwords knowing they will change soon
- Write passwords down to remember the current iteration
- Reuse passwords across systems to reduce cognitive load

NIST SP 800-63B Current Guidance

NIST explicitly recommends against mandatory periodic rotation for passwords that show no evidence of compromise. Instead:

Rotate immediately and only when:
- A credential is known or suspected to be compromised
- A user leaves the organisation (for shared credentials)
- An account shows signs of unauthorised access
- A breach of the authentication system or password store is detected
- A staff member with knowledge of the credential departs

Do not rotate on a fixed schedule if there is no evidence of compromise.

Exception: Service Accounts and Shared Credentials

For system-to-system credentials, API keys, and shared service accounts, rotation should be:
- Automated -- using a secrets manager (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault)
- Frequent -- 30-90 day rotation is reasonable when fully automated
- Never manual -- manually rotated shared secrets introduce outage risk and are frequently delayed or skipped

# AWS Secrets Manager -- automatic rotation every 30 days
aws secretsmanager rotate-secret \
  --secret-id prod/database/password \
  --rotation-lambda-arn arn:aws:lambda:eu-west-1:123456789:function:RotateDBPassword \
  --rotation-rules AutomaticallyAfterDays=30

Privileged Account Passwords

For privileged accounts (local admin, domain admin, service accounts), use LAPS (Local Administrator Password Solution) for local accounts and a Privileged Access Management (PAM) solution for shared privileged accounts:

  • Passwords are checked out on demand, used once, and rotated on check-in
  • No standing privileged credentials that can be harvested
  • Full audit trail of who accessed what privileged account and when

Account Lockout and Throttling

Lockout policies defend against brute force and password spraying while creating a denial-of-service risk if misconfigured.

Setting Recommended Value Rationale
Lockout threshold 5-10 failed attempts Balance security vs self-DoS risk
Lockout duration 15-30 minutes (soft lockout) Auto-unlock reduces helpdesk load
Admin unlock Required after X minutes For higher-security accounts
Observation window Equal to lockout duration Reset counter after window expires
Failed attempt logging All failures Feed to SIEM for spray detection

Soft lockout vs hard lockout:
- Soft lockout: Account unlocks automatically after the lockout duration. Preferred for most environments -- balances protection with usability.
- Hard lockout: Account requires manual admin unlock. Use for highly privileged accounts or environments with high phishing risk.

Detecting password spraying: A single account hitting the lockout threshold is a user forgetting their password. Hundreds of accounts each receiving 2-3 failed attempts simultaneously is a spray attack. Alert on this pattern in your SIEM:

Alert: Password spray detected
Condition: > 20 unique accounts with failed login in a 5-minute window
Severity: High
Response: Investigate source IPs; consider temporary geo-block; notify security team

Progressive Delays (API and Web)

For web and API login endpoints, implement progressive rate limiting rather than or in addition to hard lockout:

# Progressive delay on failed authentication
DELAY_SECONDS = {1: 0, 2: 1, 3: 5, 4: 15, 5: 30}

def get_login_delay(failed_attempts: int) -> int:
    return DELAY_SECONDS.get(failed_attempts, 60)  # 60s for 5+ failures

This adds cost to automated attacks while being imperceptible to legitimate users who rarely fail more than once.


Password Storage

How passwords are stored is as important as how they are chosen. A breach of weakly hashed passwords renders even strong passwords crackable offline.

Approved Hashing Algorithms

Never store passwords as plaintext, Base64, or with reversible encryption. Use a purpose-built password hashing function (not a general cryptographic hash like SHA-256):

Algorithm Recommended Notes
Argon2id First choice Winner of the Password Hashing Competition; memory-hard; resistant to GPU and ASIC attacks
bcrypt Acceptable Widely supported; work factor adjustable; limited to 72-byte input (use prehashing for longer passwords)
scrypt Acceptable Memory-hard; more complex to configure correctly
PBKDF2-HMAC-SHA256 Acceptable (FIPS environments) Required by FIPS 140; less resistant to GPU than Argon2/bcrypt
MD5, SHA-1, SHA-256 (unsalted) Never Crackable offline in hours with modern hardware; no work factor
# Argon2id -- recommended implementation (Python)
from argon2 import PasswordHasher

ph = PasswordHasher(
    time_cost=2,        # Number of iterations
    memory_cost=65536,  # 64 MB memory usage
    parallelism=2,      # Parallelism factor
    hash_len=32,        # Output hash length
    salt_len=16         # Random salt length
)

# Hash a password
hashed = ph.hash(password)

# Verify a password
try:
    ph.verify(hashed, input_password)
    if ph.check_needs_rehash(hashed):
        hashed = ph.hash(input_password)  # Rehash if parameters changed
    return True
except Exception:
    return False
# bcrypt -- acceptable alternative (Python)
import bcrypt

# Hash
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))

# Verify
bcrypt.checkpw(password.encode(), hashed)

Salting

All approved password hashing functions generate a unique random salt per password automatically. Salting ensures:
- Two users with the same password produce different hashes
- Pre-computed rainbow table attacks are infeasible
- An attacker who breaches the hash database must crack each hash individually

Never implement salting manually -- use a library that handles it correctly.

Work Factor Tuning

The work factor (iterations, memory cost) should be tuned so that hashing takes approximately 250-500ms on your authentication servers. As hardware improves, increase the work factor to maintain this target:

# Benchmark Argon2id work factors for your hardware
import time
from argon2 import PasswordHasher

for time_cost in [1, 2, 3, 4]:
    for memory_cost in [32768, 65536, 131072]:
        ph = PasswordHasher(time_cost=time_cost, memory_cost=memory_cost)
        start = time.time()
        ph.hash("benchmark_password")
        elapsed = time.time() - start
        print(f"time={time_cost}, memory={memory_cost//1024}MB: {elapsed*1000:.0f}ms")

Multi-Factor Authentication

A strong password combined with MFA provides substantially better protection than a strong password alone. If a password is phished, stuffed from a breach, or guessed, MFA prevents account takeover.

MFA factors by strength:

Factor Examples Phishing Resistant
FIDO2 / Passkey Hardware security key (YubiKey), platform authenticator (Face ID, Touch ID) Yes
TOTP Google Authenticator, Authy, Microsoft Authenticator No -- OTP can be phished in real time
Push notification Duo, Microsoft Authenticator push No -- MFA fatigue attacks work against this
SMS OTP Text message codes No -- SIM swap, SS7 attacks
Email OTP Email magic links No -- depends on email account security

For high-security accounts and privileged access, require FIDO2/passkeys -- the only MFA factor that is phishing-resistant by design. The private key never leaves the device and the credential is bound to the origin domain.


Password Managers

Encourage or mandate the use of a password manager. A password manager enables users to:
- Use a unique, randomly generated password for every account
- Never reuse passwords
- Store passwords of arbitrary length and complexity without cognitive burden
- Share credentials securely within teams (enterprise password managers)

Enterprise deployment:
- Deploy a business password manager (1Password Business, Bitwarden Teams, Keeper, Dashlane Business) centrally managed
- Enforce vault access via SSO and MFA
- Review shared credential vault access as part of access reviews
- Never store privileged account passwords in a personal vault -- use a PAM solution

For developers: Enforce the use of a secrets manager (HashiCorp Vault, AWS Secrets Manager, 1Password Secrets Automation) for application credentials -- never in .env files committed to version control.


Session Management

A strong password is undermined by weak session management. After successful authentication:

Control Recommendation
Session token length >= 128 bits of entropy (use a CSPRNG)
Session fixation Regenerate session ID after successful login
Session timeout (idle) 15-30 minutes for sensitive applications
Session timeout (absolute) 8-12 hours; require re-authentication
Concurrent sessions Limit and notify user of new sessions from unexpected locations
Cookie flags HttpOnly, Secure, SameSite=Strict
Session invalidation on logout Server-side invalidation; do not rely only on cookie deletion

Hardening Checklist

Control Priority Notes
Minimum password length >= 12 characters Critical 16+ for privileged accounts
Maximum password length >= 64 characters High Remove arbitrary low maximums
All Unicode printable characters accepted Medium Including spaces
Banned password list enforced at creation/change High Top 10,000+ common passwords
Breached password check (HIBP) at creation/change High k-anonymity API or local dataset
No mandatory periodic rotation (unless compromised) High NIST SP 800-63B
Immediate rotation on suspected compromise Critical Define and test the process
Service account credentials rotated automatically High Secrets manager with auto-rotation
Account lockout after 5-10 failed attempts High Soft lockout preferred
Password spray detection alert in SIEM High > 20 accounts with failures in 5 min
Passwords hashed with Argon2id or bcrypt Critical Never MD5, SHA-1, or plaintext
Unique salt per password Critical Automatic with approved algorithms
Work factor tuned to 250-500ms on auth server Medium Increases offline attack cost
MFA required for all user accounts Critical FIDO2 for privileged access
Password manager deployed and enforced High Unique password per account
Session tokens >= 128 bits entropy High Use CSPRNG
Session invalidated server-side on logout High Not just cookie deletion
Idle session timeout <= 30 minutes (sensitive systems) Medium Context-dependent

References

The Security Architecture Site โ€” for internal reference use. Back to contents