▶Prefer to watch? This short video runs this page's main commands in a real lab, with on-screen captions and a synthetic voiceover (an AI voice trained on the author's own recording). Click to play, or watch it on YouTube.
SSHole: an OpenSSH password authentication bypass
CVE-2026-123158 CVSS 3.1 8.1 (High) fixed in OpenSSH 10.6p2, November 5, 2026
A ten-year-old accident lets a long enough password log in to an account that has no password at all. It needs a narrower combination than most advisories: a Linux system built on musl instead of glibc, sshd compiled without PAM, and at least one account whose password field holds only an asterisk. Alpine Linux ships that combination by default, and so do some router and embedded builds. Debian, Ubuntu, Red Hat, Rocky and OpenBSD are not affected.
This page is for the machines the update does not reach: an end-of-life Alpine release, or a device whose builder has not shipped the fix. It shows how to confirm a machine has the vulnerable shape, without sending any crafted login, and how to reduce the risk by hand.
Reports disagree on how actively this is being exploited: one research firm says it saw root obtained on a honeypot within hours of disclosure, another says it found no confirmed in-the-wild exploitation. Treat it as exploited and move quickly either way.
OpenSSH's own release notes say plainly that the underlying flaw is not in sshd itself: "We consider the root cause of this bug to be outside sshd, but it is exploitable via sshd and can be avoided there." The fix in sshd is a workaround, not a repair of the real bug.
Everything below was run in lab containers on 2026-11-08 (Alpine 3.15, end of life, with OpenSSH 8.8p1, as the unpatched box; Alpine 3.21 current, with OpenSSH 9.9p2, as the patched box). No step here sends a password to sshd: everything is confirmed by reading the shadow file, the running configuration and the package version. Defensive notes only: no working exploit here, and nothing in this guide logs in anywhere.
What it is, in plain English
Unix has used the character * in the password field of /etc/shadow since 1980 to mean "this will never match anything a real password hashes to." Accounts with no password, such as service accounts or cloud images waiting for a key, use it.
musl, the C library many small Linux distributions use instead of glibc, has returned that same * since 2012 when a submitted password is too long for crypt() to hash (longer than somewhere between 256 and 1024 bytes, depending on which report you read; OpenSSH's own notes do not give the number).
sshd normally lets PAM decide whether a password is correct. Built without PAM, as Alpine's default openssh-server is, sshd hashes the password itself and compares the result with the stored field. If musl just returned * because the password was too long, and the stored field is also *, the two asterisks match, and sshd lets the login through.
One more piece, from a 2016 OpenSSH change that fixed a username-enumeration bug (CVE-2016-6210): when an account has no usable hash to compare against, sshd borrows the salt from a different account's real hash on the same machine, so it can still perform a comparison and take the same amount of time either way. That borrowed comparison is what musl's * can match.
All four conditions have to be true together: sshd built without PAM, password authentication turned on (OpenSSH's own default), an account with exactly * in its shadow field, and at least one other local account with a usable hash to borrow a salt from.
Who is affected
Needs ALL of: musl libc, sshd without PAM support, password authentication on, an account with * for a password.
Alpine Linux: affected by default. Its openssh-server package has never linked against PAM. Alpine's own cloud images (the "tiny cloud" AWS and Azure builds) ship a passwordless "alpine" user with doas access to root, which is exactly the shape the bug needs.
Other musl systems: OpenWrt, Buildroot and Yocto builds are named in reports as worth checking (not run in the lab; their sshd is often Dropbear, not OpenSSH, which is not affected).
Containers: a musl-based image (Alpine is the common one) that runs its own sshd, for example as a base for a jump box or CI runner.
NOT affected: glibc-based distributions (Debian, Ubuntu, Fedora, RHEL, Rocky, SUSE), any system using PAM to check passwords, Dropbear, and OpenBSD, which has its own sshd. Not affected if password authentication is already off.
PermitRootLogin: root has shipped with * on Alpine since roughly 3.16, but PermitRootLogin keeps root blocked by default regardless. The practical risk is other accounts with *, such as the cloud user.
Step 1: is this machine built the way the bug needs?
Three checks, none of them sends anything to sshd.
ldd /usr/sbin/sshd | grep -i musl # musl, not glibc. No output means this is probably a glibc system, and the bug does not apply
sshd -T 2>&1 | grep -i usepam # does this build even have PAM?
Alpine's build has no PAM support at all, so the grep finds nothing, not "no":
(no output at all) # this build was not compiled with PAM support: the vulnerable condition
On a build WITH PAM, the same grep would print a line like "usepam yes" or "usepam no" instead of nothing.
sshd -T 2>&1 | grep -i passwordauth # OpenSSH's default is "yes" if the config has no line for it
Lab, Alpine 3.15 (end of life), openssh-server 8.8_p1-r1:
ldd: libc.musl-x86_64.so.1
sshd -T: no usepam line at all
passwordauthentication yes (default: no line for it in the config)
All three match the vulnerable shape. The next step checks for the account, not the login.
Step 2: which accounts can actually be used
Read the shadow file. No password is sent to anything.
sudo awk -F: '$2=="*"{print $1}' /etc/shadow # accounts with EXACTLY * : the value the bug needs
sudo awk -F: '$2=="!"{print $1}' /etc/shadow # locked accounts (!) are a DIFFERENT marker and are NOT affected
sudo awk -F: '$2 ~ /^\$[0-9]+\$/{print $1}' /etc/shadow # accounts with a real hash: the salt source the bug borrows from
The exact character matters. Lab, Alpine 3.15 fresh install: every system account (root, bin, daemon, sshd and the rest) uses !, 27 of them, not *. None of those are affected. * does not appear until an account is specifically left without a password, which is how Alpine's tiny-cloud images ship their default user.
# simulate what a tiny-cloud image looks like, to show what the audit finds (this account is never logged into):
sudo adduser -D -s /bin/sh cloud
echo 'cloud:*:::::::' | sudo sh -c 'read l; u=$(echo "$l"|cut -d: -f1); sudo usermod -p "*" "$u" 2>/dev/null || sed -i "s|^$u:[^:]*:|$u:*:|" /etc/shadow'
grep ^cloud: /etc/shadow
cloud:*:20765:0:99999:7:::
Lab result: the audit immediately shows the cloud account with the bare * marker, and the alpine account with a real $6$ hash sitting right next to it, which is everything the bug needs on this machine. No login was attempted to reach this conclusion.
If the audit finds nothing with a bare *, this specific bug does not apply to this machine, even if musl and no-PAM both matched.
Step 3: is the installed OpenSSH patched?
Compare the installed package version against the vendor's fixed-version table, the same approach as checking a Debian or Red Hat changelog.
apk list -I | grep openssh-server # Alpine (apk info -v openssh-server only prints the description, not the version)
Alpine Linux security page, fixed versions by branch (checked 2026-11-08):
edge 10.6_p2-r0 3.22 10.0_p1-r11
3.24 10.3_p1-r2 3.21 9.9_p2-r1
3.23 10.2_p1-r1 3.20 9.7_p1-r6
3.19 9.6_p1-r3
Lab, Alpine 3.15 (openssh-server-8.8_p1-r1): 3.15 has no row in that table at all. Alpine's branches are supported for about two years, and 3.15 is past that window: there is no fixed package for it to receive. This is the end-of-life case this page is for.
Lab, Alpine 3.21 current (openssh-server-9.9_p2-r1): matches the 3.21 row exactly. Patched.
Upstream, for anyone running OpenSSH built by hand rather than from a distribution package:
ssh -V # compare with 10.6p2 or later
OpenWrt, Buildroot, Yocto and other embedded musl systems: check that build's own advisory and version. Not run in the lab.
Manual mitigation: turn off password authentication
OpenSSH's own developer gave this as the immediate workaround, and it removes the bug's precondition entirely: if nothing is ever compared against a password hash, the borrowed-salt match cannot happen.
sudo vi /etc/ssh/sshd_config
PasswordAuthentication no
sudo sshd -t && echo 'config OK' # always check the syntax before reloading
sudo sshd -T 2>&1 | grep -i passwordauth # confirm the RUNNING config, not just the file
Lab result after the change:
config OK
passwordauthentication no
This only works for accounts you can already reach another way: set up a key in authorized_keys for any account you still need, and confirm you can log in with the key BEFORE you apply this to a machine you are not sitting at. Locking yourself out over ssh with no other access is the most common mistake here.
If the box must keep password logins for some accounts and not others, OpenSSH supports a Match block:
Match User cloud,guest
PasswordAuthentication no
(not run in the lab; test the syntax with sshd -t before relying on it)
Manual mitigation: remove the asterisk accounts from the login path
Where you cannot turn off password authentication for everyone, deal with the specific accounts the audit found in Step 2.
sudo passwd -l cloud # lock it: this sets a ! prefix, NOT a bare *, so it no longer matches the bug
grep ^cloud: /etc/shadow # confirm: the field now starts with !, which Step 1 showed is not the affected marker
Lab: after passwd -l, the field changed from cloud:*:... to cloud:!*:..., and the earlier shadow audit (Step 2) no longer lists this account among the * accounts.
Alternatives for an account that must still work, just not over an asterisk:
sudo passwd cloud # give it a real password, removing the * entirely
or delete the account if nothing needs it: sudo deluser cloud
For a cloud image's default user specifically, also check AuthorizedKeysFile is the only way in for that account, and that PermitRootLogin is not set to yes.
The real fix
Update OpenSSH to 10.6p2 or later, or your distribution's patched package, then RESTART sshd. Installing the package alone does not restart a running daemon: the old, vulnerable binary keeps accepting connections until you do.
Alpine: sudo apk upgrade openssh-server openssh-client && sudo rc-service sshd restart
(or: sudo service sshd restart, depending on the init system)
Confirm: apk list -I | grep openssh-server # compare with the branch's fixed version again
ps aux | grep sshd # a restarted master has a fresh start time
Distribution is end of life (no row in the fixed-version table, as with Alpine 3.15 above):
Move to a supported Alpine branch, or build OpenSSH 10.6p2 from source and package it yourself, which becomes your own maintenance burden from then on.
Meanwhile, apply both manual mitigations above, and treat the machine as higher risk until it is moved.
Then remove any mitigation you only added for this bug (a Match block you do not otherwise want), once the real fix is running and confirmed.
Look for signs of an attack
A successful attempt looks like an ordinary accepted password login for an account that should never produce one.
grep -E 'Accepted password for (root|alpine|cloud)' /var/log/auth.log /var/log/messages 2>/dev/null
# name the accounts your Step 2 audit found with a bare *
One report describes early recon over the new session as ordinary, short, non-interactive commands: id, uname, hostname, run right after the login succeeds and before anything else. Look for a session that runs those and little else, especially from an account that is not supposed to have an interactive shell at all.
Also check:
last -f /var/log/wtmp | head -20 # any login at all for an account with a bare * password
who -a # is anything logged in right now under such an account
sudo cat /etc/doas.d/*.conf 2>/dev/null; sudo cat /etc/sudoers.d/* 2>/dev/null
# does the affected account have doas or sudo to root, as Alpine's default cloud user does
A password of 257 or more characters is not something a normal client sends. If your access logs capture request or session sizes, an unusually long credential is itself a signal worth a closer look (not run in the lab: this needs your own logging setup).
Common problems
sshd -T shows nothing for usepam, and I expected yes or no: that means this sshd was not built with PAM support at all, which is itself the condition to worry about, not a missing check.
I locked myself out after setting PasswordAuthentication no: you needed a working key login first. If you still have a console or out-of-band access, undo the change, add a key, confirm it logs in, then try again.
apk info -v shows a number lower than the fixed table and I thought it was already patched: Alpine's version numbers look like the upstream OpenSSH version plus a package revision (-rN). Compare the WHOLE string, including the -r number, against the table for your exact branch.
My branch is not in the fixed-version table at all: that branch is end of life. There is no patched package coming for it. Move to a supported branch.
The shadow audit found nothing with a bare *, but I was told this box is affected: locked accounts use !, not *, and are not affected by this specific bug. Re-read the exact character, and remember glibc systems are not affected at all regardless of the shadow file.
PasswordAuthentication no did not stop a key-based login: that is correct and expected. The setting only affects password logins; key-based authentication is untouched.
Tips
Patch first when you can; the mitigations here are for the machines where you genuinely cannot, not a replacement for the update.
Check your containers, not only your hosts. A musl-based image (Alpine is the common base) that runs its own sshd has the same exposure as a full machine.
The shadow audit from Step 2 is cheap and safe to run everywhere, even on systems you are fairly sure are not affected. It takes a few seconds and sends nothing anywhere.
Cloud images deserve a second look specifically: a default user meant to be replaced by your own key at boot is exactly the shape this bug needs if that step is ever skipped.
Confirm a key login works before you turn off password authentication on anything you only reach over the network.
Write the date and the CVE in a comment wherever you add a Match block or a workaround, so a future cleanup knows what it is for and when it is safe to remove.
"It is probably locked" is not the same as checking the actual character in the shadow file. The audit takes one line; run it.
Sources (checked 2026-11-08)
OpenSSH 10.6p2 release notes (the fix, and the root-cause statement): https://www.openssh.org/txt/release-10.6p2
Alpine Linux security page for this CVE (fixed versions by branch): https://security.alpinelinux.org/vuln/CVE-2026-123158
Help Net Security (exploitation reports, indicators): https://www.helpnetsecurity.com/2026/11/06/openssh-sshole-cve-2026-123158-exploitation/
SecurityWeek: https://www.securityweek.com/openssh-rushes-patches-for-sshole-authentication-bypass/
Safe Security (disputes confirmed in-the-wild exploitation): https://safe.security/resources/blog/sshole-high-severity-openssh-vulnerability-turns-disabled-passwords-into-unauthorized-access/
DEV Community (mechanism write-up, salt borrowing, CVE-2016-6210 link): https://dev.to/rbonweb/the-openssh-sshole-vulnerability-two-different-kinds-of-no-match-accidentally-matched-l2a
Reports disagree on exploitation evidence and on the exact password length range; this page notes that in the introduction rather than picking one.