Proyecto: Cluster de Kubernetes con nodos efímeros en VPS externos, honeypots corriendo en producción, logs centralizados en ELK y todo el tráfico cifrado por WireGuard. Desde un homelab.


Presentación

Por mucho que sepas utilizar una tecnología, sin un proyecto no sirve de nada. Llevo un tiempo trasteando con Kubernetes, Nodos, Proxmox, Honeypots, Servidores…. Los servicios se me acumulaban individualmente, aislados en VLANs, Redes Virtuales o Maquinas separadas.

Empezó a sonar en mi cabeza una vocecilla que decia "¡Juntalo todo!".

¡Manos a la obra!


Diseño

Antes de empezar a levantar cosas sin sentido hay que diseñar. Las tecnologías estaban claras, ahora quedaba combinarlas y darles forma.

El objetivo y la idea

Lo primero que me vino a la cabeza son los honeypot. Son faciles de detectar y una vez que sabes que estas en una trampa no vuelves a ella. Luego vino la comunicación, tengo que conseguir una red aislada que no entre en conflicto con IP’s publicas, por si surgiera cualquier imprevisto. Voy a sacar muchisima información, demasiada, tengo que ser capaz de gestionarla correctamente y darle un uso util. La gestión de el trafico tiene que ser impecable, no se puede colar ni escapar absolutamente NADA.

Ahora que tenemos la base solo me quedaba un problema. La IP se quema. En pocos días los scanners detectan el honeypot por sus firmas o por comportamiento y lo marcan.
El primer escaner que me vino a la cabeza fue Shodan, realizan escaneos diarios de practicamente todo lo que existe en internet. Si tienes un honeypot durante más de una semana date por vencido, tendrás expuestos de manera global todos tus puertos, certificados, servicios e incluso vulnerabilidades con una etiqueta enorme que dice HONEYPOT.

¿Solucion? Ser más rápido
Si rotamos la dirección IP, usamos nodos efímeros que se destruyen antes de que se realize el escaneo, habremos recogido ataques durante un periodo de tiempo suficientemente largo como para que sea fructifero y no nos hayan fichado hasta el DNI. Una IP limpia es una IP que todavía no sabe nadie que es un honeypot.

La arquitectura

Homelab (CPD)
├── hEX S → Router/Firewall/WireGuard server
├── Zimaboard (Proxmox) → Master k8s + ELK
VPS (worker k8s efímero)
└── Pod OpenCanary  → SSH, HTTP, Telnet
└── Pod Mailoney    → SMTP
└── Filebeat        → logs → Elasticsearch

El master de Kubernetes vive en el CPD, en una VM dentro del Proxmox de la Zimaboard. Los workers son VPS en Vultr que se unen al cluster por WireGuard. Los honeypots corren en esos workers. Los logs van a Elasticsearch. Desde fuera solo se ve la IP del VPS y los puertos de los honeypots. El master nunca está expuesto.

Cuando un nodo lleva demasiado tiempo activo y puede estar quemado, se destruye y se crea uno nuevo. Terraform + cloud-init. Nueva IP, mismo cluster, mismo setup automático.

Estructura

Decisiones de diseño

¿Por qué Kubernetes y no Docker Compose? K8s me da scheduling, health checks, rolling updates y la posibilidad de añadir nodos remotos de forma nativa. Un worker remoto en un VPS es un nodo más del cluster. No tengo que gestionar nada manualmente. Esta gestión de nodos facilita la creación y destrucción de honeypots de manera casi automática.

¿Por qué WireGuard y no una VPN clásica? WireGuard es simple, rápido y RouterOS v7 lo soporta de forma nativa en el MikroTik hEX. La clave pública del peer, la red del túnel y listo. El tráfico del cluster viaja cifrado sin que tenga que abrir puertos innecesarios.

¿Por qué Elasticsearch y Kibana? Elasticsearch es versatil, cómodo y eficiente, acompañado de Kibana se hace indestructible a la hora de administrar datos. Tiene la posibilidad de realizar consultas y de crear visualizaciones de datos muy definidas.


Preparación

Hardware

DispositivoRol
MikroTik hEXRouter, firewall, WireGuard Server
Zimaboard 2 (N150, 16GB RAM)Proxmox producción + Master k8s + ELK
VPS (1 vCPU, 1GB, 6€/mes)Worker k8s + honeypots

