De privileged a least-privilege: endurecí mi homelab sin romper nada

Mi contenedor 'de seguridad' era el más peligroso de todos: corría privileged con host networking. Lo bajé a una sola capability, moví los secrets a SOPS, y rechacé un shortcut tentador. La receta y los trade-offs honestos.


Tenía un contenedor llamado security-backend — el que sirve mi dashboard de seguridad. Irónicamente, era el contenedor más peligroso de todo el homelab: corría privileged: true con network_mode: host. Acceso total al kernel y a la red del host.

Osea, mi servicio de seguridad era el hueco de seguridad.

Lo endurecí sin romper nada. Aquí está el antes/después y — más importante — los trade-offs que tomé, incluyendo un shortcut tentador que decidí no tomar.

El problema: privilegios que nadie necesita

privileged: true le da al contenedor todas las capabilities de Linux, acceso a todos los devices, y básicamente control del host. La gente lo pone porque “así funciona” y ya. Pero ese servicio solo necesitaba una cosa: leer un archivo de log (/var/log/auth.log) para detectar intentos de SSH.

Leer un archivo no necesita el kernel entero.

El después: una sola capability

security-backend:
  cap_drop:
    - ALL                    # tira TODAS las capabilities
  cap_add:
    - DAC_READ_SEARCH        # solo bypass de lectura, para ese log
  security_opt:
    - "no-new-privileges:true"
  volumes:
    - /var/log/auth.log:/var/log/auth.log:ro   # solo ese archivo, read-only

De todas las capabilities a una de solo-lectura. Además: quité host networking (el contenedor ya no ve/toca la red del host) y estreché el mount de /var/log (todo) a un solo archivo :ro.

El blast radius pasó de “control del kernel” a “leer archivos”.

El trade-off: perdí algo, y está bien

Al quitar network_mode: host, el contenedor dejó de ver los puertos y el tráfico de red del host — solo ve los suyos. ¿Se perdió esa métrica? No: la moví a node-exporter, que es la fuente correcta para métricas del host de todas formas.

Ganar seguridad casi siempre cuesta algo. La pregunta no es “¿pierdo algo?” sino “¿el trade-off vale la pena y hay un lugar mejor para eso?”. Aquí sí.

El shortcut que NO tomé

Para recuperar las métricas del host sin host networking, la tentación era montar el /proc del host dentro del contenedor:

volumes:
  - /proc:/host/proc:ro   # ❌ tentador, pero no

Se ve inofensivo (:ro). No lo es. Con /proc del host montado, un proceso root en el contenedor puede leer /proc/<PID>/environ de otros procesos del host — es decir, las variables de entorno de otros contenedores. Ahí viven secrets.

Montar /host/proc para arreglar una métrica me habría abierto un camino a los secrets de todo lo demás. Lo dejé fuera. node-exporter da esas métricas sin ese riesgo.

Los secrets: fuera del texto plano

Tenía ~40 secrets en un .env en texto plano. Los pasé a SOPS + age: el archivo va cifrado, la llave privada puede vivir fuera de la caja, y el deploy desencripta al vuelo.

sops secrets.enc.env       # edita descifrado en tu editor, re-cifra al guardar
./deploy.sh                # sops -d ... > .env  y levanta

Y aquí soy honesto, porque casi nadie lo dice: en un solo host, la llave y el ciphertext conviven — si alguien tiene root en la caja, tiene ambos. El beneficio runtime es modesto. La ganancia real es cifrado-en-reposo, versionado sin miedo, y disaster recovery. Prefiero decir eso a vender SOPS como algo que no es en mi contexto.

La lección

Seguridad “de verdad” en un homelab no es agregar más herramientas (WAF, IDS, el juguete de moda). Es:

  1. Reducir el blast radius — que cada contenedor tenga el mínimo privilegio que necesita, y ni uno más.
  2. Rechazar shortcuts que se ven inofensivos pero abren superficie.
  3. Nombrar los trade-offs en vez de taparlos.

Eso es lo que separa “tengo Docker” de “opero sistemas”. El resto del stack — observabilidad, alertas, el agente de IA que lee todo esto — está en otro post y en el case study.


← todos los posts