You have spent four modules typing sudo apt install without asking where that software comes from, who guarantees nobody tampered with it along the way, or what happens if one night Tramontana's database engine jumps from version 15 to 16 and the application stops starting. Now that you know exactly what sudo lets you do, it is time to understand what you do with it most of the time: install, update and pin software. This lesson covers Debian and Ubuntu's two layers, the cryptographic signing of repositories, version pinning and the uncomfortable debate about whether a server should reboot itself after a security update.

Contents

  1. What a package manager solves
  2. Anatomy of a .deb and a comparison with .rpm
  3. The two layers: dpkg versus apt
  4. apt day to day
  5. dpkg and file archaeology
  6. Repositories: deb822, components, pockets and keys
  7. Pinning versions: hold and pinning
  8. Unattended upgrades, the reboot and cleaning the cache
  9. Snap, Flatpak and AppImage versus .deb
  10. Compiling from source when there is no alternative
  11. Equivalents with dnf on RHEL
  12. Tramontana case: dependencies, a pinned version and an audit

  1. What a package manager solves

Installing software "by hand" — downloading a .tar.gz, compiling it and copying it into /usr/local — looks harmless until the first security update arrives. A package manager solves four problems that have no reasonable manual solution:

  • Dependencies: it knows your application needs libssl3t64 and that another twenty programs use that library, so it installs it once and shares it.
  • Security updates: one command updates everything installed. With hand-compiled software, the day a critical CVE comes out you will have to remember it yourself.
  • Integrity and provenance: every package comes signed and its checksum verified. You know that what gets installed is what the maintainer published.
  • Clean uninstallation: the manager knows exactly which files each package brought. A make install does not even leave the list.

An operational rule: compiling is the last resort, and when you have to, do it in /usr/local so as not to tread on what the manager administers.

  1. Anatomy of a .deb and a comparison with .rpm

A .deb is an ar archive with three pieces: debian-binary (the format version), control.tar.zst (metadata and scripts) and data.tar.zst (the files that get installed).

$ apt-get download tree >/dev/null && dpkg-deb --info tree_*.deb | sed -n '2,7p'
 Package: tree
 Version: 2.1.1-2ubuntu3
 Depends: libc6 (>= 2.38)
 Installed-Size: 133
 Maintainer: Ubuntu Developers <[email protected]>
 Description: displays an indented directory listing

The inter-package relationship fields are the ones doing all the work:

Field Meaning
Depends Essential: without it, the package does not work
Recommends / Suggests Dispensable but installed anyway (--no-install-recommends) / purely informative
Conflicts / Breaks / Replaces / Provides Cannot coexist / replaces another one or supplies a generic capability

Inside control there are also maintainer scripts — preinst, postinst, prerm, postrm — which run as root at each phase. That is exactly why adding a third-party repository is a security decision and not a convenience: installing a package means running somebody else's code as root.

Aspect .deb (Debian/Ubuntu) .rpm (RHEL/Fedora/SUSE)
Low / high level dpkg / apt rpm / dnf (formerly yum)
Metadata A control file A binary header and a .spec at build time
Configuration conffiles, asks when merging .rpmnew / .rpmsave
Database /var/lib/dpkg/status /var/lib/rpm

  1. The two layers: dpkg versus apt

flowchart LR
    R[Repositories<br/>archive.ubuntu.com] -->|downloads and verifies signature| A
    A[apt: resolves dependencies] -->|invokes| D[dpkg: installs ONE file]
    L[A .deb downloaded by hand] --> D
    D --> S[(/var/lib/dpkg)]
    D --> F[Files on the system]

dpkg knows how to install one .deb file and keep the books on what is installed; it knows nothing about repositories and downloads no dependencies. apt knows how to talk to the repositories, resolve the dependency graph, verify signatures and decide the order, and then it delegates to dpkg.

