# SSHhole: the OpenSSH bypass and your Azure Alpine audit

OpenSSH 10.6p2, released 5 November 2026, closes CVE-2026-123158: on musl-based Linux without PAM, any password of 257 to 1,024 bytes logs in to any account whose shadow field is "*", which is the Alpine default for root and for the user in Alpine's own cloud images. Azure Linux and every endorsed distro are unaffected, so here is the audit for where it actually hides: Alpine VMs from VHDs, App Service custom containers built from Microsoft's SSH template, and sshd pods.

- Published: 2026-11-06 · Category: Security & Compliance · Tags: OpenSSH, CVE-2026-123158, Alpine Linux, Security, Azure App Service, AKS, Azure Container Registry, Defender for Cloud, NIS2, Cybersäkerhetslagen, Cyber Resilience Act
- Author: Technspire AB, Stockholm (https://technspire.com)
- Canonical: https://technspire.com/en/blog/sshhole-openssh-bypass-your-azure-alpine-audit

On 5 November 2026 the OpenSSH project shipped 10.6p2 to close CVE-2026-123158, a password authentication bypass that has sat in portable OpenSSH for roughly a decade. The conditions are narrow and the exploit is trivial: on a Linux system built on musl libc and running sshd without PAM, any password between 257 and 1,024 bytes logs you in to any account whose password field is a single asterisk. That describes a stock Alpine Linux box with sshd on it, which is why the bug has picked up the name SSHhole. MITRE rates it CVSS 8.1. A proof of concept was public within hours. Alpine pushed fixed packages for every supported branch the same day. Nothing here touches Ubuntu, Debian, Red Hat or Azure Linux, so your AKS nodes and your Marketplace VMs are fine. The exposure on Azure sits in the places most inventories do not reach: custom containers with an SSH daemon baked in, Alpine VMs uploaded from a VHD, and vendor appliances nobody has rebuilt since 2023.

## What the bug actually is

Three unrelated design decisions collide. First, musl's crypt() function refuses passwords longer than 256 bytes and signals the error by returning the string "*". glibc returns NULL in the same situation. Second, Alpine stores a locked, key-only account with exactly "*" in the password field of /etc/shadow, and since Alpine 3.16 that is the default state of root. Third, a 2016 change in OpenSSH, meant to stop username enumeration, made sshd run the hash function even when the target account has no usable password, borrowing the salt format from a real hash elsewhere on the system so the timing looks identical.

Put them together. An attacker connects, names an account whose shadow entry is "*", and sends 300 characters of anything. musl fails the hash and returns "*". sshd compares that result with the stored field, finds "*" equals "*", and accepts the login. No password knowledge, no key, one connection.

Alpine's advisory lists the four conditions that all have to hold:

- sshd built against musl and running without PAM, or the PAM build with UsePAM set to no. Alpine's default openssh-server package has no PAM.
- PasswordAuthentication enabled, which is the upstream and the Alpine default.
- At least one account whose password field is exactly "*". Accounts locked with "!" or an empty field are safe.
- At least one local account with a real $5$ or $6$ hash, because that is where sshd borrows the salt format from.

The fourth condition is the one that sounds reassuring and is not. It is satisfied the moment anyone sets a password for anyone on the box. Ethiack's writeup makes the point with Alpine's own cloud images: tiny-cloud builds from 3.18.11 through 3.24.3 ship a user named alpine with "*" in shadow and passwordless doas. The image is safe until an administrator gives any account a password, and from that moment a 257-byte login as alpine followed by doas is root.

The release note is unusually candid about why the fix shipped the way it did. OpenSSH says it is now receiving many security reports from AI tools and researchers, and that "adversaries who do not report bugs to OSS projects are likely to be able to discover these bugs too", so it will release more often rather than batch fixes. Read that as a statement about your patch cadence as much as theirs.

## Where it lands on Azure

Start by clearing the ground. Azure Linux, Microsoft's own distribution and the container host for AKS, builds on glibc; the spec is in the public repository. Ubuntu, Debian, RHEL, SUSE and every other endorsed distribution on Azure are glibc as well. Alpine is not an endorsed distribution and has no Marketplace platform image. So the question for an Azure estate is never "are our nodes vulnerable" but "where did someone put Alpine with sshd inside something we run".

| Where | Affected? | Why |
| --- | --- | --- |
| AKS nodes (Ubuntu, Azure Linux) | No | glibc, and the node sshd is the distro's PAM build |
| Marketplace Linux VMs | No | No endorsed distribution uses musl |
| Alpine VMs from an uploaded VHD | Yes, highest risk | Alpine's own cloud image ships the "*" account; port 22 is usually open to something |
| App Service custom containers with the legacy sshd pattern | Possible | Alpine base, apk add openssh, PasswordAuthentication yes; reachable only via Kudu |
| Pods and Container Apps running sshd on Alpine | Possible | Anything exposed through a LoadBalancer or TCP ingress is internet-facing |
| Alpine containers without sshd | No | Alpine states this explicitly; no daemon, no bug |
| Vendor appliances and edge devices on musl | Ask | You cannot see the shadow file; the vendor can |

The App Service row deserves a closer look because Microsoft documented the pattern. Its public example repository for SSH in custom containers builds on a Node Alpine image, runs apk add openssh, sets the root password to Docker! and copies in an sshd_config with PasswordAuthentication yes and PermitRootLogin yes on port 2222. That template has been copied into a great many production images. Setting the root password satisfies the "one real hash" condition on its own. Whether the image is exploitable then depends on a single question: does any other account in that image carry "*" in shadow? Base images and installed packages decide that, and you cannot answer it from the Dockerfile. You answer it by opening a shell and looking.

Two mitigating facts about App Service. Port 2222 is never published to the internet; the daemon is reached only through the Kudu site, which already requires Azure authorisation. And the documentation, revised in October 2026, says the browser terminal and the preview az webapp exec command "don't require you to install an SSH server, embed an SSH password, expose an SSH port, or start an SSH daemon in your custom image". The right fix for an App Service image is therefore to delete the daemon rather than patch it. More on that below.

## The audit, in the order that finds the most

### 1. Let the registry tell you which images carry musl and sshd

Alpine publishes its security database per branch, and every mainstream scanner reads it, so CVE-2026-123158 shows up against the openssh package as soon as your scanner refreshes its feed. Microsoft Defender for Containers scans Azure Container Registry images and running containers, and its support matrix lists Alpine 3.12 through 3.22 as supported operating systems. That list stops short of 3.23 and 3.24, both of which received fixes yesterday. If your images are on either, run Trivy against the registry as well and confirm the finding appears before you trust the dashboard's silence.

```
# Every repository in the registry, scanned for the one CVE
az acr login --name <registry>
for repo in $(az acr repository list --name <registry> -o tsv); do
  for tag in $(az acr repository show-tags --name <registry> --repository $repo -o tsv); do
    trivy image --quiet --severity HIGH,CRITICAL \
      --pkg-types os --format json <registry>.azurecr.io/$repo:$tag \
      | jq -r --arg img "$repo:$tag" \
        '.Results[]?.Vulnerabilities[]? | select(.VulnerabilityID=="CVE-2026-123158") | "\($img) \(.PkgName) \(.InstalledVersion) -> \(.FixedVersion)"'
  done
done
```

A hit means the openssh package is present and old. It does not yet mean the image runs sshd or has a "*" account. The scanner narrows the list; the next step decides.

### 2. Check the four conditions where the daemon runs

For each image or VM the scanner flagged, open a shell and test the conditions directly. On App Service that is az webapp exec with a sh shell, since Alpine images rarely carry bash. On AKS it is kubectl exec. On a VM it is your normal path.

```
# 1. musl?  (prints "musl libc" on Alpine)
ldd --version 2>&1 | head -1

# 2. Is sshd running, and with what effective settings?
pgrep -a sshd
sshd -T 2>/dev/null | grep -iE '^(passwordauthentication|usepam|permitrootlogin)'

# 3. Accounts with the vulnerable marker, and accounts with a real hash
awk -F: '$2=="*" {print "STAR  " $1}  $2 ~ /^\$(5|6)\$/ {print "HASH  " $1}' /etc/shadow

# 4. Installed vs fixed openssh version for this branch
cat /etc/alpine-release
apk version openssh 2>/dev/null || apk info openssh | head -1
```

If step 3 prints at least one STAR line and at least one HASH line, and step 2 shows password authentication on without PAM, the host is exploitable as it stands. Alpine's fixed versions are 10.3_p1-r2 on 3.24, 10.2_p1-r1 on 3.23, 10.0_p1-r11 on 3.22 and 9.9_p2-r1 on 3.21, with backports down to 3.16. Anything older than the figure for your branch is vulnerable.

### 3. Fix it in the image, not in the running container

On a VM the fix is two commands, and Alpine is explicit that the second is not optional: upgrading the package does not restart the daemon.

```
apk upgrade -U -a
rc-service sshd restart

# Belt and braces: refuse passwords entirely, keys only
mkdir -p /etc/ssh/sshd_config.d
printf 'PasswordAuthentication no\nKbdInteractiveAuthentication no\n' \
  > /etc/ssh/sshd_config.d/00-no-passwords.conf
sshd -t && rc-service sshd restart
```

For containers, anything you run inside a live container vanishes at the next restart. Rebuild from a patched base instead. Alpine released 3.21.10, 3.22.8, 3.23.8 and 3.24.4 alongside the advisory, so the official alpine and node:alpine tags pick the fix up as their maintainers republish. If your pipeline pins a digest, bump it. If it floats on a tag, a rebuild picks the fix up. Azure Container Registry Tasks can automate this: a task with a base image update trigger rebuilds your application image when the base image changes in Docker Hub or in your own registry, which is precisely the mechanism this kind of week is for.

```
az acr task create \
  --registry <registry> --name rebuild-on-base \
  --image myapp:{{.Run.ID}} --image myapp:latest \
  --context https://github.com/<org>/<repo>.git#main \
  --file Dockerfile \
  --git-access-token <pat> \
  --base-image-trigger-enabled true \
  --base-image-trigger-type All
```

### 4. On App Service, remove the daemon instead

If you built from the Alpine SSH template, the patched version of that image is still an image with a root password of Docker! and password login enabled. The better outcome is an image with no sshd at all. Delete the apk add openssh line, the chpasswd line, the sshd_config copy and the port 2222 exposure, and start sshd nowhere. Then verify you can still reach a shell the supported way:

```
# Interactive shell with no SSH server in the image
az webapp exec --resource-group rg-web-prod --name app-prod-swc --shell /bin/sh

# Apps that nobody should shell into at all
az webapp config set --resource-group rg-web-prod --name app-prod-swc \
  --generic-configurations '{"sshEnabled": false}'
```

Microsoft ships two built-in policies, "App Service apps should disable SSH" and the slot equivalent, that default to Audit and can be set to Deny. Assign them at the management group and read the non-compliant list as a backlog. One caveat from the same page: these controls govern access through Kudu, and they are no substitute for RBAC and network restrictions on the SCM endpoint itself. The [PHP2Shell audit](/en/blog/php2shell-on-azure-your-app-service-and-waf-audit) we published last month covers that side.

### 5. Look back before you look forward

The bug is ten years old and the exploit leaves a clean log line. A successful bypass records "Accepted password for" followed by an account that, by design, has no password. On Alpine that lands in /var/log/messages by default. If the VM ships syslog to Log Analytics through the Azure Monitor Agent, the hunt is one query.

```
// Password logins to accounts that should only ever accept keys
Syslog
| where TimeGenerated > ago(90d)
| where ProcessName == "sshd"
| where SyslogMessage has "Accepted password for"
| extend account = extract(@"Accepted password for (\S+)", 1, SyslogMessage),
         source  = extract(@"from (\S+) port", 1, SyslogMessage)
| where account in ("root", "alpine") or account startswith "svc"
| summarize logins = count(), first = min(TimeGenerated), last = max(TimeGenerated),
            sources = make_set(source, 20) by Computer, account
| order by logins desc
```

Tune the account list to the names you know carry "*" from step 2. Treat any row as an incident rather than a finding, because a password login to a passwordless account cannot have been legitimate. If the VM never shipped syslog anywhere, grep the local file before you rebuild the host, and keep a copy.

## The Swedish and EU angle

**Cybersäkerhetslagen: the clock starts on evidence, not on the advisory.** Sweden's cybersecurity law, SFS 2025:1506, has been in force since 15 January 2026 and gives in-scope organisations 24 hours from awareness of a significant incident to file an early warning. A published CVE in your estate is a vulnerability. A row in the query above is an incident. Run step 5 before you decide you have nothing to report, and record the fact that you ran it, because the risk management duties in the same law expect you to show the reasoning either way. The same single patching record we recommended for [Brotli2Shell](/en/blog/brotli2shell-hits-nodejs-and-dotnet-your-azure-patch-plan) serves here: what was exposed, when it was fixed, and which exceptions remain and why.

**Cyber Resilience Act: if you ship the appliance, you own the report.** The CRA's 24-hour reporting duty for actively exploited vulnerabilities applies to manufacturers of products with digital elements since 11 September 2026. Alpine is the base of a great many commercial appliances and edge images precisely because it is small, and a product that embeds Alpine with an SSH management interface embeds this bug. As of today no active exploitation has been reported, so the duty is not yet triggered. Public proof of concept plus a scan-friendly signature tends to change that quickly. Swedish product teams on Alpine should have the ENISA single reporting platform workflow ready rather than theoretical.

**Upphandling: the question for your vendors.** Alpine is not an endorsed Azure distribution, so Microsoft's commitment that Marketplace publishers refresh images at least quarterly does not cover it. Every Alpine host in your estate is either yours or a supplier's. For the supplier's, one line in this week's email is enough: "Does your product run OpenSSH on musl without PAM, and if so, on which date did you ship 10.6p2 or the equivalent Alpine package?" A version number in the reply means the work is done. "We are assessing" tells you where they are on the curve.

**GDPR Article 32 does not care that the bug is old.** Appropriate technical measures is a standard assessed at the time of processing. An Alpine jump host with password authentication left on, sitting in front of personal data, was a weak control before Thursday. The advisory simply made it a documented one.

## This week's checklist

- **Today.** Scan every registry for CVE-2026-123158. Separately list every VM in Azure Resource Graph whose image publisher is not one of the endorsed ones; Alpine only ever arrives as a custom image.
- **Today.** For any Alpine VM with port 22 open to anything, run the four-condition check. Exploitable hosts get the two-command fix and the no-passwords drop-in before you leave the office.
- **Monday.** Rebuild every flagged container image from the patched base. Pin the new digest. Add a base image update trigger in ACR Tasks so the next advisory rebuilds itself.
- **Tuesday.** App Service images built from the SSH template lose the daemon. Verify az webapp exec works, then assign the disable-SSH policies in Audit mode and read the list.
- **Wednesday.** Run the KQL hunt over 90 days. Any password login to a "*" account opens an incident and starts the cybersäkerhetslagen clock.
- **Thursday.** Send the one-line question to every supplier whose appliance might run on Alpine. Record the answers next to the patching log.
- **Next sprint.** Make PasswordAuthentication no the baseline in every image and VM template, regardless of libc. The bug needed a password prompt to exist. The next one probably will too.

The fix took OpenSSH one day from report to release. The exposure took a decade to build, one unremarkable Dockerfile at a time, and most of it is in images that passed every scan because the scanner did not know what to look for until yesterday. Now it does.

## Sources

- [OpenSSH 10.6p2 release notes (5 November 2026)](https://www.openssh.org/txt/release-10.6p2)
- [Alpine Linux: Security advisory, openssh-server password authentication bypass (5 November 2026)](https://alpinelinux.org/posts/2026-11-05-openssh-password-authentication-bypass/)
- [Alpine Linux: 3.21.10, 3.22.8, 3.23.8 and 3.24.4 released (5 November 2026)](https://alpinelinux.org/posts/alpine-3.21.10-3.22.8-3.23.8-3.24.4-released/)
- [Alpine Linux: Cloud images (Azure VHD, default user alpine)](https://alpinelinux.org/cloud/)
- [Ethiack: CVE-2026-123158 (sshole), OpenSSH password bypass on Alpine Linux](https://ethiack.com/info-hub/research/sshole-cve-2026-123158)
- [OX Security: OpenSSH password authentication bypass on Alpine Linux](https://www.ox.security/blog/openssh-authentication-bypass-cve-2026-123158/)
- [Microsoft Learn: Open a container shell (SSH) and run commands in App Service](https://learn.microsoft.com/en-us/azure/app-service/configure-linux-open-ssh-session)
- [GitHub azureossd: docker-container-ssh-examples, alpine-node](https://github.com/azureossd/docker-container-ssh-examples/tree/main/alpine-node)
- [Microsoft Learn: Containers support matrix in Defender for Cloud](https://learn.microsoft.com/en-us/azure/defender-for-cloud/support-matrix-defender-for-containers)
- [Microsoft Learn: Automate container builds with ACR Tasks](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-tasks-overview)
- [Microsoft Learn: Linux distributions endorsed on Azure](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/endorsed-distros)
- [GitHub microsoft/azurelinux: glibc spec](https://github.com/microsoft/azurelinux/tree/3.0/SPECS/glibc)
- [Riksdagen: Cybersäkerhetslag (2025:1506)](https://www.riksdagen.se/sv/dokument-och-lagar/dokument/svensk-forfattningssamling/cybersakerhetslag-20251506_sfs-2025-1506/)
- [EUR-Lex: Regulation (EU) 2024/2847, the Cyber Resilience Act](https://eur-lex.europa.eu/eli/reg/2024/2847/oj)

---

Technspire AB builds AI agents, Azure OpenAI solutions, and production web platforms for Swedish and EU enterprises. Book a call: https://calendly.com/technspire · hello@technspire.com · More articles: https://technspire.com/en/blog · Site overview for agents: https://technspire.com/llms.txt
