Packages and updates

apt, signed repositories and updates.

Beginner14 min · lesson 27 of 29

Almost all software on an Ubuntu server arrives as packages from repositories that Ubuntu signs. This lesson shows where those repositories are configured, how apt refreshes its lists and applies updates, how to find out what a package contains and which package owns a file, and how to add a vendor's repository without handing that vendor more trust than it needs. It finishes with the same jobs on RHEL, where dnf and rpm do them. The package manager decides whose code your server will run, which is why it deserves the same care as the SSH configuration.

Where packages come from

A package is an archive of files plus a description: its name, version, the other packages it depends on, and scripts to run when it is installed or removed. dpkg installs single package files. apt sits on top of it: it downloads packages from repositories, works out dependencies, and hands the files to dpkg. On Ubuntu 26.04 the repositories are listed in files ending in .sources under /etc/apt/sources.list.d/; the old /etc/apt/sources.list now holds only a comment pointing there.

deploy@web01 · Ubuntu 26.04 LTS
$ ls /etc/apt/sources.list.d/
ubuntu.sources
$ cat /etc/apt/sources.list.d/ubuntu.sources
## Note, this file is written by cloud-init on first boot of an instance … Types: deb URIs: http://archive.ubuntu.com/ubuntu Suites: resolute resolute-updates resolute-backports Components: main universe restricted multiverse Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg ## Ubuntu security updates. Aside from URIs and Suites, ## this should mirror your choices in the previous section. Types: deb URIs: http://security.ubuntu.com/ubuntu Suites: resolute-security Components: main universe restricted multiverse Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg

The file is written by cloud-init when a cloud image first boots and starts with a long comment, left out here. Each block of Field: value lines, in the format called deb822, describes one source. Types: deb means binary packages. URIs is the server. Suites are sections of the release: resolute is Ubuntu 26.04 as released, resolute-updates carries bug fixes, resolute-backports newer versions of some programs, and resolute-security, from security.ubuntu.com, security fixes. Components split the archive by who maintains it: main is supported by Canonical, while universe is community-maintained, and the file's own comments say Canonical's security maintenance of it comes with an Ubuntu Pro subscription. Signed-By names the keyring whose keys may sign this repository, and no other.

Those signatures protect you from a tampered mirror or network. When apt refreshes, it downloads each suite's InRelease file, a signed list of the repository's index files with their checksums, and each index lists a checksum for every package. A package is installed only if every link checks out.

What apt checks before a package reaches the disk
1Download InRelease
the signed list of index files
2Verify the signature
with the key named in Signed-By
3Check the indexes
checksums listed in InRelease
4Check each package
checksum listed in the index
5Install
dpkg unpacks what passed
Break any link and apt refuses the repository or the package.

Older guides add keys with apt-key add, which put every key in one store trusted for all repositories. apt 3 removed that command, so it does not exist on Ubuntu 26.04; the modern way is a keyring file named in Signed-By, as shown later in this lesson.

Refreshing the lists, then upgrading

apt update and apt upgrade do different jobs. update downloads fresh copies of the repository indexes, so apt knows which versions exist, and installs nothing. upgrade installs newer versions of packages you already have. Run them in that order.

deploy@web01 · Ubuntu 26.04 LTS
$ sudo apt update
… Hit:1 http://security.ubuntu.com/ubuntu resolute-security InRelease Hit:2 http://archive.ubuntu.com/ubuntu resolute InRelease Get:3 http://archive.ubuntu.com/ubuntu resolute-updates InRelease [137 kB] Hit:4 http://archive.ubuntu.com/ubuntu resolute-backports InRelease Get:5 http://archive.ubuntu.com/ubuntu resolute-updates/main arm64 Packages [691 kB] Get:6 http://archive.ubuntu.com/ubuntu resolute-updates/universe arm64 Packages [294 kB] Fetched 1123 kB in 3s (335 kB/s) … 6 packages can be upgraded. Run 'apt list --upgradable' to see them.
# … stands for lines left out; at the top, a warning apt prints only when it runs without a terminal
$ apt list --upgradable
… Listing... python3-distupgrade/resolute-updates 1:26.04.25 all [upgradable from: 1:26.04.23] ubuntu-kernel-accessories/resolute-updates 1.570.4 arm64 [upgradable from: 1.570.3] ubuntu-minimal/resolute-updates 1.570.4 arm64 [upgradable from: 1.570.3] ubuntu-release-upgrader-core/resolute-updates 1:26.04.25 all [upgradable from: 1:26.04.23] ubuntu-server/resolute-updates 1.570.4 arm64 [upgradable from: 1.570.3] ubuntu-standard/resolute-updates 1.570.4 arm64 [upgradable from: 1.570.3]

