CreaRack-SL

Runbook: Firma y distribución de paquetes del Agente (Lanzador Estable)

Propósito

Documentar el ciclo completo de build, firma y publicación de paquetes del Agente (app-X.Y.Z.zip) bajo el Lanzador Estable (F1).

Audiencia: Edo, desarrolladores, infra, quien maneje releases del Agente.

Preparación inicial (una sola vez)

1. Generar clave privada Ed25519

Máquina: la que ejecute build_agent.bat (típicamente la de Edu).

# En PowerShell
python scripts/agent/generate_signing_key.py

Salida esperada:

Clave privada generada: C:\Users\edu\.crearack\keys\agent_signing_ed25519.pem
PUBKEY_HEX (para PACKAGE_PUBKEYS_HEX del bootstrap): <64 chars hex>

COPIAR la PUBKEY_HEX.

2. Actualizar bootstrap con la clave pública

Edita terminal/agent/main.py:

# Línea ~50
PACKAGE_PUBKEYS_HEX = [
    "abc123def456...",  # ← pega aquí la pubkey_hex de arriba
]

Commit este cambio (el bootstrap 2.14.0 se compila CON la clave pública).

3. Hacer backup de la clave privada

# Cifra y copia
gpg --symmetric ~/.crearack/keys/agent_signing_ed25519.pem  
# → agent_signing_ed25519.pem.gpg

# Guarda en:
# - USB cifrada (backup físico)
# - Vault empresa (1Pass, Bitwarden, etc.)
# - JAMÁS en Git, SaaS público, o email

Si se pierde: no se pueden firmar futuras releases. Generar par nuevo, actualizar PACKAGE_PUBKEYS_HEX en bootstrap, y compilar bootstrap 2.14.1+.

Build normal de release

1. Cambiar version.py

Edita terminal/agent/version.py:

AGENT_VERSION = "1.45.14"  # ← incrementa

2. Ejecutar build_agent.bat

cd C:\ruta\al\repo
build_agent.bat

El script hace:

Paso 1-3: Verifica Python, estructura módulos, ambiente.

Paso 4.1-4.6: Compile el .exe (PyInstaller), self-test.

Paso 4.7 (nuevo, Lanzador Estable):

python scripts\agent\build_app_package.py --output "%OUTPUT_DIR%"

Internamente:

  • Empaqueta app-1.45.14.zip (config, app_main, core/, routes/, assets)
  • Calcula SHA-256
  • FIRMA con la clave privada en ~/.crearack/keys/agent_signing_ed25519.pem
  • Emite app-1.45.14.meta.json:
{
  "version": "1.45.14",
  "sha256": "a1b2c3d4e5f6...",
  "sig_ed25519": "abcdef0123456789...",
  "min_bootstrap_api": 1,
  "built_at": "2026-07-07T14:23:45+00:00"
}

Paso 4.8: Deploy a OUTPUT_DIR (típicamente static/downloads/).

Paso 5: Publica a Hetzner via scp:

scp -o ConnectTimeout=20 \
    "%OUTPUT_DIR%\CreaRackAgent.exe" \
    "%OUTPUT_DIR%\app-1.45.14.zip" \
    "%OUTPUT_DIR%\app-1.45.14.meta.json" \
    root@crearack.com:/tmp/

Luego docker cp al volumen del web:

ssh root@crearack.com "
  docker cp /tmp/app-1.45.14.zip \
    <web-container>:/app/media/downloads/app-1.45.14.zip && \
  docker cp /tmp/app-1.45.14.meta.json \
    <web-container>:/app/media/downloads/app-1.45.14.meta.json && \
  docker cp /tmp/CreaRackAgent.exe \
    <web-container>:/app/media/downloads/CreaRackAgent.exe && \
  docker exec <web-container> sh -c \
    'printf 1.45.14 > /app/media/downloads/CreaRackAgent.exe.version' && \
  rm -f /tmp/{CreaRackAgent.exe,app-1.45.14.zip,app-1.45.14.meta.json}
"

Orden crítico: zip+meta PRIMERO, luego .exe, luego .version.

3. Verificar en el servidor

ssh root@crearack.com
docker exec <web-container> ls -lh /app/media/downloads/app-1.45.14.*
# Debe listar:
# - app-1.45.14.zip       (0.5 MB típico)
# - app-1.45.14.meta.json (0.1 KB)

4. Test de descarga

Desde el cliente Agente (con bootstrap 2.14.0):

# Simula heartbeat
manifest = {
    "version": "1.45.14",
    "package_version": "1.45.14",
    "package_sha256": "a1b2c3d4e5f6...",  # de .meta.json
    "package_sig": "abcdef0123456789...",  # de .meta.json
    "package_url": "/downloads/agent-app/1.45.14",
}

