"Locked" Unix accounts aren't always locked. Here's a bug that's been sitting in plain sight since the 1980s, and why most people reading this are quietly exposed to it through a container they didn't write.
Background, because the mechanism is the whole story:
A locked account — root on a key-only box, your deploy user, an LDAP user — has a * in its password field. That's deliberate. * is not a valid hash, and nothing you can type hashes to *, so there is no password that gets you in. Bill Joy put a * next to the daemon account in 3BSD in 1980 for exactly this reason. It still works that way today.
How a password check actually runs: the system takes what you typed, hashes it with the stored salt, and compares the result to the stored field. Same string, you're in. If the field is *, there's nothing to match, so you're never in.
The fine print nobody honors: crypt() returns NULL when it can't do its job — unknown algorithm, malformed salt. The spec has said so since 1997. Plenty of code never checks for NULL and just compares whatever came back. On glibc that path was hardened. On musl — Alpine, OpenWRT, Buildroot, Yocto, and a very large share of the container images on your laptop right now — crypt() returns a non-NULL placeholder instead of NULL to avoid crashing those careless callers.
And that's the hinge. If the placeholder can ever equal the stored field for some input, the comparison that should always fail starts passing. On musl systems running OpenSSH built without PAM, for accounts whose stored field is *, a password in a specific long length band (reported at roughly 257–1024 bytes) walks crypt() down a path where the result lines up with the sentinel — and an account that is supposed to accept no password accepts one.
The lesson isn't "musl bad." It's that an authentication NULL-contract divergence between two libcs, plus one unchecked return value, plus a 40-year-old convention, quietly turns "this account is locked" into "this account takes any long-enough password" — on base images almost everyone ships and almost nobody audits.
What actually neutralizes the whole class: key-only SSH. PasswordAuthentication no and KbdInteractiveAuthentication no mean there is no password path for the bug to live in. I audited my own fleet against this — not exposed, because the servers are key-only by default and the images that are musl don't run sshd at all. If you can't say the same in one sentence, that's the work this week.
Three things to check tonight:
1. Which of your running images are musl/Alpine, and which of those run sshd.
2. Whether sshd is password-or-keyboard-interactive anywhere, especially on bastions and CI runners.
3. Whether your "locked" service accounts are locked by * and are relying on that alone.
The uncomfortable trend under all of this: post-xz, we finally started reading the auth paths in our dependencies instead of trusting them. This is the same genre — the exploit isn't a clever payload, it's a decades-old assumption that stopped being true three layers down in a container you inherited.
#Linux #OpenSSH #musl #ContainerSecurity #DevSecOps #SSH #InfrastructureSecurity #Alpine #PlatformEngineering #AppSec
commit e522bdb