You need to… Command
Install by name from the repository apt install package
Install a local .deb with dependencies apt install ./file.deb
Install a local .deb without resolving anything dpkg -i file.deb (then apt -f install)
File → package / package → files dpkg -S /path / dpkg -L package

A note: apt is the interface for humans and apt-get/apt-cache are the stable ones for scripts. In a script use apt-get: apt warns that its interface may change between versions.

  1. apt day to day

sudo apt update        # refreshes the available LIST. It updates NOTHING installed
sudo apt upgrade       # updates what is installed without removing or installing new packages
sudo apt full-upgrade  # the same, but it MAY remove packages to resolve the change

The classic confusion: update only downloads the indexes. Without it, apt works from an old snapshot of the repository and will not see the security update published this morning. The difference between upgrade and full-upgrade matters on a server: upgrade is conservative and would rather touch nothing than delete something; full-upgrade accepts removals and is what you need for a distribution version change.

sudo apt install nginx=1.24.0-2ubuntu7 # a specific version from the repository
sudo apt remove nginx                  # deletes the binaries, KEEPS the configuration in /etc
sudo apt purge nginx                   # also deletes the configuration
sudo apt autoremove --purge            # removes dependencies nobody needs any more

remove versus purge is a real decision: remove lets you reinstall and get your configuration back; purge leaves the system clean. On a server that is documented, purge stops a ghost /etc/whatever turning up in a year's time that nobody knows the origin of.

Querying:

apt search 'web server'            # searches the name and the description
apt show nginx                     # description, dependencies, size, origin
apt list --installed | wc -l       # how many packages are installed
apt list --upgradable              # what is pending an update
apt depends nginx; apt rdepends libssl3t64   # direct and reverse dependencies
apt download tree                  # downloads the .deb without installing it

And the option that saves the most disk on a server is sudo apt install --no-install-recommends nginx. The Recommends drag in extras designed for the desktop (documentation, graphical clients, complete mail servers). On a server, every extra package is attack surface and occupied disk. You can make it permanent:

# /etc/apt/apt.conf.d/99no-recommends
APT::Install-Recommends "false";
APT::Install-Suggests "false";

  1. dpkg and file archaeology

$ dpkg -l | grep -c '^ii'   # 712 packages correctly installed
$ dpkg -S /usr/bin/ss       # which package did this file come from?
iproute2: /usr/bin/ss

The states in dpkg -l are read as two letters: the first is the desired state and the second the actual one. ii is "installed and installed"; rc is "removed but configuration files remain" (candidates for purge); iU or iF indicate a half-finished installation, which is fixed with sudo dpkg --configure -a (it finishes interrupted configurations) and sudo apt -f install (it repairs broken dependencies).

To look for a file in packages you do not have installed:

$ sudo apt install apt-file && sudo apt-file update && apt-file search /usr/bin/mkfs.xfs
xfsprogs: /usr/bin/mkfs.xfs

  1. Repositories: deb822, components, pockets and keys

