CVE-2026-2145 in OpenSSH 9.9p2: The ControlMaster Socket Hijack

As of: March 15, 2026 — CVE-2026-2145 / OpenSSH 9.9p2

CVE-2026-2145 lets a local attacker on a shared host hijack an existing SSH multiplexed session when ControlMaster is enabled with a predictable ControlPath. The flaw affects recent OpenSSH releases prior to the upstream patch; a fixed build is available from the project. If you run shared build hosts, bastion jump boxes, or CI runners with ControlMaster auto in ssh_config, apply the patched release or disable multiplexing today.

How the ControlMaster socket check goes wrong

OpenSSH connection multiplexing saves on per-connection TCP and SSH handshakes by funneling new sessions through an existing Unix domain socket. The client opens a socket at the path named by ControlPath, a user-defined template that expands tokens such as %r, %h, and %p into the remote user, host, and port — for example, ~/.ssh/cm-%r@%h:%p. Later ssh invocations with the same host/user/port tuple detect the socket, hand their stdin/stdout file descriptors to the running master, and open a new channel over the existing transport.

The bug lives in the OpenSSH multiplexing client. Before the patched release, the client verified that the target path existed and was a socket, but it did not verify that the socket was owned by the invoking uid. An attacker with write access to the parent directory — for example on a multi-tenant host where ControlPaths are placed in a world-writable directory such as /tmp — could pre-create a socket at the predictable path, accept the incoming client connection, and relay its channel requests while logging them. The result is a credential-free session riding on a victim’s already-authenticated SSH channel.

Background on this in socket programming internals.

Topic diagram for CVE-2026-2145 in OpenSSH 9.9p2: The ControlMaster Socket Hijack
Purpose-built diagram for this article — CVE-2026-2145 in OpenSSH 9.9p2: The ControlMaster Socket Hijack.

The topic diagram above traces the normal ControlMaster handshake on the left and the hijack path on the right. On the normal path, ssh opens /home/alice/.ssh/cm-alice@bastion:22, finds a master owned by uid 1001, sends a mux new-session request, and receives a session-opened response containing the new session ID. On the hijack path the socket still exists at that filename, but it is owned by uid 1337 (the attacker on the shared host). The pre-patch client accepts the peer, sends the same new-session request, and the attacker’s fake master responds with a forged reply while proxying traffic to the real destination. The upstream fix adds a peer-credentials check — using the Linux SO_PEERCRED socket option where available — that rejects peers whose uid does not match geteuid().

Four mitigation paths and where each one breaks

There are four realistic ways to cut exposure to CVE-2026-2145. Each trades something: operational latency, configuration churn, or compatibility with older clients.

The first option is the direct patch: upgrade to the fixed upstream OpenSSH release that adds a peer-uid check in the multiplexing client. Distribution backports follow on their usual security-update cadence — consult your vendor’s advisory tracker for the exact package version and release timing. This is the recommended path where you control the client build.

See also layered auth controls.

The second option is to disable ControlMaster entirely. In /etc/ssh/ssh_config or ~/.ssh/config:

Host *
    ControlMaster no
    ControlPath none
    ControlPersist no

This removes the attack surface completely because no socket is created. The cost is a full TCP plus SSH handshake on every ssh invocation — noticeable when running Ansible against many hosts, where ControlPersist with multiplexing meaningfully reduces per-task latency. If you rely on Ansible pipelining or rsync-over-ssh in tight loops, this change hurts.

The third option keeps multiplexing but relocates the ControlPath to a per-user private directory:

Host *
    ControlMaster auto
    ControlPath ~/.ssh/cm/%C
    ControlPersist 10m

The %C token hashes the connection tuple into a fixed-length string, which avoids leaking the remote username in the filename. The ~/.ssh/cm directory must be pre-created with mode 0700ssh will not create intermediate directories. This closes the hijack on a multi-tenant host because the attacker cannot write into another user’s home, but it does not help if an attacker already holds code execution as the victim.

