Description
APT checks the signature of the Release file of each repository. The Signed-By option
([signed-by=...] in the one-line format, Signed-By: field in the deb822 .sources format)
names the keyring allowed to sign that repository. Without it, any key in
/etc/apt/trusted.gpg.d/ is accepted for every repository.
Per-source options can also weaken or disable the checks: trusted=yes, allow-insecure=yes,
allow-weak=yes, allow-downgrade-to-insecure=yes, check-valid-until=no and check-date=no.
Ubuntu 24.04 and 26.04 use the deb822 file /etc/apt/sources.list.d/ubuntu.sources with
Signed-By: they pass. Ubuntu 20.04 and 22.04 use one-line entries in /etc/apt/sources.list
without it, the Ubuntu keys being in /etc/apt/trusted.gpg.d/: they fail until fixed. The
automated fix adds the Ubuntu archive keyring to entries of archive.ubuntu.com,
<country>.archive.ubuntu.com, security.ubuntu.com and ports.ubuntu.com. Other
repositories, internal mirrors and the bypass options are left to a human. The repositories
written by the Ubuntu Pro client (esm.ubuntu.com) carry their own Signed-By.
Rationale
Binding each repository to its own key limits a compromised or malicious third-party key to the repository it was added for: it cannot sign packages that replace Ubuntu ones. Sources that bypass the signature checks let anyone on the network path, or controlling the mirror, install arbitrary packages as root.
Impact
None for the Ubuntu archive. A third-party repository (PPA, vendor repository) needs its key
stored as a separate file (usually under /etc/apt/keyrings/) and referenced from its source
entry. Keys left in /etc/apt/trusted.gpg.d/ keep working for entries without Signed-By.
Setting
| Location | /etc/apt/sources.list, /etc/apt/sources.list.d/*.list, /etc/apt/sources.list.d/*.sources |
| Default | Signed-By set on 24.04 and later (ubuntu.sources), missing on 20.04 and 22.04 (sources.list) |
| Recommended | Signed-By on every entry, no option that disables or weakens signature checks |
Audit
Read every enabled entry in /etc/apt/sources.list, /etc/apt/sources.list.d/*.list and
/etc/apt/sources.list.d/*.sources:
- each one-line entry has
[signed-by=<keyring>], each deb822 stanza aSigned-By:field; - no entry sets
trusted=yes,allow-insecure=yes,allow-weak=yes,allow-downgrade-to-insecure=yes,check-valid-until=noorcheck-date=no(or the matching deb822 fields).
Automated: every check must pass. Scanners report each check by its id.
sources
Signed-By and signature options of every enabled APT source entry.
script: |
files=
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do
[ -f "$f" ] && files="$files $f"
done
[ -n "$files" ] || { echo "no APT source file"; exit 2; }
# One line per entry: file:line, types, URIs, suites, ;option=value list (lowercase).
# shellcheck disable=SC2086
awk '
function flush() { if (st && en != "no") print src "\t" ty "\t" ur "\t" su "\t" op; st = 0; ty = ur = su = op = en = "" }
FNR == 1 { flush(); d = FILENAME ~ /\.sources$/ }
!d {
sub(/#.*/, ""); if ($1 !~ /^deb(-src)?$/) next
l = $0; o = ""; i = index(l, "["); j = index(l, "]")
if (i && j > i) { o = substr(l, i + 1, j - i - 1); l = substr(l, 1, i - 1) " " substr(l, j + 1) }
split(l, w, " "); gsub(/[ \t]+/, ";", o)
print FILENAME ":" FNR "\t" w[1] "\t" w[2] "\t" w[3] "\t;" tolower(o); next
}
/^#/ { next }
/^[ \t]*$/ { flush(); next }
/^[ \t]/ { next }
{
if (!st) { st = 1; src = FILENAME ":" FNR }
k = $0; sub(/:.*/, "", k); k = tolower(k); v = $0; sub(/^[^:]*:[ \t]*/, "", v)
if (k == "types") ty = v; else if (k == "uris") ur = v; else if (k == "suites") su = v
else if (k == "enabled") en = tolower(v)
else op = op ";" k "=" tolower(v == "" ? "inline" : v)
}
END { flush() }' $files | awk -F '\t' '
{
bad = ""
if ($5 !~ /;signed-by=/) bad = bad " no-signed-by"
n = split("trusted=yes allow-insecure=yes allow-weak=yes allow-downgrade-to-insecure=yes check-valid-until=no check-date=no", o, " ")
for (i = 1; i <= n; i++) if (index($5, ";" o[i])) bad = bad " " o[i]
if (bad != "") { print $1 " " $3 ":" bad; rc = 1 } else ok++
}
END { print ok + 0 " entries bound to a key"; exit rc }'
Remediation
- For each repository, store its public key in its own file, for example
/etc/apt/keyrings/<vendor>.asc(Ubuntu keys are in/usr/share/keyrings/). - Reference it from the entry:
deb [signed-by=/etc/apt/keyrings/<vendor>.asc] https://...in the one-line format, orSigned-By: /etc/apt/keyrings/<vendor>.ascin a.sourcesstanza. On 26.04,apt modernize-sourcesconverts old entries to deb822. - Remove
trusted=yesand the other options that weaken the checks. A repository that only works with them must be fixed by its owner (signed) or removed. - Run
apt-get updateand check that it prints no signature warning.
The automated fix only adds /usr/share/keyrings/ubuntu-archive-keyring.gpg to entries of the
Ubuntu archive.
| Restart | none |
| Disruption | none |
Rollback
Restore the source files from the backup directory printed by remediate.sh.
signed-by-ubuntu
#!/bin/sh
# CB-UBU-PKG-001 / signed-by-ubuntu - add Signed-By to Ubuntu archive entries that lack it.
# Only entries pointing to archive.ubuntu.com, <cc>.archive.ubuntu.com, security.ubuntu.com or
# ports.ubuntu.com are changed: the key of any other repository must be chosen by a human.
# Idempotent, no-op when compliant.
# SPDX-License-Identifier: Apache-2.0
set -eu
k=/usr/share/keyrings/ubuntu-archive-keyring.gpg
h='(https?|tor\+https?)://(([a-z]+\.)?archive|security|ports)\.ubuntu\.com/'
# One-line format: sources.list and *.list
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*.list; do
[ -f "$f" ] || continue
grep -E '^[[:space:]]*deb(-src)?[[:space:]]' "$f" | grep -v 'signed-by=' | grep -Eq "$h" || continue
[ -e "$k" ] || { echo "$k is missing, install ubuntu-keyring" >&2; exit 1; }
cb_backup "$f"
sed -E -i "/signed-by=/! {
s#^([[:space:]]*deb(-src)?[[:space:]]+)\[([^]]*)\]([[:space:]]*$h)#\1[signed-by=$k \3]\4#
t
s#^([[:space:]]*deb(-src)?[[:space:]]+)($h)#\1[signed-by=$k] \3#
}" "$f"
done
# deb822 format: *.sources, one stanza per paragraph
for f in /etc/apt/sources.list.d/*.sources; do
[ -f "$f" ] || continue
t=$(mktemp)
if awk -v k="$k" '
function flush( i) {
for (i = 1; i <= n; i++) {
print b[i]
if (i == u && ub && !sig) { print "Signed-By: " k; c = 1 }
}
n = u = ub = sig = 0
}
/^[ \t]*$/ { flush(); print; next }
{ b[++n] = $0 }
tolower($0) ~ /^signed-by:/ { sig = 1 }
tolower($0) ~ /^uris:/ { u = n; if ($0 ~ /:\/\/(([a-z]+\.)?archive|security|ports)\.ubuntu\.com\//) ub = 1 }
END { flush(); exit !c }' "$f" >"$t"; then
[ -e "$k" ] || { rm -f "$t"; echo "$k is missing, install ubuntu-keyring" >&2; exit 1; }
cb_backup "$f"
cat "$t" >"$f"
fi
rm -f "$t"
done
Handwritten code reads parameters from CB_<NAME> environment variables. Each release turns these fixes into remediate.sh and an Ansible role per profile: dry run by default, a fix runs only when its check fails.
References
- sources.list(5), Ubuntu manual pages Canonical, vendor-doc, accessed 2026-10-19
- apt-secure(8), Ubuntu manual pages Canonical, vendor-doc, accessed 2026-10-19
- Ubuntu Server documentation - Third-party repository usage Canonical, vendor-doc, accessed 2026-10-19
- ANSSI-BP-028 v2.0 - Recommandations de configuration d'un système GNU/Linux (R59) ANSSI, guide, accessed 2026-10-19
Mappings
| Framework | ID | Relation | Note |
|---|---|---|---|
| ANSSI-BP-028 - Recommandations de configuration d'un système GNU/Linux2.0 | R59 | partial | R59 asks for trusted repositories only; this control enforces that each one is authenticated with its own key. |
| NIST SP 800-53 Rev. 55.1.1 | CM-14 | related | |
| NIST SP 800-53 Rev. 55.1.1 | SI-7 | related |
How this control relates to the other framework's: equivalent, stricter, looser, partial or related.
Threats mitigated
Related controls
- related
CB-UBU-PKG-003Keep APT signature, freshness and TLS checks enabled
People and lifecycle
| Authors |
|
| Verified | 2026-10-19 by Sn0wAlice on 20.04, Proxmox VM, Ubuntu 20.04 cloud image, kernel 5.4.0-216-generic. Fails on stock (one-line sources.list without Signed-By); fix adds the Ubuntu archive keyring, apt-get update over https works; trusted=yes and deb822 stanza without Signed-By detected, disabled stanza skipped; idempotent, kept after reboot. |
| Verified | 2026-10-19 by Sn0wAlice on 22.04, Proxmox VM, Ubuntu 22.04 cloud image, kernel 5.15.0-198-generic. Fails on stock (one-line sources.list without Signed-By); fix adds the Ubuntu archive keyring, apt-get update over https works; trusted=yes and deb822 stanza without Signed-By detected, disabled stanza skipped; idempotent, kept after reboot. |
| Verified | 2026-10-19 by Sn0wAlice on 24.04, Proxmox VM, Ubuntu 24.04 cloud image, kernel 6.8.0-147-generic. Passes on stock (ubuntu.sources with Signed-By); one-line entry with trusted=yes and deb822 stanza without Signed-By detected, disabled stanza skipped; fix adds the keyring to the Ubuntu stanza only, apt-get update works; idempotent, kept after reboot. |
| Verified | 2026-10-19 by Sn0wAlice on 26.04, Proxmox VM, Ubuntu 26.04 cloud image, kernel 7.0.0-38-generic. Passes on stock (ubuntu.sources with Signed-By); one-line entry with trusted=yes and deb822 stanza without Signed-By detected, disabled stanza skipped; fix adds the keyring to the Ubuntu stanza only, apt-get update works; idempotent, kept after reboot. |
| Introduced in | v0.1.0 |
| Created, updated | 2026-10-19, 2026-10-19 |
| Translations | fr |