Seguridad

Firma y verificacion de repositorios APT con GPG: guia completa

El primer repositorio interno que firmé era un .deb perdido en un servidor de pruebas, sin ningún indice que lo respaldara. Cuando ese servidor se reseteo, todo se fue con el. Volvi a reconstruirlo desde cero y, esta vez, le puse firma. La diferencia no fue academica: la segunda version se instalaba sin que el cliente le preguntara nada, porque la cadena de confianza estaba verificada de punta a punta.

Esa es la diferencia entre “publicar un repositorio” y “publicar un repositorio verificable”. APT no descarga tus paquetes sueltos y los confía porque esten en tu servidor: descarga un indice firmado, lo valida contra una clave publica que ya tiene en su keyring, y solo despues usa ese indice. Si alguien modifica un .deb en tránsito o reemplaza tu Release, la verificacion falla antes de que se instale nada. Este es el flujo completo, con los comandos reales y los errores que me aparecieron en el camino.

TL;DR — Lo que vas a tener al final

  • Tiempo de setup: 25-35 minutos en una maquina Debian 13 con APT 3.0: crear la clave (RSA 4096), generar Packages y Release con apt-ftparchive, y firmar InRelease mas Release.gpg en ASCII armor.
  • Requisitos: gpg 2.4+, apt-ftparchive (paquete apt-utils), sqv para verificar, y permisos de escritura en el directorio del repo. En el cliente: /etc/apt/keyrings/ y la opcion signed-by= en el archivo de fuentes.
  • Resultado: un repositorio APT propio donde apt-get update descarga InRelease, lo valida con sqv contra tu clave, y solo entonces acepta el Packages. Verificado end-to-end: apt-get update sale con codigo 0 y sin advertencias.
  • Troubleshooting: dos trampas reales. La firma despegada debe ir en ASCII armor (--armor --detach-sign): una firma binaria da unsupported binary format. Y una clave expirada no rompe apt-get update en APT 3.0, pero gpg ya no te deja firmar con ella (Unusable secret key) hasta que la renuevas con --quick-set-expire.

Qué verifica APT (y qué no verifica)

Antes de los comandos, vale la pena tener claro el modelo mental, porque casi todos los errores que vas a cometer en esto vienen de confundir dos capas.

APT verifica el indice del repositorio, no cada paquete individual. Concretamente, en cada apt-get update descarga dos archivos:

  • Release: el manifiesto con la lista de hashes de cada Packages y cada .deb del repositorio, mas metadatos como Suite, Codename y Components.
  • InRelease: el mismo Release pero firmado y con la firma en linea (clearsigned). APT lo prefiere porque basta una sola descarga.
  • Release.gpg: la firma despegada de Release, usada como plan B cuando InRelease no esta disponible.

Si la clave publica que el cliente tiene en su keyring valida esa firma, APT acepta el Packages y da por buenos los hashes que contiene. De ahi sale la cadena de confianza: la clave publica del repositorio se verifico una vez, a mano o por un canal seguro, y a partir de ahi cada indice firmadoLY hereda esa confianza.

Lo que APT no hace por su cuenta es verificar la firma individual de cada .deb. Para eso existen herramientas separadas, que veremos mas adelante.

Crea el par de claves del repositorio

La clave privada vive en el servidor que publica. La publica se distribuye aparte, a quien vaya a consumir el repositorio. Nunca subas la privada al servidor web: guardala en un GNUPGHOME aparte, con permisos restrictivos.

export GNUPGHOME="$HOME/.gnupg-repo-interno"
mkdir -p "$GNUPGHOME"
chmod 700 "$GNUPGHOME"
gpg --batch --pinentry-mode loopback --passphrase '' \
    --quick-generate-key "Repo Interno TU_EMPRESA" rsa4096 sign,cert 2y
gpg --list-keys --with-colons | awk -F: '/^fpr:/ {print $10; exit}'

El ultimo comando imprime el fingerprint, que es el identificador de 40 caracteres que vas a usar en toda la configuracion. Guárdalo: es lo que permite a un cliente confirmar que esta importando la clave correcta y no otra con un nombre parecido.

La clave debe poder firmar y certificar (sign,cert): la primera para firmar el Release, la segunda para que puedas firmar tu propia clave con ella y encadenar confianza si hace falta. La validez de 2 anos es un punto de partida: mas adelante vemos como renovarla cuando venza.

Construye el indice Packages y Release

Un repositorio APT necesita tres cosas en disco: los .deb, un indice Packages que los describe, y el Release que lista los hashes de esos indices. apt-ftparchive (viene en apt-utils) genera los dos ultimos a partir de los archivos.