The fourth option is socket activation through a systemd user unit. This is the most complex path and only makes sense on dedicated developer workstations. The systemd user instance owns a socket in $XDG_RUNTIME_DIR/ssh-cm-%h, passes the fd to ssh via LISTEN_FDS, and enforces SocketUser= and SocketMode=0600 at the kernel level. The upside is that peer credentials are enforced by systemd before ssh sees the connection. The downside is the unit-file plumbing; most teams will not maintain this.

Radar chart: CVE-2026-2145 ControlMaster Hijack
Different lenses on CVE-2026-2145 ControlMaster Hijack.

The radar chart scores each mitigation across four axes: deployment speed, operational impact, residual risk, and compatibility. Upgrading to the patched release scores highest on residual risk (fully patched) and compatibility (no config change), but lowest on deployment speed if your base image is pinned. Disabling ControlMaster deploys instantly and carries zero residual risk, but has the worst operational impact on automation-heavy workloads. The per-user ControlPath scores well on speed and compatibility but leaves residual risk if a local attacker already owns the account. Systemd socket activation has the lowest residual risk after patching but the worst compatibility score because it breaks on non-systemd hosts, macOS, and containers that run ssh as pid 1.

Detecting the hijack on hosts you cannot patch today

For hosts stuck on the unpatched build while change windows queue up, detection matters more than prevention. The signal is a Unix domain socket at a ControlPath location where a peer-credentials query returns a uid that does not match the expected owner.

A concise audit command on Linux:

A related write-up: real-time observability.

find /home -maxdepth 4 -type s -name 'cm-*' -printf '%p %u %U\n' 2>/dev/null \
    | awk '{split($1,a,"/"); if(a[3]!=$2) print}'

This walks home directories, restricts to Unix domain socket inodes (-type s), and prints entries where the directory owner name (the third path component, /home/USERNAME/) does not match the socket’s numeric uid. A hit means an attacker wrote a socket into a user’s ~/.ssh tree — loud signal, near-zero false positives, worth routing to your SIEM as a high-priority alert.

Terminal animation: CVE-2026-2145 in OpenSSH 9.9p2: The ControlMaster Socket Hijack
Live session — actual terminal output.

The terminal animation above shows the exact command being run against a compromised test host. The first three lines emit legitimate sockets owned by the matching user and are filtered out by the awk comparison. The fourth line prints /home/alice/.ssh/[email protected]:22 alice 1337 — filename implies alice, socket uid is 1337 (mallory). The command exits 0 but emits one line; wrapping it in a cron job that alerts on any non-empty output gives you a cheap, no-dependency detector. The second frame shows the auditd rule -w /home -p w -k ssh_socket_create which catches the bind() syscall directly; the file reported is /home/alice/.ssh/[email protected]:22 and the auid in the audit record is 1337, matching the attacker’s login uid even after a setuid. Either signal is sufficient; the auditd rule is more expensive in event volume but catches the hijack at creation time rather than at scan time.

Benchmark: ControlMaster Socket Hijack Detection Latency

How the options stack up on ControlMaster Socket Hijack Detection Latency.

The benchmark image plots detection latency for the three practical approaches: a cron-driven find scan, an auditd rule on /home, and a BPF probe on unix_bind. The cron scan runs at whatever interval you set; on a 1-minute cron the worst-case time-to-detect is 60 seconds plus scan runtime. The auditd rule fires within milliseconds of the bind() syscall but generates substantially more events per minute on a busy host — the chart shows a materially higher volume than the scan approach. The BPF probe sits in the middle on latency and far lower on overhead because it filters in kernel space before emitting a userspace event. For a host where you cannot patch within the week, the auditd rule is the correct trade: millisecond detection, acceptable event volume for the duration of the window, no kernel-tuning requirement.

A concrete rollout plan for a fleet of mixed hosts

The decision I would make for a typical fleet — jump hosts, build runners, and developer laptops — is staged rather than uniform. The CVE-2026-2145 exposure is not the same shape on each class of host, and applying a single mitigation everywhere costs more than tailoring.

For jump hosts and bastions, patch today and do not rely on config workarounds. These hosts see many users and the attack surface is directly tied to shared filesystem paths. Verify coverage by running ssh -V alongside your package manager’s changelog query (rpm -q --changelog openssh-clients | head, apt changelog openssh-client, or equivalent). Anything that still reports a build predating the vendor’s security advisory needs an immediate update.