Software base

  • Proxmox VE en Zimaboard
  • Ubuntu 22.04 en la VM del Master k8s
  • Ubuntu 22.04 en el VPS worker
  • RouterOS v7 en el hEX S
  • WinBox gestion MikroTik

Configuración MikroTik

El Mikrotik hEX S es el cerebro de la red. Gestiona el tráfico, las subredes, el firewall y el servidor WireGuard.

Subredes

WAN  → eth1 → DHCP desde el router del ISP (Salida Internet)
PROD → eth2 → 10.10.10.0/24 → Zimaboard (Proxmox)

Firewall

Las reglas críticas en IP → Firewall → Filter Rules:

ACCEPT  established, related (tráfico ya establecido)
ACCEPT  LAN → WAN (salida a internet)
DROP    DMZ → PROD (el resto de maquinas no pueden hablar con Proxmox)
DROP    WAN → PROD (Proxmox no accesible desde fuera)
ACCEPT  forward in/out wireguard1 (tráfico entre peers WireGuard)

Con esto conseguimos aislar el resto de trafico en la red de nuestro mikrotik con la red de los Honeypots.

Rutas estáticas

El hEX S necesita saber que la red del túnel WireGuard (10.10.20.0/24) se alcanza por la interfaz WireGuard. RouterOS lo gestiona automáticamente cuando creamos la interfaz, pero los peers necesitan tener correctamente configurados sus AllowedIPs para que el routing funcione en ambas direcciones.


WireGuard

WireGuard crea una red segura que une el master con los workers en la VPS. Sin él, k8s no tendrá comunicación y no podrá orquestar los nodos. El API server del master tiene que ser alcanzable desde los workers para que el servicio kubelet pueda registrarse y recibir instrucciones.

Mapa de red del túnel

hEX           → 10.10.20.1 (server)
Master k8s     → 10.10.20.2 (peer)
Worker VPS     → 10.10.20.3 (peer)

El Serivor Wireguard en Mikrotik hEX actúa como conector. Todos los peers se conectan a él para entrutar el tráfico entre los nodos.

Configuración en Mikrotik RouterOS 7

En WinBox: WireGuard → nueva interfaz KubeHive, puerto 51820. RouterOS genera las claves automáticamente. Se añade cada peer con su clave pública y sus AllowedIPs.

Peer del master:

Public Key: <clave del master>
Allowed IPs: 10.10.20.2/32, 10.10.10.0/24

Peer del worker VPS:

Public Key: <clave del VPS>
Allowed IPs: 10.10.20.3/32

El AllowedIPs del peer del master necesitaremos incluir la red 10.10.10.0/24, es la red en la que se encuentra el Master de k8s (10.10.10.10) donde escucha el API server de k8s. De lo contrario, el worker no podra enviar los datos recogidos al Master ni recibira instrucciones de kubelet.

Configuración en el Master

/etc/wireguard/KubeHive.conf:

[Interface]
PrivateKey = <Clave privada>
Address = 10.10.20.2/24

[Peer]
PublicKey = <Clave publica Mikrotik>
Endpoint = <IP Mikrotik>:51820
AllowedIPs = 10.10.20.0/24
PersistentKeepalive = 25

Configuración en el VPS worker

/etc/wireguard/KubeHive.conf:

[Interface]
PrivateKey = <Clave privada>
Address = 10.10.20.3/24

[Peer]
PublicKey = <Clave publica Mikrotik>
Endpoint = <IP Mikrotik>:51820
AllowedIPs = 10.10.10.0/24, 10.10.20.0/24 #Añadimos la red del Master
PersistentKeepalive = 25

Los AllowedIPs del worker incluyen la red del túnel, la red del master y las redes internas de k8s (pods y servicios). Sin esto el tráfico del cluster no entra en el túnel y el nodo nunca llega a ser Ready.

Comprobación

Con todo esto, un ping 10.10.10.10 desde la VPS y un ping 10.10.20.2 desde el master confirman que el túnel funciona en ambas direcciones antes de montar k8s. Hay que tener presente que las reglas configuradas en el firewall mikrotik no tengan conflicto con la red WG.


Kubernetes