# pkg_updater descarga y verifica
await download_and_verify_package("1.45.14", manifest)
# → Debe retornar True si firma OK

O manualmente:

curl -H "Authorization: Bearer <JWT_AGENT>" \
  https://crearack.com/downloads/agent-app/1.45.14 \
  -o app-1.45.14.zip

Troubleshooting

“Clave de firma NO encontrada”

[!] Clave de firma NO encontrada: C:\Users\edu\.crearack\keys\agent_signing_ed25519.pem

Solución: ejecuta python scripts/agent/generate_signing_key.py (ver Preparación inicial).

“FIRMA ED25519 FALLO”

En el bootstrap:

if not verify_ed25519_signature(meta["sig_ed25519"], meta["sha256"]):
    raise ValueError("FIRMA ED25519 INVÁLIDA")

Causas:

  • Clave privada no coincide con PUBKEY_HEX del bootstrap
  • Paquete se alteró en tránsito
  • Bootstrap tiene pubkey vieja (después de rotación, no actualizada)

Solución:

  1. Verifica PACKAGE_PUBKEYS_HEX en terminal/agent/main.py vs. la que se usó para firmar
  2. Si rotaste la clave, asegúrate que generate_signing_key.py --rotate generó .new.pem y actualizaste PUBKEY_HEX
  3. Recompila bootstrap 2.14.1+
  4. Publica nuevo paquete con la clave nueva

“SHA-256 MISMATCH”

sha256_actual = hashlib.sha256(open("app-1.45.14.zip", "rb").read()).hexdigest()
if sha256_actual != meta["sha256"]:
    raise ValueError("SHA-256 MISMATCH")

Causas:

  • ZIP se corrompió en tránsito (red, SaaS filesystem)
  • Alguien alteró el ZIP sin actualizar .meta.json

Solución:

  • Verifica integridad del ZIP en el servidor: sha256sum app-1.45.14.zip vs. meta.json
  • Si está corrupto, borra y republica desde local (build_agent.bat nuevamente)

“Descarga falla” (cliente sin internet / SaaS inaccesible)

En pkg_updater:

if not download_and_verify_package(...):
    log.warning("Descarga/verificación falló, mantener current")
    return

Agente sigue funcionando con paquete anterior.

Próximo intento: en 6 horas (heartbeat normal).

Rotación de claves

Escenario: la clave privada se compromete o lleva >1 año.

Paso 1: Generar nueva clave

python scripts/agent/generate_signing_key.py --rotate
# → genera agent_signing_ed25519.new.pem (no toca la vieja)
# → imprime PUBKEY_HEX_NEW

Paso 2: Actualizar bootstrap (2.14.1+)

Edita terminal/agent/main.py:

PACKAGE_PUBKEYS_HEX = [
    "abc123...",        # ← vieja (permite validar paquetes viejos aún)
    "def456...",        # ← nueva (se usa para firmar nuevas releases)
]

Compila bootstrap 2.14.1.

Paso 3: Publica bootstrap 2.14.1

Una vez que clientes migren a 2.14.1, puedes retirar la clave vieja en 2.14.2:

PACKAGE_PUBKEYS_HEX = [
    "def456...",        # ← solo nueva
]

Paso 4: Firma nuevas releases con la clave nueva

# Renombra
mv ~/.crearack/keys/agent_signing_ed25519.new.pem \
   ~/.crearack/keys/agent_signing_ed25519.pem

# Próxima ejecución de build_agent.bat usa la nueva

Backup: copiar la vieja a un lugar seguro (USB cifrada, vault) antes de retirarla.

Validación de firma (offline)

Si necesitas validar la firma sin ejecutar el Agente:

# Usar cryptography de Python
python -c "
from cryptography.hazmat.primitives import serialization
import json, hashlib

meta = json.load(open('app-1.45.14.meta.json'))
zip_data = open('app-1.45.14.zip', 'rb').read()

# Verifica SHA
if hashlib.sha256(zip_data).hexdigest() != meta['sha256']:
    print('SHA-256 FALLO')
    exit(1)

# Verifica firma Ed25519
pubkey_hex = meta.get('sig_ed25519')
pubkey_bytes = bytes.fromhex(pubkey_hex[:64])  # primeros 64 chars
priv = serialization.load_pem_private_key(...)  # si tienes la privada
pub = priv.public_key()
pub.verify(bytes.fromhex(meta['sig_ed25519']), meta['sha256'].encode())
print('FIRMA OK')
"

Véase también

  • [[feature—terminal—lanzador-estable-f1]]
  • [[entity—core—service—pkg-updater]]
  • [[entity—terminal—service—app-main]]