Ubuntu 24.04 abandoned the one-line format of /etc/apt/sources.list and uses deb822, which is far more readable, in /etc/apt/sources.list.d/*.sources:

# /etc/apt/sources.list.d/ubuntu.sources
Types: deb
URIs: http://es.archive.ubuntu.com/ubuntu/
Suites: noble noble-updates noble-backports
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg

The file carries a second, identical block with URIs: http://security.ubuntu.com/ubuntu/ and Suites: noble-security, which is the one that brings the patches.

Component What it contains Support
main / restricted Free software maintained by Canonical / proprietary drivers Yes, with patches
universe / multiverse Maintained by the community / with licence restrictions "Best effort" (Ubuntu Pro extends it) / no
Pocket What it is In production?
noble The version frozen on release day Yes
noble-security Security patches Yes, always
noble-updates Fixes for non-critical bugs Yes, the usual choice
noble-backports Newer versions brought back from later releases Only occasionally

The signature: why apt-key is obsolete

Every repository signs its Release index with a GPG key. In the past all the keys went into a global keyring (apt-key add), which meant that any third-party repository could sign any package, including a fake openssh-server that replaced the official one. That is why apt-key is obsolete and has been removed in 24.04. Today the key is stored in a file and tied to that repository with Signed-By:.

sudo install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://example.test/key.asc | sudo tee /etc/apt/keyrings/example.asc >/dev/null
# /etc/apt/sources.list.d/example.sources
Types: deb
URIs: https://example.test/apt/
Suites: noble
Components: main
Signed-By: /etc/apt/keyrings/example.asc

Adding a third-party repository is a security decision. You are authorising a third party to run code as root on your server every time you update, and to replace packages if you do not limit their scope. Before adding it: check who publishes it, use HTTPS, tie the key with Signed-By, restrict Components to what is needed and, if you really care, apply pinning so that it can only supply the packages you intend. In a corporate environment this decision must be validated by the security officer.

Ubuntu's PPAs are Launchpad repositories that sudo add-apt-repository ppa:user/project adds along with their key. Convenient on the desktop; in production, every PPA is an individual maintainer you become dependent on.

  1. Pinning versions: hold and pinning

There is software that cannot update itself. Tramontana's database engine is the textbook case: a major version jump means migrating the data format, and doing that at 06:00 unsupervised means an application that is down and a complicated rollback.

The simple way, with dpkg:

$ sudo apt-mark hold postgresql-16 postgresql-client-16
postgresql-16 set on hold.
$ apt-mark showhold
postgresql-16
postgresql-client-16
$ sudo apt-mark unhold postgresql-16   # when updating: by hand and with a window

A package on hold is not touched by upgrade, nor by full-upgrade, nor by unattended-upgrades.

The more controlled way, APT pinning, lets you decide which origin each thing comes from:

# /etc/apt/preferences.d/99-tramontana-bd — the DB stays on the 16 series
Package: postgresql-16 postgresql-client-16 postgresql-common
Pin: version 16.*
Pin-Priority: 1001
Priority Effect
< 0 / 100 Never installed / only if nothing is installed
500 / 990 Normal for a repository / that of the target release
> 1000 Installed even if it means downgrading
$ apt-cache policy postgresql-16
postgresql-16:
  Installed: 16.3-1.pgdg24.04+1
  Candidate: 16.3-1.pgdg24.04+1
     17.0-1.pgdg24.04+1 500     # it exists, but it does not win
 *** 16.3-1.pgdg24.04+1 1001

apt-cache policy is the tool that confirms your pinning does what you think. Always check it: a badly written pin gives no error, it simply does not apply.

  1. Unattended upgrades, the reboot and cleaning the cache

A server without security patches is only a matter of time. Ubuntu includes unattended-upgrades, which by default installs only what comes from the -security pocket:

# /etc/apt/apt.conf.d/50unattended-upgrades  (the relevant fragment)
Unattended-Upgrade::Allowed-Origins {
        "${distro_id}:${distro_codename}-security";
};
Unattended-Upgrade::Package-Blacklist { "postgresql-16"; };
Unattended-Upgrade::Mail "[email protected]";
Unattended-Upgrade::MailReport "on-change";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Automatic-Reboot "false";
sudo unattended-upgrade --dry-run --debug   # a simulation before acting, as always

The automatic-reboot debate has no universal answer. A kernel or glibc update has no effect until you restart the affected processes or the machine:

$ cat /var/run/reboot-required.pkgs 2>/dev/null
linux-image-6.8.0-45-generic
$ sudo needrestart -b        # which services are using already-updated libraries
NEEDRESTART-SVC: tramontana.service
Position In favour Against
Automatic-Reboot "true" The patch takes effect without depending on anybody An unannounced outage; if the app does not start on its own, a long one
"false" + a warning It is rebooted in an agreed window If nobody reads the warning, the patch protects nothing

For Tramontana, with a single server and no high availability (which is the subject of 07-07), the reasonable decision is false plus an alert Marta receives, plus a written commitment to a weekly window. And the indispensable habit: a VM snapshot before a big update, which on a real server is an LVM or hypervisor snapshot (05-04).

And the cleanup, which on a server with /var on its own partition — like the one you partitioned in 01-04 — prevents a full disk: du -sh /var/cache/apt/archives usually gives hundreds of megabytes, which sudo apt clean deletes entirely and apt autoclean only for what has already been withdrawn from the repository; old kernels are cleaned by apt autoremove --purge.

  1. Snap, Flatpak and AppImage versus .deb

Format Dependencies Updating On a server
.deb Shared with the system apt, controlled The default option
snap Bundled Automatic and hard to prevent Only if there is no .deb: the auto-update disrupts change windows
flatpak Shared runtimes Manual or scheduled Designed for the desktop
AppImage Everything inside one file None: you do it yourself A standalone tool; nobody patches it for you

Ubuntu ships snapd as standard, and some pieces (such as lxd or certbot) are distributed that way. What you need to know on a server: snaps update on their own and their pace can be postponed but not cancelled, which clashes head-on with a controlled-change policy.

snap list                              # what is installed
sudo snap refresh --hold=24h certbot   # postpone, not cancel

  1. Compiling from source when there is no alternative

sudo apt install build-essential    # gcc, make, libc6-dev, dpkg-dev
tar -tzvf tool-2.4.tar.gz | head    # inspect BEFORE extracting, as always
tar -xzf tool-2.4.tar.gz && cd tool-2.4
./configure --prefix=/usr/local && make -j"$(nproc)" && sudo make install

--prefix=/usr/local is not a detail: the FHS from 01-06 reserves that tree for what the administrator installs, precisely so that it never collides with what dpkg manages in /usr. And since make install leaves no record, checkinstall exists, wrapping the installation in a real .deb:

$ sudo checkinstall --pkgname=tool --pkgversion=2.4 --default
$ dpkg -l tool | tail -1
ii  tool  2.4-1  amd64  Package created with checkinstall

Now it uninstalls cleanly with apt purge tool. Even so, there is still nobody to warn you about its security flaws: that watch becomes yours.

  1. Equivalents with dnf on RHEL

Picking up the families from 01-03, this table lets you work on Rocky, Alma or RHEL without learning anything again:

Task Debian/Ubuntu RHEL/Fedora
Refresh / update apt update / apt upgrade dnf check-update / dnf upgrade
Install / remove apt install / apt purge dnf install / dnf remove
Search / detail apt search / apt show dnf search / dnf info
File→package / package→files dpkg -S / dpkg -L rpm -qf / rpm -ql
Pin a version / history apt-mark hold, history.log dnf versionlock, dnf history

The most striking practical difference: dnf history undo reverses an entire transaction, something APT does not offer. On Debian/Ubuntu you go back by reinstalling specific versions worked out from history.log.

  1. Tramontana case: dependencies, a pinned version and an audit

First, the server's dependencies, with no recommends and with verification:

sudo apt update && sudo apt install --no-install-recommends -y \
     nginx postgresql-16 postgresql-client-16 rsync curl jq acl logrotate

Second, we pin the database so that no unattended upgrade raises its major version:

sudo cp -a /etc/apt/apt.conf.d/50unattended-upgrades{,.bak-$(date +%F)}
sudo apt-mark hold postgresql-16 postgresql-client-16

And we add the explicit pinning in /etc/apt/preferences.d/99-tramontana-bd that you saw in section 7, plus the blacklist in 50unattended-upgrades. A mandatory verification:

$ apt-cache policy postgresql-16 | head -3
postgresql-16:
  Installed: 16.3-0ubuntu0.24.04.1
  Candidate: 16.3-0ubuntu0.24.04.1   # identical: nothing pending that could slip through

Third, Marta's question this morning: "what was updated last night and why?".

$ grep -A3 '^Start-Date: 2026-08-18' /var/log/apt/history.log
Start-Date: 2026-08-18  06:12:33
Commandline: /usr/bin/unattended-upgrade
Upgrade: libssl3t64:amd64 (3.0.13-0ubuntu3.2, 3.0.13-0ubuntu3.4), openssl:amd64 (...)
End-Date: 2026-08-18  06:12:51
$ apt changelog openssl 2>/dev/null | head -2
openssl (3.0.13-0ubuntu3.4) noble-security; urgency=medium
  * SECURITY UPDATE: denial of service in TLS session handling

The answer for Marta, with the usual structure — what it protects and what it does not protect —: "Last night OpenSSL was updated because of a security flaw that allowed the TLS service to be brought down. It protects the encryption of incoming connections. It protects nothing else, and it will not take full effect until we restart the services that already had the library loaded; needrestart says tramontana.service is one of them. I propose restarting it in Thursday's window. The database is still pinned to the 16 series and has not been touched." And into the HISTORY:

sudo tee -a /opt/tramontana/HISTORY >/dev/null <<'END'
2026-08-18  Packages (operator)
  - hold + pin of postgresql-16 (16.* series); unattended-upgrades only -security
  - 06:12 automatic update of openssl (TLS CVE). Service restart still pending.
END

Common Mistakes and Tips

  • Believing that apt update updates anything. It only refreshes the list; the correct pair is apt update && apt upgrade, and the && is not decorative: if the update fails, upgrading with old indexes is worse than not upgrading.
  • Using apt in scripts instead of apt-get with DEBIAN_FRONTEND=noninteractive, or plain dpkg -i, which installs without dependencies and leaves the system in the iF state (use apt install ./file.deb).
  • Adding a repository with apt-key. Removed in 24.04 and insecure by design: the key goes in /etc/apt/keyrings/ and Signed-By: in the .sources.
  • Forgetting the -security pocket. Without it the server goes unpatched, and it gives no error at all. And do not update without a snapshot or a window: the day something breaks, the difference between 5 minutes and 5 hours is having taken the snapshot.
  • Leaving a hold in place and forgetting it. A held package does not receive security patches either: review apt-mark showhold every month and write down who decided each hold and until when.
  • Tip: keep apt-mark showmanual > packages.txt alongside your backups. Rebuilding a server from that list is a matter of minutes.

Exercises

  1. Simulate before acting. Find out which packages would be updated right now and how much disk they would take, without installing anything. Then check whether any of them requires a system reboot.
  2. A third-party repository set up properly. Describe the complete, safe steps to add a fictitious repository https://apt.tramontana.example/ (suite noble, component main) that should only supply the tramontana-agent package, and state how you would verify it cannot replace system packages.
  3. The forensics of an update. A colleague says "somebody installed nginx by hand on Tuesday". Demonstrate with commands when it was installed, whether it came from a repository or from a standalone .deb, and which configuration files it contributes.

Solutions

1.

$ sudo apt update >/dev/null && apt list --upgradable
libssl3t64/noble-security 3.0.13-0ubuntu3.4 amd64 [upgradable from: 3.0.13-0ubuntu3.2]
openssl/noble-security 3.0.13-0ubuntu3.4 amd64 [upgradable from: 3.0.13-0ubuntu3.2]
$ sudo apt-get --simulate upgrade | tail -1
Conf openssl (3.0.13-0ubuntu3.4 ...)
$ sudo apt-get --assume-no upgrade | grep -i space
After this operation, 0 B of additional disk space will be used.

--simulate (or -s) is APT's --dry-run, and it fits the course's convention of simulating before acting. For the reboot: [ -f /var/run/reboot-required ] && cat /var/run/reboot-required.pkgs || echo "No reboot required".

2. The steps, in order:

sudo install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://apt.tramontana.example/key.asc | sudo tee /etc/apt/keyrings/tramontana.asc >/dev/null
sudo chmod 0644 /etc/apt/keyrings/tramontana.asc
# /etc/apt/sources.list.d/tramontana.sources
Types: deb
URIs: https://apt.tramontana.example/
Suites: noble
Components: main
Architectures: amd64
Signed-By: /etc/apt/keyrings/tramontana.asc
# /etc/apt/preferences.d/99-repo-tramontana
Package: *
Pin: origin apt.tramontana.example
Pin-Priority: 1
# ...except the intended package:
Package: tramontana-agent
Pin: origin apt.tramontana.example
Pin-Priority: 700

Verification: sudo apt update must give no signature warnings, and apt-cache policy openssl must still show noble-security as the candidate's origin. That priority-1 pin is the safety net that stops a third-party repository hijacking a system package.

3.

$ grep -B1 -A2 'Install:.*nginx' /var/log/apt/history.log
Start-Date: 2026-08-13  11:47:02
Commandline: apt install nginx
Requested-By: luis (1001)
Install: nginx:amd64 (1.24.0-2ubuntu7.1), nginx-common:amd64 (..., automatic)
$ apt-cache policy nginx | sed -n '5p'
        500 http://es.archive.ubuntu.com/ubuntu noble-updates/main amd64 Packages
$ dpkg-query -W -f='${Conffiles}\n' nginx-common | head -1
 /etc/nginx/nginx.conf 8b1a9953c4611296a827abf8c47804d7

It came from the official repository (noble-updates), luis asked for it on 13 August with apt install, and it contributes conffiles that apt will protect in future updates. Note the Requested-By field: history.log records the real UID behind the sudo, which links directly to the traceability of the previous lesson.

Conclusion

The software on srv-tramontana no longer appears by magic. You know that a package manager solves dependencies, updates, integrity and clean uninstallation, and that compiling is the last resort; you know the anatomy of a .deb — including the maintainer scripts that run as root, which is the underlying reason for all the caution around repositories — and its equivalence with .rpm; you tell the dpkg layer from the apt layer and you know when each one applies; you handle update/upgrade/full-upgrade, remove versus purge, --no-install-recommends, depends/rdepends and the archaeology of dpkg -L, dpkg -S and apt-file.

You understand Ubuntu 24.04's repositories in deb822 format, their components and their pockets, and why the key goes in /etc/apt/keyrings/ with Signed-By: instead of the late apt-key. You know how to pin versions with apt-mark hold and with pinning, verify it with apt-cache policy, and that a forgotten hold leaves a package unpatched. You have configured unattended-upgrades so that it installs security only, warns Marta and does not reboot on its own, and you know how to read /var/run/reboot-required and needrestart. And you can place snap, Flatpak and AppImage, compile into /usr/local with checkinstall when you have to, and translate all of it into dnf.

What remains is the resource everything else depends on and that nobody looks at until it runs out: the disk. /srv/tramontana/backups has been growing for weeks, the 04:20 backup writes every night without anybody having worked out how much fits, and the apt cache you have just cleaned was a symptom, not the cause. In Disk Management you will see the full stack from the physical device to the mount point, MBR and GPT partitions, filesystems compared, /etc/fstab field by field — and why one badly written line there stops the server booting —, the case of the deleted file that does not free up space, and LVM, with which you will add a new disk to the VM and move /srv/tramontana/backups to its own volume without losing a single byte.

Linux Course: From Beginner to System Administrator

Module 1: Introduction to Linux

Module 2: Basic Linux Commands

Module 3: Advanced Command-Line Skills

Module 4: Shell Scripting

Module 5: System Administration

Module 6: Networking and Security

Module 7: Advanced Topics

Module 8: Practical Projects

© Copyright 2026. All rights reserved