Con la red funcionando ahora toca montar el cluster. En este caso usaremos la versión vanilla, Kubernetes con kubeadm, sin distribuciones gestionadas, sin managed services. Esto nos permitirá analizar y gestiona cada pieza del cluster a nuestro gusto.

Configuracion Kubernetes Master

Prerequisitos antes del init:

  • Desactivar la memoria swap
  • Activar modulo br_netfliter
  • Permitir ip_forward y modo bridge

Para esto utilizaremos los siguientes comandos.

swapoff -a
sed -i '/ swap / s/^/#/' /etc/fstab
modprobe br_netfilter
echo "br_netfilter" >> /etc/modules-load.d/k8s.conf
sysctl -w net.bridge.bridge-nf-call-iptables=1
echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
echo "net.bridge.bridge-nf-call-iptables=1" >> /etc/sysctl.conf

Iniciamos el cluster con kubeadm, utilizaremos la dirección de nuestra red interna 10.10.10.10/24 como endpoint para el control plane, de esta manera cuando los workers vayan a buscar al master el servicio estará expuesto.
Ejecutamos:

kubeadm init \
  --pod-network-cidr=10.244.0.0/16 \
  --control-plane-endpoint=10.10.10.10 \
  --node-ip=10.10.10.10

El --node-ip es crítico. Sin él, kubeadm puede coger la IP de WireGuard en lugar de la IP real del master, y el kubelet queda apuntando a una IP que no existe en sus interfaces. Esto rompe el cluster de forma silenciosa y es dificil de encontrar.

CNI: Calico

Se eligió Calico como CNI ya que permite implementar politicas de seguirdad avanzadas con eBPF y tiene integración nativa con Wireguard.

Calico nos proporciona la función hostPort, de cara a los honeypot es algo muy útil, sin embargo nosotros dispondremos del parametro hostNetwork: true ya que los honeypots necesitan escuchar en los puertos reales del sistema (22, 25, 23). Con hostNetwork: true el pod usa la red del host directamente. Sin NodePort, sin DNAT, sin reglas de iptables que mantener. Y la IP del pod no cambia cuando el pod se reinicia, porque la IP es la del host.

Aplicamos la siguiente configuración obtenida de la web del CNI Calico:

vim custom-resources.yaml
apiVersion: operator.tigera.io/v1
kind: Installation
metadata:
  name: default
spec:
  calicoNetwork:
    # Note: The ipPools section cannot be modified post-install.
    ipPools:
    - blockSize: 26
      cidr: 10.244.0.0/16
      encapsulation: VXLANCrossSubnet
      natOutgoing: Enabled
      nodeSelector: all()
---
apiVersion: operator.tigera.io/v1
kind: APIServer
metadata:
  name: default
spec: {}

Y aplicamos el fichero de configuración creado:

kubectl apply -f custom-resources.yaml

Una vez que este aplicado comprobamos que todo el cluster está listo y todos los pods en Running:

kubectl get pods -n calico-system

calico-ready.png

Worker (VPS)

Para todos los nodos tendremos los mismos prerequisitos que el master. Una vez que tengamos configurado WireGuard y haya conexion entre Master - Worker procederemos a unirlo al cluster.

Usaremos el parametro join con el token generado por el init:

kubeadm join 10.10.10.10:6443 \
  --token <token> \
  --discovery-token-ca-cert-hash sha256:<hash>

Nota: En caso de que el token generado en el init del cluster no este disponible podremos generar uno nuevo con el siguiente comando kubeadm token create --print-join-command

Una vez se haya unido, se crearán pods de Calico que se encargarán de la gestión de la red del Nodo.

Los honeypots

Los Honeypots van a ser desplegados únicamente en los Worker, la gestión de los nodos se realizará desde el Master, en esta instalación utilizaremos dos Honeypot (OpenCanary/Mailoney).

OpenCanary

OpenCanary simula SSH, HTTP y Telnet. Con hostNetwork: true escucha directamente en la IP pública del VPS. El pod tiene label role: honeypot y nodeSelector para asegurarse de que siempre va al worker, nunca al master.

Crearemos dos ficheros con la configuración.

opencanary-configmap.yaml -> Donde declaramos los puertos, servicios y formato de logs.

