[{"data":1,"prerenderedAt":341},["ShallowReactive",2],{"navigation":3,"/blog/password-security-not-complexity":141,"/blog/password-security-not-complexity-surround":330},[4,36,55,80,101,116,128],{"title":5,"path":6,"stem":7,"children":8,"icon":35},"ASP.NET Core","/aspnet-core","1.aspnet-core/1.index",[9,11,15,19,23,27,31],{"title":10,"path":6,"stem":7},"Cursor Pagination",{"title":12,"path":13,"stem":14},"Dapper - Micro ORM","/aspnet-core/dapper","1.aspnet-core/2.dapper",{"title":16,"path":17,"stem":18},"DbUp - Database Migrations","/aspnet-core/dbup","1.aspnet-core/3.dbup",{"title":20,"path":21,"stem":22},"Serilog - Structured Logging for .NET","/aspnet-core/serilog","1.aspnet-core/4.serilog",{"title":24,"path":25,"stem":26},"Class Inheritance in C#","/aspnet-core/inheritance","1.aspnet-core/5.inheritance",{"title":28,"path":29,"stem":30},"C# Collections","/aspnet-core/collections","1.aspnet-core/6.collections",{"title":32,"path":33,"stem":34},".NET gitignore Command","/aspnet-core/gitignore","1.aspnet-core/7.gitignore",false,{"title":37,"path":38,"stem":39,"children":40,"icon":35},"Oauth2.0 and OpenIdConnect","/oauth2andopenidconnect","10.oauth2andopenidconnect/1.index",[41,43,47,51],{"title":42,"path":38,"stem":39},"RFC 6749 - OAuth 2.0 Authorization Framework",{"title":44,"path":45,"stem":46},"Authentication (AuthN) vs Authorization (AuthZ)","/oauth2andopenidconnect/authnandauthz","10.oauth2andopenidconnect/2.authnandauthz",{"title":48,"path":49,"stem":50},"TOTP Authentication","/oauth2andopenidconnect/totp","10.oauth2andopenidconnect/3.totp",{"title":52,"path":53,"stem":54},"OAuth 2.0 Resource Indicators - RFC 8707","/oauth2andopenidconnect/oauth2resourceindicators","10.oauth2andopenidconnect/4.OAuth2ResourceIndicators",{"title":56,"path":57,"stem":58,"children":59,"page":35},"Azure","/azure","2.azure",[60,64,68,72,76],{"title":61,"path":62,"stem":63},"Azure Cost Management","/azure/azuremanagementandgovernance","2.azure/1.Azuremanagementandgovernance",{"title":65,"path":66,"stem":67},"Azure Policy","/azure/azurepolicy","2.azure/2.azurepolicy",{"title":69,"path":70,"stem":71},"Code Blocks","/azure/code-blocks","2.azure/3.code-blocks",{"title":73,"path":74,"stem":75},"Prose Components","/azure/prose-components","2.azure/4.prose-components",{"title":77,"path":78,"stem":79},"Images and Embeds","/azure/images-embeds","2.azure/5.images-embeds",{"title":81,"path":82,"stem":83,"children":84,"page":35},"Git","/git","3.git",[85,89,93,97],{"title":86,"path":87,"stem":88},"Git Rebase","/git/git-rebase","3.git/1.git-rebase",{"title":90,"path":91,"stem":92},"Git Stash","/git/git-stash","3.git/2.git-stash",{"title":94,"path":95,"stem":96},"SemVer","/git/semver","3.git/3.semver",{"title":98,"path":99,"stem":100},"Conventional Commits","/git/conventional-commits","3.git/4.conventional-commits",{"title":102,"path":103,"stem":104,"children":105,"icon":35},"Design Patterns","/design-patterns","7.design-patterns/1.index",[106,108,112],{"title":107,"path":103,"stem":104},"Introduction",{"title":109,"path":110,"stem":111},"Installation","/design-patterns/installation","7.design-patterns/2.installation",{"title":113,"path":114,"stem":115},"Usage","/design-patterns/usage","7.design-patterns/3.usage",{"title":117,"path":118,"stem":119,"children":120,"icon":35},"Software Principles","/principles","8.principles/1.index",[121,122,125],{"title":107,"path":118,"stem":119},{"title":109,"path":123,"stem":124},"/principles/installation","8.principles/2.installation",{"title":113,"path":126,"stem":127},"/principles/usage","8.principles/3.usage",{"title":129,"path":130,"stem":131,"children":132,"icon":35},"Software Architecture","/architecture","9.architecture/1.index",[133,135,138],{"title":134,"path":130,"stem":131},"CQRS Pattern",{"title":109,"path":136,"stem":137},"/architecture/installation","9.architecture/2.installation",{"title":113,"path":139,"stem":140},"/architecture/usage","9.architecture/3.usage",{"id":142,"title":143,"body":144,"date":319,"description":320,"extension":321,"featured":35,"meta":322,"navigation":323,"path":324,"readTime":325,"seo":326,"stem":327,"tag":328,"__hash__":329},"blog/blog/6.password-security-not-complexity.md","Password Security Was Never About Complexity — It's About Exposure",{"type":145,"value":146,"toc":307},"minimark",[147,156,159,164,171,183,187,198,213,217,220,224,233,236,240,251,254,258,261,264,268],[148,149,150,151,155],"p",{},"For most of the last two decades, \"secure password\" meant \"complex password.\" Require an uppercase letter, a digit, a symbol, a minimum length, a mandatory reset every 90 days — check every box, and the password was considered strong. That model treated password security as a formatting problem: does this string ",[152,153,154],"em",{},"look"," hard to guess?",[148,157,158],{},"The trouble is that \"looks hard to guess\" and \"is safe to use\" are two completely different properties, and attackers have never cared much about the first one.",[160,161,163],"h2",{"id":162},"attackers-arent-attacking-your-password-policy","Attackers Aren't Attacking Your Password Policy",[148,165,166,167,170],{},"Complexity rules assume the threat is a human or a brute-force script trying combinations one at a time. That's not how most credential attacks work anymore. Modern attacks lean on credential stuffing, leaked password databases, password reuse across services, and phishing — methods that don't care how a password is ",[152,168,169],{},"shaped",", only whether it's already sitting in a dataset the attacker has access to. An attacker running a credential-stuffing campaign isn't trying every possible eight-character string with a symbol in it. They're trying the millions of real passwords that showed up in the last decade of public breaches, because those are astronomically more likely to work than anything generated at random.",[148,172,173,174,178,179,182],{},"This is the gap complexity rules never closed. A password like ",[175,176,177],"code",{},"P@ssw0rd1!"," satisfies nearly every policy ever written — upper, lower, digit, symbol, ten characters. It's also one of the most common passwords in breach databases, appearing hundreds of thousands of times across known credential dumps. A policy that only checks ",[152,180,181],{},"shape"," will happily accept it, right up until it's used against a login form by someone who already has it on a list.",[160,184,186],{"id":185},"the-standard-already-changed-most-policies-havent-caught-up","The Standard Already Changed — Most Policies Haven't Caught Up",[148,188,189,190,197],{},"This isn't a fringe opinion. ",[191,192,196],"a",{"href":193,"rel":194},"https://pages.nist.gov/800-63-3/sp800-63b.html",[195],"nofollow","NIST's Digital Identity Guidelines (SP 800-63B)"," have recommended checking new passwords against known-compromise lists since 2017, and explicitly de-emphasized mandatory character-class mixing as the primary defense. The guidance is direct: verifiers should compare a chosen password against a list of values known to be commonly used, expected, or already compromised — not just verify it contains a symbol.",[148,199,200,201,206,207,212],{},"The database that makes this practical exists and is widely used. ",[191,202,205],{"href":203,"rel":204},"https://haveibeenpwned.com/Passwords",[195],"Have I Been Pwned (HIBP)",", maintained by security researcher Troy Hunt, holds a searchable index of more than 10 billion real-world passwords harvested from actual data breaches. A recent implementation guide from ",[191,208,211],{"href":209,"rel":210},"https://duendesoftware.com/blog/20260722-stop-accepting-breached-passwords-hibp-duende-usermanagement",[195],"Duende Software"," walks through wiring HIBP checks into a .NET identity system's password validation pipeline — rejecting a password not because it fails a character-class rule, but because it's already known to attackers. The API is queried using k-anonymity: only the first five characters of the password's SHA-1 hash are ever sent over the network, so the full password (or even its full hash) never leaves the client's side of the check. It's a clean illustration of the underlying shift: the meaningful question isn't \"does this password look secure,\" it's \"is this password already known to attackers\" — and that question has a real, checkable answer, at registration time, before the account exists.",[160,214,216],{"id":215},"security-questions-and-complexity-rules-fail-for-the-same-reason","Security Questions and Complexity Rules Fail for the Same Reason",[148,218,219],{},"It's worth naming the pattern, because it shows up more than once. Security questions assumed personal information stays private. Complexity rules assumed a more complicated-looking string is automatically a safer one. Both assumptions were reasonable when they were written, and both quietly stopped being true as data availability and attacker tooling changed around them. Neither failure was really about implementation quality — a security-question feature can be beautifully engineered and still insecure, and a password policy can enforce its rules flawlessly while doing nothing to stop the one attack method that actually gets used against it. Security controls that were designed around a threat model from ten years ago don't become dangerous by getting worse. They become dangerous by staying exactly the same while the threat model moves.",[160,221,223],{"id":222},"what-actually-reduces-risk","What Actually Reduces Risk",[148,225,226,227,232],{},"The alternative isn't \"no rules\" — it's rules aimed at the right target: length over artificial complexity, uniqueness across services, password managers to make uniqueness actually achievable, breached-password screening at the point of registration, multi-factor authentication as a backstop, and a longer-term push toward phishing-resistant methods that don't depend on a shared secret at all. A long, memorable passphrase that's never been used anywhere else is a stronger credential than a short, \"complex\" one that satisfies every character rule and appears in a breach dump — a pattern the ",[191,228,231],{"href":229,"rel":230},"https://xkcd.com/936/",[195],"xkcd \"correcthorsebatterystaple\" comic"," has been making the case for since 2011, well before most standards bodies caught up to it.",[148,234,235],{},"There's also a behavioral cost to getting this wrong that pure format-checking ignores entirely. Excessive complexity requirements and forced rotation schedules push real users toward exactly the behaviors that undermine security: predictable variations on an old password, passwords written down somewhere insecure, or one password reused across a dozen accounts because remembering fifteen \"compliant\" ones isn't realistic. A policy that creates that behavior isn't neutral — it's actively manufacturing the weakness it was supposed to prevent.",[160,237,239],{"id":238},"ask-what-attack-youre-actually-preventing","Ask What Attack You're Actually Preventing",[148,241,242,243,246,247,250],{},"The right way to evaluate a password control isn't \"does it look strict enough.\" It's a threat-model question: what attack does this actually stop, how do real attackers operate today, and does this control meaningfully reduce that risk or just add friction that pushes users toward worse habits? A password policy that blocks ",[175,244,245],{},"password123"," because it lacks a symbol, while happily accepting ",[175,248,249],{},"Str0ng&Compromised!"," because it technically passes every rule, is optimizing for the wrong thing. A policy that rejects both — the second one specifically because it's sitting in a breach database, not because of how it's formatted — is solving the problem that actually exists.",[148,252,253],{},"Like security questions, this is a decision that benefits from being made before implementation rather than patched afterward. Product teams should be asking whether a given password requirement still reflects current guidance; engineering teams should be asking whether a given control addresses a real, current attack pattern or just an inherited assumption nobody's revisited. The best version of this fix often isn't a smarter regex — it's swapping \"enforce four character classes\" for \"check against a breach list,\" which is a product and policy decision as much as a technical one.",[160,255,257],{"id":256},"the-takeaway","The Takeaway",[148,259,260],{},"A password can be long, complex, and fully compliant with policy, and still be worthless the moment it's already sitting in an attacker's dataset. The purpose of a password control was never to make users satisfy a validator — it was to prevent real accounts from being taken over by real attacks, and those are measurably different goals. The strongest password policy isn't the one with the most rules. It's the one built around how credentials are actually stolen and reused, which increasingly means checking exposure, not counting character classes.",[262,263],"hr",{},[160,265,267],{"id":266},"references-and-further-reading","References and Further Reading",[269,270,271,283,291,299],"ul",{},[272,273,274,275,282],"li",{},"NIST, ",[191,276,278,281],{"href":193,"rel":277},[195],[152,279,280],{},"Digital Identity Guidelines"," (SP 800-63B)"," — the standard recommending breach-list screening over mandatory character-class complexity.",[272,284,285,286,290],{},"Troy Hunt, ",[191,287,289],{"href":203,"rel":288},[195],"Have I Been Pwned: Pwned Passwords"," — the public database of breach-exposed passwords used for exactly this kind of screening.",[272,292,293,294,298],{},"Duende Software, ",[191,295,297],{"href":209,"rel":296},[195],"\"Stop Accepting Breached Passwords: Integrating HaveIBeenPwned with Duende UserManagement\""," — a concrete implementation walkthrough of breach-based password validation using k-anonymity.",[272,300,301,302,306],{},"Randall Munroe, ",[191,303,305],{"href":229,"rel":304},[195],"\"Password Strength\"",", xkcd — the widely referenced comic making the case for long passphrases over artificial complexity.",{"title":308,"searchDepth":309,"depth":310,"links":311},"",1,2,[312,313,314,315,316,317,318],{"id":162,"depth":310,"text":163},{"id":185,"depth":310,"text":186},{"id":215,"depth":310,"text":216},{"id":222,"depth":310,"text":223},{"id":238,"depth":310,"text":239},{"id":256,"depth":310,"text":257},{"id":266,"depth":310,"text":267},"2026-07-29","Complexity rules check whether a password looks hard to guess. Attackers running credential stuffing don't care how a password is shaped — only whether it's already sitting in a breach dataset.","md",{},true,"/blog/password-security-not-complexity","7 min",{"title":143,"description":320},"blog/6.password-security-not-complexity","Security","FKUwML26z77Jrcx76Nw_ltnf5NGWpM7pYTH6f2WilHg",[331,336],{"title":332,"path":333,"stem":334,"description":335,"children":-1},"RSA Isn't Banned by FIPS or FAPI — But the Way Most People Use It Basically Is","/blog/rsa-fips-fapi-myth","blog/5.rsa-fips-fapi-myth","RSA was never dropped from FIPS or FAPI. What changed is which padding scheme is allowed, and the gap between RS256 and PS256 is exactly where the myth comes from.",{"title":337,"path":338,"stem":339,"description":340,"children":-1},"A Secure Implementation Cannot Save an Insecure Product Decision","/blog/security-in-product-requirements","blog/7.security-in-product-requirements","A team can build a specification flawlessly and still ship something insecure, if the specification itself was built on a security assumption that stopped being true years ago.",1785744836757]