CVE-2026-123158 ("sshole") is a password-authentication bypass in OpenSSH's sshd, disclosed on 2026-11-05 and rated CVSS 8.1 (CVSS 3.1, from the CVE record). It only bites when four things line up: musl libc (Alpine, OpenWrt, Buildroot, Yocto), sshd built or run without PAM, PasswordAuthentication yes, and an account whose /etc/shadow field is a lone * next to another account with a real $5$ or $6$ hash. In that setup, musl's crypt() returns the string * as its error value for passwords over 256 bytes, and sshd compares it with the stored * and says yes. glibc and PAM-based sshd are not affected. The fix is OpenSSH 10.6p2 (Alpine: 10.6_p2-r0 on edge, 10.3_p1-r2 on 3.24, and builds back to 3.16). Upgrading the package does not restart the running daemon, so restart sshd too. If you cannot patch today, set PasswordAuthentication no. This post has no exploit in it: it is a check, patch and harden guide with real output from a lab container.
Introduction
On November 5, 2026, the OpenSSH project shipped 10.6p2 with what its release notes call a "critical security workaround that affects Linux systems that use the musl libc and PAM disabled." Researcher Ozgun Kultekin of Wakefield found the bug and named it *sshole. The asterisk in the name is the whole story, as you will see in a minute.
Alpine containers and OpenWrt routers are everywhere, so my first question was the boring defender one: am I affected, and how would I know? Most coverage is about how clever the bug is. I wanted to answer the operational questions instead.
- Which of my hosts can even hit this?
- What version is the fix, per Alpine branch?
- How do I verify the patch is actually live, not just installed?
- What do I look for in my logs?
- What is the cheapest hardening that makes this class of bug irrelevant?
A note on scope before anything else. This post contains no exploit, no proof of concept and no login-bypass attempt. I explain the bug at the level the public advisories do, then spend the rest of the post on detection, patch verification and hardening. I did not send a single oversized password to anything while writing it. Public PoCs exist on GitHub, and Previdian warns they are unverified and could contain malware. Do not run them on a machine you care about.
Here is the shape of the post.
What is CVE-2026-123158 (sshole) in plain English?
OpenSSH's server, sshd, can check a password in two ways. Either it hands the job to PAM (Pluggable Authentication Modules, the system's login framework), or it does the check itself: read the stored hash from /etc/shadow, hash the password you typed using the same salt, and compare the two strings.
The second path is what Alpine uses. Alpine's default openssh-server package is built without PAM. And the string comparison is where this bug lives.
According to the advisories, three old decisions interact:
- musl's
crypt()(the C library function that hashes a password) has two relevant behaviors. Since 2012 it does not return NULL on failure, it returns a placeholder (*). And its SHA-256 and SHA-512 modes refuse passwords longer than 256 bytes, which was added as protection against slow-hash denial of service. Past 256 bytes,crypt()fails and returns*. - OpenSSH, since 2016, hashes the supplied password even for accounts that have no usable hash. That change (the fix for CVE-2016-6210) was meant to stop attackers from telling real usernames from fake ones by timing. To do it, sshd borrows another account's salt and hash settings.
- Locked and key-only accounts store a lone
*in the shadow password field. Alpine's root has had*since Alpine 3.16 in 2022.
Put them together. For an account whose stored field is *, a password that makes crypt() fail produces a result of *. The comparison says the two strings are equal. Login succeeds. OX Security describes this as "an authentication logic failure caused by the interaction between two components that interpret the same value differently." BleepingComputer adds an important limit: it does not bypass passwords on accounts that have a valid hash.
OpenSSH's own framing is worth noting. Damien Miller said the root cause is "outside OpenSSH itself," and the release notes say, "We consider the root cause of this bug to be outside sshd." The 10.6p2 change is a workaround inside sshd. That is why distributions with musl-based libcs matter more than sshd itself here.
The sources disagree slightly on the oversize threshold. musl's limit is over 256 bytes, so the first failing length is 257. The researcher's page, Alpine's advisory and OX Security say 257 to 1,024 bytes. The CVE record itself says 256 to 1,024 characters. I treat the exact edge as not important for defenders: it is a long password, and patching or disabling password login removes it either way.
Why * in /etc/shadow is a lock marker, and why glibc is not affected
To see why this is a bug in the first place, you need to know what the second field of /etc/shadow means. The file has one line per account, with colon-separated fields. The second one is the password field.
| Field value | Meaning | Affected by sshole? |
|---|---|---|
$6$salt$hash... | A real SHA-512 hash | No, a password must match the hash |
$5$salt$hash... | A real SHA-256 hash | No, same |
* | No password can ever match, account can still use keys | Yes, this is the marker in play |
! | Locked, a ! is prepended or stands alone | No, per OX Security and Detectify |
| empty | No password required (dangerous on its own) | Different problem |
The idea behind * is old. The researcher's timeline traces it to BSD 3 in 1980, where the daemon account had * in /etc/passwd. The reasoning is that no real hash output can equal a single asterisk, so no password can ever match.
That reasoning holds as long as crypt() never returns a lone * by itself for a real input. The 1997 Single UNIX Specification says crypt() returns NULL on error. A caller that checks for NULL refuses the login. musl moved to a placeholder return in 2012, and a caller that does not check for NULL then compares whatever string it got.
So why are other builds safe?
- glibc builds. In the researcher's test matrix, glibc behaved correctly with and without PAM. Debian, Ubuntu and Fedora are listed as not affected.
- PAM builds. When PAM does the password check, sshd never does the string comparison. musl with PAM was also fine in the matrix.
!accounts. A leading!makes the field a different string than whatever the failingcrypt()returns, so there is no equality.
The researcher built OpenSSH in each combination of glibc and musl, with and without PAM, with and without shadow passwords, and ran about 17,600 unusual login attempts across them. OX Security quotes the result: "Every build behaved the same except one: musl without PAM, which said yes when it should have said no."
The history also explains why Alpine ended up here. Per the researcher's timeline, Alpine's root account was found to have no password in Docker images in 2019 (CVE-2019-5021). The fix put ! in the field. But without PAM, ! also broke key-based root login. So Alpine 3.16 switched root to *, which restored key login and left the placeholder problem in place. Each step was reasonable on its own.
Here is where in the check the two worlds diverge. This is a conceptual diagram, not a trace of any real session.
Am I affected? The five conditions
The advisories are consistent on scope, so I turned them into a checklist. You are affected only if every line is true.
- You run musl libc. That means Alpine, and also OpenWrt, Buildroot and Yocto builds, and a lot of Docker images based on Alpine.
- Your sshd does not use PAM. Alpine's default
openssh-serveris built without it. Theopenssh-server-pampackage is fine only whenUsePAM yesis set. Detectify listsopenssh-server-pamwithUsePAM noas affected. PasswordAuthenticationisyes. That is the OpenSSH default.- At least one account has a lone
*in its shadow field. Typical examples: root on Alpine, key-only deploy or service users, directory users via nslcd, winbind, sssd or musl-nscd, and thealpineuser on Alpine cloud images. - At least one other local account has a
$5$or$6$hash. Alpine's note says the path needs one.
Two more facts shape the real risk:
- Root is usually protected by default. Alpine's default
PermitRootLogin prohibit-passwordblocks direct root password logins even when root has*. Alpine's advisory and SOC Prime both point at this. - Cloud images are a trap. Alpine's official AWS and Azure images (3.18.11 through 3.24.3) have a default
alpineuser with passwordlessdoas, which is effectively root. Detectify says those images are safe on first boot, and become exploitable once any account gets a password, for example when an admin sets a console password. Alpine's fixed point releases are 3.21.10, 3.22.8, 3.23.8 and 3.24.4.
Not affected, per the advisories: glibc distributions (Debian, Ubuntu, Fedora, RHEL), sshd with PAM doing the check, PasswordAuthentication no, accounts locked with !, containers with no sshd at all, Dropbear, and OpenBSD. Dropbear is listed as not affected by Orca, OX Security and the researcher. If you run it, update it normally and watch for news.
Which versions fix CVE-2026-123158?
Upstream, the fix is OpenSSH 10.6p2, released November 5, 2026. For Alpine, the project backported it to every branch back to 3.16, including unsupported ones. Alpine says it published fixed packages for each release back to 3.16, even though 3.21 and older no longer get regular updates.
| Alpine branch | Fixed openssh package |
|---|---|
| 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.17 | 9.1_p1-r7 |
| 3.16 | 9.0_p1-r6 |
Source note: Alpine's own advisory (alpinelinux.org, 2026-11-05) lists all ten rows. The Alpine security tracker page listed only edge through 3.19 when I looked, and OX Security and SOC Prime match the advisory. Check the tracker or apk for your branch before you rely on the older rows.
Two version traps to know about:
- The banner lies by design. Distributions backport fixes without changing the banner, so
ssh -Vis not proof. SOC Prime says to check package status rather than the banner. On Alpine the package revision (-r2) is the evidence. - Package upgrade is not daemon restart. Alpine's
apk upgradereplaces files on disk. A running sshd keeps the old code in memory until it is restarted. Alpine's advisory, Detectify, Orca, Help Net Security and SOC Prime all say this.
Is it being exploited? What Previdian and SOC Prime disagree on
I was careful here, because the reports conflict and I would rather show you both.
Claim: yes, exploitation started within hours. Help Net Security, on November 6, quotes Previdian, a threat-intelligence firm. Founder Ryan Dewhurst said its sensors saw attacks and at least one succeeded: "Our first observed attempt arrived less than nine hours after CVE publication." Previdian says it confirmed root access on a honeypot running Alpine and OpenSSH. The source IP was 89.116.89[.]18 (Japan-based), it used oversized passwords and ran id, uname and hostname over non-interactive sessions. Previdian counted eight attempts, all from that IP and all seen by one sensor, and added the flaw to its own known-exploited list. CISA had not added it to the KEV catalog at the time.
Claim: no confirmed exploitation. SOC Prime, as of November 6, says its reviewed sources report no confirmed in-the-wild exploitation, no CISA KEV entry and no campaign IOCs. Orca's November 5 page said no exploitation so far. Detectify, as of November 5 at 15:30 CET, knew of no public exploitation reports and expected scanning to follow.
How I read it: these are not clean contradictions. Orca and Detectify wrote on November 5, before Previdian's window had run. SOC Prime wrote after, but its statement is about "reviewed sources", and a single-vendor honeypot report is a different kind of evidence than a vendor advisory. Previdian's data is eight attempts from one IP on one sensor. That is real, and also narrow. I cannot verify either claim independently, so treat the in-the-wild status as reported by one vendor, not corroborated by others and act as if it is true. The cost of acting is a package upgrade.
Researchers moved fast on the public write-up too. Andre Baptista posted that he reproduced the issue:
And International Cyber Digest relayed Damien Miller's reasoning on why OpenSSH shipped quickly:
I am citing these for the timeline and the sentiment, not as technical authority. The advisories are the technical authority. Notably, the OpenSSH release notes say the project will make more frequent releases because AI-discovered bugs may be found independently by others, so fixes should reach users sooner. That is a project statement worth planning around: patch windows are getting shorter.
How do I check my own Alpine container? A hands-on audit lab
Here is the lab. Everything ran in a local Docker (OrbStack) container on an Apple Silicon Mac, with nothing published to any network port. I built a patch-and-exposure check, not an attack. The image was alpine:latest, which resolved to Alpine 3.24.4 on aarch64. That release is one of the fixed point releases, so the package I installed is already patched. I will be upfront about that: I did not install a vulnerable package and I did not test the bug. What I can show is how to read your own state.
Step 1: Install and read the package state
docker run -d --name sshlab alpine:latest sleep 3600
docker exec sshlab apk add --no-cache openssh-server
Captured output from the install:
(3/3) Installing openssh-server (10.3_p1-r2)
Executing busybox-1.37.0-r31.trigger
OK: 10.5 MiB in 19 packages
Now ask apk what is installed and compare it with the fixed version. One surprise: on this apk, apk info -v openssh-server prints the package description rather than the version. Use apk info -v with no argument and grep, or apk version:
apk info -v | grep '^openssh-server-[0-9]'
apk version openssh-server
apk version -t 10.3_p1-r1 10.3_p1-r2
openssh-server-10.3_p1-r2
Installed: Available:
openssh-server-10.3_p1-r2 = 10.3_p1-r2
<
The = means installed equals what the repo offers. The last command is the useful trick: apk version -t A B compares two version strings and prints <, = or >. It says 10.3_p1-r1 sorts before the fixed 10.3_p1-r2, so -r1 would be unpatched on 3.24. I use that in the audit script, because plain string comparison gets Alpine revisions wrong.
Step 2: List accounts with a lock marker
I created two throwaway accounts: ops with a password (so it gets a $6$ hash) and deploy as a key-only user, which I set to * to mimic a service account:
adduser -D deploy; adduser -D ops
echo "ops:Lab-only-pw-1" | chpasswd
sed -i 's/^deploy:!:/deploy:*:/' /etc/shadow
Notice that adduser -D creates the account with !, not *. In my lab, plain adduser -D accounts were not the dangerous marker, even though Alpine's advisory lists key-only users created with adduser as an example, so check your own /etc/shadow rather than assuming. Now list every account whose field is * or starts with !:
awk -F: '$2=="*" || $2 ~ /^!/' /etc/shadow | cut -c1-40
root:*::0:::::
bin:!::0:::::
daemon:!::0:::::
lp:!::0:::::
sync:!::0:::::
shutdown:!::0:::::
halt:!::0:::::
mail:!::0:::::
news:!::0:::::
uucp:!::0:::::
cron:!::0:::::
ftp:!::0:::::
sshd:!::0:::::
games:!::0:::::
ntp:!::0:::::
guest:!::0:::::
nobody:!::0:::::
deploy:*:20764:0:99999:7:::
That one command splits the list into the two groups that matter. Most system accounts are !, which is not the marker in play. root and my deploy have *. To see what type of hash the password accounts have without printing hashes, show only the first four characters:
awk -F: '{print $1":"substr($2,1,4)}' /etc/shadow | grep -E "root|deploy|ops|nobody"
root:*
nobody:!
deploy:*
ops:$6$B
So in this lab, conditions 4 and 5 are both true: * accounts exist (root, deploy) and another account has a $6$ hash (ops). The only things standing between this container and the vulnerable profile are the patched package and the config.
Step 3: Read the effective sshd config, before hardening
I started sshd in the container (ssh-keygen -A; /usr/sbin/sshd) and asked it what it is actually running with. sshd -T prints the effective config, which is more reliable than reading the file, because it follows Include lines and defaults:
sshd -T | grep -Ei 'passwordauthentication|kbdinteractive|usepam|permitrootlogin'
permitrootlogin prohibit-password
passwordauthentication yes
kbdinteractiveauthentication yes
Two things to read here. First, passwordauthentication yes: the default is open. Second, there is no usepam line at all. That is not a typo in my grep. Alpine's sshd is built without PAM, so the option does not exist in its sshd -T output. A missing usepam line on Alpine means "no PAM", which is the vulnerable half of the profile. My audit script prints that as none (built without PAM).
Step 4: The audit script
Here is the full script. It is read-only: it reads the package database, sshd -T, /etc/shadow and /proc, and sends nothing over the network. It is POSIX sh so it runs on BusyBox. It reports one verdict: affected, fixed, or hardened, plus the in-between states I think matter.
#!/bin/sh
# sshole-audit.sh - read-only check for CVE-2026-123158 on Alpine/musl hosts.
# Sends nothing over the network. Reads package db, sshd config, /etc/shadow.
# Override FIXED_OVERRIDE only to test the logic.
fixed_for() {
case "$1" in
edge) echo "10.6_p2-r0" ;; 3.24) echo "10.3_p1-r2" ;;
3.23) echo "10.2_p1-r1" ;; 3.22) echo "10.0_p1-r11" ;;
3.21) echo "9.9_p2-r1" ;; 3.20) echo "9.7_p1-r6" ;;
3.19) echo "9.6_p1-r3" ;; 3.18) echo "9.3_p2-r4" ;;
3.17) echo "9.1_p1-r7" ;; 3.16) echo "9.0_p1-r6" ;;
*) echo "" ;;
esac
}
say() { printf '%-22s %s\n' "$1" "$2"; }
ls /lib/ld-musl-* >/dev/null 2>&1 || { say libc "not musl"; say VERDICT "NOT AFFECTED (glibc or other libc)"; exit 0; }
say libc "musl"
command -v sshd >/dev/null 2>&1 || { say sshd "not installed"; say VERDICT "NOT AFFECTED (no sshd)"; exit 0; }
rel=$(cut -d. -f1,2 /etc/alpine-release 2>/dev/null)
grep -q '_alpha\|_beta\|_rc\|_git' /etc/alpine-release 2>/dev/null && rel=edge
inst=$(apk info -v 2>/dev/null | grep '^openssh-server-[0-9]' | sed 's/^openssh-server-//')
fix=${FIXED_OVERRIDE:-$(fixed_for "$rel")}
say branch "$rel"
say installed "${inst:-unknown}"
say fixed_in "${fix:-unknown branch, check the Alpine tracker}"
cfg=$(sshd -T 2>/dev/null)
get() { echo "$cfg" | awk -v k="$1" '$1==k {print $2}'; }
pw=$(get passwordauthentication); kbd=$(get kbdinteractiveauthentication); pam=$(get usepam)
[ -z "$pam" ] && pam="none (built without PAM)"
say passwordauth "$pw"; say kbdinteractive "$kbd"; say usepam "$pam"
stars=$(awk -F: '$2=="*"{printf "%s ",$1}' /etc/shadow)
hashes=$(awk -F: '$2 ~ /^\$[56]\$/{printf "%s ",$1}' /etc/shadow)
say star_accounts "${stars:-none}"
say sha2_accounts "${hashes:-none}"
patched=no
if [ -n "$inst" ] && [ -n "$fix" ]; then
[ "$(apk version -t "$inst" "$fix")" != "<" ] && patched=yes
fi
stale=no
for p in $(pidof sshd 2>/dev/null); do
ls -l /proc/$p/exe 2>/dev/null | grep -q '(deleted)' && stale=yes
done
say stale_daemon "$stale"
reach=no
[ "$pam" != yes ] && [ "$pw$kbd" != "nono" ] && [ -n "$stars" ] && [ -n "$hashes" ] && reach=yes
if [ "$patched" = no ]; then
[ "$reach" = yes ] && v="AFFECTED: upgrade openssh-server and restart sshd" \
|| v="OLD PACKAGE but preconditions not all met: still upgrade"
elif [ "$stale" = yes ]; then
v="PATCHED ON DISK, OLD DAEMON RUNNING: restart sshd"
elif [ "$pw" = no ] && [ "$kbd" = no ]; then
v="FIXED and HARDENED"
else
v="FIXED, not hardened (password login still on)"
fi
say VERDICT "$v"
What each check does:
- musl detection.
ls /lib/ld-musl-*finds musl's dynamic loader. No file means not musl and the script exits with "not affected". - Fixed version lookup. The
fixed_fortable is the Alpine table from above, keyed on themajor.minorof/etc/alpine-release. Any_alpha,_beta,_rcor_gitsuffix is treated as edge. - Version compare.
apk version -tdoes the ordering, so10.3_p1-r2against10.3_p1-r10sorts right. - Preconditions. It marks the host reachable when PAM is not on, password or keyboard-interactive login is on, and both a
*account and a$5$/$6$account exist. - Stale daemon. After an upgrade, the running sshd's
/proc/PID/exelink shows(deleted)because the old binary was replaced on disk. That is a cheap way to catch "patched on disk, old code running".
I ran it in the lab before hardening:
libc musl
branch 3.24
installed 10.3_p1-r2
fixed_in 10.3_p1-r2
passwordauth yes
kbdinteractive yes
usepam none (built without PAM)
star_accounts root deploy
sha2_accounts ops
stale_daemon no
VERDICT FIXED, not hardened (password login still on)
The usepam line above reflects the script as published here. My first run printed an empty value there, before I added the "none" text, and the verdict was identical. I then tested the "affected" branch by lying to the script, which is allowed by design: FIXED_OVERRIDE pretends the fixed version is newer than what is installed. This only exercises my script's logic. It does not make the container vulnerable.
FIXED_OVERRIDE=10.6_p2-r0 sshole-audit.sh | tail -1
VERDICT AFFECTED: upgrade openssh-server and restart sshd
I did not test the "stale daemon" branch live, since the container was already on the latest package. The logic is a grep on ls -l /proc/PID/exe. Treat that branch as untested.
Step 5: Harden with a drop-in file
Alpine's /etc/ssh/sshd_config has Include /etc/ssh/sshd_config.d/*.conf at line 15, near the top. Per Alpine's advisory the default config reads that directory from 3.19 onward, so on 3.18 and older put the settings in sshd_config itself. sshd uses the first value it sees for each keyword, so a drop-in loaded from that Include wins over later defaults. Here is the file I used:
# /etc/ssh/sshd_config.d/10-hardening.conf
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
AllowUsers deploy
MaxAuthTries 3
LoginGraceTime 20
Before you apply it on a real host: confirm your key-based login works from a second session first, and change AllowUsers to the accounts that truly need SSH. A wrong AllowUsers locks you out.
cp 10-hardening.conf /etc/ssh/sshd_config.d/10-hardening.conf
sshd -t && echo "sshd -t: config OK"
rc-service sshd restart # on Alpine with OpenRC; in my lab container I used kill -HUP
sshd -T | grep -Ei 'passwordauthentication|kbdinteractive|usepam|permitrootlogin|allowusers'
sshd -t: config OK
permitrootlogin prohibit-password
passwordauthentication no
kbdinteractiveauthentication no
allowusers deploy
(In my lab there is no init system, so I sent sshd a SIGHUP instead of rc-service. On a real Alpine host use the command in the comment.) The audit script now reads:
VERDICT FIXED and HARDENED
That is the end state I want everywhere: patched package, restarted daemon, and password login off so that a future bug in the password path has nothing to reach.
How do I check a whole fleet of containers?
If your containers are on one Docker host, a docker exec loop is enough. Copy the script in, run it, print one line per container:
for c in $(docker ps --format '{{.Names}}'); do
printf '%-12s %-20s ' "$c" "$(docker inspect -f '{{.Config.Image}}' $c)"
docker cp sshole-audit.sh $c:/tmp/a.sh
docker exec $c sh /tmp/a.sh | awk '/VERDICT/{sub(/^VERDICT +/,"");print}'
done
My captured output across three containers (the Alpine one with sshd, a plain Alpine one, and a Debian one):
sshlab alpine:latest FIXED and HARDENED
sshlab-app alpine:latest NOT AFFECTED (no sshd)
sshlab-deb debian:stable-slim NOT AFFECTED (glibc or other libc)
Two things the loop shows. The same alpine:latest tag gave two different verdicts, because one container installed sshd and the other did not. And the script works on a glibc image too, since the first check is the libc and it exits early. The loop only sees running containers. Stopped containers and, more importantly, images that are not running can still carry a vulnerable openssh-server. For images, grep the Dockerfiles for openssh-server and rebuild anything that matches.
Better question than "is sshd on?": should it be? SOC Prime suggests considering removing SSH from containers entirely in favor of docker exec or kubectl exec. I agree. Most containers that have sshd have it out of habit.
Rebuilding images
Patching a host does not update packages baked into an image. SOC Prime and OX Security both say to rebuild images that install openssh-server and redeploy. A minimal Dockerfile that pulls in the fix and carries the hardening drop-in:
FROM alpine:3.24
RUN apk upgrade --no-cache && apk add --no-cache openssh-server
COPY 10-hardening.conf /etc/ssh/sshd_config.d/10-hardening.conf
docker build --pull --no-cache -t sshlab-rebuilt .
docker run --rm sshlab-rebuilt apk info -v | grep '^openssh-server-[0-9]'
openssh-server-10.3_p1-r2
--pull refreshes the base image and --no-cache stops Docker from reusing an old apk add layer that still has the unpatched package. The second flag is the one people forget. When I was done, I removed the lab containers and the rebuilt image.
What should I look for in my logs?
Every advisory gives the same primary signal: a successful password login for an account that should not have a password. On Alpine, sshd logs to /var/log/messages. Alpine's advisory, OX Security, Orca and Detectify all say to search for "Accepted password for" and then look at the account name. Here is the simple version:
grep "Accepted password" /var/log/messages
Then cross-check against the star accounts. This version prints a line only when a password login succeeded for an account whose shadow field is *. I tested it on a sample log with fake lines, not on a real compromise:
stars=$(awk -F: '$2=="*"{print $1}' /etc/shadow)
sed -n 's/.*Accepted password for \([^ ]*\) from \([^ ]*\).*/\1 \2/p' /var/log/messages |
while read u ip; do
for s in $stars; do
[ "$u" = "$s" ] && echo "SUSPECT: $u from $ip"
done
done
SUSPECT: deploy from 203.0.113.9
(That output is from my sample log, with a documentation-range IP.) A hit on a * account is exactly the pattern the advisories call out. Orca and OX both say to treat such a host as compromised. If you ship logs to a central place, run the same query there.
SOC Prime and Detectify add more places to look:
- Authentication attempts with abnormally long passwords. Standard sshd logs do not record the password length, so this needs your own tooling or network sensor.
- Unexpected remote access to service accounts.
- Use of
doas(Alpine's sudo alternative), especially from thealpineuser on cloud images. - Newly created users, changes to
authorized_keys, new cron jobs and startup scripts, new package installs. - Unexpected outbound connections after a login.
If you cannot rule out a login, the advisories' advice is blunt: treat the machine as compromised. Review shell history, running processes, privilege escalation, users and groups, authorized_keys, persistence, and network connections. Rotate any credentials and keys the host could reach. That is the same lesson as in my write-up of the LiteLLM 1.107.0 compromise: once a machine has run attacker code, everything it could read is burned.
One limit I want to be honest about. Log review only tells you about what your logs captured. If you did not have Accepted password lines retained from November 5 onward, "no hits" means "no evidence", not "no compromise".
Prevention checklist: patch, restart, harden, shrink exposure
Ordered by what I would do first.
- Upgrade and restart. On Alpine:
apk upgrade -U -a, thenrc-service sshd restart. Verify with the audit script. Check thestale_daemonline. - Turn password login off.
PasswordAuthentication no, andKbdInteractiveAuthentication noas SOC Prime recommends. Confirm key login works first. This is also the workaround Damien Miller gave: "set PasswordAuthentication=no or disable SSH for these systems IMMEDIATELY." - Restrict who can log in.
AllowUsersorAllowGroups. I did not test this against the bug, but it fits the logic: an account that is not allowed should never reach the password check. - Shrink the network exposure. Do not expose SSH to the internet unless you must. Use a VPN, a bastion, or a source-IP allowlist. Restrict unused ports. For containers, prefer
docker execorkubectl execto running sshd. I wrote about one way to do the bastion part in zero-trust bastion on Cloudflare WARP. - Rebuild images. Any image with
openssh-serverbaked in needs a fresh build with--pull --no-cache, then redeploy. - Clean up the
*accounts. For accounts that never need a password, a leading!is not the marker in play. Detectify and OX Security list accounts with!as not affected. Be careful: per the researcher's timeline,!can break key login on a no-PAM build, which is why Alpine moved root to*. Test key login after any change. - Check cloud images. If you use Alpine cloud images (3.18.11 through 3.24.3), do not set a console password on the
alpineuser, and update to a fixed point release. - Hunt in logs as above, going back to November 5, 2026.
- Do not run unverified PoCs or scanner scripts on production. OX Security says this and Previdian says the GitHub PoCs are unverified.
OpenWrt, Buildroot, Yocto and other embedded Linux
This is where I have the least firsthand evidence, so I will say exactly what I know. The advisories name OpenWrt, Buildroot and Yocto builds as potentially exposed because they use musl. I did not test any of them. A few practical points:
- Check whether you run OpenSSH at all. From my general knowledge (not from the advisories), OpenWrt's default SSH server is Dropbear, which the advisories list as not affected. You are in scope only if someone installed
openssh-serverand left Dropbear off or moved it. On a router, runls /usr/sbin/sshdand check which daemon is listening withnetstat -ltnporss -ltnp. - Buildroot and Yocto are build-time choices. Look at your build config for OpenSSH and for PAM. If OpenSSH is enabled and PAM is not, you are in the profile. The fix is a rebuild with OpenSSH 10.6p2 or your vendor's backport.
- Vendor firmware lags. For appliances you did not build, check the vendor's advisory page. Alpine-based appliances and add-ons that ship sshd are worth checking too, and I have not verified any specific one.
- Do the cheap thing now. On any embedded box where you cannot update this week, set
PasswordAuthentication noand make sure key login works. Then schedule the firmware update. - Be careful with banner checks. Embedded vendors backport quietly. Do not use
ssh -Vto declare yourself safe.
What I would do differently, and what I could not verify
A few honest limits on this post, so you know what to weigh:
- I did not reproduce the bug. The lab uses an already-patched package. The verdict logic is my own and rests on the advisories' conditions. The
AFFECTEDline in my lab came from a deliberate override of the fixed version, and says nothing about real exploitability. - The audit script is a heuristic. It checks the five conditions from the advisories. A host can be exploitable in ways the script does not model (for example, a PAM build with
UsePAM nois covered, but a custom build I have not seen is not). If it says "affected", believe it. If it says "fixed", also run your own checks. - I did not test OpenWrt, Buildroot or Yocto. Everything about them is from the advisories.
- The exploitation status is contested. See the Previdian and SOC Prime section above.
- Alpine tracker coverage. The tracker page did not list 3.18, 3.17 and 3.16, while Alpine's own advisory, OX Security and SOC Prime did.
- The CVE number and score. Orca's page and Alpine's advisory said no CVE was assigned yet on November 5, and the CVE record was published later that day. The 8.1 score and the vector (
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H) come from the CVE record's own CNA entry (MITRE). NVD shows that score as a secondary source and had not analyzed the entry when I looked, and the Alpine tracker shows no score.
The design lesson in the bug is the part I keep thinking about. A single character, *, means "no password can match" in a file format from 1980. A library function that used to signal failure with NULL started signaling it with a string. A caller written for the old contract kept comparing strings. No individual change was wrong in isolation. The bug lives in the gap between three decisions made by different people over ten years. When you review auth code, the most dangerous line is the one that compares two strings and assumes one of them can only mean "valid".
FAQ
What is CVE-2026-123158 (sshole)?
A password-authentication bypass in OpenSSH's sshd on musl-based Linux without PAM, disclosed on 2026-11-05 and rated CVSS 8.1. For passwords over 256 bytes, musl's crypt() returns the string * as its error value, which matches a locked account's * shadow field, so sshd accepts the login.
Is my Alpine Linux server affected by sshole?
Only if all of these hold: musl libc, sshd built or run without PAM, PasswordAuthentication yes, an account whose /etc/shadow field is a lone *, and another account with a real $5$ or $6$ hash. glibc distributions and PAM-based sshd are not affected.
Which OpenSSH version fixes CVE-2026-123158?
OpenSSH 10.6p2. On Alpine the fixed packages are 10.6_p2-r0 on edge and 10.3_p1-r2 on 3.24, with fixed builds back to 3.16. Upgrading the package does not restart the running daemon, so restart sshd afterwards.
How do I protect a server I cannot patch today?
Set PasswordAuthentication no, keep key-based logins, and reload sshd. The bug needs the password path, so turning it off closes it until you can upgrade.
If you run Alpine or any musl-based Linux with OpenSSH and no PAM, do four things today: upgrade openssh-server (10.6p2 upstream, 10.3_p1-r2 on Alpine 3.24), restart sshd because the upgrade does not, set PasswordAuthentication no and KbdInteractiveAuthentication no, and grep your logs for "Accepted password" on accounts with a * shadow field since November 5. Rebuild images that bake in sshd, and ask whether the container needs sshd at all. glibc and PAM-based hosts are not affected.
References
- OX Security: OpenSSH authentication bypass, CVE-2026-123158
- Orca Security: sshole, OpenSSH authentication bypass gives attackers root on Alpine Linux
- LWN.net: Critical security vulnerability affects OpenSSH on Alpine Linux systems
- BleepingComputer: OpenSSH flaw lets attackers log into password-disabled Linux accounts
- Help Net Security: OpenSSH flaw exploited in the wild just hours after disclosure
- SOC Prime: CVE-2026-123158 OpenSSH password authentication bypass affects Alpine Linux
- Detectify: Critical vulnerability, OpenSSH authentication bypass on Alpine Linux
- Alpine Linux: Security advisory, openssh-server password authentication bypass
- Alpine security tracker: CVE-2026-123158
- CVE record for CVE-2026-123158
- OpenSSH 10.6p2 release notes
- sshhole.com: the researcher's write-up by Ozgun Kultekin
- Andre Baptista on X: reproduced sshole in a lab
- International Cyber Digest on X: OpenSSH rushes a fix


