On 5 November 2026 Alpine Linux published an advisory and OpenSSH shipped 10.6p2 the same day. On a musl host running sshd without PAM, an account whose shadow field is will accept a password it never had. musl's crypt() returns the string to signal an over-long key, and is also exactly what the shadow field holds for an account with no password, so the comparison succeeds. root on Alpine ships as root:. So do key-only users, LDAP/SSSD users with no shadow entry, and the alpine user in tiny-cloud images.

These checks are for whoever has to produce a defensible statement about Alpine in container images, bastions, CI runners and musl-based appliance firmware, and about whether anyone used the hole first. Three artifacts a review normally accepts fail on this bug: the OpenSSH version banner, the running daemon after apk upgrade, and the dependency scanner.

Short version: exposure needs four facts to hold at once (musl, no PAM, password auth on, a * account sitting next to a real $5$ or $6$ hash), the fix is the Alpine -rN package plus an sshd restart, and your SCA tool will stay silent because the advisory carries no package and no version range.

The checks that survive a review

  1. Decide exposure from four runtime facts, not from a CVE feed. All four must hold, per Alpine's advisory: the daemon links musl, PAM is out of the auth path, password authentication is on, and the host has a * account plus at least one account with a real $5$ or $6$ hash, whose salt the attempt borrows. Run all three commands and record all three answers.
ldd /usr/sbin/sshd | grep -i musl
sshd -T | grep -iE '^(usepam|passwordauthentication|permitrootlogin)'
awk -F: '{print $1, substr($2,1,3)}' /etc/shadow
sshd on this hostlinks musl libc?Not affectedPAM in the auth path?PasswordAuthentication yes?an account with * plus one real hash?Exposed: patch and restart, then hunt logsno, glibcyesopenssh-server-pam, UsePAM yesopenssh-server, or UsePAM nonoyesnoyes
  1. Read the effective config with sshd -T, never the file. PasswordAuthentication no sitting in /etc/ssh/sshd_config proves nothing when an Include directory, a cloud-init drop-in or a config-management snippet re-enables it further down. On my own server a cloud-init drop-in silently overrode that exact keyword for weeks while the main config file read correctly, and the dump was the only place it showed. Alpine's workaround tells you to verify against the dump too.
sshd -T | grep -i passwordauthentication   # must print: passwordauthentication no
  1. Key your inventory on the -rN, because the OpenSSH version number sorts it wrong. Alpine backported the fix across ten branches, so the patched builds are 9.0_p1-r6 on 3.16, 9.9_p2-r1 on 3.21, 10.3_p1-r2 on 3.24 and 10.6_p2-r0 on edge. A host whose banner reads OpenSSH_9.9 can be either state, which is the same trap as php2shell and Node Brotli RCE: the version string lies, and the per-branch divergence matches core.fsmonitor RCE: 4 Agent Patches, 4 Different Key Lists.
apk policy openssh | head -20
apk info -v openssh openssh-server
  1. Check /proc before you call the host patched, then restart the daemon. Alpine leads its advisory with this: apk upgrade replaces the binary on disk and leaves the old sshd resident in memory. The cheap fleet-wide proof is the exec link of the live process, which gains a (deleted) suffix once the file under it has been swapped.
ls -l /proc/$(pgrep -o -x sshd)/exe   # "(deleted)" means patched on disk, vulnerable in RAM
rc-service sshd restart
  1. Expect your scanner to stay silent, and write that down before an auditor asks. GHSA-4fcc-vm97-gx3x carries a CVSS of 8.1 and, as printed today, "Affected versions: Unknown", "Patched versions: Unknown" and no package from a supported ecosystem, so GitHub states on the page that Dependabot alerts are unsupported for it. A version-range matcher has nothing to match, the same shape as the identifier problem in Fix Starlette CVE-2026-48710: 1.0.1 Misses the Sibling. What I could not confirm is whether Alpine's secdb feed has landed a fixed-version entry for every branch yet, and this query settles it per branch:
curl -s https://secdb.alpinelinux.org/v3.22/main.json | jq '.packages[] | select(.pkg.name=="openssh")'
  1. Hunt for use before you declare it closed. Alpine names the signal: sshd lines in /var/log/messages reading Accepted password for an account that has no password. The advisory's instruction when that search is inconclusive is to treat the machine as compromised, so pair the grep with a review of doas invocations, newly created users and recently touched authorized_keys files.
grep -E 'sshd.*Accepted password for (root|alpine)' /var/log/messages
find /home /root -name authorized_keys -newermt '2026-10-20' -ls
  1. Do not "lock" the key-only accounts to fix it. Swapping * for ! reads as the tidy hardening move and it closes the wrong door: Alpine's advisory notes that an account with ! cannot authenticate by password or by key, so the edit also breaks the automation that used that account's key. Turn password authentication off and leave the shadow field alone.
  2. Price the break-glass you are about to remove. PasswordAuthentication no is the durable control, and on a console-less VM or an appliance with no serial path it also deletes your last recovery route, which is how a key change turns into a dead host (see x509: no valid chains built: Go 1.27.3 blocks 8 keys). Name the replacement before the change window: a provider console, an out-of-band path, or a second key held by a separate custodian. One row I have not verified is whether KbdInteractiveAuthentication yes offers a second route into the same comparison on a PAM-less build, and the check is to read the device table in auth2-chall.c for the build you ship, since without PAM, bsdauth or skey it should register no devices at all.
  3. Inventory the sshd instances you forgot you owned. The disclosure page lists musl-based projects beyond Alpine, including OpenWrt, Buildroot and Yocto, which puts this bug in appliance and device firmware you cannot apk upgrade. Tiny-cloud images from 3.18.11 to 3.24.3 ship an alpine user with a * password and passwordless doas, so enumerate from the registry and the cluster rather than from a spreadsheet.
kubectl get pods -A -o jsonpath='{range .items[*]}{.spec.containers[*].image}{"\n"}{end}' | sort -u
docker run --rm --entrypoint sh IMAGE -c 'apk info -v openssh-server 2>/dev/null; ldd /usr/sbin/sshd 2>/dev/null'
  1. For a vendor appliance, get three answers in writing and a network control today. Ask whether the firmware links musl, whether sshd runs with PAM, and whether password authentication is enabled. Those three determine the verdict and no vendor can reasonably decline to answer them. Until a firmware build arrives, restrict port 22 to a bastion CIDR or a management VLAN and log the restriction with a date, so the compensating control is an artifact rather than a memory.
  2. Treat the class, not the instance. GitHub files this as CWE-394, "Unexpected Status Code or Return Value": musl's crypt_sha512.c defines KEY_MAX 256, rejects a longer key, and the __crypt_sha512 wrapper hands back the string "*", a sentinel the caller then compared as data. Upstream's commit 4353fdc is four lines of defensive minimum, MIN_PASSWORD_HASH_LEN 4 applied in sys_auth_passwd() ahead of the strcmp. The habit that catches this family is probing behaviour instead of grepping versions: on the October Brotli patch day I ran the decoder probe against my own box before recommending it to anyone, and the release number said patched while the error code (-9) said otherwise on an end-of-life Node 20 host.

Wrap-up

Separate patched on disk from patched in the process, and make the second one the artifact you record. The gap between those two states here is an apk upgrade with no restart, which is the most common way a host shows green in a report and still accepts the login. Build the per-host row now: musl yes/no, PAM yes/no, passwordauthentication as printed by sshd -T, package -rN, daemon restart time, log review timestamp. A version scan cannot produce that row, and it is what an auditor accepts.

If the honest answer is that you cannot enumerate every musl sshd in your estate this week, that gap is scoped work: the inventory, the proof row for each host, and the compensating controls for the appliances nobody can patch. The services page lists the packages, and the scoping form there is the fastest way to get a fixed quote.

What I read while writing this: Alpine's advisory, OpenSSH 10.6p2 release notes, and the musl crypt_sha512.c source linked above.