OpenSSH "SSHole" (CVE-2026-123158): A 2016 Patch Created This Auth Bypass — And the Internet Rooted an Alpine Honeypot in Under 9 Hours

Posted by Reda Fornera on 2026-11-07
Estimated Reading Time 18 Minutes
Words 3k In Total

Generic stock photo of a terminal screen with cascading green code — a visual stand-in for an attacker probing sshd; not an actual depiction of the CVE-2026-123158 attack or any real session

Some vulnerabilities take months to weaponize. The OpenSSH CVE-2026-123158 — the bug everyone is calling the SSHhole vulnerability — took less than nine hours, and it was aimed at the default configuration of an entire distribution family.

On November 5, 2026, researcher Özgün Kültekin of Wakefield disclosed CVE-2026-123158 — nicknamed the SSHole vulnerability — a password authentication bypass in OpenSSH’s sshd. The conditions are specific, but the headline target is the default configuration of the container ecosystem’s favorite base image: Alpine Linux’s stock openssh-server package. Within hours, a threat-intelligence honeypot running real Alpine Linux was rooted.

Here’s what actually happened, why a decade-old patch is responsible, and what to do tonight.

What Happened

The timeline is tight. November 3: Wakefield finds the flaw and reports it to the OpenSSH security team — per Wakefield’s disclosure post, the OpenSSH team sent a patch back in under an hour. November 4: Alpine and musl maintainers get word. November 5: Portable OpenSSH 10.6p2 ships, Alpine publishes fixed packages the same day, and the CVE-2026-123158 disclosure goes public.

The 10.6p2 release notes are unusually careful with words:

sshd(8): avoid password authentication bypass that is exploitable through sshd(8) that affects some Linux systems when built and run with PAM support disabled. We consider the root cause of this bug to be outside sshd, but it is exploitable via sshd and can be avoided there.

That phrasing matters. OpenSSH is saying the real defect lives in a C library behavior, but since sshd is the door it’s exploited through, sshd is where it got patched. It’s the software-vulnerability equivalent of your house being burglarized because the lock manufacturer assumes every door frame is the same.

The severity is CVSS 8.1 with the vector CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H — network-reachable, no privileges, no user interaction, but a high-complexity precondition set (more on that gate below, because most coverage flattens it).

And then the part that turned this from a patch-Tuesday item into a news story. Previdian, a threat-intelligence firm, reported that its sensors caught the first exploitation attempt less than nine hours after the CVE was published. At least one attempt succeeded: Previdian founder Ryan Dewhurst confirmed successful root access on a honeypot running real Alpine Linux and OpenSSH. Early telemetry counted eight attempts, all from a single Japan-geolocated address, 89.116.89[.]18, every one using oversized passwords and running recon commands — id, uname, hostname — over non-interactive SSH sessions. By November 7, Previdian’s tracking page had grown the tally to 15 attempts from 10 unique IPs across 8 countries. Previdian listed the CVE on its own exploited catalog as confirmed; at the time of its reporting, CISA’s KEV had no SSHole entry yet.

The Hacker News front page did the rest: the sshhole.com thread sat near the top of the pile with roughly 2,900 points and over 730 comments.

This is also the second time in weeks that an emergency patch cycle has dominated the security news — PHP ships similar fire-drill releases, as we covered in PHP’s emergency patch for CVE-2026-109618. The difference here is the speed from patch to weaponization.

The Mechanism: How the SSHole Vulnerability Turns a 2016 Fix Into a 2026 Bypass

This is the part that should be taped to the wall of every API design review. Three independent decisions, made years apart in three different projects, composed into the SSHole authentication bypass. None of them was crazy in isolation.

Change one: 1980/2012 — the password field starts meaning “no login.” Accounts that should never accept a password — root on Alpine, key-only deploy users, LDAP-synced accounts — carry a single * in the password field of /etc/shadow instead of a hash. No password can hash to *, so the comparison always fails. That’s the convention. (sshhole.com’s history even traces the * to Bill Joy putting it in for daemon in 3BSD’s /etc/passwd in 1980, where root got the treatment later.)

