Skip to content

ADR-0007: Homelab Cohesion — GitOps, Secretos, HA y Control Unificado

Status: Accepted
Date: 2026-05-19
Deciders: Ramón Kamibayashi + Hermes Agent
Technical Story: Consolidación de infraestructura homelab tras 6 meses de crecimiento orgánico

Context

El homelab ha crecido de forma orgánica hasta convertirse en infraestructura crítica: cluster Proxmox 2 nodos (pmx-50 Ryzen 7, pmx-51 Celeron), NAS TerraMaster RAID5 22TB, VM 208 como hub Docker (40 hostnames), Cloudflare Tunnel único ingress externo, CF Access SSO perimetral, Caddy+Sablier lazy services, observabilidad centralizada (Loki/Prometheus/Grafana), alertas unificadas en n8n→Telegram, agente Hermes con MCP RAG. Lo que funciona: homelab-ctl.py como source-of-truth para Caddy+CF Tunnel, stacks Docker en Git, backups PBS+ZFS replication. Lo que NO está cohesionado: configs PVE sin IaC, secretos plaintext dispersos en 8 ubicaciones, VM 208 SPOF gigante (si cae = todo HTTP externo+observability fuera), SSO solo perímetro (LAN cada app tiene login propio), Hermes sin MCPs operacionales (no puede query Prometheus/PVE/PBS), ha-ml sin features adicionales disponibles (Pi-hole queries/cliente, Zeek tv-gw, Loki events), documentación en 4 repos distintos. Mac mini Carmelo se va en semanas → stack iarquitectos desaparece pero homelab debe seguir standalone.

Decision Drivers (prioridad descendente)

  1. SPOF VM 208 — RTO actual >1h si cae (rebuild manual), todo HTTP externo + observability dependiente.
  2. Secret hygiene — 15+ tokens plaintext en /root/.env, ~/.env, stacks/.env, passwords reutilizados alfagann/Alfagann007.
  3. Developer Experience — cambios PVE = SSH manual, configs no tracked, drift invisible, rollback imposible.
  4. MTTR incidents — sin runbooks estructurados, Hermes no puede diagnosticar (sin MCP Prometheus/PVE).
  5. Single login LAN — 8 apps internas requieren login independiente (Grafana, Pi-hole, Sonarr, Radarr, n8n, Stash, HA, PBS).
  6. Observability surface ha-ml — modelo arrival_predictor con ~0 utilidad (retention 10d, features pobres), señales ricas disponibles sin pipeline.
  7. Documentation sprawl — ADRs en Mac local, runbooks VM 208, REMOVED.md histórico, memoria Claude — sin portal único.

Considered Options

1. GitOps cluster-state

Opción Pros Contras Veredicto
Ansible-pull (cron) Simple, agentless SSH, templates Jinja2, idempotente, proxmoxer lib para qm/pct Drift detection = diffs Git, no events, módulo proxmox limitado (no cubre jobs.cfg/ACLs), state PVE distribuido en /etc/pve dificulta full IaC Elegido
Salt-minion Event bus, pillar secrets, state.apply push/pull Minion daemon overhead, master requerido, overkill 2 nodos ❌ Sobrecomplejo
Terraform proxmox provider Declarative HCL, plan/apply, state file No cubre configs runtime (Prometheus rules, Grafana dashboards), provider bpg/proxmox limita features ❌ Gaps coverage
Nada (status quo) Zero effort Drift garantizado, cambios no auditables ❌ Insostenible

Decisión F2: Ansible-pull en pmx-50/pmx-51 cada 15min pull de monxas/homelab-infra (repo nuevo). Cubre: /etc/pve/lxc/*.conf, /etc/pve/qemu-server/*.conf (templates no provisioning completo), scripts /usr/local/bin, configs Prometheus/Loki/Promtail, crons, systemd units. Explícitamente fuera de scope: provisioning VMs/LXCs desde cero (manual con docs), shared storage (no hay), plugins PVE (manual). Razón: 80/20 — tracking configs críticos vs provisioning completo que nunca usamos.

2. Secretos

Opción Pros Contras Veredicto
SOPS + age Archivos YAML encrypted en Git, age sin PKI (keypairs file-based), sops exec-env inline, Ansible community.sops lookup Rotación manual keypairs, age keys = archivos a distribuir (chicken-egg), sin UI humano-friendly Elegido
Infisical Cloud UI friendly, agent fetch, rotación automática, audit log SaaS externo (vendor lock), latency fetch inicial, free tier 5 usuarios ❌ Dependencia externa
Bitwarden Secrets Manager Self-hosted Vaultwarden ya existe, CLI bws API keys por máquina = secretos para secretos, Vaultwarden no tiene Secrets Manager (solo passwords), upstream Bitwarden Secrets = SaaS ❌ Vaultwarden ≠ Secrets Mgr
Vault HashiCorp Dynamic secrets, leases, HA JVM overhead, complejidad unsealing, overkill homelab ❌ Sobrecomplejo

Decisión F3: SOPS + age. Keypair age por nodo (pmx-50, pmx-51, VM 208, LXC 101/200/251) + keypair Ramón. Repo homelab-infra/secrets/ con archivos .sops.yaml reglas por path. Secretos template con {{ lookup('community.sops', 'secrets/env.sops.yaml') }} en Ansible. Migration plan: script migrate-secrets.sh recolecta de /root/.env, ~/stacks/*/.env, morning-report.env → consolida en secrets/{node,service}.sops.yaml → Ansible distribuye. Rotación post-migración de tokens críticos (CF, HA, n8n webhooks). Age keys distribuidas en provisioning manual inicial (acceptable — una vez por nodo).

3. SPOF VM 208 Caddy

Opción Pros Contras Veredicto
Caddy active-passive + keepalived VIP Simple (2 Caddy LXCs), keepalived VRRP track process, VIP .208, Sablier apunta a ambos Caddy, CF Tunnel → VIP Sablier dynamic_config apunta backends reales (.209/.210 no VIP) — gotcha config, healthcheck Caddy antes de promote, state Sablier (wake sessions) no compartido = descartar wake history Elegido
HAProxy frontal HA ACLs flexibles, stats dashboard, health checks avanzados Layer adicional (HAProxy→Caddy→backends), complejidad config ACLs, Sablier integration custom ❌ Complejidad innecesaria
Caddy cluster Raft Builtin HA (experimental), shared state Experimental, 3 nodos mínimo (no tenemos), overhead coordinación ❌ No production-ready
Aceptar SPOF + mejorar MTTR Playbook rebuild VM 208 < 10min, backups diarios SPOF sigue, RTO garantizado >5min, accesibilidad externa caída ❌ No resuelve driver #1

Decisión F4 (ajustada 2026-05-19 post-review): Caddy active-passive en LXCs propios. VM 208 conserva .208 (Docker hub backends, no toca). LXC 270 caddy-primary (.247 pmx-50), LXC 271 caddy-secondary (.248 pmx-51), keepalived VIP .250 (libre). homelab-ctl.py genera managed.caddy en ambos (rsync SSH). Sablier dynamic_config apunta a 192.168.0.XXX,192.168.0.XXX (no VIP). CF Tunnel reconfigura ingress: service: https://192.168.0.XXX:443 (VIP). Reverse proxy de Caddy apunta a 192.168.0.XXX:<port_docker> para todos los backends (sin cambios en VM 208). Tradeoff aceptado: state Sablier no compartido = si failover ocurre con services sleeping, secundario no conoce wake history → re-wake. Frecuencia failover esperada <1/mes → acceptable. Blast radius F4: durante migración inicial todo HTTP cae si CF Tunnel apunta a VIP antes de que Caddy LXC esté servando. Mitigación: deploy LXCs + verificar curl https://192.168.0.XXX/health OK → entonces cambiar CF Tunnel a .250. Rollback = revertir Tunnel ingress a 192.168.0.XXX:443 (VM 208 sigue corriendo Caddy en paralelo durante migración hasta validar).

4. SSO LAN

Opción Pros Contras Veredicto
PocketID forward-auth existing YA INSTALADO (no usado), forward-auth simple (/verify), OIDC provider, local users file Sin LDAP/SAML (no needed), apps sin forward-auth support = bypass (HA, Stash JWT), UI básico Elegido
Authelia Feature-rich (2FA, LDAP, ACLs granulares), activamente mantenido Config YAML compleja, Redis/Postgres required, overkill features ❌ Overengineering
Authentik UI hermoso, flows visuales, LDAP/SAML/OIDC Django+Postgres+Redis+workers stack pesado, admin overhead workflows ❌ Demasiado pesado
Keycloak Enterprise-grade, federación JVM heap >512MB, complejidad realms/clients, overkill ❌ Sobrecomplejo

Decisión F5: PocketID forward_auth en Caddy para 10 apps internas (Grafana, Pi-hole, Sonarr, Radarr, Lidarr, Prowlarr, qBittorrent, Dozzle, Healthchecks, n8n admin). Apps excluidas: HA (sensores real-time, ya tiene auth robusto), Stash (API JWT separate workflow), PBS (CLI integrations), n8n /webhook (bypass existing). Config: PocketID OIDC client registration en apps que soporten (Grafana), forward-auth header X-Forwarded-User para resto. Tradeoff: apps sin OIDC client (Pi-hole, qBittorrent) solo protección perimetral (no auto-login) = 1 click extra después de PocketID. Razón: simple > perfecto.

5. Hermes MCPs operacionales

Opción Pros Contras Veredicto
MCPs Python custom prometheus-query (PromQL), proxmox-status (pvesh, pvesm), pbs-status (API /api2/json/admin/datastore), homeassistant-supervisor (REST API sensors/history) Desarrollo custom (1-2d cada MCP), mantenimiento breaking changes APIs, auth tokens en cada MCP config Elegido
HA Voice Assist + Intents Builtin HA, voice commands, mobile integration Intents limitados (no PromQL freeform), latency TTS/STT, scope solo HA entities ❌ Limitado scope
Grafana annotations API Hermes escribe annotations en dashboards, humano visualiza Hermes no interpreta gráficos (no multimodal images en MCP), unidireccional ❌ No Q&A
Status quo morning-report.py Ya funciona, cron diario 07:00 Rígido (no Q&A ad-hoc), Hermes no puede explorar on-demand ❌ No conversacional

Decisión F6: 4 MCPs custom en homelab-infra/mcps/. Hermes config MCP apunta a endpoints locales. Prioridad: prometheus-query (80% use case diagnostics), proxmox-status (uptime/resources), homeassistant-supervisor (sensors/history last 24h), pbs-status (backup success/failures). morning-report.py se mantiene (proactive push) + Hermes puede Q&A ad-hoc. Tradeoff: mantenimiento 4 MCPs vs ganancia DX masiva (Hermes puede diagnosticar alerts sin humano SSH). Razón: MTTR -50% estimado.

6. ha-ml features adicionales

Opción Pros Contras Veredicto
Pipeline Pi-hole queries + Zeek conns Datos YA capturados (Pi-hole logs, Zeek tv-gw), features: queries_last_1h_by_device, tcp_conns_last_1h, unique_domains_last_1h Pipeline ETL (Pi-hole API polling → Loki label extraction → HA sensor), latency ingestion ~1min, features indirectas (queries ≠ presence) Elegido
Loki HA events → features state_change_count_1h, automation_triggers_1h directos de Loki Ya tenemos device_tracker (mejor señal), events = ruido (automations nocturnos) ❌ Señal-ruido bajo
Bluetooth RSSI beacons Presencia room-level, RSSI = distancia Hardware adicional (ESP32 beacons), BLE no todos dispositivos, privacy concerns tracking granular ❌ Requiere hardware
Nada (defer ha-ml) Zero effort Modelo sigue inútil (0 valor) ❌ Missed opportunity

Decisión F7: Pipeline ETL Pi-hole + Zeek. Script pihole-metrics-exporter.py (LXC .204 o VM 208) cada 5min: query Pi-hole API /admin/api.php?topClients&topItems → parse por device → pushgateway Prometheus. Loki query Zeek conn.logcount_over_time({job="zeek"} |= "192.168.0.X") → HA Prometheus sensor. Features ha-ml nuevos: pihole_queries_1h, zeek_conns_1h, unique_domains_1h. Retraining con 30d retention (cuando acumule). Tradeoff: pipeline ETL + 3 sensors adicionales vs ganancia predictive incierta (necesita validación post-retrain). Razón: coste bajo (1d impl), upside potencial medio.

7. Documentation portal

Opción Pros Contras Veredicto
MkDocs Material Markdown (existing ADRs/runbooks copy-paste), search builtin, lightweight (static site), CF Pages deploy desde Git Sin RBAC (no needed homelab), comentarios = external (giscus), updates = Git push Elegido
Backstage Service catalog, TechDocs, plugins ecosystem Node.js stack pesado, PostgreSQL required, overkill features (service ownership) ❌ Overengineering
Outline Wiki colaborativo, WYSIWYG, search potente Postgres+Redis+S3, real-time collab no needed (1 user), heavyweight ❌ Demasiado pesado
Docusaurus React, versioning, i18n Build time >MkDocs, complejidad React vs Markdown puro ❌ Complejidad innecesaria

Decisión F8: MkDocs Material en homelab-infra/docs/. Estructura: architecture/ (ADRs, diagramas), runbooks/ (troubleshooting), guides/ (setup services), reference/ (inventario, network map). Deploy: CF Pages docs.monxas.casa (público read-only, secretos redacted) + versión completa LAN-only docs.home.monxas.casa (sin redact). Plugin mkdocs-git-revision-date-localized (timestamps), mkdocs-mermaid2 (diagramas). Tradeoff: docs públicos = surface attack info (IPs, stack) → mitigación redacción IPs con 192.168.0.XXX, hostnames genéricos. Razón: documentación pública fuerza hygiene (no secretos hardcoded) + portfolio showcase.

Architecture Target

graph TB
    subgraph Internet
        CF[Cloudflare Tunnel
*.monxas.casa] end subgraph "Cluster Proxmox" subgraph "pmx-50 (Ryzen)" LXC270[LXC 270
caddy-primary
.209] VM208[VM 208
media-server
backends+obs] LXC101[LXC 101
hermesbot] VM171[VM 171
Home Assistant] LXC251[LXC 251
RAG] end subgraph "pmx-51 (Celeron)" LXC271[LXC 271
caddy-secondary
.210] LXC123[LXC 123
cloudflared] LXC200[LXC 200
n8n alerts] end VIP[VIP 192.168.0.XXX
keepalived] end subgraph "NAS TerraMaster" NAS[NFS exports
SMART metrics] end subgraph "External Clients" USER[Usuario
CF Access] end subgraph "GitOps" REPO[monxas/homelab-infra
Ansible + SOPS] end subgraph "Secrets" AGE[age keypairs
pmx-50/51/208/101] end USER -->|HTTPS| CF CF -->|Tunnel| LXC123 LXC123 -->|:443| VIP VIP -.->|VRRP Active| LXC270 VIP -.->|VRRP Standby| LXC271 LXC270 -->|proxy_pass| VM208 LXC271 -->|proxy_pass| VM208 VM208 -->|Loki/Prom| LXC270 VM208 -->|metrics| NAS LXC101 -->|MCP| VM208 LXC101 -->|MCP prometheus| VM208 LXC101 -->|MCP proxmox| LXC270 LXC101 -->|MCP rag| LXC251 LXC101 -->|MCP ha| VM171 VM208 -->|alerts webhook| LXC200 LXC200 -->|cooldown| LXC200 LXC200 -->|Telegram| Internet REPO -->|git pull 15min| LXC270 REPO -->|git pull 15min| LXC271 REPO -->|git pull 15min| VM208 AGE -->|decrypt| LXC270 AGE -->|decrypt| VM208 LXC270 -->|forward_auth| LXC270 style VIP fill:#ff6b6b style LXC270 fill:#51cf66 style LXC271 fill:#51cf66 style VM208 fill:#ffd43b style REPO fill:#74c0fc style AGE fill:#da77f2

Flujo Auth

sequenceDiagram
    participant User
    participant CF as Cloudflare
    participant Caddy as Caddy VIP
    participant PocketID
    participant App as Backend App

    User->>CF: GET /grafana
    CF->>CF: CF Access (external only)
    CF->>Caddy: Forward request
    Caddy->>PocketID: GET /verify (forward_auth)

    alt Session válida
        PocketID-->>Caddy: 200 + X-Forwarded-User
        Caddy->>App: Proxy + header
        App-->>User: Response
    else Sin session
        PocketID-->>Caddy: 302 /login
        Caddy-->>User: Redirect login
        User->>PocketID: POST /login (creds)
        PocketID-->>User: 302 + session cookie
        User->>Caddy: Retry con cookie
        Caddy->>App: Proxy autorizado
    end

Flujo Secretos

graph LR
    DEV[Developer
edit secrets/] -->|sops --age| SOPS[secrets/node.sops.yaml
encrypted Git] SOPS -->|git push| REPO[homelab-infra repo] REPO -->|ansible-pull 15min| NODE[pmx-50/51] NODE -->|age decrypt| PLAIN[/etc/env.d/secrets] PLAIN -->|systemd EnvironmentFile| SERVICE[servicios locales] REPO -->|ansible template| CADDY[Caddy config
CF_API_TOKEN] REPO -->|ansible template| PROM[Prometheus
scrape configs] style SOPS fill:#da77f2 style PLAIN fill:#ff6b6b

Flujo Alertas

graph TB
    GRAF[Grafana
alert rules] -->|webhook| N8N HC[Healthchecks
check failures] -->|webhook| N8N MORNING[morning-report.py
cron 07:00] -->|webhook| N8N WATCHDOG[systemd watchdogs] -->|webhook| N8N N8N[n8n LXC 200
beszel-alert-direct] N8N -->|cooldown 5min| N8N N8N -->|format message| TG[Telegram
@Veraclawd_bot] HERMES[Hermes LXC 101] -.->|lee alertas| N8N HERMES -.->|MCP prometheus-query| PROM[Prometheus VM 208] HERMES -.->|MCP proxmox-status| PVE[pvesh API] HERMES -.->|diagnostica| TG style N8N fill:#51cf66 style HERMES fill:#74c0fc

Plan en 8 Fases

F1: Backups completos (EN CURSO — baseline)

Objetivo: Garantizar rollback completo antes de cambios invasivos.

Estado actual: - PBS LXC 186 datastore main 57% usado, backups diarios LXCs críticos (101, 123, 200, 251). - ZFS replication pmx-50↔pmx-51 snapshots zfs-ha pool cada 15min. - HA backups a NFS terramaster_backup retention 7d. - VM 208 backup semanal (140GB disk, lento).

Tareas adicionales: 1. Backup manual completo /etc/pve/ ambos nodos → Git repo homelab-infra/bootstrap/pve-configs-baseline/. 2. Snapshot ZFS manual zfs-ha@pre-cohesion ambos nodos (no auto-delete). 3. Export configs críticos: Grafana dashboards (API /api/dashboards), Prometheus rules (/etc/prometheus/rules/*.yml), n8n workflows (UI export JSON). 4. Backup age keypairs Ramón existentes (si existen) → ~/.age-backup/. 5. Documentar inventory secretos (ver Apéndice).

Done criteria: - [ ] /etc/pve/ commit en homelab-infra/bootstrap/. - [ ] Snapshots ZFS @pre-cohesion verificados (zfs list -t snapshot). - [ ] Exports Grafana/Prometheus/n8n en homelab-infra/bootstrap/observability/. - [ ] Inventory secretos completo Apéndice.

Rollback: N/A (fase lectura).

Blast radius: Cero (solo lectura).

Duración estimada: 4h humano + Hermes.


F2: GitOps repo + Ansible-pull

Objetivo: Tracking configs PVE, scripts, systemd units, Prometheus/Loki rules en Git con aplicación automática cada 15min.

Archivos clave: - Repo nuevo monxas/homelab-infra (público, secretos en SOPS separado). - ansible/playbook.yml con roles: proxmox_node, caddy, observability, scripts. - ansible/inventory.yml: hosts pmx-50, pmx-51, vm-208. - Templates: templates/prometheus/rules/*.j2, templates/systemd/*.service.j2, templates/scripts/*.sh.j2. - Files tracked: /usr/local/bin/{smart-collect,pvesr-collect,morning-report}.sh, /etc/systemd/system/*.{service,timer}, /etc/prometheus/prometheus.yml, /etc/loki/loki.yml, /etc/promtail/*.yml. - Explícitamente NO tracked: /etc/pve/lxc/*.conf contenido completo (solo templates snippet cambios comunes), /etc/pve/qemu-server/*.conf (idem), /etc/pve/priv/ (certs), /etc/pve/nodes/*/config (hardware-specific).

Implementación: 1. Crear repo homelab-infra estructura: homelab-infra/ ├── ansible/ │ ├── playbook.yml │ ├── inventory.yml │ ├── roles/ │ │ ├── proxmox_node/ │ │ ├── caddy/ │ │ ├── observability/ │ │ └── scripts/ │ └── group_vars/all.yml ├── secrets/ (F3) ├── mcps/ (F6) ├── docs/ (F8) └── bootstrap/ └── pve-configs-baseline/ 2. Playbook inicial copia configs existentes (idempotente check). 3. Ansible-pull systemd timer en pmx-50/pmx-51: ini [Unit] Description=Ansible Pull homelab-infra [Service] ExecStart=/usr/bin/ansible-pull -U https://github.com/monxas/homelab-infra.git -i ansible/inventory.yml ansible/playbook.yml [Timer] OnBootSec=5min OnUnitActiveSec=15min 4. Primera ejecución manual con --check --diff validar cambios.

Done criteria: - [ ] Repo homelab-infra con playbook ejecutable. - [ ] Timer ansible-pull activo ambos nodos (systemctl status ansible-pull.timer). - [ ] 3 ejecuciones exitosas sin cambios (idempotencia). - [ ] Scripts /usr/local/bin tracked y deployados.

Rollback: 1. systemctl stop ansible-pull.timer. 2. Restaurar configs desde /etc/pve/ baseline F1.

Blast radius: Medio — configs PVE/systemd incorrectos pueden romper servicios. Mitigación: --check obligatorio primera vez, aplicar en pmx-51 (no-crítico) primero.

Duración estimada: 2d desarrollo playbook + 1d testing.


F3: SOPS + age secrets

Objetivo: Eliminar secretos plaintext, centralizar en secrets/*.sops.yaml encrypted, distribuir vía Ansible.

Archivos clave: - homelab-infra/secrets/: proxmox.sops.yaml, cloudflare.sops.yaml, observability.sops.yaml, agents.sops.yaml, media.sops.yaml. - .sops.yaml reglas: yaml creation_rules: - path_regex: secrets/proxmox\.sops\.yaml$ age: age1pmx50...,age1pmx51...,age1ramon... - path_regex: secrets/cloudflare\.sops\.yaml$ age: age1vm208...,age1lxc123...,age1ramon... - Age keypairs: /root/.age/key.txt en cada nodo (permisos 600), keypair Ramón en ~/.age/. - Ansible role secrets con lookup community.sops: yaml - name: Deploy Prometheus scrape config template: src: prometheus.yml.j2 dest: /etc/prometheus/prometheus.yml vars: cf_api_token: "{{ lookup('community.sops', 'secrets/cloudflare.sops.yaml')['cf_api_token'] }}"

Implementación: 1. Generar age keypairs: age-keygen -o pmx-50.key, idem pmx-51, vm-208, lxc-101, lxc-200, ramon. 2. Distribuir keypairs manualmente (SSH + scp) a /root/.age/key.txt en cada nodo. 3. Script scripts/migrate-secrets.sh: - Parse /root/.env, ~/stacks/*/.env, /root/.morning-report.env. - Consolida en secrets/*.sops.yaml (agrupado por service). - Encripta con sops --age <recipients> --encrypt. 4. Actualizar playbook Ansible templates para leer de SOPS. 5. Deploy vía ansible-pull. 6. Validar servicios arrancan (Prometheus scrape, Caddy CF API, n8n webhooks). 7. Post-validación: rotar tokens críticos (CF API token nuevo, HA long-lived token nuevo, n8n webhook regenerate). 8. Borrar plaintext .env files (commit removal en homelab-stacks).

Done criteria: - [ ] Age keypairs distribuidos 5 nodos. - [ ] 15+ secretos migrados a secrets/*.sops.yaml. - [ ] Ansible templates usan lookup('community.sops'). - [ ] Servicios validated (Prometheus targets UP, Caddy certs renew OK). - [ ] Tokens críticos rotados. - [ ] Plaintext .env eliminados (git log muestra removal).

Rollback: 1. Revertir playbook a templates con valores hardcoded temporales. 2. Restaurar .env files desde Git history.

Blast radius: Alto — secretos incorrectos = servicios no arrancan (Prometheus, Caddy, alertas). Mitigación: validar cada service post-deploy antes de borrar plaintext, mantener .env backup 7d.

Duración estimada: 3d migración + 1d validación + 1d rotación.


F4: Caddy HA (active-passive + keepalived)

Objetivo: Eliminar SPOF VM 208 para ingress HTTP, RTO <2min failover automático.

Archivos clave: - LXC 270 caddy-primary pmx-50 (Debian 13, 1GB RAM, 8GB disk, .247). - LXC 271 caddy-secondary pmx-51 (idem, .248). - VIP keepalived: .250 (CF Tunnel apunta aquí). - VM 208 mantiene .208 y sigue ejecutando todos los containers Docker (backends). - /etc/keepalived/keepalived.conf: conf vrrp_script check_caddy { script "/usr/local/bin/check-caddy.sh" interval 2 weight 2 } vrrp_instance VI_1 { state MASTER # pmx-50 interface eth0 virtual_router_id 51 priority 100 # pmx-51 = 90 advert_int 1 virtual_ipaddress { 192.168.0.XXX/24 } track_script { check_caddy } } - check-caddy.sh: bash #!/bin/bash curl -sf https://localhost:443/health > /dev/null 2>&1 exit $? - homelab-ctl.py ajuste: deploy managed.caddy a ambos LXCs (SSH multi-target). - CF Tunnel config sin cambio (apunta a https://192.168.0.XXX:443 VIP). - Sablier config cambio: dynamic_config backends apuntan a .209,.210 (no VIP): yaml caddy: blocking: timeout: 60s backends: - url: http://192.168.0.XXX:8080 - url: http://192.168.0.XXX:8080

Implementación: 1. Provisionar LXC 270/271 (template Debian 13). 2. Instalar Caddy + keepalived en ambos. 3. Deploy homelab-ctl.py versión multi-target (SCP a ambos). 4. Configurar keepalived (priority 100 pmx-50, 90 pmx-51). 5. Testear failover manual: systemctl stop caddy en primary → verificar VIP migra (ip a show eth0 en secondary). 6. Migrar CF Tunnel a apuntar VIP (ya apunta a .208 — no cambio needed, pero verificar). 7. Migrar containers observability (Loki, Prometheus, Grafana) a LXC separados (opcional F4b — defer si complejo): - Decisión: defer migración observability a F4b futura (no bloqueante). Caddy HA resuelve ingress SPOF, observability SPOF separado acceptable (impacto menor = no HTTP externo caído, solo dashboards internos).

Done criteria: - [ ] LXC 270/271 UP con Caddy serving. - [ ] VIP .208 activo en primary (ip a | grep 192.168.0.XXX). - [ ] Failover test: stop Caddy primary → VIP migra en <10s → externo accesible. - [ ] homelab-ctl.py deploy exitoso a ambos LXCs. - [ ] CF Tunnel routing OK a VIP. - [ ] 24h uptime sin flapping VIP.

Rollback: 1. Stop keepalived ambos LXCs. 2. Apuntar CF Tunnel a https://192.168.0.XXX:443 directo (primary). 3. Rollback homelab-ctl.py a single-target VM 208.

Blast radius: Crítico durante implementación — error config keepalived/VIP = todo HTTP externo cae. Mitigación: implementar fuera de horas (00:00-04:00), config VIP test en red aislada primero (VM scratch), mantener CF Tunnel apuntando a .209 directo hasta validar VIP stable 1h.

Duración estimada: 2d provisioning + config + 1d testing failover.


F5: PocketID forward-auth

Objetivo: Single login LAN para 10 apps internas, reducir surface attack (no password reuse), mejorar DX.

Pre-fase F5.0 (verificar PocketID existe): - La memoria dice "PocketID+CF Access reemplazaron Authelia" pero no hay LXC dedicado en inventario actual. - Pasos: (a) ssh media-208 'docker ps -a | grep -i pocket' (b) si no existe, deploy en VM 208 como container Docker (imagen ghcr.io/pocket-id/pocket-id) con label tunnel.hostname=auth.home.monxas.casa, datos en /home/monxas/appdata/pocketid/. - Si existe ya: identificar dónde corre + version actual.

Archivos clave: - PocketID en VM 208 (Docker container, NO LXC propio — sub-stack ~/stacks/auth/). - Caddy config block forward_auth: caddy grafana.home.monxas.casa { forward_auth pocketid:8080 { uri /verify copy_headers X-Forwarded-User } reverse_proxy grafana:3000 } - PocketID config.yml: yaml users: - username: ramon password_hash: <bcrypt> email: ramonkawa@gmail.com oidc_clients: - id: grafana secret: <generated> redirect_uris: - https://grafana.home.monxas.casa/login/generic_oauth - Apps OIDC config: Grafana auth.generic_oauth, n8n admin (no OIDC, solo forward-auth).

Implementación: 1. Identificar LXC PocketID existente (revisar inventario). 2. Config users.yml con usuario ramon (password nuevo fuerte). 3. Registrar OIDC clients: Grafana (soporta), resto forward-auth header. 4. Actualizar homelab-ctl.py templates Caddy con forward_auth block en apps target: - Grafana, Pi-hole (solo perimetral), Sonarr, Radarr, Lidarr, Prowlarr, qBittorrent, Dozzle, Healthchecks, n8n. 5. Excluir: HA (/api y /auth sin forward-auth, /.well-known bypass OAuth discovery), Stash (JWT workflow separate), PBS (CLI integrations), n8n /webhook/* (bypass existing). 6. Deploy Caddy config. 7. Test login flow cada app: acceso sin cookie → redirect PocketID login → post-login vuelta a app. 8. Configurar Grafana OIDC client (auto-login después de forward-auth).

Done criteria: - [ ] PocketID accesible https://auth.home.monxas.casa. - [ ] Login único protege 10 apps (test en navegador incognito). - [ ] Grafana OIDC auto-login funcional. - [ ] Bypass confirmado: HA /api, n8n /webhook, Stash sin forward-auth. - [ ] Session timeout 24h (config PocketID).

Rollback: 1. Comentar forward_auth blocks en Caddy config. 2. Deploy Caddy config rollback.

Blast radius: Medio — forward-auth mal configurado = apps internas inaccesibles. Mitigación: implementar app por app (Grafana first, menos crítico), mantener acceso SSH a Caddy para config fix, bypass IP LAN (remote_ip 192.168.0.XXX/24 sin forward-auth durante testing).

Duración estimada: 1.5d config + testing.


F6: Hermes MCPs operacionales

Objetivo: Hermes puede diagnosticar alerts vía Q&A ad-hoc (PromQL queries, PVE status, PBS backups, HA sensors).

Archivos clave: - homelab-infra/mcps/prometheus_query/: - server.py MCP con tool query_prometheus(promql: str, start: str, end: str). - Usa requests a http://192.168.0.XXX:9090/api/v1/query_range. - Auth: ninguno (Prometheus sin auth, LAN-only). - homelab-infra/mcps/proxmox_status/: - Tool get_cluster_status(), get_node_resources(node: str), get_vm_status(vmid: int). - Usa proxmoxer lib con token API (secreto en SOPS). - homelab-infra/mcps/pbs_status/: - Tool get_datastore_status(datastore: str), get_backup_tasks(since: str). - REST API /api2/json/admin/datastore/main/status. - homelab-infra/mcps/homeassistant_supervisor/: - Tool get_sensor_state(entity_id: str), get_history(entity_id: str, hours: int). - REST API /api/states/{entity_id}, /api/history/period. - Auth: long-lived token (SOPS). - Hermes config .clauderc MCP: json { "mcps": { "prometheus-query": { "command": "python", "args": ["/opt/mcps/prometheus_query/server.py"] }, "proxmox-status": { ... }, "pbs-status": { ... }, "homeassistant-supervisor": { ... } } }

Implementación: 1. Desarrollar 4 MCPs Python (usar MCP SDK anthropic-mcp). 2. Cada MCP: input validation, timeout 30s, error handling (API down = mensaje claro). 3. Deploy MCPs a /opt/mcps/ en LXC 101 Hermes. 4. Secretos (PVE token, PBS token, HA token) en secrets/agents.sops.yaml → Ansible template config MCP. 5. Actualizar Hermes .clauderc (si usa config file) o env vars MCP paths. 6. Test cada MCP standalone: python server.py + cliente test query. 7. Test Hermes conversacional: "¿Cuál es el uso de CPU de pmx-50 última hora?" → debe query Prometheus node_cpu_seconds_total.

Done criteria: - [ ] 4 MCPs deployados /opt/mcps/ LXC 101. - [ ] Hermes reconoce tools (ask "What tools do you have?"). - [ ] Test queries: - [ ] Prometheus: "Disk usage zfs-ha pmx-50" → query exitoso. - [ ] Proxmox: "Status VM 171" → respuesta con CPU/RAM. - [ ] PBS: "Last backup failures" → lista tasks. - [ ] HA: "Temperature sensor.salon" → state actual. - [ ] Hermes puede correlacionar alert Grafana con PromQL query (test simulado).

Rollback: 1. Quitar MCPs de Hermes config. 2. Borrar /opt/mcps/.

Blast radius: Bajo — MCPs broken = Hermes sin capacidad diagnostics, no afecta servicios productivos. Sin rollback urgente needed.

Duración estimada: 3d desarrollo + testing.


F7: ha-ml signals (Pi-hole + Zeek pipeline)

Objetivo: Features adicionales modelo arrival_predictor para mejorar accuracy (actual ~0% útil).

Archivos clave: - scripts/pihole-metrics-exporter.py (LXC nuevo o VM 208): ```python import requests import time from prometheus_client import CollectorRegistry, Gauge, push_to_gateway

# Pi-hole 6 API (NO Pi-hole 5 /admin/api.php — esa API ya no existe) PIHOLE_BASE = "http://192.168.0.XXX:8080/api" PIHOLE_PASS = os.environ["PIHOLE_PASSWORD"] # SOPS PUSHGATEWAY = "http://192.168.0.XXX:9091"

def get_sid(): r = requests.post(f"{PIHOLE_BASE}/auth", json={"password": PIHOLE_PASS}) return r.json()["session"]["sid"]

def collect(): sid = get_sid() r = requests.get(f"{PIHOLE_BASE}/stats/top_clients?count=100", headers={"X-FTL-SID": sid}) clients = r.json()["clients"] for ip, count in clients.items(): # Map IP to device (hardcoded o DNS reverse) device = ip_to_device(ip) gauge.labels(device=device).set(count) push_to_gateway(PUSHGATEWAY, job='pihole', registry=registry)

while True: collect() time.sleep(300) # 5min - Prometheus scrape pushgateway. - HA sensors Prometheus query:yaml sensor: - platform: prometheus name: pihole_queries_1h_phone_ramon query: 'pihole_queries_total{device="phone_ramon"}[1h]' - Loki query Zeek `conn.log` (LXC 110 tv-gw ya pushea logs):promql count_over_time({job="zeek", filename="/nsm/zeek/conn.log"} |= "192.168.0.X" [1h]) `` - HA sensor Loki (vía Prometheus Loki datasource). - ha-ml retrain: añadir featurespihole_queries_1h,zeek_conns_1h,unique_domains_1h` a dataset, retrain con 30d data (cuando acumule), evaluar accuracy improvement.

Implementación: 1. Deploy pihole-metrics-exporter.py como systemd service (LXC nuevo o VM 208). 2. Config Prometheus scrape pushgateway :9091. 3. Configurar 3 HA sensors Prometheus/Loki (config YAML). 4. Esperar 7d acumulación data (no retrain inmediato — fase "collect"). 5. Retrain ha-ml (manual o script): python train.py --features pihole_queries_1h,zeek_conns_1h,unique_domains_1h --retention 30d. 6. Evaluar accuracy (compare predictions vs ground truth 7d test set). 7. Si improvement >10% accuracy → deploy modelo nuevo, else defer features adicionales.

Done criteria: - [ ] pihole-metrics-exporter running 24h (systemctl status). - [ ] 3 HA sensors con data (/developer-tools/states muestra valores). - [ ] 7d data acumulado (Prometheus retention OK). - [ ] Retrain ejecutado (logs modelo training). - [ ] Accuracy documented: baseline vs new model (ADR update).

Rollback: 1. Stop pihole-metrics-exporter. 2. Comentar sensors HA (no break, solo sin data). 3. Modelo ha-ml sin cambio (sigue usando features antiguos).

Blast radius: Bajo — pipeline ETL broken = sensors sin data, modelo sigue funcionando con features antiguos. Sin impacto user-facing.

Duración estimada: 2d impl + 7d collect + 1d retrain/eval.


F8: MkDocs docs.monxas.casa

Objetivo: Portal documentación único (ADRs, runbooks, guides, reference), público + LAN-only versions.

Archivos clave: - homelab-infra/docs/: docs/ ├── index.md ├── architecture/ │ ├── adr/ │ │ ├── ADR-0001-tv-gw.md │ │ ├── ADR-0007-cohesion.md (este documento) │ ├── diagrams/ │ │ └── network-map.md (mermaid) ├── runbooks/ │ ├── caddy-failover.md │ ├── proxmox-kernel-jump.md │ ├── zfs-scrub.md ├── guides/ │ ├── adding-service.md │ ├── rotating-secrets.md ├── reference/ │ ├── inventory.md (LXCs/VMs table) │ ├── network.md (IPs, VLANs) │ └── ports.md (exposed ports) └── mkdocs.yml - mkdocs.yml: yaml site_name: Homelab Monxas theme: name: material features: - navigation.instant - search.suggest plugins: - search - git-revision-date-localized - mermaid2 markdown_extensions: - pymdownx.superfences: custom_fences: - name: mermaid class: mermaid - CF Pages proyecto docs-monxas (build command mkdocs build, output site/). - Redacción secretos: script scripts/redact-secrets.sh (replace IPs 192.168.0.X con 192.168.0.XXX, tokens con <REDACTED>). - Deploy dual: - Público docs.monxas.casa: CF Pages build con redacción, read-only. - LAN docs.home.monxas.casa: Caddy serve desde VM 208 /opt/docs/site/ sin redacción, build local.

Implementación: 1. Migrar ADRs existentes: lg-tv-monitoring/docs/adr/ADR-0001homelab-infra/docs/architecture/adr/. 2. Migrar runbooks: ~/stacks/docs/runbooks/*homelab-infra/docs/runbooks/. 3. Escribir inventory.md (tabla LXCs/VMs con IPs, specs, purpose). 4. Escribir network.md (diagrama mermaid topology). 5. Setup CF Pages proyecto (connect repo, build settings). 6. Script redact-secrets.sh pre-build hook CF Pages. 7. Config Caddy serve docs.home.monxas.casa (sin CF Access, LAN-only). 8. Test: acceso público docs.monxas.casa (IPs redacted), LAN docs.home.monxas.casa (IPs reales).

Done criteria: - [ ] docs.monxas.casa accesible público con ADRs+runbooks. - [ ] docs.home.monxas.casa accesible LAN con IPs reales. - [ ] Search funcional (test query "caddy failover"). - [ ] Mermaid diagramas render OK. - [ ] Git revision dates visibles (last updated). - [ ] 0 secretos hardcoded visibles en versión pública (audit manual).

Rollback: 1. Disable CF Pages deploy (pause). 2. Remove Caddy config docs.home.monxas.casa.

Blast radius: Bajo — docs broken = portal offline, no afecta servicios. Riesgo: leak secretos en versión pública → mitigación CI check grep -r "REDACTED" antes de deploy.

Duración estimada: 2d migración contenido + setup CF Pages.


Riesgos y Mitigaciones

# Riesgo Probabilidad Severidad Impacto Mitigación
R1 Failover Caddy flapping (keepalived oscila VIP por healthcheck flaky) Media (30%) Alta Todo HTTP externo intermitente Tuning keepalived: interval 5s (no 2s), weight 10 (más agresivo stop), logs Caddy healthcheck /health endpoint añadir timeout 30s, alertar Prometheus rate(keepalived_vrrp_transitions[5m]) > 2
R2 Secret leak en docs públicas (IP/token sin redactar) Baja (10%) Crítica Credenciales expuestas, acceso no autorizado CI pre-commit hook detect-secrets, redact-secrets.sh con tests (assert no 192.168.0.[0-9] en output), peer review ADRs antes de merge, rotación inmediata si leak detectado
R3 Ansible-pull divergencia (cambio manual PVE no tracked) Alta (60%) Media Drift configs, rollback imposible Alertar Prometheus diff ansible-pull --check exitcode != 0 → webhook n8n, runbook reconciliación manual, policy "cambios solo vía Git" (enforcement humano)
R4 Age keypair pérdida (nodo reinstall sin backup keypair) Baja (15%) Alta Secretos inaccesibles en nodo Backup age keypairs en PBS (/root/.age/ incluido en backup LXC), documentar recovery process en runbook rotating-secrets.md, keypair Ramón en 1Password (offsite)
R5 MCP Hermes broken API (Prometheus/PVE API cambio breaking) Media (25%) Baja Hermes sin diagnostics, no impacto servicios Tests integración MCPs en CI (mock APIs), versionado MCP (tag Git mcp-prometheus-v1.0), fallback Hermes a "API unavailable, check manually" sin crash

Mitigación general: Fases ordenadas por dependencia (backups first, Caddy HA antes SSO), implementar fuera de horas críticas (00:00-04:00), smoke tests post-deploy cada fase (checklist done criteria), alertas Prometheus/n8n para detectar regresiones 24h post-deploy.


Métricas de Éxito

Métrica Baseline Actual Target Post-F8 Método Medición
MTTR Caddy ingress 60min (rebuild VM 208 manual) <2min (failover automático keepalived) Prometheus alert probe_success{job="blackbox"} == 0 → recovery timestamp
Secretos plaintext 15 tokens en 8 ubicaciones 0 en repos Git (100% SOPS) git grep -E 'api_token|password' -- ':!*.sops.yaml' = 0 results
Logins LAN internos 8 logins independientes 1 login PocketID (10 apps protegidas) User test: acceso 10 apps con 1 session cookie
Config drift PVE 100% manual (no tracking) <5% drift (ansible-pull 15min) ansible-pull --check diff lines count, alert si >50 lines
Hermes diagnostics autonomy 0% (no MCPs operacionales) 80% queries respondibles (PromQL, PVE, PBS, HA) Test suite 20 queries comunes (ej. "CPU usage pmx-50"), success rate
Documentation findability Disperso (4 ubicaciones, grep manual) Portal único search <3s User test: query "caddy failover" encuentra runbook en <5s
RTO total homelab 4h (rebuild servicios críticos manual) <30min (Ansible redeploy automatizado) Disaster recovery drill: zfs destroy zfs-ha/... → playbook restore time
ADR implementation progress 0/8 fases (F1 en curso) 8/8 fases done criteria cumplidos Checklist este ADR, % complete trackear en docs.monxas.casa

Cadencia review: mensual sync Ramón+Hermes (día 1 cada mes), revisar métricas Prometheus dashboard homelab_cohesion.json (crear en F6), ajustar fases si blockers detectados.


Anti-Decisiones Explícitas

Lo que NO vamos a hacer y por qué:

  1. Backup offsite automático (Backblaze B2, Wasabi, etc.)
  2. Razón: Deferred por coste (€20/mes) + complejidad encryption at rest. PBS local + ZFS replication cubre disasters locales (hardware failure). Offsite solo crítico para fire/theft (riesgo bajo homelab no-business). Revisitar cuando budget permite.

  3. Kubernetes / K3s cluster

  4. Razón: Overkill 2 nodos (pmx-51 Celeron insuficiente worker). Overhead operacional (etcd, CNI, ingress controllers) vs ganancia (declarative workloads ya cubierto por Docker Compose + Ansible). Complejidad mantenimiento (cert rotation, upgrades) no justificada. Docker Compose + Sablier cubre lazy services simpler.

  5. Microservicios observability (Loki, Prometheus, Grafana HA)

  6. Razón: Separar de VM 208 a LXCs individuales = 3 LXCs adicionales + complejidad networking (Prometheus HA = Thanos sidecar, Loki = distributor/ingester/querier split). Ganancia uptime marginal (observability SPOF no afecta HTTP externo post-F4). Defer hasta RAM pmx-50 saturado (actual 28GB, 60% usado).

  7. Terraform para provisioning VMs/LXCs

  8. Razón: Provider bpg/proxmox no cubre runtime configs (scripts, systemd units, Prometheus rules). Provisioning VMs/LXCs = 1-2 veces/año (baja frecuencia), tiempo setup Terraform (state backend, modules) no ROI positivo. Ansible templates + manual qm create / pct create con docs en F8 más simple. Terraform útil si crecemos a >10 nodos (no planeado).

  9. Migración NixOS

  10. Razón: Learning curve NixOS (3-4 semanas) + rewrite configs existentes (configuration.nix no compatible Ansible playbooks). Ganancia (atomic rollbacks, reproducibility) ya parcialmente cubierto por Ansible idempotence + ZFS snapshots. Disrupción total cluster (reinstall ambos nodos) no justificada homelab productivo. NixOS atractivo greenfield, no brownfield.

  11. Hardware adicional (NAS enterprise, nodo pmx-52, UPS)

  12. Razón: Budget constraint (no CAPEX aprobado). TerraMaster F4-423 cubre storage (22TB suficiente 2 años proyectado). Nodo adicional no aporta quorum (ya tenemos QDevice pmx-51). UPS deseable (protection power outage) pero no crítico (RTO 30min acceptable post-F8, servicios non-transactional). Revisitar UPS cuando budget permite (~€300 CyberPower 1500VA).

  13. Service mesh (Istio, Linkerd, Consul Connect)

  14. Razón: mTLS between services overkill homelab (LAN trusted). Traffic management (canary, retries) no necesario (deploys = downtime acceptable <1min). Observability ya cubierto Prometheus+Loki. Overhead (sidecars, control plane) alto vs ganancia cero. Service mesh útil multi-tenant o zero-trust, no aplica homelab single-user.

  15. Secrets auto-rotation (Vault dynamic secrets)

  16. Razón: SOPS + manual rotation (1-2 veces/año) suficiente homelab. Vault dynamic secrets (AWS IAM, DB creds) no aplica (no cloud workloads, credentials static). Complejidad unsealing Vault (HA, storage backend) + lease management no justificada. Auto-rotation útil compliance (90d rotation policy) no requerido homelab.

  17. Authentik en lugar de PocketID

  18. Razón: Authentik stack pesado (Django + Postgres + Redis + workers = 4GB RAM mínimo). Features enterprise (RBAC flows, stages visuales, policies granulares) overkill 1 usuario. PocketID cubre SSO simple (<100MB RAM, config file YAML). Si crece a multi-usuario (familia, amigos) revisitar Authentik, no ahora.

  19. Nuevo nodo físico para Caddy HA

    • Razón: LXC 270/271 en pmx-50/51 existentes suficiente (aislación failure domains física). Raspberry Pi 4 como nodo Caddy standalone = hardware adicional (~€100) + complejidad deploy (ARM64 vs x86_64). Ganancia uptime marginal (failure domain pmx-50 completo = rare, UPS futuro mitiga). No CAPEX justified.

Revisit triggers: Si alguna anti-decisión se vuelve necesaria (ej. backup offsite si leak datos, K8s si >5 nodos, Authentik si >3 usuarios), documentar en ADR nueva con context cambio.


Apéndice: Inventario Secretos a Migrar

⚠️ TODOS los valores en este apéndice son <placeholder> ilustrativos. Los reales se leen del cluster en F3 durante la migración con un script collect-secrets-inventory.sh que NO los volcará en este documento. El ADR queda público; los valores reales nunca tocan este archivo.

Ubicación actual → secreto → destino SOPS:

Proxmox (pmx-50, pmx-51)

Ubicación Secreto Valor Ejemplo Destino SOPS
/root/.ssh/id_rsa pmx-50 SSH private key (NAS, PBS) -----BEGIN OPENSSH PRIVATE KEY----- secrets/proxmox.sops.yamlssh_private_key_pmx50
/root/.morning-report.env pmx-50 HOMEASSISTANT_TOKEN eyJhbGc... secrets/observability.sops.yamlha_token
/root/.morning-report.env pmx-50 TELEGRAM_BOT_TOKEN 7873264126:AAF... secrets/agents.sops.yamltelegram_bot_veraclawd
/root/.morning-report.env pmx-50 TELEGRAM_CHAT_ID 730947207 secrets/agents.sops.yamltelegram_chat_ramon
/etc/pve/priv/authorized_keys SSH pubkey Ramón ssh-ed25519 AAAA... No migrar (pubkey OK plaintext)
PVE web UI root@pam password alfagann Rotar → secrets/proxmox.sops.yamlproxmox_root_password

VM 208 media-server

Ubicación Secreto Valor Ejemplo Destino SOPS
~/stacks/.env CF_API_TOKEN v7rqh... secrets/cloudflare.sops.yamlcf_api_token
~/stacks/.env PUID / PGID 1000 No secreto (OK plaintext)
~/stacks/caddy/.env CLOUDFLARE_API_TOKEN (duplicate CF_API_TOKEN) Consolidar en secrets/cloudflare.sops.yaml
~/stacks/sonarr/.env SONARR_API_KEY 3f8e... secrets/media.sops.yamlsonarr_api_key
~/stacks/radarr/.env RADARR_API_KEY a72c... secrets/media.sops.yamlradarr_api_key
~/stacks/prowlarr/.env PROWLARR_API_KEY 9d1a... secrets/media.sops.yamlprowlarr_api_key
~/stacks/qbittorrent/.env WEBUI_PASSWORD alfagann Rotar → secrets/media.sops.yamlqbittorrent_password
~/stacks/grafana/.env GF_SECURITY_ADMIN_PASSWORD admin (default) Rotar → secrets/observability.sops.yamlgrafana_admin_password
~/stacks/loki/.env (no auth) - N/A
~/stacks/prometheus/.env (no auth) - N/A

LXC 123 cloudflared

Ubicación Secreto Valor Ejemplo Destino SOPS
/root/.cloudflared/cert.pem Cloudflare tunnel cert -----BEGIN CERTIFICATE----- secrets/cloudflare.sops.yamltunnel_cert_pem
/root/.cloudflared/config.yml tunnel: <UUID> a3f8e7d2-... No secreto (tunnel ID público OK)
/root/.cloudflared/credentials.json TunnelSecret ABCdef123... secrets/cloudflare.sops.yamltunnel_secret

LXC 101 hermesbot

Ubicación Secreto Valor Ejemplo Destino SOPS
/opt/hermes/.env ANTHROPIC_API_KEY sk-ant-api03-... secrets/agents.sops.yamlanthropic_api_key_hermes
/opt/hermes/.env TELEGRAM_BOT_TOKEN 7454903653:AAE... secrets/agents.sops.yamltelegram_bot_verahermes
/opt/hermes/.env TELEGRAM_CHAT_ID 730947207 Duplicate (ya en morning-report)

LXC 200 n8n

Ubicación Secreto Valor Ejemplo Destino SOPS
/opt/n8n/.env N8N_ENCRYPTION_KEY f8e3d9a1... secrets/observability.sops.yamln8n_encryption_key
/opt/n8n/.env N8N_USER_MANAGEMENT_JWT_SECRET a7c2e8f... secrets/observability.sops.yamln8n_jwt_secret
Workflow credentials Telegram bot (Veraclawd) (stored encrypted n8n DB) No migrar (n8n interno)
Workflow credentials Grafana webhook URL http://192.168.0.XXX:5678/webhook/beszel-alert-direct No secreto (LAN-only)

LXC 251 rag

Ubicación Secreto Valor Ejemplo Destino SOPS
/opt/rag/.env ANTHROPIC_API_KEY (shared con Hermes?) Validar si duplicate o key separado

VM 171 Home Assistant

Ubicación Secreto Valor Ejemplo Destino SOPS
/config/secrets.yaml http_password alfagann Rotar → secrets/homeassistant.sops.yamlhttp_password
/config/.storage/auth Long-lived tokens (JSON encrypted) No migrar (HA interno)
HA UI integrations Pi-hole password alfagann Rotar post-F5 (PocketID SSO)

NAS TerraMaster F4-423

Ubicación Secreto Valor Ejemplo Destino SOPS
SSH login User admin password Alfagann007 Rotar → secrets/nas.sops.yamlnas_admin_password
TOS web UI User admin password (same) Duplicate
NFS exports (no auth, IP allowlist) - N/A

Mac mini Carmelo (EXTERNO — no migrar, documentar separación)

Ubicación Secreto Valor Ejemplo Notas
/tmp/gog_keyring_pw gog_keyring_password (temp file) Se va con Mac, no migrar
~/.zshrc export GOG_KEYRING_PW=... (plaintext) Se va con Mac
openclaw-vm OAuth Codex (shared con Hermes) (gotcha) Quota compartida = problema, resolver splitting accounts

Windows Desktop AI Tagger (.115)

Ubicación Secreto Valor Ejemplo Destino SOPS
WSL /opt/nsfw_ai_model_server/.env Patreon license keys amber_salad: XYZ... secrets/media.sops.yamlpatreon_licenses (si self-host futuro)
Stash plugin config API key Stash (stored Stash DB) No migrar (Stash interno)

Total secretos a migrar: 27 (excluye duplicates, internals apps, pubkeys).

Rotación prioritaria post-migración: 1. CF_API_TOKEN (scope: Zone:Read, DNS:Edit, Tunnel:Edit). 2. HOMEASSISTANT_TOKEN (regenerate long-lived token). 3. PROXMOX_ROOT_PASSWORD (cambiar desde web UI). 4. Passwords reutilizados alfagann → único por servicio generado 1Password. 5. ANTHROPIC_API_KEY Hermes (si shared con openclaw-vm, crear key separado).

Validación post-rotación: smoke tests cada servicio (Caddy certs renew, Prometheus scrape targets UP, morning-report ejecuta sin error, n8n workflows test).


Conclusión

Este ADR establece el contrato de implementación para los próximos 3-6 meses. Prioridad: F1 (backups) → F2 (GitOps) → F3 (secretos) son foundation, F4 (Caddy HA) crítico uptime, F5-F7 DX improvements, F8 documentation. Dueño implementación: Ramón (decisiones, validación) + Hermes (desarrollo playbooks, MCPs, testing). Review cadence: mensual día 1, ajustar fases si blockers. Success: métricas target alcanzadas F8, homelab mantenible 1 humano + 1 agente, RTO <30min, 0 secretos plaintext, single login LAN.

Next steps immediate: 1. Commit este ADR a homelab-infra/docs/architecture/adr/ADR-0007-homelab-cohesion.md. 2. Completar F1 backups baseline (1d). 3. Setup repo monxas/homelab-infra estructura (3h). 4. Kick-off F2 Ansible playbook desarrollo (2d).

Philosophy reminder: Start simple, scale smart. Cada fase debe ser smallest viable increment. Si una fase se vuelve compleja (>1 semana impl), split en sub-fases. Hermes puede hacer desarrollo (playbooks, MCPs, scripts), Ramón valida y decide tradeoffs. Pair programming humano+agente = fuerza multiplicadora.


Status: Accepted
Signed-off: Ramón Kamibayashi, 2026-05-19
Agent: Hermes, assisting implementation phases F2-F8


Adenda 2026-05-20 — Gotchas encontradas durante implementación

F4.e cutover (Caddy HA)

  • tls_server_name {host} obligatorio en transport http del reverse_proxy LXC→VM208. Sin esto, Caddy envía SNI con la IP destino y VM 208 Caddy con on_demand_tls rechaza con TLS internal_error.
  • weight -20 (no positivo) en keepalived VRRP script. Positive weight da empate cuando primary falla; negative resta priority al fallar el check.
  • caddy reload NO aplica managed.caddy cuando Sablier plugin cargado. Requiere docker restart caddy.
  • homelab-ctl.py HOST: hacía falta separar en CADDY_BACKEND_HOST (.208) y TUNNEL_ORIGIN_HOST (.250).
  • CF Tunnel + DNS: homelab-ctl.py sync añade ingress pero NO crea DNS records. Hay que crear wildcard *.monxas.casa para que nuevos hostnames "just work".
  • Pi-hole local override: /etc/dnsmasq.d/05-local-homelab.conf tiene address=/monxas.casa/192.168.0.XXX. Clientes LAN bypass CF y van directos a VM 208. Si quieres failover LAN al VIP, cambiar a .250.

F5 PocketID OIDC client creation

  • Double shell expansion en bcrypt hash: al pasar $HASH vía SSH, el $2a$10 del bcrypt prefix se interpreta como params posicionales en el remote shell, dejando hash mutilado (a0$.zes... en vez de $2a$10$.zes...).
  • Fix: generar bcrypt LOCAL en VM 208 con python3 crypt.crypt(secret, crypt.mksalt(crypt.METHOD_BLOWFISH)) produce $2b$12$... que Go bcrypt acepta como compatible.
  • PocketID v2 ya tenía 7 OIDC clients pre-configurados (Cloudflare Access, Vaultwarden, Karakeep, Audiobookshelf, Immich, Paperless, Jellyseerr). Grafana es el 8º.
  • OAuth flow validado end-to-end: Grafana login → PocketID passkey → callback → token exchange → userinfo → session OK.

Chrome DNS cache

  • ERR_NAME_NOT_RESOLVED para hostnames recién creados puede sobrevivir 30+ min en cache de macOS/Chrome.
  • Fix: sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder + chrome://net-internals/#hsts Delete domain + reload incognito.