Montar un homelab desde cero: seguridad y operaciones - Homelab (04/06)
En el Post 3 vimos cómo las aplicaciones pasan de Git a contenedores en ejecución. Pero nos saltamos una pregunta crítica: ¿cómo llegan los secretos a esos contenedores?
Una contraseña de base de datos, un token de API, un certificado TLS — nada de esto puede vivir en Git en texto plano. Y una vez desplegados, hay que respaldarlos. Y si todo falla, necesitas una forma de reconstruir desde cero.
Este último post cubre las tres capas de gestión de secretos, la estrategia de backup, la domótica, y lo que necesitarías para recuperarte de un desastre total. Es lo aburrido pero imprescindible que marca la diferencia entre un proyecto de fin de semana y una infraestructura en la que puedes confiar.
El problema de los secretos
Todo el homelab se gestiona desde un único repositorio Git. Genial para reproducibilidad y auditoría, pero genera una tensión: las configuraciones tienen que estar en Git para que funcione el despliegue GitOps, los secretos no pueden estar en Git por razones obvias de seguridad, y aun así los secretos tienen que llegar a los servicios de forma automática en el momento del despliegue.
No hay una sola herramienta que resuelva esto. Son tres, cada una para una parte del problema.
Capa 1: Infisical (la caja fuerte)
Infisical es un gestor de secretos autoalojado. Pensad en él como un almacén de contraseñas para la infraestructura. Todos los secretos del homelab — contraseñas de bases de datos, tokens de API, credenciales OAuth, contraseñas SMTP — viven en Infisical como fuente única de verdad.
Infisical corre en el clúster pizarro (red Roma) y es accesible en turing.nexolabs.dev. Al ser autoalojado, los secretos nunca salen del control del homelab.
Dos vías de salida
Para servicios Docker Compose — ptolomeo (el agente de CD) obtiene los secretos de Infisical en el momento del despliegue y los inyecta como variables de entorno:
# .doco-cd.yaml — ptolomeo lee esto
external_secrets:
DB_PASSWORD: 893d53fb-...:prod:/my-app/DB_PASSWORD
SMTP_HOST: 893d53fb-...:prod:/my-app/SMTP_HOSTCuando ptolomeo despliega un stack, llama a la API de Infisical, obtiene los valores, los escribe en .env y ejecuta docker compose up. El contenedor ve DB_PASSWORD=xyz123 como una variable de entorno normal.
Para Kubernetes — el External Secrets Operator (ESO) corre dentro del clúster y sincroniza periódicamente los secretos de Infisical como Secrets nativos de Kubernetes:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: web-secrets
spec:
refreshInterval: 1m
secretStoreRef:
name: infisical-production
kind: ClusterSecretStore
data:
- secretKey: AUTH_SECRET
remoteRef:
key: AUTH_SECRET
- secretKey: AUTH_KEYCLOAK_SECRET
remoteRef:
key: AUTH_KEYCLOAK_SECRETESO crea un Secret de Kubernetes llamado web-secrets con los valores de Infisical. Los pods lo montan como variables de entorno o ficheros — sin saber de dónde vienen los secretos.
ESO refresca cada minuto. Si rotas una contraseña en Infisical, el Secret de Kubernetes se actualiza automáticamente. Sin necesidad de redesplegar.
El patrón ClusterSecretStore
En cortes, los secretos se organizan con componentes Kustomize siguiendo el mismo patrón base/overlay que la infraestructura (del Post 3):
manifests/infisical/
base/
cluster-secret-store.yaml # Plantilla con placeholders
components/
secrets-web/ # AUTH_SECRET, KEYCLOAK_SECRET, etc.
secrets-postgres/ # POSTGRES_USER, POSTGRES_PASSWORD
secrets-keycloak/ # Credenciales KC_ADMIN, config del realm
secrets-typesense/ # TYPESENSE_API_KEY
secrets-backup/ # Credenciales S3, RESTIC_PASSWORD
secrets-stripe/ # Claves de procesamiento de pagos
secrets-ai/ # Claves de API de LLM
overlays/
prod/kustomization.yaml # Parchea nombre del store + slug de entorno
staging/kustomization.yamlLa base define una plantilla de ClusterSecretStore. Cada overlay la parchea con el proyecto y entorno de Infisical correctos. Así, producción lee del entorno prod de Infisical y staging lee de staging — misma estructura, credenciales distintas.
Capa 2: SOPS/age (el candado de los ficheros)
Hay ficheros con secretos que tienen que existir en el repositorio Git. Por ejemplo, Talos Linux necesita los certificados del clúster y los tokens de arranque para generar las configuraciones de máquina. Estos viven en talsecret.sops.yaml junto a talconfig.yaml.
SOPS cifra los valores de un fichero YAML dejando las claves legibles. Combinado con age (una herramienta de cifrado), genera ficheros que se pueden commitear sin riesgo:
# Lo que vive en Git (cifrado)
cluster:
id: ENC[AES256_GCM,data:QqUK9Xr6cWv0ed9F...,tag:tnMuAX...,type:str]
secret: ENC[AES256_GCM,data:ncoTvU2yA5He7n2r...,tag:o5SrlE...,type:str]Se ve la estructura (cluster.id, cluster.secret) pero no los valores. Descifrar requiere la clave privada de age — que está almacenada en Infisical, nunca en Git.
El fichero .sops.yaml en la raíz del repo le indica a SOPS qué clave pública usar:
creation_rules:
- path_regex: \.sops\.(yaml|yml)$
age: age1xrrk8gryc27haj9uhr4tg62rde3jen3u2pl8lv0hggdvr02pcytsvk6zf8
- path_regex: secrets\.env$
age: age1xrrk8gryc27haj9uhr4tg62rde3jen3u2pl8lv0hggdvr02pcytsvk6zf8Para descifrar (al aplicar configuraciones de Talos):
infisical run --path=/zetesis-portal -- bash -c \
'SOPS_AGE_KEY="$AGE_SECRET_KEY" talhelper genconfig'Infisical proporciona la clave privada de age como variable de entorno. SOPS la usa para descifrar los ficheros. La salida descifrada va a clusterconfig/ (gitignored, nunca se commitea).
Qué hay cifrado en Git
Fichero — Contenido
talsecret.sops.yaml — Certificados del clúster Talos, tokens de etcd
inline-secrets.sops.yaml — Secrets K8s de arranque (credenciales ESO, auth Git de ArgoCD)
secrets.env — Secretos de servicios Docker (CF_API_TOKEN de Caddy, etc.)
Todo lo demás se queda en Infisical y nunca toca Git.
Capa 3: el arranque del huevo y la gallina
El rompecabezas: ESO necesita credenciales para conectarse a Infisical. Pero esas credenciales son un Secret de Kubernetes. Y ESO es precisamente lo que crea Secrets de Kubernetes desde Infisical. ¿De dónde salen entonces las credenciales de ESO?
La respuesta: inline manifests de Talos. El fichero inline-secrets.sops.yaml contiene manifiestos de Secret de Kubernetes cifrados con SOPS. Cuando Talos arranca el plano de control, aplica estos manifiestos directamente — antes de que ArgoCD o ESO siquiera arranquen. Así se rompe la dependencia circular:
Estos secretos de arranque son los únicos gestionados vía SOPS. A partir de ahí, todo es automático a través de ESO.
Cómo encajan las tres capas
Infisical es la fuente única de verdad. ptolomeo hace de puente entre Infisical y los contenedores Docker (variables de entorno). ESO hace de puente entre Infisical y los pods de Kubernetes (Secrets nativos). Y SOPS/age cubre el caso especial de ficheros que tienen que existir en Git.
Estrategia de backup
Todos los datos del homelab se respaldan en un único lugar: MinIO en spinoza (10.1.0.11). La herramienta de backup es Restic — un programa de backup cifrado y deduplicado que soporta S3.
Hosts Docker: tolstoi
Todos los VPS y VMs corren un servicio llamado tolstoi — un wrapper de Restic que se ejecuta a diario a las 3:00 de la mañana. Respalda volúmenes Docker a MinIO a través de la pasarela Tailscale (cervantes):
# Configuración base de backup (services/tolstoi)
services:
resticker-base:
image: mazzolino/restic:1.8.2
environment:
BACKUP_CRON: "0 3 * * *"
RUN_ON_STARTUP: "true"
RESTIC_FORGET_ARGS: >-
--keep-daily 7
--keep-weekly 4
--keep-monthly 12
--keep-yearly 7Cada despliegue extiende esta base con sus propios volúmenes y bucket S3:
Host — Bucket — Qué se respalda
von-braun — von-braun-restic — Datos de Caddy, BD de CrowdSec
escohotado — escohotado-restic — PostgreSQL, Ghost, Typesense
unamuno — unamuno-restic — Foros, PostgreSQL
Kubernetes: K8up
En los clústeres Kubernetes, K8up sustituye a tolstoi. Es un operador de backup nativo de Kubernetes que descubre y respalda PVCs (Persistent Volume Claims) automáticamente. La programación se define como un recurso de Kubernetes:
apiVersion: k8up.io/v1
kind: Schedule
metadata:
name: backup-schedule
namespace: turing
spec:
backend:
s3:
endpoint: http://10.1.0.11:9000
bucket: homelab-restic
backup:
schedule: '30 2 * * *' # 2:30 AM a diario
prune:
schedule: '0 4 * * 0' # Domingos a las 4 AM
retention:
keepDaily: 7
keepWeekly: 4
keepMonthly: 6
check:
schedule: '0 5 * * 0' # Domingos a las 5 AMTres operaciones automatizadas: backup diario a las 2:30, que hace snapshot de todos los PVCs del namespace; prune semanal los domingos, que elimina snapshots antiguos según la política de retención; y check semanal tras el prune, que verifica la integridad del backup.
K8up se configura por namespace. En pizarro hay programaciones separadas para turing (Infisical), gauss (Harbor) y traefik. Cada uno respalda a su propio prefijo en el bucket.
Todo converge en spinoza
Los hosts Docker llegan a MinIO a través de la pasarela Tailscale cervantes. pizarro y cortes llegan por el túnel WireGuard (Roma a Atenas).
Domótica
El homelab no es solo servidores — también controla la casa. Dos servicios corren en la VM aristoteles de la red Atenas (marco-aurelio).
rothbard: Zigbee2MQTT
Zigbee2MQTT hace de puente entre dispositivos Zigbee del hogar (luces, sensores, interruptores) y MQTT, un protocolo de mensajería ligero. Corre junto a un broker MQTT Mosquitto:
services:
mqtt:
image: eclipse-mosquitto:2.0
ports:
- "1883:1883" # MQTT
- "9001:9001" # WebSocket
zigbee2mqtt:
image: koenkk/zigbee2mqtt:1.42.0
depends_on: [mqtt]
ports:
- 8082:8080 # Dashboard webEl coordinador Zigbee (un dongle USB) habla con los dispositivos directamente — sin ningún servicio en la nube de por medio. Zigbee2MQTT traduce los mensajes de los dispositivos a topics MQTT, dejándolos disponibles para cualquier sistema de automatización.
mises: Homebridge
Homebridge expone dispositivos no-HomeKit a Apple Home. Corre en modo de red host para gestionar el descubrimiento mDNS:
services:
homebridge:
image: homebridge/homebridge:2026-02-25
network_mode: host
environment:
- HOMEBRIDGE_INSECURE=1Homebridge recoge los dispositivos publicados en MQTT por Zigbee2MQTT y los expone como accesorios HomeKit. Un "Oye Siri, apaga las luces del salón" acaba provocando un mensaje MQTT que pasa de Homebridge a Zigbee2MQTT, de ahí a la radio Zigbee, y de la radio a la bombilla.
Ambos servicios son accesibles a través del reverse proxy Caddy local (colon) en rothbard.zetesis.localhost y mises.zetesis.localhost.
Recuperación ante desastres
Si todo se va al traste, hacen falta exactamente seis valores almacenados fuera de la infraestructura para reconstruir:
Secreto — Para qué
AGE-SECRET-KEY-... — Descifrar los ficheros SOPS del repo
K8UP_MINIO_USERNAME — Acceder al backup de Infisical en MinIO
K8UP_MINIO_PASSWORD — Acceder al backup de Infisical en MinIO
K8UP_RESTIC_PASSWORD — Descifrar el backup de Restic
ENCRYPTION_KEY — Arrancar Infisical desde el backup
AUTH_SECRET — Arrancar Infisical desde el backup
Con estos seis valores y un clon del repositorio Git, el camino de reconstrucción es:
- Descifrar los ficheros SOPS con la clave age
- Arrancar el primer nodo Talos con
talhelper genconfig+talosctl apply-config - Bootstrap de ArgoCD con
kubectl apply -f bootstrap.yaml - Restaurar Infisical desde el backup de MinIO usando las claves de Restic y cifrado
- Esperar — ESO conecta con Infisical, los secretos se propagan, los servicios arrancan
Una vez que Infisical está arriba, el resto es automático. ESO sincroniza secretos, ArgoCD despliega aplicaciones, ptolomeo tira en los hosts Docker. La reconstrucción completa está documentada en el FIRST_STEPS.md del repo.
Escenarios de pérdida
Qué se pierde — Impacto — Recuperación
Solo la clave age — No se pueden descifrar los ficheros SOPS — Obtenerla de Infisical
Solo Infisical — No se pueden desplegar servicios nuevos — Restaurar desde el backup en MinIO
Clave age + Infisical — Todo — Regenerar todos los secretos desde cero
Solo el repo Git — Se pierde la configuración — Re-clonar desde GitHub
Lo esencial: la clave age y las credenciales del backup de Infisical tienen que existir en algún sitio fuera del homelab — un gestor de contraseñas, un papel en una caja fuerte, un USB offline. Sin ellos, se empieza de cero.
GitGuardian: la red de seguridad
Como toda la infraestructura está en Git, hay una capa más: GitGuardian escanea cada push y cada pull request en busca de secretos commiteados por accidente:
# .github/workflows/main.yml
name: GitGuardian scan
on: [push, pull_request]
jobs:
scanning:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: GitGuardian/ggshield/actions/secret@v1.47.0
env:
GITGUARDIAN_API_KEY: ${{ secrets.GITGUARDIAN_API_KEY }}Si alguien commitea una contraseña en texto plano por error, GitGuardian la detecta antes de que llegue a la rama principal.
Lo que hemos construido
Hagamos zoom out. A lo largo de cuatro posts hemos cubierto:
- La visión general — Hardware, nombres, qué corre dónde
- Redes — WireGuard sitio a sitio, Tailscale en malla, Cloudflare en el borde, reverse proxy Caddy
- Kubernetes y GitOps — Talos Linux, App-of-Apps de ArgoCD, ApplicationSets, ptolomeo
- Seguridad y operaciones — Tres capas de secretos (Infisical, SOPS, ESO), backups con Restic, domótica
Todo funciona desde un único repositorio Git. Dos sistemas de CD (ArgoCD + ptolomeo) despliegan en Kubernetes y hosts Docker respectivamente. Los secretos fluyen desde Infisical por tres mecanismos distintos según el destino. Todo se respalda a MinIO en spinoza.
¿Está sobredimensionado para un homelab? Seguramente. Pero cada pieza existe porque me topé con un problema real — y la solución me enseñó algo que ahora uso a nivel profesional. Ese es el verdadero valor de un homelab: un terreno de juego donde lo que te juegas es poco, pero las lecciones son de verdad.
Si has llegado hasta aquí, espero que la serie te haya dado ideas para tu propio montaje. Empieza con poco. Rompe cosas. Arregla cosas. Así se aprende.
Anterior: Post 3 - Kubernetes y GitOps | Volver a Post 1 - La visión general