Alpine OpenSSH CVE-2026-123158: Patching and Hunting – ThreatDefence
Skip to content
Security advisories/Alpine Linux; OpenSSH
CriticalActive

Alpine OpenSSH CVE-2026-123158 (sshole): Patching sshd, Finding Exposed Accounts and Hunting Password Logins

Anton Guzhevskiy
Anton Guzhevskiy · Principal Product Researcher
Published November 6, 2026

CVE-2026-123158, called sshole, lets anyone who can reach sshd on Alpine Linux log in to accounts that have no password, such as root or the cloud image’s alpine user, by sending an overlong password. Proof-of-concept code is public.

TL;DR

Upgrade openssh and restart sshd today, or turn off password authentication.

At a glance
SeverityCritical
StatusActive
ExploitedNo exploitation in the wild reported. Public proof-of-concept code exists, and CISA's assessment in the CVE record is proof of concept, not automatable, total technical impact. Not in CISA KEV
ProductOpenSSH sshd before 10.6p2 on Linux systems that use musl libc without PAM, including Alpine Linux 3.16 to 3.24 and edge (openssh-server package)
CVEsCVE-2026-123158
Fixed inAlpine openssh packages released November 5, 2026 for every branch from 3.16 to edge, and Alpine 3.21.10, 3.22.8, 3.23.8 and 3.24.4. Upstream OpenSSH 10.6p2. Restart sshd after upgrading

Alpine Linux published a security advisory on November 5, 2026 for an authentication bypass in its openssh-server package. An attacker who can reach the SSH port sends a password between 257 and 1,024 bytes and is logged in to any account whose password field is *, the value Alpine uses for accounts with no password. Alpine says it takes one connection and works every time.

OpenSSH released 10.6p2 the same day with a workaround in sshd, and MITRE assigned CVE-2026-123158 (CVSS 3.1 8.1). OpenSSH considers the root cause to be outside sshd, in how musl’s crypt() reports errors, but fixed it in sshd because that is where it can be exploited. Alpine shipped fixed packages for every branch back to 3.16, including 3.21, which reached end of support on November 1.

Upgrading the package is not enough on its own. The running sshd keeps the old code until it is restarted.

How the bypass works

  1. Alpine’s default openssh-server is built without PAM, so sshd checks a password itself: it hashes the attempt with crypt() and compares the result to the account’s entry in /etc/shadow.
  2. When the stored entry is too short to contain a salt, as * is, sshd borrows the salt from another account’s hash.
  3. musl’s crypt() returns the string * to signal an error when the password is longer than 256 bytes.
  4. * is also exactly what is stored for accounts with no password. The comparison matches and sshd accepts the login.

Three conditions must all hold:

  • an account has * as its password field
  • at least one other local account has a $5$ (SHA-256) or $6$ (SHA-512) password hash, which supplies the salt
  • password authentication is enabled in sshd, which is the OpenSSH default

One account with a real password is enough to expose every account without one.

Who is exposed, in priority order

  1. Alpine hosts with SSH reachable from the internet, password authentication on, and any account with *. Treat these as possibly compromised if they were reachable after November 5, when the details became public.
  2. Instances built from official Alpine cloud images (tiny-cloud) 3.18.11 to 3.24.3. They include an alpine user with * as its password and passwordless doas, so a login becomes root. They are safe on first boot and become exploitable as soon as any $6$ hash exists on the machine, for example a console password or hashed_passwd in user-data.
  3. Hosts with PermitRootLogin yes. Alpine has shipped root with * since about 3.16, so root itself is reachable when root login is allowed.
  4. Key-only users created without a password or with usermod -p '*', and directory users (nslcd, winbind, sssd through musl-nscd) that have no shadow entry.
  5. Alpine-based container images that run sshd with password authentication on, such as SFTP and jump-host images. Containers that do not run sshd are not affected.
  6. Other Linux systems that build OpenSSH against musl without PAM. The CVE covers any of them. Ask your vendor whether its build uses PAM.