Hit means an index had not changed since the last refresh; Get means it had, and was downloaded again. The last line counts the packages with a newer version available, and apt list --upgradable names them: package, the suite the new version comes from, the new version, and the installed one. All six come from resolute-updates, the bug-fix suite. Before upgrading, look at what apt plans to do. --assume-no prints the plan and answers no to the question:

deploy@web01 · Ubuntu 26.04 LTS
$ sudo apt upgrade --assume-no
… Calculating upgrade... … Not upgrading yet due to phasing: python3-distupgrade ubuntu-minimal ubuntu-server ubuntu-kernel-accessories ubuntu-release-upgrader-core ubuntu-standard … Summary: Upgrading: 0, Installing: 0, Removing: 0, Not Upgrading: 6
$ apt policy ubuntu-server
… ubuntu-server: Installed: 1.570.3 Candidate: 1.570.4 Version table: 1.570.4 500 (phased 80%) 500 http://archive.ubuntu.com/ubuntu resolute-updates/main arm64 Packages *** 1.570.3 100 100 /var/lib/dpkg/status 1.570 500 500 http://archive.ubuntu.com/ubuntu resolute/main arm64 Packages

apt would upgrade nothing today. Ubuntu phases bug-fix updates: a new version goes to a small share of machines first and to more as long as error reports stay low. Each machine works out its own place from its machine ID, the package and the version. apt policy shows the new ubuntu-server released to 80% of machines, and this one is in the other 20% for now; *** marks the installed version. Security updates are never phased. Without --assume-no, sudo apt upgrade asks Continue? [Y/n] before changing anything.

You also do not have to remember to install security fixes. Ubuntu Server ships unattended-upgrades installed and switched on: a daily timer refreshes the lists and installs updates from the security suite. Verify it rather than installing it:

deploy@web01 · Ubuntu 26.04 LTS
$ systemctl is-active unattended-upgrades.service apt-daily-upgrade.timer cat /etc/apt/apt.conf.d/20auto-upgrades
active active APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Unattended-Upgrade "1";

Both units are active and both settings are "1", which means daily. What the job may install, how services pick up fixed libraries, and when to reboot are decisions for the lesson "Patching and update strategy" in the Linux hardening course. Ubuntu has a second package system, snap, whose packages snapd refreshes on its own schedule; a server install has none (snap list says No snaps are installed yet).

One package, from search to removal

apt search --names-only WORD finds packages by name. apt show describes one, and apt policy says which versions exist and where each comes from. The example is ncdu, a disk-usage browser.

deploy@web01 · Ubuntu 26.04 LTS
$ apt show ncdu
… Package: ncdu Version: 1.22-1build1 Priority: optional Section: universe/admin … Depends: libc6 (>= 2.34), libncursesw6 (>= 6), libtinfo6 (>= 6) Homepage: https://dev.yorhel.nl/ncdu/ Download-Size: 48.7 kB APT-Sources: http://archive.ubuntu.com/ubuntu resolute/universe arm64 Packages …
$ apt policy ncdu
… ncdu: Installed: (none) Candidate: 1.22-1build1 Version table: 1.22-1build1 500 500 http://archive.ubuntu.com/ubuntu resolute/universe arm64 Packages

Section: universe/admin places it in the community-maintained component. Depends lists what it needs, and APT-Sources shows which repository the candidate comes from. In apt policy, Installed: (none) means it is not installed, Candidate is the version apt install would choose, and 500 is the default priority of a repository.

deploy@web01 · Ubuntu 26.04 LTS
$ sudo apt install ncdu
… Installing: ncdu … Summary: Upgrading: 0, Installing: 1, Removing: 0, Not Upgrading: 6 Download size: 48.7 kB Space needed: 155 kB / 19.3 GB available … Get:1 http://archive.ubuntu.com/ubuntu resolute/universe arm64 ncdu arm64 1.22-1build1 [48.7 kB] … Preparing to unpack .../ncdu_1.22-1build1_arm64.deb ... Unpacking ncdu (1.22-1build1) ... Setting up ncdu (1.22-1build1) ... Processing triggers for man-db (2.13.1-1build1) ... … Running kernel seems to be up-to-date (ABI upgrades are not detected). … Restarting services... … Service restarts being deferred: /etc/needrestart/restart.d/dbus.service systemctl restart networkd-dispatcher.service systemctl restart unattended-upgrades.service …

