Saltar a contenido

Chequeos: Cloud → Edge

Responde: ¿cómo sabe la nube si una Raspberry Pi está viva, y qué confianza merece esa respuesta?

Es la única frontera donde el tráfico va de la nube a la planta, al revés que todas las demás. Y es la única sin autenticación, que es lo que decidió SYS-ADR-005 asumiendo un riesgo explícito.

Tres endpoints, y nadie los consulta solo

Endpoint Para qué Quién lo dispara Permiso
GET /status Conectividad y versión del Edge un usuario, desde la interfaz perm_edge_devices_check
GET /health Diagnóstico de hardware de la Pi un usuario perm_edge_devices_check
GET /telemetry Instantánea en vivo un usuario perm_edge_devices_view

No hay nada programado. No existe un planificador, un cron ni un trabajo de fondo que consulte estos endpoints: los tres viven detrás de rutas de la API del Cloud que un usuario autenticado invoca. Una Pi que se cae no genera ninguna señal; se descubre porque alguien abre la pantalla del dispositivo y aprieta el botón, o porque deja de haber datos.

Eso cierra el círculo con la frontera de ingesta, donde el latido tampoco está implementado en ninguno de los dos extremos: ver Edge → Cloud. El sistema no tiene detección automática de un Pi muerto, por ninguna de las dos vías que la tendrían.

Los dos primeros dejan rastro: el Cloud persiste un evento de dispositivo con el resultado, el usuario y su correo, y refresca el estado de salud en caché. /telemetry no persiste nada: es una lectura en vivo y se va con la respuesta.

Lo que /status mira, y lo que no

Tres señales, y ninguna es el PLC:

Estado Cuándo
OK Cloud alcanzable y InfluxDB alcanzable y outbox no saturado
DEGRADED falla alguna de las tres; el resumen lista cuáles, en el orden cloud, influx, outbox-full
ERROR / UNKNOWN el Edge nunca los emite por esta vía. Los produce el cliente del Cloud cuando la llamada falla

Esa última fila es la que más conviene tener presente al leer la pantalla: el ERROR que se ve en la interfaz casi nunca viene del Pi. Lo fabrica el Cloud.

El criterio que la especificación original describía —basado en si el PLC responde y en cuán viejo es el último dato escrito, con umbrales de 60 s y 5 min— no está implementado. El PLC no participa de /status en absoluto.

Lo que sí informa, cuando el forwarder está cableado: el instante del último reenvío, el último error, el tamaño del outbox y el de su cola de descartes, y la versión del catálogo, que es el sha256 del archivo cargado. Ese último campo es la forma más directa de verificar que las dos mitades de la Pi montaron el mismo catálogo. La alcanzabilidad de InfluxDB es un ping con caché de 30 s, así que el valor puede tener hasta medio minuto de atraso.

Lo que /health mide, con sus umbrales

Estado Umbral
ERROR temperatura > 80 °C o disco > 95 %
DEGRADED temperatura > 70 °C o disco > 85 % o RAM > 90 %
OK el resto

Lo que hace a este endpoint mejor de lo que parece es el orden en que decide: evalúa primero los umbrales sobre las señales que sí se pudieron leer, y solo baja a UNKNOWN si algún sensor falló y ningún umbral se disparó. Así una temperatura crítica real no queda enmascarada como «desconocido» porque un sensor no relacionado falló en la misma petición. Los sensores que fallaron se listan aparte, que es la única forma de distinguir una lectura de 0 de una lectura que no ocurrió.

La temperatura sale del archivo del sistema de la zona térmica, con null si no está disponible. No se intenta la herramienta propia de la Raspberry.

Lo que /telemetry promete y no cumple

El bloque plc del esquema declara si el PLC es alcanzable, su latencia y el instante del último latido. Está sin implementar: el paquete que lo resolvería es un marcador de posición con un pendiente escrito dentro, y la respuesta devuelve siempre «no alcanzable», sin latencia y sin último latido.

Importa porque es el único lugar de todo el sistema donde el PLC aparecería, y un operario que mire esa pantalla va a leer «PLC no alcanzable» en una máquina que está funcionando perfectamente. El diseño pendiente —una conexión TCP al puerto del PLC con un segundo de límite, su latencia, y el último latido derivado del dato más reciente de InfluxDB— ni siquiera tiene todavía la variable de entorno con la dirección del PLC.

Es el mismo hueco que PLC → historian documenta desde el otro lado.

La autenticación: el riesgo asumido, y una trampa nueva

SYS-ADR-005 aceptó que estas llamadas no lleven credencial, con el control compensatorio de que la Pi solo es alcanzable desde la nube por una red privada. Esa red no está configurada ni documentada en ningún repositorio: el control es un supuesto.

Desde entonces el Edge implementó la validación opcional que esa decisión dejaba como recomendación: un secreto compartido por cabecera, comparado en tiempo constante, que se aplica a los tres endpoints y deliberadamente no a /metrics, para que el recolector de Prometheus siga pudiendo leerlo sin credencial. Con la variable de entorno vacía el middleware es transparente, y el Edge lo avisa al arrancar con una advertencia explícita.

El Edge puede exigir el secreto y el Cloud no tiene forma de enviarlo

El cliente del Cloud construye las tres peticiones sin ninguna cabecera: no manda el secreto, no tiene configuración para él, y el nombre de esa cabecera no aparece en ninguna parte del repositorio del Cloud.

El compose del bundle ya expone la variable, comentada en su archivo de ejemplo. Quien la descomente —siguiendo exactamente la advertencia que el Edge imprime al arrancar— hace que los tres chequeos empiecen a devolver 401.

