From Ubuntu 24.04 to 26.04 and Reading Old Servers: sudo-rs, Rust Coreutils, nftables, SysV to systemd
On Ubuntu 26.04 the command names stay the same, but sudo-rs and the Rust coreutils can change behavior: in the uutils 0.10.0 I tested, ln -sfn is no longer atomic and sudo's error message differs, so run your scripts on the new release before upgrading. We cover OpenSSH 10's post-quantum key exchange, SysV scripts, the equivalents of ifconfig and iptables, RHEL differences and choosing between an in-place upgrade and a new server, with real output.
Version note: The commands in this post were run on a test VM running Ubuntu Server 24.04.5 LTS (kernel 6.8, systemd 255, sudo 1.9.15p5, coreutils 9.4, OpenSSH 9.6p1), in the official Ubuntu 26.04.1 LTS container image (sudo-rs 0.2.13, uutils coreutils 0.10.0, classic coreutils 9.7, OpenSSH 10.2p1) and in the Rocky Linux 9.8 container image. The 26.04 items about the kernel and systemd could not be tried in a container and are taken from Ubuntu's official release notes.
Guess First
In the second post we switched the releases of Bookstore, the online bookstore used as the shared example across this site's series, with the current symlink, and saw with strace that ln -sfn does this atomically: the new link is created under a temporary name and then replaces the old one with a single rename. At no moment in between is current missing. Now you are setting up the new server with Ubuntu 26.04, and your deploy script does not change at all:
$ ln -sfn /opt/bookstore/releases/2026-11-14-7 /opt/bookstore/current
Same command, same output, and in the end the link points to the new release. Is the switch still atomic? The answer is in the Rust coreutils section.
The last post of the series looks in two directions. Ahead: the changes in Ubuntu 26.04 LTS, released in April 2026, that affect a developer's server (sudo-rs, coreutils written in Rust, post-quantum key exchange with OpenSSH 10) and the strategy for moving from 24.04. Back: SysV init scripts, ifconfig and netstat, iptables on old servers that are still running, and how the RHEL family differs.
What Changed in Ubuntu 26.04 LTS?
Ubuntu 26.04 LTS, code name Resolute Raccoon, was released on 23 April 2026 and gets standard support until April 2031. From the official summary for users coming from 24.04, the items that touch the topics of this series:
- Kernel from 6.8 to 7.0, systemd from 255 to 259. 26.04 is the last release in which systemd supports SysV service scripts; cgroup v1 support was removed.
- Behind the
sudocommand is now sudo-rs, written in Rust; the original sudo stays assudo.ws. - The core commands (
ls,cat,date…) come from uutils' Rust coreutils;cp,mvandrmstay on GNU for now. - OpenSSH from 9.6 to 10.2: the default key exchange is the post-quantum
mlkem768x25519-sha256, and DSA support is gone entirely. - APT from 2.7 to 3.1, with
apt-keyremoved (the warning in the ninth post is now an error); new installs synchronize time with chrony instead ofsystemd-timesyncd.
I ran the 26.04 tests in this post in Ubuntu's official ubuntu:26.04 container image. A container does not run its own kernel or systemd, so I did not test the kernel 7.0 and systemd 259 items directly and quote them from the release notes. sudo, coreutils and OpenSSH, on the other hand, are exactly the same packages in the container.
sudo-rs: Same sudoers, Different Messages
$ cat /etc/os-release | head -n 1
PRETTY_NAME="Ubuntu 26.04.1 LTS"
$ sudo --version | head -n 1; ls --version | head -n 1; cp --version | head -n 1; ssh -V
sudo-rs 0.2.13-0ubuntu1.2
ls (uutils coreutils) 0.10.0
cp (GNU coreutils) 9.7
OpenSSH_10.2p1 Ubuntu-2ubuntu3.8, OpenSSL 3.5.5 27 Jan 2026
$ readlink -f /usr/bin/ls /usr/bin/cp /usr/bin/sudo
/usr/lib/cargo/bin/coreutils/ls
/usr/bin/gnucp
/usr/lib/cargo/bin/sudo
$ update-alternatives --display sudo | grep priority
/usr/bin/sudo.ws - priority 40
/usr/lib/cargo/bin/sudo - priority 50
sudo is an alternatives link, the update-alternatives mechanism from the ninth post: sudo-rs is registered with priority 50 and the original sudo with 40, and the higher one wins. I tried the sudoers file we wrote in the third and twelfth posts on 26.04, unchanged; I ran as root in the container, so there is no sudo in front of the admin commands:
$ visudo -cf /tmp/bookstore-deploy
/tmp/bookstore-deploy: parsed OK
$ sudo -l -U deploy
User deploy may run the following commands on bookstore-next:
(root) NOPASSWD: /usr/bin/systemctl restart bookstore-api, /usr/bin/systemctl status bookstore-api, /usr/bin/systemctl reset-failed bookstore-api
$ sudo -u deploy sudo -n cat /etc/bookstore/secrets.env; echo "exit=$?"
sudo: I'm sorry deploy. I'm afraid I can't do that
exit=1
$ visudo -cf /tmp/bad; echo "exit=$?"
/tmp/bad:1:28: syntax error: expecting ':' but found '/'
deploy ALL=(root) NOPASSWD /usr/bin/systemctl restart bookstore-api
^
visudo: invalid sudoers file
exit=1
The rules work the same: visudo, now visudo-rs, accepted the file, the allowed commands are the same, and the forbidden command was refused. What changed are the messages. When I temporarily switched the alternatives back to the original sudo in the same container and ran the same command, the message was sudo: a password is required, as in the third post. sudo-rs states the refusal outright. The exit code is 1 in both. For the broken line, visudo-rs also points at the character where the colon is missing instead of the end of the line as GNU does.
The practical consequence: a script or a monitoring rule that reads sudo's output with grep ("if you see a password is required, do this") silently stops working on 26.04. In scripts, look at the exit code, not the message. sudo-rs supports only part of sudoers and of sudo, and the project's README says some of this is intentional: for example there is no sudo -E to keep the environment, the logfile setting is not supported because logs always go to syslog, and wildcards in command rules only work in the last argument. If your scripts use sudo -E or you have a complex sudoers file, check it with visudo -c on 26.04 before you upgrade. Going back to the original sudo, if needed, is one command:
$ update-alternatives --set sudo /usr/bin/sudo.ws
update-alternatives: using /usr/bin/sudo.ws to provide /usr/bin/sudo (sudo) in manual mode
...
$ sudo --version | head -n 1
Sudo version 1.9.17p2
$ update-alternatives --auto sudo
update-alternatives: using /usr/lib/cargo/bin/sudo to provide /usr/bin/sudo (sudo) in auto mode
...
$ sudo --version | head -n 1
sudo-rs 0.2.13-0ubuntu1.2
Rust Coreutils: Same Commands, Not Always the Same Behavior
The answer to the guess. I traced the same ln -sfn on 26.04 with strace, then did the same with the GNU version that sits in the image (gnuln):
$ readlink -f /usr/bin/ln /usr/bin/mv
/usr/lib/cargo/bin/coreutils/ln
/usr/bin/gnumv
$ cd /opt/bookstore
$ strace -e trace=symlink,symlinkat,unlink,unlinkat,rename,renameat,renameat2 ln -sfn /opt/bookstore/releases/2026-11-14-7 current
unlinkat(AT_FDCWD, "current", 0) = 0
symlinkat("/opt/bookstore/releases/2026-11-14-7", AT_FDCWD, "current") = 0
$ strace -e trace=symlink,symlinkat,unlink,unlinkat,rename,renameat,renameat2 gnuln -sfn /opt/bookstore/releases/2026-11-14-6 current
symlinkat("/opt/bookstore/releases/2026-11-14-6", AT_FDCWD, "current") = -1 EEXIST (File exists)
symlinkat("/opt/bookstore/releases/2026-11-14-6", AT_FDCWD, "CuNNzgVW") = 0
renameat(AT_FDCWD, "CuNNzgVW", AT_FDCWD, "current") = 0
No, it is not atomic. ln in uutils 0.10.0 first deletes the old link (unlinkat) and then creates the new one (symlinkat). For the short time between the two system calls, current does not exist; a systemctl restart that starts at that moment, or a file Nginx reads through that path, gets "No such file or directory". GNU ln, as in the second post, creates it under a temporary name and swaps it in with renameat. The window is far shorter than a second and nobody notices it on most deploys; but it is exactly the kind of bug that shows up "sometimes, for no clear reason". Because the command name, the options and the result are the same, you can only see it at the system call level.
The fix is to stop the script depending on which coreutils runs it. Leave atomicity not to ln but to rename, which is atomic by definition: create the link under a temporary name and move it into place with mv -T. mv is still GNU on 26.04, and -T treats the target as a file rather than a directory:
$ strace -e trace=symlinkat,unlinkat,renameat,renameat2,rename bash -c "ln -s /opt/bookstore/releases/2026-11-14-7 current.tmp && mv -T current.tmp current"
renameat2(AT_FDCWD, "current.tmp", AT_FDCWD, "current", RENAME_NOREPLACE) = -1 EEXIST (File exists)
renameat(AT_FDCWD, "current.tmp", AT_FDCWD, "current") = 0
$ readlink current
/opt/bookstore/releases/2026-11-14-7
There are other differences too. I ran 25 commands I use every day (sort, cut, date, stat, du, numfmt, sha256sum, uniq…) with both versions and compared the output; most were identical, two were not:
$ mkdir /tmp; gnumkdir /tmp
mkdir: /tmp: File exists
gnumkdir: cannot create directory '/tmp': File exists
$ ls -l --block-size=K books.csv; gnuls -l --block-size=K books.csv
-rw-r--r-- 1 root root 1 Nov 1 10:00 books.csv
-rw-r--r-- 1 root root 1K Nov 1 10:00 books.csv
$ ls /usr/bin/gnu* | wc -l
104
The text of the error message has changed, and with --block-size=K the unit suffix is gone. Either can break a script that processes the message or the output as text. 26.04 keeps the GNU versions alongside with a gnu prefix, 104 commands; if a script needs the GNU behavior of a specific command, it can call that one explicitly. The way to switch the whole system back to GNU is in the release notes:
$ apt-get install -y coreutils-from-gnu coreutils-from-uutils- --allow-remove-essential
...
dpkg: warning: overriding problem because --force enabled:
dpkg: warning: this is a protected package; it should not be removed
...
$ ls --version | head -n 1; readlink -f /usr/bin/ln
ls (GNU coreutils) 9.7
/usr/bin/gnuln
The - at the end of a package name tells apt to remove that package; --allow-remove-essential is needed because coreutils is marked "essential". After switching back, ln -sfn used renameat again. Whichever you choose, decide before the upgrade and run your deploy and backup scripts on 26.04 once. The script testing post of the Bash Scripting series covers portability tests like this.
OpenSSH 10: Post-Quantum Key Exchange
At the start of an SSH connection the two sides agree on the session key with a key exchange. Against the risk that traffic recorded today could be decrypted later by a sufficiently strong quantum computer ("store now, decrypt later"), OpenSSH moved to hybrid methods that combine a classical algorithm with a post-quantum one. The theory is in the TLS 1.3 handshake post of the Networking and HTTP series. I connected from the 10.2 client on 26.04 to the Bookstore server on 24.04:
ada@bookstore-next:~$ ssh -v bookstore-dev true 2>&1 | grep -E "kex: algorithm|Remote protocol|Local version"
debug1: Local version string SSH-2.0-OpenSSH_10.2p1 Ubuntu-2ubuntu3.8
debug1: Remote protocol version 2.0, remote software version OpenSSH_9.6p1 Ubuntu-3ubuntu13.20
debug1: kex: algorithm: sntrup761x25519-sha512@openssh.com
ada@bookstore-next:~$ ssh -G bookstore-dev | grep -E "^kexalgorithms" | cut -c1-120
kexalgorithms mlkem768x25519-sha256,sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve255
The client's first choice is mlkem768x25519-sha256, but 9.6 does not know it; the two sides agreed on the second one, sntrup761x25519-sha512. That is also a post-quantum hybrid algorithm and has been the default since OpenSSH 9.0. So your 24.04 servers talk to 26.04 clients without problems and still with post-quantum protection. The real difference is with older servers. To simulate one, I started an sshd in the 26.04 container that only accepts the classical curve25519-sha256:
ada@bookstore-next:~$ ssh -o StrictHostKeyChecking=accept-new -p 2222 localhost "echo connected"
Warning: Permanently added '[localhost]:2222' (ED25519) to the list of known hosts.
** WARNING: connection is not using a post-quantum key exchange algorithm.
** This session may be vulnerable to "store now, decrypt later" attacks.
** The server may need to be upgraded. See https://openssh.com/pq.html
connected
The connection works, but since 10.1 the client warns every time. Every 26.04 machine connecting to an old server that knows no post-quantum algorithm, such as OpenSSH 8.2 on Ubuntu 20.04, will see this. The warning can be turned off with WarnWeakCrypto in ssh_config, but the right answer is to upgrade the server. The warning goes to stderr; if a CI step treats anything on stderr as a failure, check that too. DSA keys are also gone completely in 10.x: if a CI job or an old device still uses id_dsa, move to ed25519 before the upgrade (first post).
Reading Old Servers: SysV, ifconfig and iptables
On an old server you inherit, services may be managed by shell scripts under /etc/init.d/, that is SysV init, the init system before systemd. On 24.04 that folder still exists and systemd turns those scripts into units. I added an old-style Bookstore script:
$ ls /etc/init.d/ | paste -sd" "
apparmor apport console-setup.sh cron cryptdisks cryptdisks-early dbus fail2ban grub-common iscsid keyboard-setup.sh kmod nginx open-iscsi open-vm-tools plymouth plymouth-log procps rsync screen-cleanup ssh sysstat ubuntu-fan ufw unattended-upgrades uuidd
$ sudo install -m 755 /tmp/bookstore-legacy /etc/init.d/bookstore-legacy && sudo systemctl daemon-reload
$ systemctl status bookstore-legacy --no-pager | head -n 4
○ bookstore-legacy.service - LSB: Bookstore API (old init.d script)
Loaded: loaded (/etc/init.d/bookstore-legacy; generated)
Active: inactive (dead)
Docs: man:systemd-sysv-generator(8)
$ systemctl cat bookstore-legacy --no-pager | head -n 14
# /run/systemd/generator.late/bookstore-legacy.service
# Automatically generated by systemd-sysv-generator
[Unit]
Documentation=man:systemd-sysv-generator(8)
SourcePath=/etc/init.d/bookstore-legacy
Description=LSB: Bookstore API (old init.d script)
After=network-online.target
After=remote-fs.target
Wants=network-online.target
[Service]
Type=forking
Restart=no
$ service nginx status --no-pager | head -n 3
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
Drop-In: /etc/systemd/system/nginx.service.d
Most of the files in /etc/init.d/ are scripts that packages leave for backward compatibility; the real units of nginx, ssh and cron are under /usr/lib/systemd/system/. systemd-sysv-generator reads the LSB header at the top of the script, turns the dependencies into After= and sets Restart=no: if a service run by a script crashes, nobody brings it back. The service command is still there too, but systemctl runs behind it. According to the release notes, 26.04 is the last release that supports this conversion; the time to move such a script to a real unit, as in the fifth post, is before the upgrade.
The network commands typed out of habit on an old server are not on 24.04:
$ ifconfig
Command 'ifconfig' not found, but can be installed with:
sudo apt install net-tools
$ netstat -tlnp
Command 'netstat' not found, but can be installed with:
sudo apt install net-tools
$ ls /var/log/messages
ls: cannot access '/var/log/messages': No such file or directory
$ update-alternatives --display iptables | head -n 3
iptables - auto mode
link best version is /usr/sbin/iptables-nft
link currently points to /usr/sbin/iptables-nft
The equivalents of the old commands:
| On an old server | Today | In this series |
|---|---|---|
ifconfig, route -n | ip a, ip r | eighth post |
netstat -tlnp | ss -tlnp | first and eighth posts |
service x start, /etc/init.d/x | systemctl start x | fifth post |
chkconfig, update-rc.d | systemctl enable x | fifth post |
/var/log/messages (RHEL), /var/log/syslog | journalctl | sixth post |
iptables -L | nft list ruleset, ufw or firewalld | eleventh post |
iptables is a slightly different story: the command still exists and works, but on 24.04 iptables-nft is behind it, so it writes its rules into nftables. In the eleventh post we read ufw's rules with nft and saw the "managed by iptables-nft, do not touch" warning. If iptables -V on an old server shows (legacy), the rules are in the old framework; mixing the two frameworks on one machine makes it unclear which rule applies. If there is an old script that restores rules from a file (iptables-restore), moving it to ufw or directly to nftables should be part of the upgrade plan.
A Short Map for the RHEL Family
The series ran on Ubuntu, but in corporate environments you will often meet RHEL and the compatible Rocky Linux or AlmaLinux. Most commands are the same (systemctl, journalctl, ss, ip, coreutils); what differs is package management, the firewall and the security layer. In a Rocky Linux 9 container:
$ cat /etc/redhat-release
Rocky Linux release 9.8 (Blue Onyx)
$ rpm -qf /usr/bin/ls
coreutils-single-8.32-40.el9.x86_64
$ dnf -q info nginx | grep -E "^(Name|Version|Repository)"
Name : nginx
Version : 1.20.1
Repository : appstream
The equivalent of apt is dnf, and of dpkg -S it is rpm -qf. Even two distributions released in the same years ship different versions: Nginx 1.24 on Ubuntu 24.04, 1.20.1 in Rocky 9's default repository. Two more differences, which I did not run in the container and which are known from the documentation: the firewall is firewalld (firewall-cmd) instead of ufw, again with nftables behind it, and the security layer is SELinux instead of AppArmor, in enforcing mode by default. This is the most common problem when moving Bookstore to RHEL: Nginx cannot connect to the application on 5000 even though the file permissions are right. The cause is found with getenforce and in the audit log (ausearch -m avc); the fix is not to turn SELinux off but to turn on the right boolean (httpd_can_network_connect for Nginx's network connections).
Upgrading: do-release-upgrade or a New Server?
The Bookstore server already sees the new release:
$ do-release-upgrade -c
Checking for a new Ubuntu release
New release '26.04.1 LTS' available.
Run 'do-release-upgrade' to upgrade to it.
$ grep ^Prompt /etc/update-manager/release-upgrades
Prompt=lts
Prompt=lts means only LTS-to-LTS upgrades are offered. Ubuntu does not open LTS-to-LTS upgrades until the first point release, 26.04.1 here; the bugs of the first months are fixed by then. There are two ways:
In-place upgrade (do-release-upgrade): the server's address, data and every manual setting stay where they are. But the service is interrupted during the upgrade, a failure halfway can leave the server between two releases, and rolling back needs a full snapshot or backup. I did not upgrade the server this way in this series; I show the command's output only in check mode (-c).
Moving to a new server: set up a new VM with 26.04, prepare it with the files in the repo (units, sudoers, the sshd drop-in, Fail2ban, the Nginx config), deploy Bookstore, test it and switch traffic to the new server at the load balancer or in DNS. The old server stays for a few days as the way back. This is the immutable infrastructure approach, replacing servers instead of changing them, and making this upgrade possible is exactly why we put every setting in the repo throughout this series. Every change made by hand that never got into the repo will be missing on the new server, and you find out during the move. Building servers with code is the topic of the upcoming Infrastructure as Code: Terraform series.
For a single application and a few servers like Bookstore, the second way is safer and testable. The in-place upgrade is for a stateful server that is hard to move, with large local data, licensed software or undocumented old settings; even then, try it on a copy of the server first.
Production Notes
Run your scripts on 26.04 before you upgrade. The two findings in this post, the non-atomic ln -sfn and the changed sudo message, change behavior without any error; you only find them by actually running your deploy, backup and maintenance scripts on the new release, looking with strace if needed.
When you inherit an old server, take an inventory first: scripts in /etc/init.d/ that belong to no package, cron jobs, iptables rules, files changed by hand in /etc (dpkg --verify shows changes to package files). These are the settings that must move to the new server and that nobody remembers.
Common Pitfalls
Assuming the behavior is the same because the command name is. On 26.04, ls, ln and sudo come from a different codebase; the options are largely the same, the behavior in edge cases is not. For critical steps such as an atomic switch or error checks, either call the GNU version explicitly or use a method that guarantees the behavior, such as mv -T.
Grepping error messages in scripts. Messages change from version to version; the exit code is a more stable contract. The sudo-rs and uutils examples are live proof.
Upgrading in place as soon as an LTS ships. Wait for the first point release, and try on a copy or a new server first.
A Small Experiment
★☆☆ Open a container with docker run --rm -it ubuntu:26.04 and use readlink -f to list where ls, cp, mv, rm, date and sudo under /usr/bin come from. Which are GNU and which are Rust?
★★☆ Run your own deploy script in a 26.04 container under strace -f -e trace=unlinkat,symlinkat,renameat. Which system calls switch the symlink? What changes when you move to the mv -T method?
★★★ Rebuild the Bookstore server you set up in this series on a new 26.04 VM using only the files in the repo. Note every step you had to do by hand; each item on that list is something that should go into the repo.
This is where the series ends. Starting from connecting over SSH, we went through the filesystem, permissions, processes, systemd, reading logs, the network, packages, resources, hardening and troubleshooting on a Bookstore server, and talked about what to look at when releases change. Turning what you learned here into scripts continues in the Bash Scripting series, and running the same application in containers in the Docker series.