Ubuntu · Packages & updates

Bind every APT repository to its signing key

When it fails: APT repositories are not bound to a signing key or bypass signature checks. A repository without its own key is trusted with every key known to APT, and options such as trusted=yes disable signature checks entirely.

CB-UBU-PKG-001highactiveautomatedautomated fixprofile core

As released in v0.1.0 (beta). Current state.

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
DefaultSigned-By set on 24.04 and later (ubuntu.sources), missing on 20.04 and 22.04 (sources.list)
RecommendedSigned-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:

  1. each one-line entry has [signed-by=<keyring>], each deb822 stanza a Signed-By: field;
  2. no entry sets trusted=yes, allow-insecure=yes, allow-weak=yes, allow-downgrade-to-insecure=yes, check-valid-until=no or check-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

  1. 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/).
  2. Reference it from the entry: deb [signed-by=/etc/apt/keyrings/<vendor>.asc] https://... in the one-line format, or Signed-By: /etc/apt/keyrings/<vendor>.asc in a .sources stanza. On 26.04, apt modernize-sources converts old entries to deb822.
  3. Remove trusted=yes and the other options that weaken the checks. A repository that only works with them must be fixed by its owner (signed) or removed.
  4. Run apt-get update and 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.

Restartnone
Disruptionnone

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

Mappings

FrameworkIDRelationNote
ANSSI-BP-028 - Recommandations de configuration d'un système GNU/Linux2.0R59partialR59 asks for trusted repositories only; this control enforces that each one is authenticated with its own key.
NIST SP 800-53 Rev. 55.1.1CM-14related
NIST SP 800-53 Rev. 55.1.1SI-7related

How this control relates to the other framework's: equivalent, stricter, looser, partial or related.

Threats mitigated

  • related CB-UBU-PKG-003 Keep APT signature, freshness and TLS checks enabled

People and lifecycle

Authors
Verified2026-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.
Verified2026-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.
Verified2026-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.
Verified2026-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 inv0.1.0
Created, updated2026-10-19, 2026-10-19
Translationsfr