Managing Repositories

A repository (or “repo”) is a server-hosted collection of software packages, along with an index describing what’s available, that your package manager consults whenever you install, remove, or upgrade software. When you run apt install nginx or dnf install nginx, the package manager isn’t searching the internet at random — it looks up nginx in the index of every repository your system currently trusts. Managing repositories means controlling that list: which sources your system trusts, how it verifies they haven’t been tampered with, and how it keeps its local index of them up to date.

Overview: How Repositories Work

A Debian/Ubuntu repository is really just a directory tree on a web server (or local mirror) containing .deb package files plus metadata: a Packages file (often compressed as Packages.gz) listing every package with its version, dependencies, and checksum, and a Release file summarizing the whole repo, which is cryptographically signed. When you run sudo apt update, apt does not download any packages — it contacts every configured repository, downloads the signed InRelease (or Release plus Release.gpg) file and the Packages index, verifies the signature against a GPG key it already trusts, and caches the result under /var/lib/apt/lists/. Only after that succeeds does apt know what versions of what packages exist and where to fetch them — which is why apt update must run after adding, removing, or editing a repository before apt install/apt upgrade will see the change.

Where repositories are configured

The classic single file is /etc/apt/sources.list, with each enabled repo on one deb line. Modern systems split third-party repos into their own files under /etc/apt/sources.list.d/, one .list file per repo (or, on newer releases, a structured .sources “deb822” file). apt reads and merges every file in both locations. This split matters in practice: it lets you add, disable, or delete one vendor’s repository — say, Docker’s — by touching a single file, without risking a typo in the distro’s own sources.list.

Trust: why GPG signing matters

Anyone can stand up a web server and call it a repository. What stops apt from installing malicious packages from a spoofed or compromised mirror is that every official repository’s Release file is signed with a GPG private key, and apt refuses to use a repo whose signature it can’t verify against a key it trusts. Historically, trusted keys were imported system-wide with apt-key add, which placed the key in /etc/apt/trusted.gpg — but that key was then implicitly trusted for every repository on the system, not just the one it belonged to. apt-key is deprecated on modern Debian/Ubuntu specifically because of that scope problem. The modern approach is a keyring file (a binary GPG key produced with gpg --dearmor) stored under /etc/apt/keyrings/ or /usr/share/keyrings/, referenced explicitly by a signed-by= option on that one repository’s deb line, so a compromised third-party key can only ever authenticate packages from the repository it’s scoped to.

PPAs (Personal Package Archives)

Ubuntu also supports PPAs: community- or vendor-maintained repositories hosted on Launchpad, added with add-apt-repository (from the software-properties-common package). A PPA is convenient — one command adds the repo, imports its signing key, and writes the sources file — but it is still a repository you’re choosing to trust, maintained by whoever owns it, not by Canonical. Packages from a PPA can overwrite or conflict with your distro’s own packages, and there’s no guarantee a maintainer keeps publishing updates or security fixes.

RHEL/Fedora: yum/dnf repos

The RPM-based world (RHEL, Fedora, CentOS Stream, Rocky, Alma) uses the same idea with different files: each repository is described by a .repo file in /etc/yum.repos.d/, containing a baseurl, a gpgkey URL, and gpgcheck=1 to enforce signature verification. dnf (the successor to yum, and the default on Fedora and RHEL 8+) reads every .repo file the same way apt reads every file in sources.list.d/. dnf config-manager --add-repo is the RPM-world equivalent of add-apt-repository.

Syntax

deb [options] URI distribution component1 component2 ...
sudo add-apt-repository [--remove] [--yes] ppa:OWNER/NAME
sudo dnf config-manager --add-repo REPO_URL
Field Meaning
deb / deb-src deb lines carry compiled binary packages; deb-src lines point to matching source packages (rarely needed unless building from source).
[options] Optional bracketed key=value pairs, most commonly arch= (limit to one CPU architecture) and signed-by= (path to the GPG keyring that must verify this repo).
URI The base URL of the repository, e.g. https://download.docker.com/linux/ubuntu.
distribution The release codename this line applies to, e.g. jammy or noble — must match your actual Ubuntu/Debian version.
component(s) One or more named subsets, e.g. Ubuntu’s main restricted universe multiverse, or a vendor’s single stable.
--remove (add-apt-repository) Removes a previously added PPA/line instead of adding it.
--yes (add-apt-repository) Skips the confirmation prompt — useful in scripts.

Examples

Example 1: Inspecting the repositories already configured

cat /etc/apt/sources.list
ls /etc/apt/sources.list.d/
deb http://archive.ubuntu.com/ubuntu jammy main restricted universe multiverse
deb http://archive.ubuntu.com/ubuntu jammy-updates main restricted universe multiverse
deb http://security.ubuntu.com/ubuntu jammy-security main restricted universe multiverse

