CreaRack-SL

Definición

./manage.py reencrypt_credentials [--dry-run]

Ubicación: core/management/commands/reencrypt_credentials.py
Propósito: Re-cifrar todas las credenciales almacenadas con clave anterior (DEV_KEY o legacy) a la clave activa, permitiendo retirar fallback.
Precondición: Ejecutar en staging primero, validar reporte, luego en PROD con mismo comando.

Cobertura

Itera sobre los siguientes almacenes de credenciales:

ModeloCampoDescripción
monitoring.NotificationChannelconfigwebhook url/token + Authorization headers
network.DeviceProfile(snmp_community, v3 keys, ssh_password)T1 — configuración SNMP/SSH
monitoring.MonitoringTargetconfigsnmp_community / v3 keys
racks.Devicemanagement_configip/username/password/enable_password (Etapa 3 · E — nuevo)

Método principal

def handle(self, *args, **options):
    dry_run = options.get("dry_run", False)
    totals = {"rows": 0, "reencrypted": 0, "errors": 0}
    
    self._process_notification_channels(dry_run, totals)
    self._process_device_profiles(dry_run, totals)
    self._process_monitoring_targets(dry_run, totals)
    self._process_device_management_configs(dry_run, totals)  # Nuevo (E)
    
    # Imprime reporte

Nuevo: _process_device_management_configs (Etapa 3 · E)

def _process_device_management_configs(self, dry_run, totals):
    """racks.Device.management_config: JSON string con creds cifradas
    (ip/username/password/enable_password), también anidadas en "connection".
    Era el caso pendiente del fleco E antes de poder retirar la DEV_KEY.
    """
    for device in Device.objects.exclude(...).iterator():
        config = json.loads(device.management_config)
        new_config, changed = self._reencrypt_mapping(config, totals, label=...)
        
        conn = new_config.get("connection")
        if isinstance(conn, dict):
            new_conn, c_changed = self._reencrypt_mapping(conn, totals, label=...)
            if c_changed:
                new_config["connection"] = new_conn
                changed = True
        
        if changed and not dry_run:
            device.management_config = json.dumps(new_config)
            device.save(update_fields=["config"])

Sub-componentes:

  • config — dict de top-level: ip_address, username, password, enable_password (puede estar vacío).
  • connection — sub-dict anidado dentro de config con credenciales adicionales.
  • _reencrypt_mapping(mapping, totals, label) — helper que re-cifra todos los valores en un dict (busca campos que contengan ciphertext).

Descifrado multi-clave

El helper _derive_fernet(key) intenta descifrar con la cadena completa:

  1. Clave activa (SECRET_KEY o CIPHER_KEY)
  2. DEV_KEY (fallback — será retirado tras esta migración)
cipher = CredentialManager._derive_fernet(key)
plaintext = cipher.decrypt(ciphertext_bytes)

Tras descifrar, siempre re-cifra con la clave activa.

Uso

Modo dry-run (ver qué cambiaría):

./manage.py reencrypt_credentials --dry-run

Ejecución real (con confirmación interactiva):

./manage.py reencrypt_credentials

Salida esperada:

[reencrypt_credentials] rows scanned=42 reencrypted=15 errors=0 elapsed=2.3s

Tests

Véase tests/api/test_config_ia_diferidos.py::TestReencryptCoversDeviceConfig:

  • test_device_management_config_reencrypted — Device con password cifrado en legacy DEV_KEY se re-encripta a clave activa; verificar decrypt tras salvar.

Seguridad & Procedimiento

Antes de desplegar a PROD

  1. Ejecutar en staging con --dry-run → validar reporte.
  2. Ejecutar sin flag → validar que devices siguen accesibles.
  3. Comprobar en logs que no hay “errors”.

Después de desplegar

  1. En PROD: ./manage.py reencrypt_credentials --dry-run (inspeccionar reporte).
  2. En PROD: ./manage.py reencrypt_credentials (migrar).
  3. PR posterior: remover DEV_KEY + fallback del CredentialManager.decrypt_chain.

Rationale

  • Completitud: Device.management_config era el último almacén cifrado sin cobertura.
  • Atomicidad por tenant: usa transaction.atomic() para cada device.
  • Reversibilidad: descifrado multi-clave garantiza acceso incluso a credenciales antiguas.

Véase también

  • [[decision—20260611—auditoria-suprema-etapa-3-cierre-findings-m1-b6-e]] — decisión y contexto.
  • [[entity—racks—model—device]] — modelo Device con management_config.
  • [[entity—core—service—credential-manager]] — gestor de cifrado de credenciales.
  • [[concept—security—credential-lifecycle]] — ciclo de vida de credenciales.