CreaRack-SL

T1 Cifrado de Credenciales SNMP en Reposo

Resumen

Tanda transversal T1 del Informe Supremo (Auditoría Etapa 3): cifrado en reposo de todas las credenciales SNMP de la red — DeviceProfile y MonitoringTarget — utilizando Fernet vía CredentialManager.

Hasta esta entrega, las credenciales SNMP se guardaban en claro en la base de datos PostgreSQL:

  • DeviceProfile.snmp_community (v2c)
  • DeviceProfile.snmp_v3_auth_key y snmp_v3_priv_key (v3)
  • Claves sensibles en MonitoringTarget.config (sub-cadenas community, password, auth_key, priv_key, secret, credential)

Ahora se cifran automáticamente en save() (idempotente: un token Fernet gAAAAA... no se re-cifra) y los lectores descifran en el punto de uso exacto donde se necesitan en claro: pollers, deep-discover, envío al Agente, modal Config, backup/restore.

Motivación

Cierre de deudas críticas de auditoría:

  • network A8: Credenciales SNMP v2c/v3 cifradas en reposo.
  • monitoring sa2/sa5: Secretos en MonitoringTarget.config cifrados.
  • Fleco E del Plan Hardening: Alineación con estándares de seguridad de datos sensibles.

Una base de datos robada o copia de seguridad extraviada ya no expone las contraseñas de acceso a los equipos de red del cliente.

Implementación

Cambios en modelos

DeviceProfile (network/models.py):

  • snmp_community, snmp_v3_auth_key, snmp_v3_priv_key: cambiados de CharField a TextField (el token Fernet es más largo que 255 chars).
  • save(): cifra todos los campos SNMP sensibles mediante CredentialManager.encrypt_if_plain().
  • 3 nuevas properties: snmp_community_plain, snmp_v3_auth_key_plain, snmp_v3_priv_key_plain — devuelven el valor descifrado (o en claro si es legado).
  • SNMP_SECRET_FIELDS: tupla con los nombres de los campos a cifrar (reutilizada por reencrypt_credentials).

MonitoringTarget (monitoring/models.py):

  • config (JSONField): claves sensibles (detectadas por sub-cadena) se cifran en save().
  • encrypt_config(cfg): method estático que cifra idempotentemente valores sensibles dentro del dict.
  • secret_config property: descifra el dict completo para lectores (pollers, etc.).
  • public_config property: existente, sigue filtrando claves sensibles (sin exponerlas).
  • _is_sensitive_key(): helper para detectar claves sensibles.

Cambios en CredentialManager (core/security/credential_manager.py)

Dos nuevos métodos públicos:

  1. encrypt_if_plain(value): cifra un valor a menos que:

    • Sea falsy (None, "", etc.) — devuelve tal cual.
    • Ya sea un token Fernet (detectado por is_encrypted(), prefijo gAAAAA) — devuelve tal cual (idempotente).
    • En otro caso: cifra con encrypt_credential().
  2. decrypt_or_passthrough(value, field_name=""): descifra un token Fernet; no es un error encontrar plaintext legado:

    • Falsy o no-string: devuelve tal cual.
    • No es token (no empieza por gAAAAA): devuelve tal cual (plaintext legado, pre-migración).
    • Es token pero no descifrable (clave rotada sin legacy fallback, o token corrupto): devuelve "" (nunca filtra ciphertext a SNMP libs o API).

Migraciones

network/0049: Altera los 3 campos SNMP de CharField a TextField con ayuda.

network/0050: Cifra todos los DeviceProfile existentes. Idempotente: encrypt_if_plain salta tokens. Reverse: no-op deliberado (descifrar re-expondría secretos).

monitoring/0023: Cifra todos los MonitoringTarget.config existentes. Idempotente y reverse no-op.

Puntos de descifrado (solo cuando se necesita claro)

  1. SNMPService.from_config(): descifra snmp_community, snmp_v3_auth_key, snmp_v3_priv_key del dict config.
  2. DeviceDiscoveryService.build_snmp_auth_from_profile() y build_snmp_auth_from_config(): descifran credenciales antes de pasarlas a pysnmp.
  3. DeviceDiscoveryService.walk_if_table(): descifra community.
  4. Deep-discover tasks al Agent: descifran credenciales antes de serializar la tarea.
  5. WS consumer _send_targets() y REST sentinel.get_agent_targets(): descifran el dict config antes de enviarlo al Agente (quien lo recibe por TLS y re-cifra con DPAPI en su lado).
  6. Provisioning.build_snmp_config(): mantiene cifrado en el dict (sale del profile ya cifrado).
  7. Modal Config (get_profile_config): descifra para mostrar al usuario (deuda A4 sa1: exponer credential en respuesta es diferido).
  8. DeviceProfileOut resolver (network/api/common.py): descifra snmp_community para la API (statu quo del contrato).
  9. Backup/Restore:
    • Backup (ZIP): exporta credenciales SNMP en claro (decisión: el backup debe ser restaurable en otra instalación/clave; la descarga ya está gateada a rol edit).
    • Restore: lee ZIP, vuelca en BD; save() re-cifra automáticamente.

Bulk paths que saltan save()

  • MonitoringTarget bulk_create (targets.py): cifra explícitamente el dict config antes de pasarlo a .bulk_create().
  • Provisioning.build_snmp_config(): devuelve dict ya cifrado.

Rotación de claves extendida

Command reencrypt_credentials (core/management/commands/reencrypt_credentials.py):

  • Ahora también procesa DeviceProfile (3 campos SNMP + ssh_password_encrypted).
  • Ahora también procesa MonitoringTarget.config.
  • Antes cubría: StoredCredential, SignagePlayer.api_credentials, NotificationChannel.config.

Tests

tests/api/test_t1_snmp_encryption.py (14 nuevos):

  • Cifrado en save().
  • Accessors *_plain descifran.
  • Plaintext legado pasa sin error.
  • Token corrupto degrada a "".
  • Bulk endpoint cifra explícitamente.
  • Pollers descifran correctamente.
  • Provisioning mantiene cifrado.
  • Command de re-cifrado cubre los 2 almacenes nuevos.

Adaptados (4 tests que asumían plaintext):

  • test_network_profile_config: cifrado en BD, plaintext via property.
  • test_monitoring: acceso via secret_config.
  • test_monitoring_enable_profile: idem.
  • test_auto_config: idem.

Suite corrida en Docker (mismo entorno CI) antes del push.

Compatibilidad

  • Legado: Filas pre-migración con plaintext en BD pasan por decrypt_or_passthrough() sin error.
  • Idempotencia: encrypt_if_plain() permite re-ejecutar migraciones.
  • Reverse migraciones: no-op deliberado (rollback nunca descifra).
  • Multi-instalación: backup/restore exporta en claro, permitiendo restauración en otra instancia/clave.

Deudas diferidas

  • A4 sa1: Exponer snmp_community en DeviceProfileOut es inseguro pero statu quo; será cerrada en otra tanda.

Véase también

  • [[entity—network—model—device-profile]]
  • [[entity—monitoring—model—monitoring-target]]
  • [[entity—core—service—credential-manager]]
  • [[concept—saas—multi-tenancy]]
  • [[concept—security—credential-encryption]]