CoursesLinux essentialsPackages & signed repos

Packages & signed repos

apt/dnf, GPG-signed repos, updates.

Beginner12 min · lesson 24 of 25

Every program on your Linux machine arrived somehow. On a well-run system, almost all of it came through a single front door called the package manager. It works like the receiving dock behind a busy restaurant. Ingredients don't appear in the kitchen by magic. They come from known suppliers, someone checks each delivery against the order, and only then does it go on the shelf. The package manager is that dock for software. It installs programs, and it keeps track of the other pieces each program needs to run (its dependencies), so you never end up with something half-installed and broken.

There are two big families of Linux, with two package managers that work the same way. Debian and Ubuntu use apt (the Advanced Package Tool), and the files it installs end in .deb. Red Hat, Fedora, and RHEL (Red Hat Enterprise Linux) use dnf (short for Dandified YUM, the modern package tool on that side), and its files end in .rpm (the Red Hat package format). The commands look different but line up almost one to one, so learning one teaches you the other. You'll see sudo in front of most of them, which means 'run this one command as the administrator,' something installing system software requires.

~/secopslog — bash
$ # Debian/Ubuntu (apt) # RHEL/Fedora (dnf) sudo apt update $ sudo dnf check-update sudo apt upgrade $ sudo dnf upgrade sudo apt install nginx $ sudo dnf install nginx apt list --installed | wc -l $ dnf list installed | wc -l dpkg -S /usr/bin/ssh $ rpm -qf /usr/bin/ssh # which package owns a file?

For a security engineer this is the interesting part. The package manager decides what code your machine will trust, and where that code is allowed to come from. Set it up carelessly and an attacker can feed you their software in place of the real thing. Set it up properly and a tampered or counterfeit package gets thrown out before it ever lands on disk. The rest of this lesson is about how that check works, and how people get it wrong.

Update and upgrade are two different jobs

New users mix these up constantly. apt update does not update your software. It refreshes the catalog. Your machine keeps a local list of every package each repository offers and which version is newest, and apt update downloads a fresh copy of that list. apt upgrade is the step that actually installs newer versions of the things you already have. The rhythm never changes: update to get the current catalog, then upgrade to place the order. A repository (repo for short) is a web server holding a large pile of packages plus an index that describes them.

~/secopslog — bash
$ sudo apt update
Hit:1 http://archive.ubuntu.com/ubuntu jammy InRelease Get:2 http://security.ubuntu.com/ubuntu jammy-security InRelease [129 kB] Get:3 http://archive.ubuntu.com/ubuntu jammy-updates InRelease [128 kB] Get:4 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 Packages [1,412 kB] Fetched 1,669 kB in 1s (1,483 kB/s) Reading package lists... Done Building dependency tree... Done Reading state information... Done 23 packages can be upgraded. Run 'apt list --upgradable' to see them.

That last line is worth reading every time. To see exactly what's behind, ask for the list.

~/secopslog — bash
$ apt list --upgradable
Listing... Done curl/jammy-updates,jammy-security 7.81.0-1ubuntu1.20 amd64 [upgradable from: 7.81.0-1ubuntu1.16] libssl3/jammy-updates,jammy-security 3.0.2-0ubuntu1.18 amd64 [upgradable from: 3.0.2-0ubuntu1.15] openssh-server/jammy-updates,jammy-security 1:8.9p1-3ubuntu0.10 amd64 [upgradable from: 1:8.9p1-3ubuntu0.7]

Reading this before you upgrade tells you what you're about to pull in. A fix for something exposed, like openssh-server or an SSL library (the code that encrypts network connections), is a different level of urgency from a cosmetic bump. The word security in the origin (jammy-security) is your cue to act sooner rather than later.

Which package does this file belong to

Sometimes you're staring at a file and want to know where it came from. You find /usr/bin/ssh and you'd like to know whether it's part of a real, tracked package or something a person (or an intruder) dropped there by hand. The package manager keeps a record of every file it installed, so you can ask it in reverse: dpkg (the low-level Debian package tool) on one family, rpm (its Red Hat counterpart) on the other.

