Two parts of a Linux host write the same single character, *, and mean opposite things by it. The shadow file writes * to say that an account has no password and nobody should be able to log in as it. musl's crypt() writes * to say that it could not hash what it was handed. Give sshd a password longer than 256 bytes on a musl system and it compares one of those answers against the other, finds them identical, and opens a session.
That is CVE-2026-123158, which the researcher who found it, Ozgun Kultekin of Wakefield, named sshole. OpenSSH shipped the workaround in 10.6p2 on November 5 and Alpine published patched packages for every branch back to 3.16 the same day. CISA added it to the Known Exploited Vulnerabilities catalog on November 9, with a due date of November 12 and the forensic-triage flag set. NVD scores it 8.1 and classifies it CWE-394, unexpected status code or return value, which is an unusually honest label for what happened here.
Relevance verdict. Your exposure is decided by which C library your SSH daemon was linked against, not by which OpenSSH version you run. If your servers are Debian, Ubuntu, RHEL, Rocky, SUSE, or anything else built on glibc, this one is not yours, and patching OpenSSH across that fleet this week buys you nothing against it. The researcher built sshd in glibc and musl variants, with and without PAM, and ran roughly 17,600 login attempts against them; only the musl build without PAM accepted logins it should have refused. Debian, Ubuntu, Fedora, Dropbear and OpenBSD all held. What deserves your Tuesday is the musl corner of the estate that nobody keeps an inventory of: Alpine virtual machines and LXC guests, OpenWrt routers at branch sites, and appliances built with Buildroot or Yocto, which in a small business usually means the phone system, the NAS, and the camera recorder.
The reflex this advisory triggers, and where it misses
An OpenSSH authentication bypass reads like an instruction to patch every SSH host you own, in order, starting now. Follow that reflex and you will spend the two days before the KEV deadline upgrading hundreds of glibc servers that were never affected, while the handful of hosts that are affected sit at the edge of the network answering port 22.
Four conditions have to hold together, and the Alpine advisory is precise about them. The daemon has to be linked against musl. It has to be running without PAM, which on Alpine means the default openssh-server package rather than openssh-server-pam, or openssh-server-pam with UsePAM no set. PasswordAuthentication has to be on, which it is unless somebody turned it off. And the host needs both an account whose password field is a bare * and some other account carrying a real $5$ or $6$ hash, because the attack borrows that account's salt.
Two details inside those conditions are worth more than the CVE number. Alpine has shipped root with a * password field since 3.16, so on most Alpine hosts the first account condition is already met before anyone creates a user. And the alpine user on Alpine's cloud images holds passwordless doas, which makes a login as that account equivalent to root. Alpine's own advisory notes that images from 3.18.11 through 3.24.3 are safe on first boot and become exploitable the moment any account gains a real hash, which is to say the moment someone sets a console password.
The condition that saves most people is the dull one. PermitRootLogin prohibit-password is the OpenSSH default, so root is not directly reachable unless somebody set PermitRootLogin yes. Accounts that can escalate are the problem, and on a stock Alpine cloud image one of them is sitting there by design.
The character that means two things
The mechanism is two reasonable decisions, fourteen years apart, meeting on one line of C. In 2012 musl changed crypt() to report failure by returning an unmatchable string rather than NULL, and its SHA-crypt implementation capped the input key at 256 bytes. A 257-byte password is a failure, and the failure is reported as *. In 2016 OpenSSH added a defence against username enumeration (the change behind CVE-2016-6210, which the researcher's own writeup traces this bug back to): when an account cannot supply a usable salt, sshd borrows one from another account so that timing stays constant. Accounts whose password field is * are too short to contain a salt, so they take that same borrowed-salt path. The comparison at the end of it then has * on both sides.
# musl, since 2012: crypt() reports failure as an unmatchable string, and
# SHA-crypt caps the key at KEY_MAX 256 bytes. So a 257-byte password fails:
crypt("<257 bytes>", "$6$rounds=5000$...") -> "*"
# /etc/shadow on Alpine since 3.16: an account with no password
root:*:20400:0:::::
# OpenSSH 10.6p1 and earlier, auth-passwd.c, sys_auth_passwd()
return encrypted_password != NULL &&
strcmp(encrypted_password, pw_password) == 0; /* "*" == "*" -> 1 */
# OpenSSH 10.6p2 adds a floor that no real hash can fall below
#define MIN_PASSWORD_HASH_LEN 4
ret = encrypted_password != NULL &&
strlen(pw_password) >= MIN_PASSWORD_HASH_LEN &&
strcmp(encrypted_password, pw_password) == 0;
The upstream commit is titled "avoid password authn bypass" and the message says the maintainers do not consider the root cause to be inside OpenSSH, only that OpenSSH is well placed to avoid it. They are right on both counts, which is why the fix is a length floor rather than a logic change. Four characters is below any real hash and above a one-character sentinel. The same commit also makes shadow_pw() always return a heap copy that the caller frees, which cleans up ownership in pick_salt() at the same time.
Note what the bug cannot do. It never bypasses a password on an account that has a real hash, because that comparison runs against actual hash output. An attacker needs the name of a * account, which on Alpine is a short list and begins with root.
Find the hosts where all four conditions hold
The hard part of this advisory is inventory. Most teams can name every Ubuntu server they run and cannot name a single musl host, because musl hosts arrive as appliances, images, and someone's convenient container base. Four fields answer the question, and you can collect them over the SSH access you already have.
# Collect the four deciding fields from every host you hold a key for
cat > /tmp/sshole-check.sh <<'EOF'
#!/bin/sh
ldd --version 2>&1 | head -1 | grep -qi musl && libc=musl || libc=glibc
pam=$(sshd -T 2>/dev/null | awk '/^usepam/{print $2}')
pwauth=$(sshd -T 2>/dev/null | awk '/^passwordauthentication/{print $2}')
rootlogin=$(sshd -T 2>/dev/null | awk '/^permitrootlogin/{print $2}')
star=$(awk -F: '$2=="*"{c++} END{print c+0}' /etc/shadow 2>/dev/null)
salt=$(awk -F: '$2 ~ /^\$[56]\$/{c++} END{print c+0}' /etc/shadow 2>/dev/null)
echo "$(hostname) libc=$libc usepam=$pam passauth=$pwauth root=$rootlogin \
star=$star salt=$salt $(ssh -V 2>&1 | cut -d, -f1)"
EOF
while read -r h; do
ssh -o BatchMode=yes -o ConnectTimeout=5 "$h" 'sh -s' < /tmp/sshole-check.sh \
|| echo "$h unreachable"
done < hosts.txt
# On Alpine specifically, which of the two server packages is installed
apk list -I 2>/dev/null | grep -E '^openssh-server(-pam)?-'
Reading the result
libc=glibc. Out of scope. Record the host and the date you checked, and do not touch its SSH configuration this week.usepam=yes. Out of scope through this path even on musl, because the password check never reachescrypt().passauth=no. The attack has nowhere to land. Keep it that way and patch on your normal cycle.libc=musl,usepam=no,passauth=yes,starandsaltboth non-zero. Exploitable right now. Patch, restart the daemon, and run the log triage below.- musl with
star=0orsalt=0. Not exploitable today, and one new key-only user or one console password changes that. Patch it anyway rather than relying on a count that drifts.
For anything you cannot log into - the router at the branch office, the recorder in the ceiling, the phone system in the closet - the question becomes whether port 22 is reachable from anywhere it should not be. OpenWrt, Buildroot and Yocto all build against musl, and none of them had published a fix status when CISA set the deadline. Where you cannot patch, remove the exposure instead.
What the telemetry supports, and what it does not
The vendor reports on this one disagree, and the disagreement is instructive rather than embarrassing. Previdian said its first observed attempt arrived less than nine hours after the CVE was published, confirmed root on a honeypot running real Alpine, and later counted twenty attempts from thirteen addresses across nine countries; the post-exploitation it saw was id, uname and hostname over non-interactive sessions. GreyNoise put its first successful exploitation at November 7, 00:46 UTC, roughly forty hours after the fix, and logged sixteen successful chains from six addresses against networks it monitors, with cloud metadata queries, attempts to read password hashes and root SSH keys, and authorized_keys writes for persistence. One of those chains installed a Python implant whose comments and error strings were in Chinese; it never reached its command server.
Set that against the same company's scan figures: 27.5 million port-22 scans and 20.1 million attempted SSH sessions over the period, none of which it attributes to this bug. The honest read is that exploitation is real, automated, opportunistic, and so far small, and that no published figure tells you how many hosts actually fell. GreyNoise says as much about its own numbers.
Which brings the forensic-triage flag on the KEV entry into focus. CISA is not asking which version you run today. It is asking whether anyone logged in as an account that has no password, and the answer lives in logs that rotate. Alpine writes sshd events to /var/log/messages, and the artefact is an Accepted password line for an account that cannot have one.
# The contradiction to search for: a password login to a *-password account
grep -hE 'sshd.*Accepted password for (root|alpine)\b' /var/log/messages* \
/var/log/auth.log* 2>/dev/null
# Build the account list from the host itself, then search on it
for u in $(awk -F: '$2=="*"{print $1}' /etc/shadow); do
grep -hE "sshd.*Accepted password for $u\b" /var/log/messages* 2>/dev/null
done
# If anything matched, these are the next three questions
grep -hE 'doas|sudo' /var/log/messages* | tail -40
find /home /root -name authorized_keys -newermt '2026-11-05' -ls
stat -c '%n %y' /etc/passwd /etc/shadow /etc/ssh/sshd_config
One Accepted password line for a * account is the whole finding. Treat the host as compromised rather than patched, preserve the logs before they rotate, and work the keys and the escalation paths rather than reinstalling over whatever is already there.
Confirm the running sshd is the one you upgraded
The step that gets skipped on this advisory is the restart. apk upgrade replaces the binary on disk and leaves the vulnerable daemon running in memory, serving port 22, until something restarts it. Alpine's advisory says so explicitly, and it is the difference between a patched host and a host that reports as patched.
So, three things before Thursday. Run the sweep and write down which hosts are musl, because that list is the asset you were missing and it will be useful again. On every musl host, apk upgrade -U -a, then rc-service sshd restart, then ssh -V to confirm 10.6p2 or the branch fix, and sshd -T | grep -i passwordauthentication to confirm what the running daemon believes. Where you cannot patch, write PasswordAuthentication no into /etc/ssh/sshd_config.d/00-no-passwords.conf, restart, and verify key login works before you close the session you are holding.
Then grep the logs while you still have them. The patch answers the vulnerability; the logs answer the KEV entry, and only one of those two has a deadline you can still miss quietly. If the logs have already rotated past November 5, the correct answer is that you do not know, and the next step is a compromise assessment rather than a reassuring note in the ticket. That is the engagement we get called for a week later, and the week of delay is the expensive part.
Not sure which of your hosts run musl?
We build the exposure inventory that answers questions like this in an hour instead of a week, and we run the compromise assessment when the logs cannot rule a login out. Book a session and bring your host list.
