ssh-keysign-pwn: how an unprivileged user steals host keys — and how to stop it
The server is locked down. Nginx behind Cloudflare, fail2ban blocking bots, root login over SSH disabled. No public access to /etc/shadow — it’s 640, root:shadow. Private SSH host keys sit in /etc/ssh/ with permissions 600. Everything is closed.
Then someone gets an unprivileged shell — through a vulnerable plugin, a weak password, whatever — and within seconds reads /etc/shadow and all private SSH host keys. No sudo and no root. Just runs a script.
Welcome to CVE-2026-46333, also known as ssh-keysign-pwn.
WHAT HAPPENED
On May 14, 2026, Qualys reported the vulnerability to the Linux kernel security team, and the patch was committed the same day via commit 31e62c2ebbfd. An independent PoC derived from the public commit appeared the same day. Qualys published the full advisory on May 20.
The vulnerability doesn’t affect OpenSSH or any specific configuration — it’s in the Linux kernel logic itself.
HOW IT WORKS
Imagine a security guard who has already clocked out and handed in his badge — but still has the keys to the safe in his pocket. Right at that moment, someone walks up and asks for the keys. Technically the guard is no longer on duty, the check fails.
That’s roughly how CVE-2026-46333 works.
The Linux kernel uses ptrace — an interface that allows one process to observe another or access its resources. When a privileged process terminates, the kernel frees its memory (mm) — the guard handed in his badge. But the file descriptors to open files — /etc/shadow, /etc/ssh/ssh_host_*_key — are still alive. Keys still in the pocket.
In that brief window, the function __ptrace_may_access() skips part of its check: the task no longer has a memory context, but the descriptors to sensitive files still exist. The attacker catches this moment via the pidfd_getfd() syscall — which copies a file descriptor from another process into your own — and gets direct access to the file.
Two attack vectors in the public PoC:
ssh-keysign is an OpenSSH setuid helper that runs as root when the SSH client needs to sign data with the host’s private key. It ships with openssh-client and is present on most Linux systems. It opens the host’s private keys (/etc/ssh/ssh_host_*_key) and holds them open during its runtime. The attacker catches the moment ssh-keysign terminates and steals the descriptor — private keys read.
chage is a password expiry management utility. When you run chage -l , the system opens /etc/shadow to read the user’s password data. Same race condition — and the attacker has the password hashes.
This isn’t a buffer overflow or a complex exploit chain. Just a race condition in a few lines of code.
WHY THIS IS DANGEROUS
Qualys developed four working exploits against the same vulnerability:
chage — reads /etc/shadow with password hashes.
ssh-keysign — reads SSH host private keys.
pkexec — executes arbitrary commands as root. Requires an active console session, but the attacker can be logged in remotely via SSH.
accounts-daemon — also executes commands as root, without a console session. Tested on default installs of Debian 13 and Fedora.
So “just reads files” is only the public PoC. Full root via pkexec and accounts-daemon is technically achievable on most distributions with a default installation.
The SSH host private key is how your server identifies itself to clients during an SSH connection. Whoever holds that key can impersonate your server. Silent MITM against clients who have already added the key to known_hosts. Until the keys are rotated, the attacker can intercept SSH sessions undetected.
Hashes from /etc/shadow can be exfiltrated to a separate machine and cracked offline with no noise in your infrastructure. No traces in your server logs. If one password is reused across multiple places — the compromise spreads horizontally.
One server with unprivileged access becomes the entry point to the entire infrastructure.
WHAT TO DO RIGHT NOW
Step 1. Block the exploit immediately — no reboot required
While the kernel is not yet updated, two ways to close the vector right now.
Option A — sysctl. Restrict ptrace at the kernel level. Value 2 is the sufficient mitigation recommended by Ubuntu — it disables ptrace for users without CAP_SYS_PTRACE:
echo kernel.yama.ptrace_scope=2 | sudo tee /etc/sysctl.d/99-CVE-2026-46333.conf
sudo sysctl -p /etc/sysctl.d/99-CVE-2026-46333.conf
Note: value 3 disables ptrace entirely for everyone including root and cannot be reverted without a reboot — do not use it.
Side effect: unprivileged users will no longer be able to use gdb, strace on other processes. On production servers this is acceptable.
Option B — remove the SUID bit from both attack vectors:
# Debian/Ubuntu
chmod u-s /usr/lib/openssh/ssh-keysign
chmod u-s /usr/bin/chage
# RHEL/AlmaLinux/Rocky
chmod u-s /usr/libexec/openssh/ssh-keysign
chmod u-s /usr/bin/chage
This neutralizes exactly the two binaries used by the public PoC. After installing the kernel patch, restore the SUID bit.
Step 2. Update the kernel
# Debian/Ubuntu
apt update && apt upgrade -y
# RHEL/AlmaLinux/Rocky
dnf clean metadata && dnf upgrade -y
A reboot is mandatory after updating — the kernel patch only takes effect after restart.
Kernel versions with the patch:
Ubuntu 24.04 LTS — patch in version 6.8.0-117 (USN-8278-1, published May 19, 2026):
uname -r
# 6.8.0-111-generic → vulnerable, update now
# 6.8.0-117-generic → patch applied
Ubuntu 22.04 LTS — lowlatency/HWE kernel series 6.8.x: 6.8.0-117.117.1~22.04.1 (USN-8278-1). Standard 5.15.x kernel — track: https://ubuntu.com/security/CVE-2026-46333
AlmaLinux 10 — starting from kernel-6.12.0-227.el10 (in production repository since May 16).
After rebooting with the patched kernel, remove the mitigations:
sudo rm /etc/sysctl.d/99-CVE-2026-46333.conf
sudo sysctl kernel.yama.ptrace_scope=1
# Debian/Ubuntu
chmod u+s /usr/lib/openssh/ssh-keysign
chmod u+s /usr/bin/chage
# RHEL/AlmaLinux/Rocky
chmod u+s /usr/libexec/openssh/ssh-keysign
chmod u+s /usr/bin/chage
Revert ptrace_scope to 1 (Ubuntu default), not 0.
Step 3. Rotate SSH host keys
Even after patching — if an attacker already grabbed the keys before the update, the old keys are compromised. Regenerate them on critical servers:
rm /etc/ssh/ssh_host_*
ssh-keygen -A
systemctl restart ssh # Debian/Ubuntu
systemctl restart sshd # RHEL/AlmaLinux/Rocky
Warn users: on the next SSH connection they’ll see a warning about a changed host key — that’s expected, they need to accept the new fingerprint.
Step 4. Audit local accounts and change passwords
Check who actually has shell access on the machine:
awk -F: '$7 !~ /nologin|false/ {print $1, $7}' /etc/passwd
Lock anything unnecessary:
usermod -s /usr/sbin/nologin username
If unprivileged users with shell access existed on the machine — treat their password hashes as compromised and reset:
passwd username
Step 5. Check NoNewPrivileges on critical services
NoNewPrivileges is a systemd directive that prevents a process from gaining privileges beyond those it was started with. Even if an attacker gets a shell as www-data and tries to exploit a similar vector — privilege escalation will be blocked at the kernel level.
This isn’t a direct fix for CVE-2026-46333, but it reduces the attack surface for similar race conditions:
systemctl show nginx | grep NoNewPrivileges
systemctl show postgresql | grep NoNewPrivileges
If empty or false — add it:
systemctl edit nginx
[Service]
NoNewPrivileges=yes
systemctl daemon-reload && systemctl restart nginx
Same for PostgreSQL, MySQL, and any other services running as unprivileged users.
CONCLUSION
CVE-2026-46333 is a reminder that “server locked down from outside” and “server is secure” are two different statements. Local unprivileged access that seems harmless turns into the theft of secrets that outlive the vulnerable machine.
Patch today. Rotate keys after the patch. Audit accounts — a good excuse to do it regardless.
PoC is public. The clock is ticking.