Y el modo en que falla es el peor posible: el cliente del Cloud convierte cualquier respuesta que no sea 2xx en overallStatus: ERROR, sin distinguir el código. En la pantalla, una Pi perfectamente sana con el secreto bien configurado se ve igual que una Pi caída.

El arreglo es chico y está en el Cloud: guardar el secreto por dispositivo y enviarlo. Pero hasta que exista, activar la autenticación de esta frontera rompe la observabilidad del dispositivo, que es justo lo contrario de lo que se esperaría de endurecerla.

La dirección de la Pi la elige el cliente

El Cloud guarda la URL base de cada dispositivo y hace un GET contra ella. Esa URL la provee un usuario del propio cliente, al crear o editar el dispositivo, y la validación es asimétrica:

Operación Qué valida
Crear el dispositivo solo que no esté vacía
Editarlo que sea sintácticamente una URL http/https con host

Así que un dispositivo puede quedar creado con un valor que la edición rechazaría. Y en ninguno de los dos casos se valida el destino: nada restringe a qué direcciones puede apuntar.

Dicho sin rodeos: un usuario del cliente con permiso para administrar dispositivos puede hacer que el servidor del Cloud emita peticiones HTTP a una dirección que ese usuario elige, y pedirle el resultado con el permiso de chequeo. Es la clase de problema conocida como falsificación de peticiones del lado del servidor. El único control que lo acota hoy es la topología de red del servicio en la nube, que es justamente el supuesto que nadie comprobó.

Acá se registra la clase de problema y nada más, con sus dos mitigaciones baratas —validar igual en las dos operaciones, y restringir el destino a una lista permitida o excluir los rangos privados—. Ver seguridad.

Cómo falla, visto de los dos lados

Qué pasa Qué devuelve el Edge Qué muestra el Cloud
Todo bien 200 con OK o DEGRADED lo que el Edge dijo
Error parcial en el Edge 200 con DEGRADED, y el detalle en el resumen preferido: es lo más informativo para el usuario
Error no recuperable en el Edge 500 ERROR, resumen genérico
Pi inalcanzable, DNS roto, timeout nada ERROR, con el texto del error de transporte como resumen
Secreto compartido activo 401 ERROR, indistinguible de una Pi caída
Dispositivo deshabilitado en el Cloud no se llama error de dominio, sin tocar la Pi

El límite de tiempo de cada llamada es de 10 segundos, por defecto y sin variable de entorno que lo cambie: está fijado en el cableado con un comentario que lo dice.

/telemetry se comporta distinto a los otros dos ante un fallo: propaga el error en lugar de fabricar un resultado degradado. Así que la interfaz recibe un error de la API, no una instantánea vacía — que es lo correcto, pero significa que los tres endpoints no fallan igual.

Qué verifica el CI de esta frontera

Nada de punta a punta. Y de cada lado, menos de lo esperable:

Repo Qué cubre
Cloud Tests unitarios del cliente HTTP, con un servidor simulado: cubren los códigos de estado y el camino del error de transporte
Edge El contrato que expone se genera desde el código y el CI falla si hay deriva entre el código y el openapi.yaml publicado. Pero go test no corre en ningún workflow, así que la derivación de estados y los umbrales de /health no se verifican en un merge

El control de deriva del OpenAPI es lo mejor que tiene esta frontera, y vale señalarlo porque es la única de las cinco donde el contrato publicado no puede desincronizarse del código. Lo que no verifica es que el consumidor —el Cloud— siga de acuerdo con él.

Qué no puede decir esta página

  • Si estas llamadas funcionaron alguna vez contra una Pi real. El stack nunca corrió en el hardware de destino, así que los umbrales de temperatura y los valores de los sensores no se midieron en una Raspberry.
  • Si la red entre la nube y la planta es realmente privada. Es el control compensatorio de SYS-ADR-005, y es un supuesto.
  • Si la falsificación de peticiones es explotable en la infraestructura actual. Depende de esa misma topología. Lo que sí está en el código es la ausencia de validación del destino.
  • Quién mira estas pantallas, y con qué frecuencia. No hay telemetría de uso.

Dónde está implementado

Los paquetes del Cloud que sirven esta frontera son idénticos en main y en develop.

Qué Dónde
Rutas, verbos y permiso de cada una; que son on-demand y no programadas internal/api/handler/edge_devices/routes.go del Cloud, y la ausencia de cualquier planificador en cmd/
Persistencia del evento con usuario y correo en /status y /health, y su ausencia en /telemetry internal/app/edge_devices/service.go del Cloud
Conversión de cualquier fallo en ERROR, y propagación del error en /telemetry internal/platform/edgeclient/http_client.go del Cloud
Límite de 10 s sin variable de entorno internal/routes/url_mappings.go del Cloud
Validación asimétrica de la URL base, y ausencia de restricción de destino create_device.go y update_device.go del Cloud
Tres señales de /status, que el PLC no participa, y la caché de 30 s del ping a InfluxDB internal/app/checks/service.go del Edge
Umbrales de /health y el orden umbral → sensores fallidos internal/app/checks/service.go del Edge
El bloque plc sin implementar y sin variable de entorno para la dirección internal/platform/plcprobe/ y internal/config/config.go del Edge
Secreto compartido por ruta, comparación en tiempo constante, /metrics excluido a propósito, y la advertencia al arrancar internal/api/middleware/shared_secret.go, internal/routes/url_mappings.go y cmd/edge/main.go del Edge
La variable expuesta en el stack de la Pi docker-compose.yml y .env.example del bundle

El contrato del lado del Edge está en docs/contracts/edge-inbound.md, y el que el Edge expone —generado desde el código— en docs/openapi.yaml.

Relacionado