On 5 November 2026 the OpenSSH team released 10.6p2 to close a password authentication bypass, and Alpine published an advisory the same day. The researcher who found it, Özgün Kültekin, calls it sshhole. No CVE identifier had been assigned when this was written.
This post is for people who run sshd on Alpine or on another musl-based system: what has to be true for a server to be exposed, how to check a host or an image, and what to change. It contains no exploit.
What has to line up
Alpine’s advisory lists the conditions. All of them must hold:
| Condition | Alpine’s openssh-server default |
|---|---|
| sshd built against musl libc | yes |
| sshd built without PAM | yes |
PasswordAuthentication yes | yes |
an account whose password field in /etc/shadow is * (no password set, normal for key-only accounts such as root) | yes |
at least one other account with a $5$ or $6$ password hash | depends on the machine |
the attacker can reach the SSH port and knows the name of the * account | depends on the network |
When they hold, a password between 257 and 1024 bytes logs in to the * account. According to the advisory it works on the first try. One account with a real password on the machine is enough to make every account without one reachable.
Systems on glibc (Debian, Ubuntu, Fedora and most others), any sshd that authenticates through PAM, Dropbear, and OpenBSD are not affected.
How it works
Three pieces, written years apart:
/etc/shadowuses*as “no password”. A real entry looks like$6$<salt>$<hash>. A single*can never be the output of a hash, so it marks an account that only opens with a key.- musl’s
crypt()returns*on failure. Given a password longer than 256 bytes, musl doesn’t hash it. It returns the string*instead of an error. This behavior dates from 2012. - OpenSSH hashes even for accounts without a password. Since 2016, to keep response times from revealing which accounts exist, sshd runs
crypt()for accounts that have no usable hash too, with a salt borrowed from an account that has one. That is why a second account with a$5$/$6$hash is required.
sshd then compares the computed string with the stored field. For an over-long password against a * account, both are *, and the comparison succeeds.
The fix in commit 4353fdc adds a minimum length to that comparison, since no real hash is shorter than its own prefix:
#define MIN_PASSWORD_HASH_LEN 4
A stored field shorter than 4 characters never matches. The commit message notes that the root cause isn’t in OpenSSH itself and is avoided there.
Seeing the musl behavior safely
You can watch the crypt() boundary without any SSH involved. The program below only calls the local crypt() with a fixed test salt:
// crypt_probe.c: call musl's crypt() with passwords of different lengths
#define _GNU_SOURCE
#include <crypt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void probe(size_t n) {
char *pw = malloc(n + 1);
memset(pw, 'A', n); pw[n] = '\0';
char *r = crypt(pw, "$6$testsalt$");
printf("len=%-4zu result starts with: %.8s\n", n, r ? r : "(null)");
free(pw);
}
int main(void) { probe(8); probe(256); probe(257); probe(300); return 0; }
docker run --rm -v "$PWD:/src:ro" alpine:3.23 sh -c \
'apk add -q build-base && cc -o /tmp/probe /src/crypt_probe.c && /tmp/probe'
len=8 result starts with: $6$tests
len=256 result starts with: $6$tests
len=257 result starts with: *
len=300 result starts with: *output
Up to 256 bytes you get a SHA-512 hash. From 257 bytes on, the result is the same * that marks a locked account.
Check a server
On the Alpine host, as root:
# installed package (compare with the fixed versions below)
apk list -I 2>/dev/null | grep '^openssh-server'
# PAM or not: no output means sshd is built without PAM
ldd /usr/sbin/sshd | grep -i pam
# are password logins on in the running configuration?
sshd -T | grep -i '^passwordauthentication'
# accounts with no password set, and accounts with a $5$/$6$ hash
awk -F: '$2 == "*" { print "no password:", $1 }' /etc/shadow
awk -F: '$2 ~ /^\$[56]\$/ { print "hash:", $1 }' /etc/shadow
If passwordauthentication is yes, there is at least one * account and at least one hashed account, and the package is older than the fixed one, the server is exposed.
Check container images
Many images are based on Alpine, but only those that install and run openssh-server matter here:
docker run --rm --entrypoint sh <image> -c 'apk list -I 2>/dev/null | grep "^openssh-server" || echo "no openssh-server"'
Fixed packages
| Alpine branch | Fixed package |
|---|---|
| edge | openssh-10.6_p2-r0 (upstream 10.6p2) |
| 3.23 | openssh-10.2_p1-r1 (10.2p1 with the fix backported) |
The stable package keeps its old upstream version number, so sshd -V alone doesn’t tell you whether a stable host is patched. Check the package revision. The advisory covers the other supported branches.
Fix, or close it for now
Upgrade and restart. The package upgrade does not restart the running daemon, and the old process keeps the old code until it does:
apk update && apk upgrade openssh openssh-server
rc-service sshd restart
If you can’t upgrade right away, turning off password logins closes it too:
echo 'PasswordAuthentication no' > /etc/ssh/sshd_config.d/00-no-passwords.conf
sshd -t && rc-service sshd reload
sshd -T | grep -i '^passwordauthentication'
passwordauthentication nooutput
sshd -t checks the syntax before the reload, and sshd -T shows the setting the daemon will use. For images, rebuild from the updated base and redeploy. A running container keeps the old package.