Not affected: openssh-server-pam with UsePAM yes, systems with password authentication off, accounts locked with ! rather than *, systems with no $5$ or $6$ hashes, and cloud-init images that set PasswordAuthentication no. Systems using glibc, such as Debian, Ubuntu and RHEL, are not affected.

Affected and fixed versions

Alpine branch Fixed openssh package
edge 10.6_p2-r0
3.24 10.3_p1-r2 (point release 3.24.4)
3.23 10.2_p1-r1 (point release 3.23.8)
3.22 10.0_p1-r11 (point release 3.22.8)
3.21 (end of support November 1, 2026) 9.9_p2-r1 (point release 3.21.10)
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

Upstream, the fix is in OpenSSH 10.6p2. Cloud images built from 3.21.10, 3.22.8, 3.23.8, 3.24.4 and later include it. The linuxserver.io openssh-server image moved to the fixed 3.24 package in release 10.3_p1-r2-ls242 on November 5.

How to check a host

Run these on each Alpine host or container that runs sshd. They only read state:

cat /etc/alpine-release
apk info -v openssh-server openssh-server-pam 2>/dev/null
sshd -T | grep -E '^(passwordauthentication|usepam|permitrootlogin) '
awk -F: '$2=="*"{print "no password: " $1}' /etc/shadow
grep -cE '^[^:]*:\$[56]\$' /etc/shadow

The host is exposed if the package is below the fixed version for its branch, passwordauthentication is yes, usepam is no or the PAM package is not installed, at least one account has no password, and the last command prints 1 or more. If the package is already fixed, make sure sshd has been restarted since it was installed.

For container images, run the apk info and sshd -T commands inside each running container, or check the image’s base Alpine version in your registry scanner.

Mitigation and remediation

  1. Upgrade and restart sshd:
apk upgrade -U -a
rc-service sshd restart
  1. If you cannot upgrade today, turn off password authentication. Confirm that key-based login works first and keep your current session open. On Alpine 3.19 and later:
echo 'PasswordAuthentication no' > /etc/ssh/sshd_config.d/00-no-password.conf
sshd -t && rc-service sshd restart
sshd -T | grep passwordauthentication

On 3.18 and older, which ignore sshd_config.d, set PasswordAuthentication no in /etc/ssh/sshd_config itself, including inside any Match blocks.

  1. Rebuild Alpine-based images that run sshd, and redeploy the containers. Pulling a new base image does not change containers already running.
  2. Update cloud instances launched from tiny-cloud images 3.18.11 to 3.24.3, or turn off password authentication on each one. Rebuild your golden images from 3.21.10, 3.22.8, 3.23.8 or 3.24.4 or later.
  3. Lock accounts that should never log in with a password using ! rather than * (passwd -l <user>), and set PermitRootLogin prohibit-password or no.
  4. Take SSH off the internet where it does not need to be there, or restrict it to known source addresses.

Threat hunting

Alpine’s sshd logs a successful bypass as an ordinary Accepted password for <user> line in /var/log/messages. From OpenSSH 9.8, which Alpine 3.21 and later ship, the line comes from the sshd-session process rather than sshd, so the queries match both. Password length is not logged, so the signal is a password login to an account that has no password, or on a host where logins should only use keys. A successful bypass also produces no failed attempts first.

Microsoft Sentinel queries

The KQL below is example code for Microsoft Sentinel with Alpine syslog forwarded to the Syslog table. Adapt table and field names to your environment and test each query before relying on it. Replace the example host and account lists with your own, or use watchlists.

Hunt 1: password logins to accounts that have no password

