Haga esto
- Pull de un tag publicado
- Correr
linux/amd64 - Inyectar el PAT en runtime
- Rotar tag y PAT por separado
El cliente no construye el agente. Recibe una imagen ya publicada en un registro privado, un usuario de pull y un PAT de EVA. Nuclei y Nmap viajan dentro de esa imagen.
CI construye linux/amd64 y hace push al registry.
Tag semver y digest. Credencial de pull, no el PAT.
docker login y docker pull en on-prem.
docker run con el secreto EVA inyectado en runtime.
| Qué recibe | Ejemplo | Notas |
|---|---|---|
| Nombre de imagen | registry.ejemplo.com/eva/runner-nuclei |
Placeholder hasta el registry real. También existe …/runner-nmap. |
| Tag | 1.0.0 |
En producción pinneé además el digest @sha256:…. |
| Login de registry | robot / IAM / token de pull | Solo lectura del catálogo de imágenes. No autentica EVA. |
| PAT de EVA | eva_pat_… |
Scopes retest_read + retest_write. Almacén de secretos del cliente. |
linux/amd64docker build en el Mac o en el servidor:latest en producciónretest_read y retest_write. Cópielo una vez./health (proceso vivo) y /ready (binario + EVA).docker login registry.ejemplo.com docker pull registry.ejemplo.com/eva/runner-nuclei:1.0.0 docker pull registry.ejemplo.com/eva/runner-nmap:1.0.0 docker run --rm --env-file .env -p 8080:8080 \ registry.ejemplo.com/eva/runner-nuclei:1.0.0
Sustituya el host y el tag por los que Entelgy le entregue. La plataforma de la imagen de producto es linux/amd64.
No meta secretos en la imagen. El .env vive en el host y no se commitea:
EVA_INTEGRATION_TOKEN= EVA_API_BASE_URL=https://api.ejemplo.com EVA_TOOL_ID=nuclei EVA_EXECUTION_SCOPE=on_prem EVA_RUNNER_ID=nuclei-lab-01 # EVA_SITE_KEY= # opcional; vacío = cola actual EVA_HEALTH_ADDR=:8080 EVA_POLL_INTERVAL_SECONDS=15 EVA_TOOL_TIMEOUT_SECONDS=120 EVA_LOG_FORMAT=json
| Variable | Obligatorio | Valor |
|---|---|---|
EVA_API_BASE_URL | Sí | Backend Flask, no el BFF de Next |
EVA_INTEGRATION_TOKEN | Sí | PAT con retest_read + retest_write |
EVA_TOOL_ID | Sí | nuclei o nmap — debe coincidir con la imagen |
EVA_EXECUTION_SCOPE | Sí | cloud o on_prem |
EVA_RUNNER_ID | Sí | Hostname estable, 1–128, A-Za-z0-9._:- |
EVA_SITE_KEY | No | Vacío = cola actual (tenant × tool × scope). Solo si el proyecto tiene site_key. |
EVA_HEALTH_ADDR | No | :8080 |
EVA_POLL_INTERVAL_SECONDS | No | 15 |
EVA_TOOL_TIMEOUT_SECONDS | No | 120 |
EVA_LOG_FORMAT | No | json (stderr) o text en laboratorio |
El health es local al contenedor, no es la API EVA. /health es liveness (el proceso vive). /ready es readiness (binario + EVA). /metrics es texto Prometheus en el mismo puerto (host-only, como /health; no forma parte de eva.integration.retest.v1):
curl -sS http://127.0.0.1:8080/health
curl -sS -o /tmp/ready.json -w "%{http_code}\n" http://127.0.0.1:8080/ready
curl -sS http://127.0.0.1:8080/metrics
{
"status": "idle",
"runner_id": "nuclei-lab-01",
"tool_id": "nuclei",
"execution_scope": "on_prem",
"last_error": null,
"version": "1.0.0",
"tool_binary_ok": true,
"eva_reachable": true,
"last_class": "",
"last_job_id": "",
"idle_polls": 0,
"started_at": "2026-08-21T15:00:00Z"
}
status: idle o running. /health sigue en 200 si EVA está caído; /ready pasa a 503. /ready 200 es la aceptación de enganche: binario en la imagen y Flask alcanzable. No prueba el target del job. Sin el binario de Nuclei/Nmap el agente no hace claim. Docker usa HEALTHCHECK contra /health (eva-runner healthcheck). SIGTERM: si hay scan, cancela el binario y POST result (timeout si el proceso muere); idle, apaga. Un log de evidencia > 256 KiB se recorta a cabeza + cola. El caption es {tool} stdout o {tool} stderr; si hay ambos, hasta dos ítems. Target en blanco: not_applicable sin binario. Heartbeat con lease vencido o 401 aborta el tool y no envía still_open/appears_fixed; un parpadeo de EVA no aborta.
Logs del agente → JSON en stderr (runner_id, tool_id, execution_scope, site_key, class; en un job también job_id, target, recipe, outcome, error_code, reason). Al arrancar: start y watching. Cola vacía: idle (primer poll y cada 20). Nuclei/Nmap → stdout de docker logs, no al body de EVA. El PAT no se loguea. El agente envía User-Agent: eva-runner/{versión} ({tool}; {scope}); EVA lo ignora.
| Si el operador ve… | Significa | No es |
|---|---|---|
auth_failed (logs / /ready 503) |
PAT inválido o revocado. Backoff ~60 s. | El target del job caído. |
eva_unreachable (logs / /ready 503, /health 200) |
No llega a EVA (red, DNS, 5xx). Backoff 5→60 s. | Un error_code hacia EVA. Nunca se envía en result. |
target_unreachable en el result / logs tool |
Nuclei/Nmap no alcanza el target del job (LAN, DNS, firewall). |
EVA caído. |
idle en logs / idle_polls en /health, /ready 200 |
El nodo está enganchado. EVA no le ofrece job (cola vacía o site_key distinto). |
Un contenedor muerto. Eso es /health caído. |
reason=template_missing con tool_crash |
Nuclei no tiene el YAML de la receta. El error_code hacia EVA sigue siendo tool_crash. |
Target inalcanzable. |
reason=empty_target con not_applicable |
Guardia del runner: target.value en blanco. No lanza Nuclei/Nmap ni envía error_code. |
Un error_code nuevo. EVA no debería encolar esto. |
El runner no tiene inventario de hosts. EVA elige target.value al encolar y ese valor no llega vacío. El contenedor solo necesita llegar a Flask (EVA_API_BASE_URL) y a ese valor:
--network host suele ver la LAN del cliente y un Flask en el host.host.docker.internal para servicios en el host.extra_hosts).Un bridge aislado puede dejar /ready en 200 (EVA responde) y el scan en target_unreachable.
EVA_API_BASE_URL y un GET …/retests/pending.GET …/retests/pending.job != null y el binario está, POST …/claim. Si falta el binario, no reclama.target.value.POST …/result con outcome y hasta 3 evidencias. Caption {tool} stdout o {tool} stderr (dos ítems si hay ambos). Log recortado cabeza+cola si supera 256 KiB. Si EVA parpadea, reintenta 2–3 veces; 409 JOB_ALREADY_FINISHED cuenta como éxito. SIGTERM con tool en vuelo: cancela y POST result.EVA_POLL_INTERVAL_SECONDS.El runner no cambia el estado del hallazgo.
Poll, claim, heartbeat y result no pasan por la allowlist CIDR ni el cupo de 60/min de extract/ingest. Un PAT de runner con allowed_cidrs relleno sigue autenticando estas rutas. Esas barreras son solo extract/ingest.
Solo jobs pending que EVA ya encoló para el tool_id y execution_scope del contenedor (y site_key si se configura). El job incluye target.value y la receta. Cola vacía → espera el siguiente poll.
Tras revocar el PAT, pending y claim responden 401. Pare el contenedor y retire el secreto.
Los tenants están aislados. Dentro de un tenant, todos los proyectos comparten la cola de ese tool_id + execution_scope salvo que el runner envíe site_key (opt-in). Sin EVA_SITE_KEY el comportamiento es el de hoy. Dos contenedores pueden competir: el segundo claim con otro runner_id y lease vigente recibe 409 JOB_ALREADY_CLAIMED.
Si el host no puede alcanzar el registro, Entelgy puede entregar un artefacto OCI (docker save) para docker load en destino. Sigue siendo la misma imagen; no es un binario suelto ni un build en el cliente.
allowed_cidrs de extract/ingest.