Change two: 2012 — musl makes crypt() “fail” without failing. POSIX says crypt() returns NULL on error. Problem: a huge amount of real-world code never checks for NULL and compares the result to the stored hash anyway, crashing. So in August 2012, musl commit b3c4cc12 changed crypt() to always succeed, returning an unmatchable string on failure. The commit message says always “succeeding” but giving an unmatchable hash “is reportedly a better behavior than failing on error.” The relevant line from the diff:

1
2
3
4
5
-   /*
- * Should not happen.
- */
- return NULL;
+ return (setting[0]=='*') ? "x" : "*";

Note the guard: if the caller’s salt/setting string itself starts with *, musl returns x — a genuinely different sentinel. The design tried to never return something equal to the input setting. Twenty days later, musl added SHA-256/SHA-512 crypt and stamped a KEY_MAX of 256 bytes into the module as a DoS cap, since sha512crypt gets prohibitively slow on long passwords. Past 256 bytes, that crypt path errors — and, as the module’s own comment says, “on error * is returned.”

Change three: 2016 — sshd starts hashing passwords that don’t need hashing. CVE-2016-6210 was a username-enumeration timing leak: for a nonexistent user, sshd hashed the submitted password against a hardcoded fake-password structure with a Blowfish salt. On systems that understood Blowfish, the timing differed enough from real users’ SHA-512 hashing for an attacker to tell which usernames existed. The fix, commit 9286875a by Darren Tucker in July 2016, made sshd stop relying on the fakepw salt and instead borrow a real account’s salt — pulling the same hash algorithm root uses — so invalid users burn the same time as valid ones:

1
2
3
4
5
6
7
8
9
+   /*
+ * Pick an appropriate password encryption type and salt for the running
+ * system.
+ */
+ static const char *
+ pick_salt(void)
...
+ if (salt == NULL)
+ salt = pick_salt();

Now compose the three. On a musl system, an attacker targets an account whose stored field is *. sshd, faithfully balancing its timing per the 2016 fix, borrows a real salt — a $5$/$6$ string from another account — and calls crypt() with the attacker-supplied password against it. The attacker sends a password between 257 and 1,024 bytes long. The borrowed salt routes the call into musl’s SHA-2 crypt path. The password exceeds KEY_MAX. crypt “fails.” Per musl’s error behavior, it returns the string *. sshd’s comparison then executes, in effect:

1
2
crypt(attempt) == stored_field
"*" == "*"

Match. Login accepted. As sshhole.com’s write-up puts it: “so past 256 bytes crypt fails. and when crypt fails… it says *.” And * is exactly what the stored field contains for every account with no password.

One connection. No retries. Works every time — Alpine’s own advisory says exactly that: “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.”

Who’s Actually Affected: The Gate Is Narrower Than the Headlines

The SSHole vulnerability gate has four conditions, all of which must hold — and they’re spelled out precisely in Alpine’s November 5 advisory:

  1. musl libc. Alpine, OpenWrt, postmarketOS, Void (musl), Chimera, and Buildroot/Yocto builds that use musl. Wakefield’s disclosure also name-checks “a lot of the docker images you’ve pulled.”
  2. sshd built and run without PAM. This is Alpine’s default: the standard openssh-server package is compiled without PAM, so sshd does password checks itself by calling crypt() directly. Run the openssh-server-pam package with UsePAM yes and you are not affected — PAM’s stack never takes this comparison path.
  3. Password login still enabled. That’s the OpenSSH default unless you changed it.
  4. At least one account whose password field is exactly *, and at least one other local account with a real $5$ or $6$ hash. The second account is the supply for the borrowed salt; Alpine’s advisory is blunt that “one user with a password is enough: it makes every account without one reachable.” Freshly-built images with no password set anywhere are safe — for now.