// Example: Sentinel Syslog, sshd password logins to accounts that should be key-only
let AlpineHosts = dynamic(["alpine-web-01", "alpine-bastion-01"]);   // your Alpine hosts, or a watchlist
let NoPasswordAccounts = dynamic(["root", "alpine"]);                // add key-only and service users
Syslog
| where TimeGenerated > ago(30d)
| where ProcessName in~ ("sshd", "sshd-session") and SyslogMessage has "Accepted password for"
| parse SyslogMessage with * "Accepted password for " User " from " SrcIP " port " SrcPort " " *
| where Computer in~ (AlpineHosts) or User in~ (NoPasswordAccounts)
| extend ExternalSource = not(ipv4_is_private(SrcIP))
| project TimeGenerated, Computer, User, SrcIP, ExternalSource, SyslogMessage
| order by TimeGenerated desc

Any hit for root, alpine or a key-only user on a host that was unpatched at the time should be treated as a compromise.

Hunt 2: first password login on a host that normally uses keys

// Example: Sentinel Syslog, password logins on hosts and accounts that used only public keys before
let Baseline = Syslog
    | where TimeGenerated between (ago(90d) .. ago(7d))
    | where ProcessName in~ ("sshd", "sshd-session") and SyslogMessage has "Accepted "
    | parse SyslogMessage with * "Accepted " Method " for " User " from " *
    | summarize Methods=make_set(Method) by Computer, User;
Syslog
| where TimeGenerated > ago(7d)
| where ProcessName in~ ("sshd", "sshd-session") and SyslogMessage has "Accepted password for"
| parse SyslogMessage with * "Accepted password for " User " from " SrcIP " port " *
| join kind=leftouter Baseline on Computer, User
| where isnull(Methods) or not(set_has_element(Methods, "password"))
| project TimeGenerated, Computer, User, SrcIP, PreviousMethods=Methods
| order by TimeGenerated desc

Hunt 3: doas or sudo soon after a password login

On tiny-cloud images the alpine user can run doas without a password, so a bypass becomes root in one step.

// Example: Sentinel Syslog, privilege use within 30 minutes of an sshd password login
let PasswordLogins = Syslog
    | where TimeGenerated > ago(30d)
    | where ProcessName in~ ("sshd", "sshd-session") and SyslogMessage has "Accepted password for"
    | parse SyslogMessage with * "Accepted password for " User " from " SrcIP " port " *
    | project LoginTime=TimeGenerated, Computer, User, SrcIP;
Syslog
| where TimeGenerated > ago(30d)
| where ProcessName in~ ("doas", "sudo") or SyslogMessage has_any ("doas:", "sudo:")
| project PrivTime=TimeGenerated, Computer, PrivMessage=SyslogMessage
| join kind=inner PasswordLogins on Computer
| where PrivTime between (LoginTime .. LoginTime + 30m)
| project LoginTime, PrivTime, Computer, User, SrcIP, PrivMessage
| order by LoginTime desc

Hunt 4: one source logging in by password to several hosts

A scanner using the bypass gets in on the first try, so look for one address with successful password logins across many hosts and no failures.

// Example: Sentinel Syslog, sources with password logins to several hosts and no failed attempts
let Window = 1d;
let Accepted = Syslog
    | where TimeGenerated > ago(30d)
    | where ProcessName in~ ("sshd", "sshd-session") and SyslogMessage has "Accepted password for"
    | parse SyslogMessage with * "Accepted password for " User " from " SrcIP " port " *
    | summarize Hosts=dcount(Computer), HostList=make_set(Computer, 20), Users=make_set(User, 10),
                FirstSeen=min(TimeGenerated) by SrcIP, bin(TimeGenerated, Window);
let Failed = Syslog
    | where TimeGenerated > ago(30d)
    | where ProcessName in~ ("sshd", "sshd-session") and SyslogMessage has_any ("Failed password", "Invalid user")
    | parse SyslogMessage with * " from " SrcIP " port " *
    | summarize Failures=count() by SrcIP;
Accepted
| where Hosts >= 3
| join kind=leftouter Failed on SrcIP
| where isnull(Failures) or Failures == 0
| order by Hosts desc

Hunt 5: SSH to Alpine hosts allowed from the internet

This shows which Alpine hosts are reachable on SSH from outside and should be patched and checked first.

// Example: Sentinel CommonSecurityLog, inbound SSH allowed from public addresses to Alpine hosts
let AlpineIPs = dynamic(["203.0.113.10", "10.20.0.15"]);   // your Alpine host addresses
CommonSecurityLog
| where TimeGenerated > ago(30d)
| where DestinationPort == 22 and DestinationIP in (AlpineIPs)
| where DeviceAction in~ ("allow", "accept", "permitted", "pass")
| where not(ipv4_is_private(SourceIP))
| summarize Connections=count(), Sources=dcount(SourceIP), SampleSources=make_set(SourceIP, 20),
            FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated)
    by DestinationIP, DeviceVendor
| order by Sources desc

Responding to a possible compromise

  1. Keep a copy of /var/log/messages and any forwarded logs before changing anything, and note the source addresses of the password logins.
  2. Upgrade openssh and restart sshd, or turn off password authentication, then take the host off untrusted networks.
  3. Look for persistence: new entries in /etc/passwd and /etc/shadow, changes to every ~/.ssh/authorized_keys, /etc/doas.conf and /etc/doas.d/, OpenRC services and scripts in /etc/init.d/ and /etc/local.d/, cron entries in /etc/crontabs/, and new files in /tmp, /var/tmp and /dev/shm.
  4. Rotate secrets stored on the host or reachable from it: SSH keys, API tokens, cloud instance credentials and database passwords.
  5. Rebuild the host or container if a bypass login is confirmed or cannot be ruled out. Containers should be redeployed from a fixed image, not cleaned in place.

Indicators of compromise

None have been published. The bypass uses a normal SSH login, so the hunts above look for password logins where none should happen rather than for addresses or files.

Timeline

  • November 1, 2026: Alpine 3.21 reaches end of support.
  • November 5, 2026: Alpine publishes its advisory and fixed openssh packages for 3.16 to edge. OpenSSH releases 10.6p2. MITRE publishes CVE-2026-123158. linuxserver.io ships a fixed openssh-server image.
  • November 6, 2026: Proof-of-concept code is public. CVE-2026-123158 is not in CISA KEV, and FIRST has not yet scored it in EPSS.

Frequently Asked Questions

Is this a bug in OpenSSH or in Alpine? Both are involved. musl’s crypt() returns * as an error value, and that is also the stored value for accounts with no password. OpenSSH 10.6p2 adds a workaround in sshd, and Alpine shipped it in its packages.

Does the attacker need a valid account name? Yes, but the common ones are predictable: root where root login is allowed, and alpine on official cloud images.

We use SSH keys only. Are we safe? Only if password authentication is turned off in sshd. Key-only accounts usually have * as their password, which is exactly what the bypass targets. Check with sshd -T | grep passwordauthentication.

We upgraded the package. Is that enough? No. Restart sshd with rc-service sshd restart. The old daemon keeps running until you do.

Are Alpine containers affected? Only those that run sshd with password authentication on. Most Alpine containers do not run sshd at all.

Is Debian, Ubuntu or RHEL affected? No. They use glibc, and their OpenSSH builds use PAM.

Is it being exploited? No exploitation in the wild has been reported, but proof-of-concept code is public and the attack takes one connection. Assume scanning has started.

How ThreatDefence is responding

ThreatDefence has added CVE-2026-123158 to our vulnerability intelligence. Where we hold Linux syslog, firewall and cloud telemetry, our SOC is:

  • identifying Alpine hosts and containers with SSH reachable from untrusted networks
  • alerting on password logins to root, alpine and key-only accounts, and on privilege use that follows them
  • helping customers confirm the fixed package and the sshd restart across their fleet

If you run Alpine servers or images and want help checking them or investigating a suspicious login, contact us. Read more about our managed detection and response.

Sources

Need help checking your environment for this issue? Talk to the ThreatDefence SOC.Talk to Our SOC

Talk to Our SOC

Questions about this advisory or help checking your environment.

    I'm an MSP, MSSP, or reseller

    By submitting this form, you accept the terms in our privacy policy.