cd /srv/repo-interno
mkdir -p dists/stable/main/binary-amd64
cp /build/paquete-demo_1.0.0_amd64.deb dists/stable/main/binary-amd64/
apt-ftparchive packages dists/stable/main/binary-amd64 > /tmp/Packages.indice
mv /tmp/Packages.indice dists/stable/main/binary-amd64/Packages
apt-ftparchive -o APT::FTPArchive::Release::Origin="Repo Interno" \
  -o APT::FTPArchive::Release::Suite="stable" \
  -o APT::FTPArchive::Release::Codename="stable" \
  -o APT::FTPArchive::Release::Components="main" \
  -o APT::FTPArchive::Release::Architectures="amd64" \
  release dists/stable > /tmp/Release.indice
mv /tmp/Release.indice dists/stable/Release

Dos detalles que ahorran dolor de cabeza. El primero: escribe siempre a un archivo temporal y luego mv al destino. Si rediriges la salida de apt-ftparchive directamente sobre el archivo que esta leyendo, lo vacias a mitad del recorrido.

El segundo: Suite y Codename no son opcionales. Sin ellos, el Release queda sin esos campos y APT responde W: Conflicting distribution al hacer update, porque no puede casar el repositorio con el stable que pide el cliente en su archivo de fuentes. Es una advertencia, no un fallo, pero te deja el repositorio a medio verificar.

Firma el Release en InRelease y Release.gpg

Aqui esta el punto donde la mayoria de los tutoriales fallan. InRelease se firma en linea: el contenido va firmado y en el mismo archivo. Release.gpg es la firma despegada, y esa debe generarse en ASCII armor.

gpg --batch --yes --pinentry-mode loopback --passphrase '' \
    --local-user CLAVE_PUBLICA \
    --clearsign --output dists/stable/InRelease dists/stable/Release
gpg --batch --yes --pinentry-mode loopback --passphrase '' \
    --local-user CLAVE_PUBLICA \
    --armor --detach-sign --output dists/stable/Release.gpg dists/stable/Release

Si generas la firma despegada sin --armor, el archivo resultante es binario y APT lo rechaza con un error que no dice casi nada:

E: OpenPGP signature verification failed: file:/srv/repo-interno stable Release: Signed file isn't valid, got 'NODATA' (does the network require authentication?) Detached signature file '.../Release.gpg' is in unsupported binary format

El NODATA y el parentesis sobre la autenticacion de red son ruido: el problema real es la ultima frase, unsupported binary format. Por eso el bloque de arriba lleva --armor explicito, y por eso vale la pena comprobar el encabezado del archivo:

head -1 dists/stable/Release.gpg

La salida debe ser -----BEGIN PGP SIGNATURE-----. Si en cambio ves un encabezado binario, la firma se genero sin armor.

Vale notar una diferencia entre verificadores: gpgv acepta una firma despegada binaria sin quejarse, pero sqv, que es el que usa APT 3.0, la rechaza. Por eso una firma que tu gpgv da por buena puede fallar en el cliente.

Verifica la firma antes de publicar

No publiques sin verificar. gpgv es el verificador clasico: valida una firma sin necesidad de un keyring completo, y su codigo de salida te dice si la firma es buena.

gpgv --keyring repo-interno.gpg dists/stable/InRelease
gpgv --keyring repo-interno.gpg dists/stable/Release.gpg dists/stable/Release
echo "codigo de salida: $?"

La salida esperada es gpgv: Good signature from "Repo Interno TU_EMPRESA" con codigo de salida 0. Un codigo distinto de cero significa que la firma no valida: revisa que el keyring corresponda a la clave que firmo.

APT 3.0 ya no usa gpgv para esto: delega en sqv, el verificador de sequoia, que es estricto y no acepta el formato antiguo de RPM-GPG-KEY. Puedes verificar con la misma herramienta que usara el cliente:

sqv --keyring repo-interno.gpg --cleartext --output /tmp/Release.verificado dists/stable/InRelease
head -3 /tmp/Release.verificado

Lo que imprime sqv es el fingerprint de la clave que firmo, y el contenido que se extrae debe coincidir con tu Release original. Ojo con un detalle: sqv no anade el salto de linea final, asi que diff contra el Release original va a reportar una diferencia de un byte. No es una firma invalida.

Configura el cliente con signed-by

Del lado del cliente, la clave publica se instala en /etc/apt/keyrings/ y se referencia de forma explicita con la opcion signed-by=. Esta es la via moderna y la que debes usar.

