Criptominería en Kubernetes: detectarla en los audit logs
Detecta criptominería en clústeres de Kubernetes con los audit logs: imágenes y argumentos de mineros, registries inusuales, persistencia con CronJob.
TL;DR. Un minero en un clúster es un workload, y todo workload se crea a través del API server. En el audit log, busca escrituras de workloads cuya imagen o argumentos nombren un minero (XMRig y compañía) o un pool de minería (stratum+tcp://, --donate-level), imágenes descargadas de registries que no usas y la persistencia que las rodea: CronJobs, DaemonSets, a menudo en kube-system. Después localiza la identidad que los creó: el minero es el síntoma, la credencial robada es el incidente. El secuestro de recursos es ATT&CK T1496.
La criptominería es la forma más habitual en que una intrusión en un clúster se hace visible, porque cuesta dinero y CPU. También es la parte menos interesante de la investigación: quien desplegó el minero tenía acceso suficiente para hacer cosas mucho peores, y puede que las hiciera.
Un patrón documentado
Microsoft describió una campaña así en 2020: dashboards de Kubeflow expuestos a Internet mediante un balanceador permitían a cualquiera desplegar contenedores, y los atacantes lo aprovecharon para ejecutar una imagen con XMRig en decenas de clústeres, la mayoría en AKS (blog de Microsoft Security, «Misconfigured Kubeflow workloads are a security risk»). El mismo artículo explica por qué atraen esos clústeres: los nodos de machine learning son potentes y a veces tienen GPU.
El patrón es común: un dashboard expuesto o un token con demasiados privilegios como punto de entrada, y después workloads creados con las propias credenciales del clúster. El recorrido del incidente ficticio de este sitio sigue esa misma forma.
Qué registra el audit log
En nivel Request o RequestResponse para las escrituras de workloads (el valor por defecto en EKS y GKE, consulta la guía de exportación), el requestObject de un Deployment, DaemonSet, Job o CronJob contiene la plantilla del pod: imágenes, comandos, argumentos, recursos. Un CronJob ficticio:
{
"verb": "create",
"user": { "username": "system:serviceaccount:kube-system:node-health" },
"objectRef": { "resource": "cronjobs", "namespace": "kube-system", "name": "kube-state-sync", "apiGroup": "batch" },
"requestObject": {
"spec": {
"schedule": "*/5 * * * *",
"jobTemplate": { "spec": { "template": { "spec": {
"containers": [{
"name": "sync",
"image": "registry.k8s-mirror.example:5000/xmrig/xmrig:6.21.0",
"args": ["--url=pool.minexmr.example:4444", "--donate-level=0", "--cpu-max-threads-hint=75"],
"resources": { "limits": { "cpu": "2" } }
}]
} } } }
}
}
}
Todo lo que necesitas está en un evento: el minero, el pool, un nombre que imita un add-on de Kubernetes, un namespace elegido para camuflarse, un registry que no es el tuyo, una programación que recrea los pods si alguien los borra y un límite de CPU pensado para pasar desapercibido.
Detecciones
| Regla | Severidad | Lógica |
|---|---|---|
| Imagen o comando de crypto-mining | Crítica | La imagen contiene un marcador de minero (xmrig, xmr-stak, minerd, cpuminer, nicehash, kinsing, c3pool, moneroocean…), o el comando / los argumentos del contenedor contienen stratum+tcp, stratum+ssl, --donate-level, xmrig, minerd; también alertas de Falco cuyo comando coincide |
| Imagen proveniente de un registry inusual | Baja | Registry fuera de una lista de registries públicos y cloud comunes (docker.io, registry.k8s.io, gcr.io, ghcr.io, quay.io, mcr.microsoft.com, public.ecr.aws, *.amazonaws.com, *.pkg.dev, *.azurecr.io…) |
| CronJob creado | Media | Cualquier CronJob escrito por una identidad que no es de sistema |
| DaemonSet creado en kube-system | Alta | Consulta pods privilegiados y escape de contenedores |
La regla de mineros es la única del analizador que salta sea quien sea quien creó el pod, controladores incluidos: un minero lanzado por un CronJob aparece como pods creados por el controlador de Jobs, y aun así quieres verlo. La agrupación es por imagen, así que un hallazgo cubre todas las réplicas.
La regla de registries tiene severidad baja a propósito. Tu propio registry privado la disparará hasta que lo añadas a la lista de confianza; a partir de ahí, cualquier otro destaca.
La evasión que cabe esperar
Buscar por nombre atrapa a los atacantes perezosos. Los demás:
- renombran el binario y la imagen (
alpine,nginx,busyboxcon una carga descargada); - parten de una imagen legítima y descargan el minero en el comando (
curl … | sh); - minan por TLS hacia un proxy en el puerto 443 en lugar de a un pool conocido;
- ejecutan el minero en el nodo tras un escape de contenedor, sin ninguna llamada a la API.
Así que fíjate también en el contexto que sí registra el audit log: workloads nuevos creados por identidades que nunca despliegan, imágenes de registries nuevos, campos command con patrones de descarga y ejecución, CronJobs y DaemonSets creados fuera de tu pipeline de despliegue y workloads creados justo después de un binding a cluster-admin nuevo.
Señales fuera del audit log
La minería hace ruido en otros canales, y a menudo es por ahí por donde se descubre:
- Métricas de CPU y nodos: nodos al límite, cluster autoscaler añadiendo nodos sin motivo de negocio.
- Tráfico de salida: conexiones a pools de minería, típicamente en puertos como 3333, 4444 o 5555, o a endpoints TLS inusuales.
- Facturación cloud: un salto en cómputo, a veces en regiones que no usas.
- Detección de runtime: Falco puede señalar comportamiento de minería a nivel de procesos y red; el proyecto describe el enfoque en Cryptomining Detection Using Falco. Carga las alertas de Falco en JSON junto a los audit logs en el analizador para verlas en una misma línea de tiempo.
Una limpieza que funcione de verdad
Borrar los pods no basta: el CronJob o el DaemonSet los recrea en minutos.
- Preserva: guarda los specs (
kubectl get cronjob,daemonset,deployment -A -o yaml), los audit logs y, si es posible que haya habido un escape, snapshots de los discos de los nodos. - Encuentra todas las copias: busca la imagen, la dirección del pool y el comando en todos los namespaces, no solo en el que lo encontraste.
- Elimina primero los controladores (CronJobs, DaemonSets, Deployments, Jobs) y después los pods que queden.
- Elimina el acceso: el binding y la cuenta de servicio usados para crearlos, y el punto de entrada original.
- Reconstruye los nodos si el minero corrió privilegiado o con montajes del host.
- Bloquea la salida hacia pools de minería y restringe el tráfico saliente con network policies.
- Restringe los registries con una política de admisión (ValidatingAdmissionPolicy, Kyverno o Gatekeeper), idealmente con verificación de firmas.
- Revisa la facturación y el log de auditoría cloud en busca de cualquier cosa creada fuera del clúster con las mismas credenciales.