apt shows its plan, downloads the package, and dpkg unpacks and configures it. The lines left out are messages from debconf, the tool that asks packages' configuration questions, about having no terminal to ask on. The last lines come from needrestart, which Ubuntu runs after every apt install or upgrade to find running services that still use a library the upgrade replaced. Restarting services... means what it says: when apt runs it, Ubuntu sets its mode to automatic (the comments in /etc/needrestart/needrestart.conf say so), and it restarts such services without asking, except the few it defers. Here the only services still using replaced libraries were the three it defers, so nothing was restarted, but after an update of a library such as OpenSSL, the same sudo apt upgrade restarts your database or web server in the middle of the day. On production servers, plan updates for a quiet time; a file in /etc/needrestart/conf.d/ containing $nrconf{restart} = 'l'; makes needrestart only list what needs a restart. "Patching and update strategy" in the hardening course covers this policy.

Now ask dpkg what it installed, and turn the question around: which package owns a file? For the second question, first copy a script into /usr/local/bin by hand, as happens on servers that have been looked after for years (sudo tee writes what it reads to a root-owned file and also prints it):

deploy@web01 · Ubuntu 26.04 LTS
$ printf '#!/bin/sh\nrsync -a /srv/data/ backup01:data/\n' | sudo tee /usr/local/bin/packages-sync sudo chmod 755 /usr/local/bin/packages-sync
#!/bin/sh rsync -a /srv/data/ backup01:data/

Now list what ncdu installed, then ask which package owns a system program and the script:

deploy@web01 · Ubuntu 26.04 LTS
$ dpkg -L ncdu
/. /usr /usr/bin /usr/bin/ncdu /usr/share /usr/share/doc /usr/share/doc/ncdu /usr/share/doc/ncdu/changelog.Debian.gz /usr/share/doc/ncdu/copyright /usr/share/man /usr/share/man/man1 /usr/share/man/man1/ncdu.1.gz
$ dpkg -S /usr/bin/ssh
openssh-client: /usr/bin/ssh
$ dpkg -S /usr/local/bin/packages-sync
dpkg-query: no path found matching pattern /usr/local/bin/packages-sync
$ sudo rm /usr/local/bin/packages-sync

dpkg -L lists every path the package installed. dpkg -S searches the same records in reverse: /usr/bin/ssh belongs to openssh-client. The script belongs to no package, which is normal for /usr/local but worth knowing about on a server you inherit: nothing will update it, and nobody may know where it came from. (The last command removes the practice script again.)

dpkg also stored a checksum of every file it installed. dpkg --verify compares the files on disk with those checksums and prints only the ones that differ. For this example, root appended a byte to /usr/bin/ncdu:

deploy@web01 · Ubuntu 26.04 LTS
$ dpkg --verify ncdu
# before: no output, every file matches its checksum
$ dpkg --verify ncdu
??5?????? /usr/bin/ncdu
# after root appended one byte to /usr/bin/ncdu

The 5 in ??5?????? means the checksum no longer matches; the question marks are checks dpkg does not perform. The exit status is 0 either way, so read the output. dpkg(1) calls this an integrity check and not a security verification, because root can also rewrite dpkg's own records; the hardening course's lesson on file integrity monitoring with AIDE covers a baseline kept out of an intruder's reach.

deploy@web01 · Ubuntu 26.04 LTS
$ tail -n 6 /var/log/apt/history.log
Start-Date: 2026-09-27 08:22:11 Commandline: apt install ncdu Requested-By: deploy (1001) Install: ncdu:arm64 (1.22-1build1) End-Date: 2026-09-27 08:22:11
$ sudo apt purge -y ncdu
… REMOVING: ncdu* … Summary: Upgrading: 0, Installing: 0, Removing: 1, Not Upgrading: 6 Freed space: 155 kB … Removing ncdu (1.22-1build1) ... …

/var/log/apt/history.log records every apt run: the command, who ran it through sudo, and what changed. To remove a package, apt remove keeps its configuration files and apt purge deletes them too, which the * after the name marks. -y answers yes to the confirmation question.

Adding a vendor's repository

When software is not in Ubuntu's archive, many vendors publish their own apt repository. Adding one means trusting its signing key for everything that repository serves, now and in future updates, so tie the key to that repository alone. Docker's documented procedure does exactly that, and it is a good pattern to copy: a deb822 file whose Signed-By names a key file in /etc/apt/keyrings, the directory for keys managed by the administrator. The command uses three pieces of shell syntax not met before. <<EOF (a here-document) feeds the lines that follow to tee as its input, up to the line EOF. Inside $( ), . /etc/os-release reads that file's variables into the shell, and ${UBUNTU_CODENAME:-$VERSION_CODENAME} uses UBUNTU_CODENAME, or VERSION_CODENAME if the first is empty, which gives the release's code name; the second $( ) prints the machine's architecture.

deploy@web01 · Ubuntu 26.04 LTS
$ sudo tee /etc/apt/sources.list.d/docker.sources <<EOF Types: deb URIs: https://download.docker.com/linux/ubuntu Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") Components: stable Architectures: $(dpkg --print-architecture) Signed-By: /etc/apt/keyrings/docker.asc EOF
Types: deb URIs: https://download.docker.com/linux/ubuntu Suites: resolute Components: stable Architectures: arm64 Signed-By: /etc/apt/keyrings/docker.asc

The lab machine is arm64; most servers print amd64 there. The key file does not exist yet, so the next apt update shows what a repository without a trusted key looks like:

deploy@web01 · Ubuntu 26.04 LTS
$ sudo apt update
… Get:1 https://download.docker.com/linux/ubuntu resolute InRelease [32.5 kB] Err:1 https://download.docker.com/linux/ubuntu resolute InRelease The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 7EA0A9C3F273FCD8 Hit:2 http://security.ubuntu.com/ubuntu resolute-security InRelease Hit:3 http://archive.ubuntu.com/ubuntu resolute InRelease Hit:4 http://archive.ubuntu.com/ubuntu resolute-updates InRelease Hit:5 http://archive.ubuntu.com/ubuntu resolute-backports InRelease Reading package lists... Warning: OpenPGP signature verification failed: https://download.docker.com/linux/ubuntu resolute InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 7EA0A9C3F273FCD8 Error: The repository 'https://download.docker.com/linux/ubuntu resolute InRelease' is not signed.

apt fetched Docker's InRelease, could not verify its signature (NO_PUBKEY names the key it needed), refused the repository, and exited with status 100. Ubuntu's own repositories were unaffected. Never silence this with [trusted=yes] in the sources file, which turns the check off. Fetch the vendor's key into the file that Signed-By names, as Docker's instructions do, and refresh again:

deploy@web01 · Ubuntu 26.04 LTS
$ sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc
$ sudo apt update
… Get:1 https://download.docker.com/linux/ubuntu resolute InRelease [32.5 kB] Get:2 https://download.docker.com/linux/ubuntu resolute/stable arm64 Packages [24.2 kB] Hit:3 http://security.ubuntu.com/ubuntu resolute-security InRelease Hit:4 http://archive.ubuntu.com/ubuntu resolute InRelease Hit:5 http://archive.ubuntu.com/ubuntu resolute-updates InRelease Hit:6 http://archive.ubuntu.com/ubuntu resolute-backports InRelease Fetched 56.7 kB in 1s (61.8 kB/s) …
$ apt policy docker-ce
… docker-ce: Installed: (none) Candidate: 5:29.8.1-1~ubuntu.26.04~resolute Version table: 5:29.8.1-1~ubuntu.26.04~resolute 500 500 https://download.docker.com/linux/ubuntu resolute/stable arm64 Packages …

The repository now verifies, and apt policy shows Docker's versions coming from Docker's server. Its key can vouch only for that repository. Removing the .sources file and the key file, then running sudo apt update, removes the source again. A vendor's signed repository is also the answer to install instructions that pipe a script into a shell: updates then arrive through the same checked path as Ubuntu's own.

On RHEL: dnf and rpm