docker.list  google-chrome.list

The main file lists Ubuntu’s own archive and security repos, one per release pocket (base, updates, security). The two files in sources.list.d/ are third-party repos — Docker and Google Chrome — each added separately in its own file so it can be managed independently.

Example 2: Checking where a package would actually come from

sudo apt policy
Package files:
 500 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 Packages
     release v=22.04,o=Ubuntu,a=jammy-updates,n=jammy,l=Ubuntu,c=main,b=amd64
     origin archive.ubuntu.com
Pinned packages:

apt policy (equivalently apt-cache policy) prints every enabled repository with its priority (the 500) — the number apt uses to decide which source wins when a package is available from more than one repo. Run it for a specific package, e.g. apt policy nginx, to see the installed version, every candidate version, and exactly which repo each would come from.

Example 3: Adding a PPA

sudo apt install -y software-properties-common
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt update
Repository: 'deb https://ppa.launchpadcontent.net/deadsnakes/ppa/ubuntu/ jammy main'
Adding repository.
Adding deb entry to /etc/apt/sources.list.d/deadsnakes-ubuntu-ppa-jammy.list
Adding key to /etc/apt/trusted.gpg.d/deadsnakes-ubuntu-ppa.gpg with fingerprint ...
Get:6 https://ppa.launchpadcontent.net/deadsnakes/ppa/ubuntu jammy InRelease [17.5 kB]
Fetched 17.5 kB in 1s (12.9 kB/s)
Reading package lists... Done

add-apt-repository does three things in one command: writes a new .list file under sources.list.d/, downloads and imports the PPA’s signing key, and — on Ubuntu — automatically runs apt update for you, which is why packages from the well-known deadsnakes PPA are installable immediately afterward.

Example 4: Adding a vendor repository manually with a scoped key

sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
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
sudo chmod a+r /etc/apt/keyrings/docker.gpg

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update
Get:5 https://download.docker.com/linux/ubuntu jammy InRelease [48.9 kB]
Get:6 https://download.docker.com/linux/ubuntu jammy/stable amd64 Packages [15.2 kB]
Reading package lists... Done

This is the pattern to use whenever a vendor doesn’t ship a PPA. The GPG key is downloaded, converted from ASCII-armored to binary with gpg --dearmor, and saved under /etc/apt/keyrings/ rather than trusted system-wide. The deb line then references that exact keyring with signed-by=, so this key can only ever authenticate this repository. $(dpkg --print-architecture) and $(. /etc/os-release && echo "$VERSION_CODENAME") auto-detect the machine’s CPU architecture and release codename, so the same command works unmodified across supported Ubuntu versions.

Example 5: Removing a repository

sudo add-apt-repository --remove ppa:deadsnakes/ppa
sudo rm /etc/apt/sources.list.d/docker.list
sudo apt update
Removing deb entry from /etc/apt/sources.list.d/deadsnakes-ubuntu-ppa-jammy.list
Reading package lists... Done

add-apt-repository --remove knows how to cleanly reverse what it added. A manually-added repo like Docker’s has no such helper — since only one file was created, deleting it (and, if desired, the keyring under /etc/apt/keyrings/) is enough. Either way, always finish with apt update so apt’s cached index no longer references the removed source.

Example 6: The RHEL/Fedora equivalent

sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
sudo dnf install -y docker-ce
Adding repo from: https://download.docker.com/linux/fedora/docker-ce.repo
Installed:
  docker-ce-3:24.0.7-1.fc39.x86_64

dnf config-manager --add-repo downloads a ready-made .repo file (already containing baseurl and gpgkey) into /etc/yum.repos.d/. Unlike apt, dnf doesn’t need a separate “update” step before installing — it refreshes metadata for a repo automatically the first time it’s used.

How It Works Step by Step

When you run sudo apt update, roughly this happens, in order:

  1. apt reads every enabled line in /etc/apt/sources.list and every file in /etc/apt/sources.list.d/, building the list of repositories to contact.
  2. For each repository, it requests the InRelease file (or separately, Release plus Release.gpg) over HTTP or HTTPS.
  3. It checks the file’s GPG signature against the keyring specified by that repo’s signed-by= option (or, for repos without one, the system’s general trusted keyrings). If the signature doesn’t match a trusted key, apt refuses the whole repository and reports an error rather than silently skipping it.
  4. Once trusted, apt reads the checksums and file list inside Release and downloads the corresponding Packages (or Packages.gz) index for each component/architecture it needs.
  5. Each downloaded index is checksum-verified against the value recorded in the signed Release file — this is what stops a compromised mirror from serving a tampered package list even over a “valid” connection.
  6. Everything is cached under /var/lib/apt/lists/, one file per repository/component, and combined in memory into apt’s view of every package available, from every source, at every version.
  7. Only now do apt install, apt search, and apt policy have anything to work with — they read this cache, not the network, so they’re instant until you run update again.

