Adding a third-party repository safely
apt-key is gone, and Signed-By scopes a vendor's key
Sooner or later a vendor tells you to run four lines to install their software, and one of them
pipes a key into apt-key add. That instruction has been wrong for years and is now impossible:
sudo apt-key add vendor-key.asc
sudo: apt-key: command not found
apt-key carried a deprecation warning for several Debian releases and was removed outright in
apt 3.0, which is what trixie ships. The replacement is not a new command. It is a change in
where a key lives and, more importantly, in what that key is allowed to vouch for.
A repository can offer any package it likes
A repository is not a source of its own packages. It is a source of packages. Nothing stops
a repository publishing a package called curl, or openssh-server, or sudo, and if its
version number is higher than Debian's, apt will prefer it. Adding one repository with one
apt install line grants that vendor the ability to replace any piece of software on the
machine, at the next apt upgrade, without asking again.
Here is a machine that has added a single third-party repository, which happens to publish a
package named curl at version 99.0-1:
apt-cache policy curl | head -6
curl:
Installed: 8.14.1-2+deb13u4
Candidate: 99.0-1
Version table:
99.0-1 500
500 http://127.0.0.1:8081 stable/main all Packages
Read those two lines together. Installed: is Debian's curl, the one running now.
Candidate: is the version apt would move to, and it is the vendor's, from a repository that
was added to install something else entirely. Nothing was installed and nothing warned; the next
apt upgrade would take it.
That is not a flaw in apt; it is what a repository is. apt-key add was removed because it
made the problem worse: it put the vendor's key
in a keyring trusted for every repository on the machine, so that key could then validly sign
anything at all.
The modern shape of a source
Debian 13 ships no /etc/apt/sources.list at all. Sources live in /etc/apt/sources.list.d/ in
the deb822 format, one stanza per repository, including Debian's own:
cat /etc/apt/sources.list.d/debian.sources
Types: deb
URIs: http://deb.debian.org/debian
Suites: trixie trixie-updates
Components: main
Signed-By: /usr/share/keyrings/debian-archive-keyring.pgp
Types: deb
URIs: http://deb.debian.org/debian-security
Suites: trixie-security
Components: main
Signed-By: /usr/share/keyrings/debian-archive-keyring.pgp
Signed-By: is the whole point. It names the one key permitted to sign this repository, which
means a vendor's key vouches for the vendor's repository and nothing else. The old one-line
format supported the same thing with awkward bracket syntax; deb822 makes it a plain field, which
is a good enough reason to prefer it for anything you write by hand.
Adding one
Three steps: fetch the key, save it somewhere scoped, and write a source that points at it.
sudo curl -fsSL https://packages.example.com/vendor-key.asc \
-o /etc/apt/keyrings/example-vendor.asc
/etc/apt/keyrings/ already exists on a stock trixie system and is the right home for keys you
add. It is deliberately not /etc/apt/trusted.gpg.d/, where anything dropped in is trusted for
every repository on the machine, which is the global-trust problem apt-key had.
Modern apt reads an ASCII-armoured key directly, so the gpg --dearmor step in most vendor
instructions is no longer needed and neither is having gnupg installed. A .asc file works as
it downloads:
sudo tee /etc/apt/sources.list.d/example.sources > /dev/null <<'EOF'
Types: deb
URIs: https://packages.example.com
Suites: stable
Components: main
Signed-By: /etc/apt/keyrings/example-vendor.asc
EOF
sudo apt update
If you would rather store the binary form, gpg --dearmor still works and the file conventionally
ends .gpg; Signed-By: accepts either.
What it looks like when the key is missing or wrong
This is the error worth recognising, because the message changed in trixie. apt 3.0 verifies
signatures with Sequoia's sqv rather than gpgv, so the familiar NO_PUBKEY wording is gone:
sudo apt update
Err:1 http://127.0.0.1:8081 stable InRelease
Sub-process /usr/bin/sqv returned an error code (1), error message is: Missing key 7AA298319813EF392C23518252AFAF4A082E547D, which is needed to verify signature.
E: The repository 'http://127.0.0.1:8081 stable InRelease' is not signed.
"Is not signed" overstates it. The repository is signed; you have not told apt which key to
accept, so it will not act on the signature. The fingerprint in the message is the key the
repository was signed with, and checking it against one the vendor publishes over a channel you
trust is the step nearly everybody skips.
The same error with a different tail (Failed to parse keyring) means the file at Signed-By: is
not a key at all. The usual cause is a download that returned an error page which got saved over
the keyring, which is exactly what curl -f exists to prevent. Read the file before assuming the
key is at fault.
Restricting a repository to the packages it is for
Since a repository can offer any package, the safe configuration is to accept only the ones you
added it for. apt preferences do this with two stanzas: refuse everything from that origin,
then permit the package you want.
sudo tee /etc/apt/preferences.d/example-vendor > /dev/null <<'EOF'
Package: *
Pin: release o=ExampleVendor
Pin-Priority: -1
Package: hello-tips
Pin: release o=ExampleVendor
Pin-Priority: 500
EOF
sudo apt update
o= matches the Origin: field the repository declares, which
apt-cache policy will show you.
A priority below zero means "never install this", so the first stanza closes the door and the
second opens it for one package. Afterwards, the curl from earlier is Debian's again:
apt-cache policy curl | head -3
curl:
Installed: 8.14.1-2+deb13u4
Candidate: 8.14.1-2+deb13u4
while the package you wanted is still available:
apt-cache policy hello-tips | head -3
hello-tips:
Installed: (none)
Candidate: 1.0-1
This costs one file per vendor and turns "this vendor can replace anything" into "this vendor can supply the one thing I asked for".
Common misconceptions
- "
apt-keyis just deprecated." It is gone. Any instruction containing it was written for Debian 10 or earlier, which is a reasonable prompt to check what else in those instructions has aged. - "Using HTTPS for the repository makes it trusted." HTTPS protects the download. The
signature is what establishes that the vendor produced the package, and
aptchecks it whether or not the transport was encrypted. A repository reached over HTTPS with no valid signature is still refused. - "Dropping the key in
/etc/apt/trusted.gpg.d/is the same thing." It works, and it grants that key authority over every repository configured on the machine.Signed-By:is the narrower grant, and there is no reason to prefer the wider one. - "I need
gpg --dearmorfirst." Not any more. Both the armoured and binary forms work inSigned-By:. - "A repository I added for one tool can only affect that tool." The
curlexample above is exactly that happening. Without pinning, it can affect anything. - "
apt updatesucceeded, so the repository is fine." It means the signature matched the key you supplied. Whether that key belongs to who you think is a questionaptcannot answer.
Go deeper
- APT essentials: the everyday commands underneath this, including
apt-cache policyfor asking where a package would come from - Debian's release channels: the same priority machinery as it applies to Debian's own suites, and why backports need asking for in the same way
- Managing services with systemd: what happens after you install something that ships a service, which on Debian is usually "it is already running"
curl: the tested reference for the fetch, including-fso a 404 fails loudly instead of saving an error page over your keyring
When apt refuses a repository outright, with NO_PUBKEY, a missing key, or
The repository is not signed, the key and the signature have disagreed rather than the
repository being broken. The repository is not signed
works through the diagnosis.