I read OpenSSH release notes by jumping straight to the security section, because the feature list is usually someone else’s problem. On November 5, 2026, that section was one item long. Version 10.6p2, a point release a month after 10.6, fixes CVE-2026-123158: on Linux systems using musl libc with PAM disabled, sshd accepts any password longer than 256 bytes for an account whose password field is *.
That sentence undersells what happened. * is how you lock an account in /etc/shadow. It is what Alpine has shipped for root since 3.16. It is what adduser leaves behind on a key-only deploy user. The bug does not let an attacker guess a password. It lets them skip the concept of one.
🔓 What the Bypass Actually Does
The chain is three pieces of C behavior that nobody wrote down in the same document.
Without PAM, sshd hashes the submitted password with crypt() and compares the result to the stored field. An account with * has no usable hash, so since a July 2016 change (the fix for CVE-2016-6210, username enumeration through response timing), sshd borrows a salt from another account that does have a real hash. It hashes the attempt anyway, spends roughly the same time, and rejects it. That was the intent.
musl’s crypt() has its own rule. Since August 2012 it returns * instead of NULL when hashing fails, because too many programs never checked for NULL and simply crashed. Twenty days later musl added SHA-256 and SHA-512 crypt with a 256-byte key cap, added for DoS protection, and anything longer than that fails. Fails means *.
So the stored field is *, the hash attempt returns *, strcmp says they are equal, and the login is accepted. One connection, one attempt, no retries. The one extra requirement is another account on the machine with a $5$ or $6$ hash, because that is where the borrowed salt comes from.
The CVE record describes passwords “between 256 and 1024 characters.” Kültekin’s write-up says 257 to 1,024 bytes: the boundary is musl’s “longer than 256” check, and 1,024 is sshd’s own per-password limit. The off-by-one does not change who is exposed.
🧩 Ten Years of Individually Correct Decisions
None of those three decisions is a mistake on its own. musl picked a sentinel that can never match a real hash. OpenSSH kept hashing for invalid users to close a timing leak. Alpine chose * over ! in 3.16 because ! also breaks SSH keys on a PAM-less build, and root had shipped with no password at all before that, which was CVE-2019-5021.
The bug lives where the decisions meet, in the glue between sshd and libc, which is the part of the codebase almost nobody reads twice. Kültekin says he got there by building sshd in every combination he could think of (glibc, musl, PAM, no PAM, shadow, no shadow) and throwing about 17,600 malformed logins at each build. Every build agreed on every login except one: musl without PAM said yes to things it should have said no to.
OpenSSH’s fix is four characters. MIN_PASSWORD_HASH_LEN 4, so a stored field too short to hold a salt can never compare equal, in commit 4353fdc across auth-passwd.c and two compatibility files. That is the whole patch. The project calls it a workaround and says the root cause sits outside OpenSSH, which is fair: musl still returns a value that means “error” and looks like “no password.”
🐳 The Default Alpine Box Is the Worst Case
The conditions sound narrow until you check them against an image you have actually deployed. Alpine’s openssh-server is built without PAM. PasswordAuthentication defaults to yes. root is root:*. A key-only user is *. All four conditions are true on a stock Alpine host that accepts SSH and has one account with a real password.
The official tiny-cloud images from 3.18.11 through 3.24.3 are worse. They ship an alpine user with a * field and passwordless doas. That is safe on first boot and exploitable the moment any $6$ hash lands on the machine, for example when an admin sets a console password or cloud-init creates a user with hashed_passwd. Then the bypass gets you a shell as alpine, and doas gets you root.
Alpine shipped fixed packages the same day, and went further than most vendors would. 3.21.10, 3.22.8, 3.23.8 and 3.24.4 carry the fix on supported branches, and there are backports to 3.16 through 3.20, none of which get regular updates. 3.21 had reached end of support four days earlier, on 2026-11-01.
Two operational details the advisory puts in bold, and that people will still get wrong. Upgrading the package does not restart sshd, so rc-service sshd restart is part of the fix. And if you cannot patch, PasswordAuthentication no closes the hole, but only after you have confirmed key authentication works, with your current session still open.
Unaffected: glibc distributions, anything checking passwords through PAM (openssh-server-pam with UsePAM yes), accounts locked with !, Dropbear, OpenBSD, and containers that do not run sshd. Using Alpine as a base image is not exposure by itself.
🤖 The Release Cadence Is the Actual News
This is the paragraph I keep coming back to. Damien Miller, announcing 10.6p2 on openssh-unix-dev:
“We are making this release as fast as possible (we learned of the problem yesterday) because we have recently observed multiple cases of rapid bug rediscovery by AI agents, sometimes within hours, and so now consider long embargo periods to be a dangerous practice that primarily benefits attackers.”
That is a policy change with a receipt. The 10.6 release notes from October 6 already said the team was receiving a large number of AI-assisted reports, that some AI findings were later independently rediscovered by human researchers, and that it would ship more frequent releases instead of batching fixes until the next planned one. 10.6p2 is the first test of that promise, and it passed: the team learned of the bug one day and had a signed release the next, with Alpine packaging on the same day.
For everyone downstream, this cuts both ways. Faster disclosure means less time running a known-bad daemon. It also makes stable branches a shared maintenance burden. Alpine had to backport the check to nine release branches, including ones that no longer get regular updates, because “wait for the next minor release” is no longer how this works. If you maintain a fork or an internal build of OpenSSH, the cadence you plan against just changed.
⚖️ Where the Score and the Reality Disagree
MITRE assigned CVE-2026-123158 the same day, with CVSS 3.1 at 8.1 and AC:H, high attack complexity. CISA’s SSVC entry says exploitation is poc, automatable no, technical impact total. No in-the-wild exploitation has been reported. The researcher’s site ships a browser demo, and two lab repos are linked from the CVE record, so “proof of concept” is the honest label.
The 8.1 is defensible arithmetic: no privileges required, no user interaction, full confidentiality, integrity and availability impact, but the attack only works when the libc and the account layout line up. What a score cannot express is that on Alpine those conditions are not an exotic edge case, they are the default. Complexity is a property of the target as much as of the attack, and default configurations are the least complex targets there are.
So I would not read 8.1 as “patch next cycle.” The advisory’s own test is simpler. If grep "Accepted password" /var/log/messages shows a password login for an account whose /etc/shadow field is *, you were not patching late. You were compromised.
🧠 Final Thoughts
The bug is good archaeology, and the write-up at sshhole.com is worth an hour, but I do not think that is the part that lasts. What lasts is a maintainer looking at an embargo calendar and deciding the calendar was the vulnerability. AI agents are now good enough at generating weird inputs that the same flaw gets found twice, so the gap between “one researcher knows” and “everyone can know” has collapsed.
If you take one thing from this: audit your musl hosts for * accounts, patch them, and restart the daemon. If you take two: read the security section of the release notes, because the cadence of those releases is starting to say more about the ecosystem than the feature list does.
Thanks for reading.