On November 5, the OpenSSH project rushed out version 10.6p2. The fix closed a hole that let anyone who could reach your SSH port log into accounts that had no password at all. Attackers were trying it within hours, and by November 7 a threat-intel firm had logged sixteen successful exploitation chains.
The flaw is called SSHole (CVE-2026-123158), and it is one of those bugs that sounds made up until you read the root cause. I spent part of this week checking my own machines and an Alpine container against it, and every command below is one I actually ran.

The short version: if you run sshd on a musl-based Linux distribution — Alpine, OpenWrt, Buildroot, Yocto, most embedded images — and you have not turned off password logins, you need to read this. If you are on Debian, Ubuntu, Fedora, or any glibc-based system, you can relax, but stick around for the detection steps.
What SSHole actually is
Here is the mechanism, stripped to the essentials. When sshd is built without PAM, it checks a password by hashing the attempt with the C library’s crypt() and comparing the result to the stored password field. For an account that has no password — the field in /etc/shadow is * — sshd borrows the salt from another account’s hash to process the login attempt.
musl’s crypt() returns the string * when the key is longer than 256 bytes. That is an error indicator. But * is also exactly what the stored field contains for every account with no password. So the comparison matches, and the login succeeds.
The attacker sends a password between 257 and 1,024 bytes. That is it. No exploit framework, no brute force, no retry loop. One connection, every time.
The bug was reported by Özgün Kültekin of Wakefield and closed in the OpenSSH 10.6p2 release. It has been sitting in that code path since a 2016 fix for CVE-2016-6210, the username-enumeration flaw. Ten years.
Are you affected? Four conditions
SSHole is picky. All four of these have to be true at the same time:
- musl libc, PAM disabled. The Alpine
openssh-serverpackage is built without PAM, and its default configuration does not enable it. If you runopenssh-server-pamwithUsePAM yes, you are not affected. - Password authentication is on. It is the OpenSSH default unless you changed it.
- An account has
*in its password field. On Alpine that includesroot(which ships asroot:*since about 3.16), any key-only user created withadduser, directory-service users reached through musl-nscd, and thealpineuser in the official tiny-cloud images. - At least one account has a real password hash. One user with a
$5$or$6$hash is enough to make every account without one reachable.
Who is not affected: glibc distributions (Debian, Ubuntu, Fedora), systems with PAM enabled, Dropbear users, containers that do not run sshd inside them, and anything with PasswordAuthentication no. Locked accounts with ! in the password field cannot log in with a password or a key.
How to check, in about two minutes
I wrote a small read-only script that checks all four conditions and prints what it finds. Paste it into a file and run it with python3:
#!/usr/bin/env python3
# SSHole (CVE-2026-123158) exposure check - read-only
import os, subprocess
try:
r = subprocess.run(["ssh", "-V"], capture_output=True, text=True, timeout=10)
print("[1] OpenSSH:", (r.stderr or r.stdout).strip())
except Exception:
print("[1] OpenSSH: not installed")
musl = any(os.path.exists(p) for p in (
"/lib/ld-musl-x86_64.so.1", "/lib/ld-musl-aarch64.so.1"))
print("[2] libc:", "musl" if musl else "glibc (not affected)")
pam = None
try:
r = subprocess.run(["sshd", "-T"], capture_output=True, text=True, timeout=10)
for line in r.stdout.lower().splitlines():
if line.startswith("usepam"):
pam = line.split()[1] == "yes"
except Exception:
pass
print("[3] PAM:", "enabled" if pam else "disabled" if pam is False else "unknown")
locked, hashed = [], []
try:
for line in open("/etc/shadow"):
p = line.split(":")
if len(p) > 1:
if p[1] in ("*", "!", "!!"):
locked.append(p[0])
elif p[1].startswith("$"):
hashed.append(p[0])
except OSError:
pass
print("[4] accounts with '*':", ", ".join(locked) or "none")
print("[5] accounts with a hash:", ", ".join(hashed) or "none")
If you would rather not run a script, the same checks work as shell commands. On Alpine the package question settles most of it:
# Which sshd build is installed?
apk info openssh-server
apk info openssh-server-pam
# Is PAM in play?
grep -ri '^UsePAM' /etc/ssh/
# What is the effective password setting? (needs host keys)
sshd -T | grep -i passwordauthentication
# Who has no password, and who has a real one?
awk -F: '$2 == "*" {print $1}' /etc/shadow
awk -F: '$2 ~ /^\$/ {print $1}' /etc/shadow
I ran both against a fresh Alpine 3.24 container and against my Ubuntu 22.04 WSL box. The container came back with musl, no PAM build, no UsePAM line, passwordauthentication yes, and root:* in the shadow file — every condition met. My Ubuntu box came back glibc, no sshd installed, and no matching accounts. Two minutes, and I knew exactly where I stood.
One trap worth knowing: on Alpine the fixed package keeps the old upstream version number. The patched openssh-10.3_p1-r2 package still reports OpenSSH_10.3p1 when you run ssh -V. Do not trust the version string alone — check apk info openssh and compare it against the fixed package list below.
How to patch
On Alpine, the fix is one command plus a restart:
apk upgrade -U -a
rc-service sshd restart
That second line matters. Upgrading the package does not replace the running daemon. Alpine’s advisory is blunt about it: update, then restart.
The fixed package versions are:
| Branch | Fixed openssh version |
|---|---|
| edge | 10.6_p2-r0 |
| 3.24 | 10.3_p1-r2 |
| 3.23 | 10.2_p1-r1 |
| 3.22 | 10.0_p1-r11 |
| 3.21 | 9.9_p2-r1 |
| 3.20 | 9.7_p1-r6 |
| 3.19 | 9.6_p1-r3 |
| 3.18 | 9.3_p2-r4 |
| 3.16 | 9.0_p1-r6 |
Alpine published fixes all the way back to 3.16, even though 3.21 and older no longer receive regular updates. If you build from source, upgrade to OpenSSH 10.6p2 or later. The fix is commit 4353fdc, which rejects any stored hash shorter than four characters.
If you want the background on where OpenSSH is heading, I covered the October 10.6 release and its post-quantum signature work in this earlier post. The 10.6p2 release is the security follow-up to that one.
If you cannot patch right now
There is a one-line mitigation, and it works: turn off password authentication.
# Alpine drop-in (read from 3.19 onward)
mkdir -p /etc/ssh/sshd_config.d
echo 'PasswordAuthentication no' > /etc/ssh/sshd_config.d/00-no-passwords.conf
# Validate, then restart
sshd -t && rc-service sshd restart
sshd -T | grep -i passwordauthentication # should print: no
I tested this against a custom config in a throwaway container. sshd -T came back with passwordauthentication no, and sshd -t reported a clean syntax check. Before you do it on a real host, make sure your key login works and keep your current session open until you have tested a new one. Locking yourself out is a bad day.
This is a mitigation, not a fix. It closes the hole, but you still want the patched package. And if you run password logins for a reason, the real answer is to move to keys — I listed the settings worth changing in this SSH hardening guide.
How to tell if you were already attacked
Patching closes the door. It does not undo a login that already happened. GreyNoise’s write-up describes what the attackers did after getting in: system and container reconnaissance, queries to cloud metadata services, attempts to read password hashes and root SSH keys, and edits to authorized_keys files for persistence. One attacker gained root and installed a Python command-and-control agent called TAC Agent.
Three things to check on any exposed machine:
- Unexpected successful password logins. On Alpine, look in
/var/log/messagesforAccepted password forfollowed by an account that has no password —root,alpine, or any key-only user. That line should never appear. - Unauthorized SSH keys. Review
authorized_keysfiles for keys you did not add, and check for recently created users. - Persistence. Look for new startup services, and for TAC Agent files if you want to match the specific campaign. Review
doasuse as well.
If you cannot rule out a login, treat the machine as compromised. Rebuild it from a known-good image rather than trying to clean it. If you want a repeatable way to watch for this kind of thing, my Prometheus and Node Exporter setup gives you the log and metric pipeline to alert on unusual SSH activity. For endpoint-level coverage on Linux, Microsoft Defender’s Linux support is worth a look.
The bottom line
SSHole is a bad bug with a narrow blast radius and a simple fix. If you run Alpine or another musl-based image with sshd and password logins enabled, you are in the blast radius. Check the four conditions, patch to 10.6p2 or the fixed Alpine package, restart the daemon, and then go look at your logs.
I run a mix of Ubuntu and container hosts, and the check took me about two minutes end to end. The part that surprised me was how long the bug had been there — ten years, hidden behind a 2016 security fix. That is the part I keep thinking about.
If you self-host anything on Linux, the same discipline applies: know which libc you are on, know which authentication path your SSH server uses, and keep the patch pipeline warm. I have been through that checklist with self-hosted Docker services before, and the Alpine base image is exactly the kind of place this flaw hides.