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¶
SYS-ADR-005— la decisión de no autenticar esta frontera- Edge → Cloud — la frontera inversa, que sí tiene credencial por dispositivo
- Alta de un dispositivo Edge — donde se carga la URL base
- Monitoreo — qué alertas valdría construir, y por qué estas pantallas no alcanzan
- Seguridad — el inventario completo
- Contenedores C4 — las dos superficies de la API