~/secopslog — bash
$ dpkg -S /usr/bin/ssh # Debian/Ubuntu: which package owns this file? rpm -qf /usr/bin/ssh # RHEL/Fedora: same question
openssh-client: /usr/bin/ssh openssh-clients-8.7p1-38.el9.x86_64

A file that no package claims is worth a closer look on a server you're investigating. The record goes further than names. On Red Hat systems, rpm -V compares every installed file against what the package originally shipped, like checking each item on the shelf against the original packing slip, and flags anything that has changed since.

~/secopslog — bash
$ rpm -V openssh-server # has anything changed since install?
S.5....T. c /etc/ssh/sshd_config

Reading that line: the 5 means the file's contents no longer match what shipped, S means its size changed, T means its timestamp moved, and the c marks it as a config file. For sshd_config that's expected, because you edited it. For a plain binary like /usr/sbin/sshd, the same output would mean someone swapped the program out, which is exactly the kind of thing you want a tool to catch. On Debian and Ubuntu, dpkg --verify does the same job.

Signed repositories, and why the seal matters

Here's the question that makes or breaks all of this. When your machine downloads the nginx package, how does it know it got the genuine article from Ubuntu, and not a swapped-out copy from a hacked mirror, or from an attacker sitting on the network between you and the server? The answer is a digital signature. Think of the wax seal an old letter carried: one signet ring pressed it, anyone could recognize it, and a broken or wrong seal told you the letter had been opened. The digital version is built with GPG (GNU Privacy Guard, the standard tool for signing and encrypting on Linux).

A signature like this uses a matched pair of keys. The publisher keeps one of them secret forever (the private key) and hands the matching one to the whole world (the public key). They stamp their repository using the private key. Your machine checks that stamp using the public key. The math has a handy property: only the holder of the private key can produce a valid stamp, anyone with the public key can confirm it, and nobody can run the process backward to forge one. It behaves like a signature that's impossible to copy but easy for anyone to recognize.

One signature manages to cover an entire repository through a chain. The repo publishes a small index file (Ubuntu names it InRelease) and signs it. That signed index lists a hash for every larger index it points to. A hash is a short fingerprint calculated from a file's exact bytes, and changing a single byte changes the fingerprint completely. Those larger indexes in turn list a hash for every individual package. So when apt fetches nginx, it checks the package against the hash in the index, the index against the signature, and the signature against the publisher's key. Break any link in that chain and apt stops. One trusted key ends up vouching for everything beneath it.

When you add someone else's repository, you are adding their key to the set your machine trusts. The safer, modern way pins that key to one repository only, using a setting called signed-by.

~/secopslog — bash
$ # add a third-party repo the safe way (Docker on Ubuntu) sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg echo \ "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/ubuntu jammy stable" \ | sudo tee /etc/apt/sources.list.d/docker.list
/etc/apt/sources.list.d/docker.list
deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu jammy stable

The old approach, apt-key, threw every key you ever added into one global pile of trusted keys. A key you imported for a single vendor could then sign packages for anything on the system, including packages pretending to be the official Ubuntu ones. signed-by ends that by tying each key to a single repo, named right there in the sources file. It's the difference between handing a contractor the key to one room and handing them a master key to the whole building.

When the check fails, you see it. Import the wrong key, or none at all, and apt refuses to touch that repository.

~/secopslog — bash
$ sudo apt update
Err:5 https://download.docker.com/linux/ubuntu jammy InRelease The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 7EA0A9C3F273FCD8 W: GPG error: https://download.docker.com/linux/ubuntu jammy InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 7EA0A9C3F273FCD8 E: The repository 'https://download.docker.com/linux/ubuntu jammy InRelease' is not signed. N: Updating from such a repository can't be done securely, and is therefore disabled by default. N: See apt-secure(8) manpage for repository creation and user configuration details.