apiVersion: v1
kind: ConfigMap
metadata:
  name: opencanary-config
  namespace: default
data:
  opencanary.conf: |
    {
        "device.node_id": "KubeHive-XXXX",
        "ip.ignorelist": [],
        "ftp.enabled": false,
        "ftp.port": 21,
        "ftp.banner": "FTP server ready",
        "http.enabled": true,
        "http.port": 8080,
        "http.banner": "Apache/2.4.1 (Unix)",
        "http.skin": "nasLogin",
        "ssh.enabled": true,
        "ssh.port": 22,
        "ssh.version": "SSH-2.0-OpenSSH_5.9p1 Debian-5ubuntu1",
        "telnet.enabled": true,
        "telnet.port": 23,
        "smtp.enabled": true,
        "smtp.port": 25,
        "smtp.banner": "Postfix SMTP",
        "mysql.enabled": false,
        "mysql.port": 3306,
        "redis.enabled": false,
        "redis.port": 6379,
        "logger": {
            "class": "PyLogger",
            "kwargs": {
                "formatters": {
                    "plain": {
                        "format": "%(message)s"
                    }
                },
                "handlers": {
                    "console": {
                        "class": "logging.StreamHandler",
                        "stream": "ext://sys.stdout"
                    },
                    "file": {
                        "class": "logging.FileHandler",
                        "filename": "/var/log/opencanary/opencanary.log"
                    }
                }
            }
        }
    }

opencanary-deployment.yaml -> Donde declaramos los contenedores y configuraciónes.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: opencanary
  namespace: default
  labels:
    app: opencanary
spec:
  replicas: 1
  selector:
    matchLabels:
      app: opencanary
  template:
    metadata:
      labels:
        app: opencanary
    spec:
      hostNetwork: true
      dnsPolicy: ClusterFirstWithHostNet
      nodeSelector:
        role: honeypot
      containers:
        - name: opencanary
          image: thinkst/opencanary:latest
          imagePullPolicy: IfNotPresent
          securityContext:
            privileged: false
            runAsUser: 0
            allowPrivilegeEscalation: true
            capabilities:
              add:
                - NET_BIND_SERVICE
                - SETUID
                - SETGID
              drop:
                - ALL
          volumeMounts:
            - name: config
              mountPath: /etc/opencanaryd/opencanary.conf
              subPath: opencanary.conf
            - name: logs
              mountPath: /var/log/opencanary
      volumes:
        - name: config
          configMap:
            name: opencanary-config
        - name: logs
          hostPath:
            path: /var/log/opencanary
            type: DirectoryOrCreate

La versión de SSH es intencionada: este honeypot imita un servidor Ubuntu 12.04 con una versión antigua de OpenSSH.

Mailoney

Mailoney simula SMTP. Banner de Exim 4.97, logs en JSON. La imagen que uilizaremos será dtagdevsec/mailoney:24.04.1.

Como variables de entorno usaremos LOGPATH=/var/log/mailoney/ para que los logs vayan al hostPath y persistan aunque el pod se reinicie.

Crearemos un fichero con la siguiente configuración:

mailoney.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mailoney
  namespace: default
  labels:
    app: mailoney
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mailoney
  template:
    metadata:
      labels:
        app: mailoney
    spec:
      hostNetwork: true
      dnsPolicy: ClusterFirstWithHostNet
      nodeSelector:
        role: honeypot
      containers:
        - name: mailoney
          image: dtagdevsec/mailoney:24.04.1
          imagePullPolicy: IfNotPresent
          env:
            - name: LOGPATH
              value: /var/log/mailoney/
          securityContext:
            capabilities:
              add:
                - NET_BIND_SERVICE
          volumeMounts:
            - name: logs
              mountPath: /var/log/mailoney
      volumes:
        - name: logs
          hostPath:
            path: /var/log/mailoney
            type: DirectoryOrCreate 

Despliegue

Iniciamos los honeypot para que empiecen a recoger intentos de ataques.

kubectl apply -f opencanary-configmap.yaml
kubectl apply -f opencanary-deployment.yaml
kubectl apply -f mailoney.yaml

ELK Stack

Elasticsearch y Kibana corren directamente en el SO del master, fuera de k8s. No tiene sentido meterlos en el cluster cuando los logs vienen de ficheros en el host del VPS, no de los pods.

