apt · Debian 13
apt-key is gone in Debian 13: list your keys, then check which repo each one can sign
Published · HardStacked · 8 min
You upgraded to Debian 13, typed apt-key list out of habit, and got apt-key: command not found. The obvious replacement is to loop over /etc/apt/trusted.gpg.d/ with gpg --show-keys. It works. It prints something that looks exactly like the old apt-key list output. What nobody tells you is that the list itself answers the wrong question.
Someone asked the direct version of this and got nowhere:
"apt-key is removed in Debian 13. How to list keys?"
(Unix & Linux Stack Exchange, question 805005, about 338 views a month, no accepted answer.)
The question that matters isn't "what keys do I have." It's "which key can sign which repo." Those are two different questions, and the space between them is exactly where a repo you thought was locked down stays wide open.
01. The two-line verdict
A key sitting in /etc/apt/trusted.gpg.d/ is trusted for every repository that doesn't restrict itself with a Signed-By line, whatever the key's filename says. A repo with no Signed-By accepts a signature from any key in that directory. A repo with Signed-By pointing at one specific key rejects every other key, including ones sitting right there in trusted.gpg.d. And apt modernize-sources, the tool Debian ships to migrate old-style .list entries, can write a Signed-By: field that's syntactically present and completely empty. That field looks restricted. It behaves as global.
Measured 2026-09-27, Debian 13 (trixie), Raspberry Pi 5, apt 3.0.3 (signatures checked by sqv).
02. What actually replaces apt-key list
gpg --show-keys on each file in /etc/apt/trusted.gpg.d/ gives you the closest equivalent: fingerprint, uid, expiry. On this machine that's 10 files. Two of them:
These commands are provided as is, without warranty. Test on a non critical machine or a snapshot first.
gpg --show-keys, 2 of the 10 files
== /etc/apt/trusted.gpg.d/debian-archive-trixie-stable.asc
pub ed25519 2025-03-24 [SC] [expires: 2033-03-22]
41587F7DB8C774BCCF131416762F67A0B2C39DE4
uid Debian Stable Release Key (13/trixie) <debian-release@lists.debian.org>
== /etc/apt/trusted.gpg.d/debian-archive-bullseye-automatic.asc
pub rsa4096 2021-01-17 [SC] [expires: 2029-01-15]
1F89983E0081FDE018F3CC9673A4F27B8DD47936
Revocable by: 80E976F14A508A48E9CA3FE9BC372252CA1CF964
Revocable by: FBFABDB541B5DC955BD9BA6EDB16CF5BB12525C4
Revocable by: 8C823DED10AA8041639E12105ACE8D6E0C14A470
Revocable by: 309911BEA966D0613053045711B4E5FF15B0FD82
Revocable by: C74F6AC9E933B3067F52F33FA459EC6715B0705F
uid Debian Archive Automatic Signing Key (11/bullseye) <ftpmaster@debian.org>
sub rsa4096 2021-01-17 [S] [expires: 2029-01-15]
That's the list. It tells you what's in the file. It says nothing about which repo, if any, is allowed to use it, and it can't see a key that isn't stored as a separate file at all: man apt-secure documents Signed-By as accepting the key embedded directly, ASCII-armored, inside a .sources stanza. A loop over trusted.gpg.d never reaches that key, because it was never dropped there in the first place.
03. The test that separates the two questions
man sources.list describes Signed-By as a way to "require a repository to pass apt-secure(8) verification with a certain set of keys rather than all trusted keys apt has configured." Read literally: a repo without that option is checked against all trusted keys, not one. That line is the whole mechanism, and it's worth confirming against the real binary instead of trusting the sentence.
The bench: a local repo signed by a throwaway ed25519 test key, a real apt-get update, apt configuration isolated through APT_CONFIG inside a user namespace, so nothing on the actual machine gets touched.
| Case | Setup | Result |
| A | key in trusted.gpg.d, repo has no Signed-By | accepted, rc 0 |
| B | same key still in trusted.gpg.d, Signed-By points at a different key | refused, rc 100, "Missing key" |
| C | control: no key anywhere | refused, rc 100 |
| D | Signed-By points at the right key, trusted.gpg.d empty | accepted, rc 0 |
| E | the same global key, saved under zz-looks-harmless.asc | accepted, the filename doesn't matter |
| F | apt modernize-sources on a .list with no signed-by | rc 0, warning, writes Signed-By: empty |
| G | apt-get update right after F, key still in trusted.gpg.d | accepted, rc 0 |
| H | same file as G, trusted.gpg.d emptied | refused, rc 100 |
Case F, then G, is the one worth sitting with. apt modernize-sources runs, prints a warning it couldn't determine Signed-By automatically, and still writes the field into the new .sources file:
apt modernize-sources, case F (excerpt)
$ apt modernize-sources
Rewrite 1 sources? [Y/n] y
Modernizing /srv/etc/sources.list.d/test.list...
- Writing /srv/etc/sources.list.d/test.sources
Warning: Could not determine Signed-By for URIs: file:/srv/repo/, Suites: ./
[rc=0]
--- cat etc/sources.list.d/test.sources
Types: deb
URIs: file:/srv/repo/
Suites: ./
Components:
Signed-By:
A grep Signed-By on that file finds a hit and moves on, satisfied the repo is restricted. Case G shows what it actually does: apt-get update right after, with a key sitting in trusted.gpg.d, still succeeds, rc 0. An empty Signed-By: field behaved exactly like no field at all (case A). Case H is the proof, not a footnote: empty the trusted.gpg.d directory and the same file, unchanged, fails with rc 100, "no keyring is specified." The .sources file never held its own trust. It was borrowing the global one the whole time, and only case H makes that visible, because it's the only case where you take that global trust away and watch the repo break.
04. The command that answers the real question
This loops per stanza (awk RS=""), reads Signed-By, catches an embedded key block, and falls back to "any key in trusted.gpg.d" when the field is missing or empty:
liste-depots.sh
#!/bin/bash
# Which repo trusts which key? Usage: liste-depots.sh [folder] (default /etc/apt/sources.list.d)
D=${1:-/etc/apt/sources.list.d}
awk 'BEGIN{RS="";FS="\n"} {u="?";s="ANY key in trusted.gpg.d (no Signed-By)"
for(i=1;i<=NF;i++){ if($i~/^URIs:/){u=$i;sub(/^URIs:[ \t]*/,"",u)}
if($i~/^Signed-By:/){v=$i;sub(/^Signed-By:[ \t]*/,"",v)
if($(i+1)~/BEGIN PGP/) s="key embedded in this file"
else if(v=="") s="ANY key in trusted.gpg.d (Signed-By is EMPTY)"
else s=v }}
print u" -> "s}' "$D"/*.sources 2>/dev/null
grep -h '^deb' /etc/apt/sources.list "$D"/*.list 2>/dev/null | awk '{ if(match($0,/signed-by=[^] ]+/)) print $0" -> "substr($0,RSTART+10,RLENGTH-10); else print $0" -> ANY key in trusted.gpg.d (no signed-by)"}'
Checked against 4 witness files before trusting it on a real machine: a stanza with no Signed-By field, one with an empty field, one with the key embedded inline, and a legacy .list line with no signed-by=. All 4 came back correctly labeled. Real output on this machine:
liste-depots.sh on this Pi
$ bash liste-depots.sh
http://deb.debian.org/debian/ -> /usr/share/keyrings/debian-archive-keyring.pgp
http://deb.debian.org/debian-security/ -> /usr/share/keyrings/debian-archive-keyring.pgp
http://archive.raspberrypi.com/debian/ -> /usr/share/keyrings/raspberrypi-archive-keyring.pgp
Every repo names its key explicitly (a fourth, third-party repo is trimmed from this output; it names its own key too), none of them resolving to "any key in trusted.gpg.d." That's the answer gpg --show-keys on trusted.gpg.d was never going to give you, because it isn't reading the repo side of the relationship at all.
05. Fixing a repo that trusts more than it should
If liste-depots.sh prints "ANY key in trusted.gpg.d" for a repo that should only trust one key, the fix mirrors case D above: drop that key's public export into /etc/apt/keyrings/, add Signed-By: /etc/apt/keyrings/that-key.asc to the repo's .sources stanza (or signed-by= on a .list line), and rerun. Measured: with the key living outside trusted.gpg.d and Signed-By pointing straight at it, apt-get update still succeeds.
Don't pull that key out of trusted.gpg.d just yet. Case H measured exactly this: pull a key that a repo was silently relying on out of the global pool, without giving that repo its own Signed-By, and it stops verifying, rc 100. Before touching trusted.gpg.d, run liste-depots.sh first and confirm no other .sources or .list entry on the machine is quietly depending on that same global key without saying so.
06. What this looks like on a real machine
liste-depots.sh on this Raspberry Pi 5 returns 4 repos, and all 4 carry their own Signed-By, none of them resolving to "any key in trusted.gpg.d." Meanwhile /etc/apt/trusted.gpg.d/ holds 10 keys, 3 of which are Debian 11 (bullseye) signing keys. None of those 10 keys is required by any repo configured today, that part is measured. What follows from that is an inference, not a second measurement: those keys would still be accepted, unchanged, by any repo added tomorrow that skips Signed-By, because case A already showed that's exactly how an unrestricted repo behaves. That's not a reason to delete them here. All 10 files belong to a package (dpkg -S: 9 to debian-archive-keyring, 1 to raspberrypi-archive-keyring), and pulling them by hand wasn't tested; treat the finding as "check what depends on it first," not "clean it up."
Scope
Measured on apt 3.0.3, Debian 13 trixie. Not checked here: Ubuntu, or apt 2.x on Debian 12 and earlier.
apt modernize-sources says, in its own output, that it fills in Signed-By automatically "where they can be determined." For the one repo in this test, it couldn't, and said so in a warning. Which repos it does resolve automatically wasn't measured here, so this piece makes no claim about that list either way. Run liste-depots.sh after any modernize-sources pass regardless of what it printed. Case F and G together are the reason: a clean exit code and a present Signed-By: field are not proof the repo got restricted.
The same shape runs through the rest of this series, where the system declares one thing and does another: an update-ca-certificates that prints 0 added, 0 removed whether your certificate was taken or ignored and a systemctl enable that reports failure while the service ends up enabled anyway.