Notice what happened. apt didn't shrug and install anyway. It disabled the repository. That refusal is the security boundary doing its job, and it's the exact behavior you never want to switch off to make an error go away.

"curl | sudo bash" hands root to a stranger
Two habits cause most supply-chain compromises on Linux hosts. The first is turning off signature checking, or adding an untrusted key, so the package manager will install anything. The second is the install line you see everywhere, curl https://site/install.sh | sudo bash, which downloads a script and runs it immediately as root (the all-powerful administrator account) with no signature and no chance to read it first. If that web address is ever hijacked, you have run the attacker's code with full control of the box. Download the script, read it, then run it, and prefer a signed package from a repo you already trust.

Patching is the boring control that stops most breaches

Most successful attacks aren't clever. They reuse a known hole that a fix already exists for, against a machine nobody updated in time. These holes get logged in public with an identifier called a CVE (Common Vulnerabilities and Exposures), so defenders and attackers read the very same list. The gap between the day a patch ships and the day you install it is the window an attacker gets to work in. Keeping packages current closes that window, and per minute spent it is probably the highest-value security work you can do.

You don't have to do it by hand. Both families can apply security updates on their own: unattended-upgrades on Debian and Ubuntu, dnf-automatic on Fedora and RHEL.

~/secopslog — bash
$ sudo apt install unattended-upgrades sudo dpkg-reconfigure -plow unattended-upgrades # answer "Yes"
/etc/apt/apt.conf.d/50unattended-upgrades
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
// "${distro_id}:${distro_codename}-updates";
};
Unattended-Upgrade::Automatic-Reboot "false";
/etc/apt/apt.conf.d/20auto-upgrades
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";

By default this pulls only from the security origin, so you get fixes without surprise feature changes, and it leaves the reboot decision to you. On an internet-facing box, an unpatched service is the single most common way it gets taken over, so leaving this switched on is usually the right call.

What apt checks before a package touches disk
1apt update
download the repo's signed index (InRelease)
2verify signature
index must be signed by the trusted key
3resolve + fetch
pick versions, download the .deb files
4check hashes
each package must match the signed index
5install
unpack only what verified end to end
Break any link and apt refuses to install.
Quick check
01You add a company's third-party repository and import its signing key so you can install their one tool. In security terms, what have you actually agreed to trust?
Incorrect — The key isn't scoped to one package; it validates anything that repo serves, then and later.
Correct — Adding a repo and its key is standing trust in that publisher for as long as it stays configured.
Incorrect — A signature proves origin and integrity, not just transfer errors; a plain checksum handles corruption.
Incorrect — Trust is anchored to the signing key and what it signs, not to the URL.
02New users often confuse 'apt update' with 'apt upgrade'. What does 'apt update' actually do?
Correct — update downloads a fresh package index; upgrade is the separate step that installs newer versions.
Incorrect — that is what 'apt upgrade' does, not update.
Incorrect — a release upgrade is a different operation; update only refreshes the index.
Incorrect — that is closer to 'apt autoremove'; update changes nothing on disk.
03On a server you are investigating, 'rpm -V openssh-server' prints 'S.5....T.' next to /usr/sbin/sshd, the SSH server binary rather than a config file. What should you conclude?
Incorrect — that is expected for a config file you edited, but not for a program the package shipped.
Incorrect — permission changes appear as a different flag; S, 5, and T mean size, content, and timestamp changed.
Correct — for a plain binary this mismatch is exactly the tampering signature that rpm -V exists to catch.
Incorrect — rpm -V checks every installed file, binaries included.

One habit to carry onto every server you inherit. Look at whose repositories it trusts, because that list is whose code can arrive as an update forever. An unfamiliar repo in there is a tidy way for a backdoor to keep itself supplied.

~/secopslog — bash
$ ls /etc/apt/sources.list.d/
docker.list google-chrome.list

If you don't recognize a source in that directory, find out who added it and why before you run the next upgrade.

Try this

Work through “Patching is the boring control that stops most breaches” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: "curl | sudo bash" hands root to a stranger. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related