Common Mistakes

Mistake 1: Forgetting to run apt update after changing repositories

Adding, removing, or editing a repo only changes apt’s configuration — it does not refresh the cached index in /var/lib/apt/lists/. Installing immediately afterward fails, because as far as apt’s cache is concerned the new repo doesn’t exist yet:

sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt install python3.12
E: Unable to locate package python3.12

Always run apt update between changing a repository and installing from it (note add-apt-repository on Ubuntu normally triggers an update automatically, but a manual edit to a .list/.sources file will not):

sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt update
sudo apt install python3.12

Mistake 2: Trusting a key system-wide with apt-key add

It’s tempting to follow old tutorials that pipe a vendor’s key straight into apt-key:

curl -fsSL https://example.com/key.gpg | sudo apt-key add -
Warning: apt-key is deprecated. Manage keyring files in trusted.gpg.d instead (see apt-key(8))

This works, but the key becomes trusted for every repository on the system, not just the vendor’s — a compromised or malicious key added this way could sign packages claiming to come from anywhere. Scope the key to the one repo that needs it instead:

curl -fsSL https://example.com/key.gpg | sudo gpg --dearmor -o /etc/apt/keyrings/example.gpg

…then reference /etc/apt/keyrings/example.gpg with signed-by= on that repository’s deb line only, as shown in Example 4.

Mistake 3: Piping an install script straight into a root shell

Many vendors advertise a one-line install that adds their repo and installs their software in one step:

curl -fsSL https://get.example.com/install.sh | sudo bash

You’re running whatever that URL happens to return, as root, without reading it first — if the server is compromised, DNS is hijacked, or the script’s behavior simply changes between when it was recommended and when you run it, you have no chance to notice. Download it, read it, then run it:

curl -fsSL https://get.example.com/install.sh -o install-example.sh
less install-example.sh
sudo bash install-example.sh

Mistake 4: Leaving a stale codename after a distro upgrade

Third-party .list files hardcode a release codename (e.g. jammy). After you upgrade the OS with do-release-upgrade, any leftover third-party file still pointing at the old codename starts failing, because vendors eventually stop publishing to end-of-life codenames:

Err:5 https://download.docker.com/linux/ubuntu impish InRelease
  404  Not Found [IP: 143.204.68.100 443]
E: The repository 'https://download.docker.com/linux/ubuntu impish InRelease' does not have a Release file.

After every major OS upgrade, check /etc/apt/sources.list.d/ for files referencing the old codename and either update or remove them.

Best Practices

  • Only add repositories from sources you actually trust — every repo you add is code that can run as root on your machine via package maintainer scripts.
  • Prefer a scoped keyring with signed-by= over the deprecated apt-key add, so a key only authenticates the one repository it belongs to.
  • Always run sudo apt update (or let add-apt-repository do it) immediately after any repository change, before installing.
  • Keep third-party repos in their own file under /etc/apt/sources.list.d/ — never hand-edit the distro’s own /etc/apt/sources.list to add unrelated vendors.
  • Remove repositories you no longer need (sudo add-apt-repository --remove ppa:owner/name, or delete the .list/.repo file) instead of leaving them enabled indefinitely.
  • After a major OS upgrade, audit every third-party repo file for a stale codename before trusting apt update to succeed silently.
  • Use apt policy (or apt-cache policy) to see exactly which repository and priority a package would install from before running apt install.
  • On RHEL/Fedora, check that gpgcheck=1 is set in every .repo file — a repo with gpgcheck=0 installs unsigned, unverified packages.

Practice Exercises

  1. List every repository currently configured on your system (both /etc/apt/sources.list and everything under /etc/apt/sources.list.d/), and for each third-party one, identify which keyring file it’s signed by.
  2. Add a PPA of your choice (for example ppa:deadsnakes/ppa on Ubuntu), confirm with apt policy python3.12 that apt now sees a candidate from it, then remove the PPA again and confirm it’s gone from apt policy‘s output.
  3. Pick a real third-party vendor repo (for example Docker’s, from Example 4) and add it manually using a scoped signed-by= keyring rather than any install script — then explain, in your own words, why that’s safer than apt-key add.

Summary

  • A repository is a server hosting packages plus a signed index; apt update fetches and verifies that index — it never installs packages by itself.
  • APT repos live in /etc/apt/sources.list and /etc/apt/sources.list.d/*.list; RPM repos live in /etc/yum.repos.d/*.repo.
  • Trust comes from GPG signatures; the modern, safe method is a scoped keyring referenced with signed-by=, not the deprecated system-wide apt-key add.
  • add-apt-repository adds and removes Ubuntu PPAs; dnf config-manager --add-repo is the RPM-world equivalent.
  • Always run apt update after changing repositories, and remove repositories you no longer need.