RHEL, and rebuilds such as Rocky Linux, install .rpm packages with dnf, and rpm plays the part of dpkg. Repositories are defined in /etc/yum.repos.d/*.repo, and each carries gpgcheck=1 and the key that must have signed its packages:

deploy@rocky10 · Rocky Linux 10.2
$ dnf repolist
repo id repo name appstream Rocky Linux 10 - AppStream baseos Rocky Linux 10 - BaseOS extras Rocky Linux 10 - Extras
$ grep -E '^\[|^enabled|^gpgcheck|^gpgkey' /etc/yum.repos.d/rocky.repo | head -n 8
[baseos] gpgcheck=1 enabled=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 …

The commands map almost one to one. Every RHEL command in this table was run on the Rocky Linux lab machine (installs and removals with -y, the upgrade with --assumeno):

apt and dnf equivalents
Task Ubuntu 26.04 (apt, dpkg) RHEL 10 (dnf, rpm)
Refresh package lists sudo apt update (dnf refreshes when its cache expires)
List pending updates apt list --upgradable dnf check-update
Upgrade installed packages sudo apt upgrade sudo dnf upgrade
Describe a package apt show NAME dnf info NAME
Install, remove sudo apt install / purge NAME sudo dnf install / remove NAME
Which package owns a file dpkg -S PATH rpm -qf PATH
Files in a package dpkg -L NAME rpm -ql NAME
Compare files with records dpkg --verify NAME rpm -V NAME
What changed, and when /var/log/apt/history.log sudo dnf history list
Repository definitions /etc/apt/sources.list.d/ /etc/yum.repos.d/
Automatic security updates unattended-upgrades (on) dnf-automatic (not installed)
deploy@rocky10 · Rocky Linux 10.2
$ dnf check-update
Last metadata expiration check: 2:23:34 ago on Sun 27 Sep 2026 05:54:31 AM UTC. kernel-tools.aarch64 6.12.0-211.60.1.el10_2 baseos kernel-tools-libs.aarch64 6.12.0-211.60.1.el10_2 baseos
$ rpm -qf /usr/bin/ssh
openssh-clients-9.9p1-27.el10_2.rocky.0.1.aarch64
$ rpm -qf /usr/local/bin/packages-sync
file /usr/local/bin/packages-sync is not owned by any package
$ rpm -q dnf-automatic
package dnf-automatic is not installed

dnf check-update exits with status 100 when updates are waiting, which scripts can test. Here they are two kernel tool packages that match a newer kernel installed on this machine. rpm -qf answers the same question as dpkg -S, here for the same hand-copied script on the Rocky machine. The last line is the biggest difference in defaults: RHEL installs nothing that applies updates automatically, and setting up dnf-automatic is part of the patching lesson in the hardening course.

Try this

Pick a program you rely on, such as rsync. Find the package that owns it with dpkg -S /usr/bin/rsync (it prints rsync: /usr/bin/rsync), then run apt policy rsync and read which version is installed and which suites offer it: on the lab machine the installed version, marked ***, is offered by both resolute-updates and resolute-security, so it arrived as a security update. Then run sudo apt update and sudo apt upgrade --assume-no on your own machine and explain, for every package listed, why apt would or would not install it today.

Takeaway

Install from signed repositories, with each third-party key tied to its own repository through Signed-By, and read apt's plan before you let it change anything. When a file on a server matters, ask the package database who owns it.

Quick check
01You add a vendor's .sources file, run sudo apt update, and get "NO_PUBKEY ..." followed by "The repository ... is not signed". What is the right fix?
Incorrect — That switches off signature checking for the repository, so anyone who can alter the mirror or the traffic could serve you packages.
Correct — The error means apt has no key to check the signature with. With the key in place, apt verifies the repository and trusts that key for it alone.
Incorrect — apt never downloads keys by itself. The same error will appear until the key file exists.
Incorrect — apt-key no longer exists in apt 3, and when it did, it trusted the key for every repository on the machine.
02apt list --upgradable shows six packages from resolute-updates, but sudo apt upgrade reports "Not upgrading yet due to phasing" and upgrades none of them. What does that mean?
Incorrect — A signature failure makes apt update report an error and refuse the repository; the packages would not be listed as upgradable.
Incorrect — unattended-upgrades does not lock packages. The phasing message comes from apt itself.
Correct — Bug-fix updates are phased; each machine computes its place from its machine ID, the package and the version, and gets them later.
Incorrect — Security updates are never phased, and apt upgrade installs them as readily as the nightly job does.
03On a server you have inherited, dpkg -S /usr/local/bin/sync-reports prints "no path found matching pattern". What does that tell you?
Correct — dpkg -S searches the records of files that packages installed. A file outside them gets no updates and deserves a question about where it came from.
Incorrect — dpkg -S never reads the file. It searches dpkg's records of which package installed which path.
Incorrect — Snaps are mounted under /snap and run from there; they do not place their programs in /usr/local/bin.
Incorrect — dpkg -S searches every path any installed package recorded, wherever it is.

Related