On that fourth condition, the blast radius sharpens uncomfortably. Alpine ships root:* in the base image (since roughly 3.16), so root is in the target set — OpenSSH’s default PermitRootLogin prohibit-password keeps root out of reach, but only if nobody set PermitRootLogin yes. Worse still are Alpine’s official tiny-cloud images (versions 3.18.11 through 3.24.3): the alpine user ships with a * password field and passwordless doas. Those images are safe on first boot, and become exploitable the moment any $6$ hash exists on the box — say, an admin sets a console password. Then a shell as alpine is one doas away from root.

Who is off the hook, per both Alpine and Wakefield: glibc-based distros (Debian, Ubuntu, Fedora and friends), anything doing password auth through PAM, Dropbear, OpenBSD, FreeBSD, macOS, locked accounts whose field is !, and containers that don’t run sshd at all.

Why glibc is fine is the one-character tragedy of the whole saga. musl’s sentinel is * — the exact string Unix uses for “no login allowed.” libxcrypt, glibc’s modern crypt implementation, uses the failure token *0 (or x in some musl variants) — strings that correspond to no widespread password-field convention. sshhole.com’s history tells it well: a Debian developer asked glibc to copy musl’s * sentinel back in 2013, calling it “the best compromise/solution.” Debian moved to libxcrypt instead, which chose *0. One character away from CVE-2026-123158.

What To Do Now

Patch. OpenSSH 10.6p2 is the upstream fix for the SSHole vulnerability, and Alpine shipped patched packages for every supported branch on November 5 — edge gets 10.6_p2-r0, and the branches get their own builds all the way back to 3.16 (9.0_p1-r6). apk upgrade openssh gets the package, but Alpine’s advisory stresses that upgrading does not replace the running daemon — you must rc-service sshd restart afterward, or the old binary keeps listening.

If you can’t patch, close the path. PasswordAuthentication no plus an sshd restart eliminates the vector entirely — a single drop-in file does it:

1
2
echo "PasswordAuthentication no" > /etc/ssh/sshd_config.d/00-no-passwords.conf
rc-service sshd restart

OpenSSH developer Damien Miller was less diplomatic, urging on Mastodon — as reported by Help Net Security — that anyone running musl-based Linux without PAM should “stop reading and set PasswordAuthentication=no or disable SSH for these systems IMMEDIATELY.”

Audit. The tell is unambiguous. Alpine’s advisory directs admins to look in /var/log/messages for sshd lines reading Accepted password for accounts that should never take a password — alpine, root, key-only users, directory-service users. An *-field account accepting a password is not a misconfiguration; it’s exploitation. Alpine’s closing guidance, now the canonical incident instruction for this bug: “If you cannot rule out a login, treat the machine as compromised.”

Check the IOCs. The observed attacker IP 89.116.89[.]18 (Japan-geolocated); SSH authentication failures containing oversized passwords in the 257–1,024 byte range; follow-on id, uname, hostname commands over non-interactive SSH. For containers: images built from Alpine 3.18.11–3.24.3 tiny-cloud artifacts need rebuilding rather than in-place upgrades; running instances need the package patch plus restart. A community check script and PoC live at github.com/oz9un/sshole — though Previdian notes that GitHub PoCs circulating for this CVE are unverified and some may contain malware, so know what you’re running.

If an Accepted password for line matches a * account, don’t just restart and hope: review doas usage, recently created users and authorized_keys files, per Alpine’s checklist — and treat the box as hostile until proven otherwise.

Alpine’s advisory condenses to a concrete audit checklist — run through it textually, box by box:

  • [ ] Grep /var/log/messages for Accepted password for lines naming accounts that should never take a password (root, alpine, key-only users, directory-service users)
  • [ ] Confirm the patched openssh package version is installed — and that sshd has actually been restarted since the upgrade
  • [ ] Set PasswordAuthentication no and restart sshd if password login isn’t required
  • [ ] Review recent doas usage and any local privilege changes
  • [ ] Look for recently created users, modified accounts, and new or changed authorized_keys files
  • [ ] Rebuild container images derived from the Alpine 3.18.11–3.24.3 tiny-cloud artifacts instead of upgrading in place
  • [ ] Correlate logs against the observed IOC set — the attacker IP 89.116.89[.]18, 257–1,024-byte password strings, follow-on id/uname/hostname commands
  • [ ] If any *-account login cannot be ruled out, treat the machine as compromised until proven otherwise

Once you’ve triaged the box, apply the same playbook the UEFI PXE bug forced on firmware teams — patch, audit, and assume the worst until proven otherwise — as in our analysis of the UEFI PXE vulnerability CVE-2026-97631.

The Industry Implications

Three lessons have longer half-lives than the CVE itself.

musl vs. glibc divergence is a security variable, not just a size variable. The identical sshd code produced a remote auth bypass on one libc and nothing on another. The difference boiled down to one sentinel character chosen twelve years ago. Container ecosystems standardized on the smaller libc — a perfectly rational choice for image size — but it means the container fleet exercises different C-library edge-case behavior than the distributions most engineers test on. Wakefield’s data points at why this stayed hidden so long: OpenSSH is developed and tested on OpenBSD and glibc systems; musl doesn’t ship and test sshd; and most desktop/server distros route through PAM, which never makes this comparison for a * account. That’s the structural shape of a class of bugs, not a one-off.

Sentinel design is a landmine factory. musl’s intent in 2012 was defensible — returning * instead of NULL fixed a wave of NULL-dereference crashes — and it even included the x guard for the setting-starts-with-* case. What it couldn’t anticipate was a caller that (a) supplies a different salt string and (b) compares the output against a different stored value. oss-security thread participant Michael Orlitzky put the general problem compactly: “no one knows what the output from crypt(3) will be, so doing a strcmp on it to authenticate logins is… optimistic.” The HN thread had the same fight over whether a better type system would have prevented this; the honest answer is that any error-signaling scheme that collapses “failed” and “succeeded with value X” into the same char* is one clever caller away from exactly this bug. The OpenSSH 10.6p2 workaround reflects the same lesson from the other side: per the oss-security thread, the patch validates MIN_PASSWORD_HASH_LEN — checking that the stored value could plausibly be a hash — rather than accepting whatever sentinel crypt() happens to produce.

The 9-hour clock is a container-base-image economics problem. From CVE publication (15:46 UTC, Nov 5) to confirmed root (under nine hours) is the interval attackers now need — and here, the exploited surface was the default configuration of an entire distribution family, not a misconfigured minority. Base images amortize their defaults across millions of derived images, so a default-config flaw in Alpine’s sshd is effectively a fleet-wide flaw that ships inside every image that kept password logins on. Alpine’s advisory even had to spell out that “docker and other container users that do not run sshd in the container” are unaffected — an admission of how often its base image is the assumed perimeter. And patching a base image is not patching a machine: it means rebuilding, redeploying, or updating instances launched from old images, which is where the operational cost lands.

The discovery story belongs to the same pattern as recent supply-chain and AI-assisted security incidents — agent swarms probing code paths the way the trapdoor attack hid logic inside the AI toolchain as covered in the trapdoor supply-chain attack on AI coding assistants, and autonomous agents finding real leaks in shipped apps like the vibe-coding security failures in when vibe-coded AI apps leak customer data. Wakefield’s disclosure is itself a case study: the SSHole vulnerability was found by agent swarms running some 17,600 malformed logins across every build combination. The 9-hour exploit clock, meanwhile, suggests adversaries hit the same conclusion OpenSSH did about AI-scaled discovery. When both defenders and attackers run automated vulnerability discovery over the same default configurations, “patch Tuesday” stops being a schedule and becomes a race you measure in hours.

Until the next sentinel collides: check your logs, upgrade to OpenSSH 10.6p2, restart sshd — and if there’s an Accepted password for line that shouldn’t exist, CVE-2026-123158 says to assume the worst.


References and further reading


Please let us know if you enjoyed this blog post. Share it with others to spread the knowledge! If you believe any images in this post infringe your copyright, please contact us promptly so we can remove them.



// adding consent banner