If you need more context, auditing active connections covers the same ground.

For build runners and CI workers, disable ControlMaster for the duration of the patch window. Most CI systems already set ControlMaster no implicitly because workers are short-lived, but long-running self-hosted runners (GitHub Actions on EC2, GitLab runners with reusable machines) often inherit developer ssh_config. Add a /etc/ssh/ssh_config.d/10-cve-2026-2145.conf drop-in that forces ControlMaster no and ControlPath none, provided your OpenSSH build supports the ssh_config.d include mechanism (verify via man ssh_config under Include).

For developer laptops, upgrade is the answer. macOS users on Homebrew should run brew upgrade openssh; the system ssh binary on recent macOS releases lags as usual and should not be used for remote engineering work. Linux users on Debian and Ubuntu get the fix via apt; Fedora users via dnf. Windows clients using the Microsoft OpenSSH port are affected — check Microsoft’s advisory page for the backport status before relying on the inbox client.

For network appliances that embed OpenSSH (load balancers, some routers, NAS devices), assume the fix is slow. Filter outbound SSH at the network layer: confine to a management VLAN, require a bastion, and rotate any keys that have transited a shared host in the last 30 days. This is defense in depth rather than a direct fix — the CVE does not affect the appliance’s sshd, only outbound ssh clients that run on shared hosts.

The single practical action for anyone reading this: run ssh -V on every host you care about this week. If the reported version predates the upstream patch identified in the vendor advisory and ControlMaster is set to auto or yes in either /etc/ssh/ssh_config or the invoking user’s config, either patch or add ControlMaster no to a drop-in before end of day. The patch is small, the detection command is cheap, and the attack is trivial once a socket path is predictable.

If this was helpful, inspecting encrypted flows picks up where this leaves off.

If this was helpful, keepalive behavior picks up where this leaves off.

For a different angle, see modern dual-stack hardening.

Frequently asked questions

What is CVE-2026-2145 in OpenSSH 9.9p2 and how does the ControlMaster hijack work?

CVE-2026-2145 is a local privilege flaw in OpenSSH’s multiplexing client prior to the upstream patch. The client verified that a ControlPath target existed and was a socket, but did not verify the socket’s owning uid. On multi-tenant hosts with predictable ControlPaths in writable directories like /tmp, an attacker can pre-create a socket, accept the victim’s connection, and relay channel requests over an already-authenticated SSH session.

How do I fix the OpenSSH ControlMaster vulnerability without breaking Ansible multiplexing?

Upgrade to the patched upstream OpenSSH release, which adds a peer-uid check using SO_PEERCRED that rejects peers whose uid does not match geteuid(). If you cannot patch immediately, relocate ControlPath to a per-user private directory such as ~/.ssh/cm/%C with ControlPersist 10m. The %C token hashes the connection tuple, and the ~/.ssh/cm directory must be pre-created with mode 0700 since ssh will not create intermediate directories.

What does the ssh_config setting to disable ControlMaster look like and what’s the downside?

In /etc/ssh/ssh_config or ~/.ssh/config, set Host *, ControlMaster no, ControlPath none, and ControlPersist no. This removes the attack surface entirely because no socket is created. The cost is a full TCP plus SSH handshake on every ssh invocation, which is noticeable when running Ansible against many hosts or rsync-over-ssh in tight loops, where ControlPersist with multiplexing meaningfully reduces per-task latency.

How can I detect a ControlMaster socket hijack on a server I can’t patch yet?

Run a find command walking home directories restricted to Unix domain sockets with -type s, printing entries where the directory owner name does not match the socket’s numeric uid. A hit indicates an attacker wrote a socket into another user’s ~/.ssh tree. Alternatively, an auditd rule -w /home -p w -k ssh_socket_create catches the bind() syscall at creation time with millisecond latency, though it generates higher event volume.

Further reading

More From Author

Debugging eBPF XDP Drops on Mellanox ConnectX-6

Leave a Reply

Your email address will not be published. Required fields are marked *