CreaRack-SL

Plan Hardening fleco E cerrado: retirada de DEV_KEY de la cadena de descifrado

Resumen

En la sesión 128 (2026-06-11) se cierra el fleco E del Plan Hardening (s101): retirada de la clave de desarrollo hardcodeada (DEV_KEY) y del fallback implícito a SECRET_KEY de la cadena de descifrado de CredentialManager.

Una clave fija en el código fuente, aunque de desarrollo, representaba un riesgo residual: un atacante con acceso al repositorio y a la base de datos de producción podía descifrar credenciales almacenadas. Este cambio cierra ese vector tras verificación operacional completa.


Contexto

Por qué mide

El Plan Hardening hito E (mayo 2026, s101) identificó que:

  • Existía una clave de desarrollo hardcodeada (DEV_KEY) en el código.
  • El sistema la probaba como último recurso al descifrar credenciales.
  • Era la misma para cualquier instalación (pública de facto).
  • Un atacante con acceso al repositorio + base de datos podría descifrar material de producción.

La solución arquitectónica: migrar a una cadena de descifrado explícita y ordenada:

  1. Clave activa (dedicada o SECRET_KEY en dev).
  2. Clave anterior durante rotación (CREDENTIAL_ENCRYPTION_KEY_OLD).
  3. Sin fallbacks implícitos ni claves públicas.

Decisión tomada

Cambio en core/security/credential_manager.py

Antes (fleco abierto):

_decryption_chain(include_dev_rescue=True):
    # Intentaba: activa → SECRET_KEY → DEV_KEY
    chain = [active_material, secret_key, DEV_KEY]  # DEV_KEY hardcodeada

Después (fleco E cerrado):

_decryption_chain():
    # Ahora: activa → CREDENTIAL_ENCRYPTION_KEY_OLD (explícita)
    chain = [active_material, previous_old_key_or_none]

Cadena de descifrado: ¿qué cambió?

MomentoCadenaEstado
Pre-s101activa → SECRET_KEY (impl) → DEV_KEY (hardcoded)❌ Insegura (DEV_KEY pública)
s101–s127Mismo, pero plan abierto + reencrypt_credentials listo⚠️ Transitoria (re-cifrado pendiente)
s128activa → CREDENTIAL_ENCRYPTION_KEY_OLD (decl. explícita)✅ Segura (sin hardcodes)

SECRET_KEY en dev/test: continúa siendo la clave activa en esos entornos (sin cambios en el comportamiento de dev).


Ejecución: orden seguro en producción

Antes de retirar los fallbacks, se ejecutó la migración de ciphertext:

Paso 1: Ampliar cobertura de reencrypt_credentials (PR #109)

  • Incluye Device.management_config (último almacén de credenciales pendiente).

Paso 2: Re-cifrado en PROD (verificado)

reencrypt_credentials --dry-run    # 401 filas evaluadas
reencrypt_credentials               # 264 campos reescritos a clave activa
                                    # 0 irrecuperables
                                    # Verificado y completado

Paso 3: Verificación STAGE

  • Base de datos de testing confirmada vacía (sin credenciales legadas).

Paso 4: Retirada del código (este PR, s128)

  • DEV_KEY eliminada de CredentialManager.
  • include_dev_rescue removido de firmas públicas.
  • Tests reescritos al nuevo contrato.

Impacto en el código

Archivos modificados

  1. core/security/credential_manager.py (core)

    • Retirada de DEV_KEY (constante).
    • Firma de _decryption_chain() sin include_dev_rescue.
    • Firma de decrypt_credential() sin include_dev_rescue.
    • Documentación interna actualizada.
  2. core/management/commands/reencrypt_credentials.py

    • Llamadas a decrypt_credential() sin parámetro include_dev_rescue.
    • Mensajes de error actualizados (referencia a CREDENTIAL_ENCRYPTION_KEY_OLD).
  3. Tests (reescritos al nuevo contrato)

    • tests/test_credential_key_rotation.py:
      • test_secret_key_fallback_removed(): verifica que ciphertext legado no descifra sin _OLD.
      • test_chain_is_only_active_plus_old(): verifica cadena exacta (sin DEV_KEY).
    • tests/api/test_config_ia_diferidos.py:
      • test_device_management_config_reencrypted(): ajustado a CREDENTIAL_ENCRYPTION_KEY_OLD.
    • tests/api/test_t1_snmp_encryption.py:
      • test_covers_device_profiles_and_targets(): idem.

Sin regresiones

Suite Docker ejecutada tras cambios: diff vs. base = 0 (todas las pruebas pasan).


Implicaciones de seguridad

Vector cerrado

  • ❌ Clave fija pública en el código: ELIMINADA.
  • ❌ Fallback implícito a SECRET_KEY: ELIMINADO.
  • ✅ Fallbacks explícitos vía CREDENTIAL_ENCRYPTION_KEY_OLD: SOPORTADO (solo durante rotación).

Almacenes de credenciales cubiertos

  • monitoring.MonitoringTarget.config (snmp_community / v3 keys).
  • racks.Device.management_config (ip/username/password/enable_password).
  • Otros StoredCredential con campos de sistema.

Checklist operacional (completado)

  • ✅ Re-cifrado verificado en PROD (264 campos, 0 irrecuperables).
  • ✅ STAGE confirmado vacío.
  • ✅ Tests de rotación reescritos.
  • ✅ Suite Docker limpia.
  • ✅ Documentación (CHANGELOG + RELEASE_NOTES + s128).
  • ✅ Cambios de código ejecutados.

Próximos pasos

La cadena de descifrado queda cerrada y explícita. Futuras rotaciones de claves:

  1. Establecer CREDENTIAL_ENCRYPTION_KEY_OLD = <clave_anterior>.
  2. Ejecutar reencrypt_credentials para migrar al activo nuevo.
  3. Limpiar CREDENTIAL_ENCRYPTION_KEY_OLD = "".

No hay fallbacks silenciosos que rescaten ciphertext antiguo.


Véase también

  • [[entity—core—service—credential-manager]]
  • [[entity—core—management-command—reencrypt-credentials]]
  • [[concept—security—key-rotation]]
  • [[concept—saas—multi-tenancy]]
  • [[entity—core—model—organization]]