The OpenSSH 10.6p2 security note, dated 5 November, is a single paragraph, and one of its sentences says: "We consider the root cause of this bug to be outside sshd, but it is exploitable via sshd and can be avoided there." Rich Felker, musl's maintainer, replies just as calmly that his side is not a bug either, but "an erroneous use by the caller". Both are right. And a root shell fits between those two sentences.
Three pieces, none of them broken
CVE-2026-123158, named *sshole by the researcher who found it, carries a CVSS of 8.1 and an explanation that takes three steps. None of the three is a mistake.
One: musl returns an asterisk when it cannot do the job. The crypt_sha512() implementation refuses keys longer than 256 bytes, and in that case crypt() returns the string "*". It has worked that way since 2012, and not by oversight: POSIX says a failure should return a null pointer and set errno, but musl chose to return a string on purpose because, in its own words, "a large portion of programs which call crypt are not prepared for its failure and do not check that the return value is non-null". In other words: they broke the standard to protect you from your own careless code.
You do not have to take our word for it. It is a handful of lines of src/crypt/crypt_sha512.c, which we went and read in musl's own repository:
* added for DoS protection. [...] */
#define KEY_MAX 256
/* reject large keys */
for (i = 0; i <= KEY_MAX && key[i]; i++);
if (i > KEY_MAX)
return 0;
[...]
p = sha512crypt(key, setting, output);
q = sha512crypt(testkey, testsetting, testbuf);
if (!p || q != testbuf || memcmp(...))
return "*";
return p;
There are two things in there we have not seen told in any of the coverage. First: the 256-byte cap is no whim either — it carries its own comment saying it was added "for DoS protection", because the algorithm's cost grows with the square of the key length. Second: that return "*" fires for three reasons at once, and two of the three are the function failing a self-check it runs on itself on every call. Both are hardening measures. The string that ends up opening the account is, from end to end, the result of protecting the machine.
Two: in Unix, an asterisk means "no entry this way". When the password field in /etc/shadow holds a *, the account has no valid password and no hash can ever match that value. It is the long-standing convention for a system account, or for an account that only logs in with a public key.
Three: sshd computes the hash anyway, so as not to give you away. Since 7.3, from August 2016, OpenSSH always runs the password computation even when the account is locked or does not exist, so that response time does not reveal whether the user is real. And since a * is too short to serve as a salt, it borrows the salt from the first local account that does have a real hash. The function is called pick_salt(), it lives in openbsd-compat/xcrypt.c and its comment states the rule plainly: it walks the system accounts "until we find one that has a valid salt. Usually this will be root unless the root account is locked out". That is the reason behind the advisory's oddest precondition: the machine also needs to have some account with an ordinary password.
Put the three together and the result fits in three lines. In auth-passwd.c, the function sys_auth_passwd() uses strcmp() to compare what crypt() returned against what is stored:
deploy:*:20400:0:99999:7:::
# what musl returns if the password is over 256 bytes
crypt("AAAA...257...AAAA", salt) -> "*"
# what sshd does with both
strcmp("*", "*") == 0 -> authenticated
All three decisions carry a date: 2012 (musl chooses to return a string rather than a null), August 2016 (OpenSSH 7.3 starts borrowing the salt) and the Alpine default configuration that completes the chain. Ten years. Nobody blundered; it is just that nobody was watching the gap between the three.
Why nothing happened on Debian
The very same sshd, with the very same strcmp() and the very same borrowed salt, has been running on glibc for ten years without this being exploitable. The reason is almost funny: the password-hashing library those systems use returns "*0" or "*1" on failure, never a bare asterisk. Two characters instead of one.
The one lesson in this story that outlives the patch is here: what stood between your server and an open session was not a security control. It was a convention about how an error is flagged in a return value, chosen by two separate projects that never spoke to each other and never documented it as a boundary. At everyWAN we have been saying for a while that failure is inevitable and that an outage is a design decision; this case adds a less comfortable footnote: sometimes the outage never happens at all thanks to a decision nobody took.
The accounts you hardened are the ones that open
An account with a password — even a bad password — cannot be exploited this way, because its field holds a real hash and the asterisk does not match it. The accounts that do open are the ones with a *: the ones somebody, at some point, decided to leave key-only. That is, the ones that were done right.
The target list seen in the first attacks says it plainly. Threat intelligence firm Previdian caught the first attempt less than nine hours after the CVE went public: eight attempts, a single IP address hosted in Japan, a single sensor. The accounts tried were root, key-only deploy users, directory users and the default user on Alpine images for AWS and Azure. On one honeypot they reached root.
That list is no accident: it is an inventory of everything that, on a well-built modern machine, has no password. The user your deployment pipeline logs in with to run docker stack deploy. The users that come from a directory and have no local entry. The cloud-image user nobody has touched since the instance was created. And an honest note about root, because there is no need to overstate it: sshd does not allow root password logins in its default configuration, so there the attacker was trying his luck. On the rest, he was not.
Alpine's own advisory sums it up in two sentences that leave little room for interpretation: "An attacker who can reach your SSH port can log in without knowing any password. It takes one connection, works every time, and needs no retries." That is the affected distribution talking about itself, which is not usually where you find overstatement.
What is really exposed (and what is not)
Here comes the advice you will not read in most of the coverage: do not rush to rebuild your three hundred Alpine images. The vast majority of Alpine-based containers carry no sshd inside, and without an sshd listening this simply does not exist. Spending Friday rebuilding all of them means burning the only scarce resource you have — your team's attention — in the wrong place.
The conditions must hold all at once: musl libc; sshd built or configured without PAM (Alpine's default openssh-server package, or the -pam one with UsePAM no); password authentication enabled; an account with an asterisk; and another account with a real hash to borrow the salt from. Remove any one of the five and the attack is off. One caveat against our own headline, just in case: the 257-byte threshold applies to SHA-256 and SHA-512 hashes, which are the usual ones; according to TuxCare's analysis, with a different hash type on the account the salt is borrowed from, the length limit stops mattering. So 257 is the typical case, not the only one.
OpenWrt appears on nearly every list of affected systems and almost never is. OpenWrt does use musl, but its default SSH server is Dropbear, and Dropbear is not affected — musl's own maintainer says so. It only counts if somebody manually installed the OpenSSH server package. The same precision applies to Buildroot and Yocto: the question is not "is this musl?" but "is this musl and does it have OpenSSH listening?". That distinction can save you half a week, and it is the kind of nuance that separates a list of affected systems from a plan.
Where it does bite is in the places nobody calls "a server": build images and CI runners with an sshd for debugging, diagnostic containers, bastion hosts built on Alpine because it was small, embedded appliances on Buildroot or Yocto, and cloud instances brought up from an official Alpine image. It is the same pattern we saw with SSH on network gear: the box is not in the inventory because it does not look like a server.
The worst case is in Alpine's advisory and almost nobody has highlighted it: the official tiny-cloud images, from 3.18.11 through 3.24.3, ship an alpine user with no password and passwordless doas. That is not logging in as some ordinary user: logging in as alpine is logging in as root, in one hop. If you spun up an instance from one of those images and left port 22 open, that is the machine to start with today. Two honest caveats pulling the other way: Alpine itself notes this becomes exploitable once the system has some password hash on it, and your real exposure depends on your security group.
You do not patch a container: you rebuild it
If one of the genuinely affected things is a container, there is a classic trap waiting for you. Shelling in and running apk upgrade fixes the problem until the next deploy, at which point the orchestrator pulls the same old image again and the hole quietly comes back. The real fix is rebuilding the image and redeploying it.
That requires three things you cannot improvise on a Thursday afternoon: knowing which images you have and where they run, pinning the base image to a specific version — not to latest, which is not a version — and having a pipeline that rebuilds and deploys without anyone touching a server by hand. That is how we run the infrastructure we operate: Docker Swarm and Kubernetes with continuous deployment from GitLab, and NetBox as the single source of truth for what exists. It is not sophistication: it is that on the day a CVE like this lands, the difference between half an hour and three days is exactly that.
The OpenSSH version does not tell you whether you are patched
And for what is not a container — the box in the corridor cabinet, the bastion host someone who no longer works here built — the route is the distribution package. Alpine shipped the fix for ten branches: the nine stable ones from 3.16 to 3.24, plus edge. And it is worth spelling out what that means, because its own advisory has a section headed "Releases that are no longer supported (3.21 and older)" explaining that "3.21 reached its end of support on 2026-11-01, but we have published fixed packages for each release all the way back to 3.16". Six out-of-support branches patched, one of them expired four days before the CVE. The specific versions appear in almost no coverage, so we pulled them ourselves from its security database:
3.17 openssh-9.1_p1-r7 3.22 openssh-10.0_p1-r11
3.18 openssh-9.3_p2-r4 3.23 openssh-10.2_p1-r1
3.19 openssh-9.6_p1-r3 3.24 openssh-10.3_p1-r2
3.20 openssh-9.7_p1-r6 edge openssh-10.6_p2-r0
Look down that column carefully, because it has a practical consequence that takes a while to work out on your own: only edge actually carries 10.6p2. The nine stable branches get the patch backported onto whatever they already had, which ranges from a 9.0 from 2022 to a 10.3. Put another way: the OpenSSH version does not tell you whether you are patched. A system announcing OpenSSH_9.0 may be fixed and one announcing 10.3 may not be. What you have to look at is the package version, with its -rN suffix. If you cannot touch the machine today, the mitigation is further down.
And the step that turns all of the above into nothing if you forget it, said by Alpine in so many words: "Upgrading the package alone does not replace the running daemon." Until you run rc-service sshd restart, the process accepting connections is still the old one. A whole estate can end up like that: patched in the inventory and vulnerable on the port.
The four checks for this week
1. Which of your systems speaks musl and also listens on 22. The two questions go together; on their own neither is useful. The first takes a second — if /lib/ld-musl-* exists, it is musl — and the second comes from the package manager, which also gives you the -rN revision, the only thing that tells you whether you are fixed:
apk list -I 'openssh*'
2. What your configuration really allows, not what you think it does. There is a meaningful difference between what the file says and what sshd applies after reading the Include directives. You ask for it like this, with two caveats: sshd lives in /usr/sbin and needs root plus host keys already generated, and Match blocks are not evaluated unless you hand it a specific case with -C user=...,host=...,addr=.... If you have per-user or per-source policies, the output below is not telling you about them:
# Alpine's stopgap mitigation, if you cannot update today
mkdir -p /etc/ssh/sshd_config.d
echo 'PasswordAuthentication no' > /etc/ssh/sshd_config.d/00-no-passwords.conf
sed -i.bak -e '1i PasswordAuthentication no' \
-e 's/^\(\s*PasswordAuthentication[^y#]*\)yes/\1no/i' \
/etc/ssh/sshd_config
sshd -t && rc-service sshd restart
sshd -T | grep -i passwordauthentication
That block is Alpine's, copied as it stands, and it is worth understanding why the first line alone is not enough. A file dropped into sshd_config.d/ only does anything if the main sshd_config carries the Include directive. Hence the sed, which does two things: it turns every PasswordAuthentication yes line in the file to no, including those inside a Match block, and it adds the directive at the top if there was none. That "at the top" is not cosmetic: sshd keeps the first value it finds for each keyword, so a line appended at the end of a file that already said yes cancels nothing. And the warning Alpine puts ahead of all this, which we second: make sure your key login works and keep your current session open until you have tested a new one.
3. Which accounts carry the asterisk. One line, and it is the check that actually tells you something you did not know:
awk -F: '$2 == "*" { print $1 }' /etc/passwd
That output is the bulk of the list of accounts this bug opens on that machine, but not all of it: an account can carry the asterisk in /etc/passwd and have no entry in /etc/shadow, so run the same awk over the other file too. And if your users come from a directory, they will be in neither: those you have to ask the directory about. Read it slowly, because there are two kinds of name in there: system accounts that were never going to log in over SSH anyway, and the accounts someone deliberately hardened. The second kind is what matters, and it is the kind that comes out marked green in any audit.
4. The log, crossed against that list. This is not a patching task, it is an incident hunt, and it is the only one of the four you must do even if you have already updated. Look for an Accepted password — on Alpine, in /var/log/messages — for any of the accounts on the previous list. An account with no password cannot have logged in with a password: if it shows up, that is not a warning, it is an incident, and the date to look from is 5 November.
The inventory that matters is not of servers
The patch sits in sshd, and not because the bug was sshd's: it sits there because that is where it could be reached. We went and read the corrected code, and the piece is two conditions added to an if: that the salt must not be null, and that the stored field must be at least MIN_PASSWORD_HASH_LEN long, a constant the same file defines as 4. A *, a ! or an x can no longer ever produce a valid comparison. On the other side, in the very message where he says it is not his bug, Felker adds something that gets quoted a lot less: "the mechanism by which this happened is something we have been wanting to change for a while now, and it very likely will be changed in the upcoming release cycle". And in the meantime he recommends the same setting we gave you above: "Anyone who cannot immediately upgrade should edit /etc/ssh/sshd_config to set: PasswordAuthentication no". Two fixes for a bug that, formally, had no owner.
The conclusion we take away is not "patch faster". You knew that already, and besides, there is not always a patch to apply. It is another one, and it is less comfortable: for ten years, the line keeping anyone out of your bastion host was not in any document, any policy or any dashboard. It was in the gap between three projects each doing its job properly in isolation. No control protects you from that; the only thing that helps is having the complete list of things that listen — not the list of servers, the list of things that listen — and being able to rebuild them in hours. In most of the companies we come across, nobody has that list in full.
Sources (verified on 2026-11-09): the sentence "We consider the root cause of this bug to be outside sshd, but it is exploitable via sshd and can be avoided there", the scope ("affects some Linux systems when built and run with PAM support disabled") and the credit to Özgün Kültekin of Wakefield — OpenSSH 10.6p2 release notes, 2026-11-05. The mechanism (that musl returns the string "*" when crypt() fails and that its SHA-512 code refuses keys over 256 bytes we verified ourselves in the source: src/crypt/crypt_sha512.c, with its #define KEY_MAX 256, the "added for DoS protection" comment and the return "*" in __crypt_sha512, the 257-to-1,024-byte window, the salt borrowed from another account with a valid hash introduced in OpenSSH 7.3 in August 2016 as a defence against user enumeration, the strcmp() comparison in sys_auth_passwd() in auth-passwd.c, which we read in the repository along with pick_salt() and its comment in openbsd-compat/xcrypt.c and the #define MIN_PASSWORD_HASH_LEN 4 in the already-fixed code, the 8.1 CVSS and the fact that glibc with libxcrypt returns "*0" or "*1" and is therefore not exploitable), Rich Felker's position ("It is my position that this is not a bug in musl just an erroneous use by the caller") and his statement that the behaviour will change, musl's historical rationale for not returning a null pointer ("a large portion of programs which call crypt are not prepared for its failure and do not check that the return value is non-null") and the shape of the 10.6p2 fix (rejecting stored fields shorter than four characters) — analyses by TuxCare, OX Security and Horizon3.ai. That Dropbear is not affected according to musl's own maintainer, and that OpenWrt therefore only counts where someone installed the OpenSSH server, is the precision those same analyses record. The fixed Alpine versions we looked up ourselves, branch by branch, in the official security database (secdb.alpinelinux.org, the main.json files for v3.16 through v3.24 and edge): all ten have an entry for this CVE, and the observation that only edge carries 10.6p2 while the stable branches get the patch backported — and therefore that the OpenSSH version does not tell you whether you are fixed — is our own reading of that table. Field detection (first attempt less than nine hours after publication, eight attempts from a single IP hosted in Japan on one sensor, target accounts root, key-only deploy users, directory users and the default user on AWS and Azure Alpine images, and one root access on a honeypot) — Help Net Security, 2026-11-06. The two quoted sentences ("An attacker who can reach your SSH port…" and "It takes one connection, works every time, and needs no retries"), the branch-by-branch table of fixed versions, the warning that upgrading the package does not replace the running daemon, and the detail about the tiny-cloud images from 3.18.11 through 3.24.3 carrying an alpine user with no password and passwordless doas — Alpine Linux security advisory, 2026-11-05; it is worth saying that those sentences come from that advisory and not from Damien Miller, whom Alpine credits for the fix alongside Darren Tucker. TuxCare's analysis describes a second attempt being needed within the same connection in certain configurations; the affected distribution's own advisory says the opposite ("needs no retries") and we go with the distribution, which is the party answerable for its packages. The reading that an error-value convention was acting as a security boundary without anyone declaring it, the advice not to rebuild every Alpine image, and the four checks are our own work. Photograph: "A front-facing shot of a black wall-mounted network rack…", by Mohammed Kateregga, CC0 1.0 via the WordPress photo directory, cropped and brightened by us.
Do you know how many of your systems listen on port 22?
We build the inventory of everything you have powered on — servers, containers, appliances and whatever was installed by someone who no longer works there — with its version and its last patch date. It is the least glamorous part of IT maintenance, and it is what decides whether a Wednesday advisory costs you half an hour or half a week.
Talk to everyWAN