Cuando ejecutas apt install, APT no consulta un único servidor misterioso: consulta una lista de repositorios que tú has configurado, descarga índices firmados criptográficamente, y verifica que cada paquete procede de quien dice proceder antes de tocarlo. Todo eso arranca en dos sitios: /etc/apt/sources.list y el directorio /etc/apt/sources.list.d/.
La separación entre el archivo principal y el directorio .d/ no es cosmética. El archivo sources.list es tuyo y del instalador del sistema; el directorio está pensado para que herramientas externas (scripts de instalación de Docker, repositorios de terceros, tus propios scripts de aprovisionamiento) depositen sus fuentes sin tocarte el archivo principal. Cada archivo .list dentro de ese directorio se procesa exactamente igual que si su contenido estuviera en sources.list. El orden entre archivos no importa para la resolución de dependencias, pero sí para saber de dónde viene un paquete concreto cuando hay duplicados.
Anatomía de una línea de repositorio
Una entrada tiene esta forma general:
deb [opciones] URL suite componentes...
El primer campo es el tipo: deb para paquetes binarios compilados (lo que instalas normalmente) y deb-src para los tarballs de código fuente, útiles solo si vas a compilar con apt source o dpkg-source. En producción casi nunca necesitas deb-src; añadirlo duplica el trabajo que hace apt update sin beneficio práctico.
Las opciones entre corchetes son pares clave=valor separados por espacios. La más importante hoy es signed-by=, que apunta a la clave GPG que debe firmar este repositorio. Si falta esa opción y la clave no está en el llavero global de APT (que en Debian moderno está vacío por diseño), el repositorio se rechaza o genera advertencias.
La suite identifica el release: bookworm, bullseye, noble, jammy… Es un nombre simbólico que el repositorio mapea internamente a una estructura de directorios. Algunos repositorios de terceros usan simplemente stable o incluso / (repositorio plano sin suites).
Los componentes subdividen el repositorio. En Debian son main (software libre que cumple las DFSG), contrib (software libre que depende de no-libre), non-free (software propietario) y non-free-firmware (separado desde Debian 12 para los firmwares). En Ubuntu el esquema es distinto: main, restricted, universe, multiverse, con criterios de soporte comercial como eje organizador en lugar de criterios de libertad.
Por qué existe la firma GPG
Cuando APT descarga el índice de un repositorio (Release o InRelease), ese archivo contiene los hashes SHA256 de todos los paquetes listados. Si alguien intercepta la conexión y modifica un .deb en tránsito, el hash no cuadra y APT lo rechaza. Pero eso solo funciona si el propio archivo de índice es auténtico. Ahí entra GPG: el servidor firma InRelease con su clave privada, y APT verifica esa firma con la clave pública que tú has instalado localmente. Sin esa cadena, un atacante con posición de red intermediaria podría sustituir tanto el paquete como el índice y APT no detectaría nada.
La ubicación moderna para esas claves públicas es /etc/apt/keyrings/. Antes se usaba apt-key add, que metía todas las claves en un único llavero global (/etc/apt/trusted.gpg). El problema es que una clave en ese llavero global se acepta para cualquier repositorio, lo que significa que si confías en la clave del repositorio de Spotify para instalar su cliente, esa misma clave podría, en teoría, firmar paquetes en cualquier otro repositorio y APT los aceptaría. El modelo con signed-by= es de confianza acotada: esta clave solo vale para esta URL. apt-key está marcado como obsoleto desde Debian 11.
Si llegas a un sistema con entradas antiguas que usan el llavero global sin signed-by=, apt update te avisará con Key is stored in legacy trusted.gpg keyring. No es urgente, pero sí vale la pena migrar.
Ejemplo completo: añadir el repositorio de Docker
El escenario habitual al incorporar un repositorio externo involucra tres pasos: descargar la clave, guardarla en /etc/apt/keyrings/, y escribir la entrada en .list.d/. Vamos a hacerlo limpio desde el principio:
# Instalar herramientas necesarias si no están presentes apt install -y curl gnupg # Crear el directorio de keyrings si no existe (Debian 12 lo trae, pero por si acaso) install -m 0755 -d /etc/apt/keyrings # Descargar la clave pública de Docker en formato OpenPGP binario (*.gpg) # --fail-with-body: si el servidor devuelve error HTTP, curl falla y no escribimos basura curl -fsSL https://download.docker.com/linux/debian/gpg \ | gpg --dearmor -o /etc/apt/keyrings/docker.gpg # Los permisos deben permitir que el proceso de apt (root) lea la clave, # pero no tienen por qué ser world-writable chmod a+r /etc/apt/keyrings/docker.gpg # Crear la entrada del repositorio apuntando explícitamente a la clave # $(. /etc/os-release && echo "$VERSION_CODENAME") devuelve "bookworm" en Debian 12 # sin depender de lsb_release, que puede no estar instalado echo \ "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/debian \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \ > /etc/apt/sources.list.d/docker.list # Actualizar índices — APT verifica la firma en este paso, no en apt install apt update # Verificar que APT ve el repositorio y de dónde va a resolver docker-ce apt-cache policy docker-ce
Qué está pasando en cada decisión
El gpg --dearmor convierte la clave de formato ASCII armored (el que ves con las líneas -----BEGIN PGP PUBLIC KEY BLOCK-----) al formato binario que APT espera. Guardar el .asc directamente también funciona con versiones recientes de APT, pero el .gpg binario es más portable entre versiones.
El install -m 0755 -d en lugar de mkdir -p es un hábito de aprovisionamiento: crea el directorio con permisos explícitos en una sola operación, idempotente si ya existe.
La opción arch=amd64 dentro de los corchetes no es obligatoria, pero es recomendable en repositorios de terceros que no publican índices para todas las arquitecturas. Sin ella, en un sistema con arquitecturas adicionales activadas (dpkg --print-foreign-architectures), APT intentará descargar índices para arm64 o i386 y fallará ruidosamente porque el repositorio externo no los tiene.
El subshell . /etc/os-release && echo "$VERSION_CODENAME" es la forma correcta de obtener el codename en Debian sin instalar dependencias adicionales. lsb_release -cs hace lo mismo pero requiere el paquete lsb-release. En un contenedor o en una instalación mínima, ese paquete puede no estar.
apt update es el momento en que APT descarga el archivo InRelease del repositorio, extrae la firma GPG embebida, la verifica contra /etc/apt/keyrings/docker.gpg, y solo si la firma es válida actualiza la caché local con los índices de paquetes. Si la verificación falla, verás The following signatures couldn't be verified y los paquetes de ese repositorio no aparecerán disponibles. El apt-cache policy docker-ce al final te muestra la URL de origen de cada versión candidata, lo que confirma que el repositorio se está leyendo correctamente y te permite detectar si otro repositorio está sobreponiendo versiones de forma inesperada.
Un repositorio mal configurado no rompe los repositorios de Debian; APT los procesa independientemente. Lo que sí puede pasar si añades un repositorio externo sin signed-by= es que cualquier paquete de ese repositorio quede en estado «no autenticado», y dependiendo de tu configuración de APT (APT::Get::AllowUnauthenticated), la instalación se bloqueará o pedirá confirmación explícita. Eso es exactamente el comportamiento que quieres: un repositorio sin firma verificable no debería instalarse en silencio.
[Ubuntu]: Para PPAs de Launchpad, add-apt-repository ppa:nombre/ppa automatiza los tres pasos (clave, entrada y apt update) en un solo comando. En Debian no existe ese mecanismo porque los PPAs son infraestructura específica de Ubuntu/Launchpad.
N° 54