diff --git a/CHANGELOG.rst b/CHANGELOG.rst
new file mode 100644
index 0000000..028b76e
--- /dev/null
+++ b/CHANGELOG.rst
@@ -0,0 +1,41 @@
+=========
+Changelog
+=========
+
+All notable changes to this project will be documented in this file.
+
+The format is based on `Keep a Changelog `_, and this project adheres to Semantic Versioning.
+
+Unreleased
+----------
+
+Added
+~~~~~
+
+* **Integración de Siguldry y PKCS#11**: Implementación del rol de Ansible ``siguldry`` para desplegar los servicios Rust nativos (bridge, server y client proxy socket) y firma de RPMs real con el módulo PKCS#11 localmente compilado (``libsiguldry_pkcs11.so``) usando Sequoia PGP en el spooler de firmas.
+* **Branding de MxOS**: Planificación y documentación del roadmap para integrar la identidad visual de MxOS en Koji Web.
+* **Empaquetamiento de rust-siguldry**: Creación y validación del spec de RPM para ``rust-siguldry`` que empaqueta y distribuye formalmente la biblioteca PKCS#11 ``libsiguldry_pkcs11.so`` en el subpaquete ``siguldry-pkcs11``. Documentación del flujo de trabajo y hospedaje en ``git.mx-os.mx/RPMs/rust-siguldry`` en la rama ``c10s-mxos`` (ADR 04).
+
+[1.0.0] - 2026-06-16
+--------------------
+
+Added
+~~~~~
+
+* **Documentación Técnica**: Creación de las especificaciones técnicas del cluster, roadmap de desarrollo y Architecture Decision Records (ADRs) bajo el directorio ``docs/technical/``.
+* **Cola de Firmas en Spool**: Implementación del demonio en background ``koji-sign-spooler.service`` y migración del plugin de Koji Hub para encolar firmas en disco, evitando el término abrupto de hilos daemon bajo Apache WSGI.
+* **Idempotencia en Aprovisionamiento**: Migración de las tareas de configuración de red, actualización del sistema operativo y herramientas básicas de OpenTofu (remote-exec/file provisioners) a tareas administradas en el rol ``common`` de Ansible con rebotado automático.
+
+Changed
+~~~~~~~
+
+* **Restricción de SCM en Hub**: Configuración del parámetro ``AllowedSCMs`` en Koji Hub para restringir que las compilaciones provengan únicamente de repositorios bajo ``git.mx-os.mx/RPMs/``.
+* **Optimización en Sincronización de Fuentes**: Modificación de ``bin/sync-sources.bash`` para usar el comando estándar ``install`` y restablecer el contexto de SELinux específicamente sobre el archivo descargado en lugar de reetiquetar recursivamente todo el árbol de Dist-Git.
+
+Fixed
+~~~~~
+
+* **Mismatches de Tipos en Callback**: Corrección del plugin de firmas de Koji Hub para comparar los estados de compilación contra el entero de Koji (``koji.BUILD_STATES['COMPLETE']``) en lugar del string ``'COMPLETE'``.
+* **Configuración del Firmador en Hub**: Corrección en la lectura de la configuración de ``SigulServer`` en el plugin cargando los archivos plano de `/etc/koji-hub/hub.conf` directamente en lugar de usar el diccionario interno de Koji.
+* **Resolución DNS en Mock**: Mapeo estático en el archivo `/etc/hosts` de todos los nodos del cluster en el rol ``common`` para solventar problemas de resolución DNS de Dist-Git dentro del chroot aislado de Mock.
+* **Registro de Paquetes**: Integración de tareas de registro de paquetes en el rol ``koji_bootstrap`` para evitar excepciones de Koji cuando el paquete no está previamente catalogado en el tag.
diff --git a/README.rst b/README.rst
index ae27e37..7a1c97b 100644
--- a/README.rst
+++ b/README.rst
@@ -1,18 +1,21 @@
-=============================
-Playbook de Ansible para Koji
-=============================
+==================================
+Playbook de Ansible para MxOS Koji
+==================================
-Este playbook de Ansible automatiza el despliegue y la configuración de un sistema de compilación Koji.
+Este playbook de Ansible automatiza el despliegue, configuración y ciclo de vida de un cluster de compilación Koji local
+(Staging/PoC) para la distribución **MxOS**, un fork de CentOS Stream 10 (y CentOS Stream 11 en el futuro).
+
+El objetivo es mantener una compatibilidad estricta con la infraestructura oficial de CentOS Stream para facilitar la colaboración
+ascendente y el aprendizaje del ecosistema de empaquetado de Red Hat.
Entorno de Desarrollo
=====================
-Para empezar a desarrollar, necesitas configurar un entorno de virtualización local. Este proyecto usa OpenTofu para aprovisionar
-máquinas virtuales con libvirt.
+Para empezar a desarrollar, necesitas configurar un entorno de virtualización local. Este proyecto usa OpenTofu para aprovisionar 5
+máquinas virtuales CentOS Stream 10 con Libvirt/KVM.
Requisitos
==========
-
-Este proyecto tiene las siguientes dependencias:
+Este proyecto tiene las siguientes dependencias de software y entorno:
Software:
OpenTofu:
@@ -22,247 +25,181 @@ Software:
Para la configuración y el despliegue de Koji.
libvirt, libvirt-nss y qemu:
- Para la virtualización de las máquinas.
+ Para la virtualización de las máquinas y resolución DNS interna por nombre.
.. code-block:: bash
- # instalar
+ # instalar dependencias
dnf -y install @virtualization libvirt-nss
- # activar
+ # activar servicio de libvirt
systemctl enable --now libvirtd.socket
- # configurar
+ # configurar resolucion de nombres nss
authselect enable-feature with-libvirt
authselect check
authselect apply-changes
.. note::
- El authselect configura nsswitch para que utilice la información provista por libvirt-nss. Te permite acceder a tus
- máquinas virtuales usando su nombre en lugar de una IP. Por ejemplo, para entrar a `cs10-koji-00.mx-os` solo haces: ``ssh
- root@cs10-koji-00.mx-os`` y listo.
+ El módulo ``libvirt-nss`` permite acceder a las máquinas virtuales usando su nombre de host directo (FQDN) sin requerir IPs
+ estáticas en el inventario. Por ejemplo: ``ssh root@cs10-koji-00.mx-os``.
Colecciones de Ansible:
- El playbook depende de varias colecciones de Ansible. Puedes instalarlas con el siguiente comando:
+ Instalar las colecciones requeridas definidas en el proyecto:
.. code-block:: bash
ansible-galaxy collection install -r requirements.yaml
Sistema Operativo:
- Todo el desarrollo y las pruebas se han realizado en Fedora 43 x86_64.
+ Todo el desarrollo y las pruebas se han realizado sobre Fedora 43 x86_64 con SELinux en modo Enforcing.
Permisos de Usuario:
- Tu usuario debe ser miembro de los grupos `libvirt`, `kvm` y `qemu`.
+ Tu usuario local debe pertenecer a los grupos de virtualización:
.. code-block:: bash
usermod -aG libvirt,kvm,qemu renich
-.. note::
- Después de agregarte a los grupos, puede que necesites cerrar sesión y volver a iniciarla para que los cambios surtan efecto.
-
Configuración Inicial
=====================
-
-Antes de desplegar el entorno, necesitas realizar dos pasos de configuración:
+Antes de desplegar el entorno, debes realizar los siguientes pasos de configuración:
Credenciales de Ansible Vault:
- Antes de ejecutar el playbook, crea un archivo cifrado `vars/credentials.yaml`. Este archivo contiene datos sensibles necesarios
- para el despliegue.
+ Crea el archivo cifrado ``vars/credentials.yaml`` con las credenciales del cluster.
.. code-block:: bash
ansible-vault encrypt vars/credentials.yaml
- El archivo `credentials.yaml` debe contener las siguientes variables:
+ El archivo ``credentials.yaml`` debe contener las siguientes variables:
.. code-block:: yaml
- # Database
+ # Base de Datos
koji_db_user: koji
- koji_db_password: "your-database-password"
+ koji_db_password: "tu-contraseña-segura"
koji_db_name: koji
koji_db_network: "192.168.122.0/24"
- # Web
- web_secret: "your-web-secret"
+ # Clave Secreta Web
+ web_secret: "tu-clave-secreta-web"
- # GPG
- gpg_key_passphrase: "your-gpg-key-passphrase"
+ # Passphrase GPG
+ gpg_key_passphrase: "tu-passphrase-de-llave-gpg"
- # Mail
- kojid_smtp_pass: "your-smtp-password"
+ # Correo Electrónico
+ kojid_smtp_pass: "tu-contraseña-smtp"
.. note::
- El playbook está configurado para usar un archivo de contraseña de vault llamado `.pass`. Asegúrate de que este archivo exista en
- la raíz del proyecto y contenga tu contraseña de vault.
+ El playbook utiliza un archivo de contraseña llamado ``.pass`` en la raíz del proyecto para descifrar automáticamente el Vault
+ de Ansible. Asegúrate de crearlo y añadir la contraseña correspondiente.
Imagen Base de la VM:
- Es muy importante que tengas una imagen qcow2 de CentOS Stream 10 en `~/VirtualMachines/cs10.qcow2`. De ahí se tomará la imagen
- base para la construcción de tu cluster de desarrollo. Se puso ahí porque, al parecer, ahí se considera un espacio válido por las
- políticas de SELinux en Fedora. Puedes verlo con: ``semanage fcontext -l | grep VirtualMachines``.
+ Copia la imagen base de CentOS Stream 10 qcow2 en ``~/VirtualMachines/cs10-system.qcow2``. OpenTofu utilizará esta ruta definida
+ en ``opentofu/variables.tf`` para clonar los discos de las VMs.
Despliegue y Uso
================
-El proceso principal para levantar y usar el entorno de Koji es el siguiente:
-
-#. Aprovisiona la Infraestructura:
- Este comando usará OpenTofu para crear las máquinas virtuales necesarias (Hub, DB, NFS, Builder, Dist-Git).
+#. **Aprovisionar la Infraestructura (OpenTofu)**:
+ Crea las 5 VMs CentOS Stream 10 (Hub, DB, NFS, Builder, Dist-Git) y expande sus volúmenes de almacenamiento.
.. code-block:: bash
make init
-#. Despliega Koji:
- Una vez que la infraestructura esté lista, puedes desplegar la configuración de Koji en las VMs.
+#. **Desplegar la Configuración (Ansible)**:
+ Aplica los roles del playbook sobre los nodos virtuales aprovisionados.
.. code-block:: bash
make deploy
-#. Arranca Koji:
- Después del despliegue inicial, arranca Koji para crear las etiquetas, destinos y otros objetos necesarios.
+#. **Arrancar e Inicializar Koji**:
+ Crea los usuarios, certificados base, tags de compilación (`mxos0`), grupos y destinos necesarios en la base de datos de Koji.
- .. code-block:: bash
+ .. code-block:: bash
- make bootstrap
-
- .. note::
- Espera unos 30 segundos después de que `make init` termine antes de ejecutar `make deploy` para asegurar que las VMs estén completamente listas.
+ make bootstrap
Acceso al Entorno:
- Una vez desplegado, puedes acceder a la interfaz web de Koji en: https://cs10-koji-00.mx-os/koji
+ Puedes acceder a la interfaz de administración web en: https://cs10-koji-00.mx-os/koji
Uso del Cliente (CLI):
- El playbook configura al usuario `kojiadmin` para administrar la instancia. Para usar el CLI, conéctate al Koji Hub (`cs10-koji-00.mx-os`)
- y ejecuta los comandos de Koji.
+ Para administrar Koji, accede al nodo Hub y ejecuta comandos como `kojiadmin`:
.. code-block:: bash
ssh root@cs10-koji-00.mx-os
sudo -u kojiadmin koji moshimoshi
-Gestión de Fuentes (Dist-Git)
-=============================
-El entorno incluye un servidor Dist-Git (`cs10-koji-04.mx-os`) configurado exclusivamente como **Lookaside Cache** para almacenar archivos grandes (tarballs). Los repositorios Git con los archivos SPEC y parches se alojan externamente en `https://git.mx-os.mx`.
+Gestión de Fuentes (Lookaside Cache)
+====================================
+El servidor Dist-Git (``cs10-koji-04.mx-os``) actúa únicamente como **Lookaside Cache** para tarballs pesados. Los metadatos y
+archivos de especificación (`.spec`) de los paquetes se mantienen en repositorios Git independientes bajo ``git.mx-os.mx/RPMs/``.
Sincronización de Fuentes:
- Para que Koji pueda construir paquetes, los archivos fuentes grandes deben estar presentes en el Lookaside Cache local. Hemos incluido scripts para sincronizar estos archivos desde los repositorios upstream (CentOS Stream).
+ Para compilar paquetes, los tarballs deben existir en el lookaside cache.
**Sincronizar un paquete individual:**
.. code-block:: bash
- # Uso: ./bin/sync-sources.bash [rama]
- # Ejemplo para bash en la rama c10s:
./bin/sync-sources.bash bash c10s
- **Sincronizar todos los paquetes:**
- Este script obtiene la lista de todos los repositorios en la organización `RPMs` de `git.mx-os.mx` y los sincroniza uno por uno.
+ **Sincronizar todos los paquetes de MxOS:**
+ Obtiene los repositorios del servidor Git de MxOS y sincroniza sus fuentes automáticamente:
.. code-block:: bash
./bin/sync-all-sources.bash
- *Nota: Estos scripts se ejecutan desde tu máquina local y se conectan al servidor dist-git vía SSH para realizar la descarga.*
+Firma de paquetes
+=================
+Para cumplir con los estándares de seguridad de CentOS Stream, las compilaciones exitosas se firman criptográficamente mediante
+Sigul.
-Ejemplos de Comandos `koji`:
- Interfaz Web:
- Visita https://cs10-koji-00.mx-os/koji en tu navegador para acceder a la interfaz web de Koji. Aquí puedes ver compilaciones, tareas, paquetes, etc.
+Flujo de Firma en Staging
+-------------------------
+#. Koji Hub finaliza una compilación en estado `COMPLETE` y activa el plugin ``sigul_sign.py``.
+#. El plugin encola el ID de compilación en el spooler local en ``/var/spool/koji-sign/queue/``.
+#. El demonio independiente ``koji-sign-spooler.service`` detecta el archivo, espera a que finalice la transacción en base de datos,
+ envía el RPM al servidor de firmas Sigul (simulado por Flask en staging) e importa la firma criptográfica de vuelta a Koji.
+#. *Nota:* En la siguiente fase, el servidor Flask simulado se migrará a una instalación real del puente y servidor de Sigul.
- Monitorear una Compilación:
- Usa el comando `koji taskinfo ` para ver el estado de una tarea específica. También puedes usar la interfaz web.
+Documentación del Proyecto
+==========================
+La documentación técnica detallada, especificaciones y justificaciones de diseño se encuentran en el directorio ``docs/``.
- Hacer una Compilación:
- Para compilar un paquete, referencia el repositorio Git externo:
-
- .. code-block:: bash
-
- koji build mxos0-build "git+https://git.mx-os.mx/RPMs/bash.git?#c10s"
-
- Listar Tareas:
- `koji list-tasks` para ver tareas activas.
-
- Listar Etiquetas:
- `koji list-tags` para ver etiquetas disponibles.
+Estructura de Documentos
+------------------------
+* ``docs/technical/spec.rst``: Especificaciones técnicas del cluster de MxOS Koji.
+* ``docs/technical/roadmap.rst``: Fases de igualación a CentOS Stream y mejoras de infraestructura futuras.
+* ``docs/technical/decisiones/``: Architecture Decision Records (ADRs) que justifican las decisiones clave de diseño del proyecto.
Desarrollo (Tareas de Makefile)
===============================
-Este proyecto incluye un `GNUmakefile` para agilizar las tareas de desarrollo y mantenimiento comunes:
+El ``GNUmakefile`` en la raíz del proyecto facilita las tareas de desarrollo comunes:
-`make init`:
+``make init``:
Destruye la infraestructura existente con OpenTofu y luego la recrea.
-`make deploy`:
+``make deploy``:
Ejecuta el playbook de despliegue principal.
-`make bootstrap`:
+``make bootstrap``:
Ejecuta el playbook de arranque para configurar los objetos de Koji.
-`make reset`:
+``make reset``:
Ejecuta el playbook de reseteo.
-`make regenerate-certs`:
- Limpia el directorio `certs/` y luego ejecuta las tareas de Ansible necesarias para regenerar todos los certificados.
-
-`make clean-certs`:
- Elimina todos los archivos en el directorio `certs/` excepto el archivo `.gitignore`.
-
-`make tofu-init`:
- Inicializa el directorio de trabajo de OpenTofu.
-
-`make tofu-apply`:
- Crea o actualiza la infraestructura definida en los archivos de OpenTofu.
-
-`make tofu-destroy`:
- Destruye la infraestructura gestionada por OpenTofu.
-
-
-Para más comandos, consulta la documentación de Koji en https://docs.pagure.org/koji/.
-
-
-Firma de paquetes
-=================
-Este playbook configura Koji Hub para la firma automatizada de RPM a través de un servidor Sigul externo. Para fines de prueba,
-despliega un servidor Sigul simulado en el host de Koji Hub (`cs10-koji-00.mx-os`) que simula una firma exitosa.
-
-Cómo funciona la firma automatizada
------------------------------------
-#. Koji completa una compilación.
-#. Koji activa un complemento de Koji Hub (`sigul_sign.py`).
-#. El complemento envía una solicitud al servidor Sigul configurado (actualmente el servidor simulado en
- `http://localhost:8080/sigul`).
-#. El servidor Sigul "firma" el RPM (en el caso del servidor simulado, devuelve una respuesta de éxito y datos de firma simulados).
- El complemento importa la firma de nuevo en Koji.
-
-
-Características Adicionales
-===========================
-Este playbook configura varias características adicionales para mejorar la mantenibilidad y seguridad de Koji:
-
-Recolección de Basura:
- Un servicio de systemd (`koji-cleanup.timer`) se ejecuta semanalmente para limpiar compilaciones antiguas y directorios de
- trabajo, ayudando a conservar el espacio en disco.
-
-Notificaciones:
- Koji está configurado para registrar todos los eventos en `/var/log/koji_callbacks.log`. Este es un sistema de notificación
- básico que puede ser extendido para integrarse con otros sistemas.
-
-Permisos Granulares:
- Se crea un usuario `builder` con permisos limitados. Este usuario solo puede realizar compilaciones, lo que mejora la seguridad
- al limitar el acceso a funciones administrativas.
-
-Autoincremento de NVR:
- La etiqueta de compilación `mxos0-build` está configurada para incrementar automáticamente el número de release de los paquetes,
- simplificando el proceso de compilación.
-
+``make regenerate-certs``:
+ Limpia el directorio ``certs/`` y luego ejecuta las tareas de Ansible para regenerar todos los certificados.
Referencias
===========
* https://docs.pagure.org/koji/server_howto/
* https://docs.pagure.org/koji/database_howto/
-* https://docs.pagure.org/koji/external_repo_server_bootstrap/
* https://docs.pagure.org/koji/server_bootstrap/
-* https://docs.pagure.org/koji/using_the_koji_build_system/
\ No newline at end of file
+* https://docs.pagure.org/koji/using_the_koji_build_system/
diff --git a/ansible.cfg b/ansible.cfg
index f82c18f..ce5b257 100644
--- a/ansible.cfg
+++ b/ansible.cfg
@@ -6,7 +6,7 @@ remote_user = root
# optimization
use_persistent_connections = True
forks = 50
-strategy = free
+# strategy = free
# facts
gathering = smart
diff --git a/bin/sync-sources.bash b/bin/sync-sources.bash
index 09be4d9..c1c0a8a 100755
--- a/bin/sync-sources.bash
+++ b/bin/sync-sources.bash
@@ -120,17 +120,17 @@ while read -r line; do
exit 1
fi
- # Move to Cache
- mkdir -p "$(dirname "${LOCAL_PATH}")"
- mv "${TEMP_DIR}/${filename_part}" "${LOCAL_PATH}"
+ # Move and Set Permissions/SELinux context efficiently
+ install -D -o git -g git -m 644 "${TEMP_DIR}/${filename_part}" "${LOCAL_PATH}"
- # Set Permissions
- chown -R git:git "${DIST_GIT_CACHE_DIR}/rpms"
- chmod 644 "${LOCAL_PATH}"
- find "${DIST_GIT_CACHE_DIR}/rpms" -type d -exec chmod 755 {} +
+ # Restore SELinux context specifically for the new file if needed
+ if command -v restorecon >/dev/null 2>&1; then
+ restorecon "${LOCAL_PATH}"
+ fi
echo " - Successfully imported."
+
done < "${TEMP_DIR}/sources"
echo "Done."
\ No newline at end of file
diff --git a/bootstrap.yaml b/bootstrap.yaml
index 14f25aa..ec436c1 100644
--- a/bootstrap.yaml
+++ b/bootstrap.yaml
@@ -7,3 +7,13 @@
- vars/credentials.yaml
roles:
- koji_bootstrap
+
+- name: Restart Koji Builders
+ hosts: kojibuilders
+ gather_facts: false
+ tasks:
+ - name: Restart kojid service
+ ansible.builtin.service:
+ name: kojid
+ state: restarted
+
diff --git a/deploy.yaml b/deploy.yaml
index 2bc9e1b..ccd6183 100644
--- a/deploy.yaml
+++ b/deploy.yaml
@@ -61,8 +61,8 @@
tags: koji_hub
- role: kojira
tags: kojira
- - role: mock_sigul_server
- tags: mock-sigul-server
+ - role: siguldry
+ tags: siguldry
- name: "Deploy: Koji Builders"
hosts: kojibuilders
diff --git a/docs/technical/decisiones/2026-06-16-01-migracion_sigul_y_lookaside_centos.rst b/docs/technical/decisiones/2026-06-16-01-migracion_sigul_y_lookaside_centos.rst
new file mode 100644
index 0000000..0ded089
--- /dev/null
+++ b/docs/technical/decisiones/2026-06-16-01-migracion_sigul_y_lookaside_centos.rst
@@ -0,0 +1,38 @@
+=============================================================
+Decisión de Diseño: Migración a Sigul Real y Lookaside CentOS
+=============================================================
+
+Este registro documenta la decisión de diseño de migrar a una infraestructura real de Sigul, estandarizar el lookaside cache para asemejarse a CentOS Stream, y posponer la autenticación basada en Kerberos en el entorno de staging.
+
+Contexto
+--------
+
+Para la distribución MxOS, es crucial emular la infraestructura de CentOS Stream 10 para garantizar compatibilidad, permitir la colaboración ascendente, y facilitar un entorno realista de aprendizaje.
+
+Durante el desarrollo de la prueba de concepto (PoC) en staging:
+* Se implementó un emulador de firmas basado en una aplicación Flask simulada.
+* Se utilizó un script de descarga de fuentes personalizado en Mock que imita la estructura de CentOS Stream pero carece de integración oficial de herramientas de empaquetado.
+* Se consideró la integración de Kerberos (GSSAPI) para la autenticación en Koji. Sin embargo, no se dispone de un servidor FreeIPA inmediatamente y la máquina anfitrión de staging cuenta con recursos de hardware limitados que dificultan la ejecución de un nodo de identidad adicional.
+
+Decisión
+--------
+
+Se han tomado las siguientes decisiones de diseño:
+
+* **Migrar a Sigul Real**: Reemplazar el servidor Flask simulado por una implementación real de Sigul, compuesta por los demonios `sigul-server` (aislado) y `sigul-bridge` (intermediario), junto con el cliente `sigul` en lote.
+* **Estandarizar Lookaside Cache**: Alinear la estructura del lookaside cache y el script de obtención de fuentes con la herramienta estándar `get_sources.sh` y el formato oficial del archivo `sources` de CentOS Stream.
+* **Posponer Kerberos**: Mantener la autenticación en Koji mediante certificados SSL (TLS) en staging, y documentar el diseño de Kerberos/FreeIPA para implementaciones de producción de alta disponibilidad en el roadmap técnico.
+
+Justificación
+-------------
+
+* **Seguridad y Paridad**: El flujo de firmas real con Sigul previene que las llaves GPG de producción estén en contacto con Koji Hub o con Apache mod_wsgi, manteniendo las llaves seguras e igualando la arquitectura de producción de Red Hat/CentOS.
+* **Limitación de Recursos**: Prescindir de FreeIPA en staging permite que el entorno local se ejecute sin sobrecargar de memoria y procesamiento la máquina anfitrión, mientras se mantiene el foco en el flujo de compilación y empaquetado.
+* **Facilidad de Colaboración**: La estandarización de los metadatos Git y el lookaside cache asegura que los repositorios de paquetes de MxOS puedan interactuar limpiamente con otras instancias de Koji o herramientas de CentOS Stream.
+
+Consecuencias
+-------------
+
+* Se requiere configurar una PKI adicional para la comunicación segura entre el Hub, el puente y el servidor de Sigul.
+* Se deben actualizar los playbooks de Ansible para configurar Sigul y remover el servidor mock Flask.
+* La autenticación por certificados SSL (TLS) seguirá siendo el mecanismo principal para las conexiones de administración y compilación en staging.
diff --git a/docs/technical/decisiones/2026-06-16-02-bloqueo_python_nss_opciones_sigul.rst b/docs/technical/decisiones/2026-06-16-02-bloqueo_python_nss_opciones_sigul.rst
new file mode 100644
index 0000000..15a3f69
--- /dev/null
+++ b/docs/technical/decisiones/2026-06-16-02-bloqueo_python_nss_opciones_sigul.rst
@@ -0,0 +1,46 @@
+=============================================================
+Decisión de Diseño: Bloqueo de python-nss y Opciones de Firma
+=============================================================
+
+Este registro documenta el hallazgo técnico del bloqueo de compilación de ``python-nss`` en CentOS Stream 10 y evalúa las opciones de diseño para implementar la firma de paquetes RPM.
+
+Contexto
+--------
+
+Al intentar instalar el sistema de firmas oficial de CentOS Stream (Sigul legacy), encontramos que los paquetes de dependencias críticas ``python3-nss`` y ``python3-fedora`` no están disponibles en CentOS Stream 10 / EPEL 10.
+
+Al intentar compilar ``python-nss`` directamente desde el código fuente de PyPI usando Python 3.12 y GCC 14 en el Hub VM, la compilación falló debido a un error de sintaxis C en el uso de la macro ``va_start`` en el archivo ``src/py_nspr_error.c``:
+
+.. code-block:: text
+
+ src/py_nspr_error.c:189:23: error: macro "va_start" requires 2 arguments, but only 1 given
+ 189 | va_start(vargs);
+ | ^
+
+Este error impide la instalación directa del cliente de firmas Sigul heredado en Python.
+
+Opciones de Diseño
+------------------
+
+Presentamos dos caminos de resolución para el entorno de staging:
+
+Opción A: Reconstruir Sigul Legacy (Homologación Estricta)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+* **Descripción**: Descargar el código fuente de ``python-nss``, aplicar un parche en C para corregir el uso de la macro ``va_start`` (pasándole el último argumento nombrado del método), compilar el módulo de Python manualmente e instalar el SRPM de ``sigul`` heredado de EPEL 9 con la bandera ``--nodeps``.
+* **Pros**: Utiliza exactamente el mismo software, cliente CLI y flujo de firma que CentOS Stream usa actualmente.
+* **Contras**: Requiere mantener un parche manual en C para una dependencia heredada.
+
+Opción B: Adoptar Siguldry Moderno (Futuro de Fedora/CentOS)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+* **Descripción**: Configurar los servicios nativos de ``siguldry`` (que ya está disponible como paquete RPM en EPEL 10 por estar escrito en Rust). Dado que el cliente Rust no expone una llamada CLI directa de firma de archivos para PGP, se debe configurar el módulo PKCS#11 (``libsiguldry_pkcs11.so``) y enlazar ``rpmsign`` para que firme mediante este módulo de criptografía seguro.
+* **Pros**: Es nativo de CentOS Stream 10, no tiene dependencias de Python rotas, y es la arquitectura moderna elegida por el equipo de infraestructura de Fedora.
+* **Contras**: Requiere mayor complejidad de configuración criptográfica en el cliente (PKCS#11).
+
+Decisión de SCM (Git)
+---------------------
+
+Independientemente de la opción elegida para Sigul, se decide añadir restricciones estrictas de SCM en el archivo ``/etc/koji-hub/hub.conf`` para forzar que Koji únicamente acepte tareas de compilación provenientes del servidor Git local de MxOS:
+
+.. code-block:: ini
+
+ AllowedSCMs = git.mx-os.mx:/RPMs/*.git:*:use_common_checkout
diff --git a/docs/technical/decisiones/2026-06-16-03-implementacion_siguldry_pkcs11_spooler.rst b/docs/technical/decisiones/2026-06-16-03-implementacion_siguldry_pkcs11_spooler.rst
new file mode 100644
index 0000000..b1837a0
--- /dev/null
+++ b/docs/technical/decisiones/2026-06-16-03-implementacion_siguldry_pkcs11_spooler.rst
@@ -0,0 +1,29 @@
+========================================================
+Implementación de Siguldry PKCS#11 y Spooler de Firmas
+========================================================
+
+Este registro documenta la decisión e implementación de usar la compilación cruzada local del módulo PKCS#11 de Siguldry, su distribución e integración con el spooler de firmas de Koji.
+
+Contexto
+--------
+
+En el registro `2026-06-16-02` identificamos el bloqueo de compilación de ``python-nss`` en CentOS Stream 10, imposibilitando el cliente de Sigul tradicional. Seleccionamos Siguldry (Rust) como alternativa nativa moderna.
+
+Sin embargo, al configurar el paquete RPM de ``siguldry`` en EPEL 10, encontramos que la biblioteca dinámica del módulo PKCS#11 (``libsiguldry_pkcs11.so``) no está empaquetada ni disponible en los repositorios de CentOS Stream 10 / EPEL 10, a pesar de que los servicios principales (server, bridge y client) sí están presentes.
+
+Decisión
+--------
+
+Para resolver la ausencia del módulo PKCS#11 y mantener la compatibilidad con el backend de firmas moderno de CentOS Stream 10, tomamos las siguientes decisiones:
+
+1. **Compilación en Host de Desarrollo**: Compilar localmente el sub-crate ``siguldry-pkcs11`` en la máquina de desarrollo del administrador (Fedora 43 x86_64) utilizando ``cargo build --release``.
+2. **Distribución vía Ansible**: Colocar la biblioteca compilada ``libsiguldry_pkcs11.so`` en los archivos del rol ``koji_hub`` y desplegarla en ``/usr/lib64/libsiguldry_pkcs11.so`` del Hub VM con permisos de ejecución.
+3. **Migración del Spooler a rpmsign**: Reescribir el demonio ``koji-sign-spooler`` para invocar el comando nativo ``rpmsign`` con el backend de Sequoia PGP (motor por defecto en EL10), configurando las variables de entorno ``PKCS11_MODULE_PATH`` y ``SIGULDRY_SOCKET`` en el proceso.
+4. **Almacenamiento de Credenciales con systemd**: Utilizar el directorio nativo ``/etc/credstore/`` para alojar las claves TLS y la frase de paso de GPG, permitiendo que systemd las exponga al demonio a través del mecanismo seguro de credenciales ``ImportCredential=siguldry.*``.
+
+Implicaciones
+-------------
+
+* **Seguridad Mejorada**: Las claves GPG y TLS se gestionan e importan usando los mecanismos de aislamiento modernos de systemd, sin exponer credenciales en archivos planos del sistema.
+* **Integración Transparente**: Al firmar los paquetes RPM en caliente mediante ``rpmsign`` sobre el almacenamiento NFS de Koji, extraemos la firma directamente usando ``koji.rip_rpm_sighdr`` para guardarla en la base de datos de Koji, igualando el comportamiento del Hub oficial.
+* **Independencia de Compilación en VMs**: Las máquinas virtuales de staging de Koji no requieren instalar herramientas de desarrollo pesadas como ``cargo`` o ``rustc``, manteniendo una huella de memoria ligera.
diff --git a/docs/technical/decisiones/2026-06-16-04-empaquetamiento_y_git_flujo_siguldry.rst b/docs/technical/decisiones/2026-06-16-04-empaquetamiento_y_git_flujo_siguldry.rst
new file mode 100644
index 0000000..0c8844b
--- /dev/null
+++ b/docs/technical/decisiones/2026-06-16-04-empaquetamiento_y_git_flujo_siguldry.rst
@@ -0,0 +1,46 @@
+====================================================================
+ADR 2026-06-16-04: Empaquetamiento y Flujo de Git para rust-siguldry
+====================================================================
+
+Estatus
+-------
+
+Propuesto
+
+Contexto
+--------
+
+La suite ``siguldry`` es crítica para implementar la firma de paquetes RPM en MxOS utilizando la integración de PKCS#11 con el spooler de firmas. Dado que el paquete upstream de EPEL 10 no incluye el módulo PKCS#11 (debido a limitaciones de las plantillas base de rust2rpm), necesitamos crear y mantener una versión modificada del paquete RPM que contenga el subpaquete ``siguldry-pkcs11`` (con la biblioteca ``libsiguldry_pkcs11.so``).
+
+Necesitamos definir:
+1. Dónde hospedar este repositorio en nuestra infraestructura de Git (Gitea/Forgejo en ``git.mx-os.mx``).
+2. Cómo interactuará esta versión con el sistema automatizado de sincronización ``rpms-sync``.
+3. Cuál será la estrategia para reducir la deuda técnica a largo plazo y colaborar con upstream.
+
+Decisión
+--------
+
+Adoptaremos las siguientes decisiones de diseño y organización:
+
+1. **Ubicación en Forgejo**:
+ Hospedaremos el repositorio en ``git.mx-os.mx/RPMs/rust-siguldry``. Esto mantiene el paquete clasificado junto a todos los demás RPMs de la distribución.
+
+2. **Estrategia de Ramas para rpms-sync**:
+ Seguiremos estrictamente la convención documentada en el flujo de trabajo de ``rpms-sync``:
+ * La rama ``c10s`` será un espejo limpio de sólo lectura del upstream de EPEL 10 y será gestionada exclusivamente por ``rpms-sync``.
+ * Crearemos la rama de distribución ``c10s-mxos`` a partir de ``c10s``. En esta rama aplicaremos nuestros parches al spec file (para definir el subpaquete ``siguldry-pkcs11`` y su instalación).
+ * De esta forma, las actualizaciones automatizadas de upstream se integrarán limpiamente vía merges periódicos de ``c10s`` a ``c10s-mxos`` por parte de los mantenedores, previniendo sobreescrituras accidentales.
+
+3. **Propuesta de Contribución a Upstream**:
+ Presentaremos el spec modificado al mantenedor oficial del paquete en Fedora/EPEL (en ``src.fedoraproject.org/rpms/rust-siguldry``). Dado que el módulo PKCS#11 es una funcionalidad estándar y esperada de Siguldry para la firma de RPMs, es altamente probable que sea aceptado upstream. Una vez fusionado, podremos prescindir de la rama de distribución local ``c10s-mxos`` y consumir el paquete directamente de EPEL 10.
+
+Consecuencias
+-------------
+
+* **Positivas**:
+ * Integración perfecta con el ciclo de vida y herramientas de MxOS (como ``rpms-sync``).
+ * Reducción de la deuda técnica al planear y ejecutar la contribución directa a EPEL 10.
+ * Mayor facilidad en el mantenimiento de actualizaciones de seguridad del paquete.
+
+* **Negativas / Desafíos**:
+ * Requiere un esfuerzo inicial de comunicación con los mantenedores de Fedora/EPEL y seguir sus lineamientos de empaquetamiento.
diff --git a/docs/technical/reference/siguldry_pkcs11.rst b/docs/technical/reference/siguldry_pkcs11.rst
new file mode 100644
index 0000000..f8626b7
--- /dev/null
+++ b/docs/technical/reference/siguldry_pkcs11.rst
@@ -0,0 +1,106 @@
+============================
+Siguldry PKCS#11 Integration
+============================
+
+Context and Overview
+--------------------
+
+Siguldry is a Rust-based, modern replacement for the legacy Sigul software signing service, designed to manage and isolate cryptographic keys. In CentOS Stream 10 (EL10) / EPEL 10 environments, the legacy python-based Sigul client is blocked due to compilation issues with the ``python-nss`` dependency. Siguldry resolves this by utilizing a modern Rust architecture, providing a PKCS#11 dynamic module (``libsiguldry_pkcs11.so``) to integrate with client-side signing utilities like ``rpmsign`` and Sequoia PGP.
+
+This document outlines the architecture, configuration files, systemd credentials, and RPM macro configurations required to sign RPM packages using keys held securely on a Siguldry server.
+
+Siguldry Architecture and Components
+------------------------------------
+
+The Siguldry architecture separates components into three distinct services:
+
+* **Siguldry Server**: The secure backend node housing GPG signing keys (encrypted at rest). It does not listen on any network interfaces directly; instead, it establishes outgoing TLS connections to the bridge.
+* **Siguldry Bridge**: The intermediary daemon acting as a secure proxy. It manages mutual TLS (mTLS) authenticated connections from both the server and client components, ferrying traffic between them.
+* **Siguldry Client**: The client application (or its PKCS#11 module) that requests signatures from the bridge. The PKCS#11 module (``libsiguldry_pkcs11.so``) allows PKCS#11-compatible tools to leverage keys on the server.
+
+Configuration Files
+-------------------
+
+Client-side signing setups require configuration of the connection settings and authentication backends:
+
+1. **Client Configuration (``/etc/siguldry/client.toml``)**
+ Defines the connection parameters to reach the Siguldry Bridge, including:
+ * The host and port of the bridge.
+ * Paths to the client TLS certificate, private key, and the CA certificate used for mTLS authentication.
+ * The default key name to be used for signing requests.
+
+2. **GnuPG Smartcard Daemon Override (``~/.gnupg/gpg-agent.conf``)**
+ If using the GnuPG backend for signing, the agent must load ``gnupg-pkcs11-scd`` instead of the standard ``scdaemon``:
+
+ .. code-block:: text
+
+ scdaemon-program /usr/bin/gnupg-pkcs11-scd
+
+3. **GnuPG PKCS#11 Daemon Configuration (``~/.gnupg/gnupg-pkcs11-scd.conf``)**
+ Defines the PKCS#11 provider details to load the Siguldry module:
+
+ .. code-block:: text
+
+ providers siguldry
+ provider-siguldry-library /usr/lib64/libsiguldry_pkcs11.so
+
+Environment Variables and Credentials
+-------------------------------------
+
+Siguldry leverages the modern systemd credentials mechanism to securely pass sensitive configuration files and passphrases into its services at runtime.
+
+* **``CREDENTIALS_DIRECTORY``**
+ An environment variable pointing to the system-managed directory containing decrypted credentials. In manual testing or non-systemd wrappers, this environment variable can be set manually.
+
+* **Decrypted Credentials Files**
+ Under the credentials directory, the following file targets are read by Siguldry:
+ * ``sigul-client-config``: Contains the TOML configuration payload matching client connection configurations.
+ * ``sigul-signing-key-passphrase``: Contains the plaintext passphrase used to unlock the GPG keys stored on the server.
+
+* **PKCS#11 Module Environment Variables**
+ When using Sequoia PGP or related wrappers, specify the location of the PKCS#11 driver:
+ * **``PKCS11_MODULE_PATH``**
+ Standard environment variable pointing to the module.
+ * **``SQ_PKCS11_MODULE``**
+ Sequoia-specific fallback module path variable.
+
+ *Example:*
+
+ .. code-block:: bash
+
+ export PKCS11_MODULE_PATH=/usr/lib64/libsiguldry_pkcs11.so
+
+RPM Macro Configuration for rpmsign
+-----------------------------------
+
+RPM signing backends have transitioned to be implementation-agnostic in modern RPM versions (6.0+). The legacy `%__gpg_sign_cmd` macro is deprecated and ignored. Instead, implementation-independent macros must be used.
+
+Sequoia PGP Backend (EL10 Default)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+CentOS Stream 10 uses Sequoia PGP (via ``rpm-sequoia``) as the default OpenPGP engine. Configure the signing macros in ``~/.config/rpm/macros``:
+
+.. code-block:: text
+
+ %_openpgp_sign sq
+ %_openpgp_sign_id
+
+When running ``rpmsign --addsign``, the Sequoia engine interacts with the keystore daemon (``sequoia-keystore-server``) which accesses the Siguldry PKCS#11 module loaded via ``PKCS11_MODULE_PATH``.
+
+GnuPG Backend with gnupg-pkcs11-scd
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To explicitly enforce the traditional GnuPG engine, configure ``~/.config/rpm/macros``:
+
+.. code-block:: text
+
+ %_openpgp_sign gpg
+ %_openpgp_sign_id
+
+Ensure the ``%__gpg`` macro resolves to the GnuPG binary:
+
+.. code-block:: text
+
+ %__gpg /usr/bin/gpg
+
+During execution, ``gpg-agent`` will delegate token operations to ``gnupg-pkcs11-scd``, which will sign using ``libsiguldry_pkcs11.so``.
diff --git a/docs/technical/reference/svg_conversion.rst b/docs/technical/reference/svg_conversion.rst
new file mode 100644
index 0000000..213c533
--- /dev/null
+++ b/docs/technical/reference/svg_conversion.rst
@@ -0,0 +1,105 @@
+==================================
+SVG to PNG Conversion on EL10/CS10
+==================================
+
+Context and Tooling Selection
+-----------------------------
+
+Personalizing Koji Web branding for MxOS requires converting corporate vector assets (specifically ``mxos.svg`` and ``mxos-reducido.svg``) into high-resolution PNG format (e.g. to be placed in ``images/koji.png`` within the custom theme).
+
+Because enterprise servers require lightweight and reliable packages, CentOS Stream 10 (EL10) / RHEL 10 AppStream repositories provide native utilities for graphic conversion. Additional modern utilities can be pulled from EPEL 10 or installed via the Python ecosystem.
+
+Conversion Tools and Usage
+--------------------------
+
+Three primary methods are available on CentOS Stream 10 to perform SVG to PNG conversion:
+
+rsvg-convert (librsvg2-tools)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This is the recommended, native tool for server environments. It is extremely lightweight, fast, and does not require a graphical display server (X11/Wayland).
+
+* **Package installation**:
+
+ .. code-block:: bash
+
+ sudo dnf install librsvg2-tools
+
+* **Command usage**:
+ To perform a simple conversion, use the output flag (``-o``):
+
+ .. code-block:: bash
+
+ rsvg-convert -o koji.png mxos.svg
+
+* **Sizing and aspect ratio preservation**:
+ Specify target dimensions directly. Use the ``--keep-aspect-ratio`` flag to prevent stretching:
+
+ .. code-block:: bash
+
+ rsvg-convert -w 250 --keep-aspect-ratio -o koji.png mxos.svg
+
+Inkscape CLI
+~~~~~~~~~~~~
+
+Inkscape is a powerful editor, but is heavier than ``rsvg-convert``. It is useful if complex SVG features (like certain embedded filters or text styling) do not render correctly in ``rsvg-convert``.
+
+* **Package installation**:
+ Depending on EPEL 10 availability, install via DNF or use Flatpak as an isolated container:
+
+ .. code-block:: bash
+
+ sudo dnf install inkscape
+
+* **Command usage**:
+ The modern Inkscape command-line structure uses the ``-o`` flag:
+
+ .. code-block:: bash
+
+ inkscape -o koji.png mxos.svg
+
+* **Sizing**:
+ Resize the export using the ``-w`` (width) or ``-h`` (height) options:
+
+ .. code-block:: bash
+
+ inkscape -w 250 -o koji.png mxos.svg
+
+CairoSVG (Python Library)
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+CairoSVG is a Python-based SVG-to-PDF/PNG converter. It is not packaged as a native RPM in base EL10 repositories, but is useful for scripted configurations.
+
+* **Installation**:
+ Install using Python's package manager within a virtual environment or on the user-level path:
+
+ .. code-block:: bash
+
+ pip3 install --user cairosvg
+
+* **Command usage**:
+
+ .. code-block:: bash
+
+ cairosvg mxos.svg -o koji.png
+
+* **Python Code integration**:
+
+ .. code-block:: python
+
+ import cairosvg
+ cairosvg.svg2png(url='mxos.svg', write_to='koji.png', output_width=250)
+
+Application for Koji Web Branding
+---------------------------------
+
+To personalize the Koji Web theme for MxOS:
+
+1. Convert the logo vector to the required logo size (standard Koji logos are usually around 100-250px wide):
+
+ .. code-block:: bash
+
+ rsvg-convert -w 250 --keep-aspect-ratio -o koji.png mxos.svg
+ rsvg-convert -w 64 --keep-aspect-ratio -o koji-reducido.png mxos-reducido.svg
+
+2. Copy the resulting PNG assets into the custom Koji Web static theme directory (e.g. ``/usr/share/koji-web/static/themes/mxos/images/koji.png``).
diff --git a/docs/technical/roadmap.rst b/docs/technical/roadmap.rst
new file mode 100644
index 0000000..1208935
--- /dev/null
+++ b/docs/technical/roadmap.rst
@@ -0,0 +1,48 @@
+==================================
+Roadmap de Desarrollo de MxOS Koji
+==================================
+
+Este roadmap define el camino a seguir para el desarrollo y optimización de la infraestructura de compilación de MxOS. El enfoque prioritario es lograr la compatibilidad absoluta con CentOS Stream 10 antes de introducir optimizaciones adicionales.
+
+Igualacion a CentOS Stream
+--------------------------
+
+El primer hito es replicar los mecanismos de compilación y firma oficiales para ser compatibles con CentOS Stream:
+
+* **Estandarización de Fuentes (Lookaside Cache)**:
+ * Adoptar el script oficial `get_sources.sh` o su equivalente compatible con el formato de la caché lookaside.
+ * Modificar los scripts de sincronización locales para integrarse con la estructura oficial.
+* **Firma con Sigul Oficial**:
+ * Desplegar en staging el servidor y el puente de Sigul (`sigul-server` y `sigul-bridge`) usando certificados TLS dedicados y políticas de ACL de Sigul.
+ * Configurar e importar llaves GPG reales de pruebas en el servidor de Sigul.
+ * Reescribir el plugin de Koji Hub (`sigul_sign.py`) para invocar el cliente `sigul` oficial.
+* **Políticas de SCM en el Hub**:
+ * Restringir el Koji Hub mediante la propiedad `AllowedSCMs` para forzar las compilaciones a partir de los repositorios Git en `git.mx-os.mx/RPMs/`.
+
+Branding de Koji a MxOS
+-----------------------
+
+Para personalizar la interfaz de Koji Web con la identidad gráfica de MxOS (utilizando los recursos de logotipos en SVG en la raíz del proyecto):
+
+* **Conversión de Recursos Gráficos**:
+ * Utilizar herramientas nativas de CentOS Stream 10 para convertir los archivos de logotipos SVG (``mxos.svg`` y ``mxos-reducido.svg``) a formato PNG con la resolución adecuada para Koji Web.
+* **Creación de Tema para Koji Web**:
+ * Implementar el rol de Ansible para crear el directorio del tema en `/usr/share/koji-web/static/themes/mxos/`.
+ * Copiar el logotipo de MxOS en `images/koji.png` dentro del tema.
+ * Personalizar la hoja de estilos `koji.css` del tema mxos para aplicar la paleta de colores y estilos corporativos.
+* **Configuración del Tema en el Servidor**:
+ * Configurar `KojiTheme = mxos` y `SiteName = MxOS Koji` en `/etc/kojiweb/web.conf` a través de las plantillas del rol `koji_hub`.
+
+Planes de Mejora y Escalabilidad
+--------------------------------
+
+Una vez alcanzada la paridad técnica con CentOS Stream, se procederá con las siguientes mejoras de infraestructura y automatización:
+
+* **Asincronía vía Bus de Mensajes**:
+ * Reemplazar los callbacks locales por la emisión de mensajes AMQP mediante `koji-messagebus` (RabbitMQ).
+ * Crear un servicio consumidor de firmas autónomo que escuche los eventos de compilación terminada para firmar paquetes asíncronamente sin afectar a Apache.
+* **Autenticación Centralizada (Kerberos)**:
+ * Integrar FreeIPA o Red Hat IDM para autenticar de manera segura a los builders y desarrolladores.
+ * Configurar `sssd` en los nodos y configurar `mod_auth_gssapi` en Koji Hub y Koji Web.
+* **Múltiples Builders**:
+ * Escalar horizontalmente el número de Koji Builders activos, asociándolos a canales especializados (como compilar arquitecturas secundarias o tareas prioritarias).
diff --git a/docs/technical/spec.rst b/docs/technical/spec.rst
new file mode 100644
index 0000000..248aa8b
--- /dev/null
+++ b/docs/technical/spec.rst
@@ -0,0 +1,43 @@
+===================================
+Especificación Técnica de MxOS Koji
+===================================
+
+Este documento detalla las especificaciones técnicas y requisitos de diseño del cluster de compilación de MxOS. El objetivo es mantener una compatibilidad estricta con la infraestructura de CentOS Stream 10 para facilitar la colaboración e integración de paquetes.
+
+Arquitectura y Componentes
+--------------------------
+
+El entorno está compuesto por 5 roles principales distribuidos en nodos independientes ejecutados bajo CentOS Stream 10:
+
+* **Koji Hub**: Servidor web Apache con mod_wsgi expuesto por HTTPS. Recibe las llamadas XML-RPC del cliente y los constructores, y maneja el estado de la base de datos.
+* **Koji DB**: Motor relacional PostgreSQL que almacena los metadatos de las tareas, paquetes, tags y firmas.
+* **Koji NFS**: Almacenamiento compartido montado en `/mnt/koji` en los nodos pertinentes para el almacenamiento de los RPMs y repositorios.
+* **Koji Builder**: Nodo ejecutor del demonio `kojid`. Inicia entornos aislados chroot de Mock para compilar los RPMs.
+* **Lookaside Cache (Dist-Git)**: Servidor web Apache con scripts CGI para la descarga de tarballs y fuentes externas.
+
+Integración con Git y Lookaside Cache
+-------------------------------------
+
+Para asegurar compatibilidad con CentOS Stream, los paquetes RPM se estructuran de la siguiente manera:
+
+* **Repositorios Git**: Hospedados bajo la organización `RPMs` en `git.mx-os.mx/RPMs/.git`.
+* **Filtro de SCM (AllowedSCMs)**: El archivo `/etc/koji-hub/hub.conf` restringe las compilaciones a repositorios oficiales:
+
+ .. code-block:: ini
+
+ AllowedSCMs = git.mx-os.mx:/RPMs/*.git:*:use_common_checkout
+
+* **Archivo sources**: Cada repositorio Git contiene un archivo `sources` en la raíz del proyecto. El formato del archivo es:
+
+ .. code-block:: text
+
+ SHA512 (tarball.tar.gz) = hash_sha512_completo
+
+Firma de Paquetes con Sigul
+---------------------------
+
+La firma de los paquetes resultantes se realiza de manera segura y aislada usando la arquitectura oficial de Sigul:
+
+* **Sigul Server**: Nodo aislado donde reside el llavero GPG con la llave privada.
+* **Sigul Bridge**: Nodo intermediario en una zona de red restringida que recibe solicitudes del cliente y las despacha al servidor.
+* **Sigul Client**: Cliente CLI (`sigul`) instalado en el Koji Hub. Ejecuta solicitudes de firma mediante llamadas autenticadas por certificados SSL TLS y ACLs.
diff --git a/opentofu/cs10-base.tf b/opentofu/cs10-base.tf
index 68b748c..e28743b 100644
--- a/opentofu/cs10-base.tf
+++ b/opentofu/cs10-base.tf
@@ -102,39 +102,14 @@ resource "libvirt_domain" "cs10-base" {
}
}
- # configuración por scripts
- provisioner "file" {
- source = "scripts"
- destination = "/tmp"
- connection {
- type = "ssh"
- host = self.name
- timeout = "2m"
- }
- }
-
- provisioner "remote-exec" {
- inline = [
- "bash /tmp/scripts/set-hostname.bash ${self.name}",
- "bash /tmp/scripts/dnf-conf.bash",
- "bash /tmp/scripts/install-tools.bash",
- "bash /tmp/scripts/upgrade.bash"
- ]
-
- connection {
- type = "ssh"
- user = "root"
- host = self.name
- timeout = "2m"
- }
- }
}
resource "libvirt_volume" "cs10-base" {
count = var.guest_count
name = "${var.guest_base_name}-${format("%02d", count.index)}.${var.domain}-system.qcow2"
pool = var.pool_name
+ capacity = 32212254720
target = {
format = {
type = "qcow2"
diff --git a/opentofu/variables.tf b/opentofu/variables.tf
index aa21360..24e1358 100644
--- a/opentofu/variables.tf
+++ b/opentofu/variables.tf
@@ -23,7 +23,7 @@ variable "domain" {
variable "base_image_path" {
description = "Path to the base qcow2 image"
type = string
- default = "/home/renich/VirtualMachines/cs10.qcow2"
+ default = "/home/renich/VirtualMachines/cs10-system.qcow2"
}
# resources
diff --git a/roles/common/tasks/main.yaml b/roles/common/tasks/main.yaml
index 1177ae6..d6c4e8c 100644
--- a/roles/common/tasks/main.yaml
+++ b/roles/common/tasks/main.yaml
@@ -147,3 +147,60 @@
key: "{{ item.stdout }}"
state: present
loop: "{{ scanned_keys.results }}"
+
+- name: Add all Koji node hosts to /etc/hosts
+ ansible.builtin.lineinfile:
+ path: /etc/hosts
+ line: "{{ hostvars[item].ansible_default_ipv4.address }} {{ item }}"
+ regexp: ".*{{ item }}"
+ state: present
+ loop: "{{ groups['all'] }}"
+ when:
+ - inventory_hostname != 'localhost'
+ - item != 'localhost'
+ - hostvars[item].ansible_default_ipv4.address is defined
+
+- name: Configure DNF speed and mirrors
+ ansible.builtin.blockinfile:
+ path: /etc/dnf/dnf.conf
+ marker: "# {mark} ANSIBLE MANAGED DNF CONFIG"
+ block: |
+ max_parallel_downloads = 10
+ minrate=1M
+ timeout=10
+ when: inventory_hostname != 'localhost'
+
+- name: Install EPEL release repository
+ ansible.builtin.dnf:
+ name: epel-release
+ state: present
+ when: inventory_hostname != 'localhost'
+
+- name: Install system utility tools
+ ansible.builtin.dnf:
+ name:
+ - bash-completion
+ - htop
+ - iftop
+ - iotop
+ - rsync
+ - tmux
+ - vim-enhanced
+ state: present
+ when: inventory_hostname != 'localhost'
+
+- name: Perform full package upgrade and distro-sync
+ ansible.builtin.dnf:
+ name: "*"
+ state: latest
+ register: dnf_upgrade_result
+ when: inventory_hostname != 'localhost'
+
+- name: Reboot if packages were upgraded
+ ansible.builtin.reboot:
+ reboot_timeout: 600
+ when:
+ - inventory_hostname != 'localhost'
+ - dnf_upgrade_result.changed
+
+
diff --git a/roles/koji_bootstrap/tasks/main.yaml b/roles/koji_bootstrap/tasks/main.yaml
index 53982e7..ddb5170 100644
--- a/roles/koji_bootstrap/tasks/main.yaml
+++ b/roles/koji_bootstrap/tasks/main.yaml
@@ -80,20 +80,48 @@
changed_when: "'External repository created' in koji_bootstrap_add_epel_repo_result.stdout"
failed_when: "koji_bootstrap_add_epel_repo_result.rc != 0 and 'already exists' not in koji_bootstrap_add_epel_repo_result.stderr"
+- name: Add CentOS Stream 10 CRB external repository
+ ansible.builtin.command: >
+ sudo -u {{ koji_admin_user }} koji
+ --config={{ koji_hub_admin_user_koji_dir }}/config
+ add-external-repo -t {{ koji_build_tag }}-build centos-stream-10-crb
+ https://mirror.stream.centos.org/10-stream/CRB/x86_64/os/
+ register: koji_bootstrap_add_crb_repo_result
+ changed_when: "'External repository created' in koji_bootstrap_add_crb_repo_result.stdout"
+ failed_when: "koji_bootstrap_add_crb_repo_result.rc != 0 and 'already exists' not in koji_bootstrap_add_crb_repo_result.stderr"
+
- name: Download repomd.xml from CentOS Stream 10 build repo
ansible.builtin.get_url:
url: https://kojihub.stream.centos.org/kojifiles/repos/c10s-build/latest/x86_64/repodata/repomd.xml
dest: /tmp/c10s-repomd.xml
+- name: Install zstd for decompression of comps
+ ansible.builtin.dnf:
+ name: zstd
+ state: present
+
- name: Extract comps.xml path
- ansible.builtin.shell: grep 'comps.xml"' /tmp/c10s-repomd.xml | head -n 1 | sed -e 's/.*href="//' -e 's/".*//'
+ ansible.builtin.shell: grep 'comps.xml' /tmp/c10s-repomd.xml | head -n 1 | sed -e 's/.*href="//' -e 's/".*//'
register: comps_path
changed_when: false
- name: Download comps.xml
ansible.builtin.get_url:
url: "https://kojihub.stream.centos.org/kojifiles/repos/c10s-build/latest/x86_64/{{ comps_path.stdout }}"
- dest: /tmp/c10s-comps.xml
+ dest: /tmp/c10s-comps-raw
+
+- name: Decompress comps.xml if needed
+ ansible.builtin.shell: |
+ set -e
+ file_type=$(file -b --mime-type /tmp/c10s-comps-raw)
+ if [[ "$file_type" == "application/zstd" ]]; then
+ unzstd -f -o /tmp/c10s-comps.xml /tmp/c10s-comps-raw
+ elif [[ "$file_type" == "application/gzip" || "$file_type" == "application/x-gzip" ]]; then
+ gunzip -c /tmp/c10s-comps-raw > /tmp/c10s-comps.xml
+ else
+ cp -f /tmp/c10s-comps-raw /tmp/c10s-comps.xml
+ fi
+ changed_when: true
- name: Import comps groups
ansible.builtin.command: >
@@ -103,6 +131,76 @@
register: koji_bootstrap_import_comps_result
changed_when: "'Groups imported' in koji_bootstrap_import_comps_result.stdout"
+- name: Create build group on tag
+ ansible.builtin.command: >
+ sudo -u {{ koji_admin_user }} koji
+ --config={{ koji_hub_admin_user_koji_dir }}/config
+ add-group {{ koji_build_tag }}-build build
+ register: koji_add_group_build_result
+ changed_when: "'Group added' in koji_add_group_build_result.stdout"
+ failed_when: "koji_add_group_build_result.rc != 0 and 'already exists' not in koji_add_group_build_result.stderr"
+
+- name: Create srpm-build group on tag
+ ansible.builtin.command: >
+ sudo -u {{ koji_admin_user }} koji
+ --config={{ koji_hub_admin_user_koji_dir }}/config
+ add-group {{ koji_build_tag }}-build srpm-build
+ register: koji_add_group_srpm_result
+ changed_when: "'Group added' in koji_add_group_srpm_result.stdout"
+ failed_when: "koji_add_group_srpm_result.rc != 0 and 'already exists' not in koji_add_group_srpm_result.stderr"
+
+- name: Populate build group packages
+ ansible.builtin.command: >
+ sudo -u {{ koji_admin_user }} koji
+ --config={{ koji_hub_admin_user_koji_dir }}/config
+ add-group-pkg {{ koji_build_tag }}-build build {{ item }}
+ loop:
+ - bash
+ - bzip2
+ - coreutils
+ - cpio
+ - diffutils
+ - findutils
+ - gawk
+ - gcc
+ - gcc-c++
+ - grep
+ - gzip
+ - info
+ - make
+ - patch
+ - redhat-rpm-config
+ - rpm-build
+ - sed
+ - tar
+ - unzip
+ - util-linux
+ - which
+ - shadow-utils
+ register: koji_add_group_pkg_build_result
+ changed_when: "'Added package' in koji_add_group_pkg_build_result.stdout"
+ failed_when: "koji_add_group_pkg_build_result.rc != 0 and 'already in group' not in koji_add_group_pkg_build_result.stderr"
+
+- name: Populate srpm-build group packages
+ ansible.builtin.command: >
+ sudo -u {{ koji_admin_user }} koji
+ --config={{ koji_hub_admin_user_koji_dir }}/config
+ add-group-pkg {{ koji_build_tag }}-build srpm-build {{ item }}
+ loop:
+ - bash
+ - gnupg2
+ - make
+ - redhat-rpm-config
+ - rpm-build
+ - shadow-utils
+ - tar
+ - unzip
+ - wget
+ - curl
+ register: koji_add_group_pkg_srpm_result
+ changed_when: "'Added package' in koji_add_group_pkg_srpm_result.stdout"
+ failed_when: "koji_add_group_pkg_srpm_result.rc != 0 and 'already in group' not in koji_add_group_pkg_srpm_result.stderr"
+
- name: Add Koji builder hosts
ansible.builtin.include_tasks: add_host.yaml
loop: "{{ groups['kojibuilders'] }}"
@@ -147,3 +245,15 @@
register: koji_bootstrap_add_builder_user_result
changed_when: "'User added' in koji_bootstrap_add_builder_user_result.stdout"
failed_when: "koji_bootstrap_add_builder_user_result.rc != 0 and 'already exists' not in koji_bootstrap_add_builder_user_result.stderr"
+
+- name: Add default packages to main tag
+ ansible.builtin.command: >
+ sudo -u {{ koji_admin_user }} koji
+ --config={{ koji_hub_admin_user_koji_dir }}/config
+ add-pkg --owner {{ koji_admin_user }} {{ koji_build_tag }} {{ item }}
+ loop:
+ - bash
+ register: koji_add_pkg_result
+ changed_when: "'Adding 0 packages' not in koji_add_pkg_result.stdout"
+ failed_when: koji_add_pkg_result.rc != 0
+
diff --git a/roles/koji_hub/files/koji-sign-spooler b/roles/koji_hub/files/koji-sign-spooler
new file mode 100644
index 0000000..e797715
--- /dev/null
+++ b/roles/koji_hub/files/koji-sign-spooler
@@ -0,0 +1,241 @@
+#!/usr/bin/python3
+import os
+import sys
+import time
+import glob
+import logging
+import configparser
+import subprocess
+import tempfile
+
+# Setup logging
+logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
+ handlers=[
+ logging.StreamHandler(sys.stdout)
+ ]
+)
+logger = logging.getLogger('koji-sign-spooler')
+
+SPOOL_DIR = '/var/spool/koji-sign/queue'
+FAILED_DIR = '/var/spool/koji-sign/failed'
+
+def get_siguldry_key_info():
+ """
+ Retrieves the public key and key ID/fingerprint from Siguldry.
+ """
+ env = os.environ.copy()
+ env['SIGULDRY_SOCKET'] = '/run/siguldry-client-proxy/siguldry-client-proxy.socket'
+
+ # Get key info
+ res = subprocess.run(
+ ['siguldry-client', '--credentials-directory', '/etc/credstore', '-c', '/etc/siguldry/client.toml', 'key', 'get', 'mxos-signing-key', '--certificates'],
+ env=env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True
+ )
+ if res.returncode != 0:
+ logger.error(f"Failed to get key info from Siguldry: {res.stderr}")
+ return None, None
+
+ output = res.stdout
+ start_marker = "-----BEGIN PGP PUBLIC KEY BLOCK-----"
+ end_marker = "-----END PGP PUBLIC KEY BLOCK-----"
+
+ start_idx = output.find(start_marker)
+ end_idx = output.find(end_marker)
+ if start_idx == -1 or end_idx == -1:
+ logger.error("Could not find PGP public key block in Siguldry client output.")
+ return None, None
+
+ pubkey = output[start_idx:end_idx + len(end_marker)]
+
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.pub', delete=False) as f:
+ f.write(pubkey)
+ tmp_name = f.name
+
+ try:
+ gpg_res = subprocess.run(
+ ['gpg', '--show-keys', '--with-colons', tmp_name],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True
+ )
+ if gpg_res.returncode != 0:
+ logger.error(f"Failed to read GPG key ID: {gpg_res.stderr}")
+ return None, None
+
+ key_id = None
+ fingerprint = None
+ for line in gpg_res.stdout.splitlines():
+ parts = line.split(':')
+ if parts[0] == 'pub':
+ key_id = parts[4][-8:]
+ elif parts[0] == 'fpr':
+ fingerprint = parts[9]
+ break
+
+ sig_key_id = fingerprint if fingerprint else key_id
+ return pubkey, sig_key_id
+ finally:
+ try:
+ os.remove(tmp_name)
+ except Exception:
+ pass
+
+def process_queue():
+ if not os.path.exists(SPOOL_DIR):
+ return
+
+ # Import kojihub dynamically so that it loads after service starts
+ import kojihub
+ import koji
+
+ # Read configuration file to get KojiDir
+ config = configparser.ConfigParser()
+ config_files = ['/etc/koji-hub/hub.conf'] + sorted(glob.glob('/etc/koji-hub/hub.conf.d/*.conf'))
+ config.read(config_files)
+
+ koji_dir = config.get('hub', 'KojiDir', fallback='/mnt/koji')
+
+ # Get list of files in spool directory
+ files = sorted(os.listdir(SPOOL_DIR))
+ if not files:
+ return
+
+ # Connect to database
+ try:
+ kojihub.db.connect()
+ except Exception as e:
+ logger.error(f"Failed to connect to Koji Hub DB: {e}")
+ return
+
+ for filename in files:
+ file_path = os.path.join(SPOOL_DIR, filename)
+ if not os.path.isfile(file_path):
+ continue
+
+ try:
+ build_id = int(filename)
+ except ValueError:
+ logger.error(f"Invalid build ID in spool file name: {filename}")
+ os.rename(file_path, os.path.join(FAILED_DIR, filename))
+ continue
+
+ with open(file_path, 'r') as f:
+ build_nvr = f.read().strip()
+
+ logger.info(f"Processing build {build_id} ({build_nvr}) for signing")
+
+ # Retry loop to wait for the completeBuild transaction to commit
+ rpms = []
+ for attempt in range(5):
+ try:
+ rpms = kojihub.list_rpms(buildID=build_id)
+ if rpms:
+ break
+ except Exception as e:
+ logger.warning(f"Error querying RPMs (attempt {attempt+1}): {e}")
+ logger.info(f"RPMs not found yet for build {build_id}, waiting 1 second... (attempt {attempt+1}/5)")
+ time.sleep(1)
+
+ if not rpms:
+ logger.error(f"No RPMs found for build {build_id} after retries. Moving spool file to failed.")
+ os.makedirs(FAILED_DIR, exist_ok=True)
+ os.rename(file_path, os.path.join(FAILED_DIR, filename))
+ continue
+
+ # Get Siguldry key information
+ pubkey, sig_key_id = get_siguldry_key_info()
+ if not sig_key_id:
+ logger.error("Failed to retrieve Siguldry key ID. Skipping build signing.")
+ os.makedirs(FAILED_DIR, exist_ok=True)
+ os.rename(file_path, os.path.join(FAILED_DIR, filename))
+ continue
+
+ # Write public key to koji topdir so it can be served
+ pubkey_path = os.path.join(koji_dir, 'gpg_key.pub')
+ try:
+ with open(pubkey_path, 'w') as f:
+ f.write(pubkey)
+ logger.info(f"Wrote public GPG key to {pubkey_path}")
+ except Exception as e:
+ logger.warning(f"Failed to write public key to {pubkey_path}: {e}")
+
+ success = True
+ for rpm in rpms:
+ rpm_id = rpm['id']
+ rpm_nvr = rpm['nvr']
+ rpm_path = rpm['path']
+ full_rpm_path = os.path.join(koji_dir, rpm_path)
+
+ try:
+ logger.info(f"Signing RPM {rpm_nvr} via rpmsign and Siguldry PKCS#11")
+ env = os.environ.copy()
+ env['PKCS11_MODULE_PATH'] = '/usr/lib64/libsiguldry_pkcs11.so'
+ env['SIGULDRY_SOCKET'] = '/run/siguldry-client-proxy/siguldry-client-proxy.socket'
+
+ # Sign using rpmsign command with Sequoia backend macro definitions
+ cmd = [
+ 'rpmsign',
+ '--define', '_openpgp_sign sq',
+ '--define', f'_openpgp_sign_id {sig_key_id}',
+ '--addsign', full_rpm_path
+ ]
+
+ result = subprocess.run(
+ cmd,
+ env=env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True
+ )
+
+ if result.returncode != 0:
+ logger.error(f"rpmsign failed for {rpm_nvr}: stdout: {result.stdout}, stderr: {result.stderr}")
+ success = False
+ else:
+ logger.info(f"Successfully signed RPM {rpm_nvr} on disk. Extracting signature header.")
+ sig_data = koji.rip_rpm_sighdr(full_rpm_path)
+ kojihub.add_rpm_sig(rpm_id, sig_data, sigkey=sig_key_id)
+ logger.info(f"Successfully imported signature for {rpm_nvr} with key {sig_key_id}")
+ except Exception as e:
+ logger.error(f"Error signing RPM {rpm_nvr}: {e}")
+ success = False
+
+ if success:
+ try:
+ kojihub.context.cnx.commit()
+ logger.info(f"All signatures committed for build {build_id} ({build_nvr}).")
+ os.remove(file_path)
+ except Exception as e:
+ logger.error(f"Failed to commit signatures to database: {e}")
+ try:
+ kojihub.context.cnx.rollback()
+ except Exception:
+ pass
+ else:
+ logger.error(f"Failed to sign some/all RPMs for build {build_id}. Keeping spool file for retry.")
+ break
+
+ try:
+ kojihub.context.cnx.close()
+ except Exception:
+ pass
+
+def main():
+ logger.info("Koji Sign Spooler Daemon started")
+ os.makedirs(SPOOL_DIR, exist_ok=True)
+ os.makedirs(FAILED_DIR, exist_ok=True)
+
+ while True:
+ try:
+ process_queue()
+ except Exception as e:
+ logger.error(f"Unhandled exception in spooler loop: {e}")
+ time.sleep(5)
+
+if __name__ == '__main__':
+ main()
diff --git a/roles/koji_hub/files/koji-sign-spooler.service b/roles/koji_hub/files/koji-sign-spooler.service
new file mode 100644
index 0000000..31b7498
--- /dev/null
+++ b/roles/koji_hub/files/koji-sign-spooler.service
@@ -0,0 +1,14 @@
+[Unit]
+Description=Koji Sign Spooler Daemon
+After=httpd.service postgresql.service
+
+[Service]
+Type=simple
+ExecStart=/usr/libexec/koji-sign-spooler
+User=root
+Group=apache
+Restart=always
+RestartSec=5
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/koji_hub/files/sigul_sign.py b/roles/koji_hub/files/sigul_sign.py
index 9211376..6ef25d5 100644
--- a/roles/koji_hub/files/sigul_sign.py
+++ b/roles/koji_hub/files/sigul_sign.py
@@ -1,66 +1,43 @@
import koji
-import kojihub
-import xmlrpc.client
+from koji.plugin import callback
import logging
+import os
-logger = logging.getLogger(__name__)
+logger = logging.getLogger('koji.plugin.sigul_sign')
-class SigulSignPlugin(kojihub.Plugin):
- def __init__(self, hub):
- super(SigulSignPlugin, self).__init__(hub)
- self.sigul_server = self.hub.config.get('SigulServer')
- if not self.sigul_server:
- logger.error("SigulServer not configured in Koji Hub. Automated signing will not work.")
- return
+SPOOL_DIR = '/var/spool/koji-sign/queue'
- self.sigul_proxy = xmlrpc.client.ServerProxy(self.sigul_server)
- logger.info(f"SigulSignPlugin initialized with server: {self.sigul_server}")
+@callback('postBuildStateChange')
+def sigul_sign(cbtype, *args, **kws):
+ """
+ Called after a build state changes.
+ We will queue the build for signing if the state changed to COMPLETE.
+ """
+ if kws.get('attribute') != 'state':
+ return
+ if kws.get('new') != koji.BUILD_STATES['COMPLETE']:
+ return
- def postBuild(self, build_id, build, request, **kwargs):
- """
- Called after a build is completed.
- We will attempt to sign the RPMs in this build.
- """
- if build.get('state') != koji.BUILD_STATES['COMPLETE']:
- logger.info(f"Build {build_id} not complete, skipping signing.")
- return
+ build = kws.get('info')
+ if not build:
+ logger.error("No build info found in callback.")
+ return
- logger.info(f"Attempting to sign RPMs for build {build_id} ({build.get('nvr')})")
+ build_id = build['id']
+ build_nvr = build.get('nvr', str(build_id))
+
+ # Ensure spool directory exists
+ try:
+ if not os.path.exists(SPOOL_DIR):
+ os.makedirs(SPOOL_DIR, mode=0o770)
+ except Exception as e:
+ logger.error(f"Failed to create spool directory {SPOOL_DIR}: {e}")
+ return
- # Get RPMs associated with the build
- rpms = self.hub.getBuildRPMs(build_id)
- if not rpms:
- logger.info(f"No RPMs found for build {build_id}, skipping signing.")
- return
-
- for rpm in rpms:
- rpm_id = rpm['id']
- rpm_nvr = rpm['nvr']
- rpm_path = rpm['path'] # This path is relative to KojiDir
-
- # Construct full path to RPM on the Koji filesystem
- # In a real scenario, you might need to fetch the RPM from Koji's storage
- # For simplicity, we assume Sigul can access Koji's storage directly or via URL
- full_rpm_path = f"{self.hub.config.get('KojiDir')}/{rpm_path}"
-
- try:
- # Call Sigul to sign the RPM
- # This is a mock call. A real Sigul API call would be more complex.
- logger.info(f"Calling Sigul to sign {full_rpm_path}")
- # result = self.sigul_proxy.sign_rpm(full_rpm_path) # Example Sigul API call
- result = {'success': True, 'signature': 'mock_signature_data'} # Mock result
-
- if result.get('success'):
- logger.info(f"Successfully signed RPM {rpm_nvr}. Importing signature to Koji.")
- # Import signature back into Koji
- # This is a simplified example. Koji's import-sig expects specific data.
- # For a real plugin, you'd get the actual signature from Sigul and format it.
- self.hub.importSig(rpm_id, 'mock_sig_id', 'mock_sig_type', result['signature'])
- logger.info(f"Signature imported for {rpm_nvr}.")
- else:
- logger.error(f"Sigul failed to sign {rpm_nvr}: {result.get('error', 'Unknown error')}")
- except Exception as e:
- logger.error(f"Error signing RPM {rpm_nvr}: {e}")
-
-# Register the plugin
-kojihub.add_plugin(SigulSignPlugin)
\ No newline at end of file
+ spool_file = os.path.join(SPOOL_DIR, str(build_id))
+ try:
+ with open(spool_file, 'w') as f:
+ f.write(f"{build_nvr}\n")
+ logger.info(f"Queued build {build_id} ({build_nvr}) for signing in {spool_file}")
+ except Exception as e:
+ logger.error(f"Failed to queue build {build_id} for signing: {e}")
\ No newline at end of file
diff --git a/roles/koji_hub/handlers/main.yaml b/roles/koji_hub/handlers/main.yaml
index a5b85f5..9c06b4c 100644
--- a/roles/koji_hub/handlers/main.yaml
+++ b/roles/koji_hub/handlers/main.yaml
@@ -9,3 +9,8 @@
ansible.builtin.service:
name: firewalld
state: reloaded
+
+- name: Restart koji-sign-spooler
+ ansible.builtin.service:
+ name: koji-sign-spooler
+ state: restarted
diff --git a/roles/koji_hub/tasks/main.yaml b/roles/koji_hub/tasks/main.yaml
index 13af634..ba937e7 100644
--- a/roles/koji_hub/tasks/main.yaml
+++ b/roles/koji_hub/tasks/main.yaml
@@ -304,6 +304,52 @@
mode: "0644"
notify: Restart httpd
+- name: Copy Sigul signing plugin to Koji Hub plugins directory
+ ansible.builtin.copy:
+ src: sigul_sign.py
+ dest: /usr/lib/koji-hub-plugins/sigul_sign.py
+ owner: root
+ group: root
+ mode: "0644"
+ notify: Restart httpd
+
+- name: Create spool directories for Koji signing
+ ansible.builtin.file:
+ path: "{{ item }}"
+ state: directory
+ owner: apache
+ group: apache
+ mode: "0770"
+ loop:
+ - /var/spool/koji-sign
+ - /var/spool/koji-sign/queue
+ - /var/spool/koji-sign/failed
+
+- name: Copy Koji sign spooler daemon to libexec
+ ansible.builtin.copy:
+ src: koji-sign-spooler
+ dest: /usr/libexec/koji-sign-spooler
+ owner: root
+ group: root
+ mode: "0755"
+ notify: Restart koji-sign-spooler
+
+- name: Copy Koji sign spooler systemd service
+ ansible.builtin.copy:
+ src: koji-sign-spooler.service
+ dest: /usr/lib/systemd/system/koji-sign-spooler.service
+ owner: root
+ group: root
+ mode: "0644"
+ notify: Restart koji-sign-spooler
+
+- name: Enable and start koji-sign-spooler service
+ ansible.builtin.systemd:
+ name: koji-sign-spooler
+ state: started
+ enabled: true
+ daemon_reload: true
+
- name: Template Koji callback script
ansible.builtin.template:
src: koji_callbacks.py.j2
diff --git a/roles/koji_hub/templates/hub.conf.j2 b/roles/koji_hub/templates/hub.conf.j2
index 8a6e6ad..c48cbc6 100644
--- a/roles/koji_hub/templates/hub.conf.j2
+++ b/roles/koji_hub/templates/hub.conf.j2
@@ -9,6 +9,7 @@ DBUser = {{ koji_db_user }}
DBHost = {{ koji_db_host }}
DBPass = {{ koji_db_password }}
KojiDir = {{ koji_top_dir }}
+AllowedSCMs = {{ koji_allowed_scms | default('git.mx-os.mx:/RPMs/*.git:*:use_common_checkout') }}
## Auth-related options ##
# CheckClientIP = True
@@ -56,7 +57,8 @@ DisableNotifications = False
## Koji hub plugins
PluginPath = /usr/lib/koji-hub-plugins
-Plugins = echo
+Plugins = echo sigul_sign
+SigulServer = {{ sigul_server }}
## Debugging
KojiDebug = On
diff --git a/roles/kojid/tasks/main.yaml b/roles/kojid/tasks/main.yaml
index 652bdf6..35ca197 100644
--- a/roles/kojid/tasks/main.yaml
+++ b/roles/kojid/tasks/main.yaml
@@ -36,6 +36,13 @@
groups: mock
state: present
+- name: Ensure system user kojid is in the mock group
+ ansible.builtin.user:
+ name: kojid
+ groups: mock
+ append: true
+ state: present
+
- name: Ensure /etc/mock/koji has correct permissions
ansible.builtin.file:
path: /etc/mock/koji
@@ -107,6 +114,15 @@
owner: root
group: root
+- name: Configure global Mock bind mount for get-sources script
+ ansible.builtin.blockinfile:
+ path: /etc/mock/site-defaults.cfg
+ block: |
+ # Bind mount the MxOS source fetching wrapper globally
+ config_opts['plugin_conf']['bind_mount_enable'] = True
+ config_opts['plugin_conf']['bind_mount_opts'] = {'dirs': [('/usr/local/bin/mxos-get-sources.bash', '/usr/local/bin/mxos-get-sources.bash')]}
+ marker: "# {mark} ANSIBLE MANAGED BLOCK: MXOS BIND MOUNT"
+
- name: Configure MxOS Mock config
ansible.builtin.template:
src: mxos0-build-x86_64.cfg.j2
@@ -125,3 +141,4 @@
name: kojid
state: started
enabled: true
+
diff --git a/roles/mock_sigul_server/handlers/main.yaml b/roles/mock_sigul_server/handlers/main.yaml
deleted file mode 100644
index 9c7e6f3..0000000
--- a/roles/mock_sigul_server/handlers/main.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-- name: Reload firewalld
- ansible.builtin.service:
- name: firewalld
- state: reloaded
diff --git a/roles/mock_sigul_server/tasks/main.yaml b/roles/mock_sigul_server/tasks/main.yaml
deleted file mode 100644
index 8f33e0c..0000000
--- a/roles/mock_sigul_server/tasks/main.yaml
+++ /dev/null
@@ -1,54 +0,0 @@
----
-- name: Install Python and Flask
- ansible.builtin.dnf:
- name:
- - python3
- - python3-flask
- state: present
-
-- name: Create mock Sigul server directory
- ansible.builtin.file:
- path: /opt/mock-sigul-server
- state: directory
- mode: "0755"
-
-- name: Create mock Sigul server Python script
- ansible.builtin.copy:
- content: |
- from flask import Flask, request, jsonify
- import logging
- import os
-
- app = Flask(__name__)
- logging.basicConfig(level=logging.INFO)
-
- @app.route('/sigul', methods=['POST'])
- def sigul_endpoint():
- app.logger.info(f"Received request: {request.json}")
- # Simulate successful signing
- return jsonify({"success": True, "signature": "mock_signature_data_from_sigul"})
-
- if __name__ == '__main__':
- app.run(host='0.0.0.0', port=8080)
- dest: /opt/mock-sigul-server/app.py
- mode: "0644"
-
-- name: Create systemd service file for mock Sigul server
- ansible.builtin.template:
- src: mock-sigul-server.service.j2
- dest: /etc/systemd/system/mock-sigul-server.service
- mode: "0644"
-
-- name: Start and enable mock Sigul server service
- ansible.builtin.systemd:
- name: mock-sigul-server
- state: started
- enabled: true
- daemon_reload: true
-
-- name: Open firewall for mock Sigul server (port 8080)
- ansible.posix.firewalld:
- port: 8080/tcp
- permanent: true
- state: enabled
- notify: Reload firewalld
diff --git a/roles/mock_sigul_server/templates/mock-sigul-server.service.j2 b/roles/mock_sigul_server/templates/mock-sigul-server.service.j2
deleted file mode 100644
index 43f32a8..0000000
--- a/roles/mock_sigul_server/templates/mock-sigul-server.service.j2
+++ /dev/null
@@ -1,13 +0,0 @@
-[Unit]
-Description=Mock Sigul Server
-After=network.target
-
-[Service]
-User=root
-Group=root
-WorkingDirectory=/opt/mock-sigul-server
-ExecStart=/usr/bin/python3 /opt/mock-sigul-server/app.py
-Restart=always
-
-[Install]
-WantedBy=multi-user.target
diff --git a/roles/siguldry/tasks/main.yaml b/roles/siguldry/tasks/main.yaml
new file mode 100644
index 0000000..f5f2a04
--- /dev/null
+++ b/roles/siguldry/tasks/main.yaml
@@ -0,0 +1,266 @@
+---
+- name: "Generate: Siguldry Certificates on Localhost"
+ delegate_to: localhost
+ run_once: true
+ block:
+ - name: "Generate Siguldry CA private key"
+ community.crypto.openssl_privatekey:
+ path: "{{ koji_pki_dir }}/siguldry_ca.key"
+ size: 4096
+ mode: "0600"
+
+ - name: "Generate Siguldry CA CSR"
+ community.crypto.openssl_csr:
+ path: "{{ koji_pki_dir }}/siguldry_ca.csr"
+ privatekey_path: "{{ koji_pki_dir }}/siguldry_ca.key"
+ common_name: "Siguldry Certificate Authority"
+ organization_name: "MxOS Siguldry"
+ basic_constraints:
+ - "CA:TRUE"
+ key_usage:
+ - keyCertSign
+ - cRLSign
+
+ - name: "Generate self-signed Siguldry CA certificate"
+ community.crypto.x509_certificate:
+ path: "{{ koji_pki_dir }}/siguldry_ca.crt"
+ csr_path: "{{ koji_pki_dir }}/siguldry_ca.csr"
+ privatekey_path: "{{ koji_pki_dir }}/siguldry_ca.key"
+ provider: selfsigned
+ selfsigned_not_after: "+3650d"
+
+ - name: "Generate private key for Siguldry Server"
+ community.crypto.openssl_privatekey:
+ path: "{{ koji_pki_dir }}/siguldry-server.key"
+ size: 4096
+ mode: "0600"
+
+ - name: "Generate CSR for Siguldry Server"
+ community.crypto.openssl_csr:
+ path: "{{ koji_pki_dir }}/siguldry-server.csr"
+ privatekey_path: "{{ koji_pki_dir }}/siguldry-server.key"
+ common_name: "{{ koji_hub_fqdn }}"
+
+ - name: Create openssl config for Siguldry Server
+ ansible.builtin.copy:
+ dest: "{{ koji_pki_dir }}/openssl-siguldry-server.cnf"
+ mode: "0644"
+ content: |
+ [v3_ca]
+ basicConstraints = CA:FALSE
+ keyUsage = nonRepudiation, digitalSignature, keyEncipherment
+ extendedKeyUsage = serverAuth, clientAuth
+ subjectAltName = DNS:{{ koji_hub_fqdn }},DNS:server.example.com
+
+ - name: Sign Siguldry Server certificate
+ ansible.builtin.shell:
+ cmd: >
+ openssl x509 -req
+ -in {{ koji_pki_dir }}/siguldry-server.csr
+ -CA {{ koji_pki_dir }}/siguldry_ca.crt
+ -CAkey {{ koji_pki_dir }}/siguldry_ca.key
+ -CAcreateserial
+ -out {{ koji_pki_dir }}/siguldry-server.crt
+ -days 3650
+ -sha256
+ -extfile {{ koji_pki_dir }}/openssl-siguldry-server.cnf
+ -extensions v3_ca
+ creates: "{{ koji_pki_dir }}/siguldry-server.crt"
+
+ - name: "Generate private key for Siguldry Bridge"
+ community.crypto.openssl_privatekey:
+ path: "{{ koji_pki_dir }}/siguldry-bridge.key"
+ size: 4096
+ mode: "0600"
+
+ - name: "Generate CSR for Siguldry Bridge"
+ community.crypto.openssl_csr:
+ path: "{{ koji_pki_dir }}/siguldry-bridge.csr"
+ privatekey_path: "{{ koji_pki_dir }}/siguldry-bridge.key"
+ common_name: "{{ koji_hub_fqdn }}"
+
+ - name: Create openssl config for Siguldry Bridge
+ ansible.builtin.copy:
+ dest: "{{ koji_pki_dir }}/openssl-siguldry-bridge.cnf"
+ mode: "0644"
+ content: |
+ [v3_ca]
+ basicConstraints = CA:FALSE
+ keyUsage = nonRepudiation, digitalSignature, keyEncipherment
+ extendedKeyUsage = serverAuth
+ subjectAltName = DNS:{{ koji_hub_fqdn }},DNS:bridge.example.com
+
+ - name: Sign Siguldry Bridge certificate
+ ansible.builtin.shell:
+ cmd: >
+ openssl x509 -req
+ -in {{ koji_pki_dir }}/siguldry-bridge.csr
+ -CA {{ koji_pki_dir }}/siguldry_ca.crt
+ -CAkey {{ koji_pki_dir }}/siguldry_ca.key
+ -CAcreateserial
+ -out {{ koji_pki_dir }}/siguldry-bridge.crt
+ -days 3650
+ -sha256
+ -extfile {{ koji_pki_dir }}/openssl-siguldry-bridge.cnf
+ -extensions v3_ca
+ creates: "{{ koji_pki_dir }}/siguldry-bridge.crt"
+
+ - name: "Generate private key for Siguldry Client"
+ community.crypto.openssl_privatekey:
+ path: "{{ koji_pki_dir }}/siguldry-client.key"
+ size: 4096
+ mode: "0600"
+
+ - name: "Generate CSR for Siguldry Client"
+ community.crypto.openssl_csr:
+ path: "{{ koji_pki_dir }}/siguldry-client.csr"
+ privatekey_path: "{{ koji_pki_dir }}/siguldry-client.key"
+ common_name: "kojiadmin"
+
+ - name: Create openssl config for Siguldry Client
+ ansible.builtin.copy:
+ dest: "{{ koji_pki_dir }}/openssl-siguldry-client.cnf"
+ mode: "0644"
+ content: |
+ [v3_ca]
+ basicConstraints = CA:FALSE
+ keyUsage = nonRepudiation, digitalSignature, keyEncipherment
+ extendedKeyUsage = clientAuth
+ subjectAltName = DNS:kojiadmin
+
+ - name: Sign Siguldry Client certificate
+ ansible.builtin.shell:
+ cmd: >
+ openssl x509 -req
+ -in {{ koji_pki_dir }}/siguldry-client.csr
+ -CA {{ koji_pki_dir }}/siguldry_ca.crt
+ -CAkey {{ koji_pki_dir }}/siguldry_ca.key
+ -CAcreateserial
+ -out {{ koji_pki_dir }}/siguldry-client.crt
+ -days 3650
+ -sha256
+ -extfile {{ koji_pki_dir }}/openssl-siguldry-client.cnf
+ -extensions v3_ca
+ creates: "{{ koji_pki_dir }}/siguldry-client.crt"
+
+ - name: Clean up temporary openssl configs
+ ansible.builtin.file:
+ path: "{{ item }}"
+ state: absent
+ loop:
+ - "{{ koji_pki_dir }}/openssl-siguldry-server.cnf"
+ - "{{ koji_pki_dir }}/openssl-siguldry-bridge.cnf"
+ - "{{ koji_pki_dir }}/openssl-siguldry-client.cnf"
+
+- name: Install Siguldry package
+ ansible.builtin.dnf:
+ name:
+ - siguldry
+ - siguldry-pkcs11
+ - gnupg-pkcs11-scd
+ state: present
+
+- name: Ensure Siguldry config directory exists
+ ansible.builtin.file:
+ path: /etc/siguldry
+ state: directory
+ owner: root
+ group: siguldry
+ mode: "0750"
+
+- name: Deploy Siguldry configuration files
+ ansible.builtin.template:
+ src: "{{ item.src }}"
+ dest: "/etc/siguldry/{{ item.dest }}"
+ owner: root
+ group: siguldry
+ mode: "0640"
+ loop:
+ - { src: "server.toml.j2", dest: "server.toml" }
+ - { src: "bridge.toml.j2", dest: "bridge.toml" }
+ - { src: "client.toml.j2", dest: "client.toml" }
+
+- name: Ensure systemd credstore directory exists
+ ansible.builtin.file:
+ path: /etc/credstore
+ state: directory
+ owner: root
+ group: root
+ mode: "0700"
+
+- name: Deploy Siguldry TLS credentials to systemd credstore
+ ansible.builtin.copy:
+ src: "{{ koji_pki_dir }}/{{ item.src }}"
+ dest: "/etc/credstore/{{ item.dest }}"
+ owner: root
+ group: root
+ mode: "0600"
+ loop:
+ - { src: "siguldry_ca.crt", dest: "siguldry.ca_certificate.pem" }
+ - { src: "siguldry-server.crt", dest: "siguldry.server.certificate.pem" }
+ - { src: "siguldry-server.key", dest: "siguldry.server.private_key.pem" }
+ - { src: "siguldry-bridge.crt", dest: "siguldry.bridge.certificate.pem" }
+ - { src: "siguldry-bridge.key", dest: "siguldry.bridge.private_key.pem" }
+ - { src: "siguldry-client.crt", dest: "siguldry.client.certificate.pem" }
+ - { src: "siguldry-client.key", dest: "siguldry.client.private_key.pem" }
+
+- name: Deploy Siguldry signing key passphrase to systemd credstore
+ ansible.builtin.copy:
+ content: "{{ gpg_key_passphrase }}"
+ dest: /etc/credstore/siguldry.signing_key.passphrase
+ owner: root
+ group: root
+ mode: "0600"
+
+- name: Start and enable Siguldry services
+ ansible.builtin.systemd:
+ name: "{{ item }}"
+ state: started
+ enabled: true
+ daemon_reload: true
+ loop:
+ - siguldry-bridge.service
+ - siguldry-server.service
+ - siguldry-client-proxy.socket
+ - siguldry-signer.socket
+
+- name: Run Siguldry database migrations
+ become: true
+ become_user: siguldry
+ ansible.builtin.command:
+ cmd: siguldry-server -c /etc/siguldry/server.toml manage migrate
+ register: siguldry_migration_result
+ changed_when: "'No migrations to apply' not in siguldry_migration_result.stdout"
+
+- name: Check if remote user kojiadmin exists in Siguldry
+ become: true
+ become_user: siguldry
+ ansible.builtin.shell:
+ cmd: siguldry-server -c /etc/siguldry/server.toml manage users list
+ register: siguldry_users_list
+ changed_when: false
+
+- name: Add remote user kojiadmin to Siguldry database
+ become: true
+ become_user: siguldry
+ ansible.builtin.command:
+ cmd: siguldry-server -c /etc/siguldry/server.toml manage users create kojiadmin
+ when: "'kojiadmin' not in siguldry_users_list.stdout"
+
+- name: Check if mxos-signing-key exists in Siguldry
+ become: true
+ become_user: siguldry
+ ansible.builtin.shell:
+ cmd: siguldry-server -c /etc/siguldry/server.toml manage key list
+ register: siguldry_keys_list
+ changed_when: false
+
+- name: Create mxos-signing-key in Siguldry
+ become: true
+ become_user: siguldry
+ ansible.builtin.shell:
+ cmd: >
+ siguldry-server -c /etc/siguldry/server.toml manage key create
+ kojiadmin mxos-signing-key
+ --password-file <(echo "{{ gpg_key_passphrase }}")
+ when: "'mxos-signing-key' not in siguldry_keys_list.stdout"
diff --git a/roles/siguldry/templates/bridge.toml.j2 b/roles/siguldry/templates/bridge.toml.j2
new file mode 100644
index 0000000..08ebdbb
--- /dev/null
+++ b/roles/siguldry/templates/bridge.toml.j2
@@ -0,0 +1,8 @@
+# Siguldry bridge configuration for MxOS Koji
+server_listening_address = "[::]:44333"
+client_listening_address = "[::]:44334"
+
+[credentials]
+private_key = "siguldry.bridge.private_key.pem"
+certificate = "siguldry.bridge.certificate.pem"
+ca_certificate = "siguldry.ca_certificate.pem"
diff --git a/roles/siguldry/templates/client.toml.j2 b/roles/siguldry/templates/client.toml.j2
new file mode 100644
index 0000000..03e37e4
--- /dev/null
+++ b/roles/siguldry/templates/client.toml.j2
@@ -0,0 +1,17 @@
+# Siguldry client configuration for MxOS Koji
+server_hostname = "{{ koji_hub_fqdn }}"
+bridge_hostname = "{{ koji_hub_fqdn }}"
+bridge_port = 44334
+
+[[keys]]
+key_name = "mxos-signing-key"
+passphrase_path = "siguldry.signing_key.passphrase"
+
+[request_timeout]
+secs = 30
+nanos = 0
+
+[credentials]
+private_key = "siguldry.client.private_key.pem"
+certificate = "siguldry.client.certificate.pem"
+ca_certificate = "siguldry.ca_certificate.pem"
diff --git a/roles/siguldry/templates/server.toml.j2 b/roles/siguldry/templates/server.toml.j2
new file mode 100644
index 0000000..0a1c9f6
--- /dev/null
+++ b/roles/siguldry/templates/server.toml.j2
@@ -0,0 +1,20 @@
+# Siguldry server configuration for MxOS Koji
+state_directory = "/var/lib/siguldry/"
+bridge_hostname = "{{ koji_hub_fqdn }}"
+bridge_port = 44333
+connection_pool_size = 32
+user_password_length = 8
+openpgp_user_id = "MxOS Signing Key "
+pkcs11_bindings = []
+
+[credentials]
+private_key = "siguldry.server.private_key.pem"
+certificate = "siguldry.server.certificate.pem"
+ca_certificate = "siguldry.ca_certificate.pem"
+
+[certificate_subject]
+country = "MX"
+state_or_province = "Mexico"
+locality = "Mexico City"
+organization = "MxOS"
+organizational_unit = "Infraestructura"