CVE-2026-98432: check your Linux server and close public ports
By Flavio
CVE-2026-98432 is a Linux IPv4 TCP use-after-free. Check your kernel, treat disabling MPTCP as a stop gap, then patch and close every public port on the VPS.
On 1 November 2026, Pieter Levels wrote that a VPS should keep every port closed. SSH stays on Tailscale. Web traffic arrives through a Cloudflare Tunnel. He called disabling MPTCP a stop gap, and closing public ports the better move.
Levels quoted Ali Yahya’s post, which says any unpatched Linux server with one TCP port exposed, and MPTCP active, is open to complete takeover, and he describes MPTCP as the default for many Go web servers. That post quotes Tobi Lütke, who linked a write-up of CVE-2026-98432.
The CVE is real. The “MPTCP is required” part is the claim to be careful with. As of 1 November 2026, the kernel record describes a remote use-after-free in the IPv4 TCP handshake. The Go security team says one demonstrated exploit needs an MPTCP listener, and that the bug itself is not specific to MPTCP.
This post is the check, the stop gap, the patch, and the closed-port layout. The long versions of the tunnel and the tailnet already exist: Cloudflare Tunnel and a VPS with Tailscale and a tunnel. Trackers move. The status below is what the primary pages said on 1 November 2026, so read the tracker again on the day you patch.
What is CVE-2026-98432?
CVE-2026-98432 is a use-after-free in the Linux kernel’s IPv4 TCP handshake. NVD published the record on 15 October 2026 and last modified it on 31 October 2026. NVD’s status is still “Received”. The description on the page is the kernel CNA text.
During a handshake, tcp_v4_syn_recv_sock() hands ireq->ireq_opt to the child socket without copying it. Another CPU can still be reading those options, under rcu_read_lock() only, while it builds a SYN-ACK for a retransmitted SYN. The child socket can be freed in the middle of that. inet_sock_destruct() frees the options with a plain kfree(), and ip_options_build() uses the freed memory.
The kernel text says this became possible with commit 079096f103fa (“tcp/dccp: install syn_recv requests into ehash table”), which put request sockets on a path that no longer holds the listener lock. NVD marks kernels older than 4.4 unaffected, and 4.4 as affected.
A second path in the same text is request-socket migration, when net.ipv4.tcp_migrate_req=1 or a BPF_SK_REUSEPORT_SELECT_OR_MIGRATE program clones a request and the clone shares ireq_opt. tcp_v4_reqsk_destructor() also used a plain kfree().
The fix uses kfree_rcu() in inet_sock_destruct() and tcp_v4_reqsk_destructor(), so readers that already use RCU finish before the memory goes away. NVD links the stable commits, including e3f33b0a1d89. Debian’s tracker notes that commit as 7.3-rc7. A separate calendar date for every stable backport was not in the kernel.org changelog pages checked for this post. The date to cite is NVD’s publication on 15 October 2026, plus the version numbers in the next section.
The same kernel text says IPv6 is not affected by the first issue, because tcp_v6_syn_recv_sock() duplicates the options. It also says tcp_v6_reqsk_destructor() has the same migration issue with ipv6_opt, which only CALIPSO sets, and that this will be a separate patch. An IPv6-only listener is not a reason to skip the kernel update.
NVD’s CVSS data is a secondary metric from the kernel CNA, not an NVD analysis of its own:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
That is 9.8, Critical. Network, low complexity, no privileges, no user interaction, unchanged scope, high impact on confidentiality, integrity, and availability.
Ubuntu’s page shows that same 9.8 Critical vector, and also a CVSS 4.0 score of 8.3 High:
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:L/VA:H/SC:N/SI:N/SA:N
Ubuntu’s own priority label is High. Red Hat’s CVE data, fetched the same day, rates the threat Critical, with the 9.8 vector marked draft. Red Hat’s statement says an unauthenticated remote attacker can cause kernel memory corruption or an immediate crash over ordinary IPv4 TCP, and that reliable arbitrary code needs precise heap control.
The Go security note, forwarded to oss-security on 29 October 2026, describes an exploit written up by Anthropic that reaches remote code execution against a server using MPTCP listeners. The Anthropic page is linked from that note. This post uses the Go note’s description of the exploit, not a line-by-line read of the research write-up. Those three sources describe the same bug at different levels of confidence about code execution. The outcomes stay separate on purpose.
Which kernels are fixed upstream?
NVD’s affected data, from the kernel CNA, lists these as unaffected on each stable line. A version on that line is unaffected once it is at least the number in the first column, and at most the wildcard in the second.
| Unaffected from | Through |
|---|---|
| 5.10.272 | 5.10.* |
| 5.15.223 | 5.15.* |
| 6.1.190 | 6.1.* |
| 6.6.159 | 6.6.* |
| 6.12.113 | 6.12.* |
| 6.18.56 | 6.18.* |
| 7.2.10 | 7.2.* |
| 7.3 | later versions, marked original_commit_for_fix |
Anything from 4.4 up to those points is in the affected range NVD published. Distro kernels do not use these numbers. 6.8.0-147 on Ubuntu is not “6.8, so it sits between 6.6 and 6.12”. It is an Ubuntu package version. Compare it with Ubuntu’s tracker, not with the upstream table alone.
NVD also lists the fixing commit hashes. The 6.12 one is 11a03ce3c4a6, the 6.1 one is 89b5b1eb7d63, and the rest are on the NVD reference list.
Is MPTCP required?
The kernel CVE text does not mention MPTCP. The files it names are net/ipv4/af_inet.c and net/ipv4/tcp_ipv4.c.
The Go security team is explicit. The demonstrated exploit needs an MPTCP listener. The underlying bug is not specific to MPTCP. Systems with no MPTCP listeners can still be open to similar attacks. They call a per-program disable “a fragile mitigation”, and they say the best fix is a patched kernel. Discussion of whether Go itself should change the default is on issue 82717. As of that 29 October note, they were not changing Go’s default because of this bug.
So disabling MPTCP cuts off the path that write-up demonstrated. It leaves the use-after-free in the IPv4 handshake. Treat it as a stop gap while you patch, which is what Levels said, and keep going until the kernel is fixed and strangers cannot open a TCP connection to the machine.
Go listeners and the default
Go’s GODEBUG history says Go 1.24 added four values for multipathtcp:
| Value | Meaning |
|---|---|
| 0 | Disable MPTCP on dialers and listeners by default |
| 1 | Enable MPTCP on dialers and listeners by default |
| 2 | Enable MPTCP on listeners only by default |
| 3 | Enable MPTCP on dialers only by default |
For Go 1.24, the default is multipathtcp=2. Listeners use MPTCP when the operating system allows it. 0 is the pre-1.24 behavior.
That default follows the Go version in the main module. When a newer toolchain compiles a module whose go line is older, the godebug rules amend defaults back toward that older version. A repo that still says go 1.22 does not pick up the 1.24 listener default just because the compiler is newer. A repo that says go 1.24 or later does, unless a godebug line overrides it.
net.ListenConfig.SetMultipathTCP and net.Dialer.SetMultipathTCP override the GODEBUG setting. The Go security note says so, and the methods are documented on ListenConfig and Dialer.
Yahya’s “default for many Go web servers” matches a Go 1.24+ module on a kernel where MPTCP is enabled. It does not match every Go binary, and it is not what the kernel bug requires.
How do you check whether you are exposed?
Four facts matter. The running kernel. Whether MPTCP is on. Which TCP ports are listening, and on which addresses. Whether a Go program is one of the listeners that asked for MPTCP.
Run the commands on the server. The output further down is from a Linux host checked on 1 November 2026. That host is Ubuntu 24.04.5 userspace with a custom kernel. It is not a stock cloud image, and it is not your VPS.
Kernel version
uname -r
uname -v
On that host:
6.12.94+
#1 SMP PREEMPT_DYNAMIC Tue Oct 27 12:38:43 UTC 2026
6.12.94 is numerically below NVD’s fixed line of 6.12.113. The + means this build is not a kernel.org stable tarball and not a distro package. Treat that comparison as a hint until the package tracker agrees.
Then ask the package manager what it would install.
Ubuntu and Debian:
dpkg-query -W -f='${Package} ${Version}\n' 'linux-image*'
apt-cache policy linux-image-generic
The same host had no linux-image* package installed. apt-cache policy linux-image-generic showed a candidate of 6.8.0-147.147 from noble-updates and noble-security. Ubuntu’s tracker, the same day, listed the 24.04 fix as work in progress in 6.8.0-149.149. The running kernel and the archive package were two different things. Your server can look like that too, especially with a provider kernel, a cloud image, or a backports package. Read both numbers.
On RHEL and the clones that use dnf:
rpm -q kernel
uname -r
Match the package to the row on your distro’s tracker. A reboot is what makes a newly installed kernel the running one. uname -r before the reboot still shows the old one.
MPTCP
The kernel documents net.mptcp.enabled as a boolean, per network namespace. Sockets can be created when the value is 1. The documented default is 1. A distro can ship a different value, so read it.
sysctl net.mptcp.enabled
On that host:
net.mptcp.enabled = 1
ss from iproute2 can show MPTCP sockets. On this host, ss --help lists -M, --mptcp as “display only MPTCP sockets”. The man page was not installed on that host, so the help text is the source for the flag.
ss -Mlnp
Listening MPTCP sockets, numeric, with the process name. On that host the command printed only the header. Nothing on the machine was an MPTCP socket at that moment.
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
A container has its own network namespace. sysctl on the host does not answer for the container. Run it inside the container too.
Listening ports
ss -tlnp
-t is TCP, -l is listening, -n skips DNS, -p shows the process. On that host:
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 5 127.0.0.1:5901 0.0.0.0:* users:(("Xtigervnc",pid=2208,fd=9))
LISTEN 0 128 0.0.0.0:26500 0.0.0.0:*
LISTEN 0 100 0.0.0.0:26058 0.0.0.0:* users:(("python3",pid=2356,fd=3))
LISTEN 0 128 0.0.0.0:50052 0.0.0.0:*
LISTEN 0 5 0.0.0.0:2375 0.0.0.0:*
LISTEN 0 511 *:26054 *:* users:(("node",pid=806,fd=26))
LISTEN 0 511 *:26053 *:* users:(("node",pid=806,fd=25))
LISTEN 0 5 [::1]:5901 [::]:* users:(("Xtigervnc",pid=2208,fd=10))
127.0.0.1 and [::1] are loopback. 0.0.0.0 and * are every IPv4 or every address. Those are the listeners a public interface can reach, if the host firewall and the cloud firewall allow it. ss cannot see the cloud firewall. A port in this list can still be blocked upstream, and a port missing here is not open on this host.
From outside, scan the public address. That step is later, after you still have a way back in.
Is the Go binary using MPTCP?
ss -Mlnp is the direct check. If your server’s process is on that list, the socket is MPTCP.
If you have the source, look at the toolchain and the module.
go version
On the host above, that printed go version go1.22.2 linux/amd64. Go 1.22 predates the listener default. A binary this toolchain builds keeps MPTCP off unless the code calls SetMultipathTCP(true).
In a Go 1.24 or newer module, open go.mod. A go 1.24 line (or later) plus no godebug multipathtcp=0 means listeners default to MPTCP when the kernel allows it. go list -f '{{.DefaultGODEBUG}}' prints the defaults that differ from the base toolchain, which is how you see the effective multipathtcp value for that module.
How do you disable MPTCP?
This stops new MPTCP sockets. It does not patch the use-after-free. Ordinary TCP keeps working. You lose multipath connections, which most web servers were not relying on. Restart the listeners after the sysctl, then run ss -Mlnp again. Whether sockets that were already open survive the change was not tested here. Check after the restart rather than assuming.
Write a file under /etc/sysctl.d/ so a reboot keeps the setting:
echo 'net.mptcp.enabled=0' | sudo tee /etc/sysctl.d/90-disable-mptcp.conf
sudo sysctl -p /etc/sysctl.d/90-disable-mptcp.conf
sysctl net.mptcp.enabled
You want net.mptcp.enabled = 0.
The kernel doc says the value is per namespace. Set it in each container namespace you care about, or confirm the container inherited it. A Docker --sysctl net.mptcp.enabled=0 flag was not tested on that host. Check Docker’s sysctl documentation before you depend on that flag.
Go, on top of the sysctl
The sysctl is the broad switch. The Go settings matter when a later config turns MPTCP back on, or when you cannot change the host sysctl yet. The Go team still calls the program-by-program switch fragile, because one forgotten listener is enough for the demonstrated exploit.
For one process, at runtime:
GODEBUG=multipathtcp=0 ./server
For every build of the main module, in go.mod:
go 1.24
godebug multipathtcp=0
For one listener, in code. SetMultipathTCP overrides GODEBUG.
package main
import (
"context"
"fmt"
"net"
)
func main() {
lc := net.ListenConfig{}
lc.SetMultipathTCP(false)
ln, err := lc.Listen(context.Background(), "tcp", "127.0.0.1:8080")
if err != nil {
panic(err)
}
defer ln.Close()
fmt.Println("listening on", ln.Addr())
}
false here means this listener does not request MPTCP. The kernel still has to be patched.
Red Hat’s packet filter
Red Hat’s bulletin RHSB-2026-016 publishes a different stop gap. The page’s public date is 30 October 2026, 16:03, and the bulletin status is Ongoing. Red Hat says filtering can reduce exposure, can disrupt real workloads, and is not a substitute for patching. The rule drops incoming IPv4 TCP packets that contain IP options. Red Hat says it cannot fully prevent the bug. Ordinary TCP options live in the TCP header and are not matched by this rule.
They tell you to test it before production. This is their rule, saved as cve-2026-98432.nft:
table inet cve_2026_98432 {
chain input {
type filter hook input priority -310; policy accept;
ip protocol tcp ip hdrlength > 5 counter drop
}
}
sudo nft --check --file cve-2026-98432.nft
sudo nft --file cve-2026-98432.nft
sudo nft list table inet cve_2026_98432
After the kernel is fixed, remove it:
sudo nft delete table inet cve_2026_98432
That table was not loaded on the host used for the ss output.
How do you patch?
Find the fixed package on the tracker for the release you run. Install it. Reboot. Check uname -r again. If the tracker still says vulnerable, the reboot only proved you are running the newest package you could get, and that package is still the bug.
Ubuntu
Ubuntu’s CVE page was last updated 30 October 2026. Publication date on the page is 15 October 2026. That page did not list a USN. Ubuntu Livepatch has nothing to apply until a fixed kernel exists. There is no livepatch command to give for this CVE yet.
For the linux package, the page listed:
| Release | Status on the page |
|---|---|
| 26.10 | Vulnerable, work in progress in 7.3.0-10.10 |
| 26.04 LTS | Vulnerable, work in progress in 7.0.0-43.43 |
| 24.04 LTS | Vulnerable, work in progress in 6.8.0-149.149 |
| 22.04 LTS | Vulnerable, work in progress in 5.15.0-203.213 |
| 20.04, 18.04, 16.04 | Vulnerable |
| 14.04 | Not affected |
HWE, cloud, and OEM kernels have their own rows. linux-hwe-6.8 on jammy was work in progress in 6.8.0-149.149~22.04.1. Several other HWE kernels were Vulnerable with no fix version on the page. Use the tracker row for the package uname -r belongs to. The page is long because Ubuntu builds many kernels.
When a fixed package is in the archive, upgrade and reboot:
sudo apt update
apt-cache policy linux-image-generic
sudo apt upgrade
sudo reboot
linux-image-generic is the common metapackage on a desktop-style Ubuntu install. A cloud image or an HWE kernel uses a different name. If apt-cache policy does not match the kernel you are running, find the package with dpkg -l 'linux-image*' before you assume the upgrade touched it.
Debian
Debian’s security tracker, fetched 1 November 2026:
| Suite | Version on the page | Status |
|---|---|---|
| bookworm | 6.1.176-1 | vulnerable |
| bookworm security | 6.1.190-1 | fixed, DLA-4841-1 |
| bookworm linux-6.12 security | 6.12.113-1~deb12u1 | fixed, DLA-4850-1 |
| trixie | 6.12.107-1 | vulnerable |
| trixie security | 6.12.113-1 | fixed, DSA-6568-1 |
| forky | 7.2.12-1 | fixed |
| sid | 7.2.14-1 | fixed |
The tracker’s fixed-version table also lists unstable linux at 7.2.10-1. Sid had already moved on to 7.2.14-1. The note on the page points at e3f33b0a1d89 as 7.3-rc7.
On bookworm or trixie, install security updates and reboot:
sudo apt update
apt-cache policy linux-image-amd64
sudo apt upgrade
sudo reboot
uname -r
Then compare uname -r with the security-suite version, not with the base suite. A bookworm machine that is still on 6.1.176-1 has not taken DLA-4841-1.
RHEL and the family
Red Hat’s package state, from the CVE JSON on 1 November 2026:
| Product | Package | State |
|---|---|---|
| RHEL 10, 9, 8 | kernel | Affected |
| RHEL 9 and 8 | kernel-rt | Affected |
| RHEL for NVIDIA 26 | kernel | Affected |
| OpenShift 4 | ose-rhel-coreos-8 and ose-rhel-coreos-9 | Affected |
| OpenShift 5 | ose-rhel-coreos-9 | Affected |
| RHEL 7 | kernel and kernel-rt | Not affected |
| RHEL 6 | kernel | Not affected |
affected_release in that JSON was empty. No fixed build was listed. Bugzilla is 2553390. The bulletin tells you to apply kernel updates as they are released, and it does not mention kpatch. No live patch was listed there.
When Red Hat ships a fixed kernel:
sudo dnf update kernel
sudo reboot
uname -r
rpm -q kernel
Keep the previous kernel installed until the new one boots. dnf usually does that. If the new uname -r is still on the Affected side of the tracker, you rebooted into an unfixed package.
The AlmaLinux, Rocky Linux, Amazon Linux, SUSE, and Oracle trackers were not checked for this CVE. If you run one of those, use their page. A blog summary of their package versions is not a substitute for the tracker.
How do you run a VPS with no public ports?
Patching closes the bug in the kernel. A public TCP listener is how a stranger reaches that bug, and the next one. The layout Levels described is the one to end on. Visitors hit Cloudflare. cloudflared on the VPS makes an outbound connection and carries the HTTP request to a program on localhost. You SSH over Tailscale. The public address accepts nothing new.
The VPS guide builds this on Ubuntu and says what to test before you touch a production box. The steps below are the short path, with the commands that were on the vendor pages on 1 November 2026. Where to host is the decision of whether a VPS is the right machine at all. The free VPS course and Linux server security course cover the day-to-day work around it.
Keep a way in before you close port 22
Open the provider’s web console and leave it logged in. On a Hetzner, DigitalOcean, or similar panel, that console is a keyboard on the VM that does not use your SSH port. If the firewall change is wrong, you fix it there.
Do the Tailscale SSH test in the next section before you delete the public SSH rule. Closing 22 first is how people lock themselves out.
SSH over Tailscale
Install Tailscale from their Linux instructions, or review the install script and run it, which is the path in the VPS guide:
curl -fsSL https://tailscale.com/install.sh -o /tmp/install-tailscale.sh
less /tmp/install-tailscale.sh
sudo sh /tmp/install-tailscale.sh
Bring the node up. Leave --ssh off until normal SSH over the tailnet works.
sudo tailscale up --hostname=vps-web
tailscale status
tailscale ip -4
From your laptop, on the same tailnet:
ssh [email protected]
Use the address tailscale ip -4 printed. It is not the public IP.
Tailscale SSH is optional. tailscale up --ssh lets the tailnet policy decide who can get a shell, and Tailscale’s docs describe it taking over port 22 for tailnet connections. Your existing sshd and authorized_keys stay in place for everyone else until you remove public SSH. Turn it on only after a normal ssh to the tailnet address already works.
Tailscale’s firewall page says a direct connection can fail and traffic still flows through DERP relays. Direct WireGuard uses UDP with a source port that defaults to 41641, and that port can be changed. Opening 41641/udp is how you help peer-to-peer. It is not required for Tailscale to function. tailscale status shows a connection as direct or relayed. The tailnet course is the longer explanation of that network. SSH itself is unchanged. You are only changing which network can reach it.
Web traffic through Cloudflare Tunnel
Install cloudflared from Cloudflare’s apt repository, as in the Tunnel guide:
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main" | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt-get update && sudo apt-get install cloudflared
Create the tunnel locally. On 1 November 2026 that page used a single url: field for one published application, not an ingress list.
cloudflared tunnel login
cloudflared tunnel create vps-web
cloudflared tunnel list
tunnel login writes a certificate under ~/.cloudflared/. tunnel create writes a credentials JSON. Both are secrets. The create command prints a UUID.
~/.cloudflared/config.yml for one local app, with your UUID and your hostname’s origin:
url: http://localhost:8000
tunnel: <Tunnel-UUID>
credentials-file: /home/user/.cloudflared/<Tunnel-UUID>.json
Point credentials-file at the JSON tunnel create wrote. The origin stays on localhost. The public hostname never connects to port 8000 on the VPS address.
DNS, then run:
cloudflared tunnel route dns <UUID or NAME> app.example.com
cloudflared tunnel run <UUID or NAME>
route dns creates a CNAME to <UUID>.cfargotunnel.com. If the config file is not in the default directory, add --config and the path, which is what that page says.
More than one hostname still uses an ingress list, with a catch-all rule at the end. That form is the one walked through in the Tunnel guide, checked against cloudflared on 22 September 2026. Use it when url: is too small. A throwaway URL for an afternoon is a quick tunnel, and it is the wrong tool for a server you keep.
When the foreground run works, install the service. Cloudflare’s Linux service page says:
sudo cloudflared --config /home/user/.cloudflared/config.yml service install
sudo systemctl start cloudflared
sudo systemctl status cloudflared
The --config flag is there because sudo sets HOME to /root, and a config you wrote under /home/user would otherwise be invisible. Put your real home directory in the path.
The tunnel needs outbound access to Cloudflare. The VPS guide cites outbound port 7844 for that. You are not opening an inbound port for it.
Firewall on the host
The provider firewall is the public boundary. A host firewall is the second layer, for the day the cloud rule is edited by mistake.
With ufw, from the provider console, after tailnet SSH works:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow in on tailscale0 to any port 22 proto tcp
sudo ufw enable
sudo ufw status verbose
If the server uses IPv6, /etc/default/ufw should contain IPV6=yes. Do not run ufw reset on a box that already has rules you need.
Tailscale’s default netfilter mode accepts tailnet traffic before ordinary ufw input rules. A ufw allow on port 22 is not your tailnet ACL. The VPS guide covers that, with a link to Tailscale’s netfilter modes.
An nftables starting point, for a host that is not using ufw, is a drop policy on input, with established traffic, loopback, and SSH from the tailnet interface:
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
iifname "lo" accept
iifname "tailscale0" tcp dport 22 accept
}
}
Apply it from the provider console, with a working tailnet SSH session already open in another window. That table was not loaded on the check host. It is a starting shape, and it is a different table from Red Hat’s CVE mitigation above. Output stays at the default accept, so the tunnel and Tailscale can still dial out. Add what you forgot before you disconnect the console.
Docker publishes around ufw
Docker’s packet filtering documentation describes published ports being handled in the nat table before the host firewall’s input chain. ufw deny does not stop -p 3000:3000.
Publish to loopback when cloudflared runs on the host:
services:
app:
ports:
- "127.0.0.1:3000:3000"
If cloudflared is in another container, 127.0.0.1 is that container, not the app. Put them on one Docker network and point the tunnel at the service name. The VPS guide has that case too.
The cloud firewall
After ssh [email protected] works from your laptop, and the site answers on the public hostname, delete inbound 22, 80, and 443 on the provider firewall. Leave outbound open. Do this in the provider panel, not only in ufw. A host firewall does not help a scanner that reaches a port Docker published, and a provider firewall in front of the VM does.
Then try the public IP from a network that is not your tailnet. The next section is that test.
How do you verify nothing is open?
From another machine you control, one that is not joined to the tailnet:
nmap -Pn -sT -p- 203.0.113.10
203.0.113.10 is the documentation address. Put the VPS public IP there. -sT is a TCP connect scan, so it does not need raw sockets. -p- is every TCP port. -Pn skips the ping. That ping is often dropped, and a skipped host looks like a clean result when it is not.
You want no open TCP ports. filtered means a firewall dropped or rejected the probe. open means something accepted it. An open line is a listener you still have to move or close.
UDP is a weaker scan. It is slow, it needs root for a useful result, and closed versus filtered is easy to misread.
sudo nmap -Pn -sU -p 41641 203.0.113.10
If that port is open, you left Tailscale’s direct-connect port public. Tailscale still works without it. Close it if the goal is no public ports, and accept a relay when tailscale status says so.
Shodan and similar scanners are fine against an address you own. They are a second opinion, not the check you wait on. They can lag by days.
ss -tlnp on the server remains the list of what the kernel is willing to accept. Run both. The scan sees the path from the internet. ss sees the binds.
FAQ
Is my VPS vulnerable to CVE-2026-98432?
It is in range when the running kernel is at least 4.4 and older than the fixed version on its upstream line, or when your distro tracker still marks that package vulnerable. One public IPv4 TCP port is enough for a remote attacker to reach the handshake bug. MPTCP makes a demonstrated exploit path easier, and the kernel text does not require it. Read uname -r, then Ubuntu, Debian, or Red Hat for the package you boot.
How do I disable MPTCP on Linux?
Set net.mptcp.enabled=0 and store that line in /etc/sysctl.d/90-disable-mptcp.conf, then load it with sysctl -p. Restart listeners and confirm ss -Mlnp is empty. The kernel default documented for that sysctl is 1, per network namespace. On Go 1.24 or later, GODEBUG=multipathtcp=0 or godebug multipathtcp=0 in go.mod turns the listener default off, and SetMultipathTCP(false) does it for one socket.
Does disabling MPTCP fix CVE-2026-98432?
It blocks the MPTCP listener path the Go security team described. The use-after-free is in IPv4 TCP option handling during the handshake, and they say hosts with no MPTCP listeners can still be vulnerable to similar attacks. Red Hat’s IP-options filter has the same limit. Their bulletin says it cannot fully prevent the issue. Install the fixed kernel and reboot.
Which Ubuntu and RHEL kernels are fixed?
As of the pages fetched on 1 November 2026, Ubuntu had not published a fixed linux package. The tracker showed work in progress, including 6.8.0-149.149 for 24.04 and 5.15.0-203.213 for 22.04, and no USN. Red Hat marked the RHEL 8, 9, and 10 kernels Affected, with no fixed release in the CVE JSON. Debian had shipped fixes in bookworm security (6.1.190-1) and trixie security (6.12.113-1). Check again on the day you patch. Those rows will change.
How do I close every public port and still serve a site?
Serve the site through Cloudflare Tunnel, so cloudflared dials out and the app listens on localhost. SSH over Tailscale, and test ssh to the tailnet address before you remove public port 22. Default-deny incoming on ufw, allow SSH only on tailscale0, and delete inbound rules on the cloud firewall too. The longer VPS guide is the reboot and recovery checklist. Confirm with ss -tlnp and an nmap scan from outside the tailnet.
Want me to talk about your product? You can sponsor this site.