Filebeat corre en todos los VPS y mandan los logs a Elasticsearch por el túnel WireGuard:

Editando el archivo /etc/filbeat/filebeat.yml prepararemos la siguiente configuración:

ilebeat.inputs:
  - type: log
    enabled: true
    paths:
      - /var/log/opencanary/opencanary.log
    fields:
      honeypot: opencanary
    json.keys_under_root: true
    json.add_error_key: true

  - type: log
    enabled: true
    paths:
      - /var/log/mailoney/commands.log
    fields:
      honeypot: mailoney
    json.keys_under_root: true
    json.add_error_key: true
    processors:
      - rename:
          fields:
            - from: "src_ip"
              to: "src_host"
          ignore_missing: true
          fail_on_error: false

output.elasticsearch:
  hosts: ["https://10.10.10.10:9200"]
  username: "filebeat_honeypot"
  password: "${ES_PASSWORD}"
  ssl.certificate_authorities: ["/etc/filebeat/http_ca.crt"]
  index: "honeypot"

setup.kibana:
  host: "http://10.10.10.10:5601"

setup.ilm.enabled: false
setup.template.name: "honeypot"
setup.template.pattern: "honeypot*"

La contraseña va en el keystore de Filebeat, no en texto plano. Esto lo hacemos con filebeat keystore add ES_PASSWORD. Así si el honeypot se viera comprometido, conseguimos frenar el impacto en nuestro centro de datos.

El usuario filebeat_honeypot tiene un rol con permisos mínimos: solo escribir en índices honeypot-* y los cluster privileges necesarios (monitor, manage_index_templates) declarados en el servidor ELK.

El resultado: un data stream honeypot-YYYY.MM.DD con todos los eventos de los honeypots, visualizable en Kibana con un dashboard de IPs atacantes, puertos más golpeados, credenciales probadas y actividad temporal.

Resultado

Pasados unos días veremos en nuestra Dasboard de Kibana que van apareciendo logs, ataques, direcciones IP, credenciales probadas, puertos etc.

Credenciales más usadas

UserPasswordRecords
adminadmin5,612
rootxc35114,886
rootvizxv4,402
rootadmin4,223
root1234562,893
root8888882,746
rootroot2,736
supportsupport2,614

Ataques por día ataques-dia.png

Localizaciones de ataques

mapa-ataques


Terraform

En construcción.

El objetivo final es que levantar un nuevo nodo worker sea un único comando:

terraform apply

Y destruirlo también:

terraform destroy

Terraform provisionará el VPS. Cloud-init se ejecutará al arrancar y hará todo lo demás: instalar las dependencias, configurar WireGuard, unir el nodo al cluster, desplegar los honeypots y arrancar Filebeat. Una IP limpia, lista para recibir ataques, en menos de cinco minutos.

La rotación será manual al principio (cuando vea que el nodo lleva demasiado tiempo activo o que el volumen de datos cae porque los bots ya lo han marcado) y automática más adelante con un script que evalúe los datos de Kibana.


Conclusiones

Este proyecto no está terminado. Terraform, cloud-init, la automatización de la rotación, agregar nuevos tipos de honeypots, hacerlos más convincentes, análisis de los datos… queda mucho por exprimir.

Actualmente funciona: un cluster de Kubernetes con un nodo Master y un worker en un VPS, comunicados por un túnel WireGuard que pasa por un MikroTik configurado a mano, con honeypots recogiendo credenciales e intentos de acceso reales, y todo centralizado en un Elasticsearch con Kibana.

Con esto ya se puede desarrollar una inteligencia de datos, bloquear rangos de IP’s que estan escaneando constantemente internet, identificar ataques antes de que ocurran, recoger credenciales más utilizadas… una infinidad de cosas.

Esta infraestructura puede aplicar a cualquier entorno de producción, el despliegue automatico, la gestion descentralizada desde un orquestador de contenedores y la seguridad que ofrece crear tuneles cifrados internamente para el intercambio de datos otorgan infinitas posibilidades.


Stack: MikroTik RouterOS v7 · WireGuard · Proxmox · Kubernetes v1.36 · Calico · OpenCanary · Mailoney · Filebeat · Elasticsearch 8.x · Kibana