sudo install -d -m 0755 /etc/apt/keyrings
sudo install -m 0644 repo-interno.gpg /etc/apt/keyrings/repo-interno.gpg
echo "deb [signed-by=/etc/apt/keyrings/repo-interno.gpg] https://repo.tu-empresa.com/interno stable main" \
  | sudo tee /etc/apt/sources.list.d/repo-interno.list
sudo apt-get update

La ventaja de signed-by= frente al keyring global es que la confianza queda acotada a ese repositorio. Si ese repositorio se compromete, no arrastra a los demas. En el mundo APT, signed-by con ruta absoluta es obligatorio: con una ruta relativa, APT responde E: Invalid value set for option Signed-By regarding source file:... (not a fingerprint) y se niega a leer la lista de fuentes. Y si la ruta absoluta no apunta a un archivo real, el error que veras viene de sqv: Failed to parse keyring "...": No such file or directory.

Por que apt-key quedo obsoleto

Si llegaste aqui por un tutorial viejo, veras apt-key add como respuesta. Es el camino que ya no debes tomar. El manpage de apt-key en Ubuntu Noble dice que la utilidad estara disponible “hasta” Ubuntu 24.04, y los filelists de paquetes lo confirman: /usr/bin/apt-key sigue presente en Ubuntu 24.04 (Noble) y ya no aparece en Ubuntu 25.04 (Plucky). En Debian el fin es similar: la herramienta quedo obsoleta y el propio apt-key se removio en Debian 13 (Trixie).

El motivo de fondo no es capricho. apt-key add escribia en /etc/apt/trusted.gpg.d/, que es un keyring global: toda clave que pasa por alla queda confiando en todos los repositorios del sistema. signed-by= con /etc/apt/keyrings/ elimina ese acoplamiento. Es un cambio de arquitectura, no un detalle cosmetico.

Compara el ecosystem RPM: rpm, DNF y Zypper

El otro gran gestor de paquetes de Linux resuelve el mismo problema con otras piezas. La diferencia clave es que RPM si firma cada paquete individual, no solo el indice del repositorio.

Aspecto APT (Debian/Ubuntu) RPM (Fedora/RHEL/openSUSE)
Que se firma El indice Release (APT no firma cada .deb por defecto) El indice del repo y ademas cada RPM individual
Herramienta de firma gpg (--clearsign y --armor --detach-sign) rpmsign, sobre una clave en el keyring de RPM
Importar clave publica Archivo en /etc/apt/keyrings/ + signed-by= rpm --import (tambien -K / --checksig)
Verificacion sqv en APT 3.0, gpgv en APT 2.x rpm --checksig, o la politica %_pkgverify_level
Configuracion por repo signed-by= en el archivo de fuentes gpgcheck (paquetes) y repo_gpgcheck (metadatos) en .repo
Verificar el indice Automatico en cada apt-get update repo_gpgcheck=1; gpgcheck viene en False por defecto

Sobre los defaults de DNF conviene ser preciso, porque se confunden entre si. En la documentacion de dnf.conf, tanto gpgcheck (que verifica la firma de los paquetes descargados) como repo_gpgcheck (que verifica la firma de los metadatos del repositorio) estan en False por defecto. No es lo mismo que en la practica de Fedora, donde los archivos .repo oficiales ya traen la verificacion activada.

Los defaults de DNF vienen con un matiz que vale la pena conocer: la opcion gpgcheck solo puede reforzar la politica de seguridad activa de RPM,definida por la macro %_pkgverify_level. Si esa macro esta en signature o all, un gpgcheck en False se sobreescribe a True en tiempo de ejecucion y DNF imprime un aviso.

En Zypper la columna GPG Check de zypper lr te dice si cada repositorio exige firma. Y hay un matiz de diseno que conviene conocer: cuando el repo publica metadatos firmados, Zypper acepta los paquetes cuyo checksum coincide con el de los metadatos firmados, sin necesidad de verificar la firma de cada RPM. Si el repo no publica metadatos firmados, entonces si se verifica cada RPM individualmente. Eso significa que activar la firma a nivel de metadatos es lo que te da la verificacion de indice, y la de paquete individual es el plan B.

Para comparar el comando a mano, del lado RPM:

sudo rpm --import https://repo.tu-empresa.com/RPM-GPG-KEY-repo-interno
rpm --checksig tu-paquete-1.0.0-1.x86_64.rpm
rpm -q gpg-pubkey --qf '%{name}-%{version}-%{release} %{summary}'

El --checksig es el equivalente de gpgv: verifica la firma del paquete, y su salida lo deja explicito con la palabra signatures cuando la firma esta presente:

tu-paquete-1.0.0-1.x86_64.rpm: digests signatures OK

Sin firma, el mismo comando devuelve solo digests OK: los hashes cuadran, pero no hay ninguna firma que los respalde. Y rpm -K es la forma corta del mismo chequeo. La diferencia conceptual con APT es que RPM consulta la base de datos de claves del propio sistema (/var/lib/rpm), mientras APT 3.0 mantiene un keyring separado por repositorio cuando usas signed-by=.

Un detalle de formato que muerde: rpm --import exige la llave en ASCII armor. Si le pasas el keyring binario que produce gpg --export sin --armor, se niega con error: /ruta/llave.gpg: key 1 not an armored public key. y sale con codigo 1. La forma que si funciona es gpg --armor --export, que produce un .asc importable.

Que pasa cuando la clave expira

Este es el escenario que maseca al equipo: la clave vence en pleno vuelo, los clientes empiezan a fallar y nadie sabe por que. Merece la pena decir exactamente que pasa, porque la intuicion falla.

Probé el escenario con una clave creada hace 18 meses con validez de 1 ano (o sea, vencida hace 6 meses) y una firma hecha 12 meses atras, cuando la clave todavia estaba vigente. Resultado:

  • gpgv devuelve Good signature y sale con codigo 0. No senala la expiracion.
  • sqv tambien acepta la firma, con codigo 0. Igual que APT 3.0, que no mostraba ninguna advertencia y actualizaba el repositorio sin problemas.
  • gpg --verify si lo dice: marca [expired] y agrega Note: This key has expired!, aunque su codigo de salida tambien es 0.
  • Firmar con la clave vencida si falla: gpg: skipped "CLAVE": Unusable secret key, con codigo de salida 2.

El punto clave: una firma hecha antes de que la clave venciera sigue siendo valida. La expiracion no invalida retroactivamente lo que ya firmaste. Lo que si se rompe es tu capacidad de seguir firmando: en cuanto publicas una version nueva del repositorio, gpg se niega a firmar con esa clave.

Hay un matiz importante en la verificacion. La expiracion se puede imponer como politica explicita con sqv --not-before y sqv --not-after, que exigen que la firma se haya creado dentro de una ventana temporal. En mi prueba, --not-before con la fecha de hoy rejecto una firma antigua con Signature by ... was created before the --not-before date y codigo de salida 1. Si tu organizacion tiene una politica de expiracion de claves, esa es la bandera que quieres.

Renueva la clave cuando este por vencer

La buena noticia es que renovar una clave expirada es posible y no requiere generar una nueva. --quick-set-expire extiende la fecha de expiracion de una clave existente:

gpg --quick-set-expire CLAVE_PUBLICA 3y
gpg --list-keys --with-colons | awk -F: '/^pub:/ {print $7}'

Verificado: la clave expirada paso a expirar en 2029, y gpg volvio a firmar con ella sin errores, con gpgv reportando Good signature sobre la firma nueva. El fingerprint no cambia, asi que los clientes que ya tienen la clave publica no necesitan actualizarla: solo hay que volver a firmar el Release.

La leccion operativa: pon la renovacion en calendario antes de la fecha de expiracion. Una clave que expira no rompe el repositorio de golpe; rompe justo cuando intentas publicar la actualizacion de seguridad que todos estaban esperando.

Y si quieres firmar cada paquete individual

Como vimos, APT no verifica la firma de cada .deb. Si necesitas esa capa, hay dos caminos en el ecosistema Debian.

dpkg-sig firma el .deb directamente, incrustando la firma en el propio paquete. El .deb resultante se instala igual que cualquier otro, y la verificacion se hace aparte.

debsig-verify es la via soportada por el propio dpkg. Su manpage en Trixie documenta que se integra con dpkg a traves del paquete debsig-verify, y el dpkg expone el flag --no-debsig con la descripcion literal “Do not try to verify package signatures”, lo que confirma que la verificacion existe y es controlable.

La configuracion de debsig-verify se hace con archivos de politica en formato XML, no JSON. Segun su manpage, la estructura de archivos es:

/etc/debsig/policies/
/etc/debsig/policies/*/*.pol
/usr/share/debsig/keyrings/
/usr/share/debsig/keyrings/*/*.pgp
/usr/share/debsig/keyrings/*/*.gpg

El archivo .pol define tres partes: Origin (informacion del origen), Selection (reglas para decidir si la politica aplica a ese .deb) y Verification (las reglas que verifican). Las politas referencian los keyrings OpenPGP por nombre de archivo, y cada politica vive en un subdirectorio que corresponde al Public Key ID de la firma de origen del paquete.

En la practica, debsig-verify casi no se usa: la firma por indice de APT cubre el caso principal con mucha menos complejidad. Mencionalo porque es la respuesta correcta cuando alguien pregunta “como verifico que un .deb especifico no fue manipulado”.

Lo que nunca debes hacer

--allow-unauthenticated y --nogpgcheck existen como escapes para máquinas sin red o con repositorios de prueba. No los pongas en un repositorio de produccion: desactiva exactamente la proteccion que acabas de construir. Si el repo necesita ese escape, el problema esta en la distribucion de la clave, no en la bandera.

No subas la clave privada al servidor web. La publica va en /etc/apt/keyrings/ del cliente, la privada nunca sale del servidor de publicacion.

No uses apt-key add. esta obsoleto desde hace años, el binario desaparece en Ubuntu 25.04 y Debian 13 ya lo removio. Ademas, el keyring global que usaba te expone a mas superficie de la que necesitas.

Resumen operativo

El flujo completo, en orden, es este: creas la clave en un GNUPGHOME privado; generas Packages y Release con apt-ftparchive (con Suite y Codename puestos); firmas InRelease con --clearsign y Release.gpg con --armor --detach-sign; verificas con gpgv y con sqv; y en el cliente instalas la clave publica en /etc/apt/keyrings/ con signed-by=.

Lo que mas se te va a olvidar: la firma despegada tiene que ir en ASCII armor, y la renovacion de la clave se agenda antes de la fecha de expiracion, no despues.

Preguntas frecuentes

¿Cuál es la diferencia entre firmar el repositorio y firmar cada paquete?

APT firma el indice: el Release incluye los hashes de cada .deb, y esa firma cubre todo lo que hay debajo. El paquete individual no se firma por defecto en APT. En RPM es al reves: se puede firma cada RPM, y repo_gpgcheck cubre los metadatos. Para un repositorio APT propio, firmar el indice basta y es mucho mas simple.

¿Por qué mi repositorio da «unsupported binary format»?

Porque generaste Release.gpg como firma despegada binaria. APT 3.0 usa sqv, que solo acepta esa firma en ASCII armor. Regenerala con gpg --armor --detach-sign y comprueba que el archivo empieza con -----BEGIN PGP SIGNATURE-----. El error tambien menciona NODATA y la autenticacion de red, pero eso es ruido.

¿Qué hago si la clave del repositorio ya expiró?

La buena noticia es que las firmas hechas antes de la expiracion siguen siendo validas: gpgv y sqv las aceptan con codigo 0, y apt-get update sigue funcionando. Lo que falla es firmar de nuevo: gpg responde Unusable secret key. Renueva con gpg --quick-set-expire CLAVE 3y, que extiende la fecha sin cambiar el fingerprint, y vuelve a firmar el Release.

¿Debo usar apt-key o signed-by con /etc/apt/keyrings/?

signed-by con /etc/apt/keyrings/. apt-key esta obsoleto, sigue presente en Ubuntu 24.04 pero ya no en 25.04, y Debian 13 lo removio. Ademas, escribir en el keyring global como hacia apt-key add hace que la confianza de una clave se aplique a todos los repositorios, no solo al suyo.

¿Cómo verifico que el cliente confía en la clave correcta?

Por el fingerprint, no por el nombre. En el servidor, gpg --list-keys --with-colons imprime los 40 caracteres hexadecimales de tu clave. En el cliente, sqv --keyring /etc/apt/keyrings/repo-interno.gpg imprime el fingerprint de la clave que realmente firmo el Release. Si coinciden, la cadena de confianza esta completa.

Lee también

Alfonso Moreno | Sysadmin Senior

Soy Alfonso, sysadmin senior con RHCE y más de 10 años administrando sistemas Linux para empresas y proyectos personales. Me especializo en infraestructura, redes, virtualización, monitoring, automation y self-hosting. Sobre SoyLinux.com SoyLinux.com es un blog técnico en español sobre Linux, software libre y self-hosting. Lo mantengo en mi tiempo libre. Áreas de expertise - Linux server administration: RHEL, CentOS, Rocky, Alma, Fedora, Debian, Ubuntu - Network services: Postfix, BIND, Samba, NFS, Squid - Virtualización y containers: KVM, Proxmox, Docker, Podman - Monitoring: Prometheus, Grafana, Zabbix - Automation: Ansible, Bash scripting - Self-hosting: Vaultwarden, Nextcloud, Jellyfin, Immich, Homelab Contacto - Email: [email protected]

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *