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)¶
- SPOF VM 208 — RTO actual >1h si cae (rebuild manual), todo HTTP externo + observability dependiente.
- Secret hygiene — 15+ tokens plaintext en
/root/.env,~/.env,stacks/.env, passwords reutilizadosalfagann/Alfagann007. - Developer Experience — cambios PVE = SSH manual, configs no tracked, drift invisible, rollback imposible.
- MTTR incidents — sin runbooks estructurados, Hermes no puede diagnosticar (sin MCP Prometheus/PVE).
- Single login LAN — 8 apps internas requieren login independiente (Grafana, Pi-hole, Sonarr, Radarr, n8n, Stash, HA, PBS).
- Observability surface ha-ml — modelo arrival_predictor con ~0 utilidad (retention 10d, features pobres), señales ricas disponibles sin pipeline.
- 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.log → count_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-0001 → homelab-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é:
- Backup offsite automático (Backblaze B2, Wasabi, etc.)
-
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.
-
Kubernetes / K3s cluster
-
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.
-
Microservicios observability (Loki, Prometheus, Grafana HA)
-
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).
-
Terraform para provisioning VMs/LXCs
-
Razón: Provider
bpg/proxmoxno 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 + manualqm create/pct createcon docs en F8 más simple. Terraform útil si crecemos a >10 nodos (no planeado). -
Migración NixOS
-
Razón: Learning curve NixOS (3-4 semanas) + rewrite configs existentes (
configuration.nixno 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. -
Hardware adicional (NAS enterprise, nodo pmx-52, UPS)
-
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).
-
Service mesh (Istio, Linkerd, Consul Connect)
-
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.
-
Secrets auto-rotation (Vault dynamic secrets)
-
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.
-
Authentik en lugar de PocketID
-
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.
-
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 scriptcollect-secrets-inventory.shque 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.yaml → ssh_private_key_pmx50 |
/root/.morning-report.env pmx-50 |
HOMEASSISTANT_TOKEN |
eyJhbGc... |
secrets/observability.sops.yaml → ha_token |
/root/.morning-report.env pmx-50 |
TELEGRAM_BOT_TOKEN |
7873264126:AAF... |
secrets/agents.sops.yaml → telegram_bot_veraclawd |
/root/.morning-report.env pmx-50 |
TELEGRAM_CHAT_ID |
730947207 |
secrets/agents.sops.yaml → telegram_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.yaml → proxmox_root_password |
VM 208 media-server¶
| Ubicación | Secreto | Valor Ejemplo | Destino SOPS |
|---|---|---|---|
~/stacks/.env |
CF_API_TOKEN |
v7rqh... |
secrets/cloudflare.sops.yaml → cf_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.yaml → sonarr_api_key |
~/stacks/radarr/.env |
RADARR_API_KEY |
a72c... |
secrets/media.sops.yaml → radarr_api_key |
~/stacks/prowlarr/.env |
PROWLARR_API_KEY |
9d1a... |
secrets/media.sops.yaml → prowlarr_api_key |
~/stacks/qbittorrent/.env |
WEBUI_PASSWORD |
alfagann |
Rotar → secrets/media.sops.yaml → qbittorrent_password |
~/stacks/grafana/.env |
GF_SECURITY_ADMIN_PASSWORD |
admin (default) |
Rotar → secrets/observability.sops.yaml → grafana_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.yaml → tunnel_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.yaml → tunnel_secret |
LXC 101 hermesbot¶
| Ubicación | Secreto | Valor Ejemplo | Destino SOPS |
|---|---|---|---|
/opt/hermes/.env |
ANTHROPIC_API_KEY |
sk-ant-api03-... |
secrets/agents.sops.yaml → anthropic_api_key_hermes |
/opt/hermes/.env |
TELEGRAM_BOT_TOKEN |
7454903653:AAE... |
secrets/agents.sops.yaml → telegram_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.yaml → n8n_encryption_key |
/opt/n8n/.env |
N8N_USER_MANAGEMENT_JWT_SECRET |
a7c2e8f... |
secrets/observability.sops.yaml → n8n_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.yaml → http_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.yaml → nas_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.yaml → patreon_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 conon_demand_tlsrechaza 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 reloadNO aplica managed.caddy cuando Sablier plugin cargado. Requieredocker restart caddy.- homelab-ctl.py HOST: hacía falta separar en
CADDY_BACKEND_HOST(.208) yTUNNEL_ORIGIN_HOST(.250). - CF Tunnel + DNS:
homelab-ctl.py syncañade ingress pero NO crea DNS records. Hay que crear wildcard*.monxas.casapara que nuevos hostnames "just work". - Pi-hole local override:
/etc/dnsmasq.d/05-local-homelab.conftieneaddress=/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
$HASHvía SSH, el$2a$10del 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/#hstsDelete domain + reload incognito.