[
 {
  "id": 11,
  "title": "Start Here \u2014 el plano en una p\u00e1gina",
  "slug": "start-here-el-plano-en-una-pagina",
  "book_id": 1,
  "chapter_id": null,
  "description": null,
  "created_at": "2026-09-06T13:38:34.000000Z",
  "updated_at": "2026-09-06T13:38:34.000000Z",
  "markdown": "# Wetopi Knowledge & Documentation Plane\n\n## El principio en una l\u00ednea\n\n**Raw \u2192 S3 \u00b7 Knowledge \u2192 BookStack \u00b7 Contract \u2192 Git \u00b7 Wisdom \u2192 Hindsight**\n\n## Los 4 almacenes\n\n| Almac\u00e9n | Qu\u00e9 vive ah\u00ed | D\u00f3nde |\n|---|---|---|\n| **Git** (docs repo, ai-hub) | Contratos: ADRs, fichas SVC, runbooks de plataforma, specs de objetivos. SSOT de decisiones | wetopi-platform/docs, ai-hub |\n| **BookStack** (este sitio) | Conocimiento vivo: borradores, evidencia, how-to del d\u00eda a d\u00eda | book.wetopi.net |\n| **Artifact Store** (S3/Ceph RGW) | Evidencia cruda: dumps, logs, outputs | Prerequisito: RGW desplegado (fase 2 del roadmap) |\n| **Hindsight** (memory.wetopi.net) | Wisdom: hechos con timestamp, aprendizajes | banks personales (nativo) + corporate (plugin) |\n\n## Reglas de escritura (P1-P5)\n\n- **P1 \u2014 Ficha = contrato.** Una ficha por servicio/\u00e1rea responde: \u00bfqu\u00e9 es? / \u00bfc\u00f3mo se opera? / \u00bfc\u00f3mo se rompe y se arregla? Front-matter: `id, title, type, status, owner, summary, scope, tags, related, last_verified`.\n- **P2 \u2014 El runbook vive en la ficha** (d\u00eda a d\u00eda) o en `operations/runbooks/` (plataforma). Nunca dos copias.\n- **P3 \u2014 Doc rot = incidente.** `last_verified` >30 d\u00edas tras cambio relevante \u2192 marcar `stale` en el \u00edndice.\n- **P4 \u2014 DocOps duro.** Lo que toca el harness vive en ai-hub o docs. **No-documented = no-existent.**\n- **P5 \u2014 Objetivo muerto \u2192 ficha muerta.** Cuando un OBJ termina queda solo la decisi\u00f3n (ADR).\n\n## El circuito de los agentes (L0 \u2192 L3)\n\n```\nL0  Ef\u00edmero (/tmp, worktrees)      \u2192 hoy, muere hoy\n        \u2193 \u00bfimport\u00f3? \u2192 promociona\nL1  Workbench (BookStack, book 90) \u2192 borradores, evidencia, DRAFTs\n        \u2193 \u00bfmadur\u00f3? \u2192 promociona\nL2  Formal (docs repo / ai-hub)    \u2192 PR + review\nL3  Memoria (Hindsight + plugin)   \u2192 hechos, no documentos\n```\n\n**La memoria apunta, no almacena:** el bank guarda el hecho esencial + timestamp + puntero (URL). Nunca el documento entero.\n\n## Ritual de promoci\u00f3n (\u00fanico obligatorio)\n\n1. Draft en Workbench con `#draft`\n2. Estabiliza \u2192 `#ready` + PR al destino formal (Git) si procede\n3. Merge \u2192 la p\u00e1gina pasa a `PROMOVED \u2192 <link>` + `#superseded` \u2014 la copia muere al promocionar\n\n## Navegaci\u00f3n de este sitio\n\nCada book cubre un dominio (ver \u00edndice lateral). Los ADRs **viven en Git**\n(wetopi-platform/docs); el book 70 es solo mirror de lectura. Los IDs\ncan\u00f3nicos (OBJ/SVC/ADR/RUN/ART/REC) est\u00e1n registrados en\n`docs/reference/id-registry.md`.\n\n**Skill de referencia en la flota:** `wetopi-docops` (instalada en\ncarlos/joan/josep). C\u00e1rgala antes de crear, mover o promocionar documentos.",
  "html": "<h1 id=\"bkmrk-wetopi-knowledge-%26-d\">Wetopi Knowledge &amp; Documentation Plane</h1>\n<h2 id=\"bkmrk-el-principio-en-una-\">El principio en una l\u00ednea</h2>\n<p id=\"bkmrk-raw-%E2%86%92-s3-%C2%B7-knowledge\"><strong>Raw \u2192 S3 \u00b7 Knowledge \u2192 BookStack \u00b7 Contract \u2192 Git \u00b7 Wisdom \u2192 Hindsight</strong></p>\n<h2 id=\"bkmrk-los-4-almacenes\">Los 4 almacenes</h2>\n<table id=\"bkmrk-almac%C3%A9n-qu%C3%A9-vive-ah%C3%AD\">\n<thead>\n<tr>\n<th>Almac\u00e9n</th>\n<th>Qu\u00e9 vive ah\u00ed</th>\n<th>D\u00f3nde</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Git</strong> (docs repo, ai-hub)</td>\n<td>Contratos: ADRs, fichas SVC, runbooks de plataforma, specs de objetivos. SSOT de decisiones</td>\n<td>wetopi-platform/docs, ai-hub</td>\n</tr>\n<tr>\n<td><strong>BookStack</strong> (este sitio)</td>\n<td>Conocimiento vivo: borradores, evidencia, how-to del d\u00eda a d\u00eda</td>\n<td>book.wetopi.net</td>\n</tr>\n<tr>\n<td><strong>Artifact Store</strong> (S3/Ceph RGW)</td>\n<td>Evidencia cruda: dumps, logs, outputs</td>\n<td>Prerequisito: RGW desplegado (fase 2 del roadmap)</td>\n</tr>\n<tr>\n<td><strong>Hindsight</strong> (memory.wetopi.net)</td>\n<td>Wisdom: hechos con timestamp, aprendizajes</td>\n<td>banks personales (nativo) + corporate (plugin)</td>\n</tr>\n</tbody>\n</table>\n<h2 id=\"bkmrk-reglas-de-escritura-\">Reglas de escritura (P1-P5)</h2>\n<ul id=\"bkmrk-p1-%E2%80%94-ficha-%3D-contrat\">\n<li><strong>P1 \u2014 Ficha = contrato.</strong> Una ficha por servicio/\u00e1rea responde: \u00bfqu\u00e9 es? / \u00bfc\u00f3mo se opera? / \u00bfc\u00f3mo se rompe y se arregla? Front-matter: <code>id, title, type, status, owner, summary, scope, tags, related, last_verified</code>.</li>\n<li><strong>P2 \u2014 El runbook vive en la ficha</strong> (d\u00eda a d\u00eda) o en <code>operations/runbooks/</code> (plataforma). Nunca dos copias.</li>\n<li><strong>P3 \u2014 Doc rot = incidente.</strong> <code>last_verified</code> &gt;30 d\u00edas tras cambio relevante \u2192 marcar <code>stale</code> en el \u00edndice.</li>\n<li><strong>P4 \u2014 DocOps duro.</strong> Lo que toca el harness vive en ai-hub o docs. <strong>No-documented = no-existent.</strong></li>\n<li><strong>P5 \u2014 Objetivo muerto \u2192 ficha muerta.</strong> Cuando un OBJ termina queda solo la decisi\u00f3n (ADR).</li>\n</ul>\n<h2 id=\"bkmrk-el-circuito-de-los-a\">El circuito de los agentes (L0 \u2192 L3)</h2>\n<pre id=\"bkmrk-l0-ef%C3%ADmero-%28%2Ftmp%2C-wo\"><code>L0  Ef\u00edmero (/tmp, worktrees)      \u2192 hoy, muere hoy\n        \u2193 \u00bfimport\u00f3? \u2192 promociona\nL1  Workbench (BookStack, book 90) \u2192 borradores, evidencia, DRAFTs\n        \u2193 \u00bfmadur\u00f3? \u2192 promociona\nL2  Formal (docs repo / ai-hub)    \u2192 PR + review\nL3  Memoria (Hindsight + plugin)   \u2192 hechos, no documentos\n</code></pre>\n<p id=\"bkmrk-la-memoria-apunta%2C-n\"><strong>La memoria apunta, no almacena:</strong> el bank guarda el hecho esencial + timestamp + puntero (URL). Nunca el documento entero.</p>\n<h2 id=\"bkmrk-ritual-de-promoci%C3%B3n-\">Ritual de promoci\u00f3n (\u00fanico obligatorio)</h2>\n<ol id=\"bkmrk-draft-en-workbench-c\">\n<li>Draft en Workbench con <code>#draft</code></li>\n<li>Estabiliza \u2192 <code>#ready</code> + PR al destino formal (Git) si procede</li>\n<li>Merge \u2192 la p\u00e1gina pasa a <code>PROMOVED \u2192 &lt;link&gt;</code> + <code>#superseded</code> \u2014 la copia muere al promocionar</li>\n</ol>\n<h2 id=\"bkmrk-navegaci%C3%B3n-de-este-s\">Navegaci\u00f3n de este sitio</h2>\n<p id=\"bkmrk-cada-book-cubre-un-d\">Cada book cubre un dominio (ver \u00edndice lateral). Los ADRs <strong>viven en Git</strong>\n(wetopi-platform/docs); el book 70 es solo mirror de lectura. Los IDs\ncan\u00f3nicos (OBJ/SVC/ADR/RUN/ART/REC) est\u00e1n registrados en\n<code>docs/reference/id-registry.md</code>.</p>\n<p id=\"bkmrk-skill-de-referencia-\"><strong>Skill de referencia en la flota:</strong> <code>wetopi-docops</code> (instalada en\ncarlos/joan/josep). C\u00e1rgala antes de crear, mover o promocionar documentos.</p>\n"
 },
 {
  "id": 33,
  "title": "harness v31 governance",
  "slug": "harness-v31-governance",
  "book_id": 7,
  "chapter_id": 25,
  "description": null,
  "created_at": "2026-09-06T13:38:52.000000Z",
  "updated_at": "2026-09-06T13:38:52.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (agents/harness-v31-governance.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n Harness V3.1 governance \u2014 roles, gates and fail-closed state machine\n\nThis document is the operating model of the Harness V3.1 pilot. The\ndecisions live in [ADR-0008](../decisions/ADR-0008-harness-v31-objective-governance.md)\n(proposed); the pilot itself is\n[OBJ-HARNESS-V31-001](../objectives/active/OBJ-HARNESS-V31-001.md).\nNothing here is accepted governance until ADR-0008 is accepted after the\npilot verdict.\n\n## What this covers\n\nRoles and their boundaries, the review gates, degraded routing, and the\nfail-closed state machine that governs every Objective under V3.1.\n\n## Roles\n\n| Role | Actor | Delivers | Hard boundary |\n|---|---|---|---|\n| THINK (Architect) | architect-designated profile or the operator as architect | Objective Draft: WHAT / WHY / DONE (outcome, context, ACs, verification contract, constraints, boundaries, stop conditions) | **No Git write.** Never pushes branches/commits. Never claims cards. Never judges its own draft. |\n| OWN (Lead) | `wetopi-lead` | Objective acceptance, card decomposition, dispatch, Execution Ledger (single writer), documentation PRs | Does not author WHAT/WHY/DONE it was not handed; does not judge deliveries. |\n| DO (workers) | `wetopi-build`, `wetopi-senior`, `wetopi-ops` | Card implementation with self-validation and evidence | No apply/destroy of real infrastructure from build cards; no self-approval; stays inside objective boundaries and stop conditions. |\n| JUDGE (review) | `wetopi-approver` or any reviewer that did NOT implement | Verdicts against the verification contract: APPROVE / REQUEST_CHANGES / REJECT | Does not implement or patch the delivery; does not merge without operator authority. |\n\n## The Architect capability boundary\n\nThe Architect defines **WHAT, WHY and DONE \u2014 nothing else**:\n\n- **Writes:** outcome statement, context, acceptance criteria, the\n  verification contract (how DONE is proven), constraints, boundaries,\n  out-of-scope, stop conditions, review boundaries, promotion matrix.\n- **Hands off:** the draft to the OWN (Lead) role as an Objective Draft.\n  Entering Git is the Lead's documentation task, via PR \u2014 the Architect\n  never pushes.\n- **May:** answer questions, refine the draft on request, flag drift\n  between execution and intent.\n- **May not:** hold Git credentials for the docs repo, execute cards,\n  review its own draft, or expand the objective mid-flight (a scope change\n  is a new draft, re-reviewed).\n\n## Gates (conditional review, fail-closed)\n\n1. **Objective gate** \u2014 the Objective Draft must be reviewed (operator or\n   JUDGE) before the Lead acquires it. Unreviewed draft \u2192 objective does\n   not start.\n2. **Card gate** \u2014 every card carries its own self-validation evidence in\n   the DO role; the Lead spot-checks against the objective's constraints.\n3. **Delivery gate** \u2014 the objective's verification contract must pass\n   independent review (JUDGE \u2260 implementer) before the objective is DONE.\n   Missing reviewer, missing verdict, incomplete evidence \u2192 **blocked**,\n   never accepted. `goal_mode` iteration may drive a worker's loop but\n   NEVER substitutes for this gate.\n4. **Acceptance gate** \u2014 the operator accepts the accepted-and-merged\n   ADR-0008 and the pilot verdict; only then does V3.1 become standing\n   governance. (ADR-0008 itself must already be `accepted` and merged\n   before any P2 work; the post-pilot step is operator confirmation, not\n   a second ADR acceptance.)\n\n## Degraded routing\n\n| Condition | Routing |\n|---|---|\n| Reviewer (approver lane) unavailable | Objective delivery blocks (fail-closed). The operator may appoint a substitute judge that did not implement. No auto-approval, ever. |\n| Qwen local fails its capability gate or economics gate, or is in cooldown | Qwen local is **excluded**; work routes to available capable lanes or blocks. Exclusion never weakens any other gate and never blocks the role design. |\n| Worker lane unavailable | The Lead re-dispatches to another capable lane inside the same role boundaries; if none, the card blocks. |\n| Git/PR unavailable (credential or platform failure) | Objective execution continues only if no gate needs the PR; otherwise blocks. No local \"shadow merges\". |\n| Evidence incomplete at delivery | The delivery is rejected back to DO; no partial credit. |\n\n## Qwen local routability gates\n\nQwen local (Ollama) may participate only if BOTH gates pass:\n\n1. **Capability gate** \u2014 adequate reasoning/verbosity for the assigned\n   role, judged on the role's evidence requirements.\n2. **Economics gate** \u2014 a paired benchmark on the **same three\n   non-mutating fixtures**: record `wall_time_validated` for the direct\n   GPT/Lead run and for the **Qwen + Lead audit/rework** run. Qwen is\n   routable only if 3/3 fixture ACs pass under Qwen, Qwen makes zero\n   cloud/GPT fallback calls, and the aggregate Qwen time is\n   \u2264 1.20\u00d7 the direct baseline. Any failure \u21d2 excluded (fail-closed\n   routing, above).\n\n## goal_mode enablement gate (fail-closed, capability-gated)\n\n`goal_mode` (automatic goal continuation on a worker) is **DISABLED for\nevery real Objective** until the Phase P2 smoke proves the full contract\nbelow. Like Qwen local (ADR-0008 Decisions 6/9), it is a capability-gated\nparticipant \u2014 never a replacement for the JUDGE gate (Decision 5).\n\n**Enablement gate:** `goal_mode` may be enabled for real Objectives only\nafter a P2 smoke run on a non-mutating scratch card proves ALL of the\nfollowing, with recorded evidence:\n\n| Smoke case | Required (fail-closed) outcome | Prohibited |\n|---|---|---|\n| Judge `DONE` | No promotion by goal_mode alone: DONE requires AC evidence + Lead audit + independent APPROVE | auto-done, auto-merge, auto-cutover |\n| Judge `CONTINUE` / `FAIL` | Bounded turn budget honored; objective blocked/rejected as configured after the budget | unbounded loops, automatic re-dispatch |\n| Provider timeout / offline | Objective lands **Blocked** with visible triage; goal_mode stays off | silent retry storms, false DONE, silent cloud fallback |\n| Worker/gateway restart | At most one reconciled continuation run; smoke repeatable | duplicate runs/workers, lost-blocker resurrection |\n| Turn budget exhaustion | Turns bounded by configuration; overflow \u21d2 Blocked | unlimited continuation |\n| External-mutation watch | Zero mutations outside the scratch card for every case | any write outside the scratch scope |\n\n**Fail-closed rule:** on ANY failure, timeout, or unverifiable outcome in\nthis smoke \u2014 and on any later regression of the above behaviors \u2014\n`goal_mode` **remains disabled** for real Objectives. Disabling it never\nweakens any other gate and never blocks the role design; enabling it\nwithout a fully green recorded smoke is a governance violation, and a\ngoal_mode-completed delivery judged without the JUDGE step is void.\n\n## Fail-closed state machine\n\n```mermaid\nstateDiagram-v2\n    [*] --> Draft\n    Draft --> ObjectiveReview: Architect hands off draft\n    ObjectiveReview --> Accepted: reviewed\n    ObjectiveReview --> Draft: review fails / REQUEST_CHANGES on the draft\n    Accepted --> Executing: Lead acquires + decomposes\n    Accepted --> ObjectiveReview: acquisition precondition lost\n    Executing --> Delivering: cards complete with evidence\n    Delivering --> DeliveryReview: verification contract exercised\n    DeliveryReview --> Done: JUDGE approves\n    DeliveryReview --> Executing: REQUEST_CHANGES\n    Done --> AcceptedGovernance: operator confirms standing governance (pilot positive)\n    Done --> Closed: operator rejects standing governance (lessons kept)\n    ObjectiveReview --> Blocked: no reviewer / verdict\n    Accepted --> Blocked: Lead acquisition unavailable\n    Executing --> Blocked: dispatch fails at this phase\n    Delivering --> Blocked: no verdict / evidence gap\n    DeliveryReview --> Blocked: no reviewer / verdict\n    Blocked --> ObjectiveReview: re-enter the gate this phase requires\n    Blocked --> Abandoned: stop condition / rollback trigger\n    Abandoned --> [*]\n    Closed --> [*]\n    AcceptedGovernance --> [*]\n```\n\nFail-closed recovery rules:\n\n- `Blocked` always records **which phase it arose in**. Its only exit is\n  back to the gate that phase requires \u2014 ObjectiveReview blocks return to\n  ObjectiveReview, post-acceptance blocks (acquisition/dispatch/\n  delivery) return to the review or dispatch gate of their own phase.\n  There is NO `Blocked -> Executing` bypass edge.\n- `Delivering` blocked on missing reviewer/evidence goes to\n  `DeliveryReview` only when a competent independent reviewer (JUDGE \u2260\n  implementer) is actually available; otherwise it stays `Blocked`.\n  Recovery never re-enters `Executing` with unfixed evidence.\n\nInvariant: every non-happy path lands in **Blocked** or **Abandoned**,\nnever in a silently-accepted state. Abandonment and rollback follow the\ntriggers in ADR-0008 Decision 7.\n\n## Relationship to level-1 / level-2 sources\n\n- Git Objectives = level-2 versioned contracts (this repo).\n- Kanban board + Work Ledger = level-1 operational state (live).\n- The Execution Ledger (Lead single-writer, level 1) cross-references the\n  objective's commit/PR SHAs; Git never copies live state back. See the\n  [ledger template](../objectives/templates/execution-ledger.md).\n\n## Related documents\n\n- [ADR-0008](../decisions/ADR-0008-harness-v31-objective-governance.md) \u2014\n  the decisions this operating model implements (proposed).\n- [OBJ-HARNESS-V31-001](../objectives/active/OBJ-HARNESS-V31-001.md) \u2014\n  the pilot objective.\n- [Objectives index](../objectives/index.md) \u2014 how Objectives are managed.\n- Lanes and harness history: legacy\n  [ai-first-harness-plan](../design/ai-first-harness-plan.md)\n  (extraction source).",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (agents/harness-v31-governance.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-harness-v3.1-governa\">Harness V3.1 governance \u2014 roles, gates and fail-closed state machine</p>\n<p id=\"bkmrk-this-document-is-the\">This document is the operating model of the Harness V3.1 pilot. The\ndecisions live in <a href=\"https://book.wetopi.net/decisions/ADR-0008-harness-v31-objective-governance.md\">ADR-0008</a>\n(proposed); the pilot itself is\n<a href=\"https://book.wetopi.net/objectives/active/OBJ-HARNESS-V31-001.md\">OBJ-HARNESS-V31-001</a>.\nNothing here is accepted governance until ADR-0008 is accepted after the\npilot verdict.</p>\n<h2 id=\"bkmrk-what-this-covers\">What this covers</h2>\n<p id=\"bkmrk-roles-and-their-boun\">Roles and their boundaries, the review gates, degraded routing, and the\nfail-closed state machine that governs every Objective under V3.1.</p>\n<h2 id=\"bkmrk-roles\">Roles</h2>\n<table id=\"bkmrk-role-actor-delivers-\">\n<thead>\n<tr>\n<th>Role</th>\n<th>Actor</th>\n<th>Delivers</th>\n<th>Hard boundary</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>THINK (Architect)</td>\n<td>architect-designated profile or the operator as architect</td>\n<td>Objective Draft: WHAT / WHY / DONE (outcome, context, ACs, verification contract, constraints, boundaries, stop conditions)</td>\n<td><strong>No Git write.</strong> Never pushes branches/commits. Never claims cards. Never judges its own draft.</td>\n</tr>\n<tr>\n<td>OWN (Lead)</td>\n<td><code>wetopi-lead</code></td>\n<td>Objective acceptance, card decomposition, dispatch, Execution Ledger (single writer), documentation PRs</td>\n<td>Does not author WHAT/WHY/DONE it was not handed; does not judge deliveries.</td>\n</tr>\n<tr>\n<td>DO (workers)</td>\n<td><code>wetopi-build</code>, <code>wetopi-senior</code>, <code>wetopi-ops</code></td>\n<td>Card implementation with self-validation and evidence</td>\n<td>No apply/destroy of real infrastructure from build cards; no self-approval; stays inside objective boundaries and stop conditions.</td>\n</tr>\n<tr>\n<td>JUDGE (review)</td>\n<td><code>wetopi-approver</code> or any reviewer that did NOT implement</td>\n<td>Verdicts against the verification contract: APPROVE / REQUEST_CHANGES / REJECT</td>\n<td>Does not implement or patch the delivery; does not merge without operator authority.</td>\n</tr>\n</tbody>\n</table>\n<h2 id=\"bkmrk-the-architect-capabi\">The Architect capability boundary</h2>\n<p id=\"bkmrk-the-architect-define\">The Architect defines <strong>WHAT, WHY and DONE \u2014 nothing else</strong>:</p>\n<ul id=\"bkmrk-writes%3A-outcome-stat\">\n<li><strong>Writes:</strong> outcome statement, context, acceptance criteria, the\nverification contract (how DONE is proven), constraints, boundaries,\nout-of-scope, stop conditions, review boundaries, promotion matrix.</li>\n<li><strong>Hands off:</strong> the draft to the OWN (Lead) role as an Objective Draft.\nEntering Git is the Lead's documentation task, via PR \u2014 the Architect\nnever pushes.</li>\n<li><strong>May:</strong> answer questions, refine the draft on request, flag drift\nbetween execution and intent.</li>\n<li><strong>May not:</strong> hold Git credentials for the docs repo, execute cards,\nreview its own draft, or expand the objective mid-flight (a scope change\nis a new draft, re-reviewed).</li>\n</ul>\n<h2 id=\"bkmrk-gates-%28conditional-r\">Gates (conditional review, fail-closed)</h2>\n<ol id=\"bkmrk-objective-gate-%E2%80%94-the\">\n<li><strong>Objective gate</strong> \u2014 the Objective Draft must be reviewed (operator or\nJUDGE) before the Lead acquires it. Unreviewed draft \u2192 objective does\nnot start.</li>\n<li><strong>Card gate</strong> \u2014 every card carries its own self-validation evidence in\nthe DO role; the Lead spot-checks against the objective's constraints.</li>\n<li><strong>Delivery gate</strong> \u2014 the objective's verification contract must pass\nindependent review (JUDGE \u2260 implementer) before the objective is DONE.\nMissing reviewer, missing verdict, incomplete evidence \u2192 <strong>blocked</strong>,\nnever accepted. <code>goal_mode</code> iteration may drive a worker's loop but\nNEVER substitutes for this gate.</li>\n<li><strong>Acceptance gate</strong> \u2014 the operator accepts the accepted-and-merged\nADR-0008 and the pilot verdict; only then does V3.1 become standing\ngovernance. (ADR-0008 itself must already be <code>accepted</code> and merged\nbefore any P2 work; the post-pilot step is operator confirmation, not\na second ADR acceptance.)</li>\n</ol>\n<h2 id=\"bkmrk-degraded-routing\">Degraded routing</h2>\n<table id=\"bkmrk-condition-routing-re\">\n<thead>\n<tr>\n<th>Condition</th>\n<th>Routing</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Reviewer (approver lane) unavailable</td>\n<td>Objective delivery blocks (fail-closed). The operator may appoint a substitute judge that did not implement. No auto-approval, ever.</td>\n</tr>\n<tr>\n<td>Qwen local fails its capability gate or economics gate, or is in cooldown</td>\n<td>Qwen local is <strong>excluded</strong>; work routes to available capable lanes or blocks. Exclusion never weakens any other gate and never blocks the role design.</td>\n</tr>\n<tr>\n<td>Worker lane unavailable</td>\n<td>The Lead re-dispatches to another capable lane inside the same role boundaries; if none, the card blocks.</td>\n</tr>\n<tr>\n<td>Git/PR unavailable (credential or platform failure)</td>\n<td>Objective execution continues only if no gate needs the PR; otherwise blocks. No local \"shadow merges\".</td>\n</tr>\n<tr>\n<td>Evidence incomplete at delivery</td>\n<td>The delivery is rejected back to DO; no partial credit.</td>\n</tr>\n</tbody>\n</table>\n<h2 id=\"bkmrk-qwen-local-routabili\">Qwen local routability gates</h2>\n<p id=\"bkmrk-qwen-local-%28ollama%29-\">Qwen local (Ollama) may participate only if BOTH gates pass:</p>\n<ol id=\"bkmrk-capability-gate-%E2%80%94-ad\">\n<li><strong>Capability gate</strong> \u2014 adequate reasoning/verbosity for the assigned\nrole, judged on the role's evidence requirements.</li>\n<li><strong>Economics gate</strong> \u2014 a paired benchmark on the <strong>same three\nnon-mutating fixtures</strong>: record <code>wall_time_validated</code> for the direct\nGPT/Lead run and for the <strong>Qwen + Lead audit/rework</strong> run. Qwen is\nroutable only if 3/3 fixture ACs pass under Qwen, Qwen makes zero\ncloud/GPT fallback calls, and the aggregate Qwen time is\n\u2264 1.20\u00d7 the direct baseline. Any failure \u21d2 excluded (fail-closed\nrouting, above).</li>\n</ol>\n<h2 id=\"bkmrk-goal_mode-enablement\">goal_mode enablement gate (fail-closed, capability-gated)</h2>\n<p id=\"bkmrk-goal_mode-%28automatic\"><code>goal_mode</code> (automatic goal continuation on a worker) is <strong>DISABLED for\nevery real Objective</strong> until the Phase P2 smoke proves the full contract\nbelow. Like Qwen local (ADR-0008 Decisions 6/9), it is a capability-gated\nparticipant \u2014 never a replacement for the JUDGE gate (Decision 5).</p>\n<p id=\"bkmrk-enablement-gate%3A-goa\"><strong>Enablement gate:</strong> <code>goal_mode</code> may be enabled for real Objectives only\nafter a P2 smoke run on a non-mutating scratch card proves ALL of the\nfollowing, with recorded evidence:</p>\n<table id=\"bkmrk-smoke-case-required-\">\n<thead>\n<tr>\n<th>Smoke case</th>\n<th>Required (fail-closed) outcome</th>\n<th>Prohibited</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Judge <code>DONE</code></td>\n<td>No promotion by goal_mode alone: DONE requires AC evidence + Lead audit + independent APPROVE</td>\n<td>auto-done, auto-merge, auto-cutover</td>\n</tr>\n<tr>\n<td>Judge <code>CONTINUE</code> / <code>FAIL</code></td>\n<td>Bounded turn budget honored; objective blocked/rejected as configured after the budget</td>\n<td>unbounded loops, automatic re-dispatch</td>\n</tr>\n<tr>\n<td>Provider timeout / offline</td>\n<td>Objective lands <strong>Blocked</strong> with visible triage; goal_mode stays off</td>\n<td>silent retry storms, false DONE, silent cloud fallback</td>\n</tr>\n<tr>\n<td>Worker/gateway restart</td>\n<td>At most one reconciled continuation run; smoke repeatable</td>\n<td>duplicate runs/workers, lost-blocker resurrection</td>\n</tr>\n<tr>\n<td>Turn budget exhaustion</td>\n<td>Turns bounded by configuration; overflow \u21d2 Blocked</td>\n<td>unlimited continuation</td>\n</tr>\n<tr>\n<td>External-mutation watch</td>\n<td>Zero mutations outside the scratch card for every case</td>\n<td>any write outside the scratch scope</td>\n</tr>\n</tbody>\n</table>\n<p id=\"bkmrk-fail-closed-rule%3A-on\"><strong>Fail-closed rule:</strong> on ANY failure, timeout, or unverifiable outcome in\nthis smoke \u2014 and on any later regression of the above behaviors \u2014\n<code>goal_mode</code> <strong>remains disabled</strong> for real Objectives. Disabling it never\nweakens any other gate and never blocks the role design; enabling it\nwithout a fully green recorded smoke is a governance violation, and a\ngoal_mode-completed delivery judged without the JUDGE step is void.</p>\n<h2 id=\"bkmrk-fail-closed-state-ma\">Fail-closed state machine</h2>\n<pre id=\"bkmrk-statediagram-v2-%5B%2A%5D-\"><code class=\"language-mermaid\">stateDiagram-v2\n    [*] --&gt; Draft\n    Draft --&gt; ObjectiveReview: Architect hands off draft\n    ObjectiveReview --&gt; Accepted: reviewed\n    ObjectiveReview --&gt; Draft: review fails / REQUEST_CHANGES on the draft\n    Accepted --&gt; Executing: Lead acquires + decomposes\n    Accepted --&gt; ObjectiveReview: acquisition precondition lost\n    Executing --&gt; Delivering: cards complete with evidence\n    Delivering --&gt; DeliveryReview: verification contract exercised\n    DeliveryReview --&gt; Done: JUDGE approves\n    DeliveryReview --&gt; Executing: REQUEST_CHANGES\n    Done --&gt; AcceptedGovernance: operator confirms standing governance (pilot positive)\n    Done --&gt; Closed: operator rejects standing governance (lessons kept)\n    ObjectiveReview --&gt; Blocked: no reviewer / verdict\n    Accepted --&gt; Blocked: Lead acquisition unavailable\n    Executing --&gt; Blocked: dispatch fails at this phase\n    Delivering --&gt; Blocked: no verdict / evidence gap\n    DeliveryReview --&gt; Blocked: no reviewer / verdict\n    Blocked --&gt; ObjectiveReview: re-enter the gate this phase requires\n    Blocked --&gt; Abandoned: stop condition / rollback trigger\n    Abandoned --&gt; [*]\n    Closed --&gt; [*]\n    AcceptedGovernance --&gt; [*]\n</code></pre>\n<p id=\"bkmrk-fail-closed-recovery\">Fail-closed recovery rules:</p>\n<ul id=\"bkmrk-blocked-always-recor\">\n<li><code>Blocked</code> always records <strong>which phase it arose in</strong>. Its only exit is\nback to the gate that phase requires \u2014 ObjectiveReview blocks return to\nObjectiveReview, post-acceptance blocks (acquisition/dispatch/\ndelivery) return to the review or dispatch gate of their own phase.\nThere is NO <code>Blocked -&gt; Executing</code> bypass edge.</li>\n<li><code>Delivering</code> blocked on missing reviewer/evidence goes to\n<code>DeliveryReview</code> only when a competent independent reviewer (JUDGE \u2260\nimplementer) is actually available; otherwise it stays <code>Blocked</code>.\nRecovery never re-enters <code>Executing</code> with unfixed evidence.</li>\n</ul>\n<p id=\"bkmrk-invariant%3A-every-non\">Invariant: every non-happy path lands in <strong>Blocked</strong> or <strong>Abandoned</strong>,\nnever in a silently-accepted state. Abandonment and rollback follow the\ntriggers in ADR-0008 Decision 7.</p>\n<h2 id=\"bkmrk-relationship-to-leve\">Relationship to level-1 / level-2 sources</h2>\n<ul id=\"bkmrk-git-objectives-%3D-lev\">\n<li>Git Objectives = level-2 versioned contracts (this repo).</li>\n<li>Kanban board + Work Ledger = level-1 operational state (live).</li>\n<li>The Execution Ledger (Lead single-writer, level 1) cross-references the\nobjective's commit/PR SHAs; Git never copies live state back. See the\n<a href=\"https://book.wetopi.net/objectives/templates/execution-ledger.md\">ledger template</a>.</li>\n</ul>\n<h2 id=\"bkmrk-related-documents\">Related documents</h2>\n<ul id=\"bkmrk-adr-0008-%E2%80%94-the-decis\">\n<li><a href=\"https://book.wetopi.net/decisions/ADR-0008-harness-v31-objective-governance.md\">ADR-0008</a> \u2014\nthe decisions this operating model implements (proposed).</li>\n<li><a href=\"https://book.wetopi.net/objectives/active/OBJ-HARNESS-V31-001.md\">OBJ-HARNESS-V31-001</a> \u2014\nthe pilot objective.</li>\n<li><a href=\"https://book.wetopi.net/objectives/index.md\">Objectives index</a> \u2014 how Objectives are managed.</li>\n<li>Lanes and harness history: legacy\n<a href=\"https://book.wetopi.net/design/ai-first-harness-plan.md\">ai-first-harness-plan</a>\n(extraction source).</li>\n</ul>\n"
 },
 {
  "id": 34,
  "title": "hermes agents01 instances",
  "slug": "hermes-agents01-instances",
  "book_id": 7,
  "chapter_id": 24,
  "description": null,
  "created_at": "2026-09-06T13:38:52.000000Z",
  "updated_at": "2026-09-06T13:38:52.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (agents/hermes-agents01-instances.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n ARCH-HERMES-AGENTS01: Hermes agents on agents01\n\n**One host, many disposable agents.** The containers are cattle \u2014 the state\nis the bind mount. Everything an agent is (config, memory, skills, sessions,\nsecrets) lives on the host under `/opt/hermes-<name>/data`; everything the\ncontainer image or apt installs is reproducible from `bootstrap.sh`.\n\n## Host facts (agents01)\n\n| Fact | Value |\n|---|---|\n| Public IP | 92.222.245.89 |\n| Hostname | `agents01` (with s) |\n| SSH access | `ssh -i <key> root@92.222.245.89` \u2014 **root only** (`ubuntu@` is denied) |\n| Resources | ~8 GB RAM, 40 GB disk, Docker 29.x |\n| Docker network | `hermes-net` \u2014 shared by all services + nginx |\n\n## Shared services on the host (not agent-owned)\n\nAny Hermes instance reaches them via `host.docker.internal` (or container\nname on `hermes-net`):\n\n| Service | Endpoint | Use |\n|---|---|---|\n| searxng | `http://host.docker.internal:8888` | `SEARXNG_URL` env (web search) |\n| firecrawl / crw | `http://host.docker.internal:3000` | `FIRECRAWL_API_URL` env (scraping) |\n| camofox-mcp | `http://host.docker.internal:9378/mcp` | `CAMOFOX_MCP_URL` env (browser automation) |\n| lightpanda / crw-camofox | internal | browser backends for the above |\n\n## Hermes instances (validated 2026-09-01)\n\n| Instance | Dashboard (host) | API (host) | Public URL |\n|---|---|---|---|\n| hermes-carlos | 9123 | 8646 | `carlos.wetopi.net` (HTTPS) |\n| hermes-joan | 9124 | 8647 | `joan.wetopi.net` (HTTPS, LE cert) |\n| hermes-josep | 9125 | 8648 | `josep.wetopi.net` (HTTPS, LE cert) |\n\nBoth joan/josep share ONE certificate (`live/joan.wetopi.net/`, SAN:\njoan + josep, HTTP-01 webroot, expires 2026-11-30, auto-renew via the\ncertbot container). Explicit A records for both subdomains \u2192 92.222.245.89\nwere created 2026-09-01 (the wildcard still points at .76).\n\nInside every container the ports are always **9119 (dashboard)** and\n**8642 (API server)**; only the host-side ports differ.\n\n## Instance layout \u2014 where persistence actually lives\n\n```text\n/opt/hermes-<name>/data       \u2192 /opt/data      (ALL agent state, rw bind)\n/opt/hermes-<name>/project    \u2192 /home/hermes   (workspace, rw bind)\n/opt/hermes-<name>/.env       \u2192 API_SERVER_KEY (0600, per-instance secret)\n```\n\nWhat lives inside `data/` (= the container's `/opt/data`):\n`config.yaml`, `profile.yaml`, `memories/`, `skills/`, `profiles/`,\n`sessions/`, `state.db`, `secrets/`, `bootstrap.sh`.\n\n**Consequences:**\n\n- `restart=always` + Docker restarting on boot \u2192 instances survive host\n  reboots; bind mounts remount automatically.\n- `docker rm` + recreate is **safe**: all state is on the host. What is NOT\n  preserved is the container layer (apt packages, `/usr/local/bin` binaries)\n  \u2014 `bootstrap.sh` (persisted inside `data/`) reinstalls those on start.\n- Losing `data/` loses the agent. Back it up like any stateful volume.\n\n## Deploy procedure\n\nParametrized script at `/opt/deploy-hermes-instance.sh` on agents01\n(validated template lives in the lead profile skill `wetopi-hermes-hosts`):\n\n```bash\n/opt/deploy-hermes-instance.sh <name> <dashboard_port> <api_port> start\n```\n\nSteps for a NEW instance (the script automates most of this):\n\n1. `start` \u2014 creates layout, generates `.env`, runs container\n   (`nousresearch/hermes-agent:latest`, `--restart always`), joins\n   `hermes-net`, waits for s6.\n2. Copy a known-good `bootstrap.sh` into the fresh `data/` (from\n   `/opt/hermes-carlos/data/bootstrap.sh`) and run the `bootstrap` action \u2014\n   fresh instances have none.\n3. Copy the `dashboard:` `basic_auth` block from a working instance's\n   `config.yaml` and `docker restart` \u2014 without it the dashboard refuses to\n   bind publicly (see pitfalls).\n4. Configure model/providers (fresh instances ship a placeholder\n   `OPENAI_API_KEY` and no LLM access).\n5. Add the nginx vhost (below) and, once DNS points at .89, issue the cert\n   and enable the HTTPS block.\n\n## nginx routing pattern\n\nFiles: `/opt/composes/nginx/conf.d/<name>.conf` (compose in\n`/opt/composes/nginx/docker-compose.yml`; certs volume shared with the\ncertbot container, webroot `/var/www/certbot`).\n\n- Port 80 server: acme-challenge location + `proxy_pass\n  http://hermes-<name>:9119` with websocket upgrade headers (the dashboard\n  needs `Upgrade`/`Connection` and long read/send timeouts).\n- Port 443 server: same proxy, `ssl_certificate\n  /etc/nginx/certs/live/<name>.wetopi.net/fullchain.pem` \u2014 ship it COMMENTED\n  until certbot has issued the cert, then uncomment and reload.\n- Wildcard `*.wetopi.net` currently points at 92.222.245.76; each agent\n  subdomain needs an explicit A record \u2192 92.222.245.89 before HTTP-01\n  validation can succeed.\n- Apply with `docker exec nginx-proxy nginx -t && docker exec nginx-proxy\n  nginx -s reload`.\n\n## Auth requirement (dashboard)\n\nCurrent hermes-agent images **refuse to bind the dashboard to 0.0.0.0\nwithout a registered auth provider**. The working pattern (all three\ninstances): `dashboard.basic_auth` (username + `password_hash`) in each\ninstance's `config.yaml`. Hash via\n`python -c \"from plugins.dashboard_auth.basic import hash_password; print(hash_password('...'))\"`.\nCredentials are per-agent and live only in each `config.yaml` \u2014 never in\ndocs.",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (agents/hermes-agents01-instances.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-arch-hermes-agents01\">ARCH-HERMES-AGENTS01: Hermes agents on agents01</p>\n<p id=\"bkmrk-one-host%2C-many-dispo\"><strong>One host, many disposable agents.</strong> The containers are cattle \u2014 the state\nis the bind mount. Everything an agent is (config, memory, skills, sessions,\nsecrets) lives on the host under <code>/opt/hermes-&lt;name&gt;/data</code>; everything the\ncontainer image or apt installs is reproducible from <code>bootstrap.sh</code>.</p>\n<h2 id=\"bkmrk-host-facts-%28agents01\">Host facts (agents01)</h2>\n<table id=\"bkmrk-fact-value-public-ip\">\n<thead>\n<tr>\n<th>Fact</th>\n<th>Value</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Public IP</td>\n<td>92.222.245.89</td>\n</tr>\n<tr>\n<td>Hostname</td>\n<td><code>agents01</code> (with s)</td>\n</tr>\n<tr>\n<td>SSH access</td>\n<td><code>ssh -i &lt;key&gt; root@92.222.245.89</code> \u2014 <strong>root only</strong> (<code>ubuntu@</code> is denied)</td>\n</tr>\n<tr>\n<td>Resources</td>\n<td>~8 GB RAM, 40 GB disk, Docker 29.x</td>\n</tr>\n<tr>\n<td>Docker network</td>\n<td><code>hermes-net</code> \u2014 shared by all services + nginx</td>\n</tr>\n</tbody>\n</table>\n<h2 id=\"bkmrk-shared-services-on-t\">Shared services on the host (not agent-owned)</h2>\n<p id=\"bkmrk-any-hermes-instance-\">Any Hermes instance reaches them via <code>host.docker.internal</code> (or container\nname on <code>hermes-net</code>):</p>\n<table id=\"bkmrk-service-endpoint-use\">\n<thead>\n<tr>\n<th>Service</th>\n<th>Endpoint</th>\n<th>Use</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>searxng</td>\n<td><code>http://host.docker.internal:8888</code></td>\n<td><code>SEARXNG_URL</code> env (web search)</td>\n</tr>\n<tr>\n<td>firecrawl / crw</td>\n<td><code>http://host.docker.internal:3000</code></td>\n<td><code>FIRECRAWL_API_URL</code> env (scraping)</td>\n</tr>\n<tr>\n<td>camofox-mcp</td>\n<td><code>http://host.docker.internal:9378/mcp</code></td>\n<td><code>CAMOFOX_MCP_URL</code> env (browser automation)</td>\n</tr>\n<tr>\n<td>lightpanda / crw-camofox</td>\n<td>internal</td>\n<td>browser backends for the above</td>\n</tr>\n</tbody>\n</table>\n<h2 id=\"bkmrk-hermes-instances-%28va\">Hermes instances (validated 2026-09-01)</h2>\n<table id=\"bkmrk-instance-dashboard-%28\">\n<thead>\n<tr>\n<th>Instance</th>\n<th>Dashboard (host)</th>\n<th>API (host)</th>\n<th>Public URL</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>hermes-carlos</td>\n<td>9123</td>\n<td>8646</td>\n<td><code>carlos.wetopi.net</code> (HTTPS)</td>\n</tr>\n<tr>\n<td>hermes-joan</td>\n<td>9124</td>\n<td>8647</td>\n<td><code>joan.wetopi.net</code> (HTTPS, LE cert)</td>\n</tr>\n<tr>\n<td>hermes-josep</td>\n<td>9125</td>\n<td>8648</td>\n<td><code>josep.wetopi.net</code> (HTTPS, LE cert)</td>\n</tr>\n</tbody>\n</table>\n<p id=\"bkmrk-both-joan%2Fjosep-shar\">Both joan/josep share ONE certificate (<code>live/joan.wetopi.net/</code>, SAN:\njoan + josep, HTTP-01 webroot, expires 2026-11-30, auto-renew via the\ncertbot container). Explicit A records for both subdomains \u2192 92.222.245.89\nwere created 2026-09-01 (the wildcard still points at .76).</p>\n<p id=\"bkmrk-inside-every-contain\">Inside every container the ports are always <strong>9119 (dashboard)</strong> and\n<strong>8642 (API server)</strong>; only the host-side ports differ.</p>\n<h2 id=\"bkmrk-instance-layout-%E2%80%94-wh\">Instance layout \u2014 where persistence actually lives</h2>\n<pre id=\"bkmrk-%2Fopt%2Fhermes-%3Cname%3E%2Fd\"><code class=\"language-text\">/opt/hermes-&lt;name&gt;/data       \u2192 /opt/data      (ALL agent state, rw bind)\n/opt/hermes-&lt;name&gt;/project    \u2192 /home/hermes   (workspace, rw bind)\n/opt/hermes-&lt;name&gt;/.env       \u2192 API_SERVER_KEY (0600, per-instance secret)\n</code></pre>\n<p id=\"bkmrk-what-lives-inside-da\">What lives inside <code>data/</code> (= the container's <code>/opt/data</code>):\n<code>config.yaml</code>, <code>profile.yaml</code>, <code>memories/</code>, <code>skills/</code>, <code>profiles/</code>,\n<code>sessions/</code>, <code>state.db</code>, <code>secrets/</code>, <code>bootstrap.sh</code>.</p>\n<p id=\"bkmrk-consequences%3A\"><strong>Consequences:</strong></p>\n<ul id=\"bkmrk-restart%3Dalways-%2B-doc\">\n<li><code>restart=always</code> + Docker restarting on boot \u2192 instances survive host\nreboots; bind mounts remount automatically.</li>\n<li><code>docker rm</code> + recreate is <strong>safe</strong>: all state is on the host. What is NOT\npreserved is the container layer (apt packages, <code>/usr/local/bin</code> binaries)\n\u2014 <code>bootstrap.sh</code> (persisted inside <code>data/</code>) reinstalls those on start.</li>\n<li>Losing <code>data/</code> loses the agent. Back it up like any stateful volume.</li>\n</ul>\n<h2 id=\"bkmrk-deploy-procedure\">Deploy procedure</h2>\n<p id=\"bkmrk-parametrized-script-\">Parametrized script at <code>/opt/deploy-hermes-instance.sh</code> on agents01\n(validated template lives in the lead profile skill <code>wetopi-hermes-hosts</code>):</p>\n<pre id=\"bkmrk-%2Fopt%2Fdeploy-hermes-i\"><code class=\"language-bash\">/opt/deploy-hermes-instance.sh &lt;name&gt; &lt;dashboard_port&gt; &lt;api_port&gt; start\n</code></pre>\n<p id=\"bkmrk-steps-for-a-new-inst\">Steps for a NEW instance (the script automates most of this):</p>\n<ol id=\"bkmrk-start-%E2%80%94-creates-layo\">\n<li><code>start</code> \u2014 creates layout, generates <code>.env</code>, runs container\n(<code>nousresearch/hermes-agent:latest</code>, <code>--restart always</code>), joins\n<code>hermes-net</code>, waits for s6.</li>\n<li>Copy a known-good <code>bootstrap.sh</code> into the fresh <code>data/</code> (from\n<code>/opt/hermes-carlos/data/bootstrap.sh</code>) and run the <code>bootstrap</code> action \u2014\nfresh instances have none.</li>\n<li>Copy the <code>dashboard:</code> <code>basic_auth</code> block from a working instance's\n<code>config.yaml</code> and <code>docker restart</code> \u2014 without it the dashboard refuses to\nbind publicly (see pitfalls).</li>\n<li>Configure model/providers (fresh instances ship a placeholder\n<code>OPENAI_API_KEY</code> and no LLM access).</li>\n<li>Add the nginx vhost (below) and, once DNS points at .89, issue the cert\nand enable the HTTPS block.</li>\n</ol>\n<h2 id=\"bkmrk-nginx-routing-patter\">nginx routing pattern</h2>\n<p id=\"bkmrk-files%3A-%2Fopt%2Fcomposes\">Files: <code>/opt/composes/nginx/conf.d/&lt;name&gt;.conf</code> (compose in\n<code>/opt/composes/nginx/docker-compose.yml</code>; certs volume shared with the\ncertbot container, webroot <code>/var/www/certbot</code>).</p>\n<ul id=\"bkmrk-port-80-server%3A-acme\">\n<li>Port 80 server: acme-challenge location + <code>proxy_pass http://hermes-&lt;name&gt;:9119</code> with websocket upgrade headers (the dashboard\nneeds <code>Upgrade</code>/<code>Connection</code> and long read/send timeouts).</li>\n<li>Port 443 server: same proxy, <code>ssl_certificate /etc/nginx/certs/live/&lt;name&gt;.wetopi.net/fullchain.pem</code> \u2014 ship it COMMENTED\nuntil certbot has issued the cert, then uncomment and reload.</li>\n<li>Wildcard <code>*.wetopi.net</code> currently points at 92.222.245.76; each agent\nsubdomain needs an explicit A record \u2192 92.222.245.89 before HTTP-01\nvalidation can succeed.</li>\n<li>Apply with <code>docker exec nginx-proxy nginx -t &amp;&amp; docker exec nginx-proxy nginx -s reload</code>.</li>\n</ul>\n<h2 id=\"bkmrk-auth-requirement-%28da\">Auth requirement (dashboard)</h2>\n<p id=\"bkmrk-current-hermes-agent\">Current hermes-agent images <strong>refuse to bind the dashboard to 0.0.0.0\nwithout a registered auth provider</strong>. The working pattern (all three\ninstances): <code>dashboard.basic_auth</code> (username + <code>password_hash</code>) in each\ninstance's <code>config.yaml</code>. Hash via\n<code>python -c \"from plugins.dashboard_auth.basic import hash_password; print(hash_password('...'))\"</code>.\nCredentials are per-agent and live only in each <code>config.yaml</code> \u2014 never in\ndocs.</p>\n"
 },
 {
  "id": 35,
  "title": "ceph",
  "slug": "ceph",
  "book_id": 2,
  "chapter_id": 13,
  "description": null,
  "created_at": "2026-09-06T13:38:52.000000Z",
  "updated_at": "2026-09-06T21:48:05.000000Z",
  "markdown": "> **ARCHIVO \u2014 versi\u00f3n migrada cruda.** La ficha estructurada con las 3 preguntas es la versi\u00f3n `ARCH-*`/`SVC-*` de este mismo chapter (actualizada con datos live 2026-09-06). Esta copia cruda se conserva por el historial.\n\n> Migrado desde `wetopi-platform/docs@main` (architecture/infrastructure/ceph.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n ARCH-INFRA-CEPH: Ceph storage LAN and client access\n\nWetopi's primary storage is a **pre-existing Ceph cluster** that is\nhyperconverged with the legacy Proxmox/Ceph estate and lives on its own\ndedicated storage network. The master architecture plan is explicit: the\nCeph network is **EXISTING \u2014 DO NOT MODIFY**; it is never renumbered and\nnever NATed on any path (ADR-0001 invariants).\n\n## The storage network\n\n| Item | Value |\n|---|---|\n| Subnet | `192.168.96.0/20` |\n| VLAN / bridge | VLAN 2 / `vmbr2` (trunk on every Proxmox node \u2014 see [ovh-and-network.md](ovh-and-network.md)) |\n| Monitors (live, 2026-08-31) | `192.168.111.9-15` (msgr1 port 6789; msgr2 port 3300), 5 mons |\n| Cluster fsid | `b60e7cbb-e83f-42ef-8a4c-395e22cbb96f` |\n| OSD ports | TCP 6800-7300 |\n| Owner | Pre-existing cluster \u2014 **not managed by the Wetopi rebuild** |\n\nMonitors and OSDs are outside the rebuild's scope; the rebuild only\n*consumes* this cluster via Ceph CSI. Live cluster facts above were read\nfrom the deployed CSI config (ConfigMap `ceph-csi-config`) on 2026-08-31.\n\n## Client access model \u2014 storage-native (ADR-0007)\n\nThe access design is fixed by\n[ADR-0007 \u2014 Ceph client access via direct\nstorage-network attachment](../../decisions/ADR-0007-ceph-storage-native.md)\n(operator decision 2026-08-30, superseding the ADR-0004 routing section):\n\n- The 3 platform workers (`rke-platform01/02/03`, the PVC-carrying nodes)\n  carry a **third NIC on `vmbr2`/VLAN 2** \u2014 the Ceph subnet becomes a\n  directly connected L2 domain, the standard Ceph client pattern.\n- **Operator allocation**: sub-CIDR `192.168.98.0/25` reserved for\n  platform storage consumers \u2014 `.10/.11/.12` = workers 01/02/03\n  (all `/20`); the rest of the /25 is free for future consumers.\n- **No routing, no NAT, no return-routes, no natgw involvement**: the\n  storage NIC carries no gateway and no routes; the `/20` connected route\n  appears automatically. The default route stays exclusively `10.72.0.1`\n  on the LAN NIC.\n- Control planes stay single-NIC in v1 (they carry no PVC workloads).\n\nVerified on the live workers (2026-08-31, all three):\n\n```text\neth1  UP  192.168.98.10/20            # rke-platform01 (.11/.12 on 02/03)\nip route get 192.168.111.9\n  -> 192.168.111.9 dev eth1 src 192.168.98.10     # storage NIC, direct\ndefault via 10.72.0.1 dev eth0                    # LAN-only default\n```\n\n```mermaid\nflowchart LR\n  subgraph workers[\"rke-platform01/02/03\"]\n    E0[eth0 \u2014 vmbr4 / 10.72.0.x/23<br/>default via 10.72.0.1]\n    E1[eth1 \u2014 vmbr2 / 192.168.98.10-12/20<br/>no gateway, no routes]\n  end\n  subgraph ceph[\"Ceph LAN 192.168.96.0/20 (VLAN 2, pre-existing)\"]\n    M[mons 192.168.111.9-15<br/>6789 / 3300]\n    O[OSDs 6800-7300]\n  end\n  E1 -->|directly connected L2| M\n  E1 --> O\n  E0 -.->|\"mgmt only; never Ceph I/O\"| LAN[10.72.0.0/23]\n```\n\n## Ceph CSI (RBD) \u2014 current state\n\nThe RKE2 cluster gets RBD-backed PVCs from this cluster via the\n`ceph-csi-rbd` chart artifacts in\n[infrastructure kubernetes/ceph-csi/](https://github.com/wetopi-platform/infrastructure/tree/main/kubernetes/ceph-csi).\n**Deployed reality (verified 2026-08-31): the live driver is an\nOUT-OF-CHART install of upstream ceph-csi v3.12.0 raw manifests**, applied\nby the operator (not via the repo chart, not via Helm \u2014 `helm ls` shows no\nceph-csi release) during the VAULT apply unblock (2026-08-31). The repo\nchart artifacts remain the declared desired state for a future chart-based\nadoption/sync, pinned to match the live version.\n\n**Status as verified 2026-08-31 (kubectl via ops01): deployed and\nrunning.** The earlier bootstrap state \u2014 CSI pods stuck in\n`ContainerCreating` waiting for the operator-delivered inputs\n(`clusterID`/monitors and the `ceph-config` / `ceph-csi-encryption-kms`\nConfigMaps, operator inputs tracked in card t_8eee26e4) \u2014 has been\nresolved: the ConfigMaps (`ceph-config`, `ceph-csi-config`,\n`ceph-csi-encryption-kms-config`) exist in `kube-system` and the CSI pods\nare Running:\n\n| Component | State (2026-08-31) |\n|---|---|\n| `csi-rbdplugin` DaemonSet | 3/3 Running (on the 3 platform workers) |\n| `csi-rbdplugin-provisioner` Deployment | 3/3 Running (replicas 3, raw-manifest default) |\n| StorageClass `ceph-rbd` | pool `kubernetes`, ext4, `Immediate`, reclaim `Delete`, volume expansion enabled; secret `ceph-csi-secret`; NO `imageFeatures` |\n| CSI image (live) | `quay.io/cephcsi/cephcsi:v3.12.0` |\n| Sidecars (live) | provisioner `v5.0.1`, snapshotter `v8.0.1`, attacher `v4.6.1`, resizer `v1.11.1`, registrar `v2.11.1` |\n| RBAC naming (live) | raw-manifest names (`rbd-csi-nodeplugin`, `rbd-csi-provisioner`) |\n| Vault PVCs | `vault-postgres-data` 5Gi + `vault-redis-data` 1Gi \u2014 Bound on `ceph-rbd` |\n\n**Drift resolved (2026-08-31, LEAD decision, card t_f091f122):** the live\nCSI image `v3.12.0` was found drifted against the repo pin\n`3.17.1` (a design-time pin written before any deployment). Canonical =\n**live v3.12.0** (validated in prod: vault PVCs Bound on it); the repo pin\nwas realigned to `3.12.0` in infrastructure PR\n[#30](https://github.com/wetopi-platform/infrastructure/pull/30), which\nalso documents the live-vs-repo deltas that any future chart-based sync\nmust reconcile first (Secret name `ceph-csi-secret` vs `csi-rbd-secret`,\nprovisioner replicas 3 vs 2, adoption of the out-of-chart objects).\nUpstream upgrade to 3.17.x is DEFERRED, deliberate, operator-gated\nmaintenance (ADR-0007: Ceph prod untouched outside operator sessions).\n\n## Takeover notes\n\n- Storage health is the Ceph cluster's own concern: `ceph -s` on a mon\n  (operator access to the pre-existing cluster) \u2014 not from the K8s side.\n- The storage NIC must **never** acquire a default route (asserted in the\n  ADR-0007 validation gates); node identity stays LAN-only.\n- Worker firewall: the `ceph_storage` Ansible role admits only Ceph client\n  ports (3300/6789 + 6800-7300) from the Ceph /20 on `eth1`, in a\n  dedicated nftables table \u2014 see\n  `infrastructure/ansible/roles/ceph_storage`.\n- Ceph *design* (CSI model, storage classes, placement rules) belongs in\n  `../storage/`; this pill covers the physical LAN and the access path.",
  "html": "<blockquote id=\"bkmrk-archivo-%E2%80%94-versi%C3%B3n-mi\">\n<p><strong>ARCHIVO \u2014 versi\u00f3n migrada cruda.</strong> La ficha estructurada con las 3 preguntas es la versi\u00f3n <code>ARCH-*</code>/<code>SVC-*</code> de este mismo chapter (actualizada con datos live 2026-09-06). Esta copia cruda se conserva por el historial.</p>\n</blockquote>\n<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (architecture/infrastructure/ceph.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-arch-infra-ceph%3A-cep\">ARCH-INFRA-CEPH: Ceph storage LAN and client access</p>\n<p id=\"bkmrk-wetopi%27s-primary-sto\">Wetopi's primary storage is a <strong>pre-existing Ceph cluster</strong> that is\nhyperconverged with the legacy Proxmox/Ceph estate and lives on its own\ndedicated storage network. The master architecture plan is explicit: the\nCeph network is <strong>EXISTING \u2014 DO NOT MODIFY</strong>; it is never renumbered and\nnever NATed on any path (ADR-0001 invariants).</p>\n<h2 id=\"bkmrk-the-storage-network\">The storage network</h2>\n<table id=\"bkmrk-item-value-subnet-19\">\n<thead>\n<tr>\n<th>Item</th>\n<th>Value</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Subnet</td>\n<td><code>192.168.96.0/20</code></td>\n</tr>\n<tr>\n<td>VLAN / bridge</td>\n<td>VLAN 2 / <code>vmbr2</code> (trunk on every Proxmox node \u2014 see <a href=\"https://book.wetopi.net/ovh-and-network.md\">ovh-and-network.md</a>)</td>\n</tr>\n<tr>\n<td>Monitors (live, 2026-08-31)</td>\n<td><code>192.168.111.9-15</code> (msgr1 port 6789; msgr2 port 3300), 5 mons</td>\n</tr>\n<tr>\n<td>Cluster fsid</td>\n<td><code>b60e7cbb-e83f-42ef-8a4c-395e22cbb96f</code></td>\n</tr>\n<tr>\n<td>OSD ports</td>\n<td>TCP 6800-7300</td>\n</tr>\n<tr>\n<td>Owner</td>\n<td>Pre-existing cluster \u2014 <strong>not managed by the Wetopi rebuild</strong></td>\n</tr>\n</tbody>\n</table>\n<p id=\"bkmrk-monitors-and-osds-ar\">Monitors and OSDs are outside the rebuild's scope; the rebuild only\n<em>consumes</em> this cluster via Ceph CSI. Live cluster facts above were read\nfrom the deployed CSI config (ConfigMap <code>ceph-csi-config</code>) on 2026-08-31.</p>\n<h2 id=\"bkmrk-client-access-model-\">Client access model \u2014 storage-native (ADR-0007)</h2>\n<p id=\"bkmrk-the-access-design-is\">The access design is fixed by\n<a href=\"https://book.wetopi.net/decisions/ADR-0007-ceph-storage-native.md\">ADR-0007 \u2014 Ceph client access via direct\nstorage-network attachment</a>\n(operator decision 2026-08-30, superseding the ADR-0004 routing section):</p>\n<ul id=\"bkmrk-the-3-platform-worke\">\n<li>The 3 platform workers (<code>rke-platform01/02/03</code>, the PVC-carrying nodes)\ncarry a <strong>third NIC on <code>vmbr2</code>/VLAN 2</strong> \u2014 the Ceph subnet becomes a\ndirectly connected L2 domain, the standard Ceph client pattern.</li>\n<li><strong>Operator allocation</strong>: sub-CIDR <code>192.168.98.0/25</code> reserved for\nplatform storage consumers \u2014 <code>.10/.11/.12</code> = workers 01/02/03\n(all <code>/20</code>); the rest of the /25 is free for future consumers.</li>\n<li><strong>No routing, no NAT, no return-routes, no natgw involvement</strong>: the\nstorage NIC carries no gateway and no routes; the <code>/20</code> connected route\nappears automatically. The default route stays exclusively <code>10.72.0.1</code>\non the LAN NIC.</li>\n<li>Control planes stay single-NIC in v1 (they carry no PVC workloads).</li>\n</ul>\n<p id=\"bkmrk-verified-on-the-live\">Verified on the live workers (2026-08-31, all three):</p>\n<pre id=\"bkmrk-eth1-up-192.168.98.1\"><code class=\"language-text\">eth1  UP  192.168.98.10/20            # rke-platform01 (.11/.12 on 02/03)\nip route get 192.168.111.9\n  -&gt; 192.168.111.9 dev eth1 src 192.168.98.10     # storage NIC, direct\ndefault via 10.72.0.1 dev eth0                    # LAN-only default\n</code></pre>\n<pre id=\"bkmrk-flowchart-lr-subgrap\"><code class=\"language-mermaid\">flowchart LR\n  subgraph workers[\"rke-platform01/02/03\"]\n    E0[eth0 \u2014 vmbr4 / 10.72.0.x/23&lt;br/&gt;default via 10.72.0.1]\n    E1[eth1 \u2014 vmbr2 / 192.168.98.10-12/20&lt;br/&gt;no gateway, no routes]\n  end\n  subgraph ceph[\"Ceph LAN 192.168.96.0/20 (VLAN 2, pre-existing)\"]\n    M[mons 192.168.111.9-15&lt;br/&gt;6789 / 3300]\n    O[OSDs 6800-7300]\n  end\n  E1 --&gt;|directly connected L2| M\n  E1 --&gt; O\n  E0 -.-&gt;|\"mgmt only; never Ceph I/O\"| LAN[10.72.0.0/23]\n</code></pre>\n<h2 id=\"bkmrk-ceph-csi-%28rbd%29-%E2%80%94-cur\">Ceph CSI (RBD) \u2014 current state</h2>\n<p id=\"bkmrk-the-rke2-cluster-get\">The RKE2 cluster gets RBD-backed PVCs from this cluster via the\n<code>ceph-csi-rbd</code> chart artifacts in\n<a href=\"https://github.com/wetopi-platform/infrastructure/tree/main/kubernetes/ceph-csi\">infrastructure kubernetes/ceph-csi/</a>.\n<strong>Deployed reality (verified 2026-08-31): the live driver is an\nOUT-OF-CHART install of upstream ceph-csi v3.12.0 raw manifests</strong>, applied\nby the operator (not via the repo chart, not via Helm \u2014 <code>helm ls</code> shows no\nceph-csi release) during the VAULT apply unblock (2026-08-31). The repo\nchart artifacts remain the declared desired state for a future chart-based\nadoption/sync, pinned to match the live version.</p>\n<p id=\"bkmrk-status-as-verified-2\"><strong>Status as verified 2026-08-31 (kubectl via ops01): deployed and\nrunning.</strong> The earlier bootstrap state \u2014 CSI pods stuck in\n<code>ContainerCreating</code> waiting for the operator-delivered inputs\n(<code>clusterID</code>/monitors and the <code>ceph-config</code> / <code>ceph-csi-encryption-kms</code>\nConfigMaps, operator inputs tracked in card t_8eee26e4) \u2014 has been\nresolved: the ConfigMaps (<code>ceph-config</code>, <code>ceph-csi-config</code>,\n<code>ceph-csi-encryption-kms-config</code>) exist in <code>kube-system</code> and the CSI pods\nare Running:</p>\n<table id=\"bkmrk-component-state-%28202\">\n<thead>\n<tr>\n<th>Component</th>\n<th>State (2026-08-31)</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>csi-rbdplugin</code> DaemonSet</td>\n<td>3/3 Running (on the 3 platform workers)</td>\n</tr>\n<tr>\n<td><code>csi-rbdplugin-provisioner</code> Deployment</td>\n<td>3/3 Running (replicas 3, raw-manifest default)</td>\n</tr>\n<tr>\n<td>StorageClass <code>ceph-rbd</code></td>\n<td>pool <code>kubernetes</code>, ext4, <code>Immediate</code>, reclaim <code>Delete</code>, volume expansion enabled; secret <code>ceph-csi-secret</code>; NO <code>imageFeatures</code></td>\n</tr>\n<tr>\n<td>CSI image (live)</td>\n<td><code>quay.io/cephcsi/cephcsi:v3.12.0</code></td>\n</tr>\n<tr>\n<td>Sidecars (live)</td>\n<td>provisioner <code>v5.0.1</code>, snapshotter <code>v8.0.1</code>, attacher <code>v4.6.1</code>, resizer <code>v1.11.1</code>, registrar <code>v2.11.1</code></td>\n</tr>\n<tr>\n<td>RBAC naming (live)</td>\n<td>raw-manifest names (<code>rbd-csi-nodeplugin</code>, <code>rbd-csi-provisioner</code>)</td>\n</tr>\n<tr>\n<td>Vault PVCs</td>\n<td><code>vault-postgres-data</code> 5Gi + <code>vault-redis-data</code> 1Gi \u2014 Bound on <code>ceph-rbd</code></td>\n</tr>\n</tbody>\n</table>\n<p id=\"bkmrk-drift-resolved-%282026\"><strong>Drift resolved (2026-08-31, LEAD decision, card t_f091f122):</strong> the live\nCSI image <code>v3.12.0</code> was found drifted against the repo pin\n<code>3.17.1</code> (a design-time pin written before any deployment). Canonical =\n<strong>live v3.12.0</strong> (validated in prod: vault PVCs Bound on it); the repo pin\nwas realigned to <code>3.12.0</code> in infrastructure PR\n<a href=\"https://github.com/wetopi-platform/infrastructure/pull/30\">#30</a>, which\nalso documents the live-vs-repo deltas that any future chart-based sync\nmust reconcile first (Secret name <code>ceph-csi-secret</code> vs <code>csi-rbd-secret</code>,\nprovisioner replicas 3 vs 2, adoption of the out-of-chart objects).\nUpstream upgrade to 3.17.x is DEFERRED, deliberate, operator-gated\nmaintenance (ADR-0007: Ceph prod untouched outside operator sessions).</p>\n<h2 id=\"bkmrk-takeover-notes\">Takeover notes</h2>\n<ul id=\"bkmrk-storage-health-is-th\">\n<li>Storage health is the Ceph cluster's own concern: <code>ceph -s</code> on a mon\n(operator access to the pre-existing cluster) \u2014 not from the K8s side.</li>\n<li>The storage NIC must <strong>never</strong> acquire a default route (asserted in the\nADR-0007 validation gates); node identity stays LAN-only.</li>\n<li>Worker firewall: the <code>ceph_storage</code> Ansible role admits only Ceph client\nports (3300/6789 + 6800-7300) from the Ceph /20 on <code>eth1</code>, in a\ndedicated nftables table \u2014 see\n<code>infrastructure/ansible/roles/ceph_storage</code>.</li>\n<li>Ceph <em>design</em> (CSI model, storage classes, placement rules) belongs in\n<code>../storage/</code>; this pill covers the physical LAN and the access path.</li>\n</ul>\n"
 },
 {
  "id": 36,
  "title": "ovh and network",
  "slug": "ovh-and-network",
  "book_id": 2,
  "chapter_id": 14,
  "description": null,
  "created_at": "2026-09-06T13:38:53.000000Z",
  "updated_at": "2026-09-06T21:48:05.000000Z",
  "markdown": "> **ARCHIVO \u2014 versi\u00f3n migrada cruda.** La ficha estructurada con las 3 preguntas es la versi\u00f3n `ARCH-*`/`SVC-*` de este mismo chapter (actualizada con datos live 2026-09-06). Esta copia cruda se conserva por el historial.\n\n> Migrado desde `wetopi-platform/docs@main` (architecture/infrastructure/ovh-and-network.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n ARCH-INFRA-OVH: OVH hosting, vRack and network blocks\n\nWetopi runs on **8 OVH dedicated servers** in a single **vRack** (OVH's\nprivate L2 fabric that interconnects the servers' vRack ports). Proxmox VE\nruns on all 8; the VMs and the RKE2 cluster live on top of it. The whole\nstack is provisioned from the GitOps repo\n[https://github.com/wetopi-platform/infrastructure](https://github.com/wetopi-platform/infrastructure)\n(`tofu/proxmox/environments/prod/` \u2014 [authoritative source](https://github.com/wetopi-platform/infrastructure/tree/main/tofu/proxmox/environments/prod);\nthe committed `terraform.tfvars.example` documents the shape, the real\n`terraform.tfvars` is gitignored/0600).\n\n## The 8 servers\n\nNode names are the OVH server identifiers used throughout tfvars\n(`proxmox_nodes`) and live Proxmox (cluster `wetopi7`):\n\n| Proxmox node | Trunk NIC (live-verified) |\n|---|---|\n| ns31826925 | enp10s0f1np1 |\n| ns31826929 | enp10s0f1np1 |\n| ns3214108  | enp2s0f1np1  |\n| ns3226931  | enp2s0f1np1  |\n| ns3226939  | enp2s0f1np1  |\n| ns3235424  | enp10s0f1np1 |\n| ns3243421  | enp10s0f1np1 |\n| ns3249468  | enp10s0f1np1 |\n\nEach node carries a single **802.1Q trunk NIC** (the `vlan-raw-device`\nverified live per node via the Proxmox API) through which the vRack VLANs\nbelow flow. VM placement across the 8 nodes spreads the control planes and\nworkers over distinct failure domains (see\n[proxmox-cluster.md](proxmox-cluster.md)).\n\n## Public (WAN) IP blocks\n\nTwo OVH-allocated public blocks are attached to the servers and reachable\non the WAN bridge `vmbr3`:\n\n| Block | Addresses in use (verified) |\n|---|---|\n| `92.222.245.64/27` | `92.222.245.91` = wetopi-ops01 (bastion, direct WAN); `92.222.245.83` = wetopi-natgw01 (WAN egress); gateway `92.222.245.94` |\n| `79.137.114.192/27` | `79.137.114.193` = rke-wan01; `79.137.114.194` = rke-wan02 (edge v1, ADR-0006); gateway `79.137.114.222` |\n\n`vmbr3` carries **untagged public IPs** on the VM NICs (no VLAN tag, no IP\non the hypervisor). The ops01 `.91` address is the SSH bastion for the\nwhole internal network (every other VM is reached through it); natgw `.83`\nis the LAN's NAT egress.\n\n## Internal networks and bridges\n\nThree L2 domains exist on the vRack, each a bridge on every Proxmox node:\n\n| Bridge | VLAN | Subnet | Purpose |\n|---|---|---|---|\n| `vmbr4` | 4 | `10.72.0.0/23` (MTU 1500, from reserved `10.72.0.0/20` \u2014 ADR-0001) | RKE2 node LAN: all 8 cluster VMs. No IP on the hypervisors. |\n| `vmbr2` | 2 | `192.168.96.0/20` | Pre-existing **Ceph storage LAN** (see [ceph.md](ceph.md)). |\n| `vmbr3` | (untagged) | public blocks above | WAN attachment. |\n\nAddress plan on `10.72.0.0/23` (from tfvars `ansible_host` + NIC configs):\n`.1` LAN gateway (non-managed default gateway hop), `.2` natgw, `.10`\nops01, `.20` RKE2 API VIP (keepalived, ADR-0004), `.21-23` control\nplanes, `.41-43` platform workers, `.51-52` edge nodes. VMs resolve names\nvia public resolvers (`1.1.1.1`, `1.0.0.1`, domain `wetopi.priv`) \u2014 there\nis **no internal DNS** in the platform; stable endpoints are IP-based.\n\n```mermaid\nflowchart TB\n  subgraph OVH[\"OVH (8 dedicated servers, one vRack)\"]\n    direction TB\n    subgraph WAN[\"vmbr3 \u2014 WAN (untagged)\"]\n      O91[ops01 92.222.245.91<br/>bastion]\n      N83[natgw 92.222.245.83<br/>LAN egress]\n      W1[rke-wan01 79.137.114.193]\n      W2[rke-wan02 79.137.114.194]\n    end\n    subgraph LAN[\"vmbr4 \u2014 VLAN 4, 10.72.0.0/23\"]\n      CP[rke-cp01/02/03<br/>.21/.22/.23 + VIP .20]\n      PL[rke-platform01/02/03<br/>.41/.42/.43]\n    end\n    subgraph CEPH[\"vmbr2 \u2014 VLAN 2, 192.168.96.0/20\"]\n      C1[\"ceph mons/OSDs (pre-existing)\"]\n      PL2[\"workers eth1 192.168.98.10-12/20\"]\n    end\n  end\n  Internet((Internet)) --- WAN\n  WAN ---|LAN-to-WAN NAT| LAN\n  LAN --- CP & PL\n  C1 --- CEPH\n  CEPH --- PL2\n  O91 -.-|\"SSH bastion to all VMs\"| LAN\n```\n\n## Cloudflare DNS (zone `wetopi.net`)\n\nThe zone is managed on Cloudflare (zone id\n`6728a5c341cfadd578c1539ccb28c9ad` \u2014 non-secret, read via the CF API) and\n**Wetopi-new service records are declared in OpenTofu** (`tofu/proxmox/environments/prod/cloudflare-dns.tf` +\n`var.cloudflare_dns_records`), one module instance per record with\n`prevent_destroy`. Verified live via the Cloudflare API on 2026-08-31:\n\n| Name | Records | Shape |\n|---|---|---|\n| `edge-test.wetopi.net` | A \u00d7 79.137.114.193 / .194 | TTL 300, proxied=false (grey-cloud) |\n| `docs-new.wetopi.net`  | A \u00d7 79.137.114.193 / .194 | TTL 300, grey-cloud |\n| `docs.wetopi.net`      | A \u00d7 79.137.114.193 / .194 | TTL 300, grey-cloud (F4 cutover) |\n| `argo.wetopi.net`      | A \u00d7 79.137.114.193 / .194 | TTL 300, grey-cloud |\n\n**Scope rule (WAN Edge v1 plan \u00a74):** only Wetopi-new service records are\nin tofu scope. Legacy records (wildcards, `agg`, `carlos-*`,\n`devops-wan-*`, \u2026) and `_acme-challenge` TXT records (cert-manager) are\ndeliberately out of scope \u2014 never imported, declared or touched.\n`edge-test` was the Let's Encrypt staging gate (F3); `docs-new` the\npre-cutover staging name; `docs` and `argo` are the live v1-window edge\nservices (ADR-0006: DNS round-robin, reduced availability, operator-driven\nremediation).\n\n## Takeover notes\n\n- To re-verify this pill: `tofu plan` on\n  `tofu/proxmox/environments/prod` (no-changes expected), CF API\n  `GET /zones/<id>/dns_records`, and `kubectl get nodes` via ops01.\n- Never commit `terraform.tfstate`, `terraform.tfvars` or provider tokens\n  (`.env.proxmox`, `.cloudflare-wetopi-net.env`) \u2014 see\n  [providers.md](providers.md) for where each secret lives.",
  "html": "<blockquote id=\"bkmrk-archivo-%E2%80%94-versi%C3%B3n-mi\">\n<p><strong>ARCHIVO \u2014 versi\u00f3n migrada cruda.</strong> La ficha estructurada con las 3 preguntas es la versi\u00f3n <code>ARCH-*</code>/<code>SVC-*</code> de este mismo chapter (actualizada con datos live 2026-09-06). Esta copia cruda se conserva por el historial.</p>\n</blockquote>\n<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (architecture/infrastructure/ovh-and-network.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-arch-infra-ovh%3A-ovh-\">ARCH-INFRA-OVH: OVH hosting, vRack and network blocks</p>\n<p id=\"bkmrk-wetopi-runs-on-8-ovh\">Wetopi runs on <strong>8 OVH dedicated servers</strong> in a single <strong>vRack</strong> (OVH's\nprivate L2 fabric that interconnects the servers' vRack ports). Proxmox VE\nruns on all 8; the VMs and the RKE2 cluster live on top of it. The whole\nstack is provisioned from the GitOps repo\n<a href=\"https://github.com/wetopi-platform/infrastructure\">https://github.com/wetopi-platform/infrastructure</a>\n(<code>tofu/proxmox/environments/prod/</code> \u2014 <a href=\"https://github.com/wetopi-platform/infrastructure/tree/main/tofu/proxmox/environments/prod\">authoritative source</a>;\nthe committed <code>terraform.tfvars.example</code> documents the shape, the real\n<code>terraform.tfvars</code> is gitignored/0600).</p>\n<h2 id=\"bkmrk-the-8-servers\">The 8 servers</h2>\n<p id=\"bkmrk-node-names-are-the-o\">Node names are the OVH server identifiers used throughout tfvars\n(<code>proxmox_nodes</code>) and live Proxmox (cluster <code>wetopi7</code>):</p>\n<table id=\"bkmrk-proxmox-node-trunk-n\">\n<thead>\n<tr>\n<th>Proxmox node</th>\n<th>Trunk NIC (live-verified)</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>ns31826925</td>\n<td>enp10s0f1np1</td>\n</tr>\n<tr>\n<td>ns31826929</td>\n<td>enp10s0f1np1</td>\n</tr>\n<tr>\n<td>ns3214108</td>\n<td>enp2s0f1np1</td>\n</tr>\n<tr>\n<td>ns3226931</td>\n<td>enp2s0f1np1</td>\n</tr>\n<tr>\n<td>ns3226939</td>\n<td>enp2s0f1np1</td>\n</tr>\n<tr>\n<td>ns3235424</td>\n<td>enp10s0f1np1</td>\n</tr>\n<tr>\n<td>ns3243421</td>\n<td>enp10s0f1np1</td>\n</tr>\n<tr>\n<td>ns3249468</td>\n<td>enp10s0f1np1</td>\n</tr>\n</tbody>\n</table>\n<p id=\"bkmrk-each-node-carries-a-\">Each node carries a single <strong>802.1Q trunk NIC</strong> (the <code>vlan-raw-device</code>\nverified live per node via the Proxmox API) through which the vRack VLANs\nbelow flow. VM placement across the 8 nodes spreads the control planes and\nworkers over distinct failure domains (see\n<a href=\"https://book.wetopi.net/proxmox-cluster.md\">proxmox-cluster.md</a>).</p>\n<h2 id=\"bkmrk-public-%28wan%29-ip-bloc\">Public (WAN) IP blocks</h2>\n<p id=\"bkmrk-two-ovh-allocated-pu\">Two OVH-allocated public blocks are attached to the servers and reachable\non the WAN bridge <code>vmbr3</code>:</p>\n<table id=\"bkmrk-block-addresses-in-u\">\n<thead>\n<tr>\n<th>Block</th>\n<th>Addresses in use (verified)</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>92.222.245.64/27</code></td>\n<td><code>92.222.245.91</code> = wetopi-ops01 (bastion, direct WAN); <code>92.222.245.83</code> = wetopi-natgw01 (WAN egress); gateway <code>92.222.245.94</code></td>\n</tr>\n<tr>\n<td><code>79.137.114.192/27</code></td>\n<td><code>79.137.114.193</code> = rke-wan01; <code>79.137.114.194</code> = rke-wan02 (edge v1, ADR-0006); gateway <code>79.137.114.222</code></td>\n</tr>\n</tbody>\n</table>\n<p id=\"bkmrk-vmbr3-carries-untagg\"><code>vmbr3</code> carries <strong>untagged public IPs</strong> on the VM NICs (no VLAN tag, no IP\non the hypervisor). The ops01 <code>.91</code> address is the SSH bastion for the\nwhole internal network (every other VM is reached through it); natgw <code>.83</code>\nis the LAN's NAT egress.</p>\n<h2 id=\"bkmrk-internal-networks-an\">Internal networks and bridges</h2>\n<p id=\"bkmrk-three-l2-domains-exi\">Three L2 domains exist on the vRack, each a bridge on every Proxmox node:</p>\n<table id=\"bkmrk-bridge-vlan-subnet-p\">\n<thead>\n<tr>\n<th>Bridge</th>\n<th>VLAN</th>\n<th>Subnet</th>\n<th>Purpose</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>vmbr4</code></td>\n<td>4</td>\n<td><code>10.72.0.0/23</code> (MTU 1500, from reserved <code>10.72.0.0/20</code> \u2014 ADR-0001)</td>\n<td>RKE2 node LAN: all 8 cluster VMs. No IP on the hypervisors.</td>\n</tr>\n<tr>\n<td><code>vmbr2</code></td>\n<td>2</td>\n<td><code>192.168.96.0/20</code></td>\n<td>Pre-existing <strong>Ceph storage LAN</strong> (see <a href=\"https://book.wetopi.net/ceph.md\">ceph.md</a>).</td>\n</tr>\n<tr>\n<td><code>vmbr3</code></td>\n<td>(untagged)</td>\n<td>public blocks above</td>\n<td>WAN attachment.</td>\n</tr>\n</tbody>\n</table>\n<p id=\"bkmrk-address-plan-on-10.7\">Address plan on <code>10.72.0.0/23</code> (from tfvars <code>ansible_host</code> + NIC configs):\n<code>.1</code> LAN gateway (non-managed default gateway hop), <code>.2</code> natgw, <code>.10</code>\nops01, <code>.20</code> RKE2 API VIP (keepalived, ADR-0004), <code>.21-23</code> control\nplanes, <code>.41-43</code> platform workers, <code>.51-52</code> edge nodes. VMs resolve names\nvia public resolvers (<code>1.1.1.1</code>, <code>1.0.0.1</code>, domain <code>wetopi.priv</code>) \u2014 there\nis <strong>no internal DNS</strong> in the platform; stable endpoints are IP-based.</p>\n<pre id=\"bkmrk-flowchart-tb-subgrap\"><code class=\"language-mermaid\">flowchart TB\n  subgraph OVH[\"OVH (8 dedicated servers, one vRack)\"]\n    direction TB\n    subgraph WAN[\"vmbr3 \u2014 WAN (untagged)\"]\n      O91[ops01 92.222.245.91&lt;br/&gt;bastion]\n      N83[natgw 92.222.245.83&lt;br/&gt;LAN egress]\n      W1[rke-wan01 79.137.114.193]\n      W2[rke-wan02 79.137.114.194]\n    end\n    subgraph LAN[\"vmbr4 \u2014 VLAN 4, 10.72.0.0/23\"]\n      CP[rke-cp01/02/03&lt;br/&gt;.21/.22/.23 + VIP .20]\n      PL[rke-platform01/02/03&lt;br/&gt;.41/.42/.43]\n    end\n    subgraph CEPH[\"vmbr2 \u2014 VLAN 2, 192.168.96.0/20\"]\n      C1[\"ceph mons/OSDs (pre-existing)\"]\n      PL2[\"workers eth1 192.168.98.10-12/20\"]\n    end\n  end\n  Internet((Internet)) --- WAN\n  WAN ---|LAN-to-WAN NAT| LAN\n  LAN --- CP &amp; PL\n  C1 --- CEPH\n  CEPH --- PL2\n  O91 -.-|\"SSH bastion to all VMs\"| LAN\n</code></pre>\n<h2 id=\"bkmrk-cloudflare-dns-%28zone\">Cloudflare DNS (zone <code>wetopi.net</code>)</h2>\n<p id=\"bkmrk-the-zone-is-managed-\">The zone is managed on Cloudflare (zone id\n<code>6728a5c341cfadd578c1539ccb28c9ad</code> \u2014 non-secret, read via the CF API) and\n<strong>Wetopi-new service records are declared in OpenTofu</strong> (<code>tofu/proxmox/environments/prod/cloudflare-dns.tf</code> +\n<code>var.cloudflare_dns_records</code>), one module instance per record with\n<code>prevent_destroy</code>. Verified live via the Cloudflare API on 2026-08-31:</p>\n<table id=\"bkmrk-name-records-shape-e\">\n<thead>\n<tr>\n<th>Name</th>\n<th>Records</th>\n<th>Shape</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>edge-test.wetopi.net</code></td>\n<td>A \u00d7 79.137.114.193 / .194</td>\n<td>TTL 300, proxied=false (grey-cloud)</td>\n</tr>\n<tr>\n<td><code>docs-new.wetopi.net</code></td>\n<td>A \u00d7 79.137.114.193 / .194</td>\n<td>TTL 300, grey-cloud</td>\n</tr>\n<tr>\n<td><code>docs.wetopi.net</code></td>\n<td>A \u00d7 79.137.114.193 / .194</td>\n<td>TTL 300, grey-cloud (F4 cutover)</td>\n</tr>\n<tr>\n<td><code>argo.wetopi.net</code></td>\n<td>A \u00d7 79.137.114.193 / .194</td>\n<td>TTL 300, grey-cloud</td>\n</tr>\n</tbody>\n</table>\n<p id=\"bkmrk-scope-rule-%28wan-edge\"><strong>Scope rule (WAN Edge v1 plan \u00a74):</strong> only Wetopi-new service records are\nin tofu scope. Legacy records (wildcards, <code>agg</code>, <code>carlos-*</code>,\n<code>devops-wan-*</code>, \u2026) and <code>_acme-challenge</code> TXT records (cert-manager) are\ndeliberately out of scope \u2014 never imported, declared or touched.\n<code>edge-test</code> was the Let's Encrypt staging gate (F3); <code>docs-new</code> the\npre-cutover staging name; <code>docs</code> and <code>argo</code> are the live v1-window edge\nservices (ADR-0006: DNS round-robin, reduced availability, operator-driven\nremediation).</p>\n<h2 id=\"bkmrk-takeover-notes\">Takeover notes</h2>\n<ul id=\"bkmrk-to-re-verify-this-pi\">\n<li>To re-verify this pill: <code>tofu plan</code> on\n<code>tofu/proxmox/environments/prod</code> (no-changes expected), CF API\n<code>GET /zones/&lt;id&gt;/dns_records</code>, and <code>kubectl get nodes</code> via ops01.</li>\n<li>Never commit <code>terraform.tfstate</code>, <code>terraform.tfvars</code> or provider tokens\n(<code>.env.proxmox</code>, <code>.cloudflare-wetopi-net.env</code>) \u2014 see\n<a href=\"https://book.wetopi.net/providers.md\">providers.md</a> for where each secret lives.</li>\n</ul>\n"
 },
 {
  "id": 37,
  "title": "providers",
  "slug": "providers",
  "book_id": 2,
  "chapter_id": 15,
  "description": null,
  "created_at": "2026-09-06T13:38:53.000000Z",
  "updated_at": "2026-09-06T21:48:06.000000Z",
  "markdown": "> **ARCHIVO \u2014 versi\u00f3n migrada cruda.** La ficha estructurada con las 3 preguntas es la versi\u00f3n `ARCH-*`/`SVC-*` de este mismo chapter (actualizada con datos live 2026-09-06). Esta copia cruda se conserva por el historial.\n\n> Migrado desde `wetopi-platform/docs@main` (architecture/infrastructure/providers.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n ARCH-INFRA-PROVIDERS: IaC providers\n\nThe physical/virtual layer is owned by three tools with a strict\nresponsibility split\n([ADR-0003 \u2014 OpenTofu and Ansible responsibility\nsplit](../../decisions/ADR-0003-opentofu-ansible.md)). Everything below\nlives in the single GitOps repo\n[https://github.com/wetopi-platform/infrastructure](https://github.com/wetopi-platform/infrastructure)\n\u2014 there is exactly one `infrastructure` repo; `docs` explains it, never\nduplicates it.\n\n| Tool | Owns | Does NOT own |\n|---|---|---|\n| **OpenTofu** | Proxmox VMs, node VLAN/bridge shape, base template clone, Cloudflare DNS records \u2014 via the HTTPS API | Guest OS config, K8s workloads, secrets in HCL |\n| **cloud-init** | First-boot identity, network, SSH bootstrap only | Anything after first boot |\n| **Ansible** | Ubuntu baseline, NAT gateway, RKE2, Ceph client firewall, edge | VM identity/placement, K8s manifests |\n| **Argo CD** | K8s add-ons and workloads, after the cluster exists | Infra VMs |\n\nNo secrets are embedded in HCL or committed inventory; DR reconstructs\ninfra and guest state from separate explicit sources of truth.\n\n## OpenTofu + Proxmox VE provider\n\n- Provider: **`bpg/proxmox`** pinned `= 0.111.1`\n  (`tofu/proxmox/environments/prod/versions.tf`), talking to the Proxmox\n  **HTTPS API** (no SSH to the hypervisors).\n- Provider config (`tofu/proxmox/environments/prod/provider.tf`) reads\n  `PROXMOX_VE_ENDPOINT`, `PROXMOX_ve_API_TOKEN` and `PROXMOX_VE_INSECURE`\n  from the **gitignored, repo-level `.env.proxmox`** (mode 0600) \u2014 never\n  from HCL. The controller is the phase-1 harness VM (operator decision),\n  not ops01; see the state-loss runbook below.\n- Modules under `tofu/proxmox/modules/`: `vm` (clone-from-template + NICs),\n  `node-vlan` (bridge/VLAN shape), `cloudflare-dns-record`\n  (`prevent_destroy` per record).\n\n## Cloudflare provider\n\n- Provider: **`cloudflare/cloudflare`** pinned `= 4.52.8`\n  (`modules/cloudflare-dns-record/versions.tf`).\n- Reads `CLOUDFLARE_API_TOKEN` **from the environment only** \u2014 never in\n  code, tfvars or state. On ops01 source\n  `/home/ubuntu/secrets/.cloudflare-wetopi-net.env` (0600) before\n  `tofu init/plan/apply`.\n- Token contract: **both** `Zone - DNS - Edit` **and** `Zone - Zone -\n  Read` \u2014 the two-permission set the cert-manager Cloudflare DNS-01 solver\n  requires (a DNS-Edit-only token fails zone discovery).\n- Records managed by tofu: see [ovh-and-network.md](ovh-and-network.md).\n\n## Ansible\n\n- Lives at `infrastructure/ansible/` (roles, inventories, playbooks).\n  Key roles: `common`, `ssh_keys`, `nat_gateway`, `rke2-server`,\n  `rke2-agent`, `keepalived`, `ceph_storage`, `edge_lb`, `wan_bastion`,\n  `ops_kubectl`.\n- Ansible converges guest config idempotently; it never changes VM\n  identity or placement (that is OpenTofu's job). The `ops_kubectl` role\n  gives ops01 `kubectl` access via the API VIP (kubeconfig fetched at\n  apply-time \u2014 no credentials in Git).\n\n## Where each state lives\n\n| State | Location | Mode / in Git? |\n|---|---|---|\n| `terraform.tfstate` (PROD) | phase-1 controller (harness VM), `\u2026/tofu/proxmox/environments/prod/` | **0600, gitignored \u2014 NEVER in Git** |\n| `terraform.tfvars` (real) | same dir | 0600, gitignored (`.gitignore`: `*.tfvars` except `*.tfvars.example`) |\n| `.env.proxmox` (provider tokens) | repo-level, controller tree | 0600, gitignored |\n| Cloudflare token | `/home/ubuntu/secrets/.cloudflare-wetopi-net.env` on ops01 | 0600, not in Git |\n| K8s kubeconfig | fetched at apply-time from rke-cp01 by `ops_kubectl` role | not persisted in Git |\n| GitOps desired state | `infrastructure` repo (committed) | in Git, reviewed |\n\n**Recovery if the PROD tfstate is lost:** see\n[RUN-TOFU-STATE-LOSS \u2014 OpenTofu PROD state loss &\nre-import recovery](../../operations/runbooks/tofu-prod-state-loss-and-reimport.md)\n(read-only re-import from the live Proxmox API, `tofu plan` = No-changes\ngate). Never reconstruct state by speculative `apply`.\n\n## Secret handling\n\n- **SOPS + age is bootstrap/recovery only** (master plan \u00a711): it answers\n  \"where do we keep the secrets needed to boot the system that keeps\n  secrets\". Two age keys \u2014 a bootstrap/automation key and an offline\n  recovery key. **Private age keys never go in Git.**\n- **The durable runtime vault is Infisical, self-hosted**\n  ([ADR-0009 \u2014 Secrets vault\n  harness](../../decisions/ADR-0009-secrets-vault-harness.md)); the\n  `vault.*` contract + machine identities live there. SOPS+age is *not*\n  the daily vault.\n- The rule \"secrets used, not necessarily seen\" (master plan \u00a73.7) applies:\n  tool runtimes and the vault, least-privilege \u2014 no plaintext in Git,\n  prompts or logs.\n\n## Takeover notes\n\n- Before any `tofu` command: confirm you are in the controller dir, the\n  `.env.proxmox` is sourced (0600), and no token is on disk in the tree.\n- `tofu plan` for `tofu/proxmox/environments/prod` should be a no-change\n  when state is healthy; a plan that wants to CREATE all 28+ resources\n  means the state is lost \u2192 run the state-loss runbook.\n- The GitOps repo is the single source of declarative truth; a merge is a\n  deploy (Argo CD / `tofu` apply), so blast-radius review is required.",
  "html": "<blockquote id=\"bkmrk-archivo-%E2%80%94-versi%C3%B3n-mi\">\n<p><strong>ARCHIVO \u2014 versi\u00f3n migrada cruda.</strong> La ficha estructurada con las 3 preguntas es la versi\u00f3n <code>ARCH-*</code>/<code>SVC-*</code> de este mismo chapter (actualizada con datos live 2026-09-06). Esta copia cruda se conserva por el historial.</p>\n</blockquote>\n<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (architecture/infrastructure/providers.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-arch-infra-providers\">ARCH-INFRA-PROVIDERS: IaC providers</p>\n<p id=\"bkmrk-the-physical%2Fvirtual\">The physical/virtual layer is owned by three tools with a strict\nresponsibility split\n(<a href=\"https://book.wetopi.net/decisions/ADR-0003-opentofu-ansible.md\">ADR-0003 \u2014 OpenTofu and Ansible responsibility\nsplit</a>). Everything below\nlives in the single GitOps repo\n<a href=\"https://github.com/wetopi-platform/infrastructure\">https://github.com/wetopi-platform/infrastructure</a>\n\u2014 there is exactly one <code>infrastructure</code> repo; <code>docs</code> explains it, never\nduplicates it.</p>\n<table id=\"bkmrk-tool-owns-does-not-o\">\n<thead>\n<tr>\n<th>Tool</th>\n<th>Owns</th>\n<th>Does NOT own</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>OpenTofu</strong></td>\n<td>Proxmox VMs, node VLAN/bridge shape, base template clone, Cloudflare DNS records \u2014 via the HTTPS API</td>\n<td>Guest OS config, K8s workloads, secrets in HCL</td>\n</tr>\n<tr>\n<td><strong>cloud-init</strong></td>\n<td>First-boot identity, network, SSH bootstrap only</td>\n<td>Anything after first boot</td>\n</tr>\n<tr>\n<td><strong>Ansible</strong></td>\n<td>Ubuntu baseline, NAT gateway, RKE2, Ceph client firewall, edge</td>\n<td>VM identity/placement, K8s manifests</td>\n</tr>\n<tr>\n<td><strong>Argo CD</strong></td>\n<td>K8s add-ons and workloads, after the cluster exists</td>\n<td>Infra VMs</td>\n</tr>\n</tbody>\n</table>\n<p id=\"bkmrk-no-secrets-are-embed\">No secrets are embedded in HCL or committed inventory; DR reconstructs\ninfra and guest state from separate explicit sources of truth.</p>\n<h2 id=\"bkmrk-opentofu-%2B-proxmox-v\">OpenTofu + Proxmox VE provider</h2>\n<ul id=\"bkmrk-provider%3A-bpg%2Fproxmo\">\n<li>Provider: <strong><code>bpg/proxmox</code></strong> pinned <code>= 0.111.1</code>\n(<code>tofu/proxmox/environments/prod/versions.tf</code>), talking to the Proxmox\n<strong>HTTPS API</strong> (no SSH to the hypervisors).</li>\n<li>Provider config (<code>tofu/proxmox/environments/prod/provider.tf</code>) reads\n<code>PROXMOX_VE_ENDPOINT</code>, <code>PROXMOX_ve_API_TOKEN</code> and <code>PROXMOX_VE_INSECURE</code>\nfrom the <strong>gitignored, repo-level <code>.env.proxmox</code></strong> (mode 0600) \u2014 never\nfrom HCL. The controller is the phase-1 harness VM (operator decision),\nnot ops01; see the state-loss runbook below.</li>\n<li>Modules under <code>tofu/proxmox/modules/</code>: <code>vm</code> (clone-from-template + NICs),\n<code>node-vlan</code> (bridge/VLAN shape), <code>cloudflare-dns-record</code>\n(<code>prevent_destroy</code> per record).</li>\n</ul>\n<h2 id=\"bkmrk-cloudflare-provider\">Cloudflare provider</h2>\n<ul id=\"bkmrk-provider%3A-cloudflare\">\n<li>Provider: <strong><code>cloudflare/cloudflare</code></strong> pinned <code>= 4.52.8</code>\n(<code>modules/cloudflare-dns-record/versions.tf</code>).</li>\n<li>Reads <code>CLOUDFLARE_API_TOKEN</code> <strong>from the environment only</strong> \u2014 never in\ncode, tfvars or state. On ops01 source\n<code>/home/ubuntu/secrets/.cloudflare-wetopi-net.env</code> (0600) before\n<code>tofu init/plan/apply</code>.</li>\n<li>Token contract: <strong>both</strong> <code>Zone - DNS - Edit</code> <strong>and</strong> <code>Zone - Zone - Read</code> \u2014 the two-permission set the cert-manager Cloudflare DNS-01 solver\nrequires (a DNS-Edit-only token fails zone discovery).</li>\n<li>Records managed by tofu: see <a href=\"https://book.wetopi.net/ovh-and-network.md\">ovh-and-network.md</a>.</li>\n</ul>\n<h2 id=\"bkmrk-ansible\">Ansible</h2>\n<ul id=\"bkmrk-lives-at-infrastruct\">\n<li>Lives at <code>infrastructure/ansible/</code> (roles, inventories, playbooks).\nKey roles: <code>common</code>, <code>ssh_keys</code>, <code>nat_gateway</code>, <code>rke2-server</code>,\n<code>rke2-agent</code>, <code>keepalived</code>, <code>ceph_storage</code>, <code>edge_lb</code>, <code>wan_bastion</code>,\n<code>ops_kubectl</code>.</li>\n<li>Ansible converges guest config idempotently; it never changes VM\nidentity or placement (that is OpenTofu's job). The <code>ops_kubectl</code> role\ngives ops01 <code>kubectl</code> access via the API VIP (kubeconfig fetched at\napply-time \u2014 no credentials in Git).</li>\n</ul>\n<h2 id=\"bkmrk-where-each-state-liv\">Where each state lives</h2>\n<table id=\"bkmrk-state-location-mode-\">\n<thead>\n<tr>\n<th>State</th>\n<th>Location</th>\n<th>Mode / in Git?</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>terraform.tfstate</code> (PROD)</td>\n<td>phase-1 controller (harness VM), <code>\u2026/tofu/proxmox/environments/prod/</code></td>\n<td><strong>0600, gitignored \u2014 NEVER in Git</strong></td>\n</tr>\n<tr>\n<td><code>terraform.tfvars</code> (real)</td>\n<td>same dir</td>\n<td>0600, gitignored (<code>.gitignore</code>: <code>*.tfvars</code> except <code>*.tfvars.example</code>)</td>\n</tr>\n<tr>\n<td><code>.env.proxmox</code> (provider tokens)</td>\n<td>repo-level, controller tree</td>\n<td>0600, gitignored</td>\n</tr>\n<tr>\n<td>Cloudflare token</td>\n<td><code>/home/ubuntu/secrets/.cloudflare-wetopi-net.env</code> on ops01</td>\n<td>0600, not in Git</td>\n</tr>\n<tr>\n<td>K8s kubeconfig</td>\n<td>fetched at apply-time from rke-cp01 by <code>ops_kubectl</code> role</td>\n<td>not persisted in Git</td>\n</tr>\n<tr>\n<td>GitOps desired state</td>\n<td><code>infrastructure</code> repo (committed)</td>\n<td>in Git, reviewed</td>\n</tr>\n</tbody>\n</table>\n<p id=\"bkmrk-recovery-if-the-prod\"><strong>Recovery if the PROD tfstate is lost:</strong> see\n<a href=\"https://book.wetopi.net/operations/runbooks/tofu-prod-state-loss-and-reimport.md\">RUN-TOFU-STATE-LOSS \u2014 OpenTofu PROD state loss &amp;\nre-import recovery</a>\n(read-only re-import from the live Proxmox API, <code>tofu plan</code> = No-changes\ngate). Never reconstruct state by speculative <code>apply</code>.</p>\n<h2 id=\"bkmrk-secret-handling\">Secret handling</h2>\n<ul id=\"bkmrk-sops-%2B-age-is-bootst\">\n<li><strong>SOPS + age is bootstrap/recovery only</strong> (master plan \u00a711): it answers\n\"where do we keep the secrets needed to boot the system that keeps\nsecrets\". Two age keys \u2014 a bootstrap/automation key and an offline\nrecovery key. <strong>Private age keys never go in Git.</strong></li>\n<li><strong>The durable runtime vault is Infisical, self-hosted</strong>\n(<a href=\"https://book.wetopi.net/decisions/ADR-0009-secrets-vault-harness.md\">ADR-0009 \u2014 Secrets vault\nharness</a>); the\n<code>vault.*</code> contract + machine identities live there. SOPS+age is <em>not</em>\nthe daily vault.</li>\n<li>The rule \"secrets used, not necessarily seen\" (master plan \u00a73.7) applies:\ntool runtimes and the vault, least-privilege \u2014 no plaintext in Git,\nprompts or logs.</li>\n</ul>\n<h2 id=\"bkmrk-takeover-notes\">Takeover notes</h2>\n<ul id=\"bkmrk-before-any-tofu-comm\">\n<li>Before any <code>tofu</code> command: confirm you are in the controller dir, the\n<code>.env.proxmox</code> is sourced (0600), and no token is on disk in the tree.</li>\n<li><code>tofu plan</code> for <code>tofu/proxmox/environments/prod</code> should be a no-change\nwhen state is healthy; a plan that wants to CREATE all 28+ resources\nmeans the state is lost \u2192 run the state-loss runbook.</li>\n<li>The GitOps repo is the single source of declarative truth; a merge is a\ndeploy (Argo CD / <code>tofu</code> apply), so blast-radius review is required.</li>\n</ul>\n"
 },
 {
  "id": 38,
  "title": "proxmox cluster",
  "slug": "proxmox-cluster",
  "book_id": 2,
  "chapter_id": 12,
  "description": null,
  "created_at": "2026-09-06T13:38:54.000000Z",
  "updated_at": "2026-09-06T21:48:04.000000Z",
  "markdown": "> **ARCHIVO \u2014 versi\u00f3n migrada cruda.** La ficha estructurada con las 3 preguntas es la versi\u00f3n `ARCH-*`/`SVC-*` de este mismo chapter (actualizada con datos live 2026-09-06). Esta copia cruda se conserva por el historial.\n\n> Migrado desde `wetopi-platform/docs@main` (architecture/infrastructure/proxmox-cluster.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n ARCH-INFRA-PROXMOX: Proxmox cluster wetopi7\n\nThe RKE2 platform and all its VMs run on a single **Proxmox VE cluster**\nnamed `wetopi7`, spread over the 8 OVH dedicated servers (see\n[ovh-and-network.md](ovh-and-network.md)). OpenTofu owns the VMs and the\nnode VLAN/bridge shape through the Proxmox HTTPS API ([ADR-0003 \u2014\nOpenTofu and Ansible responsibility split](../../decisions/ADR-0003-opentofu-ansible.md)).\n\n**This pill does not restate the tfvars.** The authoritative declarative\nsource is\n[tofu/proxmox/environments/prod/](https://github.com/wetopi-platform/infrastructure/tree/main/tofu/proxmox/environments/prod)\nin the GitOps repo\n[https://github.com/wetopi-platform/infrastructure](https://github.com/wetopi-platform/infrastructure).\nThe committed `terraform.tfvars.example` documents the shape; the real\n`terraform.tfvars` is gitignored and mode 0600 (never in Git). The table\nbelow is a **point-in-time restatement of that tfvars** (verified\n2026-08-31) so a new engineer can read the inventory without the ignored\nfile; the tfvars always wins on any drift.\n\n## Cluster nodes\n\n8 Proxmox nodes (= the 8 OVH servers), each with one 802.1Q **trunk NIC**\ncarrying the vRack VLANs (vmbr3 WAN, vmbr4 node LAN, vmbr2 Ceph). Trunk\ndevices (`proxmox_nodes` in tfvars, verified live via the Proxmox API):\n\n| Node | Trunk NIC |\n|---|---|\n| ns31826925, ns31826929, ns3235424, ns3243421, ns3249468 | `enp10s0f1np1` |\n| ns3214108, ns3226931, ns3226939 | `enp2s0f1np1` |\n\n## Base template and datastores\n\n| Item | Value | Source |\n|---|---|---|\n| Base VM template | `vmssd`, **VMID 9000** on node `ns3243421` | `template_vm_id = 9000`, `template_node_name = \"ns3243421\"` |\n| VM datastore | `vmssd` (NVMe SSD) | `datastore_id` per VM |\n| Template download datastore | `local` | `template_download_datastore_id = \"local\"` |\n| Guest cloud-init identity | Ubuntu 24.04, first-boot SSH/network | template + `ssh_public_key_file` |\n\nNew VMs are **cloned from template 9000** and then converged by cloud-init\n(first boot) and Ansible (guest config) \u2014 OpenTofu sets identity/placement\nand never touches guest OS state.\n\n## VM inventory (source: prod tfvars, verified 2026-08-31)\n\nVMIDs `8001-8042`. \"Datastore\" is the VM disk location; LAN IPs are on\n`vmbr4`/`10.72.0.0/23`; WAN IPs (where present) are on `vmbr3`.\n\n| Name | VMID | Node | Role | vCPU/RAM/Disk | Datastore | LAN IP | WAN IP |\n|---|---|---|---|---|---|---|---|\n| wetopi-ops01   | 8001 | ns3249468 | operations (bastion) | 2 / 4096 MB / 40 GB | vmssd | 10.72.0.10 | 92.222.245.91 |\n| rke-cp01       | 8011 | ns3243421 | control-plane | 4 / 8192 MB / 50 GB | local | 10.72.0.21 | \u2014 |\n| rke-cp02       | 8012 | ns3226939 | control-plane | 4 / 8192 MB / 50 GB | local | 10.72.0.22 | \u2014 |\n| rke-cp03       | 8013 | ns3214108 | control-plane | 4 / 8192 MB / 50 GB | local | 10.72.0.23 | \u2014 |\n| rke-platform01 | 8021 | ns31826929 | platform worker | 4 / 8192 MB / 60 GB | vmssd | 10.72.0.41 | \u2014 |\n| rke-platform02 | 8022 | ns3226931 | platform worker | 4 / 8192 MB / 60 GB | vmssd | 10.72.0.42 | \u2014 |\n| rke-platform03 | 8023 | ns31826925 | platform worker | 4 / 8192 MB / 60 GB | vmssd | 10.72.0.43 | \u2014 |\n| wetopi-natgw01 | 8031 | ns3243421 | nat-gateway (egress) | 2 / 2048 MB / 20 GB | vmssd | 10.72.0.2 | 92.222.245.83 |\n| rke-wan01      | 8041 | ns3235424 | edge (WAN) | 2 / 4096 MB / 20 GB | vmssd | 10.72.0.51 | 79.137.114.193 |\n| rke-wan02      | 8042 | ns3226931 | edge (WAN) | 2 / 4096 MB / 20 GB | vmssd | 10.72.0.52 | 79.137.114.194 |\n\n**Roles and notes.** `wetopi-ops01` is the SSH bastion (`bastion = true`) \u2014\nthe only VM with a direct public address other than the edge/natgw; all\nother VMs are reached through it. The three control planes own the keepalived\nRKE2 API VIP `10.72.0.20` ([ADR-0004](../../decisions/ADR-0004-rke2-api-endpoint.md)).\n`wetopi-natgw01` is the LAN's NAT egress (LAN\u2192WAN forward policy only).\n`rke-wan01/02` are the edge v1 nodes ([ADR-0006](../../decisions/ADR-0006-edge-v1-wan-exception.md));\nthey were initially built as `wetopi3-wan1/2` and renamed to `rke-wan*` on\n2026-08-30 (VMIDs 8041/8042 unchanged).\n\n**Storage NICs (ADR-0007 \u2014 verified live 2026-08-31).** The three platform\nworkers additionally carry a **third NIC on `vmbr2`/VLAN 2** (the Ceph\nstorage network) with the operator-reserved addresses:\n`rke-platform01=192.168.98.10/20`, `rke-platform02=192.168.98.11/20`,\n`rke-platform03=192.168.98.12/20` \u2014 no gateway, no default route (storage\ntraffic is directly connected; see [ceph.md](ceph.md)). The control planes\nstay single-NIC in v1. This third NIC was the ADR-0007 mutating apply; it is\npresent in the live VMs and the OpenTofu state (the base table above reflects\nthe tfvars snapshot and lists only the LAN/WAN NICs).\n\n```mermaid\nflowchart TB\n  subgraph cluster[\"Proxmox cluster wetopi7 \u2014 8 nodes\"]\n    direction TB\n    subgraph nA[\"ns3249468\"]\n      ops01[\"wetopi-ops01 \u00b7 8001 \u00b7 bastion\"]\n    end\n    subgraph nB[\"ns3243421\"]\n      cp01[\"rke-cp01 \u00b7 8011 \u00b7 control-plane\"]\n      natgw[\"wetopi-natgw01 \u00b7 8031 \u00b7 NAT egress\"]\n      tpl[\"base template vmssd \u00b7 VMID 9000\"]\n    end\n    subgraph nC[\"ns3226939\"]\n      cp02[\"rke-cp02 \u00b7 8012 \u00b7 control-plane\"]\n    end\n    subgraph nD[\"ns3214108\"]\n      cp03[\"rke-cp03 \u00b7 8013 \u00b7 control-plane\"]\n    end\n    subgraph nE[\"ns31826929\"]\n      p01[\"rke-platform01 \u00b7 8021 \u00b7 worker (+Ceph NIC)\"]\n    end\n    subgraph nF[\"ns3226931\"]\n      p02[\"rke-platform02 \u00b7 8022 \u00b7 worker (+Ceph NIC)\"]\n      wan2[\"rke-wan02 \u00b7 8042 \u00b7 edge\"]\n    end\n    subgraph nG[\"ns31826925\"]\n      p03[\"rke-platform03 \u00b7 8023 \u00b7 worker (+Ceph NIC)\"]\n    end\n    subgraph nH[\"ns3235424\"]\n      wan1[\"rke-wan01 \u00b7 8041 \u00b7 edge\"]\n    end\n  end\n  ops01 -.->|\"SSH bastion to all VMs\"| cp01\n```\n\n## Takeover notes\n\n- Re-verify with `tofu plan` on `tofu/proxmox/environments/prod` (no-changes\n  expected) and `qemu-pending`/`qemu-status` per VM via the Proxmox API or\n  ops01.\n- The `ops01` node `ns3249468` also hosts the Proxmox API endpoint used by\n  OpenTofu (`PROXMOX_VE_ENDPOINT`); see\n  [providers.md](providers.md).\n- VMs must never be renumbered; VMID + node placement is stable identity.\n  Sizing is deliberately minimal and expands after observation\n  ([ADR-0002 \u2014 Minimal expandable bootstrap\n  sizing](../../decisions/ADR-0002-bootstrap-sizing.md)).",
  "html": "<blockquote id=\"bkmrk-archivo-%E2%80%94-versi%C3%B3n-mi\">\n<p><strong>ARCHIVO \u2014 versi\u00f3n migrada cruda.</strong> La ficha estructurada con las 3 preguntas es la versi\u00f3n <code>ARCH-*</code>/<code>SVC-*</code> de este mismo chapter (actualizada con datos live 2026-09-06). Esta copia cruda se conserva por el historial.</p>\n</blockquote>\n<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (architecture/infrastructure/proxmox-cluster.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-arch-infra-proxmox%3A-\">ARCH-INFRA-PROXMOX: Proxmox cluster wetopi7</p>\n<p id=\"bkmrk-the-rke2-platform-an\">The RKE2 platform and all its VMs run on a single <strong>Proxmox VE cluster</strong>\nnamed <code>wetopi7</code>, spread over the 8 OVH dedicated servers (see\n<a href=\"https://book.wetopi.net/ovh-and-network.md\">ovh-and-network.md</a>). OpenTofu owns the VMs and the\nnode VLAN/bridge shape through the Proxmox HTTPS API (<a href=\"https://book.wetopi.net/decisions/ADR-0003-opentofu-ansible.md\">ADR-0003 \u2014\nOpenTofu and Ansible responsibility split</a>).</p>\n<p id=\"bkmrk-this-pill-does-not-r\"><strong>This pill does not restate the tfvars.</strong> The authoritative declarative\nsource is\n<a href=\"https://github.com/wetopi-platform/infrastructure/tree/main/tofu/proxmox/environments/prod\">tofu/proxmox/environments/prod/</a>\nin the GitOps repo\n<a href=\"https://github.com/wetopi-platform/infrastructure\">https://github.com/wetopi-platform/infrastructure</a>.\nThe committed <code>terraform.tfvars.example</code> documents the shape; the real\n<code>terraform.tfvars</code> is gitignored and mode 0600 (never in Git). The table\nbelow is a <strong>point-in-time restatement of that tfvars</strong> (verified\n2026-08-31) so a new engineer can read the inventory without the ignored\nfile; the tfvars always wins on any drift.</p>\n<h2 id=\"bkmrk-cluster-nodes\">Cluster nodes</h2>\n<p id=\"bkmrk-8-proxmox-nodes-%28%3D-t\">8 Proxmox nodes (= the 8 OVH servers), each with one 802.1Q <strong>trunk NIC</strong>\ncarrying the vRack VLANs (vmbr3 WAN, vmbr4 node LAN, vmbr2 Ceph). Trunk\ndevices (<code>proxmox_nodes</code> in tfvars, verified live via the Proxmox API):</p>\n<table id=\"bkmrk-node-trunk-nic-ns318\">\n<thead>\n<tr>\n<th>Node</th>\n<th>Trunk NIC</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>ns31826925, ns31826929, ns3235424, ns3243421, ns3249468</td>\n<td><code>enp10s0f1np1</code></td>\n</tr>\n<tr>\n<td>ns3214108, ns3226931, ns3226939</td>\n<td><code>enp2s0f1np1</code></td>\n</tr>\n</tbody>\n</table>\n<h2 id=\"bkmrk-base-template-and-da\">Base template and datastores</h2>\n<table id=\"bkmrk-item-value-source-ba\">\n<thead>\n<tr>\n<th>Item</th>\n<th>Value</th>\n<th>Source</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Base VM template</td>\n<td><code>vmssd</code>, <strong>VMID 9000</strong> on node <code>ns3243421</code></td>\n<td><code>template_vm_id = 9000</code>, <code>template_node_name = \"ns3243421\"</code></td>\n</tr>\n<tr>\n<td>VM datastore</td>\n<td><code>vmssd</code> (NVMe SSD)</td>\n<td><code>datastore_id</code> per VM</td>\n</tr>\n<tr>\n<td>Template download datastore</td>\n<td><code>local</code></td>\n<td><code>template_download_datastore_id = \"local\"</code></td>\n</tr>\n<tr>\n<td>Guest cloud-init identity</td>\n<td>Ubuntu 24.04, first-boot SSH/network</td>\n<td>template + <code>ssh_public_key_file</code></td>\n</tr>\n</tbody>\n</table>\n<p id=\"bkmrk-new-vms-are-cloned-f\">New VMs are <strong>cloned from template 9000</strong> and then converged by cloud-init\n(first boot) and Ansible (guest config) \u2014 OpenTofu sets identity/placement\nand never touches guest OS state.</p>\n<h2 id=\"bkmrk-vm-inventory-%28source\">VM inventory (source: prod tfvars, verified 2026-08-31)</h2>\n<p id=\"bkmrk-vmids-8001-8042.-%22da\">VMIDs <code>8001-8042</code>. \"Datastore\" is the VM disk location; LAN IPs are on\n<code>vmbr4</code>/<code>10.72.0.0/23</code>; WAN IPs (where present) are on <code>vmbr3</code>.</p>\n<table id=\"bkmrk-name-vmid-node-role-\">\n<thead>\n<tr>\n<th>Name</th>\n<th>VMID</th>\n<th>Node</th>\n<th>Role</th>\n<th>vCPU/RAM/Disk</th>\n<th>Datastore</th>\n<th>LAN IP</th>\n<th>WAN IP</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>wetopi-ops01</td>\n<td>8001</td>\n<td>ns3249468</td>\n<td>operations (bastion)</td>\n<td>2 / 4096 MB / 40 GB</td>\n<td>vmssd</td>\n<td>10.72.0.10</td>\n<td>92.222.245.91</td>\n</tr>\n<tr>\n<td>rke-cp01</td>\n<td>8011</td>\n<td>ns3243421</td>\n<td>control-plane</td>\n<td>4 / 8192 MB / 50 GB</td>\n<td>local</td>\n<td>10.72.0.21</td>\n<td>\u2014</td>\n</tr>\n<tr>\n<td>rke-cp02</td>\n<td>8012</td>\n<td>ns3226939</td>\n<td>control-plane</td>\n<td>4 / 8192 MB / 50 GB</td>\n<td>local</td>\n<td>10.72.0.22</td>\n<td>\u2014</td>\n</tr>\n<tr>\n<td>rke-cp03</td>\n<td>8013</td>\n<td>ns3214108</td>\n<td>control-plane</td>\n<td>4 / 8192 MB / 50 GB</td>\n<td>local</td>\n<td>10.72.0.23</td>\n<td>\u2014</td>\n</tr>\n<tr>\n<td>rke-platform01</td>\n<td>8021</td>\n<td>ns31826929</td>\n<td>platform worker</td>\n<td>4 / 8192 MB / 60 GB</td>\n<td>vmssd</td>\n<td>10.72.0.41</td>\n<td>\u2014</td>\n</tr>\n<tr>\n<td>rke-platform02</td>\n<td>8022</td>\n<td>ns3226931</td>\n<td>platform worker</td>\n<td>4 / 8192 MB / 60 GB</td>\n<td>vmssd</td>\n<td>10.72.0.42</td>\n<td>\u2014</td>\n</tr>\n<tr>\n<td>rke-platform03</td>\n<td>8023</td>\n<td>ns31826925</td>\n<td>platform worker</td>\n<td>4 / 8192 MB / 60 GB</td>\n<td>vmssd</td>\n<td>10.72.0.43</td>\n<td>\u2014</td>\n</tr>\n<tr>\n<td>wetopi-natgw01</td>\n<td>8031</td>\n<td>ns3243421</td>\n<td>nat-gateway (egress)</td>\n<td>2 / 2048 MB / 20 GB</td>\n<td>vmssd</td>\n<td>10.72.0.2</td>\n<td>92.222.245.83</td>\n</tr>\n<tr>\n<td>rke-wan01</td>\n<td>8041</td>\n<td>ns3235424</td>\n<td>edge (WAN)</td>\n<td>2 / 4096 MB / 20 GB</td>\n<td>vmssd</td>\n<td>10.72.0.51</td>\n<td>79.137.114.193</td>\n</tr>\n<tr>\n<td>rke-wan02</td>\n<td>8042</td>\n<td>ns3226931</td>\n<td>edge (WAN)</td>\n<td>2 / 4096 MB / 20 GB</td>\n<td>vmssd</td>\n<td>10.72.0.52</td>\n<td>79.137.114.194</td>\n</tr>\n</tbody>\n</table>\n<p id=\"bkmrk-roles-and-notes.-wet\"><strong>Roles and notes.</strong> <code>wetopi-ops01</code> is the SSH bastion (<code>bastion = true</code>) \u2014\nthe only VM with a direct public address other than the edge/natgw; all\nother VMs are reached through it. The three control planes own the keepalived\nRKE2 API VIP <code>10.72.0.20</code> (<a href=\"https://book.wetopi.net/decisions/ADR-0004-rke2-api-endpoint.md\">ADR-0004</a>).\n<code>wetopi-natgw01</code> is the LAN's NAT egress (LAN\u2192WAN forward policy only).\n<code>rke-wan01/02</code> are the edge v1 nodes (<a href=\"https://book.wetopi.net/decisions/ADR-0006-edge-v1-wan-exception.md\">ADR-0006</a>);\nthey were initially built as <code>wetopi3-wan1/2</code> and renamed to <code>rke-wan*</code> on\n2026-08-30 (VMIDs 8041/8042 unchanged).</p>\n<p id=\"bkmrk-storage-nics-%28adr-00\"><strong>Storage NICs (ADR-0007 \u2014 verified live 2026-08-31).</strong> The three platform\nworkers additionally carry a <strong>third NIC on <code>vmbr2</code>/VLAN 2</strong> (the Ceph\nstorage network) with the operator-reserved addresses:\n<code>rke-platform01=192.168.98.10/20</code>, <code>rke-platform02=192.168.98.11/20</code>,\n<code>rke-platform03=192.168.98.12/20</code> \u2014 no gateway, no default route (storage\ntraffic is directly connected; see <a href=\"https://book.wetopi.net/ceph.md\">ceph.md</a>). The control planes\nstay single-NIC in v1. This third NIC was the ADR-0007 mutating apply; it is\npresent in the live VMs and the OpenTofu state (the base table above reflects\nthe tfvars snapshot and lists only the LAN/WAN NICs).</p>\n<pre id=\"bkmrk-flowchart-tb-subgrap\"><code class=\"language-mermaid\">flowchart TB\n  subgraph cluster[\"Proxmox cluster wetopi7 \u2014 8 nodes\"]\n    direction TB\n    subgraph nA[\"ns3249468\"]\n      ops01[\"wetopi-ops01 \u00b7 8001 \u00b7 bastion\"]\n    end\n    subgraph nB[\"ns3243421\"]\n      cp01[\"rke-cp01 \u00b7 8011 \u00b7 control-plane\"]\n      natgw[\"wetopi-natgw01 \u00b7 8031 \u00b7 NAT egress\"]\n      tpl[\"base template vmssd \u00b7 VMID 9000\"]\n    end\n    subgraph nC[\"ns3226939\"]\n      cp02[\"rke-cp02 \u00b7 8012 \u00b7 control-plane\"]\n    end\n    subgraph nD[\"ns3214108\"]\n      cp03[\"rke-cp03 \u00b7 8013 \u00b7 control-plane\"]\n    end\n    subgraph nE[\"ns31826929\"]\n      p01[\"rke-platform01 \u00b7 8021 \u00b7 worker (+Ceph NIC)\"]\n    end\n    subgraph nF[\"ns3226931\"]\n      p02[\"rke-platform02 \u00b7 8022 \u00b7 worker (+Ceph NIC)\"]\n      wan2[\"rke-wan02 \u00b7 8042 \u00b7 edge\"]\n    end\n    subgraph nG[\"ns31826925\"]\n      p03[\"rke-platform03 \u00b7 8023 \u00b7 worker (+Ceph NIC)\"]\n    end\n    subgraph nH[\"ns3235424\"]\n      wan1[\"rke-wan01 \u00b7 8041 \u00b7 edge\"]\n    end\n  end\n  ops01 -.-&gt;|\"SSH bastion to all VMs\"| cp01\n</code></pre>\n<h2 id=\"bkmrk-takeover-notes\">Takeover notes</h2>\n<ul id=\"bkmrk-re-verify-with-tofu-\">\n<li>Re-verify with <code>tofu plan</code> on <code>tofu/proxmox/environments/prod</code> (no-changes\nexpected) and <code>qemu-pending</code>/<code>qemu-status</code> per VM via the Proxmox API or\nops01.</li>\n<li>The <code>ops01</code> node <code>ns3249468</code> also hosts the Proxmox API endpoint used by\nOpenTofu (<code>PROXMOX_VE_ENDPOINT</code>); see\n<a href=\"https://book.wetopi.net/providers.md\">providers.md</a>.</li>\n<li>VMs must never be renumbered; VMID + node placement is stable identity.\nSizing is deliberately minimal and expands after observation\n(<a href=\"https://book.wetopi.net/decisions/ADR-0002-bootstrap-sizing.md\">ADR-0002 \u2014 Minimal expandable bootstrap\nsizing</a>).</li>\n</ul>\n"
 },
 {
  "id": 39,
  "title": "SVC-PLAT \u2014 Topolog\u00eda general",
  "slug": "svc-plat-topologia-general",
  "book_id": 3,
  "chapter_id": 16,
  "description": null,
  "created_at": "2026-09-06T13:38:54.000000Z",
  "updated_at": "2026-09-06T21:45:52.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (architecture/overview-topology.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n ARCH-PLATFORM-TOPOLOGY: Platform topology \u2014 global three-layer map\n\nThe handover map: **one diagram, three layers, every node linked to its\npill.** This is the page a new technician reads first; from here they\nnavigate the entire platform \u2014 physical infrastructure, the K8s platform\nlayer, and the deployed services \u2014 without guessing where anything lives.\n\n## How to read this map\n\n- **Bottom-up is the dependency direction**: infrastructure hosts the\n  platform; the platform hosts the services. Read the layers in order\n  (infra \u2192 platform \u2192 services) when taking over.\n- **Every node is clickable** (on the rendered site) and is listed in the\n  node\u2192pill table below with its relative link. The pills carry the depth;\n  this page carries the shape.\n- **Dated facts only**: the \"verified state\" section below is a snapshot as\n  of `last_verified`. Live state always wins (DocOps \u00a71) \u2014 re-read it with\n  `kubectl` before relying on it.\n- What is NOT here: decision rationale (\u2192 [decisions](../decisions/index.md)),\n  how to operate (\u2192 [operations](../operations/index.md)), and the handover\n  procedure itself (\u2192 [taking-over-the-platform.md](../project/taking-over-the-platform.md)).\n\n## The global diagram\n\n```mermaid\nflowchart TB\n  subgraph L1[\"Layer 1 \u2014 INFRASTRUCTURE (OVH \u00b7 Proxmox \u00b7 Ceph)\"]\n    direction TB\n    OVH[\"OVH \u2014 8 dedicated servers \u00b7 one vRack\"]\n    PX[\"Proxmox VE cluster wetopi7 \u2014 8 nodes \u00b7 VMs 8001-8042\"]\n    IAC[\"IaC \u2014 OpenTofu (bpg) + Cloudflare + Ansible \u00b7 state 0600, never in Git\"]\n    BAST[\"wetopi-ops01 \u2014 SSH bastion \u00b7 92.222.245.91\"]\n    NAT[\"natgw \u2014 LAN egress \u00b7 92.222.245.83\"]\n    CEPH[\"Ceph LAN 192.168.96.0/20 \u2014 VLAN 2 \u00b7 pre-existing\"]\n    OVH --> PX\n    OVH --> BAST\n    OVH --> NAT\n    IAC -.->|\"Proxmox HTTPS API\"| PX\n  end\n  subgraph L2[\"Layer 2 \u2014 PLATFORM (RKE2 \u00b7 Cilium \u00b7 Edge \u00b7 GitOps)\"]\n    direction TB\n    K8S[\"RKE2 cluster wetopi3 \u2014 3 CP + 3 platform + 2 edge workers \u00b7 API VIP 10.72.0.20\"]\n    CILIUM[\"Cilium CNI + Hubble (no Canal \u2014 ADR-0005)\"]\n    EDGE[\"WAN edge v1 \u2014 Traefik on rke-wan01/02 \u00b7 Gateway wetopi-edge\"]\n    GITOPS[\"GitOps \u2014 Argo CD v3.4.8 \u00b7 AppProject platform\"]\n    K8S --- CILIUM\n    K8S --- EDGE\n    K8S --- GITOPS\n  end\n  subgraph L3[\"Layer 3 \u2014 SERVICES (deployed via GitOps)\"]\n    direction LR\n    SVCARGO[\"Argo CD \u2014 argo.wetopi.net\"]\n    SVCVAULT[\"Vault / Infisical \u2014 vault.wetopi.net\"]\n    SVCTRAEFIK[\"Traefik edge \u2014 public routing\"]\n    SVCCSI[\"Ceph CSI \u2014 StorageClass ceph-rbd\"]\n    SVCCERT[\"cert-manager \u2014 Let's Encrypt DNS-01\"]\n    SVODOCS[\"Docs site \u2014 docs.wetopi.net\"]\n  end\n  PX -->|\"VMs 8001-8042 host the nodes\"| K8S\n  BAST -->|\"SSH + kubectl (kubeconfig from rke-cp01)\"| K8S\n  CEPH -->|\"workers eth1 \u00b7 ADR-0007\"| SVCCSI\n  GITOPS -->|\"syncs kubernetes/ tree\"| L3\n  EDGE --- SVCTRAEFIK\n  SVCTRAEFIK --- SVCARGO\n  SVCTRAEFIK --- SVCVAULT\n  SVCTRAEFIK --- SVODOCS\n  SVCCERT -->|\"argo-tls \u00b7 docs-tls \u00b7 vault-tls\"| EDGE\n  SVCCSI -->|\"PVCs on ceph-rbd\"| SVCVAULT\n\n  click L1 \"infrastructure section map\" \"infrastructure/index.html\"\n  click OVH \"ARCH-INFRA-OVH\" \"infrastructure/ovh-and-network.html\"\n  click PX \"ARCH-INFRA-PROXMOX\" \"infrastructure/proxmox-cluster.html\"\n  click IAC \"ARCH-INFRA-PROVIDERS\" \"infrastructure/providers.html\"\n  click BAST \"bastion ops01 (in ARCH-INFRA-OVH)\" \"infrastructure/ovh-and-network.html\"\n  click NAT \"natgw (in ARCH-INFRA-OVH)\" \"infrastructure/ovh-and-network.html\"\n  click CEPH \"ARCH-INFRA-CEPH\" \"infrastructure/ceph.html\"\n  click L2 \"platform section map\" \"platform/index.html\"\n  click K8S \"ARCH-PLATFORM-RKE2\" \"platform/rke2-cluster.html\"\n  click CILIUM \"ARCH-PLATFORM-CILIUM\" \"platform/cilium-network.html\"\n  click EDGE \"ARCH-PLATFORM-EDGE\" \"platform/edge.html\"\n  click GITOPS \"ARCH-PLATFORM-GITOPS\" \"platform/gitops.html\"\n  click SVCARGO \"SVC-ARGOCD\" \"../services/argocd.html\"\n  click SVCVAULT \"SVC-VAULT\" \"../services/vault.html\"\n  click SVCTRAEFIK \"SVC-TRAEFIK-EDGE\" \"../services/traefik-edge.html\"\n  click SVCCSI \"SVC-CEPH-CSI\" \"../services/ceph-csi.html\"\n  click SVCCERT \"SVC-CERT-MANAGER\" \"../services/cert-manager.html\"\n  click SVODOCS \"SVC-DOCS-SITE\" \"../services/docs-site.html\"\n```\n\n## Node \u2192 pill map\n\nEvery node of the diagram resolves to the pill below. Relative links work\nfrom this page; the rendered site mirrors the same mapping.\n\n| Layer | Node | Pill | What it covers |\n|---|---|---|---|\n| 1 \u2014 infra | OVH, bastion (ops01), natgw | [ARCH-INFRA-OVH](infrastructure/ovh-and-network.md) | Provider, vRack, WAN blocks, VLAN/bridge model, Cloudflare DNS. |\n| 1 \u2014 infra | Proxmox cluster wetopi7 | [ARCH-INFRA-PROXMOX](infrastructure/proxmox-cluster.md) | 8 nodes, template, datastores, VM inventory (8001\u20138042). |\n| 1 \u2014 infra | IaC toolchain | [ARCH-INFRA-PROVIDERS](infrastructure/providers.md) | OpenTofu/Cloudflare/Ansible split, where state lives, secret handling. |\n| 1 \u2014 infra | Ceph LAN | [ARCH-INFRA-CEPH](infrastructure/ceph.md) | Pre-existing storage LAN, storage-native access (ADR-0007). |\n| 2 \u2014 platform | RKE2 cluster wetopi3 | [ARCH-PLATFORM-RKE2](platform/rke2-cluster.md) | Topology, control plane, API VIP, node roles. |\n| 2 \u2014 platform | Cilium | [ARCH-PLATFORM-CILIUM](platform/cilium-network.md) | CNI, Hubble, edge-node datapath. |\n| 2 \u2014 platform | WAN edge v1 | [ARCH-PLATFORM-EDGE](platform/edge.md) | Direct Traefik exposure, Gateway API, nftables posture. |\n| 2 \u2014 platform | GitOps plane | [ARCH-PLATFORM-GITOPS](platform/gitops.md) | Argo CD bootstrap, AppProject, app layout. |\n| 3 \u2014 services | Argo CD | [SVC-ARGOCD](../services/argocd.md) | GitOps control plane, argo.wetopi.net. |\n| 3 \u2014 services | Vault / Infisical | [SVC-VAULT](../services/vault.md) | Secrets system, vault.wetopi.net UI, Universal Auth (private). |\n| 3 \u2014 services | Traefik edge | [SVC-TRAEFIK-EDGE](../services/traefik-edge.md) | Public routing for docs/argo/vault hostnames. |\n| 3 \u2014 services | Ceph CSI | [SVC-CEPH-CSI](../services/ceph-csi.md) | RBD storage, StorageClass `ceph-rbd`. |\n| 3 \u2014 services | cert-manager | [SVC-CERT-MANAGER](../services/cert-manager.md) | ACME/Let's Encrypt DNS-01, the edge certificates. |\n| 3 \u2014 services | Docs site | [SVC-DOCS-SITE](../services/docs-site.md) | docs.wetopi.net, the site you are reading. |\n\nSection entry points: [infrastructure section](infrastructure/index.md) \u00b7\n[platform section](platform/index.md) \u00b7 [services section](../services/index.md) \u00b7\n[decisions](../decisions/index.md) \u00b7 [runbooks](../operations/runbooks/index.md).\n\n## Verified state (2026-08-31, live via ops01)\n\nDated handover snapshot \u2014 re-read live before relying on it:\n\n- **Layer 1**: 8/8 Proxmox nodes operational; VMs 8001\u20138042 carrying the\n  platform (VM table: [ARCH-INFRA-PROXMOX](infrastructure/proxmox-cluster.md)).\n- **Layer 2**: 8/8 cluster nodes Ready (3 CP / 3 platform / 2 edge);\n  Gateway `wetopi-edge` Accepted+Programmed with the `web` :8000 and\n  `https` :8443 listeners.\n- **Layer 3**: five Argo Applications \u2014 `root`, `cert-manager`, `docs-site`,\n  `argo-edge` (all Healthy/Synced) and `vault` (Synced; its `Degraded`\n  health is the documented **false positive** of the fail-closed deny\n  rules \u2014 pods 3/3 Running, see [SVC-VAULT](../services/vault.md));\n  4 certificates Ready (`argo-tls`, `docs-tls`, `vault-tls`, `edge-test-tls`).\n\n## Taking over\n\nThis map is the *shape*. The *procedure* is\n[taking-over-the-platform.md](../project/taking-over-the-platform.md)\n(PROC-PLATFORM-TAKEOVER): access by location, sources of truth, day-1\nverification, frequent operations, escalation and the no-go rules.",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (architecture/overview-topology.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-arch-platform-topolo\">ARCH-PLATFORM-TOPOLOGY: Platform topology \u2014 global three-layer map</p>\n<p id=\"bkmrk-the-handover-map%3A-on\">The handover map: <strong>one diagram, three layers, every node linked to its\npill.</strong> This is the page a new technician reads first; from here they\nnavigate the entire platform \u2014 physical infrastructure, the K8s platform\nlayer, and the deployed services \u2014 without guessing where anything lives.</p>\n<h2 id=\"bkmrk-how-to-read-this-map\">How to read this map</h2>\n<ul id=\"bkmrk-bottom-up-is-the-dep\">\n<li><strong>Bottom-up is the dependency direction</strong>: infrastructure hosts the\nplatform; the platform hosts the services. Read the layers in order\n(infra \u2192 platform \u2192 services) when taking over.</li>\n<li><strong>Every node is clickable</strong> (on the rendered site) and is listed in the\nnode\u2192pill table below with its relative link. The pills carry the depth;\nthis page carries the shape.</li>\n<li><strong>Dated facts only</strong>: the \"verified state\" section below is a snapshot as\nof <code>last_verified</code>. Live state always wins (DocOps \u00a71) \u2014 re-read it with\n<code>kubectl</code> before relying on it.</li>\n<li>What is NOT here: decision rationale (\u2192 <a href=\"https://book.wetopi.net/decisions/index.md\">decisions</a>),\nhow to operate (\u2192 <a href=\"https://book.wetopi.net/operations/index.md\">operations</a>), and the handover\nprocedure itself (\u2192 <a href=\"https://book.wetopi.net/project/taking-over-the-platform.md\">taking-over-the-platform.md</a>).</li>\n</ul>\n<h2 id=\"bkmrk-the-global-diagram\">The global diagram</h2>\n<pre id=\"bkmrk-flowchart-tb-subgrap\"><code class=\"language-mermaid\">flowchart TB\n  subgraph L1[\"Layer 1 \u2014 INFRASTRUCTURE (OVH \u00b7 Proxmox \u00b7 Ceph)\"]\n    direction TB\n    OVH[\"OVH \u2014 8 dedicated servers \u00b7 one vRack\"]\n    PX[\"Proxmox VE cluster wetopi7 \u2014 8 nodes \u00b7 VMs 8001-8042\"]\n    IAC[\"IaC \u2014 OpenTofu (bpg) + Cloudflare + Ansible \u00b7 state 0600, never in Git\"]\n    BAST[\"wetopi-ops01 \u2014 SSH bastion \u00b7 92.222.245.91\"]\n    NAT[\"natgw \u2014 LAN egress \u00b7 92.222.245.83\"]\n    CEPH[\"Ceph LAN 192.168.96.0/20 \u2014 VLAN 2 \u00b7 pre-existing\"]\n    OVH --&gt; PX\n    OVH --&gt; BAST\n    OVH --&gt; NAT\n    IAC -.-&gt;|\"Proxmox HTTPS API\"| PX\n  end\n  subgraph L2[\"Layer 2 \u2014 PLATFORM (RKE2 \u00b7 Cilium \u00b7 Edge \u00b7 GitOps)\"]\n    direction TB\n    K8S[\"RKE2 cluster wetopi3 \u2014 3 CP + 3 platform + 2 edge workers \u00b7 API VIP 10.72.0.20\"]\n    CILIUM[\"Cilium CNI + Hubble (no Canal \u2014 ADR-0005)\"]\n    EDGE[\"WAN edge v1 \u2014 Traefik on rke-wan01/02 \u00b7 Gateway wetopi-edge\"]\n    GITOPS[\"GitOps \u2014 Argo CD v3.4.8 \u00b7 AppProject platform\"]\n    K8S --- CILIUM\n    K8S --- EDGE\n    K8S --- GITOPS\n  end\n  subgraph L3[\"Layer 3 \u2014 SERVICES (deployed via GitOps)\"]\n    direction LR\n    SVCARGO[\"Argo CD \u2014 argo.wetopi.net\"]\n    SVCVAULT[\"Vault / Infisical \u2014 vault.wetopi.net\"]\n    SVCTRAEFIK[\"Traefik edge \u2014 public routing\"]\n    SVCCSI[\"Ceph CSI \u2014 StorageClass ceph-rbd\"]\n    SVCCERT[\"cert-manager \u2014 Let's Encrypt DNS-01\"]\n    SVODOCS[\"Docs site \u2014 docs.wetopi.net\"]\n  end\n  PX --&gt;|\"VMs 8001-8042 host the nodes\"| K8S\n  BAST --&gt;|\"SSH + kubectl (kubeconfig from rke-cp01)\"| K8S\n  CEPH --&gt;|\"workers eth1 \u00b7 ADR-0007\"| SVCCSI\n  GITOPS --&gt;|\"syncs kubernetes/ tree\"| L3\n  EDGE --- SVCTRAEFIK\n  SVCTRAEFIK --- SVCARGO\n  SVCTRAEFIK --- SVCVAULT\n  SVCTRAEFIK --- SVODOCS\n  SVCCERT --&gt;|\"argo-tls \u00b7 docs-tls \u00b7 vault-tls\"| EDGE\n  SVCCSI --&gt;|\"PVCs on ceph-rbd\"| SVCVAULT\n\n  click L1 \"infrastructure section map\" \"infrastructure/index.html\"\n  click OVH \"ARCH-INFRA-OVH\" \"infrastructure/ovh-and-network.html\"\n  click PX \"ARCH-INFRA-PROXMOX\" \"infrastructure/proxmox-cluster.html\"\n  click IAC \"ARCH-INFRA-PROVIDERS\" \"infrastructure/providers.html\"\n  click BAST \"bastion ops01 (in ARCH-INFRA-OVH)\" \"infrastructure/ovh-and-network.html\"\n  click NAT \"natgw (in ARCH-INFRA-OVH)\" \"infrastructure/ovh-and-network.html\"\n  click CEPH \"ARCH-INFRA-CEPH\" \"infrastructure/ceph.html\"\n  click L2 \"platform section map\" \"platform/index.html\"\n  click K8S \"ARCH-PLATFORM-RKE2\" \"platform/rke2-cluster.html\"\n  click CILIUM \"ARCH-PLATFORM-CILIUM\" \"platform/cilium-network.html\"\n  click EDGE \"ARCH-PLATFORM-EDGE\" \"platform/edge.html\"\n  click GITOPS \"ARCH-PLATFORM-GITOPS\" \"platform/gitops.html\"\n  click SVCARGO \"SVC-ARGOCD\" \"../services/argocd.html\"\n  click SVCVAULT \"SVC-VAULT\" \"../services/vault.html\"\n  click SVCTRAEFIK \"SVC-TRAEFIK-EDGE\" \"../services/traefik-edge.html\"\n  click SVCCSI \"SVC-CEPH-CSI\" \"../services/ceph-csi.html\"\n  click SVCCERT \"SVC-CERT-MANAGER\" \"../services/cert-manager.html\"\n  click SVODOCS \"SVC-DOCS-SITE\" \"../services/docs-site.html\"\n</code></pre>\n<h2 id=\"bkmrk-node-%E2%86%92-pill-map\">Node \u2192 pill map</h2>\n<p id=\"bkmrk-every-node-of-the-di\">Every node of the diagram resolves to the pill below. Relative links work\nfrom this page; the rendered site mirrors the same mapping.</p>\n<table id=\"bkmrk-layer-node-pill-what\">\n<thead>\n<tr>\n<th>Layer</th>\n<th>Node</th>\n<th>Pill</th>\n<th>What it covers</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>1 \u2014 infra</td>\n<td>OVH, bastion (ops01), natgw</td>\n<td><a href=\"https://book.wetopi.net/infrastructure/ovh-and-network.md\">ARCH-INFRA-OVH</a></td>\n<td>Provider, vRack, WAN blocks, VLAN/bridge model, Cloudflare DNS.</td>\n</tr>\n<tr>\n<td>1 \u2014 infra</td>\n<td>Proxmox cluster wetopi7</td>\n<td><a href=\"https://book.wetopi.net/infrastructure/proxmox-cluster.md\">ARCH-INFRA-PROXMOX</a></td>\n<td>8 nodes, template, datastores, VM inventory (8001\u20138042).</td>\n</tr>\n<tr>\n<td>1 \u2014 infra</td>\n<td>IaC toolchain</td>\n<td><a href=\"https://book.wetopi.net/infrastructure/providers.md\">ARCH-INFRA-PROVIDERS</a></td>\n<td>OpenTofu/Cloudflare/Ansible split, where state lives, secret handling.</td>\n</tr>\n<tr>\n<td>1 \u2014 infra</td>\n<td>Ceph LAN</td>\n<td><a href=\"https://book.wetopi.net/infrastructure/ceph.md\">ARCH-INFRA-CEPH</a></td>\n<td>Pre-existing storage LAN, storage-native access (ADR-0007).</td>\n</tr>\n<tr>\n<td>2 \u2014 platform</td>\n<td>RKE2 cluster wetopi3</td>\n<td><a href=\"https://book.wetopi.net/platform/rke2-cluster.md\">ARCH-PLATFORM-RKE2</a></td>\n<td>Topology, control plane, API VIP, node roles.</td>\n</tr>\n<tr>\n<td>2 \u2014 platform</td>\n<td>Cilium</td>\n<td><a href=\"https://book.wetopi.net/platform/cilium-network.md\">ARCH-PLATFORM-CILIUM</a></td>\n<td>CNI, Hubble, edge-node datapath.</td>\n</tr>\n<tr>\n<td>2 \u2014 platform</td>\n<td>WAN edge v1</td>\n<td><a href=\"https://book.wetopi.net/platform/edge.md\">ARCH-PLATFORM-EDGE</a></td>\n<td>Direct Traefik exposure, Gateway API, nftables posture.</td>\n</tr>\n<tr>\n<td>2 \u2014 platform</td>\n<td>GitOps plane</td>\n<td><a href=\"https://book.wetopi.net/platform/gitops.md\">ARCH-PLATFORM-GITOPS</a></td>\n<td>Argo CD bootstrap, AppProject, app layout.</td>\n</tr>\n<tr>\n<td>3 \u2014 services</td>\n<td>Argo CD</td>\n<td><a href=\"https://book.wetopi.net/services/argocd.md\">SVC-ARGOCD</a></td>\n<td>GitOps control plane, argo.wetopi.net.</td>\n</tr>\n<tr>\n<td>3 \u2014 services</td>\n<td>Vault / Infisical</td>\n<td><a href=\"https://book.wetopi.net/services/vault.md\">SVC-VAULT</a></td>\n<td>Secrets system, vault.wetopi.net UI, Universal Auth (private).</td>\n</tr>\n<tr>\n<td>3 \u2014 services</td>\n<td>Traefik edge</td>\n<td><a href=\"https://book.wetopi.net/services/traefik-edge.md\">SVC-TRAEFIK-EDGE</a></td>\n<td>Public routing for docs/argo/vault hostnames.</td>\n</tr>\n<tr>\n<td>3 \u2014 services</td>\n<td>Ceph CSI</td>\n<td><a href=\"https://book.wetopi.net/services/ceph-csi.md\">SVC-CEPH-CSI</a></td>\n<td>RBD storage, StorageClass <code>ceph-rbd</code>.</td>\n</tr>\n<tr>\n<td>3 \u2014 services</td>\n<td>cert-manager</td>\n<td><a href=\"https://book.wetopi.net/services/cert-manager.md\">SVC-CERT-MANAGER</a></td>\n<td>ACME/Let's Encrypt DNS-01, the edge certificates.</td>\n</tr>\n<tr>\n<td>3 \u2014 services</td>\n<td>Docs site</td>\n<td><a href=\"https://book.wetopi.net/services/docs-site.md\">SVC-DOCS-SITE</a></td>\n<td>docs.wetopi.net, the site you are reading.</td>\n</tr>\n</tbody>\n</table>\n<p id=\"bkmrk-section-entry-points\">Section entry points: <a href=\"https://book.wetopi.net/infrastructure/index.md\">infrastructure section</a> \u00b7\n<a href=\"https://book.wetopi.net/platform/index.md\">platform section</a> \u00b7 <a href=\"https://book.wetopi.net/services/index.md\">services section</a> \u00b7\n<a href=\"https://book.wetopi.net/decisions/index.md\">decisions</a> \u00b7 <a href=\"https://book.wetopi.net/operations/runbooks/index.md\">runbooks</a>.</p>\n<h2 id=\"bkmrk-verified-state-%282026\">Verified state (2026-08-31, live via ops01)</h2>\n<p id=\"bkmrk-dated-handover-snaps\">Dated handover snapshot \u2014 re-read live before relying on it:</p>\n<ul id=\"bkmrk-layer-1%3A-8%2F8-proxmox\">\n<li><strong>Layer 1</strong>: 8/8 Proxmox nodes operational; VMs 8001\u20138042 carrying the\nplatform (VM table: <a href=\"https://book.wetopi.net/infrastructure/proxmox-cluster.md\">ARCH-INFRA-PROXMOX</a>).</li>\n<li><strong>Layer 2</strong>: 8/8 cluster nodes Ready (3 CP / 3 platform / 2 edge);\nGateway <code>wetopi-edge</code> Accepted+Programmed with the <code>web</code> :8000 and\n<code>https</code> :8443 listeners.</li>\n<li><strong>Layer 3</strong>: five Argo Applications \u2014 <code>root</code>, <code>cert-manager</code>, <code>docs-site</code>,\n<code>argo-edge</code> (all Healthy/Synced) and <code>vault</code> (Synced; its <code>Degraded</code>\nhealth is the documented <strong>false positive</strong> of the fail-closed deny\nrules \u2014 pods 3/3 Running, see <a href=\"https://book.wetopi.net/services/vault.md\">SVC-VAULT</a>);\n4 certificates Ready (<code>argo-tls</code>, <code>docs-tls</code>, <code>vault-tls</code>, <code>edge-test-tls</code>).</li>\n</ul>\n<h2 id=\"bkmrk-taking-over\">Taking over</h2>\n<p id=\"bkmrk-this-map-is-the-shap\">This map is the <em>shape</em>. The <em>procedure</em> is\n<a href=\"https://book.wetopi.net/project/taking-over-the-platform.md\">taking-over-the-platform.md</a>\n(PROC-PLATFORM-TAKEOVER): access by location, sources of truth, day-1\nverification, frequent operations, escalation and the no-go rules.</p>\n"
 },
 {
  "id": 40,
  "title": "cilium network",
  "slug": "cilium-network",
  "book_id": 3,
  "chapter_id": 16,
  "description": null,
  "created_at": "2026-09-06T13:38:55.000000Z",
  "updated_at": "2026-09-06T21:48:07.000000Z",
  "markdown": "> **ARCHIVO \u2014 versi\u00f3n migrada cruda.** La ficha estructurada con las 3 preguntas es la versi\u00f3n `ARCH-*`/`SVC-*` de este mismo chapter (actualizada con datos live 2026-09-06). Esta copia cruda se conserva por el historial.\n\n> Migrado desde `wetopi-platform/docs@main` (architecture/platform/cilium-network.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n Cilium network \u2014 CNI, Hubble and edge-node datapath\n\n## Purpose\n\nThe pod/service network of the RKE2 cluster: who assigns pod IPs, how nodes\ntalk across the LAN, and how the dual-NIC edge workers keep their cluster\ndatapath strictly on the LAN. Decision record:\n[ADR-0005](../../decisions/ADR-0005-cilium-first-addon.md) (accepted) \u2014 Cilium\nis the CNI from day one; there was never a Canal provisional phase.\n\n## Design\n\nRKE2 boots with `cni: none` so the bundled Canal manifest never deploys; the\nofficial Cilium chart is installed as the first addon and owns the datapath.\n\n- **Chart**: pinned `cilium:1.20.1` from `oci://quay.io/cilium/charts`\n  (values in Git: `kubernetes/cilium/values.yaml` in the\n  [infrastructure](https://github.com/wetopi-platform/infrastructure) repo).\n- **Mode**: VXLAN encapsulation (`routingMode: tunnel`,\n  `tunnelProtocol: vxlan`); no BGP, no native routing.\n- **IPAM**: cluster pool mirroring the RKE2 pod CIDR `10.64.0.0/14` exactly\n  (`clusterPoolIPv4PodCIDRList`), `/24` allocated per node.\n- **kube-proxy coexistence**: `kubeProxyReplacement: false` for v1 \u2014 the\n  service CIDR `10.68.0.0/16` stays owned by kube-proxy.\n- **Hubble**: enabled with Hubble Relay; relay UI off at bootstrap.\n\nCIDR ownership (must never diverge from the rke2-server config):\n\n| Range | Owner | Cilium role |\n|---|---|---|\n| `10.64.0.0/14` | RKE2 (pod CIDR) | IPAM cluster pool mirrors it |\n| `10.68.0.0/16` | RKE2 / kube-proxy (service CIDR) | none |\n\n## Underlay and ports\n\nTunnels ride the node LAN (`10.72.0.0/23`, single-NIC nodes, MTU 1500). The\nCilium MTU is left at its auto-computed default (~1450, 50-byte VXLAN\noverhead). Node-to-node matrix that must stay allowed on the LAN:\n\n```text\nVXLAN tunnels:  UDP 8472\nHubble Relay:   TCP 4244\nkubelet:        TCP 10250\n```\n\nThe edge workers carry these ports on their **LAN** interface only\n(`edge_lb` role asserts they never appear on the WAN interface).\n\n## Edge-node datapath pin (WAN Edge v1)\n\nThe chart leaves `devices` unset, so the Cilium agent auto-detects its\ninterface. On the single-NIC nodes that picks `eth0` (the LAN) \u2014 correct. On\nthe dual-NIC edge workers `eth0` is the **WAN**; auto-detection would attach\nthe datapath to it. The fix is a namespaced `CiliumNodeConfig`\n(`kubernetes/cilium/edge-node-config.yaml`, applied after the chart install\nby the bootstrap playbook and reconciled by the Cilium Argo Application):\n\n```yaml\nspec:\n  nodeSelector:\n    matchLabels:\n      wetopi.io/role: edge\n  defaults:\n    devices: eth1    # eth1 = LAN NIC on edge nodes (WAN Edge v1 plan \u00a7F1)\n```\n\nIt selects only `wetopi.io/role=edge` nodes; the six other nodes keep the\nauto-detect default.\n\n## GitOps ownership\n\nThe Cilium release is declared in Git as Argo CD Application `cilium`\n(`kubernetes/cilium/application.yaml`): Helm source (pinned 1.20.1 + Git\nvalues) plus a directory source carrying exactly\n`edge-node-config.yaml`, so the release and its edge override share one\nownership/pruning policy. `Prune=false` and `ignoreDifferences` protect the\nchart-generated TLS secrets (`cilium-ca`, `hubble-relay-client-certs`,\n`hubble-server-certs`) whose live `.data` must never be replaced by Helm\nmaterial. Like `argo-edge`, the Application itself still follows the\noperator-bootstrap path (`kubectl apply` from ops01) \u2014 it is not yet anchored\nin the root app's path (see [gitops.md](gitops.md)).\n\n## Invariants\n\n- Pod IPs only from `10.64.0.0/14`; the Cilium pool must always mirror the\n  rke2-server `cluster-cidr`.\n- Edge-node datapath is pinned to `eth1` (LAN); a `CiliumNodeConfig` change\n  or label drift that moves it to `eth0` is an incident (cluster traffic on\n  the WAN \u2014 ADR-0006 violation).\n- VXLAN/Hubble/kubelet ports LAN-only on edge nodes (asserted by the\n  `edge_lb` role).\n- No BGP/native routing in v1.\n\n## Verified state (2026-08-31, live from ops01)\n\nCilium 1.20.1 pods Running in `kube-system` on all nodes (DaemonSet +\noperator), Hubble Relay up, one `CiliumNode` per node, the\n`wetopi-edge-devices` CiliumNodeConfig present and matching the edge label.\nDated observation \u2014 re-read `kubectl -n kube-system get pods,daemonset,ciliumnode`\nbefore relying on it.\n\n## Dependencies\n\n- Depends on: the RKE2 node set and CIDRs ([rke2-cluster.md](rke2-cluster.md)),\n  the node-LAN underlay, the edge label contract\n  ([edge.md](edge.md)).\n- Depends on this: the edge plane (Traefik ingress scheduling tolerates the\n  `node.cilium.io/agent-not-ready` taint), GitOps handoff.\n\n## Failure considerations\n\n- Cilium down on a node: that node's pod traffic is down; with `cni: none`\n  below it, the node is NotReady \u2014 no fallback CNI exists (by design).\n- VXLAN MTU surprises: symptom is black-holed large packets; the auto MTU\n  keeps headroom, do not pin it manually.\n- Edge datapath mis-attachment: detect via `cilium status`/connectivity\n  checks per [VALIDATION.md](https://github.com/wetopi-platform/infrastructure/blob/main/kubernetes/cilium/VALIDATION.md)\n  in the infrastructure repo.",
  "html": "<blockquote id=\"bkmrk-archivo-%E2%80%94-versi%C3%B3n-mi\">\n<p><strong>ARCHIVO \u2014 versi\u00f3n migrada cruda.</strong> La ficha estructurada con las 3 preguntas es la versi\u00f3n <code>ARCH-*</code>/<code>SVC-*</code> de este mismo chapter (actualizada con datos live 2026-09-06). Esta copia cruda se conserva por el historial.</p>\n</blockquote>\n<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (architecture/platform/cilium-network.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-cilium-network-%E2%80%94-cni\">Cilium network \u2014 CNI, Hubble and edge-node datapath</p>\n<h2 id=\"bkmrk-purpose\">Purpose</h2>\n<p id=\"bkmrk-the-pod%2Fservice-netw\">The pod/service network of the RKE2 cluster: who assigns pod IPs, how nodes\ntalk across the LAN, and how the dual-NIC edge workers keep their cluster\ndatapath strictly on the LAN. Decision record:\n<a href=\"https://book.wetopi.net/decisions/ADR-0005-cilium-first-addon.md\">ADR-0005</a> (accepted) \u2014 Cilium\nis the CNI from day one; there was never a Canal provisional phase.</p>\n<h2 id=\"bkmrk-design\">Design</h2>\n<p id=\"bkmrk-rke2-boots-with-cni%3A\">RKE2 boots with <code>cni: none</code> so the bundled Canal manifest never deploys; the\nofficial Cilium chart is installed as the first addon and owns the datapath.</p>\n<ul id=\"bkmrk-chart%3A-pinned-cilium\">\n<li><strong>Chart</strong>: pinned <code>cilium:1.20.1</code> from <code>oci://quay.io/cilium/charts</code>\n(values in Git: <code>kubernetes/cilium/values.yaml</code> in the\n<a href=\"https://github.com/wetopi-platform/infrastructure\">infrastructure</a> repo).</li>\n<li><strong>Mode</strong>: VXLAN encapsulation (<code>routingMode: tunnel</code>,\n<code>tunnelProtocol: vxlan</code>); no BGP, no native routing.</li>\n<li><strong>IPAM</strong>: cluster pool mirroring the RKE2 pod CIDR <code>10.64.0.0/14</code> exactly\n(<code>clusterPoolIPv4PodCIDRList</code>), <code>/24</code> allocated per node.</li>\n<li><strong>kube-proxy coexistence</strong>: <code>kubeProxyReplacement: false</code> for v1 \u2014 the\nservice CIDR <code>10.68.0.0/16</code> stays owned by kube-proxy.</li>\n<li><strong>Hubble</strong>: enabled with Hubble Relay; relay UI off at bootstrap.</li>\n</ul>\n<p id=\"bkmrk-cidr-ownership-%28must\">CIDR ownership (must never diverge from the rke2-server config):</p>\n<table id=\"bkmrk-range-owner-cilium-r\">\n<thead>\n<tr>\n<th>Range</th>\n<th>Owner</th>\n<th>Cilium role</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>10.64.0.0/14</code></td>\n<td>RKE2 (pod CIDR)</td>\n<td>IPAM cluster pool mirrors it</td>\n</tr>\n<tr>\n<td><code>10.68.0.0/16</code></td>\n<td>RKE2 / kube-proxy (service CIDR)</td>\n<td>none</td>\n</tr>\n</tbody>\n</table>\n<h2 id=\"bkmrk-underlay-and-ports\">Underlay and ports</h2>\n<p id=\"bkmrk-tunnels-ride-the-nod\">Tunnels ride the node LAN (<code>10.72.0.0/23</code>, single-NIC nodes, MTU 1500). The\nCilium MTU is left at its auto-computed default (~1450, 50-byte VXLAN\noverhead). Node-to-node matrix that must stay allowed on the LAN:</p>\n<pre id=\"bkmrk-vxlan-tunnels%3A-udp-8\"><code class=\"language-text\">VXLAN tunnels:  UDP 8472\nHubble Relay:   TCP 4244\nkubelet:        TCP 10250\n</code></pre>\n<p id=\"bkmrk-the-edge-workers-car\">The edge workers carry these ports on their <strong>LAN</strong> interface only\n(<code>edge_lb</code> role asserts they never appear on the WAN interface).</p>\n<h2 id=\"bkmrk-edge-node-datapath-p\">Edge-node datapath pin (WAN Edge v1)</h2>\n<p id=\"bkmrk-the-chart-leaves-dev\">The chart leaves <code>devices</code> unset, so the Cilium agent auto-detects its\ninterface. On the single-NIC nodes that picks <code>eth0</code> (the LAN) \u2014 correct. On\nthe dual-NIC edge workers <code>eth0</code> is the <strong>WAN</strong>; auto-detection would attach\nthe datapath to it. The fix is a namespaced <code>CiliumNodeConfig</code>\n(<code>kubernetes/cilium/edge-node-config.yaml</code>, applied after the chart install\nby the bootstrap playbook and reconciled by the Cilium Argo Application):</p>\n<pre id=\"bkmrk-spec%3A-nodeselector%3A-\"><code class=\"language-yaml\">spec:\n  nodeSelector:\n    matchLabels:\n      wetopi.io/role: edge\n  defaults:\n    devices: eth1    # eth1 = LAN NIC on edge nodes (WAN Edge v1 plan \u00a7F1)\n</code></pre>\n<p id=\"bkmrk-it-selects-only-weto\">It selects only <code>wetopi.io/role=edge</code> nodes; the six other nodes keep the\nauto-detect default.</p>\n<h2 id=\"bkmrk-gitops-ownership\">GitOps ownership</h2>\n<p id=\"bkmrk-the-cilium-release-i\">The Cilium release is declared in Git as Argo CD Application <code>cilium</code>\n(<code>kubernetes/cilium/application.yaml</code>): Helm source (pinned 1.20.1 + Git\nvalues) plus a directory source carrying exactly\n<code>edge-node-config.yaml</code>, so the release and its edge override share one\nownership/pruning policy. <code>Prune=false</code> and <code>ignoreDifferences</code> protect the\nchart-generated TLS secrets (<code>cilium-ca</code>, <code>hubble-relay-client-certs</code>,\n<code>hubble-server-certs</code>) whose live <code>.data</code> must never be replaced by Helm\nmaterial. Like <code>argo-edge</code>, the Application itself still follows the\noperator-bootstrap path (<code>kubectl apply</code> from ops01) \u2014 it is not yet anchored\nin the root app's path (see <a href=\"https://book.wetopi.net/gitops.md\">gitops.md</a>).</p>\n<h2 id=\"bkmrk-invariants\">Invariants</h2>\n<ul id=\"bkmrk-pod-ips-only-from-10\">\n<li>Pod IPs only from <code>10.64.0.0/14</code>; the Cilium pool must always mirror the\nrke2-server <code>cluster-cidr</code>.</li>\n<li>Edge-node datapath is pinned to <code>eth1</code> (LAN); a <code>CiliumNodeConfig</code> change\nor label drift that moves it to <code>eth0</code> is an incident (cluster traffic on\nthe WAN \u2014 ADR-0006 violation).</li>\n<li>VXLAN/Hubble/kubelet ports LAN-only on edge nodes (asserted by the\n<code>edge_lb</code> role).</li>\n<li>No BGP/native routing in v1.</li>\n</ul>\n<h2 id=\"bkmrk-verified-state-%282026\">Verified state (2026-08-31, live from ops01)</h2>\n<p id=\"bkmrk-cilium-1.20.1-pods-r\">Cilium 1.20.1 pods Running in <code>kube-system</code> on all nodes (DaemonSet +\noperator), Hubble Relay up, one <code>CiliumNode</code> per node, the\n<code>wetopi-edge-devices</code> CiliumNodeConfig present and matching the edge label.\nDated observation \u2014 re-read <code>kubectl -n kube-system get pods,daemonset,ciliumnode</code>\nbefore relying on it.</p>\n<h2 id=\"bkmrk-dependencies\">Dependencies</h2>\n<ul id=\"bkmrk-depends-on%3A-the-rke2\">\n<li>Depends on: the RKE2 node set and CIDRs (<a href=\"https://book.wetopi.net/rke2-cluster.md\">rke2-cluster.md</a>),\nthe node-LAN underlay, the edge label contract\n(<a href=\"https://book.wetopi.net/edge.md\">edge.md</a>).</li>\n<li>Depends on this: the edge plane (Traefik ingress scheduling tolerates the\n<code>node.cilium.io/agent-not-ready</code> taint), GitOps handoff.</li>\n</ul>\n<h2 id=\"bkmrk-failure-consideratio\">Failure considerations</h2>\n<ul id=\"bkmrk-cilium-down-on-a-nod\">\n<li>Cilium down on a node: that node's pod traffic is down; with <code>cni: none</code>\nbelow it, the node is NotReady \u2014 no fallback CNI exists (by design).</li>\n<li>VXLAN MTU surprises: symptom is black-holed large packets; the auto MTU\nkeeps headroom, do not pin it manually.</li>\n<li>Edge datapath mis-attachment: detect via <code>cilium status</code>/connectivity\nchecks per <a href=\"https://github.com/wetopi-platform/infrastructure/blob/main/kubernetes/cilium/VALIDATION.md\">VALIDATION.md</a>\nin the infrastructure repo.</li>\n</ul>\n"
 },
 {
  "id": 41,
  "title": "edge",
  "slug": "edge",
  "book_id": 3,
  "chapter_id": 18,
  "description": null,
  "created_at": "2026-09-06T13:38:55.000000Z",
  "updated_at": "2026-09-06T21:48:07.000000Z",
  "markdown": "> **ARCHIVO \u2014 versi\u00f3n migrada cruda.** La ficha estructurada con las 3 preguntas es la versi\u00f3n `ARCH-*`/`SVC-*` de este mismo chapter (actualizada con datos live 2026-09-06). Esta copia cruda se conserva por el historial.\n\n> Migrado desde `wetopi-platform/docs@main` (architecture/platform/edge.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n WAN edge v1 \u2014 direct Traefik exposure without L4 layer\n\n## Purpose\n\nHow the platform publishes services to the Internet in the v1 window: the\nbounded exception that lets the first public services run **without** the\nfull L4 edge stack (VRRP VIP + IPVS + DSR) that the master plan requires for\nproduction. Decision record:\n[ADR-0006](../../decisions/ADR-0006-edge-v1-wan-exception.md) (accepted\n2026-08-30, with the 2026-08-30 UPDATE adding `argo.wetopi.net` to the\nservice list).\n\n## The v1 mechanism\n\n- **Exactly two edge nodes**: `rke-wan01` (`79.137.114.193`) and\n  `rke-wan02` (`79.137.114.194`), range `79.137.114.192/27`. NIC1 = WAN\n  (`vmbr3`, `eth0`), NIC2 = node-LAN (`vmbr4`, `eth1`) \u2014 the reverse of the\n  single-NIC nodes; the `edge_lb` Ansible role owns the interface contract.\n  These are also cluster workers: `wetopi.io/role=edge`\n  ([rke2-cluster.md](rke2-cluster.md)).\n- **Traefik directly on the WAN**: the bundled rke2-traefik chart\n  (`HelmChartConfig rke2-traefik`, `hostNetwork: true`) runs on edge nodes\n  only and binds the chart-native entryPoints **8000 (web) / 8443\n  (websecure)** in the host netns. Public 80/443 is bridged by in-VM\n  nftables `REDIRECT` 80\u21928000, 443\u21928443 (the `edge_lb` role is the single\n  enforcement point).\n- **HA = DNS round-robin** over the two public A records (TTL 300,\n  grey-cloud, OpenTofu-owned). **Reduced availability by design**: there is\n  no L4 health check or automatic failover. A dead node's A record keeps\n  resolving until the operator removes/repoints it (tofu PR or emergency\n  Cloudflare edit); TTL only bounds propagation of that manual remediation.\n  Per-address uptime monitoring is part of the v1 posture.\n- **nftables posture**: in-VM nftables deny-by-default; the WAN interface\n  exposes **80/443 ONLY** (the role asserts the port set is a subset of\n  {80,443}; anything else needs a recorded operator decision). The LAN\n  interface carries only the cluster matrix (UDP 8472, TCP 4244/10250) and\n  rate-limited management SSH. Pod/Service CIDR routes are pinned to the LAN\n  gateway so cluster traffic never uses the WAN default.\n\n## Gateway API surface\n\nGateway API (v1) on the Traefik Gateway controller:\n\n| Object | Namespace | Role |\n|---|---|---|\n| `GatewayClass wetopi-edge` | cluster-scoped | `controllerName: traefik.io/gateway-controller` (chart provider enabled) |\n| `Gateway wetopi-edge` | `kube-system` | listeners `web` :8000 (HTTP), `https` :8443 (Terminate); https `allowedRoutes.namespaces.from: All` to admit cross-namespace routes |\n| `HTTPRoute argo` (+ `argo-http-redirect`) | `kube-system` | `argo.wetopi.net` \u2192 `argocd/argocd-server:80` behind Middleware `argo-basic-auth` (ReferenceGrant in `argocd`); port 80 route 308-redirects to https |\n| `HTTPRoute docs-site` | `docs-site` | `docs.wetopi.net` \u2192 docs-site Deployment (basic-auth Middleware), cross-namespace attachment via the `from: All` widening |\n| `HTTPRoute vault` | `vault` | `vault.wetopi.net` \u2192 Infisical Web UI; **no** basic-auth Middleware \u2014 the UI's own login+TOTP is the gate (operator decision) |\n| Certificates `argo-tls`, `docs-tls`, `vault-tls` | `kube-system` | Let's Encrypt **PROD**, DNS-01 via cert-manager; all four certs (incl. staging `edge-test-tls`) Ready live 2026-08-31 |\n| `Secret wetopi-edge-default-cert` | `kube-system` | bootstrap self-signed fallback cert (superseded per-name by the LE certs) |\n\nCertificateRefs are additive and fail-safe ordered: a missing referenced\nSecret would make the whole `https` listener `ResolvedRefs=False`; every\ncert addition therefore follows \"Secret Ready first, Gateway after\".\n\n```mermaid\nflowchart LR\n    I[\"internet client\"] -->|A x2 TTL 300| D[\"DNS round-robin<br/>79.137.114.193 / .194\"]\n    D --> W1[\"rke-wan01<br/>nftables 80/443 REDIRECT \u2192 8000/8443\"]\n    D --> W2[\"rke-wan02<br/>nftables 80/443 REDIRECT \u2192 8000/8443\"]\n    W1 & W2 --> T[\"Traefik (rke2-traefik, hostNetwork)<br/>entryPoints web :8000 / https :8443\"]\n    T --> G[\"Gateway wetopi-edge (kube-system)<br/>https listener :8443 \u2014 TLS Terminate\"]\n    G -->|HTTPRoute argo| M1[\"Middleware argo-basic-auth\"] --> S1[\"argocd/argocd-server:80\"]\n    G -->|HTTPRoute docs-site| S2[\"docs-site Deployment :80\"]\n    G -->|HTTPRoute vault| S3[\"Infisical Web UI (vault ns)\"]\n```\n\n## Service list and its hard bounds\n\nThe v1 exception covers exactly **docs.wetopi.net + argo.wetopi.net**\n(both recorded operator decisions; each further name is its own decision).\n`vault.wetopi.net` was added 2026-08-31 for the in-cluster Infisical Web UI\n(OBJ-VAULT-001 / [ADR-0009](../../decisions/ADR-0009-secrets-vault-harness.md))\nthrough the same edge machinery \u2014 see its ADR for the recorded decision.\n\nThe ADR's hard conditions:\n\n1. **Any service with external paying customers requires the v2 edge**\n   (VRRP public VIP + IPVS directors + DSR, with the DSR PoC checklist done) \u2014\n   no exceptions. The v2 track is scheduled BEFORE the hosting pilot phase\n   (master plan \u00a717/\u00a733 Fase 16), not an aspiration.\n2. Reduced-availability semantics are documented here and monitored\n   (independent per-address uptime checks).\n3. Cluster-plane identity/traffic of edge nodes is LAN-only (ADR-0006\n   condition 4): node IP `10.72.0.5x`, kubelet, Cilium/VXLAN on `eth1`.\n\n## GitOps ownership\n\nEdge objects are split: the Gateway/GatewayClass are live-verified objects in\n`kubernetes/edge/` (F2 landing; the Gateway carries a strict\ncertificateRefs coordination note), and the per-service routes/certs live in\n`kubernetes/edge-argo/` (Application `argo-edge`) and the service\ndirectories (`docs-site/`, `vault/`). See\n[gitops.md](gitops.md) for the app inventory.\n\n## Invariants\n\n- WAN ingress = {80, 443} only; asserted at render time by `edge_lb`.\n- The public 80/443 contract exists ONLY through the nftables REDIRECT; the\n  Gateway never binds 80/443 (entryPoint mismatch would leave it\n  `Accepted=False`).\n- TLS terminates at the edge Gateway; plaintext 80 routes exist only as\n  308\u2192https redirects.\n- Cluster CIDRs route over the LAN gateway on edge nodes (never the WAN\n  default).\n- v1 service list is closed: additions require a recorded operator decision;\n  customer hosting requires v2.\n\n## Verified state (2026-08-31, live from ops01)\n\nOne Gateway `wetopi-edge` (kube-system) Accepted=True + Programmed=True\n(plus the `wetopi-edge` GatewayClass); 3 HTTPRoutes (docs-site, argo,\nvault) attached; 4 Certificates Ready (`docs-tls`, `argo-tls`, `vault-tls`\nPROD + `edge-test-tls` staging). Dated observation \u2014 re-read\n`kubectl -n kube-system get gateway,httproute` and\n`kubectl get certificate -A` before relying on it.\n\n## Failure considerations\n\n- One edge node dead: ~half of new connections fail until operator\n  remediation (no L4 health check) \u2014 the documented v1 failure mode;\n  monitoring probes both A records independently.\n- Missing/revoked LE cert: the affected hostname degrades at renewal\n  failure; cert-manager auto-renews (DNS-01, port-mapping-immune).\n- Unhealthy but alive node: not detected (no health check) \u2014 same remediation\n  path; the v2 track eliminates this class.",
  "html": "<blockquote id=\"bkmrk-archivo-%E2%80%94-versi%C3%B3n-mi\">\n<p><strong>ARCHIVO \u2014 versi\u00f3n migrada cruda.</strong> La ficha estructurada con las 3 preguntas es la versi\u00f3n <code>ARCH-*</code>/<code>SVC-*</code> de este mismo chapter (actualizada con datos live 2026-09-06). Esta copia cruda se conserva por el historial.</p>\n</blockquote>\n<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (architecture/platform/edge.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-wan-edge-v1-%E2%80%94-direct\">WAN edge v1 \u2014 direct Traefik exposure without L4 layer</p>\n<h2 id=\"bkmrk-purpose\">Purpose</h2>\n<p id=\"bkmrk-how-the-platform-pub\">How the platform publishes services to the Internet in the v1 window: the\nbounded exception that lets the first public services run <strong>without</strong> the\nfull L4 edge stack (VRRP VIP + IPVS + DSR) that the master plan requires for\nproduction. Decision record:\n<a href=\"https://book.wetopi.net/decisions/ADR-0006-edge-v1-wan-exception.md\">ADR-0006</a> (accepted\n2026-08-30, with the 2026-08-30 UPDATE adding <code>argo.wetopi.net</code> to the\nservice list).</p>\n<h2 id=\"bkmrk-the-v1-mechanism\">The v1 mechanism</h2>\n<ul id=\"bkmrk-exactly-two-edge-nod\">\n<li><strong>Exactly two edge nodes</strong>: <code>rke-wan01</code> (<code>79.137.114.193</code>) and\n<code>rke-wan02</code> (<code>79.137.114.194</code>), range <code>79.137.114.192/27</code>. NIC1 = WAN\n(<code>vmbr3</code>, <code>eth0</code>), NIC2 = node-LAN (<code>vmbr4</code>, <code>eth1</code>) \u2014 the reverse of the\nsingle-NIC nodes; the <code>edge_lb</code> Ansible role owns the interface contract.\nThese are also cluster workers: <code>wetopi.io/role=edge</code>\n(<a href=\"https://book.wetopi.net/rke2-cluster.md\">rke2-cluster.md</a>).</li>\n<li><strong>Traefik directly on the WAN</strong>: the bundled rke2-traefik chart\n(<code>HelmChartConfig rke2-traefik</code>, <code>hostNetwork: true</code>) runs on edge nodes\nonly and binds the chart-native entryPoints <strong>8000 (web) / 8443\n(websecure)</strong> in the host netns. Public 80/443 is bridged by in-VM\nnftables <code>REDIRECT</code> 80\u21928000, 443\u21928443 (the <code>edge_lb</code> role is the single\nenforcement point).</li>\n<li><strong>HA = DNS round-robin</strong> over the two public A records (TTL 300,\ngrey-cloud, OpenTofu-owned). <strong>Reduced availability by design</strong>: there is\nno L4 health check or automatic failover. A dead node's A record keeps\nresolving until the operator removes/repoints it (tofu PR or emergency\nCloudflare edit); TTL only bounds propagation of that manual remediation.\nPer-address uptime monitoring is part of the v1 posture.</li>\n<li><strong>nftables posture</strong>: in-VM nftables deny-by-default; the WAN interface\nexposes <strong>80/443 ONLY</strong> (the role asserts the port set is a subset of\n{80,443}; anything else needs a recorded operator decision). The LAN\ninterface carries only the cluster matrix (UDP 8472, TCP 4244/10250) and\nrate-limited management SSH. Pod/Service CIDR routes are pinned to the LAN\ngateway so cluster traffic never uses the WAN default.</li>\n</ul>\n<h2 id=\"bkmrk-gateway-api-surface\">Gateway API surface</h2>\n<p id=\"bkmrk-gateway-api-%28v1%29-on-\">Gateway API (v1) on the Traefik Gateway controller:</p>\n<table id=\"bkmrk-object-namespace-rol\">\n<thead>\n<tr>\n<th>Object</th>\n<th>Namespace</th>\n<th>Role</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>GatewayClass wetopi-edge</code></td>\n<td>cluster-scoped</td>\n<td><code>controllerName: traefik.io/gateway-controller</code> (chart provider enabled)</td>\n</tr>\n<tr>\n<td><code>Gateway wetopi-edge</code></td>\n<td><code>kube-system</code></td>\n<td>listeners <code>web</code> :8000 (HTTP), <code>https</code> :8443 (Terminate); https <code>allowedRoutes.namespaces.from: All</code> to admit cross-namespace routes</td>\n</tr>\n<tr>\n<td><code>HTTPRoute argo</code> (+ <code>argo-http-redirect</code>)</td>\n<td><code>kube-system</code></td>\n<td><code>argo.wetopi.net</code> \u2192 <code>argocd/argocd-server:80</code> behind Middleware <code>argo-basic-auth</code> (ReferenceGrant in <code>argocd</code>); port 80 route 308-redirects to https</td>\n</tr>\n<tr>\n<td><code>HTTPRoute docs-site</code></td>\n<td><code>docs-site</code></td>\n<td><code>docs.wetopi.net</code> \u2192 docs-site Deployment (basic-auth Middleware), cross-namespace attachment via the <code>from: All</code> widening</td>\n</tr>\n<tr>\n<td><code>HTTPRoute vault</code></td>\n<td><code>vault</code></td>\n<td><code>vault.wetopi.net</code> \u2192 Infisical Web UI; <strong>no</strong> basic-auth Middleware \u2014 the UI's own login+TOTP is the gate (operator decision)</td>\n</tr>\n<tr>\n<td>Certificates <code>argo-tls</code>, <code>docs-tls</code>, <code>vault-tls</code></td>\n<td><code>kube-system</code></td>\n<td>Let's Encrypt <strong>PROD</strong>, DNS-01 via cert-manager; all four certs (incl. staging <code>edge-test-tls</code>) Ready live 2026-08-31</td>\n</tr>\n<tr>\n<td><code>Secret wetopi-edge-default-cert</code></td>\n<td><code>kube-system</code></td>\n<td>bootstrap self-signed fallback cert (superseded per-name by the LE certs)</td>\n</tr>\n</tbody>\n</table>\n<p id=\"bkmrk-certificaterefs-are-\">CertificateRefs are additive and fail-safe ordered: a missing referenced\nSecret would make the whole <code>https</code> listener <code>ResolvedRefs=False</code>; every\ncert addition therefore follows \"Secret Ready first, Gateway after\".</p>\n<pre id=\"bkmrk-flowchart-lr-i%5B%22inte\"><code class=\"language-mermaid\">flowchart LR\n    I[\"internet client\"] --&gt;|A x2 TTL 300| D[\"DNS round-robin&lt;br/&gt;79.137.114.193 / .194\"]\n    D --&gt; W1[\"rke-wan01&lt;br/&gt;nftables 80/443 REDIRECT \u2192 8000/8443\"]\n    D --&gt; W2[\"rke-wan02&lt;br/&gt;nftables 80/443 REDIRECT \u2192 8000/8443\"]\n    W1 &amp; W2 --&gt; T[\"Traefik (rke2-traefik, hostNetwork)&lt;br/&gt;entryPoints web :8000 / https :8443\"]\n    T --&gt; G[\"Gateway wetopi-edge (kube-system)&lt;br/&gt;https listener :8443 \u2014 TLS Terminate\"]\n    G --&gt;|HTTPRoute argo| M1[\"Middleware argo-basic-auth\"] --&gt; S1[\"argocd/argocd-server:80\"]\n    G --&gt;|HTTPRoute docs-site| S2[\"docs-site Deployment :80\"]\n    G --&gt;|HTTPRoute vault| S3[\"Infisical Web UI (vault ns)\"]\n</code></pre>\n<h2 id=\"bkmrk-service-list-and-its\">Service list and its hard bounds</h2>\n<p id=\"bkmrk-the-v1-exception-cov\">The v1 exception covers exactly <strong>docs.wetopi.net + argo.wetopi.net</strong>\n(both recorded operator decisions; each further name is its own decision).\n<code>vault.wetopi.net</code> was added 2026-08-31 for the in-cluster Infisical Web UI\n(OBJ-VAULT-001 / <a href=\"https://book.wetopi.net/decisions/ADR-0009-secrets-vault-harness.md\">ADR-0009</a>)\nthrough the same edge machinery \u2014 see its ADR for the recorded decision.</p>\n<p id=\"bkmrk-the-adr%27s-hard-condi\">The ADR's hard conditions:</p>\n<ol id=\"bkmrk-any-service-with-ext\">\n<li><strong>Any service with external paying customers requires the v2 edge</strong>\n(VRRP public VIP + IPVS directors + DSR, with the DSR PoC checklist done) \u2014\nno exceptions. The v2 track is scheduled BEFORE the hosting pilot phase\n(master plan \u00a717/\u00a733 Fase 16), not an aspiration.</li>\n<li>Reduced-availability semantics are documented here and monitored\n(independent per-address uptime checks).</li>\n<li>Cluster-plane identity/traffic of edge nodes is LAN-only (ADR-0006\ncondition 4): node IP <code>10.72.0.5x</code>, kubelet, Cilium/VXLAN on <code>eth1</code>.</li>\n</ol>\n<h2 id=\"bkmrk-gitops-ownership\">GitOps ownership</h2>\n<p id=\"bkmrk-edge-objects-are-spl\">Edge objects are split: the Gateway/GatewayClass are live-verified objects in\n<code>kubernetes/edge/</code> (F2 landing; the Gateway carries a strict\ncertificateRefs coordination note), and the per-service routes/certs live in\n<code>kubernetes/edge-argo/</code> (Application <code>argo-edge</code>) and the service\ndirectories (<code>docs-site/</code>, <code>vault/</code>). See\n<a href=\"https://book.wetopi.net/gitops.md\">gitops.md</a> for the app inventory.</p>\n<h2 id=\"bkmrk-invariants\">Invariants</h2>\n<ul id=\"bkmrk-wan-ingress-%3D-%7B80%2C-4\">\n<li>WAN ingress = {80, 443} only; asserted at render time by <code>edge_lb</code>.</li>\n<li>The public 80/443 contract exists ONLY through the nftables REDIRECT; the\nGateway never binds 80/443 (entryPoint mismatch would leave it\n<code>Accepted=False</code>).</li>\n<li>TLS terminates at the edge Gateway; plaintext 80 routes exist only as\n308\u2192https redirects.</li>\n<li>Cluster CIDRs route over the LAN gateway on edge nodes (never the WAN\ndefault).</li>\n<li>v1 service list is closed: additions require a recorded operator decision;\ncustomer hosting requires v2.</li>\n</ul>\n<h2 id=\"bkmrk-verified-state-%282026\">Verified state (2026-08-31, live from ops01)</h2>\n<p id=\"bkmrk-one-gateway-wetopi-e\">One Gateway <code>wetopi-edge</code> (kube-system) Accepted=True + Programmed=True\n(plus the <code>wetopi-edge</code> GatewayClass); 3 HTTPRoutes (docs-site, argo,\nvault) attached; 4 Certificates Ready (<code>docs-tls</code>, <code>argo-tls</code>, <code>vault-tls</code>\nPROD + <code>edge-test-tls</code> staging). Dated observation \u2014 re-read\n<code>kubectl -n kube-system get gateway,httproute</code> and\n<code>kubectl get certificate -A</code> before relying on it.</p>\n<h2 id=\"bkmrk-failure-consideratio\">Failure considerations</h2>\n<ul id=\"bkmrk-one-edge-node-dead%3A-\">\n<li>One edge node dead: ~half of new connections fail until operator\nremediation (no L4 health check) \u2014 the documented v1 failure mode;\nmonitoring probes both A records independently.</li>\n<li>Missing/revoked LE cert: the affected hostname degrades at renewal\nfailure; cert-manager auto-renews (DNS-01, port-mapping-immune).</li>\n<li>Unhealthy but alive node: not detected (no health check) \u2014 same remediation\npath; the v2 track eliminates this class.</li>\n</ul>\n"
 },
 {
  "id": 42,
  "title": "gitops",
  "slug": "gitops",
  "book_id": 3,
  "chapter_id": 19,
  "description": null,
  "created_at": "2026-09-06T13:38:55.000000Z",
  "updated_at": "2026-09-06T21:48:08.000000Z",
  "markdown": "> **ARCHIVO \u2014 versi\u00f3n migrada cruda.** La ficha estructurada con las 3 preguntas es la versi\u00f3n `ARCH-*`/`SVC-*` de este mismo chapter (actualizada con datos live 2026-09-06). Esta copia cruda se conserva por el historial.\n\n> Migrado desde `wetopi-platform/docs@main` (architecture/platform/gitops.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n GitOps plane \u2014 Argo CD v3.4.8 bootstrap and app layout\n\n## Purpose\n\nThe deployment plane for everything that runs inside the cluster. The\nownership boundary (ADR-0003 split, made operational): **Ansible = node\nbootstrap only; Argo CD = everything in-cluster after node bootstrap.**\nThere is deliberately no CI `kubectl apply` pipeline \u2014 Git is the\ndeployment path.\n\n## Install and version pin\n\n- **Argo CD v3.4.8**, non-HA plain install, pinned by commit-tagged URL\n  (`argoproj/argo-cd` `v3.4.8` manifest, upstream SHA-256 recorded in\n  `docs/gitops.md` of the\n  [infrastructure](https://github.com/wetopi-platform/infrastructure) repo)\n  and vendored bit-identical in\n  `kubernetes/gitops/bootstrap/argocd-install-v3.4.8/` (namespace file,\n  `server.insecure=\"true\"` params, `install.yaml`).\n- **Installed by human from `wetopi-ops01`** during apply runbook Phase G\n  (2026-08-30), with two recorded apply-time deviations: the org disables\n  deploy keys (repo credential is therefore a read-only **HTTPS** token in\n  repository Secret `argocd/repo-infra-https`), and the\n  `applicationsets.argoproj.io` CRD exceeds kubectl's client-side annotation\n  cap and is applied **server-side** (split manifest).\n- **Edge-terminated TLS**: `server.insecure: \"true\"` \u2014 the Argo CD server\n  serves plain HTTP; TLS for `argo.wetopi.net` terminates at the edge\n  Gateway ([edge.md](edge.md)).\n- No HA at bootstrap (single replicas); HA layout is OPEN. Upgrades replace\n  the pinned directory \u2014 never a moving `stable` URL.\n\n## Project and app layout\n\nOne AppProject, `platform`\n(`kubernetes/gitops/app.project.yaml`):\n\n- `sourceRepos`: the private infrastructure repo over **HTTPS** (token in\n  `argocd/repo-infra-https`) + `quay.io/jetstack/charts` (public OCI Helm\n  registry for cert-manager; nothing outside that exact path is admitted).\n- `destinations`: in-cluster only, `namespace: '*'` \u2014 AppProject\n  destinations support globs but not negation, so the **argocd-namespace\n  self-management exclusion is procedural** (the Argo control plane is\n  installed only from the pinned manifests by human apply; Applications are\n  reviewed before apply; mechanical enforcement = admission control, which\n  does not exist at bootstrap \u2014 OPEN). `ResourceQuota`/`LimitRange` kinds are\n  blacklisted.\n- `clusterResourceWhitelist: */*` (cluster-scoped resources needed at\n  bootstrap).\n\nThe layout is a minimal viable app-of-apps:\n\n```mermaid\nflowchart TB\n    subgraph GIT[\"GitHub (private)\"]\n        R[\"wetopi-platform/infrastructure<br/>kubernetes/ (desired state)\"]\n    end\n    subgraph CL[\"cluster (ops01 kubectl for bootstrap only)\"]\n        RC[\"AppProject platform\"]\n        ROOT[\"Application root<br/>path kubernetes/platform/\"]\n        ROOT --> CM[\"Application cert-manager<br/>\u2192 Helm chart + F3 manifests\"]\n        ROOT --> DS[\"Application docs-site<br/>\u2192 6 workload manifests\"]\n        ROOT --> VT[\"Application vault<br/>\u2192 Infisical manifests (ns vault)\"]\n        OE[\"Application argo-edge (not anchored)<br/>\u2192 kube-system edge-argo manifests\"]\n        CL2[\"Application cilium (not anchored)<br/>\u2192 Helm 1.20.1 + edge-node-config\"]\n    end\n    R -->|\"HTTPS + repo Secret argocd/repo-infra-https\"| RC\n    RC --> ROOT\n    RC -. \"bootstrap apply (ops01)\" .-> OE & CL2\n```\n\n- **Anchored in Git** (child Application manifests live in the root app's\n  synced path `kubernetes/platform/`, so they survive cluster state loss via\n  root prune+selfHeal): `root`, `cert-manager`, `docs-site`, `vault`.\n  Anchored children carry the `resources-finalizer.argocd.argoproj.io`\n  finalizer \u2014 deleting an anchoring merge cascades foreground (children's\n  workloads are deleted before the Application CR disappears), so a revert\n  never orphans live state.\n- **Not yet anchored** (operator-bootstrap path, include-only self-management\n  contract each): `argo-edge`, `cilium`.\n- **Live app inventory (2026-08-31)**: 5 applications \u2014 `root`, `argo-edge`,\n  `cert-manager`, `docs-site`, `vault`. `vault` is **Degraded**: its two\n  operator-provisioned Secrets (postgres/infra credentials) predate S0\n  inputs and must exist on-cluster before first sync \u2014 expected, tracked by\n  OBJ-VAULT-001. Per-service facts live in `services/`.\n\nThe broader App-of-Apps vs ApplicationSet vs per-domain split remains **OPEN**\n(master architecture plan \u00a79.4); this layout is built to stay evolvable.\n\n## Invariants\n\n- The only deployment path is Git \u2192 Argo CD; ad-hoc `kubectl` from ops01 is\n  for recovery/surgery and must be reconciled back into Git.\n- Child Applications never self-manage Application objects (include-only\n  sources; anchored ones are applied only by the root app).\n- Secrets never live in Git: operator-provisioned Secrets (`argo-basic-auth`,\n  `docs-site-basic-auth`, the vault credentials, the repo token) exist only\n  on-cluster \u2014 Argo with default sync options neither prunes nor replaces\n  them.\n- argocd-namespace exclusion stays procedural until admission control lands.\n- Pinned version: upgrades are reviewed replacements of the pin directory.\n\n## Dependencies\n\n- Depends on: the cluster + API VIP ([rke2-cluster.md](rke2-cluster.md)),\n  the edge plane for the public WebUI path ([edge.md](edge.md)).\n- Depends on this: every in-cluster add-on and service (cert-manager \u2192\n  edge TLS; docs-site, vault, argo-edge, cilium).\n\n## Verified state (2026-08-31, live from ops01)\n\nArgo CD v3.4.8 7/7 pods Running/Ready; 5 Applications in project\n`platform` as listed above; `argocd/repo-infra-https` present. Dated\nobservation \u2014 re-read `argocd app list` before relying on it.",
  "html": "<blockquote id=\"bkmrk-archivo-%E2%80%94-versi%C3%B3n-mi\">\n<p><strong>ARCHIVO \u2014 versi\u00f3n migrada cruda.</strong> La ficha estructurada con las 3 preguntas es la versi\u00f3n <code>ARCH-*</code>/<code>SVC-*</code> de este mismo chapter (actualizada con datos live 2026-09-06). Esta copia cruda se conserva por el historial.</p>\n</blockquote>\n<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (architecture/platform/gitops.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-gitops-plane-%E2%80%94-argo-\">GitOps plane \u2014 Argo CD v3.4.8 bootstrap and app layout</p>\n<h2 id=\"bkmrk-purpose\">Purpose</h2>\n<p id=\"bkmrk-the-deployment-plane\">The deployment plane for everything that runs inside the cluster. The\nownership boundary (ADR-0003 split, made operational): <strong>Ansible = node\nbootstrap only; Argo CD = everything in-cluster after node bootstrap.</strong>\nThere is deliberately no CI <code>kubectl apply</code> pipeline \u2014 Git is the\ndeployment path.</p>\n<h2 id=\"bkmrk-install-and-version-\">Install and version pin</h2>\n<ul id=\"bkmrk-argo-cd-v3.4.8%2C-non-\">\n<li><strong>Argo CD v3.4.8</strong>, non-HA plain install, pinned by commit-tagged URL\n(<code>argoproj/argo-cd</code> <code>v3.4.8</code> manifest, upstream SHA-256 recorded in\n<code>docs/gitops.md</code> of the\n<a href=\"https://github.com/wetopi-platform/infrastructure\">infrastructure</a> repo)\nand vendored bit-identical in\n<code>kubernetes/gitops/bootstrap/argocd-install-v3.4.8/</code> (namespace file,\n<code>server.insecure=\"true\"</code> params, <code>install.yaml</code>).</li>\n<li><strong>Installed by human from <code>wetopi-ops01</code></strong> during apply runbook Phase G\n(2026-08-30), with two recorded apply-time deviations: the org disables\ndeploy keys (repo credential is therefore a read-only <strong>HTTPS</strong> token in\nrepository Secret <code>argocd/repo-infra-https</code>), and the\n<code>applicationsets.argoproj.io</code> CRD exceeds kubectl's client-side annotation\ncap and is applied <strong>server-side</strong> (split manifest).</li>\n<li><strong>Edge-terminated TLS</strong>: <code>server.insecure: \"true\"</code> \u2014 the Argo CD server\nserves plain HTTP; TLS for <code>argo.wetopi.net</code> terminates at the edge\nGateway (<a href=\"https://book.wetopi.net/edge.md\">edge.md</a>).</li>\n<li>No HA at bootstrap (single replicas); HA layout is OPEN. Upgrades replace\nthe pinned directory \u2014 never a moving <code>stable</code> URL.</li>\n</ul>\n<h2 id=\"bkmrk-project-and-app-layo\">Project and app layout</h2>\n<p id=\"bkmrk-one-appproject%2C-plat\">One AppProject, <code>platform</code>\n(<code>kubernetes/gitops/app.project.yaml</code>):</p>\n<ul id=\"bkmrk-sourcerepos%3A-the-pri\">\n<li><code>sourceRepos</code>: the private infrastructure repo over <strong>HTTPS</strong> (token in\n<code>argocd/repo-infra-https</code>) + <code>quay.io/jetstack/charts</code> (public OCI Helm\nregistry for cert-manager; nothing outside that exact path is admitted).</li>\n<li><code>destinations</code>: in-cluster only, <code>namespace: '*'</code> \u2014 AppProject\ndestinations support globs but not negation, so the <strong>argocd-namespace\nself-management exclusion is procedural</strong> (the Argo control plane is\ninstalled only from the pinned manifests by human apply; Applications are\nreviewed before apply; mechanical enforcement = admission control, which\ndoes not exist at bootstrap \u2014 OPEN). <code>ResourceQuota</code>/<code>LimitRange</code> kinds are\nblacklisted.</li>\n<li><code>clusterResourceWhitelist: */*</code> (cluster-scoped resources needed at\nbootstrap).</li>\n</ul>\n<p id=\"bkmrk-the-layout-is-a-mini\">The layout is a minimal viable app-of-apps:</p>\n<pre id=\"bkmrk-flowchart-tb-subgrap\"><code class=\"language-mermaid\">flowchart TB\n    subgraph GIT[\"GitHub (private)\"]\n        R[\"wetopi-platform/infrastructure&lt;br/&gt;kubernetes/ (desired state)\"]\n    end\n    subgraph CL[\"cluster (ops01 kubectl for bootstrap only)\"]\n        RC[\"AppProject platform\"]\n        ROOT[\"Application root&lt;br/&gt;path kubernetes/platform/\"]\n        ROOT --&gt; CM[\"Application cert-manager&lt;br/&gt;\u2192 Helm chart + F3 manifests\"]\n        ROOT --&gt; DS[\"Application docs-site&lt;br/&gt;\u2192 6 workload manifests\"]\n        ROOT --&gt; VT[\"Application vault&lt;br/&gt;\u2192 Infisical manifests (ns vault)\"]\n        OE[\"Application argo-edge (not anchored)&lt;br/&gt;\u2192 kube-system edge-argo manifests\"]\n        CL2[\"Application cilium (not anchored)&lt;br/&gt;\u2192 Helm 1.20.1 + edge-node-config\"]\n    end\n    R --&gt;|\"HTTPS + repo Secret argocd/repo-infra-https\"| RC\n    RC --&gt; ROOT\n    RC -. \"bootstrap apply (ops01)\" .-&gt; OE &amp; CL2\n</code></pre>\n<ul id=\"bkmrk-anchored-in-git-%28chi\">\n<li><strong>Anchored in Git</strong> (child Application manifests live in the root app's\nsynced path <code>kubernetes/platform/</code>, so they survive cluster state loss via\nroot prune+selfHeal): <code>root</code>, <code>cert-manager</code>, <code>docs-site</code>, <code>vault</code>.\nAnchored children carry the <code>resources-finalizer.argocd.argoproj.io</code>\nfinalizer \u2014 deleting an anchoring merge cascades foreground (children's\nworkloads are deleted before the Application CR disappears), so a revert\nnever orphans live state.</li>\n<li><strong>Not yet anchored</strong> (operator-bootstrap path, include-only self-management\ncontract each): <code>argo-edge</code>, <code>cilium</code>.</li>\n<li><strong>Live app inventory (2026-08-31)</strong>: 5 applications \u2014 <code>root</code>, <code>argo-edge</code>,\n<code>cert-manager</code>, <code>docs-site</code>, <code>vault</code>. <code>vault</code> is <strong>Degraded</strong>: its two\noperator-provisioned Secrets (postgres/infra credentials) predate S0\ninputs and must exist on-cluster before first sync \u2014 expected, tracked by\nOBJ-VAULT-001. Per-service facts live in <code>services/</code>.</li>\n</ul>\n<p id=\"bkmrk-the-broader-app-of-a\">The broader App-of-Apps vs ApplicationSet vs per-domain split remains <strong>OPEN</strong>\n(master architecture plan \u00a79.4); this layout is built to stay evolvable.</p>\n<h2 id=\"bkmrk-invariants\">Invariants</h2>\n<ul id=\"bkmrk-the-only-deployment-\">\n<li>The only deployment path is Git \u2192 Argo CD; ad-hoc <code>kubectl</code> from ops01 is\nfor recovery/surgery and must be reconciled back into Git.</li>\n<li>Child Applications never self-manage Application objects (include-only\nsources; anchored ones are applied only by the root app).</li>\n<li>Secrets never live in Git: operator-provisioned Secrets (<code>argo-basic-auth</code>,\n<code>docs-site-basic-auth</code>, the vault credentials, the repo token) exist only\non-cluster \u2014 Argo with default sync options neither prunes nor replaces\nthem.</li>\n<li>argocd-namespace exclusion stays procedural until admission control lands.</li>\n<li>Pinned version: upgrades are reviewed replacements of the pin directory.</li>\n</ul>\n<h2 id=\"bkmrk-dependencies\">Dependencies</h2>\n<ul id=\"bkmrk-depends-on%3A-the-clus\">\n<li>Depends on: the cluster + API VIP (<a href=\"https://book.wetopi.net/rke2-cluster.md\">rke2-cluster.md</a>),\nthe edge plane for the public WebUI path (<a href=\"https://book.wetopi.net/edge.md\">edge.md</a>).</li>\n<li>Depends on this: every in-cluster add-on and service (cert-manager \u2192\nedge TLS; docs-site, vault, argo-edge, cilium).</li>\n</ul>\n<h2 id=\"bkmrk-verified-state-%282026\">Verified state (2026-08-31, live from ops01)</h2>\n<p id=\"bkmrk-argo-cd-v3.4.8-7%2F7-p\">Argo CD v3.4.8 7/7 pods Running/Ready; 5 Applications in project\n<code>platform</code> as listed above; <code>argocd/repo-infra-https</code> present. Dated\nobservation \u2014 re-read <code>argocd app list</code> before relying on it.</p>\n"
 },
 {
  "id": 43,
  "title": "rke2 cluster",
  "slug": "rke2-cluster",
  "book_id": 3,
  "chapter_id": 16,
  "description": null,
  "created_at": "2026-09-06T13:38:56.000000Z",
  "updated_at": "2026-09-06T21:48:08.000000Z",
  "markdown": "> **ARCHIVO \u2014 versi\u00f3n migrada cruda.** La ficha estructurada con las 3 preguntas es la versi\u00f3n `ARCH-*`/`SVC-*` de este mismo chapter (actualizada con datos live 2026-09-06). Esta copia cruda se conserva por el historial.\n\n> Migrado desde `wetopi-platform/docs@main` (architecture/platform/rke2-cluster.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n RKE2 cluster \u2014 topology, control plane and node roles\n\n## Purpose\n\nHow the Kubernetes cluster layer is built on the Wetopi node LAN: node\ntopology, the HA control plane and its stable API endpoint, and the node-role\ncontract that the CNI, edge and GitOps planes all depend on.\n\n## Topology\n\n3 control planes + 3 platform workers + 2 edge workers = **8 nodes**, all\nReady (live-verified from `wetopi-ops01`, 2026-08-31). RKE2 **v1.36.3+rke2r1**\npinned on every node. All node identity and cluster-plane traffic rides the\nnode LAN (VLAN 4, `10.72.0.0/23`); edge nodes additionally carry a WAN NIC\n(see [edge.md](edge.md)).\n\n```mermaid\nflowchart LR\n    subgraph LAN[\"Node LAN \u2014 VLAN 4, 10.72.0.0/23 (vmbr4)\"]\n        direction TB\n        subgraph CP[\"Control plane (embedded etcd)\"]\n            CP1[\"rke-cp01<br/>10.72.0.21 (bootstrap)\"]\n            CP2[\"rke-cp02<br/>10.72.0.22\"]\n            CP3[\"rke-cp03<br/>10.72.0.23\"]\n        end\n        VIP[\"API VIP 10.72.0.20<br/>keepalived VRRP\"]\n        subgraph PW[\"Platform workers<br/>wetopi.io/role=platform\"]\n            P1[\"rke-platform01<br/>10.72.0.41\"]\n            P2[\"rke-platform02<br/>10.72.0.42\"]\n            P3[\"rke-platform03<br/>10.72.0.43\"]\n        end\n        subgraph EW[\"Edge workers<br/>wetopi.io/role=edge (dual-NIC)\"]\n            E1[\"rke-wan01<br/>LAN 10.72.0.51 / WAN 79.137.114.193\"]\n            E2[\"rke-wan02<br/>LAN 10.72.0.52 / WAN 79.137.114.194\"]\n        end\n    end\n    VIP --- CP1 & CP2 & CP3\n    CP1 -->|server :9345 / API :6443| P1 & P2 & P3 & E1 & E2\n```\n\n| Node | LAN IP | Role / label | Notes |\n|---|---|---|---|\n| `rke-cp01` | `10.72.0.21` | control-plane (bootstrap) | `node-role.kubernetes.io/control-plane:NoSchedule` taint |\n| `rke-cp02` | `10.72.0.22` | control-plane | keepalived priority 100 |\n| `rke-cp03` | `10.72.0.23` | control-plane | keepalived priority 50 |\n| `rke-platform01..03` | `10.72.0.41-43` | `wetopi.io/role=platform` | host internal platform services |\n| `rke-wan01` | `10.72.0.51` | `wetopi.io/role=edge` | WAN `79.137.114.193` (eth0); `node-ip` bound to LAN |\n| `rke-wan02` | `10.72.0.52` | `wetopi.io/role=edge` | WAN `79.137.114.194` (eth0) |\n\nNode sizing and datastore placement (Proxmox nodes, vCPU/RAM/disk, `local`\nvs `vmssd`) are physical-infra facts \u2014 see the legacy\n[bootstrap VMs record](../../archive/platform/bootstrap-vms.md) until the\ninfra section reorg lands.\n\n## Control plane and API endpoint (ADR-0004)\n\n- **Embedded etcd** on the three control planes (no external etcd).\n- **API VIP `10.72.0.20`** owned by a keepalived VRRP group (VRID 51, unicast\n  peers) on the CP nodes: cp01 priority 150 (MASTER), cp02 100, cp03 50. The\n  health check probes the **local** kube-apiserver `https://127.0.0.1:6443`\n  and the RKE2 supervisor on `127.0.0.1:9345`, so a failing CP drops the VIP\n  to the next priority.\n- **Client endpoints**: server/registration URL `https://10.72.0.20:9345`,\n  API endpoint `https://10.72.0.20:6443` (kubeconfig clients). `tls-san` is\n  IP-only: the VIP + the three CP IPs, no DNS SAN.\n- The VIP is reachable **only on the node LAN** \u2014 never on the Internet\n  (ADR-0004).\n- RKE2 at-rest encryption of Kubernetes secrets is ACTIVE by default\n  (AES-CBC; verified enabled on all three CPs).\n\nThe decision record and failure-mode analysis:\n[ADR-0004](../../decisions/ADR-0004-rke2-api-endpoint.md).\n\n## Deployment model\n\nThe cluster is built by Ansible from the\n[infrastructure](https://github.com/wetopi-platform/infrastructure) repo \u2014\nAnsible owns node bootstrap only; everything inside the cluster is Argo CD's\n(see [gitops.md](gitops.md)):\n\n| Piece | Path in infrastructure | Role |\n|---|---|---|\n| `rke2-server` role + `rke2-cp.yml` | `ansible/roles/rke2-server/`, `ansible/playbooks/rke2-cp.yml` | renders `/etc/rancher/rke2/config.yaml` on CPs (bootstrap/join split), `tls-san`, CIDRs, `cni: none` |\n| `keepalived` role | `ansible/roles/keepalived/` | VRRP VIP `10.72.0.20` + local `/readyz`/9345 health check |\n| `rke2-agent` role + `rke2-workers.yml` | `ansible/roles/rke2-agent/`, `ansible/playbooks/rke2-workers.yml` | worker join (platform + edge), exact-label gates |\n| `rke2-workers-validate.yml` | `ansible/playbooks/` | post-join validation (data-dir, labels, scheduling) |\n| node token | `ansible/files/rke2/node-token` | mode `0600`, gitignored, never committed |\n\nRKE2 configuration: `cni: none` (no Canal; Cilium is the CNI \u2014\n[cilium-network.md](cilium-network.md)), `cluster-cidr: 10.64.0.0/14`,\n`service-cidr: 10.68.0.0/16`. Edge workers additionally carry\n`node-ip: 10.72.0.5x` (LAN-bound node identity, ADR-0006 condition 4) and the\nedge label/taint consumed by the edge Traefik placement.\n\n## Invariants\n\n- CP taint `NoSchedule`: control planes host no platform workloads.\n- Worker labels are exact: `wetopi.io/role=platform` / `wetopi.io/role=edge`\n  (asserted at join time; the CNI edge override and edge scheduling both key\n  off them).\n- All cluster-plane traffic (etcd, kubelet, VXLAN, service CIDR) stays on the\n  node LAN; the WAN interface of edge nodes carries only public 80/443\n  inbound (plus ordinary egress) \u2014 ADR-0006 condition 4.\n- The API VIP is the only stable client endpoint; per-CP IPs are internal.\n- No secrets in Git: join token and kubeconfigs exist only on hosts/ops01.\n\n## Verified state (2026-08-31, live from ops01)\n\n8/8 nodes Ready; RKE2 v1.36.3+rke2r1 on all nodes; embedded etcd quorum on\nthe three CPs. Dated observation \u2014 re-read `kubectl get nodes` before\nrelying on it (DocOps \u00a71: live state outranks this pill).\n\n## Dependencies\n\n- Depends on: node LAN (VLAN 4), the keepalived VIP design (ADR-0004), the\n  Cilium addon (no node is Ready without it \u2014 ADR-0005).\n- Depends on this: [Cilium network](cilium-network.md) (node roles, LAN\n  underlay), [edge v1](edge.md) (edge node NIC/label contract),\n  [GitOps plane](gitops.md) (in-cluster ownership boundary).\n\n## Failure considerations\n\n- Losing 1 CP: VIP fails over (etcd quorum survives 1 loss of 3); cluster\n  stays up. Losing 2: etcd quorum lost \u2014 restore path per the\n  [RKE2 apply & restore runbook](../../platform/rke2-apply-runbook.md).\n- Losing 1 edge worker: its A record keeps resolving until operator\n  remediation \u2014 reduced-availability semantics documented in\n  [edge.md](edge.md).\n- Losing 1 platform worker: reschedulable; no VIP/endpoint impact.",
  "html": "<blockquote id=\"bkmrk-archivo-%E2%80%94-versi%C3%B3n-mi\">\n<p><strong>ARCHIVO \u2014 versi\u00f3n migrada cruda.</strong> La ficha estructurada con las 3 preguntas es la versi\u00f3n <code>ARCH-*</code>/<code>SVC-*</code> de este mismo chapter (actualizada con datos live 2026-09-06). Esta copia cruda se conserva por el historial.</p>\n</blockquote>\n<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (architecture/platform/rke2-cluster.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-rke2-cluster-%E2%80%94-topol\">RKE2 cluster \u2014 topology, control plane and node roles</p>\n<h2 id=\"bkmrk-purpose\">Purpose</h2>\n<p id=\"bkmrk-how-the-kubernetes-c\">How the Kubernetes cluster layer is built on the Wetopi node LAN: node\ntopology, the HA control plane and its stable API endpoint, and the node-role\ncontract that the CNI, edge and GitOps planes all depend on.</p>\n<h2 id=\"bkmrk-topology\">Topology</h2>\n<p id=\"bkmrk-3-control-planes-%2B-3\">3 control planes + 3 platform workers + 2 edge workers = <strong>8 nodes</strong>, all\nReady (live-verified from <code>wetopi-ops01</code>, 2026-08-31). RKE2 <strong>v1.36.3+rke2r1</strong>\npinned on every node. All node identity and cluster-plane traffic rides the\nnode LAN (VLAN 4, <code>10.72.0.0/23</code>); edge nodes additionally carry a WAN NIC\n(see <a href=\"https://book.wetopi.net/edge.md\">edge.md</a>).</p>\n<pre id=\"bkmrk-flowchart-lr-subgrap\"><code class=\"language-mermaid\">flowchart LR\n    subgraph LAN[\"Node LAN \u2014 VLAN 4, 10.72.0.0/23 (vmbr4)\"]\n        direction TB\n        subgraph CP[\"Control plane (embedded etcd)\"]\n            CP1[\"rke-cp01&lt;br/&gt;10.72.0.21 (bootstrap)\"]\n            CP2[\"rke-cp02&lt;br/&gt;10.72.0.22\"]\n            CP3[\"rke-cp03&lt;br/&gt;10.72.0.23\"]\n        end\n        VIP[\"API VIP 10.72.0.20&lt;br/&gt;keepalived VRRP\"]\n        subgraph PW[\"Platform workers&lt;br/&gt;wetopi.io/role=platform\"]\n            P1[\"rke-platform01&lt;br/&gt;10.72.0.41\"]\n            P2[\"rke-platform02&lt;br/&gt;10.72.0.42\"]\n            P3[\"rke-platform03&lt;br/&gt;10.72.0.43\"]\n        end\n        subgraph EW[\"Edge workers&lt;br/&gt;wetopi.io/role=edge (dual-NIC)\"]\n            E1[\"rke-wan01&lt;br/&gt;LAN 10.72.0.51 / WAN 79.137.114.193\"]\n            E2[\"rke-wan02&lt;br/&gt;LAN 10.72.0.52 / WAN 79.137.114.194\"]\n        end\n    end\n    VIP --- CP1 &amp; CP2 &amp; CP3\n    CP1 --&gt;|server :9345 / API :6443| P1 &amp; P2 &amp; P3 &amp; E1 &amp; E2\n</code></pre>\n<table id=\"bkmrk-node-lan-ip-role-%2F-l\">\n<thead>\n<tr>\n<th>Node</th>\n<th>LAN IP</th>\n<th>Role / label</th>\n<th>Notes</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>rke-cp01</code></td>\n<td><code>10.72.0.21</code></td>\n<td>control-plane (bootstrap)</td>\n<td><code>node-role.kubernetes.io/control-plane:NoSchedule</code> taint</td>\n</tr>\n<tr>\n<td><code>rke-cp02</code></td>\n<td><code>10.72.0.22</code></td>\n<td>control-plane</td>\n<td>keepalived priority 100</td>\n</tr>\n<tr>\n<td><code>rke-cp03</code></td>\n<td><code>10.72.0.23</code></td>\n<td>control-plane</td>\n<td>keepalived priority 50</td>\n</tr>\n<tr>\n<td><code>rke-platform01..03</code></td>\n<td><code>10.72.0.41-43</code></td>\n<td><code>wetopi.io/role=platform</code></td>\n<td>host internal platform services</td>\n</tr>\n<tr>\n<td><code>rke-wan01</code></td>\n<td><code>10.72.0.51</code></td>\n<td><code>wetopi.io/role=edge</code></td>\n<td>WAN <code>79.137.114.193</code> (eth0); <code>node-ip</code> bound to LAN</td>\n</tr>\n<tr>\n<td><code>rke-wan02</code></td>\n<td><code>10.72.0.52</code></td>\n<td><code>wetopi.io/role=edge</code></td>\n<td>WAN <code>79.137.114.194</code> (eth0)</td>\n</tr>\n</tbody>\n</table>\n<p id=\"bkmrk-node-sizing-and-data\">Node sizing and datastore placement (Proxmox nodes, vCPU/RAM/disk, <code>local</code>\nvs <code>vmssd</code>) are physical-infra facts \u2014 see the legacy\n<a href=\"https://book.wetopi.net/archive/platform/bootstrap-vms.md\">bootstrap VMs record</a> until the\ninfra section reorg lands.</p>\n<h2 id=\"bkmrk-control-plane-and-ap\">Control plane and API endpoint (ADR-0004)</h2>\n<ul id=\"bkmrk-embedded-etcd-on-the\">\n<li><strong>Embedded etcd</strong> on the three control planes (no external etcd).</li>\n<li><strong>API VIP <code>10.72.0.20</code></strong> owned by a keepalived VRRP group (VRID 51, unicast\npeers) on the CP nodes: cp01 priority 150 (MASTER), cp02 100, cp03 50. The\nhealth check probes the <strong>local</strong> kube-apiserver <code>https://127.0.0.1:6443</code>\nand the RKE2 supervisor on <code>127.0.0.1:9345</code>, so a failing CP drops the VIP\nto the next priority.</li>\n<li><strong>Client endpoints</strong>: server/registration URL <code>https://10.72.0.20:9345</code>,\nAPI endpoint <code>https://10.72.0.20:6443</code> (kubeconfig clients). <code>tls-san</code> is\nIP-only: the VIP + the three CP IPs, no DNS SAN.</li>\n<li>The VIP is reachable <strong>only on the node LAN</strong> \u2014 never on the Internet\n(ADR-0004).</li>\n<li>RKE2 at-rest encryption of Kubernetes secrets is ACTIVE by default\n(AES-CBC; verified enabled on all three CPs).</li>\n</ul>\n<p id=\"bkmrk-the-decision-record-\">The decision record and failure-mode analysis:\n<a href=\"https://book.wetopi.net/decisions/ADR-0004-rke2-api-endpoint.md\">ADR-0004</a>.</p>\n<h2 id=\"bkmrk-deployment-model\">Deployment model</h2>\n<p id=\"bkmrk-the-cluster-is-built\">The cluster is built by Ansible from the\n<a href=\"https://github.com/wetopi-platform/infrastructure\">infrastructure</a> repo \u2014\nAnsible owns node bootstrap only; everything inside the cluster is Argo CD's\n(see <a href=\"https://book.wetopi.net/gitops.md\">gitops.md</a>):</p>\n<table id=\"bkmrk-piece-path-in-infras\">\n<thead>\n<tr>\n<th>Piece</th>\n<th>Path in infrastructure</th>\n<th>Role</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>rke2-server</code> role + <code>rke2-cp.yml</code></td>\n<td><code>ansible/roles/rke2-server/</code>, <code>ansible/playbooks/rke2-cp.yml</code></td>\n<td>renders <code>/etc/rancher/rke2/config.yaml</code> on CPs (bootstrap/join split), <code>tls-san</code>, CIDRs, <code>cni: none</code></td>\n</tr>\n<tr>\n<td><code>keepalived</code> role</td>\n<td><code>ansible/roles/keepalived/</code></td>\n<td>VRRP VIP <code>10.72.0.20</code> + local <code>/readyz</code>/9345 health check</td>\n</tr>\n<tr>\n<td><code>rke2-agent</code> role + <code>rke2-workers.yml</code></td>\n<td><code>ansible/roles/rke2-agent/</code>, <code>ansible/playbooks/rke2-workers.yml</code></td>\n<td>worker join (platform + edge), exact-label gates</td>\n</tr>\n<tr>\n<td><code>rke2-workers-validate.yml</code></td>\n<td><code>ansible/playbooks/</code></td>\n<td>post-join validation (data-dir, labels, scheduling)</td>\n</tr>\n<tr>\n<td>node token</td>\n<td><code>ansible/files/rke2/node-token</code></td>\n<td>mode <code>0600</code>, gitignored, never committed</td>\n</tr>\n</tbody>\n</table>\n<p id=\"bkmrk-rke2-configuration%3A-\">RKE2 configuration: <code>cni: none</code> (no Canal; Cilium is the CNI \u2014\n<a href=\"https://book.wetopi.net/cilium-network.md\">cilium-network.md</a>), <code>cluster-cidr: 10.64.0.0/14</code>,\n<code>service-cidr: 10.68.0.0/16</code>. Edge workers additionally carry\n<code>node-ip: 10.72.0.5x</code> (LAN-bound node identity, ADR-0006 condition 4) and the\nedge label/taint consumed by the edge Traefik placement.</p>\n<h2 id=\"bkmrk-invariants\">Invariants</h2>\n<ul id=\"bkmrk-cp-taint-noschedule%3A\">\n<li>CP taint <code>NoSchedule</code>: control planes host no platform workloads.</li>\n<li>Worker labels are exact: <code>wetopi.io/role=platform</code> / <code>wetopi.io/role=edge</code>\n(asserted at join time; the CNI edge override and edge scheduling both key\noff them).</li>\n<li>All cluster-plane traffic (etcd, kubelet, VXLAN, service CIDR) stays on the\nnode LAN; the WAN interface of edge nodes carries only public 80/443\ninbound (plus ordinary egress) \u2014 ADR-0006 condition 4.</li>\n<li>The API VIP is the only stable client endpoint; per-CP IPs are internal.</li>\n<li>No secrets in Git: join token and kubeconfigs exist only on hosts/ops01.</li>\n</ul>\n<h2 id=\"bkmrk-verified-state-%282026\">Verified state (2026-08-31, live from ops01)</h2>\n<p id=\"bkmrk-8%2F8-nodes-ready%3B-rke\">8/8 nodes Ready; RKE2 v1.36.3+rke2r1 on all nodes; embedded etcd quorum on\nthe three CPs. Dated observation \u2014 re-read <code>kubectl get nodes</code> before\nrelying on it (DocOps \u00a71: live state outranks this pill).</p>\n<h2 id=\"bkmrk-dependencies\">Dependencies</h2>\n<ul id=\"bkmrk-depends-on%3A-node-lan\">\n<li>Depends on: node LAN (VLAN 4), the keepalived VIP design (ADR-0004), the\nCilium addon (no node is Ready without it \u2014 ADR-0005).</li>\n<li>Depends on this: <a href=\"https://book.wetopi.net/cilium-network.md\">Cilium network</a> (node roles, LAN\nunderlay), <a href=\"https://book.wetopi.net/edge.md\">edge v1</a> (edge node NIC/label contract),\n<a href=\"https://book.wetopi.net/gitops.md\">GitOps plane</a> (in-cluster ownership boundary).</li>\n</ul>\n<h2 id=\"bkmrk-failure-consideratio\">Failure considerations</h2>\n<ul id=\"bkmrk-losing-1-cp%3A-vip-fai\">\n<li>Losing 1 CP: VIP fails over (etcd quorum survives 1 loss of 3); cluster\nstays up. Losing 2: etcd quorum lost \u2014 restore path per the\n<a href=\"https://book.wetopi.net/platform/rke2-apply-runbook.md\">RKE2 apply &amp; restore runbook</a>.</li>\n<li>Losing 1 edge worker: its A record keeps resolving until operator\nremediation \u2014 reduced-availability semantics documented in\n<a href=\"https://book.wetopi.net/edge.md\">edge.md</a>.</li>\n<li>Losing 1 platform worker: reschedulable; no VIP/endpoint impact.</li>\n</ul>\n"
 },
 {
  "id": 44,
  "title": "ADR 0001 rke2 node lan",
  "slug": "adr-0001-rke2-node-lan",
  "book_id": 8,
  "chapter_id": 28,
  "description": null,
  "created_at": "2026-09-06T13:38:56.000000Z",
  "updated_at": "2026-09-06T13:38:56.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (decisions/ADR-0001-rke2-node-lan.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n ADR-0001: Dedicated RKE2 node LAN\n\n**Date:** 2026-08-29\n**Status:** Accepted, pending implementation\n\n## Context\n\nThe existing `192.168.96.0/20` network is used by Ceph and legacy VMs. The new\nRKE2 foundation must not increase coupling with that network or require\nrenumbering when hosting workers grow.\n\n## Decision\n\n- Reserve `10.72.0.0/20` for Wetopi platform networks.\n- Use `10.72.0.0/23` for the initial RKE2 node LAN.\n- Use VLAN 4 across the OVH vRack.\n- Create custom VLAN interface `vlan_rke` and access bridge `vmbr4` on each\n  Proxmox node.\n- Do not assign an IP address to `vmbr4` on the hypervisors.\n- Use MTU 1500 until end-to-end jumbo-frame support is demonstrated.\n- Keep Pod CIDR `10.64.0.0/14` and Service CIDR `10.68.0.0/16` separate.\n- ~~Route, rather than NAT, traffic from the node LAN to Ceph.~~\n  **Superseded by [ADR-0007](ADR-0007-ceph-storage-native.md)** (operator\n  decision 2026-08-30): the Ceph clients (the 3 platform workers) attach\n  directly to the Ceph storage network (VLAN 2 / `vmbr2`,\n  `192.168.96.0/20`) with addresses from the operator-reserved\n  `192.168.98.0/25` \u2014 no routing and no NAT on the Ceph path. The\n  invariants this bullet protected are PRESERVED and unchanged: the Ceph\n  network is still never NATed on any path, and `192.168.96.0/20` is\n  still never renumbered; only the access model (routed vs directly\n  attached) is replaced.\n\n## Consequences\n\n- Node, Pod, Service, and Ceph networks are distinct.\n- The initial node LAN has 510 usable addresses.\n- Future networks can consume adjacent `/24` blocks from the reserved `/20`.\n- A router/NAT gateway is required before private VMs can reach the Internet.\n- VLAN 4 and the proposed CIDRs require validation outside Proxmox before\n  implementation.\n\n## Supersedes\n\nThis decision supersedes earlier drafts that left the node LAN undefined or\nimplicitly reused `192.168.96.0/20`.",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (decisions/ADR-0001-rke2-node-lan.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-adr-0001%3A-dedicated-\">ADR-0001: Dedicated RKE2 node LAN</p>\n<p id=\"bkmrk-date%3A-2026-08-29-sta\"><strong>Date:</strong> 2026-08-29\n<strong>Status:</strong> Accepted, pending implementation</p>\n<h2 id=\"bkmrk-context\">Context</h2>\n<p id=\"bkmrk-the-existing-192.168\">The existing <code>192.168.96.0/20</code> network is used by Ceph and legacy VMs. The new\nRKE2 foundation must not increase coupling with that network or require\nrenumbering when hosting workers grow.</p>\n<h2 id=\"bkmrk-decision\">Decision</h2>\n<ul id=\"bkmrk-reserve-10.72.0.0%2F20\">\n<li>Reserve <code>10.72.0.0/20</code> for Wetopi platform networks.</li>\n<li>Use <code>10.72.0.0/23</code> for the initial RKE2 node LAN.</li>\n<li>Use VLAN 4 across the OVH vRack.</li>\n<li>Create custom VLAN interface <code>vlan_rke</code> and access bridge <code>vmbr4</code> on each\nProxmox node.</li>\n<li>Do not assign an IP address to <code>vmbr4</code> on the hypervisors.</li>\n<li>Use MTU 1500 until end-to-end jumbo-frame support is demonstrated.</li>\n<li>Keep Pod CIDR <code>10.64.0.0/14</code> and Service CIDR <code>10.68.0.0/16</code> separate.</li>\n<li><s>Route, rather than NAT, traffic from the node LAN to Ceph.</s>\n<strong>Superseded by <a href=\"https://book.wetopi.net/ADR-0007-ceph-storage-native.md\">ADR-0007</a></strong> (operator\ndecision 2026-08-30): the Ceph clients (the 3 platform workers) attach\ndirectly to the Ceph storage network (VLAN 2 / <code>vmbr2</code>,\n<code>192.168.96.0/20</code>) with addresses from the operator-reserved\n<code>192.168.98.0/25</code> \u2014 no routing and no NAT on the Ceph path. The\ninvariants this bullet protected are PRESERVED and unchanged: the Ceph\nnetwork is still never NATed on any path, and <code>192.168.96.0/20</code> is\nstill never renumbered; only the access model (routed vs directly\nattached) is replaced.</li>\n</ul>\n<h2 id=\"bkmrk-consequences\">Consequences</h2>\n<ul id=\"bkmrk-node%2C-pod%2C-service%2C-\">\n<li>Node, Pod, Service, and Ceph networks are distinct.</li>\n<li>The initial node LAN has 510 usable addresses.</li>\n<li>Future networks can consume adjacent <code>/24</code> blocks from the reserved <code>/20</code>.</li>\n<li>A router/NAT gateway is required before private VMs can reach the Internet.</li>\n<li>VLAN 4 and the proposed CIDRs require validation outside Proxmox before\nimplementation.</li>\n</ul>\n<h2 id=\"bkmrk-supersedes\">Supersedes</h2>\n<p id=\"bkmrk-this-decision-supers\">This decision supersedes earlier drafts that left the node LAN undefined or\nimplicitly reused <code>192.168.96.0/20</code>.</p>\n"
 },
 {
  "id": 45,
  "title": "ADR 0002 bootstrap sizing",
  "slug": "adr-0002-bootstrap-sizing",
  "book_id": 8,
  "chapter_id": 28,
  "description": null,
  "created_at": "2026-09-06T13:38:57.000000Z",
  "updated_at": "2026-09-06T13:38:57.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (decisions/ADR-0002-bootstrap-sizing.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n ADR-0002: Minimal expandable bootstrap sizing\n\n**Date:** 2026-08-29\n**Status:** Accepted, pending implementation\n\n## Context\n\nThe RKE2 foundation needs production-shaped HA without reserving the larger\nresources proposed in early planning documents before actual utilization is\nknown.\n\n## Decision\n\n- Start with three control planes at 4 vCPU, 8 GiB RAM, and 50 GiB local disk.\n- Start with three platform workers at 4 vCPU, 8 GiB RAM, and 60 GiB shared\n  disk.\n- Start the external operations VM at 2 vCPU, 4 GiB RAM, and 40 GiB shared\n  disk.\n- Start the platform NAT gateway at 2 vCPU, 2 GiB RAM, and 20 GiB shared disk.\n- Disable memory ballooning.\n- Expand CPU, RAM, and disk after observation instead of provisioning the\n  former upper bounds immediately.\n\n## Consequences\n\n- The eight running VMs initially reserve 28 vCPU, 54 GiB RAM, and 390 GiB\n  disk.\n- Three-node etcd quorum is preserved.\n- Platform workers must be expanded before deploying memory-heavy\n  observability or agent services.\n- Resource headroom and placement are rechecked immediately before creation.\n\n## Supersedes\n\nThis decision replaces the larger initial allocations in earlier bootstrap\ndrafts while retaining their intended node roles and failure-domain separation.",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (decisions/ADR-0002-bootstrap-sizing.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-adr-0002%3A-minimal-ex\">ADR-0002: Minimal expandable bootstrap sizing</p>\n<p id=\"bkmrk-date%3A-2026-08-29-sta\"><strong>Date:</strong> 2026-08-29\n<strong>Status:</strong> Accepted, pending implementation</p>\n<h2 id=\"bkmrk-context\">Context</h2>\n<p id=\"bkmrk-the-rke2-foundation-\">The RKE2 foundation needs production-shaped HA without reserving the larger\nresources proposed in early planning documents before actual utilization is\nknown.</p>\n<h2 id=\"bkmrk-decision\">Decision</h2>\n<ul id=\"bkmrk-start-with-three-con\">\n<li>Start with three control planes at 4 vCPU, 8 GiB RAM, and 50 GiB local disk.</li>\n<li>Start with three platform workers at 4 vCPU, 8 GiB RAM, and 60 GiB shared\ndisk.</li>\n<li>Start the external operations VM at 2 vCPU, 4 GiB RAM, and 40 GiB shared\ndisk.</li>\n<li>Start the platform NAT gateway at 2 vCPU, 2 GiB RAM, and 20 GiB shared disk.</li>\n<li>Disable memory ballooning.</li>\n<li>Expand CPU, RAM, and disk after observation instead of provisioning the\nformer upper bounds immediately.</li>\n</ul>\n<h2 id=\"bkmrk-consequences\">Consequences</h2>\n<ul id=\"bkmrk-the-eight-running-vm\">\n<li>The eight running VMs initially reserve 28 vCPU, 54 GiB RAM, and 390 GiB\ndisk.</li>\n<li>Three-node etcd quorum is preserved.</li>\n<li>Platform workers must be expanded before deploying memory-heavy\nobservability or agent services.</li>\n<li>Resource headroom and placement are rechecked immediately before creation.</li>\n</ul>\n<h2 id=\"bkmrk-supersedes\">Supersedes</h2>\n<p id=\"bkmrk-this-decision-replac\">This decision replaces the larger initial allocations in earlier bootstrap\ndrafts while retaining their intended node roles and failure-domain separation.</p>\n"
 },
 {
  "id": 46,
  "title": "ADR 0003 opentofu ansible",
  "slug": "adr-0003-opentofu-ansible",
  "book_id": 8,
  "chapter_id": 28,
  "description": null,
  "created_at": "2026-09-06T13:38:57.000000Z",
  "updated_at": "2026-09-06T13:38:57.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (decisions/ADR-0003-opentofu-ansible.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n ADR-0003: OpenTofu and Ansible responsibility split\n\n**Date:** 2026-08-29\n**Status:** Accepted\n\n## Context\n\nProxmox resources and Ubuntu configuration have different lifecycles. Using\none tool for both would either weaken infrastructure state management or rely\non fragile remote provisioners.\n\n## Decision\n\n- OpenTofu owns Proxmox resources through its HTTPS API.\n- cloud-init owns only the first-boot identity, network, and SSH bootstrap.\n- Ansible owns the Ubuntu baseline, NAT gateway, and RKE2 configuration.\n- Argo CD owns Kubernetes add-ons and workloads after the cluster exists.\n- SSH access to Proxmox hypervisors is not required for the selected resource\n  operations.\n\n## Consequences\n\n- OpenTofu plans and records virtual infrastructure lifecycle.\n- Ansible can repeatedly converge guest configuration without changing VM\n  identity or placement.\n- No secrets are embedded in HCL or committed inventory.\n- Disaster recovery can reconstruct both infrastructure and guest state from\n  separate, explicit sources of truth.",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (decisions/ADR-0003-opentofu-ansible.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-adr-0003%3A-opentofu-a\">ADR-0003: OpenTofu and Ansible responsibility split</p>\n<p id=\"bkmrk-date%3A-2026-08-29-sta\"><strong>Date:</strong> 2026-08-29\n<strong>Status:</strong> Accepted</p>\n<h2 id=\"bkmrk-context\">Context</h2>\n<p id=\"bkmrk-proxmox-resources-an\">Proxmox resources and Ubuntu configuration have different lifecycles. Using\none tool for both would either weaken infrastructure state management or rely\non fragile remote provisioners.</p>\n<h2 id=\"bkmrk-decision\">Decision</h2>\n<ul id=\"bkmrk-opentofu-owns-proxmo\">\n<li>OpenTofu owns Proxmox resources through its HTTPS API.</li>\n<li>cloud-init owns only the first-boot identity, network, and SSH bootstrap.</li>\n<li>Ansible owns the Ubuntu baseline, NAT gateway, and RKE2 configuration.</li>\n<li>Argo CD owns Kubernetes add-ons and workloads after the cluster exists.</li>\n<li>SSH access to Proxmox hypervisors is not required for the selected resource\noperations.</li>\n</ul>\n<h2 id=\"bkmrk-consequences\">Consequences</h2>\n<ul id=\"bkmrk-opentofu-plans-and-r\">\n<li>OpenTofu plans and records virtual infrastructure lifecycle.</li>\n<li>Ansible can repeatedly converge guest configuration without changing VM\nidentity or placement.</li>\n<li>No secrets are embedded in HCL or committed inventory.</li>\n<li>Disaster recovery can reconstruct both infrastructure and guest state from\nseparate, explicit sources of truth.</li>\n</ul>\n"
 },
 {
  "id": 47,
  "title": "ADR 0004 rke2 api endpoint",
  "slug": "adr-0004-rke2-api-endpoint",
  "book_id": 8,
  "chapter_id": 28,
  "description": null,
  "created_at": "2026-09-06T13:38:58.000000Z",
  "updated_at": "2026-09-06T13:38:58.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (decisions/ADR-0004-rke2-api-endpoint.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n ADR-0004: RKE2 API endpoint and routing to Ceph\n\n**Date:** 2026-08-29\n**Status:** Proposed \u2014 API endpoint in force; the **Ceph routing decision\nbelow is superseded by [ADR-0007](ADR-0007-ceph-storage-native.md)**\n(operator decision 2026-08-30: storage-native attachment, no routing, no\nnatgw involvement). The API endpoint design (keepalived VIP 10.72.0.20)\nis unaffected and remains in force.\n\n## Context\n\nRKE2 needs one stable control-plane endpoint, and the infrastructure README\ngates RKE2 installation on that endpoint and on routing to Ceph being\naccepted. Joining servers and agents register against port 9345\n(`server: https://<endpoint>:9345`); kube-apiserver serves 6443. A VIP is\nnot automatically included in the apiserver serving certificate, so it must\nbe added with `tls-san`. The SAN list is fixed at bootstrap; adding a SAN\nlater forces per-node certificate rotation. After a node joins, its rke2\nagent load-balances locally (port 6444) across the apiserver endpoints\ndiscovered from etcd, so the configured `server:` URL matters only for\ninitial or full re-registration.\n\nNo DNS is controlled anywhere in the platform (VMs use public resolvers),\nso bootstrap-safe endpoints are limited to an IP URL or `/etc/hosts`\nentries. The node LAN address plan already reserves `10.72.0.20` as the\nstable RKE2 API VIP. Nothing routes node-LAN traffic to Ceph\n`192.168.96.0/20` today: the gateway has no VLAN 2 interface, no static\nroute, and its nftables forward policy drops everything except LAN-to-WAN.\nCandidate endpoints:\n\n- Gateway VIP `10.72.0.1` on `wetopi-natgw01`: rejected. It couples API\n  availability to a 2 GiB egress gateway VM, whose loss would take the API\n  and node egress down together, contradicts the reserved `10.72.0.20`, and\n  would flap the API with future gateway VRRP changes.\n- Round-robin DNS over the control-plane IPs: needs DNS control that does\n  not exist; the `/etc/hosts` variant is unreliable (glibc typically returns\n  only the first matching entry) and must be kept in sync on eight nodes.\n- TCP load balancer on `wetopi-ops01`: workable without Proxmox firewall\n  changes, but makes the deliberately minimal recovery bastion the API\n  single point of failure and adds a long-lived service to it.\n- Direct server URL to `10.72.0.21` with documented manual failover: zero\n  extra SANs and the lowest operational burden, but fragile exactly when\n  re-registration coincides with a downed control plane; upstream warns\n  node IPs may change over time.\n- Dedicated API VIP `10.72.0.20` on the three control planes via\n  keepalived: already reserved in the address plan, lives inside the\n  cluster failure domain, adds no VM or new single point of failure,\n  availability tracks local apiserver readiness including etcd\n  connectivity, and costs one new Ansible role.\n\nThe design set carries a standing proposal for this endpoint: \"2 x HAProxy\n+ Keepalived, private VIP\" (master architecture plan, section 16 \"Endpoint\nHA de la API RKE2\"; decisions log, section 29.2). Its status is OPEN, to be\nclosed before the definitive cluster install (action NOW-05); it is\ndescribed as conservative and compatible with existing operational\nknowledge, but not yet approved as a final decision.\n\n## Decision\n\n- Adopt the dedicated API VIP: a keepalived VRRP group on VLAN 4 across\n  `rke-cp01/02/03` owns `10.72.0.20`.\n- This supersedes the standing proposal and closes it: the CP-owned\n  keepalived VIP, with no separate HAProxy boxes, is the endpoint design\n  that satisfies NOW-05. Rationale: the HAProxy pair would add two new\n  VMs and roles for a capability the VIP alone already provides at this\n  scale; the three control planes terminate 9345 and 6443 directly, and\n  both ports are served by the same process set, so a TCP load balancer\n  in front adds a hop and a failure domain without adding correctness;\n  keeping the VIP on the control planes keeps the endpoint inside the\n  cluster failure domain, in line with the v1 constraint of simplicity\n  over sophistication (no unnecessary new infrastructure roles).\n  Trade-off acknowledged: without a separate LB layer there is no place\n  for per-backend health-aware balancing beyond VRRP failover\n  granularity; accepted for v1 at this scale.\n- `rke-cp01` is MASTER by priority, failing over to `rke-cp02`, then\n  `rke-cp03`. VRID and priority values are implementation parameters;\n  no other VRRP is documented on VLAN 4.\n- Holding the VIP is gated on local health: a control plane keeps the\n  VIP only while its local apiserver is ready (`/readyz`, which includes\n  etcd-backed readiness and fails when the local apiserver cannot reach\n  etcd as part of a quorum) and its local registration service on 9345\n  is reachable. Exact check commands, scripts, and timings are\n  implementation parameters.\n  - For 6443 (kubectl and kubeconfig clients) and 9345 (new node\n    registration) alike, this places the VIP only on a control plane\n    whose apiserver reports ready including etcd connectivity: a\n    strictly stronger signal than a TCP listen check on 6443, which\n    proves neither etcd-backed readiness nor a live 9345.\n  - It is not a global quorum oracle. With one control plane down, the\n    remaining owners are etcd-ready and keep serving both ports. With\n    two down, the last control plane's `/readyz` fails, it releases the\n    VIP, and the endpoint is down, which is correct: a usable API\n    requires etcd quorum (at least two control planes). The check does\n    not create quorum; it avoids pointing the endpoint at an apiserver\n    that already reports not ready.\n- Agents and joining servers use `server: https://10.72.0.20:9345`;\n  kubeconfig and kubectl clients use `https://10.72.0.20:6443`. IP-only, no\n  hostname SAN: no DNS is controlled, and this avoids a split-horizon\n  hazard.\n- Set `tls-san` identically on all three servers to the VIP plus all\n  control-plane IPs: `10.72.0.20`, `10.72.0.21`, `10.72.0.22`,\n  `10.72.0.23`, so any control-plane IP works for direct registration or\n  clients.\n- Bootstrap order: because every control-plane IP is in the SAN list,\n  initial bring-up may register directly against `10.72.0.21:9345` before\n  keepalived is deployed, then switch `server:` URLs to the VIP; no\n  certificate rotation is needed either way.\n- ~~Route, do not NAT, node-LAN traffic to Ceph `192.168.96.0/20` (per\n  ADR-0001), using gateway-side routing on `wetopi-natgw01`.~~\n  **SUPERSEDED by [ADR-0007](ADR-0007-ceph-storage-native.md)** (operator\n  decision 2026-08-30): the Ceph clients (the 3 platform workers) attach\n  directly to VLAN 2 / `vmbr2` with addresses from the reserved\n  `192.168.98.0/25` \u2014 no routing, no NAT, no return-routes, no natgw\n  involvement. The bullet below is retained as the historical record of\n  the superseded design; its shape never left the default-off state.\n  Shape (superseded):\n  - give the gateway a third interface on VLAN 2 / `vmbr2` with an\n    address in `192.168.96.0/20` (netplan; the OpenTofu VM module already\n    supports extra NICs), making the Ceph subnet a directly connected\n    route with no next-hop;\n  - add no routes on the node LAN: the default gateway `10.72.0.1` already\n    covers the destination once the gateway owns the connected route;\n  - Ceph-side return path: hosts in `192.168.96.0/20` need a route for\n    `10.72.0.0/23` via the gateway's VLAN 2 address, shaped as\n    `ip route 10.72.0.0/23 via <natgw VLAN 2 address>`;\n  - add nftables accept rules in the gateway forward chain for\n    `iifname <lan_if> ip daddr 192.168.96.0/20` and the return direction\n    `oifname <lan_if> ip saddr 192.168.96.0/20`.\n- Keep MTU at 1500 per ADR-0001, to be confirmed against the live\n  VLAN 2 MTU (a pre-implementation gate). MTU 1500 is carried over\n  unchanged by ADR-0007.\n\n## Failure modes\n\nEffects are stated for each scenario on: API reachability (6443 via the\nVIP for kubectl and new kubeconfigs), new node registration (9345 join),\nalready-joined agents (client-side LB on 6444 across surviving\ncontrol planes), Internet egress (nodes' default gateway `10.72.0.1` on\n`wetopi-natgw01`), and Ceph connectivity (since ADR-0007: the workers'\ndirect VLAN 2 attachment \u2014 storage no longer depends on the gateway at\nall; the pre-ADR-0007 routing remarks below are retained as the\nhistorical record).\n\n- **VIP-owner CP death (`rke-cp01` dies).** VRRP fails the VIP over to\n  `rke-cp02` within the failover time: a brief interruption of API\n  reachability and of new node registration, then both served by the new\n  owner. Etcd keeps quorum (2 of 3). Already-joined agents are\n  unaffected: their 6444 LB already spans the surviving control planes.\n  Internet egress and Ceph routing are unaffected.\n- **Non-owner CP death (`rke-cp03` dies while not holding the VIP).** No\n  VIP movement. API reachability and new node registration are\n  unaffected; etcd keeps quorum (2 of 3). Already-joined agents\n  re-balance around the dead control plane on 6444. Internet egress and\n  Ceph routing are unaffected.\n- **Endpoint service degradation (apiserver process up, etcd degraded\n  or quorum lost).** With the `/readyz`-gated check, a degraded owner\n  does not hold the VIP: if etcd degrades on the owner only, the VIP\n  moves to a healthy owner and API reachability and registration\n  continue. If quorum is lost cluster-wide, no control plane passes the\n  check, none holds the VIP, and the API is intentionally unavailable\n  regardless of VIP placement: a usable API requires etcd quorum, and\n  the endpoint must not present an API that cannot serve.\n  Already-joined agents see failing API requests for as long as quorum\n  is lost (an etcd failure, not an endpoint failure) and resume when\n  quorum returns. Internet egress and Ceph routing are unaffected.\n- **`wetopi-natgw01` death.** The VIP lives on the node LAN, so API\n  reachability and new node registration are unaffected (this is\n  exactly why the gateway VIP was rejected), and already-joined agents\n  are unaffected. Internet egress is down for all nodes (default\n  gateway `10.72.0.1` lost). ~~Ceph routing is down (the sole proposed\n  route rides the gateway's VLAN 2 leg).~~ **Since ADR-0007, Ceph\n  connectivity is UNAFFECTED by a natgw death**: the workers reach Ceph\n  directly on VLAN 2. (Historical record: under the superseded routing\n  design the gateway would have been a single point of failure for both\n  Internet egress and Ceph routing; gateway redundancy stays out of\n  scope either way, see Consequences.)\n\n## Consequences\n\n- VIP loss blocks new node registrations and new kubeconfig clients;\n  already-joined agents keep working through their client-side load\n  balancer (port 6444) against the surviving control planes, and running\n  workloads are unaffected.\n- Rollback is cheap: revert `server:` URLs to a control-plane IP (for\n  example `https://10.72.0.21:9345`) and uninstall keepalived. No\n  certificate rotation is needed because all control-plane IPs are already\n  in the SAN list.\n- The stable endpoint survives control-plane renumbering or reprovisioning,\n  kubeconfigs never change, and accepting this ADR satisfies the\n  infrastructure README gate, letting RKE2 installation proceed.\n- VRRP is a new operational surface on the node LAN (VRID collision care),\n  and keepalived on the control planes adds guest configuration that must\n  stay Ansible-owned per ADR-0003.\n- A later move to a DNS-name endpoint would need a SAN addition plus\n  certificate rotation.\n- ~~Pre-implementation gates for the Ceph route~~ \u2014 **closed by\n  ADR-0007** (the route itself is superseded; the address allocation is\n  now the operator-reserved `192.168.98.0/25`, and no return-route owner,\n  gateway firewall path or rp_filter verification is needed \u2014 there is no\n  gateway on the path. The live VLAN 2 MTU check (1500, ADR-0001) and the\n  VPN CIDR overlap validation survive as apply-time gates in\n  `infrastructure/kubernetes/ceph-csi/VALIDATION.md`). Historical record\n  of the superseded gates:\n  - VLAN 2 address allocation: the exact address for the gateway's third\n    NIC in `192.168.96.0/20`, confirmed against live Ceph allocations;\n  - Ceph-side return-route owner: which L3 device or hosts on the Ceph\n    side own the return route for `10.72.0.0/23` via the gateway's VLAN 2\n    address;\n  - firewall path: the nftables accept rules needed in both directions,\n    including any Ceph-side firewall;\n  - `rp_filter` behavior: the gateway enforces `rp_filter=1`, and\n    asymmetric replies could be dropped; must be field-verified;\n  - live MTU on VLAN 2: actual MTU and jumbo-frame support verified\n    against the 1500 kept per ADR-0001;\n  - VPN CIDR overlap validation for `10.64.0.0/14` and `10.68.0.0/16`.\n- Out of scope, to be decided separately: gateway keepalived redundancy for\n  `10.72.0.1`, any DNS infrastructure, MTU changes, and storage design.",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (decisions/ADR-0004-rke2-api-endpoint.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-adr-0004%3A-rke2-api-e\">ADR-0004: RKE2 API endpoint and routing to Ceph</p>\n<p id=\"bkmrk-date%3A-2026-08-29-sta\"><strong>Date:</strong> 2026-08-29\n<strong>Status:</strong> Proposed \u2014 API endpoint in force; the <strong>Ceph routing decision\nbelow is superseded by <a href=\"https://book.wetopi.net/ADR-0007-ceph-storage-native.md\">ADR-0007</a></strong>\n(operator decision 2026-08-30: storage-native attachment, no routing, no\nnatgw involvement). The API endpoint design (keepalived VIP 10.72.0.20)\nis unaffected and remains in force.</p>\n<h2 id=\"bkmrk-context\">Context</h2>\n<p id=\"bkmrk-rke2-needs-one-stabl\">RKE2 needs one stable control-plane endpoint, and the infrastructure README\ngates RKE2 installation on that endpoint and on routing to Ceph being\naccepted. Joining servers and agents register against port 9345\n(<code>server: https://&lt;endpoint&gt;:9345</code>); kube-apiserver serves 6443. A VIP is\nnot automatically included in the apiserver serving certificate, so it must\nbe added with <code>tls-san</code>. The SAN list is fixed at bootstrap; adding a SAN\nlater forces per-node certificate rotation. After a node joins, its rke2\nagent load-balances locally (port 6444) across the apiserver endpoints\ndiscovered from etcd, so the configured <code>server:</code> URL matters only for\ninitial or full re-registration.</p>\n<p id=\"bkmrk-no-dns-is-controlled\">No DNS is controlled anywhere in the platform (VMs use public resolvers),\nso bootstrap-safe endpoints are limited to an IP URL or <code>/etc/hosts</code>\nentries. The node LAN address plan already reserves <code>10.72.0.20</code> as the\nstable RKE2 API VIP. Nothing routes node-LAN traffic to Ceph\n<code>192.168.96.0/20</code> today: the gateway has no VLAN 2 interface, no static\nroute, and its nftables forward policy drops everything except LAN-to-WAN.\nCandidate endpoints:</p>\n<ul id=\"bkmrk-gateway-vip-10.72.0.\">\n<li>Gateway VIP <code>10.72.0.1</code> on <code>wetopi-natgw01</code>: rejected. It couples API\navailability to a 2 GiB egress gateway VM, whose loss would take the API\nand node egress down together, contradicts the reserved <code>10.72.0.20</code>, and\nwould flap the API with future gateway VRRP changes.</li>\n<li>Round-robin DNS over the control-plane IPs: needs DNS control that does\nnot exist; the <code>/etc/hosts</code> variant is unreliable (glibc typically returns\nonly the first matching entry) and must be kept in sync on eight nodes.</li>\n<li>TCP load balancer on <code>wetopi-ops01</code>: workable without Proxmox firewall\nchanges, but makes the deliberately minimal recovery bastion the API\nsingle point of failure and adds a long-lived service to it.</li>\n<li>Direct server URL to <code>10.72.0.21</code> with documented manual failover: zero\nextra SANs and the lowest operational burden, but fragile exactly when\nre-registration coincides with a downed control plane; upstream warns\nnode IPs may change over time.</li>\n<li>Dedicated API VIP <code>10.72.0.20</code> on the three control planes via\nkeepalived: already reserved in the address plan, lives inside the\ncluster failure domain, adds no VM or new single point of failure,\navailability tracks local apiserver readiness including etcd\nconnectivity, and costs one new Ansible role.</li>\n</ul>\n<p id=\"bkmrk-the-design-set-carri\">The design set carries a standing proposal for this endpoint: \"2 x HAProxy</p>\n<ul id=\"bkmrk-keepalived%2C-private-\">\n<li>Keepalived, private VIP\" (master architecture plan, section 16 \"Endpoint\nHA de la API RKE2\"; decisions log, section 29.2). Its status is OPEN, to be\nclosed before the definitive cluster install (action NOW-05); it is\ndescribed as conservative and compatible with existing operational\nknowledge, but not yet approved as a final decision.</li>\n</ul>\n<h2 id=\"bkmrk-decision\">Decision</h2>\n<ul id=\"bkmrk-adopt-the-dedicated-\">\n<li>Adopt the dedicated API VIP: a keepalived VRRP group on VLAN 4 across\n<code>rke-cp01/02/03</code> owns <code>10.72.0.20</code>.</li>\n<li>This supersedes the standing proposal and closes it: the CP-owned\nkeepalived VIP, with no separate HAProxy boxes, is the endpoint design\nthat satisfies NOW-05. Rationale: the HAProxy pair would add two new\nVMs and roles for a capability the VIP alone already provides at this\nscale; the three control planes terminate 9345 and 6443 directly, and\nboth ports are served by the same process set, so a TCP load balancer\nin front adds a hop and a failure domain without adding correctness;\nkeeping the VIP on the control planes keeps the endpoint inside the\ncluster failure domain, in line with the v1 constraint of simplicity\nover sophistication (no unnecessary new infrastructure roles).\nTrade-off acknowledged: without a separate LB layer there is no place\nfor per-backend health-aware balancing beyond VRRP failover\ngranularity; accepted for v1 at this scale.</li>\n<li><code>rke-cp01</code> is MASTER by priority, failing over to <code>rke-cp02</code>, then\n<code>rke-cp03</code>. VRID and priority values are implementation parameters;\nno other VRRP is documented on VLAN 4.</li>\n<li>Holding the VIP is gated on local health: a control plane keeps the\nVIP only while its local apiserver is ready (<code>/readyz</code>, which includes\netcd-backed readiness and fails when the local apiserver cannot reach\netcd as part of a quorum) and its local registration service on 9345\nis reachable. Exact check commands, scripts, and timings are\nimplementation parameters.\n<ul>\n<li>For 6443 (kubectl and kubeconfig clients) and 9345 (new node\nregistration) alike, this places the VIP only on a control plane\nwhose apiserver reports ready including etcd connectivity: a\nstrictly stronger signal than a TCP listen check on 6443, which\nproves neither etcd-backed readiness nor a live 9345.</li>\n<li>It is not a global quorum oracle. With one control plane down, the\nremaining owners are etcd-ready and keep serving both ports. With\ntwo down, the last control plane's <code>/readyz</code> fails, it releases the\nVIP, and the endpoint is down, which is correct: a usable API\nrequires etcd quorum (at least two control planes). The check does\nnot create quorum; it avoids pointing the endpoint at an apiserver\nthat already reports not ready.</li>\n</ul>\n</li>\n<li>Agents and joining servers use <code>server: https://10.72.0.20:9345</code>;\nkubeconfig and kubectl clients use <code>https://10.72.0.20:6443</code>. IP-only, no\nhostname SAN: no DNS is controlled, and this avoids a split-horizon\nhazard.</li>\n<li>Set <code>tls-san</code> identically on all three servers to the VIP plus all\ncontrol-plane IPs: <code>10.72.0.20</code>, <code>10.72.0.21</code>, <code>10.72.0.22</code>,\n<code>10.72.0.23</code>, so any control-plane IP works for direct registration or\nclients.</li>\n<li>Bootstrap order: because every control-plane IP is in the SAN list,\ninitial bring-up may register directly against <code>10.72.0.21:9345</code> before\nkeepalived is deployed, then switch <code>server:</code> URLs to the VIP; no\ncertificate rotation is needed either way.</li>\n<li><s>Route, do not NAT, node-LAN traffic to Ceph <code>192.168.96.0/20</code> (per\nADR-0001), using gateway-side routing on <code>wetopi-natgw01</code>.</s>\n<strong>SUPERSEDED by <a href=\"https://book.wetopi.net/ADR-0007-ceph-storage-native.md\">ADR-0007</a></strong> (operator\ndecision 2026-08-30): the Ceph clients (the 3 platform workers) attach\ndirectly to VLAN 2 / <code>vmbr2</code> with addresses from the reserved\n<code>192.168.98.0/25</code> \u2014 no routing, no NAT, no return-routes, no natgw\ninvolvement. The bullet below is retained as the historical record of\nthe superseded design; its shape never left the default-off state.\nShape (superseded):\n<ul>\n<li>give the gateway a third interface on VLAN 2 / <code>vmbr2</code> with an\naddress in <code>192.168.96.0/20</code> (netplan; the OpenTofu VM module already\nsupports extra NICs), making the Ceph subnet a directly connected\nroute with no next-hop;</li>\n<li>add no routes on the node LAN: the default gateway <code>10.72.0.1</code> already\ncovers the destination once the gateway owns the connected route;</li>\n<li>Ceph-side return path: hosts in <code>192.168.96.0/20</code> need a route for\n<code>10.72.0.0/23</code> via the gateway's VLAN 2 address, shaped as\n<code>ip route 10.72.0.0/23 via &lt;natgw VLAN 2 address&gt;</code>;</li>\n<li>add nftables accept rules in the gateway forward chain for\n<code>iifname &lt;lan_if&gt; ip daddr 192.168.96.0/20</code> and the return direction\n<code>oifname &lt;lan_if&gt; ip saddr 192.168.96.0/20</code>.</li>\n</ul>\n</li>\n<li>Keep MTU at 1500 per ADR-0001, to be confirmed against the live\nVLAN 2 MTU (a pre-implementation gate). MTU 1500 is carried over\nunchanged by ADR-0007.</li>\n</ul>\n<h2 id=\"bkmrk-failure-modes\">Failure modes</h2>\n<p id=\"bkmrk-effects-are-stated-f\">Effects are stated for each scenario on: API reachability (6443 via the\nVIP for kubectl and new kubeconfigs), new node registration (9345 join),\nalready-joined agents (client-side LB on 6444 across surviving\ncontrol planes), Internet egress (nodes' default gateway <code>10.72.0.1</code> on\n<code>wetopi-natgw01</code>), and Ceph connectivity (since ADR-0007: the workers'\ndirect VLAN 2 attachment \u2014 storage no longer depends on the gateway at\nall; the pre-ADR-0007 routing remarks below are retained as the\nhistorical record).</p>\n<ul id=\"bkmrk-vip-owner-cp-death-%28\">\n<li><strong>VIP-owner CP death (<code>rke-cp01</code> dies).</strong> VRRP fails the VIP over to\n<code>rke-cp02</code> within the failover time: a brief interruption of API\nreachability and of new node registration, then both served by the new\nowner. Etcd keeps quorum (2 of 3). Already-joined agents are\nunaffected: their 6444 LB already spans the surviving control planes.\nInternet egress and Ceph routing are unaffected.</li>\n<li><strong>Non-owner CP death (<code>rke-cp03</code> dies while not holding the VIP).</strong> No\nVIP movement. API reachability and new node registration are\nunaffected; etcd keeps quorum (2 of 3). Already-joined agents\nre-balance around the dead control plane on 6444. Internet egress and\nCeph routing are unaffected.</li>\n<li><strong>Endpoint service degradation (apiserver process up, etcd degraded\nor quorum lost).</strong> With the <code>/readyz</code>-gated check, a degraded owner\ndoes not hold the VIP: if etcd degrades on the owner only, the VIP\nmoves to a healthy owner and API reachability and registration\ncontinue. If quorum is lost cluster-wide, no control plane passes the\ncheck, none holds the VIP, and the API is intentionally unavailable\nregardless of VIP placement: a usable API requires etcd quorum, and\nthe endpoint must not present an API that cannot serve.\nAlready-joined agents see failing API requests for as long as quorum\nis lost (an etcd failure, not an endpoint failure) and resume when\nquorum returns. Internet egress and Ceph routing are unaffected.</li>\n<li><strong><code>wetopi-natgw01</code> death.</strong> The VIP lives on the node LAN, so API\nreachability and new node registration are unaffected (this is\nexactly why the gateway VIP was rejected), and already-joined agents\nare unaffected. Internet egress is down for all nodes (default\ngateway <code>10.72.0.1</code> lost). <s>Ceph routing is down (the sole proposed\nroute rides the gateway's VLAN 2 leg).</s> <strong>Since ADR-0007, Ceph\nconnectivity is UNAFFECTED by a natgw death</strong>: the workers reach Ceph\ndirectly on VLAN 2. (Historical record: under the superseded routing\ndesign the gateway would have been a single point of failure for both\nInternet egress and Ceph routing; gateway redundancy stays out of\nscope either way, see Consequences.)</li>\n</ul>\n<h2 id=\"bkmrk-consequences\">Consequences</h2>\n<ul id=\"bkmrk-vip-loss-blocks-new-\">\n<li>VIP loss blocks new node registrations and new kubeconfig clients;\nalready-joined agents keep working through their client-side load\nbalancer (port 6444) against the surviving control planes, and running\nworkloads are unaffected.</li>\n<li>Rollback is cheap: revert <code>server:</code> URLs to a control-plane IP (for\nexample <code>https://10.72.0.21:9345</code>) and uninstall keepalived. No\ncertificate rotation is needed because all control-plane IPs are already\nin the SAN list.</li>\n<li>The stable endpoint survives control-plane renumbering or reprovisioning,\nkubeconfigs never change, and accepting this ADR satisfies the\ninfrastructure README gate, letting RKE2 installation proceed.</li>\n<li>VRRP is a new operational surface on the node LAN (VRID collision care),\nand keepalived on the control planes adds guest configuration that must\nstay Ansible-owned per ADR-0003.</li>\n<li>A later move to a DNS-name endpoint would need a SAN addition plus\ncertificate rotation.</li>\n<li><s>Pre-implementation gates for the Ceph route</s> \u2014 <strong>closed by\nADR-0007</strong> (the route itself is superseded; the address allocation is\nnow the operator-reserved <code>192.168.98.0/25</code>, and no return-route owner,\ngateway firewall path or rp_filter verification is needed \u2014 there is no\ngateway on the path. The live VLAN 2 MTU check (1500, ADR-0001) and the\nVPN CIDR overlap validation survive as apply-time gates in\n<code>infrastructure/kubernetes/ceph-csi/VALIDATION.md</code>). Historical record\nof the superseded gates:\n<ul>\n<li>VLAN 2 address allocation: the exact address for the gateway's third\nNIC in <code>192.168.96.0/20</code>, confirmed against live Ceph allocations;</li>\n<li>Ceph-side return-route owner: which L3 device or hosts on the Ceph\nside own the return route for <code>10.72.0.0/23</code> via the gateway's VLAN 2\naddress;</li>\n<li>firewall path: the nftables accept rules needed in both directions,\nincluding any Ceph-side firewall;</li>\n<li><code>rp_filter</code> behavior: the gateway enforces <code>rp_filter=1</code>, and\nasymmetric replies could be dropped; must be field-verified;</li>\n<li>live MTU on VLAN 2: actual MTU and jumbo-frame support verified\nagainst the 1500 kept per ADR-0001;</li>\n<li>VPN CIDR overlap validation for <code>10.64.0.0/14</code> and <code>10.68.0.0/16</code>.</li>\n</ul>\n</li>\n<li>Out of scope, to be decided separately: gateway keepalived redundancy for\n<code>10.72.0.1</code>, any DNS infrastructure, MTU changes, and storage design.</li>\n</ul>\n"
 },
 {
  "id": 48,
  "title": "ADR 0005 cilium first addon",
  "slug": "adr-0005-cilium-first-addon",
  "book_id": 8,
  "chapter_id": 28,
  "description": null,
  "created_at": "2026-09-06T13:38:58.000000Z",
  "updated_at": "2026-09-06T13:38:58.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (decisions/ADR-0005-cilium-first-addon.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n ADR-0005: Cilium as the CNI from day one (no Canal provisional CNI)\n\n**Date:** 2026-08-29\n**Status:** Decided by operator; implementation pending\n\n## Context\n\nRKE2 installs a bundled Canal (Flannel VXLAN + Calico policy) CNI by default.\nPLATFORM-007 already names Cilium as the CNI and PLATFORM-008 names Hubble as\nthe network-observability base. This decision is recorded in the decisions log\nas PLATFORM-018 (the next free identifier; PLATFORM-008 was already assigned\nto Hubble), and the master\narchitecture plan (section 14.4) fixes the first-version baseline as Cilium\nwith VXLAN encapsulation and Hubble enabled, explicitly without BGP or native\nrouting (PLATFORM-017). A provisional Canal bootstrap \u2014 installing Canal first\nand replacing it with Cilium later \u2014 would force every node through a CNI\nmigration (restart of all pods, datapath teardown, stale flannel state) and\nbuild NetworkPolicy experience on a stack the platform never intends to keep.\nThe official Cilium RKE2 documentation offers exactly two paths: the bundled\n`rke2-cilium` chart or RKE2 with `cni: none` plus the official Cilium Helm\nchart. The cluster does not exist yet, so the CNI choice costs nothing today\nand an extra migration later. Pod CIDR `10.64.0.0/14` and Service CIDR\n`10.68.0.0/16` are set at the RKE2 level; Cilium must not re-derive them.\nFinally, Argo CD does not exist yet at first bootstrap, so the first cluster\naddon cannot be managed by GitOps from minute one.\n\n## Decision\n\n- Cilium is the CNI from the first RKE2 node. No Canal provisional CNI:\n  the rke2-server configuration must set `cni: none`, and the future RKE2\n  Ansible role must keep the bundled Canal disabled.\n- Install the official Cilium chart from\n  `oci://quay.io/cilium/charts/cilium`, pinned to chart version `1.20.1`\n  (chart versions carry no `v` prefix; appVersion v1.20.1) into\n  `kube-system`, not the `rke2-cilium` addon chart, so the Helm release and\n  values stay under this repository's control.\n- Baseline values (master plan 14.4): `routingMode: tunnel` with\n  `tunnelProtocol: vxlan`; IPAM `cluster-pool` with\n  `clusterPoolIPv4PodCIDRList: [10.64.0.0/14]` and\n  `clusterPoolIPv4MaskSize: 24`, exactly mirroring the RKE2 pod CIDR;\n  `kubeProxyReplacement: false`; `hubble.enabled: true` with\n  `hubble.relay.enabled: true` and `hubble.ui.enabled: false` (documented\n  flip-on option). No BGP, no native routing, no egress features initially.\n- CIDR ownership stays with RKE2: RKE2 owns the pod CIDR `10.64.0.0/14` and\n  the Service CIDR `10.68.0.0/16` (the latter stays owned by kube-proxy);\n  the Cilium IPAM pool only consumes the pod range and must never diverge\n  from the rke2-server configuration.\n- Keep the Cilium MTU at its default (auto-computed, about 1450 on the\n  MTU-1500 node LAN `10.72.0.0/23`, VLAN 4, single NIC `eth0` per node);\n  no explicit MTU unless a measured reason appears.\n- Node-to-node firewall paths must allow VXLAN UDP `8472` and Hubble Relay\n  TCP `4244`.\n- Bootstrap-time install from `wetopi-ops01`: the addon is installed with\n  Helm during the apply phase (`ansible/playbooks/cilium.yml` in\n  `wetopi-platform/infrastructure`, with pinned values in\n  `kubernetes/cilium/values.yaml`). This is a bootstrap-time install only;\n  once Argo CD exists, the addon moves under Argo CD management and the\n  playbook is retired.\n- Enabling kube-proxy replacement, Hubble UI by default, BGP or native\n  routing are explicitly deferred; each needs its own reviewed decision.\n\n## Consequences\n\n- No CNI migration is ever needed: the cluster is born with its final CNI.\n- The egress path depends on RKE2's kube-proxy staying installed and\n  healthy; replacing kube-proxy later is a deliberate, separate change.\n- VXLAN tunnels ride the node LAN `10.72.0.0/23`, so UDP 8472 (and TCP 4244\n  for Hubble Relay) must stay reachable node-to-node on VLAN 4.\n- The Cilium IPAM pool must always mirror the RKE2 pod CIDR; divergent\n  `clusterPoolIPv4PodCIDRList` values are a standing misconfiguration risk\n  to guard in review.\n- Until Argo CD exists, the addon is reconciled by hand (the pinned\n  one-liner in `kubernetes/cilium/README.md`), so drift between Git values\n  and the installed release is possible in the interim window.\n- The apply-phase validation checklist (Cilium status and connectivity,\n  Hubble flows, DNS, default-deny baseline) lives in\n  `kubernetes/cilium/VALIDATION.md` in the infrastructure repository.\n\n## References\n\n- Master architecture plan, sections 14.3 (CIDRs) and 14.4 (Cilium baseline).\n- Decisions log, section 32 (PLATFORM-018 \u2014 CNI Cilium desde el d\u00eda uno).\n- Cilium v1.20.1 Helm chart values and the official Cilium installation\n  notes for RKE2.",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (decisions/ADR-0005-cilium-first-addon.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-adr-0005%3A-cilium-as-\">ADR-0005: Cilium as the CNI from day one (no Canal provisional CNI)</p>\n<p id=\"bkmrk-date%3A-2026-08-29-sta\"><strong>Date:</strong> 2026-08-29\n<strong>Status:</strong> Decided by operator; implementation pending</p>\n<h2 id=\"bkmrk-context\">Context</h2>\n<p id=\"bkmrk-rke2-installs-a-bund\">RKE2 installs a bundled Canal (Flannel VXLAN + Calico policy) CNI by default.\nPLATFORM-007 already names Cilium as the CNI and PLATFORM-008 names Hubble as\nthe network-observability base. This decision is recorded in the decisions log\nas PLATFORM-018 (the next free identifier; PLATFORM-008 was already assigned\nto Hubble), and the master\narchitecture plan (section 14.4) fixes the first-version baseline as Cilium\nwith VXLAN encapsulation and Hubble enabled, explicitly without BGP or native\nrouting (PLATFORM-017). A provisional Canal bootstrap \u2014 installing Canal first\nand replacing it with Cilium later \u2014 would force every node through a CNI\nmigration (restart of all pods, datapath teardown, stale flannel state) and\nbuild NetworkPolicy experience on a stack the platform never intends to keep.\nThe official Cilium RKE2 documentation offers exactly two paths: the bundled\n<code>rke2-cilium</code> chart or RKE2 with <code>cni: none</code> plus the official Cilium Helm\nchart. The cluster does not exist yet, so the CNI choice costs nothing today\nand an extra migration later. Pod CIDR <code>10.64.0.0/14</code> and Service CIDR\n<code>10.68.0.0/16</code> are set at the RKE2 level; Cilium must not re-derive them.\nFinally, Argo CD does not exist yet at first bootstrap, so the first cluster\naddon cannot be managed by GitOps from minute one.</p>\n<h2 id=\"bkmrk-decision\">Decision</h2>\n<ul id=\"bkmrk-cilium-is-the-cni-fr\">\n<li>Cilium is the CNI from the first RKE2 node. No Canal provisional CNI:\nthe rke2-server configuration must set <code>cni: none</code>, and the future RKE2\nAnsible role must keep the bundled Canal disabled.</li>\n<li>Install the official Cilium chart from\n<code>oci://quay.io/cilium/charts/cilium</code>, pinned to chart version <code>1.20.1</code>\n(chart versions carry no <code>v</code> prefix; appVersion v1.20.1) into\n<code>kube-system</code>, not the <code>rke2-cilium</code> addon chart, so the Helm release and\nvalues stay under this repository's control.</li>\n<li>Baseline values (master plan 14.4): <code>routingMode: tunnel</code> with\n<code>tunnelProtocol: vxlan</code>; IPAM <code>cluster-pool</code> with\n<code>clusterPoolIPv4PodCIDRList: [10.64.0.0/14]</code> and\n<code>clusterPoolIPv4MaskSize: 24</code>, exactly mirroring the RKE2 pod CIDR;\n<code>kubeProxyReplacement: false</code>; <code>hubble.enabled: true</code> with\n<code>hubble.relay.enabled: true</code> and <code>hubble.ui.enabled: false</code> (documented\nflip-on option). No BGP, no native routing, no egress features initially.</li>\n<li>CIDR ownership stays with RKE2: RKE2 owns the pod CIDR <code>10.64.0.0/14</code> and\nthe Service CIDR <code>10.68.0.0/16</code> (the latter stays owned by kube-proxy);\nthe Cilium IPAM pool only consumes the pod range and must never diverge\nfrom the rke2-server configuration.</li>\n<li>Keep the Cilium MTU at its default (auto-computed, about 1450 on the\nMTU-1500 node LAN <code>10.72.0.0/23</code>, VLAN 4, single NIC <code>eth0</code> per node);\nno explicit MTU unless a measured reason appears.</li>\n<li>Node-to-node firewall paths must allow VXLAN UDP <code>8472</code> and Hubble Relay\nTCP <code>4244</code>.</li>\n<li>Bootstrap-time install from <code>wetopi-ops01</code>: the addon is installed with\nHelm during the apply phase (<code>ansible/playbooks/cilium.yml</code> in\n<code>wetopi-platform/infrastructure</code>, with pinned values in\n<code>kubernetes/cilium/values.yaml</code>). This is a bootstrap-time install only;\nonce Argo CD exists, the addon moves under Argo CD management and the\nplaybook is retired.</li>\n<li>Enabling kube-proxy replacement, Hubble UI by default, BGP or native\nrouting are explicitly deferred; each needs its own reviewed decision.</li>\n</ul>\n<h2 id=\"bkmrk-consequences\">Consequences</h2>\n<ul id=\"bkmrk-no-cni-migration-is-\">\n<li>No CNI migration is ever needed: the cluster is born with its final CNI.</li>\n<li>The egress path depends on RKE2's kube-proxy staying installed and\nhealthy; replacing kube-proxy later is a deliberate, separate change.</li>\n<li>VXLAN tunnels ride the node LAN <code>10.72.0.0/23</code>, so UDP 8472 (and TCP 4244\nfor Hubble Relay) must stay reachable node-to-node on VLAN 4.</li>\n<li>The Cilium IPAM pool must always mirror the RKE2 pod CIDR; divergent\n<code>clusterPoolIPv4PodCIDRList</code> values are a standing misconfiguration risk\nto guard in review.</li>\n<li>Until Argo CD exists, the addon is reconciled by hand (the pinned\none-liner in <code>kubernetes/cilium/README.md</code>), so drift between Git values\nand the installed release is possible in the interim window.</li>\n<li>The apply-phase validation checklist (Cilium status and connectivity,\nHubble flows, DNS, default-deny baseline) lives in\n<code>kubernetes/cilium/VALIDATION.md</code> in the infrastructure repository.</li>\n</ul>\n<h2 id=\"bkmrk-references\">References</h2>\n<ul id=\"bkmrk-master-architecture-\">\n<li>Master architecture plan, sections 14.3 (CIDRs) and 14.4 (Cilium baseline).</li>\n<li>Decisions log, section 32 (PLATFORM-018 \u2014 CNI Cilium desde el d\u00eda uno).</li>\n<li>Cilium v1.20.1 Helm chart values and the official Cilium installation\nnotes for RKE2.</li>\n</ul>\n"
 },
 {
  "id": 49,
  "title": "ADR 0006 edge v1 wan exception",
  "slug": "adr-0006-edge-v1-wan-exception",
  "book_id": 8,
  "chapter_id": 28,
  "description": null,
  "created_at": "2026-09-06T13:38:59.000000Z",
  "updated_at": "2026-09-06T13:38:59.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (decisions/ADR-0006-edge-v1-wan-exception.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n ADR-0006: Edge v1 WAN exception \u2014 direct Traefik, no L4 layer\n\n## Status\naccepted (2026-08-30 \u2014 approver r4 verification t_02a006fc; operator authorization recorded in the WAN Edge v1 plan review trail)\n\n## Context\nThe master plan (\u00a717 EdgeOps) preserves VRRP + IPVS + DSR as the production edge architecture and requires a DSR PoC (\u00a717.4) before production. The rebuild has now reached the point where the first internet-facing service (docs.wetopi.net \u2014 serving the platform's own documentation statics, with HTTPS + basic auth) is desired BEFORE the full L4 edge stack is built.\n\nThe operator decided (2026-08-30, recorded in the WAN Edge v1 plan doc): to ease testing and debugging, v1 enters WITHOUT the L4 load-balancing layer \u2014 two edge nodes with Traefik exposed directly, DNS round-robin between two public A records.\n\nThe approver's plan-review (t_a79fed0e, t_2d5385cc) correctly held that this conflicts with the prevailing master plan and that an \"ADR-lite\" cannot reconcile the conflict: a published internet endpoint is not an \"internal\" service. Hence this ADR \u2014 the accepted mechanism (same as ADR-0004 superseding the \u00a716 endpoint design).\n\n## Decision\nFor a **bounded v1 window**, the edge consists of exactly:\n- 2 RKE2 edge nodes (rke-wan01 79.137.114.193, rke-wan02 79.137.114.194, range 79.137.114.192/27, WAN NIC on vmbr3, mgmt NIC on vmbr4/LAN). History: these VMs were built and initially named wetopi3-wan1/wetopi3-wan2 and renamed to rke-wan01/rke-wan02 on 2026-08-30 (operator decree; vmids 8041/8042 unchanged) \u2014 the rke-wan names are the active/canonical ones everywhere (infrastructure repo PR #16);\n- bundled Traefik exposed DIRECTLY on the nodes' WAN interfaces (ports 80/443 ONLY; in-VM nftables deny-by-default);\n- HA via DNS round-robin over 2 A records (TTL 300) \u2014 explicitly **reduced availability**: no L4 health-check, no automatic failover. **Failure semantics (precise)**: when a node dies, its A record CONTINUES to be returned by DNS until the node recovers or the record is explicitly removed/repointed \u2014 TTL does NOT end the failures; TTL only bounds how quickly a manual removal/repoint propagates to clients. Observed failure rate is approximately half of new connections but varies with client ordering, retries, connection reuse and resolver behavior. Remediation is OPERATOR-driven: per-address health monitoring (both A records probed independently), a documented manual removal path (tofu PR dropping the dead address, or emergency Cloudflare edit), and a safe re-addition procedure after node recovery (tofu revert).\n\nThe v1 exception applies ONLY to the services explicitly listed in this ADR:\n\n- **docs.wetopi.net** (documentation statics, HTTPS + basic auth) \u2014 first service, the low-risk pilot.\n- **argo.wetopi.net** (Argo CD WebUI, HTTPS + two auth layers) \u2014 second service, added by UPDATE 2026-08-30 (below).\n\nNo other service name is covered; each addition to this list is its own recorded operator decision (the security rule \"nada m\u00e1s sin necesidad demostrada y decisi\u00f3n expl\u00edcita\" holds).\n\n**The exception is bounded by these hard conditions:**\n1. ANY service with external paying customers (hosting workloads) REQUIRES the v2 edge: VRRP public VIP + IPVS directors + DSR, with the \u00a717.4 PoC checklist completed \u2014 no exceptions.\n2. The v2 track is a scheduled deliverable, not an aspiration: it enters the plan BEFORE the hosting pilot phase (master plan \u00a733 Fase 16).\n3. Reduced-availability semantics are documented in the service's runbook and monitored (uptime checks on both A records independently).\n4. Cluster-plane identity and traffic of edge nodes are LAN-only: RKE2 node identity (node-ip), management, kubelet, and Cilium/VXLAN flows use the LAN interface exclusively (node-ip bound to 10.72.0.5x; see WAN Edge v1 plan \u00a7F1). WAN is limited to INBOUND exposure on ports 80/443; ordinary node EGRESS (image pulls, updates, outbound internet) MAY use the WAN default route \u2014 that is not cluster-plane traffic.\n\n### UPDATE 2026-08-30 \u2014 scope + security posture for argo.wetopi.net\n\n**Operator directive (verbatim, 2026-08-30 ~17:00, goal session docs.wetopi.net thread):**\n\n> \"en cuanto podamos, tambien quiero argo.wetopi.net securizado a internet, y sacado con https, para poder ver el estado de argo desde webui\"\n\nThis is the explicit decision the platform security rule requires (\"nada m\u00e1s sin necesidad demostrada y decisi\u00f3n expl\u00edcita\") \u2014 recorded here verbatim per that rule. It adds the SECOND (and so far last) v1-window service:\n\n**Scope:**\n- v1 window service list = **docs.wetopi.net + argo.wetopi.net**. argo.wetopi.net is the Argo CD WebUI: an operator-only CONTROL-PLANE UI for viewing Argo state from the web \u2014 NOT a customer-facing service.\n- NO external customers on either name; the v2-before-customers hard condition (1 above) is UNCHANGED and still binding.\n- Same edge machinery as docs.wetopi.net: the two A records (TTL 300, grey-cloud), the same reduced-availability DNS round-robin failure semantics, the same nftables WAN 80/443-only posture, TLS terminated at the edge Gateway.\n\n**Security posture for argo (TWO auth layers + edge TLS):**\n1. **Layer 1 \u2014 Traefik basic-auth** at the edge (Middleware `argo-basic-auth`, htpasswd Secret operator-managed on the cluster, never in Git).\n2. **Layer 2 \u2014 Argo CD's own login** (local admin auth; SSO/Dex integration remains OPEN per the bootstrap decision). Both layers must succeed to reach the WebUI.\n3. **TLS**: Let's Encrypt PRODUCTION certificate at the edge Gateway (`argo-tls`, DNS-01 via the cert-manager chain validated staging-first by the F3 edge-test gate) \u2014 a control-plane UI in daily operator use does not take a staging-cert detour.\n4. **argocd CLI unaffected**: CLI usage continues over the LAN (API VIP), exactly as before the exposure \u2014 the WAN edge is an additional, authenticated view path, not a replacement.\n5. **Initial admin password rotated by the operator on first login** (the bootstrap-time initial admin secret is a bootstrap artifact, not a durable credential).\n\n## Consequences\n- Positive: first internet-facing service ships weeks earlier; edge path debuggable end-to-end (client\u2192Traefik, no L4 indirection); the 2 edge VMs and their RKE2 join carry over unchanged into v2 (the L4 layer is added IN FRONT, additive).\n- Negative: reduced availability during the whole v1 window (a dead address serves failures until operator remediation \u2014 removal/repoint propagates within the TTL bound; rollback to the legacy address is likewise TTL-bounded for clients with cached answers); no protection against a node serving while unhealthy (no L4 health-check) \u2014 mitigated by independent per-address uptime monitoring and the documented manual removal/re-addition runbook.\n- Neutral: the DSR bandwidth-aggregation benefit of \u00a717 is not realized in v1 (irrelevant for a static docs site's traffic).\n\n## Alternatives considered\n- **Build the full \u00a717 edge first**: rejected for sequencing \u2014 it adds weeks of IPVS/DSR PoC work before ANY internet-facing validation of the new platform; the docs service is deliberately the low-risk pilot.\n- **Cilium LB-IPAM / BGP instead of IPVS**: viable v2 alternative; deferred with the v2 track (would change the L4 choice at v2 design time).\n- **Keep docs on the legacy platform**: rejected \u2014 the goal is validating the NEW platform's edge chain (DNS\u2192TLS\u2192Gateway\u2192pods) with the most innocuous workload possible.\n\n## Migration / implementation notes\n- This ADR supersedes master plan \u00a717 ONLY for the v1 window and ONLY for the listed service; \u00a717 remains the production target (v2).\n- Implementation plan: WAN Edge v1 (doc_wetopi-wan-edge-v1-plan.md r3) \u2014 F1..F5; the v2 track lives in the plan's F6 and becomes its own plan when scheduled.\n- Rollback of this decision = execute the v2 track ahead of schedule and repoint DNS (bounded by TTL).\n\n## Related documents\n- WAN Edge v1 plan (base_project: doc_wetopi-wan-edge-v1-plan.md, r3)\n- ADR-0004 (endpoint HA precedent for superseding \u00a716 via ADR)\n- master-architecture-plan \u00a717 (superseded partially, v1 window), \u00a733 Fase 14\n- Implementation: infrastructure PR #20 (kubernetes/edge-argo + DNS tfvars) \u2014 argo.wetopi.net service manifests; docs-site service (docs.wetopi.net) in infrastructure PR #15",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (decisions/ADR-0006-edge-v1-wan-exception.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-adr-0006%3A-edge-v1-wa\">ADR-0006: Edge v1 WAN exception \u2014 direct Traefik, no L4 layer</p>\n<h2 id=\"bkmrk-status\">Status</h2>\n<p id=\"bkmrk-accepted-%282026-08-30\">accepted (2026-08-30 \u2014 approver r4 verification t_02a006fc; operator authorization recorded in the WAN Edge v1 plan review trail)</p>\n<h2 id=\"bkmrk-context\">Context</h2>\n<p id=\"bkmrk-the-master-plan-%28%C2%A717\">The master plan (\u00a717 EdgeOps) preserves VRRP + IPVS + DSR as the production edge architecture and requires a DSR PoC (\u00a717.4) before production. The rebuild has now reached the point where the first internet-facing service (docs.wetopi.net \u2014 serving the platform's own documentation statics, with HTTPS + basic auth) is desired BEFORE the full L4 edge stack is built.</p>\n<p id=\"bkmrk-the-operator-decided\">The operator decided (2026-08-30, recorded in the WAN Edge v1 plan doc): to ease testing and debugging, v1 enters WITHOUT the L4 load-balancing layer \u2014 two edge nodes with Traefik exposed directly, DNS round-robin between two public A records.</p>\n<p id=\"bkmrk-the-approver%27s-plan-\">The approver's plan-review (t_a79fed0e, t_2d5385cc) correctly held that this conflicts with the prevailing master plan and that an \"ADR-lite\" cannot reconcile the conflict: a published internet endpoint is not an \"internal\" service. Hence this ADR \u2014 the accepted mechanism (same as ADR-0004 superseding the \u00a716 endpoint design).</p>\n<h2 id=\"bkmrk-decision\">Decision</h2>\n<p id=\"bkmrk-for-a-bounded-v1-win\">For a <strong>bounded v1 window</strong>, the edge consists of exactly:</p>\n<ul id=\"bkmrk-2-rke2-edge-nodes-%28r\">\n<li>2 RKE2 edge nodes (rke-wan01 79.137.114.193, rke-wan02 79.137.114.194, range 79.137.114.192/27, WAN NIC on vmbr3, mgmt NIC on vmbr4/LAN). History: these VMs were built and initially named wetopi3-wan1/wetopi3-wan2 and renamed to rke-wan01/rke-wan02 on 2026-08-30 (operator decree; vmids 8041/8042 unchanged) \u2014 the rke-wan names are the active/canonical ones everywhere (infrastructure repo PR #16);</li>\n<li>bundled Traefik exposed DIRECTLY on the nodes' WAN interfaces (ports 80/443 ONLY; in-VM nftables deny-by-default);</li>\n<li>HA via DNS round-robin over 2 A records (TTL 300) \u2014 explicitly <strong>reduced availability</strong>: no L4 health-check, no automatic failover. <strong>Failure semantics (precise)</strong>: when a node dies, its A record CONTINUES to be returned by DNS until the node recovers or the record is explicitly removed/repointed \u2014 TTL does NOT end the failures; TTL only bounds how quickly a manual removal/repoint propagates to clients. Observed failure rate is approximately half of new connections but varies with client ordering, retries, connection reuse and resolver behavior. Remediation is OPERATOR-driven: per-address health monitoring (both A records probed independently), a documented manual removal path (tofu PR dropping the dead address, or emergency Cloudflare edit), and a safe re-addition procedure after node recovery (tofu revert).</li>\n</ul>\n<p id=\"bkmrk-the-v1-exception-app\">The v1 exception applies ONLY to the services explicitly listed in this ADR:</p>\n<ul id=\"bkmrk-docs.wetopi.net-%28doc\">\n<li><strong>docs.wetopi.net</strong> (documentation statics, HTTPS + basic auth) \u2014 first service, the low-risk pilot.</li>\n<li><strong>argo.wetopi.net</strong> (Argo CD WebUI, HTTPS + two auth layers) \u2014 second service, added by UPDATE 2026-08-30 (below).</li>\n</ul>\n<p id=\"bkmrk-no-other-service-nam\">No other service name is covered; each addition to this list is its own recorded operator decision (the security rule \"nada m\u00e1s sin necesidad demostrada y decisi\u00f3n expl\u00edcita\" holds).</p>\n<p id=\"bkmrk-the-exception-is-bou\"><strong>The exception is bounded by these hard conditions:</strong></p>\n<ol id=\"bkmrk-any-service-with-ext\">\n<li>ANY service with external paying customers (hosting workloads) REQUIRES the v2 edge: VRRP public VIP + IPVS directors + DSR, with the \u00a717.4 PoC checklist completed \u2014 no exceptions.</li>\n<li>The v2 track is a scheduled deliverable, not an aspiration: it enters the plan BEFORE the hosting pilot phase (master plan \u00a733 Fase 16).</li>\n<li>Reduced-availability semantics are documented in the service's runbook and monitored (uptime checks on both A records independently).</li>\n<li>Cluster-plane identity and traffic of edge nodes are LAN-only: RKE2 node identity (node-ip), management, kubelet, and Cilium/VXLAN flows use the LAN interface exclusively (node-ip bound to 10.72.0.5x; see WAN Edge v1 plan \u00a7F1). WAN is limited to INBOUND exposure on ports 80/443; ordinary node EGRESS (image pulls, updates, outbound internet) MAY use the WAN default route \u2014 that is not cluster-plane traffic.</li>\n</ol>\n<h3 id=\"bkmrk-update-2026-08-30-%E2%80%94-\">UPDATE 2026-08-30 \u2014 scope + security posture for argo.wetopi.net</h3>\n<p id=\"bkmrk-operator-directive-%28\"><strong>Operator directive (verbatim, 2026-08-30 ~17:00, goal session docs.wetopi.net thread):</strong></p>\n<blockquote id=\"bkmrk-%22en-cuanto-podamos%2C-\">\n<p>\"en cuanto podamos, tambien quiero argo.wetopi.net securizado a internet, y sacado con https, para poder ver el estado de argo desde webui\"</p>\n</blockquote>\n<p id=\"bkmrk-this-is-the-explicit\">This is the explicit decision the platform security rule requires (\"nada m\u00e1s sin necesidad demostrada y decisi\u00f3n expl\u00edcita\") \u2014 recorded here verbatim per that rule. It adds the SECOND (and so far last) v1-window service:</p>\n<p id=\"bkmrk-scope%3A\"><strong>Scope:</strong></p>\n<ul id=\"bkmrk-v1-window-service-li\">\n<li>v1 window service list = <strong>docs.wetopi.net + argo.wetopi.net</strong>. argo.wetopi.net is the Argo CD WebUI: an operator-only CONTROL-PLANE UI for viewing Argo state from the web \u2014 NOT a customer-facing service.</li>\n<li>NO external customers on either name; the v2-before-customers hard condition (1 above) is UNCHANGED and still binding.</li>\n<li>Same edge machinery as docs.wetopi.net: the two A records (TTL 300, grey-cloud), the same reduced-availability DNS round-robin failure semantics, the same nftables WAN 80/443-only posture, TLS terminated at the edge Gateway.</li>\n</ul>\n<p id=\"bkmrk-security-posture-for\"><strong>Security posture for argo (TWO auth layers + edge TLS):</strong></p>\n<ol id=\"bkmrk-layer-1-%E2%80%94-traefik-ba\">\n<li><strong>Layer 1 \u2014 Traefik basic-auth</strong> at the edge (Middleware <code>argo-basic-auth</code>, htpasswd Secret operator-managed on the cluster, never in Git).</li>\n<li><strong>Layer 2 \u2014 Argo CD's own login</strong> (local admin auth; SSO/Dex integration remains OPEN per the bootstrap decision). Both layers must succeed to reach the WebUI.</li>\n<li><strong>TLS</strong>: Let's Encrypt PRODUCTION certificate at the edge Gateway (<code>argo-tls</code>, DNS-01 via the cert-manager chain validated staging-first by the F3 edge-test gate) \u2014 a control-plane UI in daily operator use does not take a staging-cert detour.</li>\n<li><strong>argocd CLI unaffected</strong>: CLI usage continues over the LAN (API VIP), exactly as before the exposure \u2014 the WAN edge is an additional, authenticated view path, not a replacement.</li>\n<li><strong>Initial admin password rotated by the operator on first login</strong> (the bootstrap-time initial admin secret is a bootstrap artifact, not a durable credential).</li>\n</ol>\n<h2 id=\"bkmrk-consequences\">Consequences</h2>\n<ul id=\"bkmrk-positive%3A-first-inte\">\n<li>Positive: first internet-facing service ships weeks earlier; edge path debuggable end-to-end (client\u2192Traefik, no L4 indirection); the 2 edge VMs and their RKE2 join carry over unchanged into v2 (the L4 layer is added IN FRONT, additive).</li>\n<li>Negative: reduced availability during the whole v1 window (a dead address serves failures until operator remediation \u2014 removal/repoint propagates within the TTL bound; rollback to the legacy address is likewise TTL-bounded for clients with cached answers); no protection against a node serving while unhealthy (no L4 health-check) \u2014 mitigated by independent per-address uptime monitoring and the documented manual removal/re-addition runbook.</li>\n<li>Neutral: the DSR bandwidth-aggregation benefit of \u00a717 is not realized in v1 (irrelevant for a static docs site's traffic).</li>\n</ul>\n<h2 id=\"bkmrk-alternatives-conside\">Alternatives considered</h2>\n<ul id=\"bkmrk-build-the-full-%C2%A717-e\">\n<li><strong>Build the full \u00a717 edge first</strong>: rejected for sequencing \u2014 it adds weeks of IPVS/DSR PoC work before ANY internet-facing validation of the new platform; the docs service is deliberately the low-risk pilot.</li>\n<li><strong>Cilium LB-IPAM / BGP instead of IPVS</strong>: viable v2 alternative; deferred with the v2 track (would change the L4 choice at v2 design time).</li>\n<li><strong>Keep docs on the legacy platform</strong>: rejected \u2014 the goal is validating the NEW platform's edge chain (DNS\u2192TLS\u2192Gateway\u2192pods) with the most innocuous workload possible.</li>\n</ul>\n<h2 id=\"bkmrk-migration-%2F-implemen\">Migration / implementation notes</h2>\n<ul id=\"bkmrk-this-adr-supersedes-\">\n<li>This ADR supersedes master plan \u00a717 ONLY for the v1 window and ONLY for the listed service; \u00a717 remains the production target (v2).</li>\n<li>Implementation plan: WAN Edge v1 (doc_wetopi-wan-edge-v1-plan.md r3) \u2014 F1..F5; the v2 track lives in the plan's F6 and becomes its own plan when scheduled.</li>\n<li>Rollback of this decision = execute the v2 track ahead of schedule and repoint DNS (bounded by TTL).</li>\n</ul>\n<h2 id=\"bkmrk-related-documents\">Related documents</h2>\n<ul id=\"bkmrk-wan-edge-v1-plan-%28ba\">\n<li>WAN Edge v1 plan (base_project: doc_wetopi-wan-edge-v1-plan.md, r3)</li>\n<li>ADR-0004 (endpoint HA precedent for superseding \u00a716 via ADR)</li>\n<li>master-architecture-plan \u00a717 (superseded partially, v1 window), \u00a733 Fase 14</li>\n<li>Implementation: infrastructure PR #20 (kubernetes/edge-argo + DNS tfvars) \u2014 argo.wetopi.net service manifests; docs-site service (docs.wetopi.net) in infrastructure PR #15</li>\n</ul>\n"
 },
 {
  "id": 50,
  "title": "ADR 0007 ceph storage native",
  "slug": "adr-0007-ceph-storage-native",
  "book_id": 8,
  "chapter_id": 28,
  "description": null,
  "created_at": "2026-09-06T13:38:59.000000Z",
  "updated_at": "2026-09-06T13:38:59.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (decisions/ADR-0007-ceph-storage-native.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n ADR-0007: Ceph client access via direct storage-network attachment\n\n**Date:** 2026-08-30\n**Status:** Accepted (operator decision recorded 2026-08-30 \u2014 the\n`192.168.98.0/25` allocation, `.10/.11/.12` = the 3 platform workers \u2014\non build card t_7415a1d6; accepted status set after the round-1\nreview cycle of infrastructure PR #13 / docs PR #12, verdict\nt_4ed11dad)\n\n## Context\n\nADR-0004 bundled a second decision with the RKE2 API endpoint: node-LAN\ntraffic to the Ceph cluster (`192.168.96.0/20`, VLAN 2 / `vmbr2`) would be\n**routed via the NAT gateway** \u2014 `wetopi-natgw01` gaining a third\ninterface on VLAN 2 so the Ceph subnet became a directly connected route\non the gateway, with a Ceph-side return route for `10.72.0.0/23` and\nforward-chain firewall rules. That design was accepted as design only,\ngated behind pre-implementation gates (address allocation, return-route\nowner, firewall path, rp_filter, MTU, CIDR overlap), and shipped\ndefault-off in the `nat_gateway` Ansible role.\n\nField reality (2026-08-30) invalidated the routing hypothesis:\n\n- **No path exists today**: from a worker, `192.168.96.10` routes via the\n  LAN default gateway `10.72.0.1` with 100% loss \u2014 the natgw has no VLAN 2\n  leg, no static route, and a forward policy that drops everything except\n  LAN-to-WAN.\n- **The LAN gateway is not ours to depend on for storage**: routing via\n  `10.72.0.1` means every Ceph I/O depends on a non-managed default-gateway\n  hop (plus, in the ADR-0004 shape, on a Ceph-side return route nobody\n  owns).\n- **The dedicated network is already there**: `vmbr2` / VLAN 2 is present\n  on the Proxmox hosts \u2014 the Ceph storage network is a directly connected\n  L2 domain one NIC away from every VM.\n\nThe operator decided (Carlos, 2026-08-30): *\"storage on the native\nnetwork; routing into Ceph is unnecessary overhead.\"*\n\n## Decision\n\nThe 3 platform workers (`rke-platform01/02/03` \u2014 the PVC-carrying nodes)\nget a **third NIC on VLAN 2 / `vmbr2`** with addresses in\n`192.168.96.0/20`, attaching them **natively to the Ceph storage network**\n\u2014 the standard Ceph client pattern:\n\n- **Operator allocation (2026-08-30)**: the sub-CIDR **`192.168.98.0/25`**\n  is reserved for the platform's storage consumers \u2014\n  `rke-platform01=192.168.98.10`, `rke-platform02=192.168.98.11`,\n  `rke-platform03=192.168.98.12` (all with the `/20` mask per the connected\n  subnet); the rest of the /25 stays free for future storage consumers.\n- **No routing, no NAT, no return-routes, no natgw involvement**: the /20\n  connected route appears automatically (scope link) when the address is\n  assigned; the storage NIC carries no gateway and no routes. The default\n  route stays exclusively `10.72.0.1` on the LAN NIC (unchanged). The\n  ADR-0001 no-NAT invariant is trivially preserved (there is no\n  intermediary to NAT).\n- **Control planes stay single-NIC in v1**: CPs are tainted\n  (`node-role.kubernetes.io/control-plane:NoSchedule`) and carry no PVC\n  workloads; their storage-NIC shape is documented (commented) in the\n  tfvars for future need.\n- **Firewall**: the workers' storage interface admits only the Ceph client\n  ports from `192.168.96.0/20` \u2014 mons TCP 3300 (msgr2) + 6789 (msgr1),\n  OSDs TCP 6800-7300 \u2014 inbound plus established/related (`ceph_storage`\n  Ansible role, default-off until the reviewed apply). LAN semantics stay\n  permissive (identical to the pre-change no-firewall state); the workers\n  carry no WAN surface.\n- **MTU stays 1500** (ADR-0001), verified against the live VLAN 2 MTU as\n  an apply-time gate.\n- This ADR **supersedes the Ceph routing decision section of ADR-0004**\n  (its last Decision bullet and the corresponding failure-mode/consequence\n  text). ADR-0004's API-endpoint decision (keepalived VIP `10.72.0.20`) is\n  UNAFFECTED and remains in force.\n\n## Consequences\n\n- **3 addresses consumed** in the storage LAN (`.10-.12` from the reserved\n  /25); the allocation is now explicit and recorded, replacing ADR-0004's\n  open \"VLAN 2 address allocation\" gate.\n- **The apply is MUTATING on live workers**: adding a NIC requires a\n  serial drain/reboot per node (the Phase W pattern, already proven). The\n  ops sequence (plan gate \u2192 per-worker drain/apply/reboot/verify/uncordon\n  \u2192 firewall enable) is documented in\n  `infrastructure/kubernetes/ceph-csi/VALIDATION.md` \u00a72.\n- **Storage traffic is isolated from the management LAN**: Ceph I/O never\n  touches vmbr4 or the natgw; the gateway's failure can no longer affect\n  storage at all (ADR-0004 had accepted the natgw as a Ceph-routing SPOF).\n- **The ADR-0004 routing artifacts are retired** in the infrastructure\n  repo: the `natgw_ceph_*` enablement is reduced to a permanent\n  default-false gate + cleanup task; the natgw carries no VLAN 2 leg, ever.\n- Prerequisites for Ceph CSI shrink to: address allocation (done), Ceph\n  pool + dedicated user (`client.kubernetes-csi`, never admin), fsid/mons\n  read-only, `csi-rbd-secret`.\n- Worker VMs become dual-homed; operational care points (documented in the\n  runbook): the storage NIC must never acquire a default route\n  (asserted), and node identity stays LAN-only.\n\n## Alternatives considered\n\n- **Gateway-side routing (ADR-0004 original)**: rejected \u2014 indirection\n  with no benefit, a natgw SPOF on the storage path, an unowned Ceph-side\n  return route, and 100% loss today because no path exists. The\n  pre-implementation gates could never be satisfied cheaply.\n- **Routing via the LAN gateway without a natgw VLAN 2 leg**: rejected \u2014\n  depends on a non-managed gateway that currently drops the path (100%\n  loss); still requires Ceph-side return routes; storage I/O coupled to\n  egress infrastructure.\n- **Moving Ceph storage traffic to the management LAN (10.72.0.0/23)**:\n  rejected \u2014 worse isolation (storage and management share fate and\n  bandwidth) and touches the untouched-Wetopi-Ceph-LAN principle in the\n  wrong direction.\n- **NAT to Ceph**: never considered \u2014 violates the ADR-0001 no-NAT\n  invariant.\n\n## Related documents\n\n- ADR-0001 (node LAN, no-NAT invariant, Ceph LAN untouched)\n- ADR-0004 (API endpoint \u2014 unaffected; Ceph routing section superseded by\n  this ADR)\n- ADR-0003 (tofu owns VM/NIC shape, Ansible owns guest config \u2014 both\n  applied here)\n- infrastructure PR `feat/ceph-storage-native` (the build:\n  tfvars, `ceph_storage` role, ADR-0004 artifact retirement,\n  README/VALIDATION rewrite)",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (decisions/ADR-0007-ceph-storage-native.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-adr-0007%3A-ceph-clien\">ADR-0007: Ceph client access via direct storage-network attachment</p>\n<p id=\"bkmrk-date%3A-2026-08-30-sta\"><strong>Date:</strong> 2026-08-30\n<strong>Status:</strong> Accepted (operator decision recorded 2026-08-30 \u2014 the\n<code>192.168.98.0/25</code> allocation, <code>.10/.11/.12</code> = the 3 platform workers \u2014\non build card t_7415a1d6; accepted status set after the round-1\nreview cycle of infrastructure PR #13 / docs PR #12, verdict\nt_4ed11dad)</p>\n<h2 id=\"bkmrk-context\">Context</h2>\n<p id=\"bkmrk-adr-0004-bundled-a-s\">ADR-0004 bundled a second decision with the RKE2 API endpoint: node-LAN\ntraffic to the Ceph cluster (<code>192.168.96.0/20</code>, VLAN 2 / <code>vmbr2</code>) would be\n<strong>routed via the NAT gateway</strong> \u2014 <code>wetopi-natgw01</code> gaining a third\ninterface on VLAN 2 so the Ceph subnet became a directly connected route\non the gateway, with a Ceph-side return route for <code>10.72.0.0/23</code> and\nforward-chain firewall rules. That design was accepted as design only,\ngated behind pre-implementation gates (address allocation, return-route\nowner, firewall path, rp_filter, MTU, CIDR overlap), and shipped\ndefault-off in the <code>nat_gateway</code> Ansible role.</p>\n<p id=\"bkmrk-field-reality-%282026-\">Field reality (2026-08-30) invalidated the routing hypothesis:</p>\n<ul id=\"bkmrk-no-path-exists-today\">\n<li><strong>No path exists today</strong>: from a worker, <code>192.168.96.10</code> routes via the\nLAN default gateway <code>10.72.0.1</code> with 100% loss \u2014 the natgw has no VLAN 2\nleg, no static route, and a forward policy that drops everything except\nLAN-to-WAN.</li>\n<li><strong>The LAN gateway is not ours to depend on for storage</strong>: routing via\n<code>10.72.0.1</code> means every Ceph I/O depends on a non-managed default-gateway\nhop (plus, in the ADR-0004 shape, on a Ceph-side return route nobody\nowns).</li>\n<li><strong>The dedicated network is already there</strong>: <code>vmbr2</code> / VLAN 2 is present\non the Proxmox hosts \u2014 the Ceph storage network is a directly connected\nL2 domain one NIC away from every VM.</li>\n</ul>\n<p id=\"bkmrk-the-operator-decided\">The operator decided (Carlos, 2026-08-30): <em>\"storage on the native\nnetwork; routing into Ceph is unnecessary overhead.\"</em></p>\n<h2 id=\"bkmrk-decision\">Decision</h2>\n<p id=\"bkmrk-the-3-platform-worke\">The 3 platform workers (<code>rke-platform01/02/03</code> \u2014 the PVC-carrying nodes)\nget a <strong>third NIC on VLAN 2 / <code>vmbr2</code></strong> with addresses in\n<code>192.168.96.0/20</code>, attaching them <strong>natively to the Ceph storage network</strong>\n\u2014 the standard Ceph client pattern:</p>\n<ul id=\"bkmrk-operator-allocation-\">\n<li><strong>Operator allocation (2026-08-30)</strong>: the sub-CIDR <strong><code>192.168.98.0/25</code></strong>\nis reserved for the platform's storage consumers \u2014\n<code>rke-platform01=192.168.98.10</code>, <code>rke-platform02=192.168.98.11</code>,\n<code>rke-platform03=192.168.98.12</code> (all with the <code>/20</code> mask per the connected\nsubnet); the rest of the /25 stays free for future storage consumers.</li>\n<li><strong>No routing, no NAT, no return-routes, no natgw involvement</strong>: the /20\nconnected route appears automatically (scope link) when the address is\nassigned; the storage NIC carries no gateway and no routes. The default\nroute stays exclusively <code>10.72.0.1</code> on the LAN NIC (unchanged). The\nADR-0001 no-NAT invariant is trivially preserved (there is no\nintermediary to NAT).</li>\n<li><strong>Control planes stay single-NIC in v1</strong>: CPs are tainted\n(<code>node-role.kubernetes.io/control-plane:NoSchedule</code>) and carry no PVC\nworkloads; their storage-NIC shape is documented (commented) in the\ntfvars for future need.</li>\n<li><strong>Firewall</strong>: the workers' storage interface admits only the Ceph client\nports from <code>192.168.96.0/20</code> \u2014 mons TCP 3300 (msgr2) + 6789 (msgr1),\nOSDs TCP 6800-7300 \u2014 inbound plus established/related (<code>ceph_storage</code>\nAnsible role, default-off until the reviewed apply). LAN semantics stay\npermissive (identical to the pre-change no-firewall state); the workers\ncarry no WAN surface.</li>\n<li><strong>MTU stays 1500</strong> (ADR-0001), verified against the live VLAN 2 MTU as\nan apply-time gate.</li>\n<li>This ADR <strong>supersedes the Ceph routing decision section of ADR-0004</strong>\n(its last Decision bullet and the corresponding failure-mode/consequence\ntext). ADR-0004's API-endpoint decision (keepalived VIP <code>10.72.0.20</code>) is\nUNAFFECTED and remains in force.</li>\n</ul>\n<h2 id=\"bkmrk-consequences\">Consequences</h2>\n<ul id=\"bkmrk-3-addresses-consumed\">\n<li><strong>3 addresses consumed</strong> in the storage LAN (<code>.10-.12</code> from the reserved\n/25); the allocation is now explicit and recorded, replacing ADR-0004's\nopen \"VLAN 2 address allocation\" gate.</li>\n<li><strong>The apply is MUTATING on live workers</strong>: adding a NIC requires a\nserial drain/reboot per node (the Phase W pattern, already proven). The\nops sequence (plan gate \u2192 per-worker drain/apply/reboot/verify/uncordon\n\u2192 firewall enable) is documented in\n<code>infrastructure/kubernetes/ceph-csi/VALIDATION.md</code> \u00a72.</li>\n<li><strong>Storage traffic is isolated from the management LAN</strong>: Ceph I/O never\ntouches vmbr4 or the natgw; the gateway's failure can no longer affect\nstorage at all (ADR-0004 had accepted the natgw as a Ceph-routing SPOF).</li>\n<li><strong>The ADR-0004 routing artifacts are retired</strong> in the infrastructure\nrepo: the <code>natgw_ceph_*</code> enablement is reduced to a permanent\ndefault-false gate + cleanup task; the natgw carries no VLAN 2 leg, ever.</li>\n<li>Prerequisites for Ceph CSI shrink to: address allocation (done), Ceph\npool + dedicated user (<code>client.kubernetes-csi</code>, never admin), fsid/mons\nread-only, <code>csi-rbd-secret</code>.</li>\n<li>Worker VMs become dual-homed; operational care points (documented in the\nrunbook): the storage NIC must never acquire a default route\n(asserted), and node identity stays LAN-only.</li>\n</ul>\n<h2 id=\"bkmrk-alternatives-conside\">Alternatives considered</h2>\n<ul id=\"bkmrk-gateway-side-routing\">\n<li><strong>Gateway-side routing (ADR-0004 original)</strong>: rejected \u2014 indirection\nwith no benefit, a natgw SPOF on the storage path, an unowned Ceph-side\nreturn route, and 100% loss today because no path exists. The\npre-implementation gates could never be satisfied cheaply.</li>\n<li><strong>Routing via the LAN gateway without a natgw VLAN 2 leg</strong>: rejected \u2014\ndepends on a non-managed gateway that currently drops the path (100%\nloss); still requires Ceph-side return routes; storage I/O coupled to\negress infrastructure.</li>\n<li><strong>Moving Ceph storage traffic to the management LAN (10.72.0.0/23)</strong>:\nrejected \u2014 worse isolation (storage and management share fate and\nbandwidth) and touches the untouched-Wetopi-Ceph-LAN principle in the\nwrong direction.</li>\n<li><strong>NAT to Ceph</strong>: never considered \u2014 violates the ADR-0001 no-NAT\ninvariant.</li>\n</ul>\n<h2 id=\"bkmrk-related-documents\">Related documents</h2>\n<ul id=\"bkmrk-adr-0001-%28node-lan%2C-\">\n<li>ADR-0001 (node LAN, no-NAT invariant, Ceph LAN untouched)</li>\n<li>ADR-0004 (API endpoint \u2014 unaffected; Ceph routing section superseded by\nthis ADR)</li>\n<li>ADR-0003 (tofu owns VM/NIC shape, Ansible owns guest config \u2014 both\napplied here)</li>\n<li>infrastructure PR <code>feat/ceph-storage-native</code> (the build:\ntfvars, <code>ceph_storage</code> role, ADR-0004 artifact retirement,\nREADME/VALIDATION rewrite)</li>\n</ul>\n"
 },
 {
  "id": 51,
  "title": "ADR 0008 harness v31 objective governance",
  "slug": "adr-0008-harness-v31-objective-governance",
  "book_id": 8,
  "chapter_id": 28,
  "description": null,
  "created_at": "2026-09-06T13:38:59.000000Z",
  "updated_at": "2026-09-06T13:39:00.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (decisions/ADR-0008-harness-v31-objective-governance.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n ADR-0008: Harness V3.1 objective governance \u2014 versioned Git Objectives with separated THINK, OWN, DO and JUDGE\n\n**Date:** 2026-08-30\n**Status:** Accepted (2026-08-30). Lifecycle completed per the build-p1 spec v2:\nthe ADR opened as `proposed` for independent review; the independent content\nreview reached `APPROVE` (round 2, `t_f5f1fb23`) with green CI, and this narrow\nin-PR transition updates front matter, this human status line and the decisions\nindex atomically, with all DocOps gates re-run. Landing the pilot is NOT\nautomatic standing governance \u2014 V3.1 becoming standing governance is recorded\nonly after the accepted ADR is merged and the pilot verdict is in, per the\npromotion matrix of\n[OBJ-HARNESS-V31-001](../objectives/active/OBJ-HARNESS-V31-001.md).\n\n## Context\n\nWetopi is built and operated by an AI-agent harness (Hermes Kanban lanes:\ndiscovery / build / ops / senior / approver) supervised by one human\noperator. Two structural risks have surfaced during Phases 0\u20134:\n\n1. **Work definition lives only in operational state.** The Kanban board\n   (and the Work Ledger that will supersede it) is excellent for live\n   coordination but is mutable, profile-local, and not reviewable as a\n   versioned contract. WHAT we are building, WHY, and the definition of\n   DONE deserve a reviewed, versioned, auditable source that survives board\n   churn.\n2. **One agent can currently both define and judge work.** Without a role\n   separation, the harness risks self-confirmation: the agent that wrote a\n   plan also reviews it, and execution shortcuts (auto-continuation,\n   degraded lanes, capability substitutions) can silently weaken review\n   discipline.\n\nHarness V3.1 is the pilot that addresses both: versioned Git Objectives\nplus a strict role separation. This ADR records the governance decisions;\n[ARCH-HARNESS-V31-GOVERNANCE](../agents/harness-v31-governance.md)\ndocuments the operating model (roles, gates, degraded routing, fail-closed\nstate machine); [OBJ-HARNESS-V31-001](../objectives/active/OBJ-HARNESS-V31-001.md)\nis the first Objective exercising the mechanism end-to-end.\n\nThis ADR follows the accepted decision conventions (ADR-0003 among them)\nand does not modify any infrastructure responsibility split \u2014 it is\nagent-governance only. It is consistent with ADR-0003's principle of\nseparate, explicit sources of truth per lifecycle.\n\n## Decision\n\n1. **Role separation THINK / OWN / DO / JUDGE.** Harness work is split into\n   four non-overlapping roles:\n\n   | Role | Lane / actor | Responsibility | Explicitly NOT allowed |\n   |---|---|---|---|\n   | THINK (Architect) | architect capability (e.g. an architect-designated profile or the operator acting as architect) | Define WHAT, WHY and DONE as an **Objective Draft**: outcome, context, acceptance criteria, verification contract, constraints, boundaries, stop conditions | Git write, execution, card claiming, review of own draft |\n   | OWN (Lead) | `wetopi-lead` | Acquire the accepted Objective, decompose it into cards, dispatch, maintain the Execution Ledger, own the documentation handoff | Authoring the Objective's WHAT/WHY/DONE it did not receive; judging deliveries |\n   | DO (workers) | `wetopi-build`, `wetopi-senior`, `wetopi-ops` (read-only ops) | Implement cards within objective boundaries, self-validate, deliver evidence | Applying/destroying real infrastructure from build cards; approving own work |\n   | JUDGE (independent review) | `wetopi-approver` or a reviewer that did not implement | Verify deliveries against the Objective's verification contract; record verdicts (APPROVE / REQUEST_CHANGES / REJECT) | Implementing; merging without operator authority |\n\n2. **The Architect has no Git write.** The Architect produces the Objective\n   Draft (WHAT/WHY/DONE) and hands it to the Lead. The draft enters Git only\n   through a **separate Lead/Docs task** that opens the documentation PR.\n   This PR \u2014 not the draft itself \u2014 is the reviewed, mergeable artifact.\n   The Architect may propose, comment, and answer questions, but never\n   pushes branches or commits.\n\n3. **Lead acquires and maintains execution.** Once an Objective is accepted,\n   the Lead OWNs its execution: decomposition into cards, assignment to\n   lanes, the Execution Ledger (single-writer, Lead-owned), and the\n   documentation PRs that land Objective artifacts in Git. The Kanban board\n   and Work Ledger remain the **operational state (level 1)**; the Git\n   Objective is the **versioned contract (level 2)**. The ledger references\n   the Objective (commit/PR SHA); the Objective never copies ledger state.\n   **Never copied into Git:** live snapshots, `.env`, `auth.json`, tokens,\n   connection URLs, credential hashes, pool output, or any secret \u2014 Git\n   holds contracts and evidence references, never live operational state\n   (DocOps \u00a71 authority model preserved).\n\n4. **Build/PR is separated from apply.** No build or documentation card\n   applies or destroys real infrastructure. Apply runs only from explicitly\n   authorized ops cards following reviewed runbooks with the human gate \u2014\n   the standing platform rule. The Objective layer does not change it: an\n   Objective's verification contract may REQUIRE an apply as a follow-up\n   ops card, but the build/PR deliverable itself never executes one.\n\n5. **Review is conditional and fail-closed.** A delivery is NOT done until\n   its verification contract passes independent review. If the reviewer is\n   unavailable, the verdict is missing, or evidence is incomplete, the\n   outcome is **blocked**, never silently accepted. **`goal_mode`\n   (automatic goal continuation) never substitutes for review** \u2014 it may\n   drive a worker's iteration budget, but the JUDGE step remains a\n   separate, human-supervised gate. **`goal_mode` is additionally\n   capability-gated like Qwen local:** it stays DISABLED for real\n   Objectives until a non-mutating P2 smoke proves the fail-closed\n   contract \u2014 DONE requires AC evidence + Lead audit + independent\n   APPROVE (never auto-done); CONTINUE/FAIL honor a bounded turn budget;\n   provider timeout/offline and any failure land Blocked and leave\n   `goal_mode` disabled; restarts deduplicate to a single reconciled run;\n   no external mutation in any case. It **remains disabled on any\n   failure**; the full enablement gate and smoke contract are specified\n   in [ARCH-HARNESS-V31-GOVERNANCE](../agents/harness-v31-governance.md)\n\n6. **Qwen local is capability-gated and may remain excluded.** Local-model\n   participants (Qwen via Ollama) may take part in the pilot only if a\n   capability gate passes (adequate reasoning/verbosity for the assigned\n   role) AND the economics gate of Decision 9 passes. If either fails or\n   availability degrades (e.g. multi-day provider cooldowns), Qwen local\n   is simply **excluded** \u2014 exclusion never weakens any other gate and\n   never blocks the role design; work routes to available capable lanes\n   or blocks, fail-closed.\n\n7. **Backup, restoration and rollback triggers.** The level-1 operational\n   stores (Kanban DB, Work Ledger) are backed up on a defined schedule with\n   a documented restore procedure (ledger template carries the\n   backup/restore locator field). The level-2 Git contract is its own\n   backup (branch/PR history). **Rollback triggers** \u2014 any of these\n   invalidates the pilot state and invokes recovery: (a) a secret or live\n   credential detected in Git \u2192 immediate revert of the offending commit\n   plus credential rotation; (b) an unreviewed or unverified merge reached\n   `main` \u2192 revert, re-review; (c) an Objective or its ADR contradicts an\n   accepted ADR or the master plan \u2192 block the objective, amend or abandon;\n   (d) the generated catalog drifts from the source front matter \u2192\n   regenerate and re-run gates before further merges.\n\n8. **ADR lifecycle and merge gate.** This ADR opens as `proposed` for\n   independent review of the real PR diff. After an independent `APPROVE`\n   verdict, the Lead authorizes a **narrow in-PR transition to\n   `accepted`**: front matter status, the human Status line and the\n   decisions index updated atomically, all DocOps gates re-run. Only after\n   that transition (with a green branch check and a current approval) is\n   the PR mergeable \u2014 and even then this build card does not merge; the\n   merge is a separate operator-gated act. No P2 work starts until `main`\n   contains the accepted ADR with consistent index/navigation.\n\n9. **Qwen economics gate (routability, optional participant).** Qwen local\n   is additionally subject to an economics gate before routing any pilot\n   work to it: a **paired benchmark on the same three non-mutating\n   fixtures** recording `wall_time_validated` for the direct GPT/Lead run\n   and for the Qwen + Lead audit/rework run. Qwen is routable only if ALL\n   hold: 3/3 fixture ACs pass under Qwen; Qwen invokes zero cloud/GPT\n   fallback calls; and the aggregate Qwen (including audit/rework) is\n   \u2264 1.20\u00d7 the direct baseline's validated wall time. Any failure \u21d2 Qwen\n   excluded \u2014 exclusion never blocks the role design.\n\n10. **Status discipline.** Even after `accepted`, the pilot still has to\n    run: Harness V3.1 becomes the standing governance only if the operator\n    confirms it after the pilot verdict (promotion matrix of\n    OBJ-HARNESS-V31-001). Nothing in this document retires the existing\n    lane model, and no V3.1 mechanism may be treated as accepted\n    precedent for other work until then.\n\n## Rationale\n\n- A versioned WHAT/WHY/DONE gives the operator a reviewable contract\n  BEFORE execution and an auditable record AFTER it; Kanban alone gives\n  neither (cards are rewritten, completed runs are summarized).\n- Separating the definer from the executor and both from the judge is the\n  cheapest available defense against self-confirmation bias in an\n  agent-operated system.\n- Fail-closed semantics trade throughput for correctness: a stalled review\n  is a visible blocker the operator can resolve, whereas a silently\n  weakened gate is invisible until it fails in production.\n- The capability gate on local models encodes an honest limit: a model\n  that cannot sustain the required reasoning depth must not be scheduled\n  into a governance role just because it is available.\n\n## Consequences\n\n- **Positive:** objectives become reviewable, durable contracts; review\n  independence is structural, not aspirational; secrets and live state\n  stay out of Git by contract; degraded conditions surface as visible\n  blockers instead of silent substitutions.\n- **Negative:** more ceremony per objective (draft, handoff, PR, review,\n  verdict) \u2014 meaningful overhead for small tasks, so Objectives are\n  reserved for multi-card, cross-component work; small work stays\n  Kanban-only. Review availability can bottleneck delivery (fail-closed\n  by design). A second source (Git contract) can drift from the live\n  board if the Lead neglects the ledger cross-references \u2014 drift is\n  mitigated by review, not by copying state into Git.\n- **Neutral:** the Architect role needs a profile (or operator mode) with\n  deliberately no Git credentials; this is an operational setup detail\n  owned by the Lead.\n\n## Alternatives considered\n\n| Alternative | Why rejected |\n|---|---|\n| Objectives as Kanban cards only (no Git contract) | Keeps WHAT/WHY/DONE in mutable operational state; no reviewed contract; no versioned audit trail. |\n| Full independence: every card independently reviewed | Prohibitive review cost for the pilot's size; Objectives batch the review at the contract and delivery boundaries instead. |\n| `goal_mode` auto-completion as the acceptance signal | Substitutes a heuristic judge for independent review; explicitly rejected (Decision 5). |\n| A dedicated objectives database / RAG store | Heavier than the pilot needs; Git + generated catalog already provide versioning and retrieval; revisit if objective count grows. |\n| Wait for agent-services planes before governing | Governance gaps identified in Phases 0\u20134 exist today; the pilot is deliberately lightweight so it runs on the current Kanban harness. |\n\n## Migration / implementation notes\n\n- Phase P1 (this PR, unmerged): ADR-0008 (proposed), the governance\n  architecture doc, the objectives section with the first active Objective\n  (OBJ-HARNESS-V31-001) and both templates; nav and catalog regenerated;\n  independent review of the real PR diff (max two rounds).\n- P1 closure: after independent APPROVE, the narrow in-PR\n  proposed\u2192accepted transition (Decision 8) with all gates re-run; the PR\n  becomes mergeable but this card does not merge \u2014 the merge is the\n  operator's separate act.\n- Phase P2 (pilot run): starts ONLY after `main` contains the accepted\n  ADR with consistent index/navigation. The Lead executes\n  OBJ-HARNESS-V31-001 end-to-end under the mechanism; deviations and\n  evidence are recorded in the Execution Ledger (level 1), not here.\n- Phase P3 (verdict): operator confirmation after the pilot \u2192 V3.1\n  becomes standing governance (objective moved to `completed/`) or the\n  pilot is closed with lessons recorded.\n- Rollback = Decision 7 triggers; the docs revert via normal Git revert\n  of the objective's merge, and level-1 stores restore from backup.\n\n## Related documents\n\n- [ARCH-HARNESS-V31-GOVERNANCE](../agents/harness-v31-governance.md) \u2014\n  operating model: roles, gates, degraded routing, fail-closed state\n  machine.\n- [OBJ-HARNESS-V31-001](../objectives/active/OBJ-HARNESS-V31-001.md) \u2014\n  the first active Objective (the pilot itself).\n- [REF-OBJECTIVE-SPEC-TEMPLATE](../objectives/templates/objective-spec.md)\n  and\n  [PROC-EXECUTION-LEDGER-TEMPLATE](../objectives/templates/execution-ledger.md)\n  \u2014 the templates every Objective uses.\n- ADR-0003 \u2014 the established pattern of separate, explicit sources of\n  truth per lifecycle that the L1/L2 split extends to agent governance.",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (decisions/ADR-0008-harness-v31-objective-governance.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-adr-0008%3A-harness-v3\">ADR-0008: Harness V3.1 objective governance \u2014 versioned Git Objectives with separated THINK, OWN, DO and JUDGE</p>\n<p id=\"bkmrk-date%3A-2026-08-30-sta\"><strong>Date:</strong> 2026-08-30\n<strong>Status:</strong> Accepted (2026-08-30). Lifecycle completed per the build-p1 spec v2:\nthe ADR opened as <code>proposed</code> for independent review; the independent content\nreview reached <code>APPROVE</code> (round 2, <code>t_f5f1fb23</code>) with green CI, and this narrow\nin-PR transition updates front matter, this human status line and the decisions\nindex atomically, with all DocOps gates re-run. Landing the pilot is NOT\nautomatic standing governance \u2014 V3.1 becoming standing governance is recorded\nonly after the accepted ADR is merged and the pilot verdict is in, per the\npromotion matrix of\n<a href=\"https://book.wetopi.net/objectives/active/OBJ-HARNESS-V31-001.md\">OBJ-HARNESS-V31-001</a>.</p>\n<h2 id=\"bkmrk-context\">Context</h2>\n<p id=\"bkmrk-wetopi-is-built-and-\">Wetopi is built and operated by an AI-agent harness (Hermes Kanban lanes:\ndiscovery / build / ops / senior / approver) supervised by one human\noperator. Two structural risks have surfaced during Phases 0\u20134:</p>\n<ol id=\"bkmrk-work-definition-live\">\n<li><strong>Work definition lives only in operational state.</strong> The Kanban board\n(and the Work Ledger that will supersede it) is excellent for live\ncoordination but is mutable, profile-local, and not reviewable as a\nversioned contract. WHAT we are building, WHY, and the definition of\nDONE deserve a reviewed, versioned, auditable source that survives board\nchurn.</li>\n<li><strong>One agent can currently both define and judge work.</strong> Without a role\nseparation, the harness risks self-confirmation: the agent that wrote a\nplan also reviews it, and execution shortcuts (auto-continuation,\ndegraded lanes, capability substitutions) can silently weaken review\ndiscipline.</li>\n</ol>\n<p id=\"bkmrk-harness-v3.1-is-the-\">Harness V3.1 is the pilot that addresses both: versioned Git Objectives\nplus a strict role separation. This ADR records the governance decisions;\n<a href=\"https://book.wetopi.net/agents/harness-v31-governance.md\">ARCH-HARNESS-V31-GOVERNANCE</a>\ndocuments the operating model (roles, gates, degraded routing, fail-closed\nstate machine); <a href=\"https://book.wetopi.net/objectives/active/OBJ-HARNESS-V31-001.md\">OBJ-HARNESS-V31-001</a>\nis the first Objective exercising the mechanism end-to-end.</p>\n<p id=\"bkmrk-this-adr-follows-the\">This ADR follows the accepted decision conventions (ADR-0003 among them)\nand does not modify any infrastructure responsibility split \u2014 it is\nagent-governance only. It is consistent with ADR-0003's principle of\nseparate, explicit sources of truth per lifecycle.</p>\n<h2 id=\"bkmrk-decision\">Decision</h2>\n<ol id=\"bkmrk-role-separation-thin\">\n<li>\n<p><strong>Role separation THINK / OWN / DO / JUDGE.</strong> Harness work is split into\nfour non-overlapping roles:</p>\n<table>\n<thead>\n<tr>\n<th>Role</th>\n<th>Lane / actor</th>\n<th>Responsibility</th>\n<th>Explicitly NOT allowed</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>THINK (Architect)</td>\n<td>architect capability (e.g. an architect-designated profile or the operator acting as architect)</td>\n<td>Define WHAT, WHY and DONE as an <strong>Objective Draft</strong>: outcome, context, acceptance criteria, verification contract, constraints, boundaries, stop conditions</td>\n<td>Git write, execution, card claiming, review of own draft</td>\n</tr>\n<tr>\n<td>OWN (Lead)</td>\n<td><code>wetopi-lead</code></td>\n<td>Acquire the accepted Objective, decompose it into cards, dispatch, maintain the Execution Ledger, own the documentation handoff</td>\n<td>Authoring the Objective's WHAT/WHY/DONE it did not receive; judging deliveries</td>\n</tr>\n<tr>\n<td>DO (workers)</td>\n<td><code>wetopi-build</code>, <code>wetopi-senior</code>, <code>wetopi-ops</code> (read-only ops)</td>\n<td>Implement cards within objective boundaries, self-validate, deliver evidence</td>\n<td>Applying/destroying real infrastructure from build cards; approving own work</td>\n</tr>\n<tr>\n<td>JUDGE (independent review)</td>\n<td><code>wetopi-approver</code> or a reviewer that did not implement</td>\n<td>Verify deliveries against the Objective's verification contract; record verdicts (APPROVE / REQUEST_CHANGES / REJECT)</td>\n<td>Implementing; merging without operator authority</td>\n</tr>\n</tbody>\n</table>\n</li>\n<li>\n<p><strong>The Architect has no Git write.</strong> The Architect produces the Objective\nDraft (WHAT/WHY/DONE) and hands it to the Lead. The draft enters Git only\nthrough a <strong>separate Lead/Docs task</strong> that opens the documentation PR.\nThis PR \u2014 not the draft itself \u2014 is the reviewed, mergeable artifact.\nThe Architect may propose, comment, and answer questions, but never\npushes branches or commits.</p>\n</li>\n<li>\n<p><strong>Lead acquires and maintains execution.</strong> Once an Objective is accepted,\nthe Lead OWNs its execution: decomposition into cards, assignment to\nlanes, the Execution Ledger (single-writer, Lead-owned), and the\ndocumentation PRs that land Objective artifacts in Git. The Kanban board\nand Work Ledger remain the <strong>operational state (level 1)</strong>; the Git\nObjective is the <strong>versioned contract (level 2)</strong>. The ledger references\nthe Objective (commit/PR SHA); the Objective never copies ledger state.\n<strong>Never copied into Git:</strong> live snapshots, <code>.env</code>, <code>auth.json</code>, tokens,\nconnection URLs, credential hashes, pool output, or any secret \u2014 Git\nholds contracts and evidence references, never live operational state\n(DocOps \u00a71 authority model preserved).</p>\n</li>\n<li>\n<p><strong>Build/PR is separated from apply.</strong> No build or documentation card\napplies or destroys real infrastructure. Apply runs only from explicitly\nauthorized ops cards following reviewed runbooks with the human gate \u2014\nthe standing platform rule. The Objective layer does not change it: an\nObjective's verification contract may REQUIRE an apply as a follow-up\nops card, but the build/PR deliverable itself never executes one.</p>\n</li>\n<li>\n<p><strong>Review is conditional and fail-closed.</strong> A delivery is NOT done until\nits verification contract passes independent review. If the reviewer is\nunavailable, the verdict is missing, or evidence is incomplete, the\noutcome is <strong>blocked</strong>, never silently accepted. <strong><code>goal_mode</code>\n(automatic goal continuation) never substitutes for review</strong> \u2014 it may\ndrive a worker's iteration budget, but the JUDGE step remains a\nseparate, human-supervised gate. <strong><code>goal_mode</code> is additionally\ncapability-gated like Qwen local:</strong> it stays DISABLED for real\nObjectives until a non-mutating P2 smoke proves the fail-closed\ncontract \u2014 DONE requires AC evidence + Lead audit + independent\nAPPROVE (never auto-done); CONTINUE/FAIL honor a bounded turn budget;\nprovider timeout/offline and any failure land Blocked and leave\n<code>goal_mode</code> disabled; restarts deduplicate to a single reconciled run;\nno external mutation in any case. It <strong>remains disabled on any\nfailure</strong>; the full enablement gate and smoke contract are specified\nin <a href=\"https://book.wetopi.net/agents/harness-v31-governance.md\">ARCH-HARNESS-V31-GOVERNANCE</a></p>\n</li>\n<li>\n<p><strong>Qwen local is capability-gated and may remain excluded.</strong> Local-model\nparticipants (Qwen via Ollama) may take part in the pilot only if a\ncapability gate passes (adequate reasoning/verbosity for the assigned\nrole) AND the economics gate of Decision 9 passes. If either fails or\navailability degrades (e.g. multi-day provider cooldowns), Qwen local\nis simply <strong>excluded</strong> \u2014 exclusion never weakens any other gate and\nnever blocks the role design; work routes to available capable lanes\nor blocks, fail-closed.</p>\n</li>\n<li>\n<p><strong>Backup, restoration and rollback triggers.</strong> The level-1 operational\nstores (Kanban DB, Work Ledger) are backed up on a defined schedule with\na documented restore procedure (ledger template carries the\nbackup/restore locator field). The level-2 Git contract is its own\nbackup (branch/PR history). <strong>Rollback triggers</strong> \u2014 any of these\ninvalidates the pilot state and invokes recovery: (a) a secret or live\ncredential detected in Git \u2192 immediate revert of the offending commit\nplus credential rotation; (b) an unreviewed or unverified merge reached\n<code>main</code> \u2192 revert, re-review; (c) an Objective or its ADR contradicts an\naccepted ADR or the master plan \u2192 block the objective, amend or abandon;\n(d) the generated catalog drifts from the source front matter \u2192\nregenerate and re-run gates before further merges.</p>\n</li>\n<li>\n<p><strong>ADR lifecycle and merge gate.</strong> This ADR opens as <code>proposed</code> for\nindependent review of the real PR diff. After an independent <code>APPROVE</code>\nverdict, the Lead authorizes a <strong>narrow in-PR transition to\n<code>accepted</code></strong>: front matter status, the human Status line and the\ndecisions index updated atomically, all DocOps gates re-run. Only after\nthat transition (with a green branch check and a current approval) is\nthe PR mergeable \u2014 and even then this build card does not merge; the\nmerge is a separate operator-gated act. No P2 work starts until <code>main</code>\ncontains the accepted ADR with consistent index/navigation.</p>\n</li>\n<li>\n<p><strong>Qwen economics gate (routability, optional participant).</strong> Qwen local\nis additionally subject to an economics gate before routing any pilot\nwork to it: a <strong>paired benchmark on the same three non-mutating\nfixtures</strong> recording <code>wall_time_validated</code> for the direct GPT/Lead run\nand for the Qwen + Lead audit/rework run. Qwen is routable only if ALL\nhold: 3/3 fixture ACs pass under Qwen; Qwen invokes zero cloud/GPT\nfallback calls; and the aggregate Qwen (including audit/rework) is\n\u2264 1.20\u00d7 the direct baseline's validated wall time. Any failure \u21d2 Qwen\nexcluded \u2014 exclusion never blocks the role design.</p>\n</li>\n<li>\n<p><strong>Status discipline.</strong> Even after <code>accepted</code>, the pilot still has to\nrun: Harness V3.1 becomes the standing governance only if the operator\nconfirms it after the pilot verdict (promotion matrix of\nOBJ-HARNESS-V31-001). Nothing in this document retires the existing\nlane model, and no V3.1 mechanism may be treated as accepted\nprecedent for other work until then.</p>\n</li>\n</ol>\n<h2 id=\"bkmrk-rationale\">Rationale</h2>\n<ul id=\"bkmrk-a-versioned-what%2Fwhy\">\n<li>A versioned WHAT/WHY/DONE gives the operator a reviewable contract\nBEFORE execution and an auditable record AFTER it; Kanban alone gives\nneither (cards are rewritten, completed runs are summarized).</li>\n<li>Separating the definer from the executor and both from the judge is the\ncheapest available defense against self-confirmation bias in an\nagent-operated system.</li>\n<li>Fail-closed semantics trade throughput for correctness: a stalled review\nis a visible blocker the operator can resolve, whereas a silently\nweakened gate is invisible until it fails in production.</li>\n<li>The capability gate on local models encodes an honest limit: a model\nthat cannot sustain the required reasoning depth must not be scheduled\ninto a governance role just because it is available.</li>\n</ul>\n<h2 id=\"bkmrk-consequences\">Consequences</h2>\n<ul id=\"bkmrk-positive%3A-objectives\">\n<li><strong>Positive:</strong> objectives become reviewable, durable contracts; review\nindependence is structural, not aspirational; secrets and live state\nstay out of Git by contract; degraded conditions surface as visible\nblockers instead of silent substitutions.</li>\n<li><strong>Negative:</strong> more ceremony per objective (draft, handoff, PR, review,\nverdict) \u2014 meaningful overhead for small tasks, so Objectives are\nreserved for multi-card, cross-component work; small work stays\nKanban-only. Review availability can bottleneck delivery (fail-closed\nby design). A second source (Git contract) can drift from the live\nboard if the Lead neglects the ledger cross-references \u2014 drift is\nmitigated by review, not by copying state into Git.</li>\n<li><strong>Neutral:</strong> the Architect role needs a profile (or operator mode) with\ndeliberately no Git credentials; this is an operational setup detail\nowned by the Lead.</li>\n</ul>\n<h2 id=\"bkmrk-alternatives-conside\">Alternatives considered</h2>\n<table id=\"bkmrk-alternative-why-reje\">\n<thead>\n<tr>\n<th>Alternative</th>\n<th>Why rejected</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Objectives as Kanban cards only (no Git contract)</td>\n<td>Keeps WHAT/WHY/DONE in mutable operational state; no reviewed contract; no versioned audit trail.</td>\n</tr>\n<tr>\n<td>Full independence: every card independently reviewed</td>\n<td>Prohibitive review cost for the pilot's size; Objectives batch the review at the contract and delivery boundaries instead.</td>\n</tr>\n<tr>\n<td><code>goal_mode</code> auto-completion as the acceptance signal</td>\n<td>Substitutes a heuristic judge for independent review; explicitly rejected (Decision 5).</td>\n</tr>\n<tr>\n<td>A dedicated objectives database / RAG store</td>\n<td>Heavier than the pilot needs; Git + generated catalog already provide versioning and retrieval; revisit if objective count grows.</td>\n</tr>\n<tr>\n<td>Wait for agent-services planes before governing</td>\n<td>Governance gaps identified in Phases 0\u20134 exist today; the pilot is deliberately lightweight so it runs on the current Kanban harness.</td>\n</tr>\n</tbody>\n</table>\n<h2 id=\"bkmrk-migration-%2F-implemen\">Migration / implementation notes</h2>\n<ul id=\"bkmrk-phase-p1-%28this-pr%2C-u\">\n<li>Phase P1 (this PR, unmerged): ADR-0008 (proposed), the governance\narchitecture doc, the objectives section with the first active Objective\n(OBJ-HARNESS-V31-001) and both templates; nav and catalog regenerated;\nindependent review of the real PR diff (max two rounds).</li>\n<li>P1 closure: after independent APPROVE, the narrow in-PR\nproposed\u2192accepted transition (Decision 8) with all gates re-run; the PR\nbecomes mergeable but this card does not merge \u2014 the merge is the\noperator's separate act.</li>\n<li>Phase P2 (pilot run): starts ONLY after <code>main</code> contains the accepted\nADR with consistent index/navigation. The Lead executes\nOBJ-HARNESS-V31-001 end-to-end under the mechanism; deviations and\nevidence are recorded in the Execution Ledger (level 1), not here.</li>\n<li>Phase P3 (verdict): operator confirmation after the pilot \u2192 V3.1\nbecomes standing governance (objective moved to <code>completed/</code>) or the\npilot is closed with lessons recorded.</li>\n<li>Rollback = Decision 7 triggers; the docs revert via normal Git revert\nof the objective's merge, and level-1 stores restore from backup.</li>\n</ul>\n<h2 id=\"bkmrk-related-documents\">Related documents</h2>\n<ul id=\"bkmrk-arch-harness-v31-gov\">\n<li><a href=\"https://book.wetopi.net/agents/harness-v31-governance.md\">ARCH-HARNESS-V31-GOVERNANCE</a> \u2014\noperating model: roles, gates, degraded routing, fail-closed state\nmachine.</li>\n<li><a href=\"https://book.wetopi.net/objectives/active/OBJ-HARNESS-V31-001.md\">OBJ-HARNESS-V31-001</a> \u2014\nthe first active Objective (the pilot itself).</li>\n<li><a href=\"https://book.wetopi.net/objectives/templates/objective-spec.md\">REF-OBJECTIVE-SPEC-TEMPLATE</a>\nand\n<a href=\"https://book.wetopi.net/objectives/templates/execution-ledger.md\">PROC-EXECUTION-LEDGER-TEMPLATE</a>\n\u2014 the templates every Objective uses.</li>\n<li>ADR-0003 \u2014 the established pattern of separate, explicit sources of\ntruth per lifecycle that the L1/L2 split extends to agent governance.</li>\n</ul>\n"
 },
 {
  "id": 52,
  "title": "ADR 0009 secrets vault harness",
  "slug": "adr-0009-secrets-vault-harness",
  "book_id": 8,
  "chapter_id": 28,
  "description": null,
  "created_at": "2026-09-06T13:39:00.000000Z",
  "updated_at": "2026-09-06T13:39:00.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (decisions/ADR-0009-secrets-vault-harness.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n ADR-0009: Secrets vault harness \u2014 Infisical self-hosted in-cluster (ns vault), identidades por perfil y contrato ai-hub\n\n**Date:** 2026-08-31\n**Status:** Accepted (2026-08-31). Lifecycle: el ADR abri\u00f3 como `proposed`;\nel **operador decidi\u00f3 el 2026-08-31** dejar Infisical \"totalmente operativo\ninternamente por el cluster service endpoint, con el service provider\ninstalado en Kubernetes\", lo que sustituye la VM dedicada por un despliegue\nIN-CLUSTER (namespace `vault`) y trae el Operator/CRDs v1beta1 a ESTA fase.\nEsta transici\u00f3n in-PR (front matter, status line, decisiones 1 y 8 y el\n\u00edndice de decisions, actualizados at\u00f3micamente, patr\u00f3n ADR-0008) registra\nesa decisi\u00f3n del operador con su fecha; el despliegue concreto se materializa\ny revisa independiente en infra PR #27 (`feat/vault-infra`).\n\n## Contexto\n\nHoy hay ocho ficheros plaintext bajo `ops01:~/secrets/`, copias manuales en\n`.env` de perfiles, credenciales GHCR de alcance excesivo, dos credenciales\nexpuestas pendientes de revocaci\u00f3n (`t_b904ba3f`), un acceso `argocd admin`\nest\u00e1tico y ausencia de trazabilidad de lecturas. El master plan \u00a710 ya\nseleccion\u00f3 Infisical self-hosted, SOPS+age \u00fanicamente para bootstrap/recovery\ny el contrato estable `vault.get/put/update/delete/list/exists` en ai-hub. La\nvalidaci\u00f3n de licencia, machine identities e integraci\u00f3n Kubernetes (\u00a710.6,\n`t_7459a827`) estaba en curso al redactar el handoff y es gate obligatorio\nantes de producci\u00f3n; su veredicto se registra bajo\n[Estado de la validaci\u00f3n \u00a710.6](#estado-de-la-validaci\u00f3n-106).\n\n## Decisi\u00f3n\n\n1. Ejecutar Infisical self-hosted **IN-CLUSTER, en el namespace `vault` de\n   `wetopi3`** (decisi\u00f3n del operador, 2026-08-31: Infisical totalmente\n   operativo internamente v\u00eda el service endpoint del cl\u00faster, con el\n   service provider instalado en Kubernetes). Esta decisi\u00f3n SUSTITUYE la\n   VM dedicada fuera del cl\u00faster del borrador `proposed`: el datastore\n   soportado (PostgreSQL + Redis `noeviction` persistentes) corre en el\n   mismo cl\u00faster, con backup cifrado off-host, restauraci\u00f3n ensayada y\n   Universal Auth privado (s\u00f3lo service interno). El trade-off de fallo\n   correlacionado con `wetopi3` se acepta expl\u00edcitamente y se compensa con\n   el contrato de recovery (pg_dump + age ensayado, AC8) \u2014 la alternativa\n   de VM dedicada queda registrada como alternativa considerada y descartada\n   por decisi\u00f3n del operador con fecha 2026-08-31.\n2. Publicar `vault.wetopi.net` detr\u00e1s del edge aprobado, con TLS LE production\n   y autenticaci\u00f3n nativa Infisical siempre obligatoria. La ruta p\u00fablica sirva\n   S\u00d3LO la UI humana: el API de machine identities (Universal Auth) NO es\n   alcanzable por el hostname p\u00fablico \u2014 enforcement verificable en el edge\n   (path-filter con DENY expl\u00edcito del resto, infra #27). Basic Auth se eval\u00faa\n   s\u00f3lo como defensa adicional de la UI humana; no puede sustituir la\n   autenticaci\u00f3n del vault ni interceptar/romper el API de machine identities.\n   Si no puede separarse con rutas inequ\u00edvocas o un hostname administrativo\n   distinto, no se activa.\n3. Crear una machine identity distinta por perfil Hermes activo; no se\n   comparte identidad ni bootstrap secret entre perfiles. Pol\u00edtica\n   default-deny y privilegios por proyecto/environment/path. El llamador no\n   puede elegir o suplantar otra identidad.\n4. ai-hub es la \u00fanica interfaz consumida por agentes. Su adaptador traduce el\n   contrato estable a la API versionada de Infisical. Los agentes no importan\n   SDKs, endpoints ni modelos de Infisical.\n5. El \u00fanico secreto persistente local de cada perfil es su credencial\n   bootstrap de machine identity, en fichero owner-only fuera de Git y fuera\n   de `.env`; el access token derivado es corto, s\u00f3lo en memoria y nunca se\n   registra. Client ID/referencias no secretas pueden declararse en\n   configuraci\u00f3n.\n6. Infisical es la autoridad runtime de valores, pol\u00edticas operativas y\n   eventos de auditor\u00eda. Git es autoridad L2 de arquitectura, contratos,\n   referencias y policy-as-code sin valores. El registro can\u00f3nico de perfiles\n   de Hermes determina el inventario de identidades. SOPS+age s\u00f3lo es\n   autoridad durante bootstrap/recovery declarado; Kanban s\u00f3lo refleja estado\n   L1 y no contiene secretos.\n7. La migraci\u00f3n no copia deuda: toda credencial expuesta, broad-scope,\n   compartida o sin propietario se reemplaza por una de m\u00ednimo alcance, se\n   verifica el consumidor y se revoca la anterior. No se importan credenciales\n   expuestas como estado final.\n8. La materializaci\u00f3n de Kubernetes Secrets y el Infisical Secrets Operator\n   suceden EN ESTA FASE (decisi\u00f3n del operador, 2026-08-31, que sustituye el\n   \"objetivo posterior\" del borrador `proposed`): el Operator v0.11.8 con\n   CRDs v1beta1 (`InfisicalAuth` + `InfisicalStaticSecret`; la superficie\n   `InfisicalSecret` v1alpha1 est\u00e1 deprecated upstream y NO se usa en\n   instalaciones nuevas) se instala junto al vault in-cluster y sincroniza\n   secrets hacia los K8s Secrets de los workloads v\u00eda Universal Auth por el\n   endpoint interno.\n\n### Alternativas consideradas\n\n- Infisical dentro de `wetopi3`: la versi\u00f3n `proposed` de este ADR (2026-08-31\n  por la ma\u00f1ana) lo descart\u00f3 por fallo correlacionado y p\u00e9rdida de acceso\n  durante recuperaci\u00f3n del cl\u00faster, y propuso una VM dedicada. El operador\n  reconsider\u00f3 esa premisa el mismo d\u00eda (chat 2026-08-31: dejar Infisical\n  totalmente operativo internamente por el cluster service endpoint, service\n  provider instalado en Kubernetes) y la decisi\u00f3n vigente es IN-CLUSTER; el\n  trade-off de fallo correlacionado se mitiga con el contrato de recovery\n  (decisi\u00f3n 1) y queda registrado aqu\u00ed para trazabilidad.\n- SOPS+age como vault diario: descartado porque no ofrece lectura din\u00e1mica\n  por identidad ni auditor\u00eda runtime suficiente; se conserva para\n  bootstrap/recovery.\n- API/SDK Infisical directa desde cada agente: descartado por acoplamiento,\n  proliferaci\u00f3n de credenciales y sem\u00e1nticas inconsistentes.\n- Una identidad compartida para Hermes/ai-hub: descartada porque impide\n  atribuci\u00f3n y ampl\u00eda el blast radius.\n- Copiar secretos a `.env`: prohibido como mecanismo normal; s\u00f3lo referencias\n  no secretas y una credencial bootstrap local por perfil.\n- Edge Basic Auth como control \u00fanico: descartado; no expresa autorizaci\u00f3n por\n  secreto y dificulta atribuci\u00f3n de machine identities.\n\n### Consecuencias y compliance ADR-0003\n\nEl vault in-cluster y su backup pasan a ser infraestructura Tier-0; se exige\nhardening (securityContexts non-root, secretos nunca en Git), monitorizaci\u00f3n,\nparcheo y runbook de recuperaci\u00f3n. El fallo correlacionado con `wetopi3` es\nun riesgo aceptado expl\u00edcitamente (decisi\u00f3n 1) \u2014 la disponibilidad del vault\nqueda ligada a la del cl\u00faster, y el recovery ensayado es el compensatorio\nobligatorio. Se a\u00f1ade un adaptador ai-hub y una pol\u00edtica por identidad, pero\nlos agentes quedan desacoplados.\nLas autoridades quedan expl\u00edcitas: Infisical runtime; Git L2; registro Hermes\npara perfiles; SOPS+age s\u00f3lo bootstrap/recovery; auditor\u00eda append-only para\nevidencia; Kanban no es fuente de verdad del dise\u00f1o.\n\nEl architecture pack normativo (boundary y topolog\u00eda, namespaces y\nreferencias, contrato `vault.*`, matriz de identidades y permisos, bootstrap\ny ciclo de credenciales, rotaci\u00f3n, backup y recovery, migraci\u00f3n del estado\nvivo, auditor\u00eda, riesgos y diagramas) vive como pill de arquitectura en\n[ARCH-VAULT-SYSTEM](../architecture/vault-system/index.md); el ADR\nregistra aqu\u00ed s\u00f3lo la decisi\u00f3n y sus consecuencias.\n\n<a id=\"estado-de-la-validaci\u00f3n-106\"></a>\n## Estado de la validaci\u00f3n \u00a710.6\n\nEl addendum de integraci\u00f3n del handoff THINK (que registraba la validaci\u00f3n\nen curso) se sustituye por el veredicto final de la discovery\n(`t_7459a827`, 2026-08-31): **GO para Infisical self-hosted como vault del\nharness, con TRES condicionales**. Informe completo:\n`findings-infisical-selfhosted.md` (adjunto a las cards `t_7459a827` y\n`t_e2292367`).\n\n### Hechos materiales verificados (con fuente oficial)\n\n| # | Hecho | Fuente oficial |\n|---|---|---|\n| 1 | Licensing: core MIT Expat; directorio `ee/` bajo Infisical Enterprise License; activaci\u00f3n self-hosted por `LICENSE_KEY` (online u offline) | https://github.com/Infisical/infisical/blob/main/LICENSE \u00b7 https://github.com/Infisical/infisical/blob/main/backend/src/ee/LICENSE.md \u00b7 https://infisical.com/docs/self-hosting/ee |\n| 2 | Machine identities / Universal Auth completo en MIT core: clientId/secret \u2192 JWT de corta vida, TTL/renew, rotaci\u00f3n y revocaci\u00f3n de client secrets, expiry alerts | https://infisical.com/docs/documentation/platform/identities/machine-identities \u00b7 https://infisical.com/docs/documentation/platform/identities/universal-auth |\n| 3 | Audit logs en self-hosted es feature de pago (Enterprise license) | https://infisical.com/docs/documentation/platform/audit-logs |\n| 4 | Audit log streams (SIEM/Splunk/Datadog) tambi\u00e9n paid EE | https://infisical.com/docs/documentation/platform/audit-log-streams/audit-log-streams |\n| 5 | Point-in-Time Recovery (rollback de folder/env a snapshot) es paid EE | https://infisical.com/docs/documentation/platform/pit-recovery |\n| 6 | Trusted IPs sobre client secret / access token de identidades es paid EE | https://infisical.com/docs/documentation/platform/identities/universal-auth |\n| 7 | Contrato `vault.*` mapea 1:1 a la API v4 (`/api/v4/secrets*`), verificada contra el OpenAPI oficial | https://app.infisical.com/api/docs/json |\n| 8 | Secrets Operator K8s vivo (v0.11.8; CRDs v1beta1 recomendados; `InfisicalSecret` v1alpha1 deprecated) | https://github.com/Infisical/kubernetes-operator/releases \u00b7 https://infisical.com/docs/integrations/platforms/kubernetes/infisical-static-secret-crd |\n| 9 | Datastore: PostgreSQL es la \u00fanica BD soportada y Redis es dependencia dura y persistente (`noeviction`, AOF/RDB) | https://infisical.com/docs/self-hosting/configuration/requirements.md |\n| 10 | Recovery SOPS+age viable v\u00eda `pg_dump` + age; no existe export cifrado nativo program\u00e1tico | https://infisical.com/docs/self-hosting/configuration/requirements.md \u00b7 https://infisical.com/docs/self-hosting/guides/rotating-the-encryption-key |\n| 11 | Secret versioning base (MIT) con lectura por `?version=N` | https://infisical.com/docs/documentation/platform/secret-versioning |\n\n### Decisi\u00f3n expl\u00edcita por condicional\n\n1. **Audit logs (EE, paid) \u2014 DECISI\u00d3N: NO-GO sin licencia EE aceptada; sin\n   control compensatorio admisible.** El audit append-only sin valores es\n   requisito normativo (AC7) y la alternativa a nivel ingress/infraestructura\n   no cubre actor identity, decisi\u00f3n allow/deny ni retenci\u00f3n 365 d\u00edas.\n   Por tanto, la compra de la licencia Enterprise (`LICENSE_KEY`) que\n   habilita el audit es condici\u00f3n de cierre de S1. Si el operador no la\n   acepta, S1 es NO-GO y el objetivo se detiene antes de S3.\n   Fuente: https://infisical.com/docs/documentation/platform/audit-logs\n2. **PIT recovery (EE, paid) \u2014 DECISI\u00d3N: control compensatorio; NO compra.**\n   El PIT de Infisical (rollback por folder/env a snapshot) no es requisito\n   del contrato: la ruta de recovery normativa es backup/restore SOPS+age\n   v\u00eda `pg_dump` + age con restore drill aislado (AC8), y la lectura de\n   versiones hist\u00f3ricas usa el versioning base MIT. PIT queda como mejora\n   opcional futura. Reapertura: si en S7 el restore drill demostrara que la\n   ventana de recuperaci\u00f3n exigida no es alcanzable sin PIT, vuelve aqu\u00ed la\n   decisi\u00f3n \u2014 compra de licencia EE o NO-GO, nunca promesa abierta.\n   Fuente: https://infisical.com/docs/documentation/platform/pit-recovery\n3. **Trusted IPs (EE, paid) \u2014 DECISI\u00d3N: control compensatorio de red; NO\n   compra.** La restricci\u00f3n de IP nativa de Universal Auth no se compra: el\n   binding de origen se compensa a nivel de red. Con la decisi\u00f3n de\n   despliegue in-cluster (2026-08-31) la forma del compensatorio es el\n   AISLAMIENTO DE RED DEL SERVICE: Universal Auth s\u00f3lo es alcanzable por el\n   service interno `vault.vault.svc.cluster.local`; la ruta p\u00fablica\n   `vault.wetopi.net` sirve exclusivamente la UI humana con DENY expl\u00edcito\n   de los endpoints de API (infra #27), y los StorageClasses PVC no exponen\n   nada a Internet.\n\n   > **NOTA (2026-08-31) \u2014 RESUELTO por la decisi\u00f3n del operador:** la\n   > exposici\u00f3n de Universal Auth qued\u00f3 decidida como **private-network-only\n   > in-cluster** ( Universal Auth s\u00f3lo por `vault.vault.svc.cluster.local`,\n   > ruta p\u00fablica = s\u00f3lo UI humana con DENY de API; chat del operador\n   > 2026-08-31 e infra #27). Este era el OPEN \"a resolverse en S0 antes del\n   > primer identity bootstrap\"; la decisi\u00f3n de arriba y el enforcement de\n   > infra #27 lo cierran. La eficacia del control (login p\u00fablico rechazado /\n   > login interno autorizado) se demuestra antes del primer bootstrap.\n\nConsecuencia normativa para S1: la etapa exige firma conjunta\napprover+operador que fije versi\u00f3n/edici\u00f3n/componentes y acepte\nexpl\u00edcitamente licencia/coste (licencia base + audit EE, seg\u00fan las\ndecisiones de arriba). No se puede degradar AC7 para usar una edici\u00f3n\ngratuita. Si el condicional audit no queda cubierto por una licencia\naceptada, el resultado es NO-GO y se detiene antes de S3.\n\n## OPEN expl\u00edcitos\n\n- Requisitos exactos de edici\u00f3n/licencia, audit log, Universal Auth, CRUD\n  API, datastore, export y operator: resueltos por el veredicto \u00a710.6 de\n  `t_7459a827` (arriba); pendiente la aceptaci\u00f3n expl\u00edcita de licencia/coste\n  por el operador (gate S1).\n- Inventario exacto de perfiles Hermes activos y scopes; se deriva del\n  registro can\u00f3nico, no de esta conversaci\u00f3n.\n- Decisi\u00f3n operador: activar Basic Auth adicional en UI s\u00f3lo despu\u00e9s de\n  prueba de no interferencia y valorar hostname/ruta administrativa\n  separada.\n- Retenci\u00f3n 365 d\u00edas y periodicidad de restore drill: confirmar\n  coste/operaci\u00f3n en S1/S2.\n\n## Related documents\n\n- [OBJ-VAULT-001](../objectives/active/OBJ-VAULT-001.md) \u2014 el Objective con\n  ACs, dashboard S0-S10, gates y promotion matrix.\n- ADR-0003 \u2014 el patr\u00f3n de fuentes de verdad separadas y expl\u00edcitas por\n  ciclo de vida (compliance registrado en Consecuencias).",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (decisions/ADR-0009-secrets-vault-harness.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-adr-0009%3A-secrets-va\">ADR-0009: Secrets vault harness \u2014 Infisical self-hosted in-cluster (ns vault), identidades por perfil y contrato ai-hub</p>\n<p id=\"bkmrk-date%3A-2026-08-31-sta\"><strong>Date:</strong> 2026-08-31\n<strong>Status:</strong> Accepted (2026-08-31). Lifecycle: el ADR abri\u00f3 como <code>proposed</code>;\nel <strong>operador decidi\u00f3 el 2026-08-31</strong> dejar Infisical \"totalmente operativo\ninternamente por el cluster service endpoint, con el service provider\ninstalado en Kubernetes\", lo que sustituye la VM dedicada por un despliegue\nIN-CLUSTER (namespace <code>vault</code>) y trae el Operator/CRDs v1beta1 a ESTA fase.\nEsta transici\u00f3n in-PR (front matter, status line, decisiones 1 y 8 y el\n\u00edndice de decisions, actualizados at\u00f3micamente, patr\u00f3n ADR-0008) registra\nesa decisi\u00f3n del operador con su fecha; el despliegue concreto se materializa\ny revisa independiente en infra PR #27 (<code>feat/vault-infra</code>).</p>\n<h2 id=\"bkmrk-contexto\">Contexto</h2>\n<p id=\"bkmrk-hoy-hay-ocho-fichero\">Hoy hay ocho ficheros plaintext bajo <code>ops01:~/secrets/</code>, copias manuales en\n<code>.env</code> de perfiles, credenciales GHCR de alcance excesivo, dos credenciales\nexpuestas pendientes de revocaci\u00f3n (<code>t_b904ba3f</code>), un acceso <code>argocd admin</code>\nest\u00e1tico y ausencia de trazabilidad de lecturas. El master plan \u00a710 ya\nseleccion\u00f3 Infisical self-hosted, SOPS+age \u00fanicamente para bootstrap/recovery\ny el contrato estable <code>vault.get/put/update/delete/list/exists</code> en ai-hub. La\nvalidaci\u00f3n de licencia, machine identities e integraci\u00f3n Kubernetes (\u00a710.6,\n<code>t_7459a827</code>) estaba en curso al redactar el handoff y es gate obligatorio\nantes de producci\u00f3n; su veredicto se registra bajo\n<a href=\"#estado-de-la-validaci%C3%B3n-106\">Estado de la validaci\u00f3n \u00a710.6</a>.</p>\n<h2 id=\"bkmrk-decisi%C3%B3n\">Decisi\u00f3n</h2>\n<ol id=\"bkmrk-ejecutar-infisical-s\">\n<li>Ejecutar Infisical self-hosted <strong>IN-CLUSTER, en el namespace <code>vault</code> de\n<code>wetopi3</code></strong> (decisi\u00f3n del operador, 2026-08-31: Infisical totalmente\noperativo internamente v\u00eda el service endpoint del cl\u00faster, con el\nservice provider instalado en Kubernetes). Esta decisi\u00f3n SUSTITUYE la\nVM dedicada fuera del cl\u00faster del borrador <code>proposed</code>: el datastore\nsoportado (PostgreSQL + Redis <code>noeviction</code> persistentes) corre en el\nmismo cl\u00faster, con backup cifrado off-host, restauraci\u00f3n ensayada y\nUniversal Auth privado (s\u00f3lo service interno). El trade-off de fallo\ncorrelacionado con <code>wetopi3</code> se acepta expl\u00edcitamente y se compensa con\nel contrato de recovery (pg_dump + age ensayado, AC8) \u2014 la alternativa\nde VM dedicada queda registrada como alternativa considerada y descartada\npor decisi\u00f3n del operador con fecha 2026-08-31.</li>\n<li>Publicar <code>vault.wetopi.net</code> detr\u00e1s del edge aprobado, con TLS LE production\ny autenticaci\u00f3n nativa Infisical siempre obligatoria. La ruta p\u00fablica sirva\nS\u00d3LO la UI humana: el API de machine identities (Universal Auth) NO es\nalcanzable por el hostname p\u00fablico \u2014 enforcement verificable en el edge\n(path-filter con DENY expl\u00edcito del resto, infra #27). Basic Auth se eval\u00faa\ns\u00f3lo como defensa adicional de la UI humana; no puede sustituir la\nautenticaci\u00f3n del vault ni interceptar/romper el API de machine identities.\nSi no puede separarse con rutas inequ\u00edvocas o un hostname administrativo\ndistinto, no se activa.</li>\n<li>Crear una machine identity distinta por perfil Hermes activo; no se\ncomparte identidad ni bootstrap secret entre perfiles. Pol\u00edtica\ndefault-deny y privilegios por proyecto/environment/path. El llamador no\npuede elegir o suplantar otra identidad.</li>\n<li>ai-hub es la \u00fanica interfaz consumida por agentes. Su adaptador traduce el\ncontrato estable a la API versionada de Infisical. Los agentes no importan\nSDKs, endpoints ni modelos de Infisical.</li>\n<li>El \u00fanico secreto persistente local de cada perfil es su credencial\nbootstrap de machine identity, en fichero owner-only fuera de Git y fuera\nde <code>.env</code>; el access token derivado es corto, s\u00f3lo en memoria y nunca se\nregistra. Client ID/referencias no secretas pueden declararse en\nconfiguraci\u00f3n.</li>\n<li>Infisical es la autoridad runtime de valores, pol\u00edticas operativas y\neventos de auditor\u00eda. Git es autoridad L2 de arquitectura, contratos,\nreferencias y policy-as-code sin valores. El registro can\u00f3nico de perfiles\nde Hermes determina el inventario de identidades. SOPS+age s\u00f3lo es\nautoridad durante bootstrap/recovery declarado; Kanban s\u00f3lo refleja estado\nL1 y no contiene secretos.</li>\n<li>La migraci\u00f3n no copia deuda: toda credencial expuesta, broad-scope,\ncompartida o sin propietario se reemplaza por una de m\u00ednimo alcance, se\nverifica el consumidor y se revoca la anterior. No se importan credenciales\nexpuestas como estado final.</li>\n<li>La materializaci\u00f3n de Kubernetes Secrets y el Infisical Secrets Operator\nsuceden EN ESTA FASE (decisi\u00f3n del operador, 2026-08-31, que sustituye el\n\"objetivo posterior\" del borrador <code>proposed</code>): el Operator v0.11.8 con\nCRDs v1beta1 (<code>InfisicalAuth</code> + <code>InfisicalStaticSecret</code>; la superficie\n<code>InfisicalSecret</code> v1alpha1 est\u00e1 deprecated upstream y NO se usa en\ninstalaciones nuevas) se instala junto al vault in-cluster y sincroniza\nsecrets hacia los K8s Secrets de los workloads v\u00eda Universal Auth por el\nendpoint interno.</li>\n</ol>\n<h3 id=\"bkmrk-alternativas-conside\">Alternativas consideradas</h3>\n<ul id=\"bkmrk-infisical-dentro-de-\">\n<li>Infisical dentro de <code>wetopi3</code>: la versi\u00f3n <code>proposed</code> de este ADR (2026-08-31\npor la ma\u00f1ana) lo descart\u00f3 por fallo correlacionado y p\u00e9rdida de acceso\ndurante recuperaci\u00f3n del cl\u00faster, y propuso una VM dedicada. El operador\nreconsider\u00f3 esa premisa el mismo d\u00eda (chat 2026-08-31: dejar Infisical\ntotalmente operativo internamente por el cluster service endpoint, service\nprovider instalado en Kubernetes) y la decisi\u00f3n vigente es IN-CLUSTER; el\ntrade-off de fallo correlacionado se mitiga con el contrato de recovery\n(decisi\u00f3n 1) y queda registrado aqu\u00ed para trazabilidad.</li>\n<li>SOPS+age como vault diario: descartado porque no ofrece lectura din\u00e1mica\npor identidad ni auditor\u00eda runtime suficiente; se conserva para\nbootstrap/recovery.</li>\n<li>API/SDK Infisical directa desde cada agente: descartado por acoplamiento,\nproliferaci\u00f3n de credenciales y sem\u00e1nticas inconsistentes.</li>\n<li>Una identidad compartida para Hermes/ai-hub: descartada porque impide\natribuci\u00f3n y ampl\u00eda el blast radius.</li>\n<li>Copiar secretos a <code>.env</code>: prohibido como mecanismo normal; s\u00f3lo referencias\nno secretas y una credencial bootstrap local por perfil.</li>\n<li>Edge Basic Auth como control \u00fanico: descartado; no expresa autorizaci\u00f3n por\nsecreto y dificulta atribuci\u00f3n de machine identities.</li>\n</ul>\n<h3 id=\"bkmrk-consecuencias-y-comp\">Consecuencias y compliance ADR-0003</h3>\n<p id=\"bkmrk-el-vault-in-cluster-\">El vault in-cluster y su backup pasan a ser infraestructura Tier-0; se exige\nhardening (securityContexts non-root, secretos nunca en Git), monitorizaci\u00f3n,\nparcheo y runbook de recuperaci\u00f3n. El fallo correlacionado con <code>wetopi3</code> es\nun riesgo aceptado expl\u00edcitamente (decisi\u00f3n 1) \u2014 la disponibilidad del vault\nqueda ligada a la del cl\u00faster, y el recovery ensayado es el compensatorio\nobligatorio. Se a\u00f1ade un adaptador ai-hub y una pol\u00edtica por identidad, pero\nlos agentes quedan desacoplados.\nLas autoridades quedan expl\u00edcitas: Infisical runtime; Git L2; registro Hermes\npara perfiles; SOPS+age s\u00f3lo bootstrap/recovery; auditor\u00eda append-only para\nevidencia; Kanban no es fuente de verdad del dise\u00f1o.</p>\n<p id=\"bkmrk-el-architecture-pack\">El architecture pack normativo (boundary y topolog\u00eda, namespaces y\nreferencias, contrato <code>vault.*</code>, matriz de identidades y permisos, bootstrap\ny ciclo de credenciales, rotaci\u00f3n, backup y recovery, migraci\u00f3n del estado\nvivo, auditor\u00eda, riesgos y diagramas) vive como pill de arquitectura en\n<a href=\"https://book.wetopi.net/architecture/vault-system/index.md\">ARCH-VAULT-SYSTEM</a>; el ADR\nregistra aqu\u00ed s\u00f3lo la decisi\u00f3n y sus consecuencias.</p>\n<p id=\"bkmrk-\"><a id=\"bkmrk--1\"></a></p>\n<h2 id=\"bkmrk-estado-de-la-validac\">Estado de la validaci\u00f3n \u00a710.6</h2>\n<p id=\"bkmrk-el-addendum-de-integ\">El addendum de integraci\u00f3n del handoff THINK (que registraba la validaci\u00f3n\nen curso) se sustituye por el veredicto final de la discovery\n(<code>t_7459a827</code>, 2026-08-31): <strong>GO para Infisical self-hosted como vault del\nharness, con TRES condicionales</strong>. Informe completo:\n<code>findings-infisical-selfhosted.md</code> (adjunto a las cards <code>t_7459a827</code> y\n<code>t_e2292367</code>).</p>\n<h3 id=\"bkmrk-hechos-materiales-ve\">Hechos materiales verificados (con fuente oficial)</h3>\n<table id=\"bkmrk-%23-hecho-fuente-ofici\">\n<thead>\n<tr>\n<th>#</th>\n<th>Hecho</th>\n<th>Fuente oficial</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>1</td>\n<td>Licensing: core MIT Expat; directorio <code>ee/</code> bajo Infisical Enterprise License; activaci\u00f3n self-hosted por <code>LICENSE_KEY</code> (online u offline)</td>\n<td>https://github.com/Infisical/infisical/blob/main/LICENSE \u00b7 https://github.com/Infisical/infisical/blob/main/backend/src/ee/LICENSE.md \u00b7 https://infisical.com/docs/self-hosting/ee</td>\n</tr>\n<tr>\n<td>2</td>\n<td>Machine identities / Universal Auth completo en MIT core: clientId/secret \u2192 JWT de corta vida, TTL/renew, rotaci\u00f3n y revocaci\u00f3n de client secrets, expiry alerts</td>\n<td>https://infisical.com/docs/documentation/platform/identities/machine-identities \u00b7 https://infisical.com/docs/documentation/platform/identities/universal-auth</td>\n</tr>\n<tr>\n<td>3</td>\n<td>Audit logs en self-hosted es feature de pago (Enterprise license)</td>\n<td>https://infisical.com/docs/documentation/platform/audit-logs</td>\n</tr>\n<tr>\n<td>4</td>\n<td>Audit log streams (SIEM/Splunk/Datadog) tambi\u00e9n paid EE</td>\n<td>https://infisical.com/docs/documentation/platform/audit-log-streams/audit-log-streams</td>\n</tr>\n<tr>\n<td>5</td>\n<td>Point-in-Time Recovery (rollback de folder/env a snapshot) es paid EE</td>\n<td>https://infisical.com/docs/documentation/platform/pit-recovery</td>\n</tr>\n<tr>\n<td>6</td>\n<td>Trusted IPs sobre client secret / access token de identidades es paid EE</td>\n<td>https://infisical.com/docs/documentation/platform/identities/universal-auth</td>\n</tr>\n<tr>\n<td>7</td>\n<td>Contrato <code>vault.*</code> mapea 1:1 a la API v4 (<code>/api/v4/secrets*</code>), verificada contra el OpenAPI oficial</td>\n<td>https://app.infisical.com/api/docs/json</td>\n</tr>\n<tr>\n<td>8</td>\n<td>Secrets Operator K8s vivo (v0.11.8; CRDs v1beta1 recomendados; <code>InfisicalSecret</code> v1alpha1 deprecated)</td>\n<td>https://github.com/Infisical/kubernetes-operator/releases \u00b7 https://infisical.com/docs/integrations/platforms/kubernetes/infisical-static-secret-crd</td>\n</tr>\n<tr>\n<td>9</td>\n<td>Datastore: PostgreSQL es la \u00fanica BD soportada y Redis es dependencia dura y persistente (<code>noeviction</code>, AOF/RDB)</td>\n<td>https://infisical.com/docs/self-hosting/configuration/requirements.md</td>\n</tr>\n<tr>\n<td>10</td>\n<td>Recovery SOPS+age viable v\u00eda <code>pg_dump</code> + age; no existe export cifrado nativo program\u00e1tico</td>\n<td>https://infisical.com/docs/self-hosting/configuration/requirements.md \u00b7 https://infisical.com/docs/self-hosting/guides/rotating-the-encryption-key</td>\n</tr>\n<tr>\n<td>11</td>\n<td>Secret versioning base (MIT) con lectura por <code>?version=N</code></td>\n<td>https://infisical.com/docs/documentation/platform/secret-versioning</td>\n</tr>\n</tbody>\n</table>\n<h3 id=\"bkmrk-decisi%C3%B3n-expl%C3%ADcita-p\">Decisi\u00f3n expl\u00edcita por condicional</h3>\n<ol id=\"bkmrk-audit-logs-%28ee%2C-paid\">\n<li>\n<p><strong>Audit logs (EE, paid) \u2014 DECISI\u00d3N: NO-GO sin licencia EE aceptada; sin\ncontrol compensatorio admisible.</strong> El audit append-only sin valores es\nrequisito normativo (AC7) y la alternativa a nivel ingress/infraestructura\nno cubre actor identity, decisi\u00f3n allow/deny ni retenci\u00f3n 365 d\u00edas.\nPor tanto, la compra de la licencia Enterprise (<code>LICENSE_KEY</code>) que\nhabilita el audit es condici\u00f3n de cierre de S1. Si el operador no la\nacepta, S1 es NO-GO y el objetivo se detiene antes de S3.\nFuente: https://infisical.com/docs/documentation/platform/audit-logs</p>\n</li>\n<li>\n<p><strong>PIT recovery (EE, paid) \u2014 DECISI\u00d3N: control compensatorio; NO compra.</strong>\nEl PIT de Infisical (rollback por folder/env a snapshot) no es requisito\ndel contrato: la ruta de recovery normativa es backup/restore SOPS+age\nv\u00eda <code>pg_dump</code> + age con restore drill aislado (AC8), y la lectura de\nversiones hist\u00f3ricas usa el versioning base MIT. PIT queda como mejora\nopcional futura. Reapertura: si en S7 el restore drill demostrara que la\nventana de recuperaci\u00f3n exigida no es alcanzable sin PIT, vuelve aqu\u00ed la\ndecisi\u00f3n \u2014 compra de licencia EE o NO-GO, nunca promesa abierta.\nFuente: https://infisical.com/docs/documentation/platform/pit-recovery</p>\n</li>\n<li>\n<p><strong>Trusted IPs (EE, paid) \u2014 DECISI\u00d3N: control compensatorio de red; NO\ncompra.</strong> La restricci\u00f3n de IP nativa de Universal Auth no se compra: el\nbinding de origen se compensa a nivel de red. Con la decisi\u00f3n de\ndespliegue in-cluster (2026-08-31) la forma del compensatorio es el\nAISLAMIENTO DE RED DEL SERVICE: Universal Auth s\u00f3lo es alcanzable por el\nservice interno <code>vault.vault.svc.cluster.local</code>; la ruta p\u00fablica\n<code>vault.wetopi.net</code> sirve exclusivamente la UI humana con DENY expl\u00edcito\nde los endpoints de API (infra #27), y los StorageClasses PVC no exponen\nnada a Internet.</p>\n<blockquote>\n<p><strong>NOTA (2026-08-31) \u2014 RESUELTO por la decisi\u00f3n del operador:</strong> la\nexposici\u00f3n de Universal Auth qued\u00f3 decidida como <strong>private-network-only\nin-cluster</strong> ( Universal Auth s\u00f3lo por <code>vault.vault.svc.cluster.local</code>,\nruta p\u00fablica = s\u00f3lo UI humana con DENY de API; chat del operador\n2026-08-31 e infra #27). Este era el OPEN \"a resolverse en S0 antes del\nprimer identity bootstrap\"; la decisi\u00f3n de arriba y el enforcement de\ninfra #27 lo cierran. La eficacia del control (login p\u00fablico rechazado /\nlogin interno autorizado) se demuestra antes del primer bootstrap.</p>\n</blockquote>\n</li>\n</ol>\n<p id=\"bkmrk-consecuencia-normati\">Consecuencia normativa para S1: la etapa exige firma conjunta\napprover+operador que fije versi\u00f3n/edici\u00f3n/componentes y acepte\nexpl\u00edcitamente licencia/coste (licencia base + audit EE, seg\u00fan las\ndecisiones de arriba). No se puede degradar AC7 para usar una edici\u00f3n\ngratuita. Si el condicional audit no queda cubierto por una licencia\naceptada, el resultado es NO-GO y se detiene antes de S3.</p>\n<h2 id=\"bkmrk-open-expl%C3%ADcitos\">OPEN expl\u00edcitos</h2>\n<ul id=\"bkmrk-requisitos-exactos-d\">\n<li>Requisitos exactos de edici\u00f3n/licencia, audit log, Universal Auth, CRUD\nAPI, datastore, export y operator: resueltos por el veredicto \u00a710.6 de\n<code>t_7459a827</code> (arriba); pendiente la aceptaci\u00f3n expl\u00edcita de licencia/coste\npor el operador (gate S1).</li>\n<li>Inventario exacto de perfiles Hermes activos y scopes; se deriva del\nregistro can\u00f3nico, no de esta conversaci\u00f3n.</li>\n<li>Decisi\u00f3n operador: activar Basic Auth adicional en UI s\u00f3lo despu\u00e9s de\nprueba de no interferencia y valorar hostname/ruta administrativa\nseparada.</li>\n<li>Retenci\u00f3n 365 d\u00edas y periodicidad de restore drill: confirmar\ncoste/operaci\u00f3n en S1/S2.</li>\n</ul>\n<h2 id=\"bkmrk-related-documents\">Related documents</h2>\n<ul id=\"bkmrk-obj-vault-001-%E2%80%94-el-o\">\n<li><a href=\"https://book.wetopi.net/objectives/active/OBJ-VAULT-001.md\">OBJ-VAULT-001</a> \u2014 el Objective con\nACs, dashboard S0-S10, gates y promotion matrix.</li>\n<li>ADR-0003 \u2014 el patr\u00f3n de fuentes de verdad separadas y expl\u00edcitas por\nciclo de vida (compliance registrado en Consecuencias).</li>\n</ul>\n"
 },
 {
  "id": 53,
  "title": "ADR 0014 edge source ip allowlist",
  "slug": "adr-0014-edge-source-ip-allowlist",
  "book_id": 8,
  "chapter_id": 28,
  "description": null,
  "created_at": "2026-09-06T13:39:00.000000Z",
  "updated_at": "2026-09-06T13:39:00.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (decisions/ADR-0014-edge-source-ip-allowlist.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n ADR-0014: Edge source-IP allow-list (capa VPN del operador)\n\n## Status\n\n**ACCEPTED** (2026-09-01, directriz del operador durante la operaci\u00f3n;\nimplementado y verificado en vivo el mismo d\u00eda \u2014 infra PR #36, merge\nb369b0a).\n\nAmpl\u00eda ADR-0009 (re-cierre de su decisi\u00f3n 2 por otra v\u00eda de\nenforcement) y la postura de ADR-0006 (exposici\u00f3n de servicios por el\nedge WAN): el edge pasa de \"auth HTTP\" a \"filtro de red por origen\".\n\n## Context\n\nLa decisi\u00f3n 2 de ADR-0009 prohib\u00eda que el API de machine identities\n(Universal Auth) fuera alcanzable por `vault.wetopi.net`, con\nenforcement por path-DENY en el edge (infra#27). El filtro result\u00f3\ndemasiado ancho: bloqueaba tambi\u00e9n POSTs same-origin de la propia UI\nhumana (`/api/v1/auth/universal-auth/identities/{id}`), rompiendo la\nconfiguraci\u00f3n interactiva de identidades. El deny se retir\u00f3 (infra#35,\n2026-09-01) y el API completo qued\u00f3 expuesto detr\u00e1s de la \u00fanica auth\nde Infisical, dejando la Ley 1 de ADR-0009 en deuda abierta con un\ncompensating control prometido: \"el filtrado de m\u00e1quinas pasa a la\ncapa VPN/red\".\n\nEn paralelo, el operador dictamin\u00f3 retirar el HTTP Basic Auth del edge\n(\"quitamos los authbasic\"): la autenticaci\u00f3n por htpasswd duplicaba la\nauth nativa de las apps (Argo login, Infisical login+TOTP), no expresa\nautorizaci\u00f3n por secreto (alternativa ya descartada en ADR-0009) y su\ngesti\u00f3n de credenciales (Secrets operator-side con rotaci\u00f3n manual) es\ndeuda operativa sin valor a\u00f1adido si existe un control de red previo.\n\nLos hostnames de control-plane del edge (`argo.wetopi.net`,\n`vault.wetopi.net`, `docs.wetopi.net`) son de uso exclusivo del\noperador; no hay raz\u00f3n para que internet pueda ni siquiera intentar\nlogin contra ellos.\n\n## Decision\n\n1. **Todo hostname de control-plane publicado por el edge se filtra por\n   IP de origen**: Middleware Traefik `ipAllowList`\n   (`edge-ip-allowlist`, una instancia por namespace de ruta:\n   kube-system, vault, docs-site) con `sourceRange: 51.254.106.87/32`\n   \u2014 el egress de la VPN del operador. Cualquier otro origen recibe\n   **403 Forbidden en el edge**, antes de alcanzar backend, auth de\n   aplicaci\u00f3n o API.\n2. **El HTTP Basic Auth se retira de las rutas del edge** (directriz\n   del operador, 2026-09-01): la auth de aplicaci\u00f3n (login de Argo CD,\n   login+TOTP de Infisical) queda como \u00fanica capa de autenticaci\u00f3n,\n   sobre el control de red. El middleware/Secret de docs-site\n   (`docs-basic-auth`) se conserva temporalmente SOLO porque el\n   runbook de rollback del cutover docs-runtime (CUTOVER.md) restaura\n   la ruta pre-incidente desde git history con ese filtro; se elimina\n   en el paso de cutover.\n3. **Excepci\u00f3n expl\u00edcita: el webhook de rebuild de docs**\n   (`docs.wetopi.net/hooks/rebuild`, ruta `docs-runtime-webhook`) NO\n   lleva filtro IP: su cliente es GitHub (IPs no controlables) y su\n   auth es la firma HMAC-SHA256 (ADR-0011) validada timing-safe por el\n   receiver. Un endpoint de m\u00e1quina autenticado por criptograf\u00eda no\n   necesita ni debe heredar el gate de origen humano.\n4. **El CIDR autorizado vive en 4 ficheros** (`middleware-ipallowlist.yaml`\n   \u00d73 + el valor en el runbook del apply): cuando exista el pool VPN\n   definitivo se extiende `sourceRange` con su CIDR (un cambio\n   reviewed por Git). Hasta entonces, 51.254.106.87/32 es el egress VPN\n   vigente del operador (NO es la WAN del basti\u00f3n ops01, que es\n   92.222.245.91).\n\n## Rationale\n\n- **La Ley 1 se re-implementa donde de verdad vive**: no importan los\n  paths que abra la UI (el fallo de infra#27/#35 desaparece \u2014 el API\n  completo puede quedar abierto sin riesgo), porque las m\u00e1quinas no\n  llegan: su IP de origen no est\u00e1 en la lista. Elimina la fragilidad\n  estructural del path-DENY (cada endpoint nuevo de la UI puede romper\n  la regla) por un invariante de red.\n- **Fail-closed garantizado por dise\u00f1o**: Traefik `ipAllowList`\n  rechaza con 403 lo no listado; si el middleware no resuelve, la ruta\n  falla cerrada (500), nunca abierta.\n- **Menos superficie y menos deuda**: sin htpasswd que rotar ni\n  Secrets de credenciales fuera del vault; la identidad del operador\n  en el edge es su egress VPN, rotable en un sitio.\n- **Coste**: el control depende de la estabilidad del egress VPN. Si\n  el egress rota, el operador pierde acceso (403) hasta actualizar la\n  lista \u2014 riesgo aceptado y documentado; la reversibilidad es un\n  `git revert` (Argo selfHeal aplica el rollback).\n\n## Consequences\n\n- Positivas: Ley 1 de ADR-0009 enforcement de nuevo verificable\n  (403 en `/api/v1/auth/universal-auth/login` desde internet); el edge\n  queda con una postura uniforme \"control de red + auth nativa de la\n  app\"; un solo mecanismo para argo/vault/docs.\n- Negativas / deuda registrada: acceso humano desde una IP fuera de la\n  lista = 403 (si el operador navega fuera de su VPN, no entra \u2014\n  extender sourceRange o conectarse por la VPN); el webhook de docs\n  queda expuesto a internet con HMAC como \u00fanica defensa (flood/DoS\n  mitigable con rateLimit futuro si se necesita); el `docs-basic-auth`\n  legacy persiste hasta el cutover (objeto inerte pero vivo).\n- Operativas: a\u00f1adir una fuente autorizada = 1 l\u00ednea en los 4\n  `middleware-ipallowlist.yaml` (PR revisado, Argo sincroniza). Las\n  apps include-only exigen que cualquier fichero nuevo est\u00e9 en la\n  include list de su Application (lecci\u00f3n del caso PR#36, en\n  wetopi-gitops-ops).\n\n## Alternatives considered\n\n| Alternativa | Por qu\u00e9 se descart\u00f3 |\n|---|---|\n| Restaurar el path-DENY en vault (Ley 1 por paths) | Ya demostr\u00f3 ser fr\u00e1gil: rompi\u00f3 la UI misma (infra#27\u2192#35); cada endpoint nuevo del API reabre la herida |\n| Mantener basic-auth como capa extra | Duplicaba la auth nativa, credenciales fuera del vault, sin valor tras el gate IP (directriz expl\u00edcita del operador) |\n| WireGuard/VPN propia ya (Ley 1 en L3) | Es el objetivo final pero requiere dise\u00f1ar/desplegar la VPN; el ipAllowList entrega HOY el mismo enforcement con el egress existente del operador |\n| Cloudflare Access / SSO en el edge | A\u00f1ade dependencia externa y cambia el modelo de acceso; no requerido por el operador |\n\n## Migration / implementation notes\n\n- Implementado en infra PR #36 (branch `feat/edge-ip-allowlist`,\n  merge b369b0a, 2026-09-01), revisi\u00f3n en 2 rondas (r1 REQUEST_CHANGES\n  por include lists de las apps include-only, corregido; r2 APPROVE).\n- Apply-time ejecutado: middleware creado en 3 namespaces (la app\n  `argo-edge` no est\u00e1 anclada a la root app \u2014 su Application se\n  re-aplic\u00f3 por el patr\u00f3n bootstrap documentado), ruta legacy de docs\n  parcheada out-of-band (objeto restore deliberadamente no gestionado\n  por Argo; re-own por Git la resucitar\u00eda tras el cutover \u2014 invariante\n  RC4), Secret `argo-basic-auth` borrado.\n- Verificaci\u00f3n p\u00fablica (2026-09-01): 403 en argo/vault/docs desde\n  egress no autorizado (92.222.245.x); 200/302 esperado desde la VPN\n  del operador; webhook sin abrir hasta el cutover docs-runtime.\n- Pendiente: extender `sourceRange` cuando exista el pool VPN\n  (ampliaci\u00f3n trivial); el webhook pasar\u00e1 a estar routable con el flip\n  de docs-runtime (card t_ffe4d39b, secrets de webhook pendientes del\n  operador).\n\n## Related documents\n\n- ADR-0009 (vault del harness): su decisi\u00f3n 2 queda **amendida** \u2014 la\n  Ley 1 se enforcementa por IP de origen (esta ADR) en lugar de\n  path-DENY; \"Edge Basic Auth como control \u00fanico\" sigue descartado.\n- ADR-0006 (edge v1 WAN): postura de exposici\u00f3n de servicios por el\n  edge, ahora con control de origen para control-plane.\n- Infra PRs: #27 (deny original), #35 (apertura del API), #36 (este\n  gate). Runbooks: kubernetes/edge-argo/README.md,\n  kubernetes/docs-runtime/CUTOVER.md.",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (decisions/ADR-0014-edge-source-ip-allowlist.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-adr-0014%3A-edge-sourc\">ADR-0014: Edge source-IP allow-list (capa VPN del operador)</p>\n<h2 id=\"bkmrk-status\">Status</h2>\n<p id=\"bkmrk-accepted-%282026-09-01\"><strong>ACCEPTED</strong> (2026-09-01, directriz del operador durante la operaci\u00f3n;\nimplementado y verificado en vivo el mismo d\u00eda \u2014 infra PR #36, merge\nb369b0a).</p>\n<p id=\"bkmrk-ampl%C3%ADa-adr-0009-%28re-\">Ampl\u00eda ADR-0009 (re-cierre de su decisi\u00f3n 2 por otra v\u00eda de\nenforcement) y la postura de ADR-0006 (exposici\u00f3n de servicios por el\nedge WAN): el edge pasa de \"auth HTTP\" a \"filtro de red por origen\".</p>\n<h2 id=\"bkmrk-context\">Context</h2>\n<p id=\"bkmrk-la-decisi%C3%B3n-2-de-adr\">La decisi\u00f3n 2 de ADR-0009 prohib\u00eda que el API de machine identities\n(Universal Auth) fuera alcanzable por <code>vault.wetopi.net</code>, con\nenforcement por path-DENY en el edge (infra#27). El filtro result\u00f3\ndemasiado ancho: bloqueaba tambi\u00e9n POSTs same-origin de la propia UI\nhumana (<code>/api/v1/auth/universal-auth/identities/{id}</code>), rompiendo la\nconfiguraci\u00f3n interactiva de identidades. El deny se retir\u00f3 (infra#35,\n2026-09-01) y el API completo qued\u00f3 expuesto detr\u00e1s de la \u00fanica auth\nde Infisical, dejando la Ley 1 de ADR-0009 en deuda abierta con un\ncompensating control prometido: \"el filtrado de m\u00e1quinas pasa a la\ncapa VPN/red\".</p>\n<p id=\"bkmrk-en-paralelo%2C-el-oper\">En paralelo, el operador dictamin\u00f3 retirar el HTTP Basic Auth del edge\n(\"quitamos los authbasic\"): la autenticaci\u00f3n por htpasswd duplicaba la\nauth nativa de las apps (Argo login, Infisical login+TOTP), no expresa\nautorizaci\u00f3n por secreto (alternativa ya descartada en ADR-0009) y su\ngesti\u00f3n de credenciales (Secrets operator-side con rotaci\u00f3n manual) es\ndeuda operativa sin valor a\u00f1adido si existe un control de red previo.</p>\n<p id=\"bkmrk-los-hostnames-de-con\">Los hostnames de control-plane del edge (<code>argo.wetopi.net</code>,\n<code>vault.wetopi.net</code>, <code>docs.wetopi.net</code>) son de uso exclusivo del\noperador; no hay raz\u00f3n para que internet pueda ni siquiera intentar\nlogin contra ellos.</p>\n<h2 id=\"bkmrk-decision\">Decision</h2>\n<ol id=\"bkmrk-todo-hostname-de-con\">\n<li><strong>Todo hostname de control-plane publicado por el edge se filtra por\nIP de origen</strong>: Middleware Traefik <code>ipAllowList</code>\n(<code>edge-ip-allowlist</code>, una instancia por namespace de ruta:\nkube-system, vault, docs-site) con <code>sourceRange: 51.254.106.87/32</code>\n\u2014 el egress de la VPN del operador. Cualquier otro origen recibe\n<strong>403 Forbidden en el edge</strong>, antes de alcanzar backend, auth de\naplicaci\u00f3n o API.</li>\n<li><strong>El HTTP Basic Auth se retira de las rutas del edge</strong> (directriz\ndel operador, 2026-09-01): la auth de aplicaci\u00f3n (login de Argo CD,\nlogin+TOTP de Infisical) queda como \u00fanica capa de autenticaci\u00f3n,\nsobre el control de red. El middleware/Secret de docs-site\n(<code>docs-basic-auth</code>) se conserva temporalmente SOLO porque el\nrunbook de rollback del cutover docs-runtime (CUTOVER.md) restaura\nla ruta pre-incidente desde git history con ese filtro; se elimina\nen el paso de cutover.</li>\n<li><strong>Excepci\u00f3n expl\u00edcita: el webhook de rebuild de docs</strong>\n(<code>docs.wetopi.net/hooks/rebuild</code>, ruta <code>docs-runtime-webhook</code>) NO\nlleva filtro IP: su cliente es GitHub (IPs no controlables) y su\nauth es la firma HMAC-SHA256 (ADR-0011) validada timing-safe por el\nreceiver. Un endpoint de m\u00e1quina autenticado por criptograf\u00eda no\nnecesita ni debe heredar el gate de origen humano.</li>\n<li><strong>El CIDR autorizado vive en 4 ficheros</strong> (<code>middleware-ipallowlist.yaml</code>\n\u00d73 + el valor en el runbook del apply): cuando exista el pool VPN\ndefinitivo se extiende <code>sourceRange</code> con su CIDR (un cambio\nreviewed por Git). Hasta entonces, 51.254.106.87/32 es el egress VPN\nvigente del operador (NO es la WAN del basti\u00f3n ops01, que es\n92.222.245.91).</li>\n</ol>\n<h2 id=\"bkmrk-rationale\">Rationale</h2>\n<ul id=\"bkmrk-la-ley-1-se-re-imple\">\n<li><strong>La Ley 1 se re-implementa donde de verdad vive</strong>: no importan los\npaths que abra la UI (el fallo de infra#27/#35 desaparece \u2014 el API\ncompleto puede quedar abierto sin riesgo), porque las m\u00e1quinas no\nllegan: su IP de origen no est\u00e1 en la lista. Elimina la fragilidad\nestructural del path-DENY (cada endpoint nuevo de la UI puede romper\nla regla) por un invariante de red.</li>\n<li><strong>Fail-closed garantizado por dise\u00f1o</strong>: Traefik <code>ipAllowList</code>\nrechaza con 403 lo no listado; si el middleware no resuelve, la ruta\nfalla cerrada (500), nunca abierta.</li>\n<li><strong>Menos superficie y menos deuda</strong>: sin htpasswd que rotar ni\nSecrets de credenciales fuera del vault; la identidad del operador\nen el edge es su egress VPN, rotable en un sitio.</li>\n<li><strong>Coste</strong>: el control depende de la estabilidad del egress VPN. Si\nel egress rota, el operador pierde acceso (403) hasta actualizar la\nlista \u2014 riesgo aceptado y documentado; la reversibilidad es un\n<code>git revert</code> (Argo selfHeal aplica el rollback).</li>\n</ul>\n<h2 id=\"bkmrk-consequences\">Consequences</h2>\n<ul id=\"bkmrk-positivas%3A-ley-1-de-\">\n<li>Positivas: Ley 1 de ADR-0009 enforcement de nuevo verificable\n(403 en <code>/api/v1/auth/universal-auth/login</code> desde internet); el edge\nqueda con una postura uniforme \"control de red + auth nativa de la\napp\"; un solo mecanismo para argo/vault/docs.</li>\n<li>Negativas / deuda registrada: acceso humano desde una IP fuera de la\nlista = 403 (si el operador navega fuera de su VPN, no entra \u2014\nextender sourceRange o conectarse por la VPN); el webhook de docs\nqueda expuesto a internet con HMAC como \u00fanica defensa (flood/DoS\nmitigable con rateLimit futuro si se necesita); el <code>docs-basic-auth</code>\nlegacy persiste hasta el cutover (objeto inerte pero vivo).</li>\n<li>Operativas: a\u00f1adir una fuente autorizada = 1 l\u00ednea en los 4\n<code>middleware-ipallowlist.yaml</code> (PR revisado, Argo sincroniza). Las\napps include-only exigen que cualquier fichero nuevo est\u00e9 en la\ninclude list de su Application (lecci\u00f3n del caso PR#36, en\nwetopi-gitops-ops).</li>\n</ul>\n<h2 id=\"bkmrk-alternatives-conside\">Alternatives considered</h2>\n<table id=\"bkmrk-alternativa-por-qu%C3%A9-\">\n<thead>\n<tr>\n<th>Alternativa</th>\n<th>Por qu\u00e9 se descart\u00f3</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Restaurar el path-DENY en vault (Ley 1 por paths)</td>\n<td>Ya demostr\u00f3 ser fr\u00e1gil: rompi\u00f3 la UI misma (infra#27\u2192#35); cada endpoint nuevo del API reabre la herida</td>\n</tr>\n<tr>\n<td>Mantener basic-auth como capa extra</td>\n<td>Duplicaba la auth nativa, credenciales fuera del vault, sin valor tras el gate IP (directriz expl\u00edcita del operador)</td>\n</tr>\n<tr>\n<td>WireGuard/VPN propia ya (Ley 1 en L3)</td>\n<td>Es el objetivo final pero requiere dise\u00f1ar/desplegar la VPN; el ipAllowList entrega HOY el mismo enforcement con el egress existente del operador</td>\n</tr>\n<tr>\n<td>Cloudflare Access / SSO en el edge</td>\n<td>A\u00f1ade dependencia externa y cambia el modelo de acceso; no requerido por el operador</td>\n</tr>\n</tbody>\n</table>\n<h2 id=\"bkmrk-migration-%2F-implemen\">Migration / implementation notes</h2>\n<ul id=\"bkmrk-implementado-en-infr\">\n<li>Implementado en infra PR #36 (branch <code>feat/edge-ip-allowlist</code>,\nmerge b369b0a, 2026-09-01), revisi\u00f3n en 2 rondas (r1 REQUEST_CHANGES\npor include lists de las apps include-only, corregido; r2 APPROVE).</li>\n<li>Apply-time ejecutado: middleware creado en 3 namespaces (la app\n<code>argo-edge</code> no est\u00e1 anclada a la root app \u2014 su Application se\nre-aplic\u00f3 por el patr\u00f3n bootstrap documentado), ruta legacy de docs\nparcheada out-of-band (objeto restore deliberadamente no gestionado\npor Argo; re-own por Git la resucitar\u00eda tras el cutover \u2014 invariante\nRC4), Secret <code>argo-basic-auth</code> borrado.</li>\n<li>Verificaci\u00f3n p\u00fablica (2026-09-01): 403 en argo/vault/docs desde\negress no autorizado (92.222.245.x); 200/302 esperado desde la VPN\ndel operador; webhook sin abrir hasta el cutover docs-runtime.</li>\n<li>Pendiente: extender <code>sourceRange</code> cuando exista el pool VPN\n(ampliaci\u00f3n trivial); el webhook pasar\u00e1 a estar routable con el flip\nde docs-runtime (card t_ffe4d39b, secrets de webhook pendientes del\noperador).</li>\n</ul>\n<h2 id=\"bkmrk-related-documents\">Related documents</h2>\n<ul id=\"bkmrk-adr-0009-%28vault-del-\">\n<li>ADR-0009 (vault del harness): su decisi\u00f3n 2 queda <strong>amendida</strong> \u2014 la\nLey 1 se enforcementa por IP de origen (esta ADR) en lugar de\npath-DENY; \"Edge Basic Auth como control \u00fanico\" sigue descartado.</li>\n<li>ADR-0006 (edge v1 WAN): postura de exposici\u00f3n de servicios por el\nedge, ahora con control de origen para control-plane.</li>\n<li>Infra PRs: #27 (deny original), #35 (apertura del API), #36 (este\ngate). Runbooks: kubernetes/edge-argo/README.md,\nkubernetes/docs-runtime/CUTOVER.md.</li>\n</ul>\n"
 },
 {
  "id": 54,
  "title": "ai first harness plan",
  "slug": "ai-first-harness-plan",
  "book_id": 8,
  "chapter_id": 29,
  "description": null,
  "created_at": "2026-09-06T13:39:01.000000Z",
  "updated_at": "2026-09-06T13:39:01.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (design/ai-first-harness-plan.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n# Wetopi AI-First Kubernetes Rebuild\n## Memoria t\u00e9cnica y ejecutiva del proyecto\n\n**Fecha:** 28 de agosto de 2026  \n**Estado:** Dise\u00f1o inicial consolidado  \n**\u00c1mbito:** Reconstrucci\u00f3n de Wetopi sobre Kubernetes con una arquitectura AI-first y una flota interna de sistemas ag\u00e9nticos.\n\n---\n\n# \u00cdndice\n\n1. [Resumen ejecutivo](#1-resumen-ejecutivo)\n2. [Objetivo del proyecto](#2-objetivo-del-proyecto)\n3. [Principios de dise\u00f1o](#3-principios-de-dise\u00f1o)\n4. [Arquitectura general](#4-arquitectura-general)\n5. [Actores del sistema](#5-actores-del-sistema)\n6. [Servicios ag\u00e9nticos prioritarios](#6-servicios-ag\u00e9nticos-prioritarios)\n7. [Work Coordination Plane: Kanban, tickets y coordinaci\u00f3n](#7-work-coordination-plane-kanban-tickets-y-coordinaci\u00f3n)\n8. [Memory Plane: memoria compartida de Wetopi](#8-memory-plane-memoria-compartida-de-wetopi)\n9. [Memoria local de Hermes + memoria factual global](#9-memoria-local-de-hermes--memoria-factual-global)\n10. [Integraci\u00f3n de Hermes con la memoria de Wetopi](#10-integraci\u00f3n-de-hermes-con-la-memoria-de-wetopi)\n11. [Secrets Plane: Vault central con OpenBao](#11-secrets-plane-vault-central-con-openbao)\n12. [Documentation Plane: Markdown + Git + MkDocs Material](#12-documentation-plane-markdown--git--mkdocs-material)\n13. [Estrategia de repositorios](#13-estrategia-de-repositorios)\n14. [wetopi-ai-hub: repositorio com\u00fan ag\u00e9ntico](#14-wetopi-ai-hub-repositorio-com\u00fan-ag\u00e9ntico)\n15. [Repositorios de documentaci\u00f3n](#15-repositorios-de-documentaci\u00f3n)\n16. [Modelo de distribuci\u00f3n de tools y skills](#16-modelo-de-distribuci\u00f3n-de-tools-y-skills)\n17. [Relaci\u00f3n entre memoria, documentaci\u00f3n y Work Ledger](#17-relaci\u00f3n-entre-memoria-documentaci\u00f3n-y-work-ledger)\n18. [Modelo de identidad y permisos](#18-modelo-de-identidad-y-permisos)\n19. [Modelo de scopes y recursos](#19-modelo-de-scopes-y-recursos)\n20. [Observabilidad y auditor\u00eda](#20-observabilidad-y-auditor\u00eda)\n21. [Flujos operativos de ejemplo](#21-flujos-operativos-de-ejemplo)\n22. [Topolog\u00eda inicial de Kubernetes](#22-topolog\u00eda-inicial-de-kubernetes)\n23. [Orden recomendado de despliegue](#23-orden-recomendado-de-despliegue)\n24. [Decisiones consolidadas](#24-decisiones-consolidadas)\n25. [Riesgos y controles](#25-riesgos-y-controles)\n26. [Cuestiones abiertas](#26-cuestiones-abiertas)\n27. [Resultado esperado](#27-resultado-esperado)\n\n---\n\n# 1. Resumen ejecutivo\n\nWetopi se va a reconstruir sobre Kubernetes con un enfoque **AI-first desde el primer d\u00eda**.\n\nLa intenci\u00f3n no es construir una plataforma tradicional y a\u00f1adir agentes despu\u00e9s. La nueva Wetopi debe asumir desde su base que:\n\n- habr\u00e1 agentes aut\u00f3nomos operando de forma continua;\n- habr\u00e1 varios Hermes actuando como asistentes personales de empleados y como agentes de coding;\n- habr\u00e1 agentes Google ADK standalone, especialistas, observers y managers;\n- humanos y agentes compartir\u00e1n conocimiento, tareas, documentaci\u00f3n y servicios;\n- los agentes deber\u00e1n coordinarse antes de modificar recursos compartidos;\n- las acciones relevantes deber\u00e1n quedar registradas;\n- los secrets deber\u00e1n estar fuera del contexto LLM siempre que sea posible;\n- la documentaci\u00f3n debe ser directamente editable por agentes;\n- la memoria factual de la organizaci\u00f3n debe ser compartida entre runtimes;\n- las memorias personales de cada Hermes deber\u00e1n mantenerse separadas.\n\nLa **primera capa que se desplegar\u00e1** en la nueva infraestructura ser\u00e1 precisamente la infraestructura ag\u00e9ntica com\u00fan.\n\nLos componentes iniciales ser\u00e1n:\n\n1. **Agent Runtime Layer**\n   - Hermes por empleado.\n   - Agentes ADK de flota.\n\n2. **Memory Plane**\n   - Memoria institucional com\u00fan de Wetopi.\n   - Memoria factual, procedural, operacional, decisiones y episodios relevantes.\n\n3. **Work Coordination Plane**\n   - Kanban/ticketing como interfaz humana.\n   - Work Ledger como mecanismo real de coordinaci\u00f3n entre agentes.\n\n4. **Secrets Plane**\n   - OpenBao como vault central.\n\n5. **Documentation Plane**\n   - Markdown puro versionado en Git.\n   - MkDocs Material como interfaz web.\n\n6. **AI Hub**\n   - Repositorio com\u00fan para skills, tools, librer\u00edas, componentes y contratos reutilizables por todos los agentes.\n\nEl objetivo es que estos servicios formen la **capa base de operaci\u00f3n inteligente de Wetopi** antes de reconstruir encima el resto de servicios de negocio y hosting.\n\n---\n\n# 2. Objetivo del proyecto\n\nConstruir una nueva plataforma interna de Wetopi sobre Kubernetes donde los agentes sean **ciudadanos de primera clase**.\n\nLa plataforma debe permitir que cualquier agente:\n\n- descubra conocimiento antes de actuar;\n- conozca qu\u00e9 tareas est\u00e1n en curso;\n- detecte conflictos con otros agentes;\n- reclame trabajo;\n- adquiera leases sobre recursos;\n- declare su intenci\u00f3n;\n- deje trazabilidad;\n- utilice secrets sin exponerlos innecesariamente al modelo;\n- consulte documentaci\u00f3n en Markdown;\n- contribuya conocimiento factual a la organizaci\u00f3n;\n- comparta infraestructura com\u00fan independientemente de si corre sobre Hermes, ADK u otro runtime futuro.\n\n---\n\n# 3. Principios de dise\u00f1o\n\n## 3.1 AI-first\n\nCada servicio deber\u00e1 responder afirmativamente a esta pregunta:\n\n> \u00bfPuede un agente consumir este sistema de forma program\u00e1tica, segura y auditable sin depender de una interfaz humana?\n\nSe priorizar\u00e1n:\n\n- APIs;\n- Git;\n- Markdown;\n- identidad de workload;\n- webhooks y eventos;\n- formatos estructurados;\n- pol\u00edticas;\n- trazabilidad;\n- contratos estables.\n\n---\n\n## 3.2 Human-compatible, agent-native\n\nLos humanos seguir\u00e1n teniendo UI:\n\n- Kanban;\n- documentaci\u00f3n web;\n- Web UI de secretos;\n- dashboards;\n- revisi\u00f3n de tareas.\n\nPero la UI nunca ser\u00e1 el \u00fanico canal de acceso.\n\n---\n\n## 3.3 Source of truth expl\u00edcitos\n\n| Dominio | Source of truth |\n|---|---|\n| C\u00f3digo | Git |\n| Skills / tools / componentes AI | `wetopi-ai-hub` |\n| Documentaci\u00f3n | Markdown en Git |\n| Trabajo y coordinaci\u00f3n | Work Ledger |\n| Conocimiento factual | Memory Plane |\n| Secrets | OpenBao |\n| Estado real de infraestructura | APIs del sistema correspondiente |\n| Preferencias personales | Memoria local de Hermes |\n\n---\n\n## 3.4 No convertir la memoria en una copia del mundo\n\nLa memoria guarda conocimiento \u00fatil, no sustituye a los sistemas autoritativos.\n\nEjemplo:\n\n```text\nKubernetes replicas = source of truth real\n\nFleet Memory =\n\"este servicio suele correr con 5 replicas\"\n+\nhist\u00f3rico\n+\ndecisiones\n+\nprocedimientos\n```\n\nSi hay contradicci\u00f3n, gana la fuente viva autoritativa.\n\n---\n\n## 3.5 Least privilege\n\nCada agente tendr\u00e1 \u00fanicamente:\n\n- tools necesarias;\n- scopes de memoria necesarios;\n- secrets necesarios;\n- permisos de Work Ledger necesarios;\n- recursos modificables necesarios.\n\n---\n\n## 3.6 Trazabilidad antes que autonom\u00eda ciega\n\nLas acciones importantes deben poder responder:\n\n```text\n\u00bfQu\u00e9 ocurri\u00f3?\n\u00bfQui\u00e9n lo hizo?\n\u00bfPor qu\u00e9?\n\u00bfSobre qu\u00e9 recurso?\n\u00bfCon qu\u00e9 ticket?\n\u00bfCon qu\u00e9 tool?\n\u00bfCu\u00e1ndo?\n\u00bfCu\u00e1l fue el resultado?\n```\n\n---\n\n## 3.7 Runtime-agnostic\n\nLos servicios compartidos no deben dise\u00f1arse exclusivamente para ADK o Hermes.\n\nSe debe poder a\u00f1adir en el futuro:\n\n```text\nnuevo runtime\nnuevo coding agent\nnuevo autonomous agent\n```\n\nsin cambiar el dise\u00f1o institucional.\n\n---\n\n# 4. Arquitectura general\n\n```text\n                               HUMANS\n                                  \u2502\n                    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                    \u2502             \u2502             \u2502\n                    \u25bc             \u25bc             \u25bc\n                 Hermes       Web UIs       Git / CLI\n                    \u2502\n                    \u2502\n       \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n       \u2502            AGENTIC SERVICES LAYER              \u2502\n       \u2502                                                \u2502\n       \u2502  Memory     Work Ledger    Secrets     Docs    \u2502\n       \u2502    \u2502             \u2502            \u2502          \u2502     \u2502\n       \u2502    \u2502             \u2502            \u2502          \u2502     \u2502\n       \u2502    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 AI HUB / contracts \u2500\u2500\u2500\u2500\u2500\u2518     \u2502\n       \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                    \u2502\n              \u250c\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n              \u2502                                   \u2502\n              \u25bc                                   \u25bc\n          ADK Fleet                            Hermes Fleet\n              \u2502                                   \u2502\n              \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                               \u2502\n                               \u25bc\n                          Kubernetes\n                               \u2502\n                 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                 \u25bc             \u25bc             \u25bc\n               Infra        Hosting       Business\n```\n\n---\n\n# 5. Actores del sistema\n\nModelo com\u00fan:\n\n```text\nActor\n\u251c\u2500\u2500 Human\n\u251c\u2500\u2500 Hermes\n\u251c\u2500\u2500 ADK Agent\n\u2514\u2500\u2500 Future Agent Runtime\n```\n\nCada actor debe tener como m\u00ednimo:\n\n```yaml\nactor_id:\nactor_type:\nruntime:\ncrew:\nrole:\nuser_id:\nenvironment:\n```\n\nEjemplos:\n\n```yaml\nactor_id: hermes-developer\nactor_type: hermes\nruntime: hermes\ncrew: development\n```\n\n```yaml\nactor_id: platform-observability-manager\nactor_type: agent\nruntime: adk\ncrew: platform\nrole: manager\n```\n\n---\n\n# 6. Servicios ag\u00e9nticos prioritarios\n\nLos primeros servicios funcionales que deben existir son:\n\n```text\n1. Kubernetes base\n2. Identity / workload identity\n3. OpenBao\n4. Memory Plane\n5. Work Coordination Plane\n6. Documentation Plane\n7. wetopi-ai-hub\n8. Hermes\n9. ADK Fleet\n```\n\nTodos los dem\u00e1s servicios internos podr\u00e1n despu\u00e9s apoyarse en esta base.\n\n---\n\n# 7. Work Coordination Plane: Kanban, tickets y coordinaci\u00f3n\n\n## 7.1 El Kanban no es el sistema\n\nLa interfaz Kanban es para humanos.\n\nEl sistema real es un:\n\n> **Shared Work Ledger / Work Coordination Plane**\n\nSu misi\u00f3n es evitar que Hermes, ADK y humanos ejecuten trabajos contradictorios.\n\n---\n\n## 7.2 Regla principal\n\n> Ning\u00fan agente modifica un recurso compartido sin consultar primero el Work Ledger cuando la acci\u00f3n pueda interferir con otro actor, requiera seguimiento o tenga impacto operativo.\n\nNo toda lectura genera ticket.\n\n---\n\n## 7.3 Estados\n\n```text\nBACKLOG\nREADY\nCLAIMED\nIN_PROGRESS\nBLOCKED\nREVIEW\nDONE\nCANCELLED\n```\n\n---\n\n## 7.4 Ticket m\u00ednimo\n\n```yaml\nid: OPS-381\n\ntitle: Upgrade ingress\n\nowner: hermes-sre\n\nstatus: IN_PROGRESS\n\nscope:\n  - k8s:cluster:k3s01\n  - k8s:deployment:ingress\n\nrisk: medium\n\nlease_until: ...\n\nactivity:\n  - inspected current deployment\n  - updating configuration\n```\n\n---\n\n## 7.5 Resource scopes\n\nEjemplos:\n\n```text\ngithub:repo:wetopi-api\nk8s:cluster:k3s01\nk8s:namespace:agents\nk8s:deployment:support-manager\nceph:cluster:wetopi\nwordpress:site:12345\ndns:zone:wetopi.com\nmarketing:campaign:summer\n```\n\n---\n\n## 7.6 Lock modes\n\n```text\nshared\nexclusive\n```\n\n---\n\n## 7.7 Leases\n\nLos locks deben tener TTL.\n\n```text\nclaim\n \u2193\nlease\n \u2193\nheartbeat\n \u2193\nrenew\n```\n\nSi el agente desaparece, el recurso vuelve a quedar disponible.\n\n---\n\n## 7.8 Intent\n\nAntes de ejecutar cambios relevantes:\n\n```yaml\nintent:\n  action: restart\n  target: support-manager\n  reason: memory leak\n  expected_effect: rolling restart\n```\n\n---\n\n## 7.9 Activity journal\n\nNo guardar chain-of-thought.\n\nGuardar hechos operativos:\n\n```text\n11:02 claimed OPS-381\n11:03 inspected deployment\n11:05 rollout started\n11:08 health checks OK\n11:09 released lease\n```\n\n---\n\n## 7.10 Task vs Agent Run\n\n```text\nTask\n\u251c\u2500\u2500 Run 1\n\u251c\u2500\u2500 Run 2\n\u251c\u2500\u2500 Run 3\n\u2514\u2500\u2500 Run N\n```\n\nLa tarea es durable.\n\nEl run es una ejecuci\u00f3n.\n\n---\n\n## 7.11 API com\u00fan\n\nDentro de `wetopi-ai-hub`:\n\n```text\ntools/workledger/\n```\n\nInterfaz:\n\n```python\nwork.list(...)\nwork.get(...)\nwork.create(...)\nwork.claim(...)\nwork.release(...)\nwork.update(...)\nwork.comment(...)\nwork.complete(...)\n\nwork.find_conflicts(...)\nwork.acquire(...)\nwork.renew(...)\nwork.release_resources(...)\n\nwork.link(...)\n```\n\nOperaci\u00f3n sem\u00e1ntica de alto nivel:\n\n```python\nwork.plan(...)\n```\n\n---\n\n## 7.12 Backend\n\nCandidatos analizados:\n\n1. Muster\n2. Plane\n3. It's a Plan\n\nLa decisi\u00f3n arquitect\u00f3nica importante es:\n\n> el backend es reemplazable.\n\nLos agentes consumen el **Wetopi Work Ledger contract**, no directamente el API espec\u00edfico del producto.\n\n---\n\n# 8. Memory Plane: memoria compartida de Wetopi\n\nWetopi ya dispone de una memoria propia inspirada conceptualmente en Memory Bank.\n\nDebe evolucionar hacia un servicio institucional central.\n\n---\n\n## 8.1 Consumidores\n\n```text\nHermes Developer\nHermes SRE\nHermes Marketing\nADK Observers\nADK Specialists\nADK Managers\nfuturos agentes\n```\n\n---\n\n## 8.2 Clases de memoria\n\n### Factual\n\n```text\nEl servicio X usa PostgreSQL.\n```\n\n### Operational\n\n```text\nosd.4 est\u00e1 actualmente degraded.\n```\n\nDebe admitir TTL.\n\n### Procedural\n\n```text\nPara retirar un OSD se sigue este procedimiento.\n```\n\n### Decisions\n\n```text\nADR-034 decidi\u00f3 adoptar X.\n```\n\n### Shared episodic\n\n```text\nINC-1234 tuvo s\u00edntomas similares.\n```\n\n---\n\n## 8.3 Temporalidad\n\nNo sobrescribir sin historia.\n\n```text\nfact version 1\nvalid_until\n\nfact version 2\nvalid_from\n```\n\n---\n\n## 8.4 Escritura conservadora\n\nAPI preferida:\n\n```python\nmemory.propose_fact(...)\n```\n\nResultado:\n\n```text\naccept\nmerge\nsupersede\nreject\nneeds_validation\n```\n\n---\n\n## 8.5 Provenance\n\nCada memoria relevante debe poder almacenar:\n\n```yaml\nsource:\n  actor:\n  tool:\n  system:\n  evidence:\n  observed_at:\n\nconfidence:\nvalid_from:\nvalid_until:\nscope:\n```\n\n---\n\n## 8.6 Autoridad\n\n```text\nAuthoritative live source\n>\nFleet Memory\n>\nLocal agent memory\n>\nLLM inference\n```\n\n---\n\n## 8.7 Scopes\n\n```text\ncompany:wetopi\ncrew:platform\ncrew:development\ncrew:marketing\ncustomer:X\nsite:X\nuser:X\nagent:X\n```\n\n---\n\n## 8.8 API\n\n```python\nmemory_search()\nmemory_get_fact()\nmemory_propose_fact()\nmemory_update_fact()\nmemory_get_history()\nmemory_forget()\n```\n\nLa implementaci\u00f3n interna queda desacoplada.\n\n---\n\n# 9. Memoria local de Hermes + memoria factual global\n\nCada Hermes tendr\u00e1 dos tipos de memoria.\n\n```text\nHermes\n\u251c\u2500\u2500 Local Personal Memory\n\u2502   \u251c\u2500\u2500 USER.md\n\u2502   \u2514\u2500\u2500 MEMORY.md\n\u2502\n\u2514\u2500\u2500 Wetopi Fleet Memory\n```\n\n---\n\n## 9.1 Local Personal Memory\n\nAlmacena informaci\u00f3n \u00fatil para asistir a ese empleado:\n\n- preferencias;\n- forma de trabajar;\n- contexto individual;\n- aprendizaje;\n- tareas personales;\n- h\u00e1bitos.\n\nNo se comparte autom\u00e1ticamente.\n\n---\n\n## 9.2 Fleet Memory\n\nMemoria institucional:\n\n- arquitectura;\n- infraestructura;\n- procedimientos;\n- hechos;\n- decisiones;\n- incidentes;\n- conocimiento colectivo.\n\n---\n\n## 9.3 Regla\n\n> Local memory is permissive. Fleet memory is conservative.\n\n---\n\n# 10. Integraci\u00f3n de Hermes con la memoria de Wetopi\n\nHermes dispone de un mecanismo de **Memory Provider Plugin**.\n\nLa integraci\u00f3n propuesta:\n\n```text\nHermes\n\u251c\u2500\u2500 USER.md\n\u251c\u2500\u2500 MEMORY.md\n\u2514\u2500\u2500 Wetopi Memory Provider\n       \u2502\n       \u25bc\n  Wetopi Memory API\n```\n\n---\n\n## 10.1 Retrieval autom\u00e1tico\n\nEl provider puede ejecutar:\n\n```python\nprefetch(query)\n```\n\nantes del turno.\n\nAs\u00ed Hermes recibe autom\u00e1ticamente contexto institucional relevante.\n\n---\n\n## 10.2 No sincronizar conversaciones globalmente\n\nSe recomienda:\n\n```text\nsync_turn() -> no-op hacia Fleet Memory\non_memory_write() -> no-op hacia Fleet Memory\n```\n\nLas memorias personales permanecen personales.\n\n---\n\n## 10.3 Escritura expl\u00edcita\n\nTools:\n\n```python\nwetopi_memory_search()\nwetopi_memory_get()\nwetopi_memory_propose_fact()\nwetopi_memory_get_history()\n```\n\n---\n\n# 11. Secrets Plane: Vault central con OpenBao\n\n## 11.1 Decisi\u00f3n\n\n**OpenBao** es el candidato principal.\n\nEl requisito de autofill de navegador no es relevante.\n\nLa Web UI es suficiente para humanos.\n\n---\n\n## 11.2 Humanos\n\nPodr\u00e1n:\n\n- guardar credenciales;\n- editar;\n- buscar;\n- copiar passwords;\n- gestionar secrets.\n\n---\n\n## 11.3 Agentes\n\nLos agentes deben usar workload identity.\n\n```text\nKubernetes ServiceAccount\n        \u2193\nJWT\n        \u2193\nOpenBao Kubernetes Auth\n        \u2193\nRole\n        \u2193\nPolicy\n        \u2193\nShort-lived token\n```\n\n---\n\n## 11.4 Secrets fuera del LLM\n\nNo:\n\n```text\nLLM -> read secret -> token plaintext\n```\n\nS\u00ed:\n\n```text\nLLM\n \u2193\ntool\n \u2193\ntool runtime\n \u2193\nOpenBao\n \u2193\nexternal API\n```\n\n---\n\n## 11.5 Dynamic secrets\n\nObjetivo futuro:\n\n- DB temporary credentials;\n- SSH certificates;\n- PKI;\n- short-lived credentials;\n- automated revocation.\n\n---\n\n## 11.6 Audit\n\nTodos los accesos relevantes deben quedar auditados.\n\n---\n\n# 12. Documentation Plane: Markdown + Git + MkDocs Material\n\n## 12.1 Decisi\u00f3n\n\nMigrar progresivamente de:\n\n```text\nMarkdown + BookStack\n```\n\na:\n\n```text\nMarkdown + Git + MkDocs Material\n```\n\n---\n\n## 12.2 Git es source of truth\n\nLos agentes no necesitan API de documentaci\u00f3n.\n\nPueden utilizar:\n\n```text\nread\ngrep\nripgrep\nedit\ndiff\ncommit\nbranch\nPR\n```\n\n---\n\n## 12.3 Humans\n\nMkDocs Material aporta:\n\n- navegaci\u00f3n;\n- search;\n- responsive UI;\n- dark mode;\n- syntax highlighting;\n- tablas;\n- TOC;\n- admonitions;\n- estilos t\u00e9cnicos;\n- CSS limpio.\n\n---\n\n## 12.4 Pipeline\n\n```text\nAgent / Human\n     \u2193\nGit\n     \u2193\nCI\n     \u2193\nmkdocs build\n     \u2193\nstatic HTML\n     \u2193\nweb server\n     \u2193\ndocs.wetopi.net\n```\n\n---\n\n# 13. Estrategia de repositorios\n\nLa nueva plataforma necesita una estrategia expl\u00edcita de repositorios desde el inicio.\n\nEl objetivo es evitar:\n\n- duplicar tools;\n- copiar skills entre agentes;\n- mantener variantes divergentes;\n- mezclar documentaci\u00f3n con c\u00f3digo runtime;\n- acoplar componentes compartidos a un agente concreto.\n\nSe propone separar cinco categor\u00edas.\n\n```text\nWetopi Git Organization\n\u2502\n\u251c\u2500\u2500 wetopi-ai-hub\n\u2502\n\u251c\u2500\u2500 wetopi-docs\n\u2502\n\u251c\u2500\u2500 wetopi-agents\n\u2502   \u2514\u2500\u2500 o repos por crew/agente si crece\n\u2502\n\u251c\u2500\u2500 wetopi-agent-services\n\u2502\n\u2514\u2500\u2500 wetopi-platform\n```\n\nLa nomenclatura definitiva puede cambiar, pero las responsabilidades deben mantenerse separadas.\n\n---\n\n## 13.1 Repositorio: `wetopi-ai-hub`\n\nRepositorio central reutilizable por todos los sistemas ag\u00e9nticos.\n\nContendr\u00e1:\n\n- skills;\n- tools;\n- prompts/contracts compartidos;\n- schemas;\n- agent components;\n- SDKs internos;\n- wrappers;\n- policies reutilizables;\n- evaluaciones;\n- test fixtures.\n\nNo contendr\u00e1:\n\n- secretos;\n- documentaci\u00f3n general de Wetopi;\n- state de agentes;\n- memories personales;\n- configuraci\u00f3n espec\u00edfica sensible de producci\u00f3n.\n\n---\n\n## 13.2 Repositorio: `wetopi-docs`\n\nSource of truth de la documentaci\u00f3n institucional y operativa.\n\nRenderizado mediante MkDocs Material.\n\n---\n\n## 13.3 Repositorio: `wetopi-agents`\n\nOpci\u00f3n inicial recomendada para mantener la flota interna mientras el n\u00famero de agentes sea manejable.\n\nPuede contener:\n\n```text\nagents/\n\u251c\u2500\u2500 platform/\n\u251c\u2500\u2500 wordpress/\n\u251c\u2500\u2500 support/\n\u251c\u2500\u2500 development/\n\u2514\u2500\u2500 marketing/\n```\n\nCada agente tendr\u00e1:\n\n```text\nagent.yaml / config\ninstructions\nruntime integration\ndeployment\ntests\n```\n\nLos tools comunes NO se duplican aqu\u00ed.\n\nSe consumen desde `wetopi-ai-hub`.\n\n---\n\n## 13.4 Repositorio: `wetopi-agent-services`\n\nServicios centrales del plano ag\u00e9ntico:\n\n```text\nmemory-service/\nwork-ledger-adapter/\nhermes-memory-provider/\nagent-identity/\napproval-service/\nfuture shared services/\n```\n\nPuede comenzar como monorepo y separarse cuando alg\u00fan componente necesite lifecycle independiente.\n\n---\n\n## 13.5 Repositorio: `wetopi-platform`\n\nInfraestructura declarativa Kubernetes.\n\nEjemplo:\n\n```text\nclusters/\nbase/\napps/\nnamespaces/\npolicies/\nnetworking/\nstorage/\nobservability/\nagent-services/\n```\n\nDebe ser la fuente GitOps de la infraestructura nueva.\n\n---\n\n# 14. wetopi-ai-hub: repositorio com\u00fan ag\u00e9ntico\n\nEste repo ser\u00e1 una pieza estrat\u00e9gica.\n\nSu prop\u00f3sito es responder:\n\n> \u00bfD\u00f3nde vive cualquier capability reutilizable por m\u00e1s de un agente?\n\nRespuesta:\n\n```text\nwetopi-ai-hub\n```\n\n---\n\n## 14.1 Estructura propuesta\n\n```text\nwetopi-ai-hub/\n\u2502\n\u251c\u2500\u2500 README.md\n\u251c\u2500\u2500 pyproject.toml\n\u2502\n\u251c\u2500\u2500 skills/\n\u2502   \u251c\u2500\u2500 kubernetes/\n\u2502   \u251c\u2500\u2500 ceph/\n\u2502   \u251c\u2500\u2500 wordpress/\n\u2502   \u251c\u2500\u2500 incident-response/\n\u2502   \u251c\u2500\u2500 development/\n\u2502   \u251c\u2500\u2500 marketing/\n\u2502   \u2514\u2500\u2500 ...\n\u2502\n\u251c\u2500\u2500 tools/\n\u2502   \u251c\u2500\u2500 kubernetes/\n\u2502   \u251c\u2500\u2500 ceph/\n\u2502   \u251c\u2500\u2500 consul/\n\u2502   \u251c\u2500\u2500 wordpress/\n\u2502   \u251c\u2500\u2500 workledger/\n\u2502   \u251c\u2500\u2500 memory/\n\u2502   \u251c\u2500\u2500 docs/\n\u2502   \u251c\u2500\u2500 github/\n\u2502   \u2514\u2500\u2500 ...\n\u2502\n\u251c\u2500\u2500 components/\n\u2502   \u251c\u2500\u2500 evidence-ledger/\n\u2502   \u251c\u2500\u2500 investigation-loop/\n\u2502   \u251c\u2500\u2500 approval/\n\u2502   \u251c\u2500\u2500 conflict-check/\n\u2502   \u2514\u2500\u2500 ...\n\u2502\n\u251c\u2500\u2500 contracts/\n\u2502   \u251c\u2500\u2500 work-ledger/\n\u2502   \u251c\u2500\u2500 memory/\n\u2502   \u251c\u2500\u2500 actor-identity/\n\u2502   \u251c\u2500\u2500 evidence/\n\u2502   \u2514\u2500\u2500 ...\n\u2502\n\u251c\u2500\u2500 schemas/\n\u2502   \u251c\u2500\u2500 task.schema.json\n\u2502   \u251c\u2500\u2500 fact.schema.json\n\u2502   \u251c\u2500\u2500 actor.schema.json\n\u2502   \u2514\u2500\u2500 ...\n\u2502\n\u251c\u2500\u2500 sdk/\n\u2502   \u251c\u2500\u2500 python/\n\u2502   \u2514\u2500\u2500 ...\n\u2502\n\u251c\u2500\u2500 evals/\n\u2502   \u251c\u2500\u2500 tool-calls/\n\u2502   \u251c\u2500\u2500 memory-retrieval/\n\u2502   \u2514\u2500\u2500 ...\n\u2502\n\u2514\u2500\u2500 tests/\n```\n\n---\n\n## 14.2 Skills vs tools\n\nDebe mantenerse una separaci\u00f3n sem\u00e1ntica.\n\n### Skill\n\nDescribe:\n\n- conocimiento;\n- procedimiento;\n- estrategia;\n- instrucciones;\n- cu\u00e1ndo utilizar determinadas capabilities.\n\nEjemplo:\n\n```text\nskills/ceph/replace-osd.md\n```\n\n### Tool\n\nEjecuta una acci\u00f3n concreta.\n\nEjemplo:\n\n```text\ntools/ceph/check_health.py\n```\n\nUn skill puede utilizar varias tools.\n\n---\n\n## 14.3 Components\n\nLos components son comportamientos reutilizables de nivel superior.\n\nEjemplos:\n\n```text\ninvestigation-loop\nhypothesis-manager\nevidence-ledger\nwork-conflict-guard\nhuman-approval\nmemory-curator\n```\n\nNo son necesariamente tools individuales.\n\nPueden ser:\n\n- librer\u00edas;\n- callbacks;\n- subagents;\n- middleware;\n- adapters;\n- policies.\n\n---\n\n## 14.4 Contracts\n\nLos sistemas centrales deben exponerse mediante contratos propios.\n\nEjemplos:\n\n```text\nWetopi Work Ledger Contract\nWetopi Memory Contract\nWetopi Actor Identity Contract\nWetopi Evidence Contract\n```\n\nAs\u00ed se evita que los agentes dependan directamente de:\n\n- Muster;\n- Plane;\n- OpenBao internals;\n- la implementaci\u00f3n concreta de Memory Plane.\n\n---\n\n## 14.5 Versionado\n\nTodo componente compartido debe ser versionable.\n\nEjemplo:\n\n```text\nwetopi-ai-hub==0.8.2\n```\n\no componentes versionados independientemente cuando sea necesario.\n\nLos agentes desplegados deben poder informar:\n\n```yaml\nai_hub_version: 0.8.2\n```\n\npara reproducibilidad.\n\n---\n\n## 14.6 Distribuci\u00f3n\n\nNo se recomienda copiar manualmente tools.\n\nOpciones v\u00e1lidas:\n\n### Python package interno\n\n```text\npip install wetopi-ai-hub\n```\n\no paquetes separados:\n\n```text\nwetopi-agent-tools\nwetopi-memory-sdk\nwetopi-workledger-sdk\n```\n\n### Build-time vendoring\n\nLa imagen del agente incorpora las capabilities necesarias durante build.\n\n### Symlinks\n\n\u00datiles en desarrollo local/monorepo, menos deseables como mecanismo de distribuci\u00f3n productivo.\n\n---\n\n## 14.7 Regla\n\n> Una tool reutilizada por dos agentes deja de pertenecer al agente y pasa a `wetopi-ai-hub`.\n\n---\n\n# 15. Repositorios de documentaci\u00f3n\n\nSe propone inicialmente un repositorio principal:\n\n```text\nwetopi-docs\n```\n\nEstructura:\n\n```text\nwetopi-docs/\n\u251c\u2500\u2500 mkdocs.yml\n\u2514\u2500\u2500 docs/\n    \u251c\u2500\u2500 index.md\n    \u2502\n    \u251c\u2500\u2500 company/\n    \u2502\n    \u251c\u2500\u2500 architecture/\n    \u2502\n    \u251c\u2500\u2500 infrastructure/\n    \u2502\n    \u251c\u2500\u2500 kubernetes/\n    \u2502\n    \u251c\u2500\u2500 wordpress/\n    \u2502\n    \u251c\u2500\u2500 procedures/\n    \u2502\n    \u251c\u2500\u2500 runbooks/\n    \u2502\n    \u251c\u2500\u2500 agents/\n    \u2502\n    \u251c\u2500\u2500 security/\n    \u2502\n    \u251c\u2500\u2500 incidents/\n    \u2502\n    \u2514\u2500\u2500 adr/\n```\n\n---\n\n## 15.1 Separaci\u00f3n futura\n\nSi la documentaci\u00f3n crece mucho, podr\u00eda separarse en:\n\n```text\nwetopi-docs-platform\nwetopi-docs-wordpress\nwetopi-docs-business\n```\n\npero **no hacerlo inicialmente** sin necesidad.\n\nUn repositorio \u00fanico ofrece:\n\n- b\u00fasqueda global;\n- enlaces simples;\n- menor fragmentaci\u00f3n;\n- mejor contexto para agentes;\n- CI m\u00e1s sencillo.\n\n---\n\n## 15.2 Docs del propio c\u00f3digo\n\nLa documentaci\u00f3n espec\u00edfica de desarrollo de un componente puede quedarse junto al c\u00f3digo.\n\nEjemplo:\n\n```text\nwetopi-agent-services/memory-service/README.md\n```\n\nMientras que el conocimiento institucional debe ir a:\n\n```text\nwetopi-docs\n```\n\nRegla:\n\n> README explica el componente.  \n> `wetopi-docs` explica c\u00f3mo Wetopi lo utiliza, opera y gobierna.\n\n---\n\n# 16. Modelo de distribuci\u00f3n de tools y skills\n\nFlujo propuesto:\n\n```text\n                  wetopi-ai-hub\n                         \u2502\n        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n        \u2502                \u2502                \u2502\n        \u25bc                \u25bc                \u25bc\n  Hermes Developer   Hermes SRE      ADK Fleet\n        \u2502                \u2502                \u2502\n        \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                         \u2502\n                   shared contracts\n```\n\n---\n\n## 16.1 No entregar 70 tools al LLM\n\nQue existan 70 tools en el AI Hub no significa que un agente deba verlas todas.\n\nCada agente declara sus capabilities.\n\nEjemplo:\n\n```yaml\nagent: wordpress-db-specialist\n\ntools:\n  - memory.search\n  - work.plan\n  - work.comment\n  - wordpress.inspect_db\n  - database.query_readonly\n```\n\n---\n\n## 16.2 Capabilities por rol\n\nEl bundle se construye seg\u00fan:\n\n```text\nruntime\ncrew\nrole\nenvironment\n```\n\n---\n\n## 16.3 Testing central\n\nCada tool reutilizable debe tener:\n\n- unit tests;\n- contract tests;\n- permission tests;\n- safe failure behavior;\n- structured result schema.\n\n---\n\n# 17. Relaci\u00f3n entre memoria, documentaci\u00f3n y Work Ledger\n\nSon tres sistemas diferentes.\n\n## Work Ledger\n\n```text\n\u00bfQu\u00e9 estamos haciendo?\n```\n\n## Memory Plane\n\n```text\n\u00bfQu\u00e9 sabemos?\n```\n\n## Documentation\n\n```text\n\u00bfQu\u00e9 conocimiento hemos decidido formalizar?\n```\n\nFlujo:\n\n```text\nincident\n  \u2193\nticket\n  \u2193\ninvestigation\n  \u2193\nvalidated facts\n  \u2193\nMemory Plane\n  \u2193\nprocedure / ADR / postmortem\n  \u2193\nwetopi-docs\n```\n\n---\n\n# 18. Modelo de identidad y permisos\n\nLa identidad debe ser transversal.\n\nEjemplo:\n\n```text\nactor_id = platform-sre-manager\n```\n\nDebe aparecer en:\n\n- Kubernetes;\n- OpenBao;\n- Memory Plane;\n- Work Ledger;\n- audit logs;\n- observability.\n\nIdeal:\n\n```yaml\nactor_id:\nactor_type:\nruntime:\ncrew:\nrole:\nuser:\nenvironment:\n```\n\n---\n\n# 19. Modelo de scopes y recursos\n\nConvenci\u00f3n compartida:\n\n```text\ncompany:wetopi\ncrew:platform\ncrew:development\ncrew:marketing\ncustomer:123\nsite:456\ncluster:k3s01\nnamespace:agents\nrepo:wetopi-api\nservice:foo\n```\n\nPuede utilizarse para:\n\n- ACL;\n- memoria;\n- locks;\n- visibilidad;\n- ownership;\n- auditor\u00eda.\n\n---\n\n# 20. Observabilidad y auditor\u00eda\n\nCampos comunes:\n\n```text\nactor_id\nactor_type\nruntime\ncrew\ntask_id\nrun_id\nresource\ntool\naction\nresult\nduration\ntimestamp\n```\n\nEjemplo:\n\n```yaml\nactor_id: platform-sre-manager\ntask_id: INC-492\nrun_id: run-492-03\ntool: kubernetes.restart\nresource: k8s:deployment:support-manager\nresult: success\n```\n\nNo registrar:\n\n- secrets;\n- chain-of-thought;\n- informaci\u00f3n privada sin necesidad.\n\n---\n\n# 21. Flujos operativos de ejemplo\n\n## 21.1 ADK detecta incidencia\n\n```text\nObserver\n \u2193\ndetect\n \u2193\nWork Ledger\n \u2193\nINC-492\n \u2193\nSpecialist claims\n \u2193\nresource lease\n \u2193\ninvestigation\n \u2193\njournal\n \u2193\nfix\n \u2193\nreview\n \u2193\ndone\n```\n\n---\n\n## 21.2 Hermes intenta tocar un recurso ocupado\n\n```text\nHermes SRE\n \u2193\nwork.plan()\n \u2193\nconflict detected\n \u2193\nresource owned by ADK specialist\n \u2193\nHermes waits / comments / coordinates\n```\n\n---\n\n## 21.3 Hermes aprende un hecho de Wetopi\n\n```text\nHermes\n \u2193\npropose_fact\n \u2193\nMemory Service\n \u2193\nvalidation\n \u2193\nFleet Memory\n \u2193\navailable to ADK + other Hermes\n```\n\n---\n\n## 21.4 Agent uses secret\n\n```text\nAgent\n \u2193\ntool call\n \u2193\nTool Runtime\n \u2193\nOpenBao\n \u2193\ncredential\n \u2193\nexternal service\n\nLLM does not see credential\n```\n\n---\n\n## 21.5 Agent updates docs\n\n```text\nAgent\n \u2193\nedit Markdown\n \u2193\nbranch / commit / PR\n \u2193\nreview\n \u2193\nmerge\n \u2193\nMkDocs build\n \u2193\nhuman-readable website\n```\n\n---\n\n# 22. Topolog\u00eda inicial de Kubernetes\n\nPropuesta conceptual:\n\n```text\nKubernetes\n\u2502\n\u251c\u2500\u2500 agent-runtime/\n\u2502   \u251c\u2500\u2500 hermes-*\n\u2502   \u2514\u2500\u2500 adk-*\n\u2502\n\u251c\u2500\u2500 agent-services/\n\u2502   \u251c\u2500\u2500 memory-service\n\u2502   \u251c\u2500\u2500 work-ledger\n\u2502   \u251c\u2500\u2500 openbao\n\u2502   \u2514\u2500\u2500 docs\n\u2502\n\u251c\u2500\u2500 shared-data/\n\u2502   \u251c\u2500\u2500 postgres\n\u2502   \u2514\u2500\u2500 object-storage\n\u2502\n\u251c\u2500\u2500 observability/\n\u2502\n\u2514\u2500\u2500 business-workloads/\n```\n\nLos namespaces definitivos deber\u00e1n dise\u00f1arse considerando:\n\n- seguridad;\n- NetworkPolicies;\n- lifecycle;\n- RBAC;\n- ownership.\n\n---\n\n# 23. Orden recomendado de despliegue\n\n## Fase 0 \u2014 Kubernetes foundation\n\n- cluster;\n- networking;\n- ingress;\n- DNS;\n- TLS;\n- storage;\n- PostgreSQL;\n- object storage si aplica;\n- observabilidad b\u00e1sica;\n- GitOps.\n\n---\n\n## Fase 1 \u2014 Identity + Secrets\n\nDesplegar OpenBao.\n\nImplementar:\n\n- Kubernetes Auth;\n- workload identities;\n- policies;\n- audit.\n\n---\n\n## Fase 2 \u2014 Git foundations\n\nCrear:\n\n```text\nwetopi-platform\nwetopi-ai-hub\nwetopi-docs\nwetopi-agents\nwetopi-agent-services\n```\n\nA\u00f1adir:\n\n- branch protection;\n- CI;\n- CODEOWNERS;\n- versioning;\n- release conventions.\n\n---\n\n## Fase 3 \u2014 Memory Plane\n\nProductivizar la memoria existente.\n\nA\u00f1adir:\n\n- API estable;\n- scopes;\n- provenance;\n- candidate facts;\n- contradiction handling;\n- TTL;\n- history.\n\n---\n\n## Fase 4 \u2014 Work Coordination Plane\n\nSeleccionar backend.\n\nCrear el adapter/contract propio.\n\nImplementar:\n\n- actors;\n- tickets;\n- resource scopes;\n- conflict detection;\n- leases;\n- intents;\n- journal.\n\n---\n\n## Fase 5 \u2014 Documentation Plane\n\nCrear `wetopi-docs`.\n\nDesplegar:\n\n- MkDocs Material;\n- CI;\n- static hosting.\n\nMigrar progresivamente documentaci\u00f3n \u00fatil existente.\n\n---\n\n## Fase 6 \u2014 wetopi-ai-hub\n\nConsolidar tools hoy dispersas.\n\nPrioridad inicial:\n\n```text\ntools/memory\ntools/workledger\ntools/kubernetes\ntools/ceph\ntools/wordpress\n```\n\nDefinir contratos estables.\n\n---\n\n## Fase 7 \u2014 Hermes\n\nDesplegar un Hermes por empleado seg\u00fan necesidad.\n\nCada Hermes tendr\u00e1:\n\n```text\npersonal local memory\n+\nWetopi Fleet Memory\n+\nWork Ledger\n+\nrole-specific tools\n+\ndocumentation access\n```\n\n---\n\n## Fase 8 \u2014 ADK Fleet\n\nDesplegar observers, specialists y managers.\n\nTodos usan:\n\n- Memory Plane;\n- Work Ledger;\n- OpenBao;\n- Docs;\n- AI Hub;\n- observabilidad com\u00fan.\n\n---\n\n## Fase 9 \u2014 Construcci\u00f3n del resto de Wetopi\n\nCon los servicios ag\u00e9nticos ya operativos, comenzar la migraci\u00f3n/reconstrucci\u00f3n de los dem\u00e1s workloads de Wetopi.\n\nLos agentes participan desde el inicio en:\n\n- discovery;\n- documentaci\u00f3n;\n- migration;\n- testing;\n- operaciones;\n- observabilidad;\n- incident response.\n\n---\n\n# 24. Decisiones consolidadas\n\n## D-001 \u2014 Kubernetes como plataforma base\n\nLa nueva Wetopi se reconstruir\u00e1 sobre Kubernetes.\n\n---\n\n## D-002 \u2014 AI-first\n\nLos sistemas ag\u00e9nticos se despliegan antes que el resto de la plataforma.\n\n---\n\n## D-003 \u2014 Hermes + ADK\n\nSe combinar\u00e1n:\n\n- Hermes como asistente personal/coding por empleado;\n- ADK para la flota standalone y especializada.\n\n---\n\n## D-004 \u2014 Memoria dual\n\nHermes utilizar\u00e1:\n\n```text\nlocal personal memory\n+\nWetopi Fleet Memory\n```\n\nLos ADK utilizar\u00e1n principalmente Fleet Memory m\u00e1s state/session cuando corresponda.\n\n---\n\n## D-005 \u2014 Memoria institucional propia\n\nSe reutilizar\u00e1 y evolucionar\u00e1 el Memory Plane ya creado internamente.\n\n---\n\n## D-006 \u2014 Integraci\u00f3n de Hermes mediante Memory Provider\n\nEl retrieval global se integrar\u00e1 mediante el mecanismo de provider de Hermes.\n\nNo se volcar\u00e1n conversaciones personales autom\u00e1ticamente a Fleet Memory.\n\n---\n\n## D-007 \u2014 OpenBao\n\nOpenBao ser\u00e1 el candidato principal como secrets manager central.\n\n---\n\n## D-008 \u2014 Secret isolation\n\nLas tools consumir\u00e1n secrets sin introducirlos en contexto LLM siempre que sea posible.\n\n---\n\n## D-009 \u2014 Work Ledger\n\nEl Kanban se considera UI de un sistema m\u00e1s profundo de coordinaci\u00f3n.\n\n---\n\n## D-010 \u2014 Backend desacoplado\n\nMuster, Plane o alternativa se utilizar\u00e1n detr\u00e1s de un contrato Wetopi.\n\n---\n\n## D-011 \u2014 Markdown como formato documental\n\nMarkdown + Git ser\u00e1 el source of truth.\n\n---\n\n## D-012 \u2014 MkDocs Material\n\nSe utilizar\u00e1 como primera opci\u00f3n para renderizado web de documentaci\u00f3n.\n\n---\n\n## D-013 \u2014 AI Hub\n\nSe crear\u00e1 `wetopi-ai-hub` como repositorio com\u00fan de:\n\n- skills;\n- tools;\n- components;\n- contracts;\n- schemas;\n- SDKs;\n- evals.\n\n---\n\n## D-014 \u2014 Tools compartidas centralizadas\n\nUna tool usada por m\u00e1s de un agente debe trasladarse al AI Hub.\n\n---\n\n## D-015 \u2014 GitOps\n\nLa infraestructura Kubernetes debe quedar declarada en Git.\n\n---\n\n# 25. Riesgos y controles\n\n## 25.1 Contaminaci\u00f3n de memoria\n\n**Riesgo:** agentes almacenan hip\u00f3tesis como hechos.\n\n**Control:**\n\n- candidate facts;\n- provenance;\n- confidence;\n- validation;\n- source authority.\n\n---\n\n## 25.2 Conflictos entre agentes\n\n**Riesgo:** varios agentes modifican el mismo recurso.\n\n**Control:**\n\n- Work Ledger;\n- scopes;\n- leases;\n- intents;\n- conflict check.\n\n---\n\n## 25.3 Secrets en contexto\n\n**Riesgo:** token termina en prompt/log/trace.\n\n**Control:**\n\n- secret retrieval dentro de tool runtime;\n- dynamic secrets;\n- redaction;\n- audit.\n\n---\n\n## 25.4 Tool sprawl\n\n**Riesgo:** cada agente implementa su versi\u00f3n.\n\n**Control:**\n\n- `wetopi-ai-hub`;\n- contracts;\n- versioning;\n- tests.\n\n---\n\n## 25.5 Context overload\n\n**Riesgo:** demasiadas tools disponibles para un LLM.\n\n**Control:**\n\n- bundles por rol;\n- tool routing;\n- APIs sem\u00e1nticas de alto nivel.\n\n---\n\n## 25.6 Documentaci\u00f3n obsoleta\n\n**Riesgo:** los agentes conf\u00edan en docs antiguas.\n\n**Control:**\n\n- Git;\n- ownership;\n- review dates;\n- links con fuentes autoritativas;\n- documentaci\u00f3n vs factual memory claramente diferenciadas.\n\n---\n\n# 26. Cuestiones abiertas\n\nA\u00fan deben decidirse:\n\n1. Backend final del Work Coordination Plane:\n   - Muster;\n   - Plane;\n   - It's a Plan;\n   - alternativa.\n\n2. Authentication humana central:\n   - OIDC provider existente;\n   - SSO objetivo.\n\n3. Modelo exacto de namespaces Kubernetes.\n\n4. Storage definitivo de Memory Plane.\n\n5. Estrategia de embeddings/reranking del Memory Plane.\n\n6. Convenci\u00f3n exacta de nombres de scopes.\n\n7. Si `wetopi-agents` permanece monorepo o se divide cuando crezca.\n\n8. Packaging definitivo de `wetopi-ai-hub`.\n\n9. CI/CD y GitOps concretos.\n\n10. Modelo de aprobaci\u00f3n humana para acciones de alto riesgo.\n\n---\n\n# 27. Resultado esperado\n\nAl finalizar la primera etapa, Wetopi deber\u00eda disponer de una base parecida a:\n\n```text\n                   WETOPI AI-FIRST FOUNDATION\n\n                           Humans\n                              \u2502\n                \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                \u25bc             \u25bc             \u25bc\n              Hermes       Kanban         Docs\n                \u2502             \u2502             \u2502\n                \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                       \u2502             \u2502\n                 Work Ledger     Fleet Memory\n                       \u2502             \u2502\n                       \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                              \u2502\n                         wetopi-ai-hub\n                              \u2502\n           \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n           \u25bc                  \u25bc                  \u25bc\n        Hermes Fleet      ADK Fleet        Future Agents\n           \u2502                  \u2502\n           \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                              \u2502\n                           OpenBao\n                              \u2502\n                           Kubernetes\n                              \u2502\n                   \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                   \u25bc          \u25bc          \u25bc\n                  Infra     Hosting    Business\n```\n\nLa consecuencia m\u00e1s importante es que Wetopi no tendr\u00e1 simplemente \"algunos agentes\".\n\nLa organizaci\u00f3n dispondr\u00e1 de una **infraestructura operacional dise\u00f1ada para humanos y agentes simult\u00e1neamente**:\n\n- conocimiento institucional compartido;\n- trabajo coordinado;\n- secrets gobernados;\n- documentaci\u00f3n agent-native;\n- tools y skills reutilizables;\n- identidad y auditor\u00eda comunes;\n- repositorios definidos;\n- capacidad de incorporar nuevos runtimes sin redise\u00f1ar la plataforma.\n\n\u00c9sta ser\u00e1 la base sobre la que reconstruir el resto de Wetopi.",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (design/ai-first-harness-plan.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<h1 id=\"bkmrk-wetopi-ai-first-kube\">Wetopi AI-First Kubernetes Rebuild</h1>\n<h2 id=\"bkmrk-memoria-t%C3%A9cnica-y-ej\">Memoria t\u00e9cnica y ejecutiva del proyecto</h2>\n<p id=\"bkmrk-fecha%3A-28-de-agosto-\"><strong>Fecha:</strong> 28 de agosto de 2026<br>\n<strong>Estado:</strong> Dise\u00f1o inicial consolidado<br>\n<strong>\u00c1mbito:</strong> Reconstrucci\u00f3n de Wetopi sobre Kubernetes con una arquitectura AI-first y una flota interna de sistemas ag\u00e9nticos.</p>\n<hr id=\"bkmrk-\">\n<h1 id=\"bkmrk-%C3%8Dndice\">\u00cdndice</h1>\n<ol id=\"bkmrk-resumen-ejecutivo-ob\">\n<li><a href=\"#1-resumen-ejecutivo\">Resumen ejecutivo</a></li>\n<li><a href=\"#2-objetivo-del-proyecto\">Objetivo del proyecto</a></li>\n<li><a href=\"#3-principios-de-dise%C3%B1o\">Principios de dise\u00f1o</a></li>\n<li><a href=\"#4-arquitectura-general\">Arquitectura general</a></li>\n<li><a href=\"#5-actores-del-sistema\">Actores del sistema</a></li>\n<li><a href=\"#6-servicios-ag%C3%A9nticos-prioritarios\">Servicios ag\u00e9nticos prioritarios</a></li>\n<li><a href=\"#7-work-coordination-plane-kanban-tickets-y-coordinaci%C3%B3n\">Work Coordination Plane: Kanban, tickets y coordinaci\u00f3n</a></li>\n<li><a href=\"#8-memory-plane-memoria-compartida-de-wetopi\">Memory Plane: memoria compartida de Wetopi</a></li>\n<li><a href=\"#9-memoria-local-de-hermes--memoria-factual-global\">Memoria local de Hermes + memoria factual global</a></li>\n<li><a href=\"#10-integraci%C3%B3n-de-hermes-con-la-memoria-de-wetopi\">Integraci\u00f3n de Hermes con la memoria de Wetopi</a></li>\n<li><a href=\"#11-secrets-plane-vault-central-con-openbao\">Secrets Plane: Vault central con OpenBao</a></li>\n<li><a href=\"#12-documentation-plane-markdown--git--mkdocs-material\">Documentation Plane: Markdown + Git + MkDocs Material</a></li>\n<li><a href=\"#13-estrategia-de-repositorios\">Estrategia de repositorios</a></li>\n<li><a href=\"#14-wetopi-ai-hub-repositorio-com%C3%BAn-ag%C3%A9ntico\">wetopi-ai-hub: repositorio com\u00fan ag\u00e9ntico</a></li>\n<li><a href=\"#15-repositorios-de-documentaci%C3%B3n\">Repositorios de documentaci\u00f3n</a></li>\n<li><a href=\"#16-modelo-de-distribuci%C3%B3n-de-tools-y-skills\">Modelo de distribuci\u00f3n de tools y skills</a></li>\n<li><a href=\"#17-relaci%C3%B3n-entre-memoria-documentaci%C3%B3n-y-work-ledger\">Relaci\u00f3n entre memoria, documentaci\u00f3n y Work Ledger</a></li>\n<li><a href=\"#18-modelo-de-identidad-y-permisos\">Modelo de identidad y permisos</a></li>\n<li><a href=\"#19-modelo-de-scopes-y-recursos\">Modelo de scopes y recursos</a></li>\n<li><a href=\"#20-observabilidad-y-auditor%C3%ADa\">Observabilidad y auditor\u00eda</a></li>\n<li><a href=\"#21-flujos-operativos-de-ejemplo\">Flujos operativos de ejemplo</a></li>\n<li><a href=\"#22-topolog%C3%ADa-inicial-de-kubernetes\">Topolog\u00eda inicial de Kubernetes</a></li>\n<li><a href=\"#23-orden-recomendado-de-despliegue\">Orden recomendado de despliegue</a></li>\n<li><a href=\"#24-decisiones-consolidadas\">Decisiones consolidadas</a></li>\n<li><a href=\"#25-riesgos-y-controles\">Riesgos y controles</a></li>\n<li><a href=\"#26-cuestiones-abiertas\">Cuestiones abiertas</a></li>\n<li><a href=\"#27-resultado-esperado\">Resultado esperado</a></li>\n</ol>\n<hr id=\"bkmrk--1\">\n<h1 id=\"bkmrk-1.-resumen-ejecutivo\">1. Resumen ejecutivo</h1>\n<p id=\"bkmrk-wetopi-se-va-a-recon\">Wetopi se va a reconstruir sobre Kubernetes con un enfoque <strong>AI-first desde el primer d\u00eda</strong>.</p>\n<p id=\"bkmrk-la-intenci%C3%B3n-no-es-c\">La intenci\u00f3n no es construir una plataforma tradicional y a\u00f1adir agentes despu\u00e9s. La nueva Wetopi debe asumir desde su base que:</p>\n<ul id=\"bkmrk-habr%C3%A1-agentes-aut%C3%B3no\">\n<li>habr\u00e1 agentes aut\u00f3nomos operando de forma continua;</li>\n<li>habr\u00e1 varios Hermes actuando como asistentes personales de empleados y como agentes de coding;</li>\n<li>habr\u00e1 agentes Google ADK standalone, especialistas, observers y managers;</li>\n<li>humanos y agentes compartir\u00e1n conocimiento, tareas, documentaci\u00f3n y servicios;</li>\n<li>los agentes deber\u00e1n coordinarse antes de modificar recursos compartidos;</li>\n<li>las acciones relevantes deber\u00e1n quedar registradas;</li>\n<li>los secrets deber\u00e1n estar fuera del contexto LLM siempre que sea posible;</li>\n<li>la documentaci\u00f3n debe ser directamente editable por agentes;</li>\n<li>la memoria factual de la organizaci\u00f3n debe ser compartida entre runtimes;</li>\n<li>las memorias personales de cada Hermes deber\u00e1n mantenerse separadas.</li>\n</ul>\n<p id=\"bkmrk-la-primera-capa-que-\">La <strong>primera capa que se desplegar\u00e1</strong> en la nueva infraestructura ser\u00e1 precisamente la infraestructura ag\u00e9ntica com\u00fan.</p>\n<p id=\"bkmrk-los-componentes-inic\">Los componentes iniciales ser\u00e1n:</p>\n<ol id=\"bkmrk-agent-runtime-layer-\">\n<li>\n<p><strong>Agent Runtime Layer</strong></p>\n<ul>\n<li>Hermes por empleado.</li>\n<li>Agentes ADK de flota.</li>\n</ul>\n</li>\n<li>\n<p><strong>Memory Plane</strong></p>\n<ul>\n<li>Memoria institucional com\u00fan de Wetopi.</li>\n<li>Memoria factual, procedural, operacional, decisiones y episodios relevantes.</li>\n</ul>\n</li>\n<li>\n<p><strong>Work Coordination Plane</strong></p>\n<ul>\n<li>Kanban/ticketing como interfaz humana.</li>\n<li>Work Ledger como mecanismo real de coordinaci\u00f3n entre agentes.</li>\n</ul>\n</li>\n<li>\n<p><strong>Secrets Plane</strong></p>\n<ul>\n<li>OpenBao como vault central.</li>\n</ul>\n</li>\n<li>\n<p><strong>Documentation Plane</strong></p>\n<ul>\n<li>Markdown puro versionado en Git.</li>\n<li>MkDocs Material como interfaz web.</li>\n</ul>\n</li>\n<li>\n<p><strong>AI Hub</strong></p>\n<ul>\n<li>Repositorio com\u00fan para skills, tools, librer\u00edas, componentes y contratos reutilizables por todos los agentes.</li>\n</ul>\n</li>\n</ol>\n<p id=\"bkmrk-el-objetivo-es-que-e\">El objetivo es que estos servicios formen la <strong>capa base de operaci\u00f3n inteligente de Wetopi</strong> antes de reconstruir encima el resto de servicios de negocio y hosting.</p>\n<hr id=\"bkmrk--2\">\n<h1 id=\"bkmrk-2.-objetivo-del-proy\">2. Objetivo del proyecto</h1>\n<p id=\"bkmrk-construir-una-nueva-\">Construir una nueva plataforma interna de Wetopi sobre Kubernetes donde los agentes sean <strong>ciudadanos de primera clase</strong>.</p>\n<p id=\"bkmrk-la-plataforma-debe-p\">La plataforma debe permitir que cualquier agente:</p>\n<ul id=\"bkmrk-descubra-conocimient\">\n<li>descubra conocimiento antes de actuar;</li>\n<li>conozca qu\u00e9 tareas est\u00e1n en curso;</li>\n<li>detecte conflictos con otros agentes;</li>\n<li>reclame trabajo;</li>\n<li>adquiera leases sobre recursos;</li>\n<li>declare su intenci\u00f3n;</li>\n<li>deje trazabilidad;</li>\n<li>utilice secrets sin exponerlos innecesariamente al modelo;</li>\n<li>consulte documentaci\u00f3n en Markdown;</li>\n<li>contribuya conocimiento factual a la organizaci\u00f3n;</li>\n<li>comparta infraestructura com\u00fan independientemente de si corre sobre Hermes, ADK u otro runtime futuro.</li>\n</ul>\n<hr id=\"bkmrk--3\">\n<h1 id=\"bkmrk-3.-principios-de-dis\">3. Principios de dise\u00f1o</h1>\n<h2 id=\"bkmrk-3.1-ai-first\">3.1 AI-first</h2>\n<p id=\"bkmrk-cada-servicio-deber%C3%A1\">Cada servicio deber\u00e1 responder afirmativamente a esta pregunta:</p>\n<blockquote id=\"bkmrk-%C2%BFpuede-un-agente-con\">\n<p>\u00bfPuede un agente consumir este sistema de forma program\u00e1tica, segura y auditable sin depender de una interfaz humana?</p>\n</blockquote>\n<p id=\"bkmrk-se-priorizar%C3%A1n%3A\">Se priorizar\u00e1n:</p>\n<ul id=\"bkmrk-apis%3B-git%3B-markdown%3B\">\n<li>APIs;</li>\n<li>Git;</li>\n<li>Markdown;</li>\n<li>identidad de workload;</li>\n<li>webhooks y eventos;</li>\n<li>formatos estructurados;</li>\n<li>pol\u00edticas;</li>\n<li>trazabilidad;</li>\n<li>contratos estables.</li>\n</ul>\n<hr id=\"bkmrk--4\">\n<h2 id=\"bkmrk-3.2-human-compatible\">3.2 Human-compatible, agent-native</h2>\n<p id=\"bkmrk-los-humanos-seguir%C3%A1n\">Los humanos seguir\u00e1n teniendo UI:</p>\n<ul id=\"bkmrk-kanban%3B-documentaci%C3%B3\">\n<li>Kanban;</li>\n<li>documentaci\u00f3n web;</li>\n<li>Web UI de secretos;</li>\n<li>dashboards;</li>\n<li>revisi\u00f3n de tareas.</li>\n</ul>\n<p id=\"bkmrk-pero-la-ui-nunca-ser\">Pero la UI nunca ser\u00e1 el \u00fanico canal de acceso.</p>\n<hr id=\"bkmrk--5\">\n<h2 id=\"bkmrk-3.3-source-of-truth-\">3.3 Source of truth expl\u00edcitos</h2>\n<table id=\"bkmrk-dominio-source-of-tr\">\n<thead>\n<tr>\n<th>Dominio</th>\n<th>Source of truth</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>C\u00f3digo</td>\n<td>Git</td>\n</tr>\n<tr>\n<td>Skills / tools / componentes AI</td>\n<td><code>wetopi-ai-hub</code></td>\n</tr>\n<tr>\n<td>Documentaci\u00f3n</td>\n<td>Markdown en Git</td>\n</tr>\n<tr>\n<td>Trabajo y coordinaci\u00f3n</td>\n<td>Work Ledger</td>\n</tr>\n<tr>\n<td>Conocimiento factual</td>\n<td>Memory Plane</td>\n</tr>\n<tr>\n<td>Secrets</td>\n<td>OpenBao</td>\n</tr>\n<tr>\n<td>Estado real de infraestructura</td>\n<td>APIs del sistema correspondiente</td>\n</tr>\n<tr>\n<td>Preferencias personales</td>\n<td>Memoria local de Hermes</td>\n</tr>\n</tbody>\n</table>\n<hr id=\"bkmrk--6\">\n<h2 id=\"bkmrk-3.4-no-convertir-la-\">3.4 No convertir la memoria en una copia del mundo</h2>\n<p id=\"bkmrk-la-memoria-guarda-co\">La memoria guarda conocimiento \u00fatil, no sustituye a los sistemas autoritativos.</p>\n<p id=\"bkmrk-ejemplo%3A\">Ejemplo:</p>\n<pre id=\"bkmrk-kubernetes-replicas-\"><code class=\"language-text\">Kubernetes replicas = source of truth real\n\nFleet Memory =\n\"este servicio suele correr con 5 replicas\"\n+\nhist\u00f3rico\n+\ndecisiones\n+\nprocedimientos\n</code></pre>\n<p id=\"bkmrk-si-hay-contradicci%C3%B3n\">Si hay contradicci\u00f3n, gana la fuente viva autoritativa.</p>\n<hr id=\"bkmrk--7\">\n<h2 id=\"bkmrk-3.5-least-privilege\">3.5 Least privilege</h2>\n<p id=\"bkmrk-cada-agente-tendr%C3%A1-%C3%BA\">Cada agente tendr\u00e1 \u00fanicamente:</p>\n<ul id=\"bkmrk-tools-necesarias%3B-sc\">\n<li>tools necesarias;</li>\n<li>scopes de memoria necesarios;</li>\n<li>secrets necesarios;</li>\n<li>permisos de Work Ledger necesarios;</li>\n<li>recursos modificables necesarios.</li>\n</ul>\n<hr id=\"bkmrk--8\">\n<h2 id=\"bkmrk-3.6-trazabilidad-ant\">3.6 Trazabilidad antes que autonom\u00eda ciega</h2>\n<p id=\"bkmrk-las-acciones-importa\">Las acciones importantes deben poder responder:</p>\n<pre id=\"bkmrk-%C2%BFqu%C3%A9-ocurri%C3%B3%3F-%C2%BFqui%C3%A9n\"><code class=\"language-text\">\u00bfQu\u00e9 ocurri\u00f3?\n\u00bfQui\u00e9n lo hizo?\n\u00bfPor qu\u00e9?\n\u00bfSobre qu\u00e9 recurso?\n\u00bfCon qu\u00e9 ticket?\n\u00bfCon qu\u00e9 tool?\n\u00bfCu\u00e1ndo?\n\u00bfCu\u00e1l fue el resultado?\n</code></pre>\n<hr id=\"bkmrk--9\">\n<h2 id=\"bkmrk-3.7-runtime-agnostic\">3.7 Runtime-agnostic</h2>\n<p id=\"bkmrk-los-servicios-compar\">Los servicios compartidos no deben dise\u00f1arse exclusivamente para ADK o Hermes.</p>\n<p id=\"bkmrk-se-debe-poder-a%C3%B1adir\">Se debe poder a\u00f1adir en el futuro:</p>\n<pre id=\"bkmrk-nuevo-runtime-nuevo-\"><code class=\"language-text\">nuevo runtime\nnuevo coding agent\nnuevo autonomous agent\n</code></pre>\n<p id=\"bkmrk-sin-cambiar-el-dise%C3%B1\">sin cambiar el dise\u00f1o institucional.</p>\n<hr id=\"bkmrk--10\">\n<h1 id=\"bkmrk-4.-arquitectura-gene\">4. Arquitectura general</h1>\n<pre id=\"bkmrk-humans-%E2%94%82-%E2%94%8C%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80\"><code class=\"language-text\">                               HUMANS\n                                  \u2502\n                    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                    \u2502             \u2502             \u2502\n                    \u25bc             \u25bc             \u25bc\n                 Hermes       Web UIs       Git / CLI\n                    \u2502\n                    \u2502\n       \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n       \u2502            AGENTIC SERVICES LAYER              \u2502\n       \u2502                                                \u2502\n       \u2502  Memory     Work Ledger    Secrets     Docs    \u2502\n       \u2502    \u2502             \u2502            \u2502          \u2502     \u2502\n       \u2502    \u2502             \u2502            \u2502          \u2502     \u2502\n       \u2502    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 AI HUB / contracts \u2500\u2500\u2500\u2500\u2500\u2518     \u2502\n       \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                    \u2502\n              \u250c\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n              \u2502                                   \u2502\n              \u25bc                                   \u25bc\n          ADK Fleet                            Hermes Fleet\n              \u2502                                   \u2502\n              \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                               \u2502\n                               \u25bc\n                          Kubernetes\n                               \u2502\n                 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                 \u25bc             \u25bc             \u25bc\n               Infra        Hosting       Business\n</code></pre>\n<hr id=\"bkmrk--11\">\n<h1 id=\"bkmrk-5.-actores-del-siste\">5. Actores del sistema</h1>\n<p id=\"bkmrk-modelo-com%C3%BAn%3A\">Modelo com\u00fan:</p>\n<pre id=\"bkmrk-actor-%E2%94%9C%E2%94%80%E2%94%80-human-%E2%94%9C%E2%94%80%E2%94%80-\"><code class=\"language-text\">Actor\n\u251c\u2500\u2500 Human\n\u251c\u2500\u2500 Hermes\n\u251c\u2500\u2500 ADK Agent\n\u2514\u2500\u2500 Future Agent Runtime\n</code></pre>\n<p id=\"bkmrk-cada-actor-debe-tene\">Cada actor debe tener como m\u00ednimo:</p>\n<pre id=\"bkmrk-actor_id%3A-actor_type\"><code class=\"language-yaml\">actor_id:\nactor_type:\nruntime:\ncrew:\nrole:\nuser_id:\nenvironment:\n</code></pre>\n<p id=\"bkmrk-ejemplos%3A\">Ejemplos:</p>\n<pre id=\"bkmrk-actor_id%3A-hermes-dev\"><code class=\"language-yaml\">actor_id: hermes-developer\nactor_type: hermes\nruntime: hermes\ncrew: development\n</code></pre>\n<pre id=\"bkmrk-actor_id%3A-platform-o\"><code class=\"language-yaml\">actor_id: platform-observability-manager\nactor_type: agent\nruntime: adk\ncrew: platform\nrole: manager\n</code></pre>\n<hr id=\"bkmrk--12\">\n<h1 id=\"bkmrk-6.-servicios-ag%C3%A9ntic\">6. Servicios ag\u00e9nticos prioritarios</h1>\n<p id=\"bkmrk-los-primeros-servici\">Los primeros servicios funcionales que deben existir son:</p>\n<pre id=\"bkmrk-1.-kubernetes-base-2\"><code class=\"language-text\">1. Kubernetes base\n2. Identity / workload identity\n3. OpenBao\n4. Memory Plane\n5. Work Coordination Plane\n6. Documentation Plane\n7. wetopi-ai-hub\n8. Hermes\n9. ADK Fleet\n</code></pre>\n<p id=\"bkmrk-todos-los-dem%C3%A1s-serv\">Todos los dem\u00e1s servicios internos podr\u00e1n despu\u00e9s apoyarse en esta base.</p>\n<hr id=\"bkmrk--13\">\n<h1 id=\"bkmrk-7.-work-coordination\">7. Work Coordination Plane: Kanban, tickets y coordinaci\u00f3n</h1>\n<h2 id=\"bkmrk-7.1-el-kanban-no-es-\">7.1 El Kanban no es el sistema</h2>\n<p id=\"bkmrk-la-interfaz-kanban-e\">La interfaz Kanban es para humanos.</p>\n<p id=\"bkmrk-el-sistema-real-es-u\">El sistema real es un:</p>\n<blockquote id=\"bkmrk-shared-work-ledger-%2F\">\n<p><strong>Shared Work Ledger / Work Coordination Plane</strong></p>\n</blockquote>\n<p id=\"bkmrk-su-misi%C3%B3n-es-evitar-\">Su misi\u00f3n es evitar que Hermes, ADK y humanos ejecuten trabajos contradictorios.</p>\n<hr id=\"bkmrk--14\">\n<h2 id=\"bkmrk-7.2-regla-principal\">7.2 Regla principal</h2>\n<blockquote id=\"bkmrk-ning%C3%BAn-agente-modifi\">\n<p>Ning\u00fan agente modifica un recurso compartido sin consultar primero el Work Ledger cuando la acci\u00f3n pueda interferir con otro actor, requiera seguimiento o tenga impacto operativo.</p>\n</blockquote>\n<p id=\"bkmrk-no-toda-lectura-gene\">No toda lectura genera ticket.</p>\n<hr id=\"bkmrk--15\">\n<h2 id=\"bkmrk-7.3-estados\">7.3 Estados</h2>\n<pre id=\"bkmrk-backlog-ready-claime\"><code class=\"language-text\">BACKLOG\nREADY\nCLAIMED\nIN_PROGRESS\nBLOCKED\nREVIEW\nDONE\nCANCELLED\n</code></pre>\n<hr id=\"bkmrk--16\">\n<h2 id=\"bkmrk-7.4-ticket-m%C3%ADnimo\">7.4 Ticket m\u00ednimo</h2>\n<pre id=\"bkmrk-id%3A-ops-381-title%3A-u\"><code class=\"language-yaml\">id: OPS-381\n\ntitle: Upgrade ingress\n\nowner: hermes-sre\n\nstatus: IN_PROGRESS\n\nscope:\n  - k8s:cluster:k3s01\n  - k8s:deployment:ingress\n\nrisk: medium\n\nlease_until: ...\n\nactivity:\n  - inspected current deployment\n  - updating configuration\n</code></pre>\n<hr id=\"bkmrk--17\">\n<h2 id=\"bkmrk-7.5-resource-scopes\">7.5 Resource scopes</h2>\n<p id=\"bkmrk-ejemplos%3A-1\">Ejemplos:</p>\n<pre id=\"bkmrk-github%3Arepo%3Awetopi-a\"><code class=\"language-text\">github:repo:wetopi-api\nk8s:cluster:k3s01\nk8s:namespace:agents\nk8s:deployment:support-manager\nceph:cluster:wetopi\nwordpress:site:12345\ndns:zone:wetopi.com\nmarketing:campaign:summer\n</code></pre>\n<hr id=\"bkmrk--18\">\n<h2 id=\"bkmrk-7.6-lock-modes\">7.6 Lock modes</h2>\n<pre id=\"bkmrk-shared-exclusive\"><code class=\"language-text\">shared\nexclusive\n</code></pre>\n<hr id=\"bkmrk--19\">\n<h2 id=\"bkmrk-7.7-leases\">7.7 Leases</h2>\n<p id=\"bkmrk-los-locks-deben-tene\">Los locks deben tener TTL.</p>\n<pre id=\"bkmrk-claim-%E2%86%93-lease-%E2%86%93-hear\"><code class=\"language-text\">claim\n \u2193\nlease\n \u2193\nheartbeat\n \u2193\nrenew\n</code></pre>\n<p id=\"bkmrk-si-el-agente-desapar\">Si el agente desaparece, el recurso vuelve a quedar disponible.</p>\n<hr id=\"bkmrk--20\">\n<h2 id=\"bkmrk-7.8-intent\">7.8 Intent</h2>\n<p id=\"bkmrk-antes-de-ejecutar-ca\">Antes de ejecutar cambios relevantes:</p>\n<pre id=\"bkmrk-intent%3A-action%3A-rest\"><code class=\"language-yaml\">intent:\n  action: restart\n  target: support-manager\n  reason: memory leak\n  expected_effect: rolling restart\n</code></pre>\n<hr id=\"bkmrk--21\">\n<h2 id=\"bkmrk-7.9-activity-journal\">7.9 Activity journal</h2>\n<p id=\"bkmrk-no-guardar-chain-of-\">No guardar chain-of-thought.</p>\n<p id=\"bkmrk-guardar-hechos-opera\">Guardar hechos operativos:</p>\n<pre id=\"bkmrk-11%3A02-claimed-ops-38\"><code class=\"language-text\">11:02 claimed OPS-381\n11:03 inspected deployment\n11:05 rollout started\n11:08 health checks OK\n11:09 released lease\n</code></pre>\n<hr id=\"bkmrk--22\">\n<h2 id=\"bkmrk-7.10-task-vs-agent-r\">7.10 Task vs Agent Run</h2>\n<pre id=\"bkmrk-task-%E2%94%9C%E2%94%80%E2%94%80-run-1-%E2%94%9C%E2%94%80%E2%94%80-r\"><code class=\"language-text\">Task\n\u251c\u2500\u2500 Run 1\n\u251c\u2500\u2500 Run 2\n\u251c\u2500\u2500 Run 3\n\u2514\u2500\u2500 Run N\n</code></pre>\n<p id=\"bkmrk-la-tarea-es-durable.\">La tarea es durable.</p>\n<p id=\"bkmrk-el-run-es-una-ejecuc\">El run es una ejecuci\u00f3n.</p>\n<hr id=\"bkmrk--23\">\n<h2 id=\"bkmrk-7.11-api-com%C3%BAn\">7.11 API com\u00fan</h2>\n<p id=\"bkmrk-dentro-de-wetopi-ai-\">Dentro de <code>wetopi-ai-hub</code>:</p>\n<pre id=\"bkmrk-tools%2Fworkledger%2F\"><code class=\"language-text\">tools/workledger/\n</code></pre>\n<p id=\"bkmrk-interfaz%3A\">Interfaz:</p>\n<pre id=\"bkmrk-work.list%28...%29-work.\"><code class=\"language-python\">work.list(...)\nwork.get(...)\nwork.create(...)\nwork.claim(...)\nwork.release(...)\nwork.update(...)\nwork.comment(...)\nwork.complete(...)\n\nwork.find_conflicts(...)\nwork.acquire(...)\nwork.renew(...)\nwork.release_resources(...)\n\nwork.link(...)\n</code></pre>\n<p id=\"bkmrk-operaci%C3%B3n-sem%C3%A1ntica-\">Operaci\u00f3n sem\u00e1ntica de alto nivel:</p>\n<pre id=\"bkmrk-work.plan%28...%29\"><code class=\"language-python\">work.plan(...)\n</code></pre>\n<hr id=\"bkmrk--24\">\n<h2 id=\"bkmrk-7.12-backend\">7.12 Backend</h2>\n<p id=\"bkmrk-candidatos-analizado\">Candidatos analizados:</p>\n<ol id=\"bkmrk-muster-plane-it%27s-a-\">\n<li>Muster</li>\n<li>Plane</li>\n<li>It's a Plan</li>\n</ol>\n<p id=\"bkmrk-la-decisi%C3%B3n-arquitec\">La decisi\u00f3n arquitect\u00f3nica importante es:</p>\n<blockquote id=\"bkmrk-el-backend-es-reempl\">\n<p>el backend es reemplazable.</p>\n</blockquote>\n<p id=\"bkmrk-los-agentes-consumen\">Los agentes consumen el <strong>Wetopi Work Ledger contract</strong>, no directamente el API espec\u00edfico del producto.</p>\n<hr id=\"bkmrk--25\">\n<h1 id=\"bkmrk-8.-memory-plane%3A-mem\">8. Memory Plane: memoria compartida de Wetopi</h1>\n<p id=\"bkmrk-wetopi-ya-dispone-de\">Wetopi ya dispone de una memoria propia inspirada conceptualmente en Memory Bank.</p>\n<p id=\"bkmrk-debe-evolucionar-hac\">Debe evolucionar hacia un servicio institucional central.</p>\n<hr id=\"bkmrk--26\">\n<h2 id=\"bkmrk-8.1-consumidores\">8.1 Consumidores</h2>\n<pre id=\"bkmrk-hermes-developer-her\"><code class=\"language-text\">Hermes Developer\nHermes SRE\nHermes Marketing\nADK Observers\nADK Specialists\nADK Managers\nfuturos agentes\n</code></pre>\n<hr id=\"bkmrk--27\">\n<h2 id=\"bkmrk-8.2-clases-de-memori\">8.2 Clases de memoria</h2>\n<h3 id=\"bkmrk-factual\">Factual</h3>\n<pre id=\"bkmrk-el-servicio-x-usa-po\"><code class=\"language-text\">El servicio X usa PostgreSQL.\n</code></pre>\n<h3 id=\"bkmrk-operational\">Operational</h3>\n<pre id=\"bkmrk-osd.4-est%C3%A1-actualmen\"><code class=\"language-text\">osd.4 est\u00e1 actualmente degraded.\n</code></pre>\n<p id=\"bkmrk-debe-admitir-ttl.\">Debe admitir TTL.</p>\n<h3 id=\"bkmrk-procedural\">Procedural</h3>\n<pre id=\"bkmrk-para-retirar-un-osd-\"><code class=\"language-text\">Para retirar un OSD se sigue este procedimiento.\n</code></pre>\n<h3 id=\"bkmrk-decisions\">Decisions</h3>\n<pre id=\"bkmrk-adr-034-decidi%C3%B3-adop\"><code class=\"language-text\">ADR-034 decidi\u00f3 adoptar X.\n</code></pre>\n<h3 id=\"bkmrk-shared-episodic\">Shared episodic</h3>\n<pre id=\"bkmrk-inc-1234-tuvo-s%C3%ADntom\"><code class=\"language-text\">INC-1234 tuvo s\u00edntomas similares.\n</code></pre>\n<hr id=\"bkmrk--28\">\n<h2 id=\"bkmrk-8.3-temporalidad\">8.3 Temporalidad</h2>\n<p id=\"bkmrk-no-sobrescribir-sin-\">No sobrescribir sin historia.</p>\n<pre id=\"bkmrk-fact-version-1-valid\"><code class=\"language-text\">fact version 1\nvalid_until\n\nfact version 2\nvalid_from\n</code></pre>\n<hr id=\"bkmrk--29\">\n<h2 id=\"bkmrk-8.4-escritura-conser\">8.4 Escritura conservadora</h2>\n<p id=\"bkmrk-api-preferida%3A\">API preferida:</p>\n<pre id=\"bkmrk-memory.propose_fact%28\"><code class=\"language-python\">memory.propose_fact(...)\n</code></pre>\n<p id=\"bkmrk-resultado%3A\">Resultado:</p>\n<pre id=\"bkmrk-accept-merge-superse\"><code class=\"language-text\">accept\nmerge\nsupersede\nreject\nneeds_validation\n</code></pre>\n<hr id=\"bkmrk--30\">\n<h2 id=\"bkmrk-8.5-provenance\">8.5 Provenance</h2>\n<p id=\"bkmrk-cada-memoria-relevan\">Cada memoria relevante debe poder almacenar:</p>\n<pre id=\"bkmrk-source%3A-actor%3A-tool%3A\"><code class=\"language-yaml\">source:\n  actor:\n  tool:\n  system:\n  evidence:\n  observed_at:\n\nconfidence:\nvalid_from:\nvalid_until:\nscope:\n</code></pre>\n<hr id=\"bkmrk--31\">\n<h2 id=\"bkmrk-8.6-autoridad\">8.6 Autoridad</h2>\n<pre id=\"bkmrk-authoritative-live-s\"><code class=\"language-text\">Authoritative live source\n&gt;\nFleet Memory\n&gt;\nLocal agent memory\n&gt;\nLLM inference\n</code></pre>\n<hr id=\"bkmrk--32\">\n<h2 id=\"bkmrk-8.7-scopes\">8.7 Scopes</h2>\n<pre id=\"bkmrk-company%3Awetopi-crew%3A\"><code class=\"language-text\">company:wetopi\ncrew:platform\ncrew:development\ncrew:marketing\ncustomer:X\nsite:X\nuser:X\nagent:X\n</code></pre>\n<hr id=\"bkmrk--33\">\n<h2 id=\"bkmrk-8.8-api\">8.8 API</h2>\n<pre id=\"bkmrk-memory_search%28%29-memo\"><code class=\"language-python\">memory_search()\nmemory_get_fact()\nmemory_propose_fact()\nmemory_update_fact()\nmemory_get_history()\nmemory_forget()\n</code></pre>\n<p id=\"bkmrk-la-implementaci%C3%B3n-in\">La implementaci\u00f3n interna queda desacoplada.</p>\n<hr id=\"bkmrk--34\">\n<h1 id=\"bkmrk-9.-memoria-local-de-\">9. Memoria local de Hermes + memoria factual global</h1>\n<p id=\"bkmrk-cada-hermes-tendr%C3%A1-d\">Cada Hermes tendr\u00e1 dos tipos de memoria.</p>\n<pre id=\"bkmrk-hermes-%E2%94%9C%E2%94%80%E2%94%80-local-per\"><code class=\"language-text\">Hermes\n\u251c\u2500\u2500 Local Personal Memory\n\u2502   \u251c\u2500\u2500 USER.md\n\u2502   \u2514\u2500\u2500 MEMORY.md\n\u2502\n\u2514\u2500\u2500 Wetopi Fleet Memory\n</code></pre>\n<hr id=\"bkmrk--35\">\n<h2 id=\"bkmrk-9.1-local-personal-m\">9.1 Local Personal Memory</h2>\n<p id=\"bkmrk-almacena-informaci%C3%B3n\">Almacena informaci\u00f3n \u00fatil para asistir a ese empleado:</p>\n<ul id=\"bkmrk-preferencias%3B-forma-\">\n<li>preferencias;</li>\n<li>forma de trabajar;</li>\n<li>contexto individual;</li>\n<li>aprendizaje;</li>\n<li>tareas personales;</li>\n<li>h\u00e1bitos.</li>\n</ul>\n<p id=\"bkmrk-no-se-comparte-autom\">No se comparte autom\u00e1ticamente.</p>\n<hr id=\"bkmrk--36\">\n<h2 id=\"bkmrk-9.2-fleet-memory\">9.2 Fleet Memory</h2>\n<p id=\"bkmrk-memoria-instituciona\">Memoria institucional:</p>\n<ul id=\"bkmrk-arquitectura%3B-infrae\">\n<li>arquitectura;</li>\n<li>infraestructura;</li>\n<li>procedimientos;</li>\n<li>hechos;</li>\n<li>decisiones;</li>\n<li>incidentes;</li>\n<li>conocimiento colectivo.</li>\n</ul>\n<hr id=\"bkmrk--37\">\n<h2 id=\"bkmrk-9.3-regla\">9.3 Regla</h2>\n<blockquote id=\"bkmrk-local-memory-is-perm\">\n<p>Local memory is permissive. Fleet memory is conservative.</p>\n</blockquote>\n<hr id=\"bkmrk--38\">\n<h1 id=\"bkmrk-10.-integraci%C3%B3n-de-h\">10. Integraci\u00f3n de Hermes con la memoria de Wetopi</h1>\n<p id=\"bkmrk-hermes-dispone-de-un\">Hermes dispone de un mecanismo de <strong>Memory Provider Plugin</strong>.</p>\n<p id=\"bkmrk-la-integraci%C3%B3n-propu\">La integraci\u00f3n propuesta:</p>\n<pre id=\"bkmrk-hermes-%E2%94%9C%E2%94%80%E2%94%80-user.md-%E2%94%9C\"><code class=\"language-text\">Hermes\n\u251c\u2500\u2500 USER.md\n\u251c\u2500\u2500 MEMORY.md\n\u2514\u2500\u2500 Wetopi Memory Provider\n       \u2502\n       \u25bc\n  Wetopi Memory API\n</code></pre>\n<hr id=\"bkmrk--39\">\n<h2 id=\"bkmrk-10.1-retrieval-autom\">10.1 Retrieval autom\u00e1tico</h2>\n<p id=\"bkmrk-el-provider-puede-ej\">El provider puede ejecutar:</p>\n<pre id=\"bkmrk-prefetch%28query%29\"><code class=\"language-python\">prefetch(query)\n</code></pre>\n<p id=\"bkmrk-antes-del-turno.\">antes del turno.</p>\n<p id=\"bkmrk-as%C3%AD-hermes-recibe-au\">As\u00ed Hermes recibe autom\u00e1ticamente contexto institucional relevante.</p>\n<hr id=\"bkmrk--40\">\n<h2 id=\"bkmrk-10.2-no-sincronizar-\">10.2 No sincronizar conversaciones globalmente</h2>\n<p id=\"bkmrk-se-recomienda%3A\">Se recomienda:</p>\n<pre id=\"bkmrk-sync_turn%28%29--%3E-no-op\"><code class=\"language-text\">sync_turn() -&gt; no-op hacia Fleet Memory\non_memory_write() -&gt; no-op hacia Fleet Memory\n</code></pre>\n<p id=\"bkmrk-las-memorias-persona\">Las memorias personales permanecen personales.</p>\n<hr id=\"bkmrk--41\">\n<h2 id=\"bkmrk-10.3-escritura-expl%C3%AD\">10.3 Escritura expl\u00edcita</h2>\n<p id=\"bkmrk-tools%3A\">Tools:</p>\n<pre id=\"bkmrk-wetopi_memory_search\"><code class=\"language-python\">wetopi_memory_search()\nwetopi_memory_get()\nwetopi_memory_propose_fact()\nwetopi_memory_get_history()\n</code></pre>\n<hr id=\"bkmrk--42\">\n<h1 id=\"bkmrk-11.-secrets-plane%3A-v\">11. Secrets Plane: Vault central con OpenBao</h1>\n<h2 id=\"bkmrk-11.1-decisi%C3%B3n\">11.1 Decisi\u00f3n</h2>\n<p id=\"bkmrk-openbao-es-el-candid\"><strong>OpenBao</strong> es el candidato principal.</p>\n<p id=\"bkmrk-el-requisito-de-auto\">El requisito de autofill de navegador no es relevante.</p>\n<p id=\"bkmrk-la-web-ui-es-suficie\">La Web UI es suficiente para humanos.</p>\n<hr id=\"bkmrk--43\">\n<h2 id=\"bkmrk-11.2-humanos\">11.2 Humanos</h2>\n<p id=\"bkmrk-podr%C3%A1n%3A\">Podr\u00e1n:</p>\n<ul id=\"bkmrk-guardar-credenciales\">\n<li>guardar credenciales;</li>\n<li>editar;</li>\n<li>buscar;</li>\n<li>copiar passwords;</li>\n<li>gestionar secrets.</li>\n</ul>\n<hr id=\"bkmrk--44\">\n<h2 id=\"bkmrk-11.3-agentes\">11.3 Agentes</h2>\n<p id=\"bkmrk-los-agentes-deben-us\">Los agentes deben usar workload identity.</p>\n<pre id=\"bkmrk-kubernetes-serviceac\"><code class=\"language-text\">Kubernetes ServiceAccount\n        \u2193\nJWT\n        \u2193\nOpenBao Kubernetes Auth\n        \u2193\nRole\n        \u2193\nPolicy\n        \u2193\nShort-lived token\n</code></pre>\n<hr id=\"bkmrk--45\">\n<h2 id=\"bkmrk-11.4-secrets-fuera-d\">11.4 Secrets fuera del LLM</h2>\n<p id=\"bkmrk-no%3A\">No:</p>\n<pre id=\"bkmrk-llm--%3E-read-secret--\"><code class=\"language-text\">LLM -&gt; read secret -&gt; token plaintext\n</code></pre>\n<p id=\"bkmrk-s%C3%AD%3A\">S\u00ed:</p>\n<pre id=\"bkmrk-llm-%E2%86%93-tool-%E2%86%93-tool-ru\"><code class=\"language-text\">LLM\n \u2193\ntool\n \u2193\ntool runtime\n \u2193\nOpenBao\n \u2193\nexternal API\n</code></pre>\n<hr id=\"bkmrk--46\">\n<h2 id=\"bkmrk-11.5-dynamic-secrets\">11.5 Dynamic secrets</h2>\n<p id=\"bkmrk-objetivo-futuro%3A\">Objetivo futuro:</p>\n<ul id=\"bkmrk-db-temporary-credent\">\n<li>DB temporary credentials;</li>\n<li>SSH certificates;</li>\n<li>PKI;</li>\n<li>short-lived credentials;</li>\n<li>automated revocation.</li>\n</ul>\n<hr id=\"bkmrk--47\">\n<h2 id=\"bkmrk-11.6-audit\">11.6 Audit</h2>\n<p id=\"bkmrk-todos-los-accesos-re\">Todos los accesos relevantes deben quedar auditados.</p>\n<hr id=\"bkmrk--48\">\n<h1 id=\"bkmrk-12.-documentation-pl\">12. Documentation Plane: Markdown + Git + MkDocs Material</h1>\n<h2 id=\"bkmrk-12.1-decisi%C3%B3n\">12.1 Decisi\u00f3n</h2>\n<p id=\"bkmrk-migrar-progresivamen\">Migrar progresivamente de:</p>\n<pre id=\"bkmrk-markdown-%2B-bookstack\"><code class=\"language-text\">Markdown + BookStack\n</code></pre>\n<p id=\"bkmrk-a%3A\">a:</p>\n<pre id=\"bkmrk-markdown-%2B-git-%2B-mkd\"><code class=\"language-text\">Markdown + Git + MkDocs Material\n</code></pre>\n<hr id=\"bkmrk--49\">\n<h2 id=\"bkmrk-12.2-git-es-source-o\">12.2 Git es source of truth</h2>\n<p id=\"bkmrk-los-agentes-no-neces\">Los agentes no necesitan API de documentaci\u00f3n.</p>\n<p id=\"bkmrk-pueden-utilizar%3A\">Pueden utilizar:</p>\n<pre id=\"bkmrk-read-grep-ripgrep-ed\"><code class=\"language-text\">read\ngrep\nripgrep\nedit\ndiff\ncommit\nbranch\nPR\n</code></pre>\n<hr id=\"bkmrk--50\">\n<h2 id=\"bkmrk-12.3-humans\">12.3 Humans</h2>\n<p id=\"bkmrk-mkdocs-material-apor\">MkDocs Material aporta:</p>\n<ul id=\"bkmrk-navegaci%C3%B3n%3B-search%3B-\">\n<li>navegaci\u00f3n;</li>\n<li>search;</li>\n<li>responsive UI;</li>\n<li>dark mode;</li>\n<li>syntax highlighting;</li>\n<li>tablas;</li>\n<li>TOC;</li>\n<li>admonitions;</li>\n<li>estilos t\u00e9cnicos;</li>\n<li>CSS limpio.</li>\n</ul>\n<hr id=\"bkmrk--51\">\n<h2 id=\"bkmrk-12.4-pipeline\">12.4 Pipeline</h2>\n<pre id=\"bkmrk-agent-%2F-human-%E2%86%93-git-\"><code class=\"language-text\">Agent / Human\n     \u2193\nGit\n     \u2193\nCI\n     \u2193\nmkdocs build\n     \u2193\nstatic HTML\n     \u2193\nweb server\n     \u2193\ndocs.wetopi.net\n</code></pre>\n<hr id=\"bkmrk--52\">\n<h1 id=\"bkmrk-13.-estrategia-de-re\">13. Estrategia de repositorios</h1>\n<p id=\"bkmrk-la-nueva-plataforma-\">La nueva plataforma necesita una estrategia expl\u00edcita de repositorios desde el inicio.</p>\n<p id=\"bkmrk-el-objetivo-es-evita\">El objetivo es evitar:</p>\n<ul id=\"bkmrk-duplicar-tools%3B-copi\">\n<li>duplicar tools;</li>\n<li>copiar skills entre agentes;</li>\n<li>mantener variantes divergentes;</li>\n<li>mezclar documentaci\u00f3n con c\u00f3digo runtime;</li>\n<li>acoplar componentes compartidos a un agente concreto.</li>\n</ul>\n<p id=\"bkmrk-se-propone-separar-c\">Se propone separar cinco categor\u00edas.</p>\n<pre id=\"bkmrk-wetopi-git-organizat\"><code class=\"language-text\">Wetopi Git Organization\n\u2502\n\u251c\u2500\u2500 wetopi-ai-hub\n\u2502\n\u251c\u2500\u2500 wetopi-docs\n\u2502\n\u251c\u2500\u2500 wetopi-agents\n\u2502   \u2514\u2500\u2500 o repos por crew/agente si crece\n\u2502\n\u251c\u2500\u2500 wetopi-agent-services\n\u2502\n\u2514\u2500\u2500 wetopi-platform\n</code></pre>\n<p id=\"bkmrk-la-nomenclatura-defi\">La nomenclatura definitiva puede cambiar, pero las responsabilidades deben mantenerse separadas.</p>\n<hr id=\"bkmrk--53\">\n<h2 id=\"bkmrk-13.1-repositorio%3A-we\">13.1 Repositorio: <code>wetopi-ai-hub</code></h2>\n<p id=\"bkmrk-repositorio-central-\">Repositorio central reutilizable por todos los sistemas ag\u00e9nticos.</p>\n<p id=\"bkmrk-contendr%C3%A1%3A\">Contendr\u00e1:</p>\n<ul id=\"bkmrk-skills%3B-tools%3B-promp\">\n<li>skills;</li>\n<li>tools;</li>\n<li>prompts/contracts compartidos;</li>\n<li>schemas;</li>\n<li>agent components;</li>\n<li>SDKs internos;</li>\n<li>wrappers;</li>\n<li>policies reutilizables;</li>\n<li>evaluaciones;</li>\n<li>test fixtures.</li>\n</ul>\n<p id=\"bkmrk-no-contendr%C3%A1%3A\">No contendr\u00e1:</p>\n<ul id=\"bkmrk-secretos%3B-documentac\">\n<li>secretos;</li>\n<li>documentaci\u00f3n general de Wetopi;</li>\n<li>state de agentes;</li>\n<li>memories personales;</li>\n<li>configuraci\u00f3n espec\u00edfica sensible de producci\u00f3n.</li>\n</ul>\n<hr id=\"bkmrk--54\">\n<h2 id=\"bkmrk-13.2-repositorio%3A-we\">13.2 Repositorio: <code>wetopi-docs</code></h2>\n<p id=\"bkmrk-source-of-truth-de-l\">Source of truth de la documentaci\u00f3n institucional y operativa.</p>\n<p id=\"bkmrk-renderizado-mediante\">Renderizado mediante MkDocs Material.</p>\n<hr id=\"bkmrk--55\">\n<h2 id=\"bkmrk-13.3-repositorio%3A-we\">13.3 Repositorio: <code>wetopi-agents</code></h2>\n<p id=\"bkmrk-opci%C3%B3n-inicial-recom\">Opci\u00f3n inicial recomendada para mantener la flota interna mientras el n\u00famero de agentes sea manejable.</p>\n<p id=\"bkmrk-puede-contener%3A\">Puede contener:</p>\n<pre id=\"bkmrk-agents%2F-%E2%94%9C%E2%94%80%E2%94%80-platform\"><code class=\"language-text\">agents/\n\u251c\u2500\u2500 platform/\n\u251c\u2500\u2500 wordpress/\n\u251c\u2500\u2500 support/\n\u251c\u2500\u2500 development/\n\u2514\u2500\u2500 marketing/\n</code></pre>\n<p id=\"bkmrk-cada-agente-tendr%C3%A1%3A\">Cada agente tendr\u00e1:</p>\n<pre id=\"bkmrk-agent.yaml-%2F-config-\"><code class=\"language-text\">agent.yaml / config\ninstructions\nruntime integration\ndeployment\ntests\n</code></pre>\n<p id=\"bkmrk-los-tools-comunes-no\">Los tools comunes NO se duplican aqu\u00ed.</p>\n<p id=\"bkmrk-se-consumen-desde-we\">Se consumen desde <code>wetopi-ai-hub</code>.</p>\n<hr id=\"bkmrk--56\">\n<h2 id=\"bkmrk-13.4-repositorio%3A-we\">13.4 Repositorio: <code>wetopi-agent-services</code></h2>\n<p id=\"bkmrk-servicios-centrales-\">Servicios centrales del plano ag\u00e9ntico:</p>\n<pre id=\"bkmrk-memory-service%2F-work\"><code class=\"language-text\">memory-service/\nwork-ledger-adapter/\nhermes-memory-provider/\nagent-identity/\napproval-service/\nfuture shared services/\n</code></pre>\n<p id=\"bkmrk-puede-comenzar-como-\">Puede comenzar como monorepo y separarse cuando alg\u00fan componente necesite lifecycle independiente.</p>\n<hr id=\"bkmrk--57\">\n<h2 id=\"bkmrk-13.5-repositorio%3A-we\">13.5 Repositorio: <code>wetopi-platform</code></h2>\n<p id=\"bkmrk-infraestructura-decl\">Infraestructura declarativa Kubernetes.</p>\n<p id=\"bkmrk-ejemplo%3A-1\">Ejemplo:</p>\n<pre id=\"bkmrk-clusters%2F-base%2F-apps\"><code class=\"language-text\">clusters/\nbase/\napps/\nnamespaces/\npolicies/\nnetworking/\nstorage/\nobservability/\nagent-services/\n</code></pre>\n<p id=\"bkmrk-debe-ser-la-fuente-g\">Debe ser la fuente GitOps de la infraestructura nueva.</p>\n<hr id=\"bkmrk--58\">\n<h1 id=\"bkmrk-14.-wetopi-ai-hub%3A-r\">14. wetopi-ai-hub: repositorio com\u00fan ag\u00e9ntico</h1>\n<p id=\"bkmrk-este-repo-ser%C3%A1-una-p\">Este repo ser\u00e1 una pieza estrat\u00e9gica.</p>\n<p id=\"bkmrk-su-prop%C3%B3sito-es-resp\">Su prop\u00f3sito es responder:</p>\n<blockquote id=\"bkmrk-%C2%BFd%C3%B3nde-vive-cualquie\">\n<p>\u00bfD\u00f3nde vive cualquier capability reutilizable por m\u00e1s de un agente?</p>\n</blockquote>\n<p id=\"bkmrk-respuesta%3A\">Respuesta:</p>\n<pre id=\"bkmrk-wetopi-ai-hub\"><code class=\"language-text\">wetopi-ai-hub\n</code></pre>\n<hr id=\"bkmrk--59\">\n<h2 id=\"bkmrk-14.1-estructura-prop\">14.1 Estructura propuesta</h2>\n<pre id=\"bkmrk-wetopi-ai-hub%2F-%E2%94%82-%E2%94%9C%E2%94%80%E2%94%80\"><code class=\"language-text\">wetopi-ai-hub/\n\u2502\n\u251c\u2500\u2500 README.md\n\u251c\u2500\u2500 pyproject.toml\n\u2502\n\u251c\u2500\u2500 skills/\n\u2502   \u251c\u2500\u2500 kubernetes/\n\u2502   \u251c\u2500\u2500 ceph/\n\u2502   \u251c\u2500\u2500 wordpress/\n\u2502   \u251c\u2500\u2500 incident-response/\n\u2502   \u251c\u2500\u2500 development/\n\u2502   \u251c\u2500\u2500 marketing/\n\u2502   \u2514\u2500\u2500 ...\n\u2502\n\u251c\u2500\u2500 tools/\n\u2502   \u251c\u2500\u2500 kubernetes/\n\u2502   \u251c\u2500\u2500 ceph/\n\u2502   \u251c\u2500\u2500 consul/\n\u2502   \u251c\u2500\u2500 wordpress/\n\u2502   \u251c\u2500\u2500 workledger/\n\u2502   \u251c\u2500\u2500 memory/\n\u2502   \u251c\u2500\u2500 docs/\n\u2502   \u251c\u2500\u2500 github/\n\u2502   \u2514\u2500\u2500 ...\n\u2502\n\u251c\u2500\u2500 components/\n\u2502   \u251c\u2500\u2500 evidence-ledger/\n\u2502   \u251c\u2500\u2500 investigation-loop/\n\u2502   \u251c\u2500\u2500 approval/\n\u2502   \u251c\u2500\u2500 conflict-check/\n\u2502   \u2514\u2500\u2500 ...\n\u2502\n\u251c\u2500\u2500 contracts/\n\u2502   \u251c\u2500\u2500 work-ledger/\n\u2502   \u251c\u2500\u2500 memory/\n\u2502   \u251c\u2500\u2500 actor-identity/\n\u2502   \u251c\u2500\u2500 evidence/\n\u2502   \u2514\u2500\u2500 ...\n\u2502\n\u251c\u2500\u2500 schemas/\n\u2502   \u251c\u2500\u2500 task.schema.json\n\u2502   \u251c\u2500\u2500 fact.schema.json\n\u2502   \u251c\u2500\u2500 actor.schema.json\n\u2502   \u2514\u2500\u2500 ...\n\u2502\n\u251c\u2500\u2500 sdk/\n\u2502   \u251c\u2500\u2500 python/\n\u2502   \u2514\u2500\u2500 ...\n\u2502\n\u251c\u2500\u2500 evals/\n\u2502   \u251c\u2500\u2500 tool-calls/\n\u2502   \u251c\u2500\u2500 memory-retrieval/\n\u2502   \u2514\u2500\u2500 ...\n\u2502\n\u2514\u2500\u2500 tests/\n</code></pre>\n<hr id=\"bkmrk--60\">\n<h2 id=\"bkmrk-14.2-skills-vs-tools\">14.2 Skills vs tools</h2>\n<p id=\"bkmrk-debe-mantenerse-una-\">Debe mantenerse una separaci\u00f3n sem\u00e1ntica.</p>\n<h3 id=\"bkmrk-skill\">Skill</h3>\n<p id=\"bkmrk-describe%3A\">Describe:</p>\n<ul id=\"bkmrk-conocimiento%3B-proced\">\n<li>conocimiento;</li>\n<li>procedimiento;</li>\n<li>estrategia;</li>\n<li>instrucciones;</li>\n<li>cu\u00e1ndo utilizar determinadas capabilities.</li>\n</ul>\n<p id=\"bkmrk-ejemplo%3A-2\">Ejemplo:</p>\n<pre id=\"bkmrk-skills%2Fceph%2Freplace-\"><code class=\"language-text\">skills/ceph/replace-osd.md\n</code></pre>\n<h3 id=\"bkmrk-tool\">Tool</h3>\n<p id=\"bkmrk-ejecuta-una-acci%C3%B3n-c\">Ejecuta una acci\u00f3n concreta.</p>\n<p id=\"bkmrk-ejemplo%3A-3\">Ejemplo:</p>\n<pre id=\"bkmrk-tools%2Fceph%2Fcheck_hea\"><code class=\"language-text\">tools/ceph/check_health.py\n</code></pre>\n<p id=\"bkmrk-un-skill-puede-utili\">Un skill puede utilizar varias tools.</p>\n<hr id=\"bkmrk--61\">\n<h2 id=\"bkmrk-14.3-components\">14.3 Components</h2>\n<p id=\"bkmrk-los-components-son-c\">Los components son comportamientos reutilizables de nivel superior.</p>\n<p id=\"bkmrk-ejemplos%3A-2\">Ejemplos:</p>\n<pre id=\"bkmrk-investigation-loop-h\"><code class=\"language-text\">investigation-loop\nhypothesis-manager\nevidence-ledger\nwork-conflict-guard\nhuman-approval\nmemory-curator\n</code></pre>\n<p id=\"bkmrk-no-son-necesariament\">No son necesariamente tools individuales.</p>\n<p id=\"bkmrk-pueden-ser%3A\">Pueden ser:</p>\n<ul id=\"bkmrk-librer%C3%ADas%3B-callbacks\">\n<li>librer\u00edas;</li>\n<li>callbacks;</li>\n<li>subagents;</li>\n<li>middleware;</li>\n<li>adapters;</li>\n<li>policies.</li>\n</ul>\n<hr id=\"bkmrk--62\">\n<h2 id=\"bkmrk-14.4-contracts\">14.4 Contracts</h2>\n<p id=\"bkmrk-los-sistemas-central\">Los sistemas centrales deben exponerse mediante contratos propios.</p>\n<p id=\"bkmrk-ejemplos%3A-3\">Ejemplos:</p>\n<pre id=\"bkmrk-wetopi-work-ledger-c\"><code class=\"language-text\">Wetopi Work Ledger Contract\nWetopi Memory Contract\nWetopi Actor Identity Contract\nWetopi Evidence Contract\n</code></pre>\n<p id=\"bkmrk-as%C3%AD-se-evita-que-los\">As\u00ed se evita que los agentes dependan directamente de:</p>\n<ul id=\"bkmrk-muster%3B-plane%3B-openb\">\n<li>Muster;</li>\n<li>Plane;</li>\n<li>OpenBao internals;</li>\n<li>la implementaci\u00f3n concreta de Memory Plane.</li>\n</ul>\n<hr id=\"bkmrk--63\">\n<h2 id=\"bkmrk-14.5-versionado\">14.5 Versionado</h2>\n<p id=\"bkmrk-todo-componente-comp\">Todo componente compartido debe ser versionable.</p>\n<p id=\"bkmrk-ejemplo%3A-4\">Ejemplo:</p>\n<pre id=\"bkmrk-wetopi-ai-hub%3D%3D0.8.2\"><code class=\"language-text\">wetopi-ai-hub==0.8.2\n</code></pre>\n<p id=\"bkmrk-o-componentes-versio\">o componentes versionados independientemente cuando sea necesario.</p>\n<p id=\"bkmrk-los-agentes-desplega\">Los agentes desplegados deben poder informar:</p>\n<pre id=\"bkmrk-ai_hub_version%3A-0.8.\"><code class=\"language-yaml\">ai_hub_version: 0.8.2\n</code></pre>\n<p id=\"bkmrk-para-reproducibilida\">para reproducibilidad.</p>\n<hr id=\"bkmrk--64\">\n<h2 id=\"bkmrk-14.6-distribuci%C3%B3n\">14.6 Distribuci\u00f3n</h2>\n<p id=\"bkmrk-no-se-recomienda-cop\">No se recomienda copiar manualmente tools.</p>\n<p id=\"bkmrk-opciones-v%C3%A1lidas%3A\">Opciones v\u00e1lidas:</p>\n<h3 id=\"bkmrk-python-package-inter\">Python package interno</h3>\n<pre id=\"bkmrk-pip-install-wetopi-a\"><code class=\"language-text\">pip install wetopi-ai-hub\n</code></pre>\n<p id=\"bkmrk-o-paquetes-separados\">o paquetes separados:</p>\n<pre id=\"bkmrk-wetopi-agent-tools-w\"><code class=\"language-text\">wetopi-agent-tools\nwetopi-memory-sdk\nwetopi-workledger-sdk\n</code></pre>\n<h3 id=\"bkmrk-build-time-vendoring\">Build-time vendoring</h3>\n<p id=\"bkmrk-la-imagen-del-agente\">La imagen del agente incorpora las capabilities necesarias durante build.</p>\n<h3 id=\"bkmrk-symlinks\">Symlinks</h3>\n<p id=\"bkmrk-%C3%9Atiles-en-desarrollo\">\u00datiles en desarrollo local/monorepo, menos deseables como mecanismo de distribuci\u00f3n productivo.</p>\n<hr id=\"bkmrk--65\">\n<h2 id=\"bkmrk-14.7-regla\">14.7 Regla</h2>\n<blockquote id=\"bkmrk-una-tool-reutilizada\">\n<p>Una tool reutilizada por dos agentes deja de pertenecer al agente y pasa a <code>wetopi-ai-hub</code>.</p>\n</blockquote>\n<hr id=\"bkmrk--66\">\n<h1 id=\"bkmrk-15.-repositorios-de-\">15. Repositorios de documentaci\u00f3n</h1>\n<p id=\"bkmrk-se-propone-inicialme\">Se propone inicialmente un repositorio principal:</p>\n<pre id=\"bkmrk-wetopi-docs\"><code class=\"language-text\">wetopi-docs\n</code></pre>\n<p id=\"bkmrk-estructura%3A\">Estructura:</p>\n<pre id=\"bkmrk-wetopi-docs%2F-%E2%94%9C%E2%94%80%E2%94%80-mkd\"><code class=\"language-text\">wetopi-docs/\n\u251c\u2500\u2500 mkdocs.yml\n\u2514\u2500\u2500 docs/\n    \u251c\u2500\u2500 index.md\n    \u2502\n    \u251c\u2500\u2500 company/\n    \u2502\n    \u251c\u2500\u2500 architecture/\n    \u2502\n    \u251c\u2500\u2500 infrastructure/\n    \u2502\n    \u251c\u2500\u2500 kubernetes/\n    \u2502\n    \u251c\u2500\u2500 wordpress/\n    \u2502\n    \u251c\u2500\u2500 procedures/\n    \u2502\n    \u251c\u2500\u2500 runbooks/\n    \u2502\n    \u251c\u2500\u2500 agents/\n    \u2502\n    \u251c\u2500\u2500 security/\n    \u2502\n    \u251c\u2500\u2500 incidents/\n    \u2502\n    \u2514\u2500\u2500 adr/\n</code></pre>\n<hr id=\"bkmrk--67\">\n<h2 id=\"bkmrk-15.1-separaci%C3%B3n-futu\">15.1 Separaci\u00f3n futura</h2>\n<p id=\"bkmrk-si-la-documentaci%C3%B3n-\">Si la documentaci\u00f3n crece mucho, podr\u00eda separarse en:</p>\n<pre id=\"bkmrk-wetopi-docs-platform\"><code class=\"language-text\">wetopi-docs-platform\nwetopi-docs-wordpress\nwetopi-docs-business\n</code></pre>\n<p id=\"bkmrk-pero-no-hacerlo-inic\">pero <strong>no hacerlo inicialmente</strong> sin necesidad.</p>\n<p id=\"bkmrk-un-repositorio-%C3%BAnico\">Un repositorio \u00fanico ofrece:</p>\n<ul id=\"bkmrk-b%C3%BAsqueda-global%3B-enl\">\n<li>b\u00fasqueda global;</li>\n<li>enlaces simples;</li>\n<li>menor fragmentaci\u00f3n;</li>\n<li>mejor contexto para agentes;</li>\n<li>CI m\u00e1s sencillo.</li>\n</ul>\n<hr id=\"bkmrk--68\">\n<h2 id=\"bkmrk-15.2-docs-del-propio\">15.2 Docs del propio c\u00f3digo</h2>\n<p id=\"bkmrk-la-documentaci%C3%B3n-esp\">La documentaci\u00f3n espec\u00edfica de desarrollo de un componente puede quedarse junto al c\u00f3digo.</p>\n<p id=\"bkmrk-ejemplo%3A-5\">Ejemplo:</p>\n<pre id=\"bkmrk-wetopi-agent-service\"><code class=\"language-text\">wetopi-agent-services/memory-service/README.md\n</code></pre>\n<p id=\"bkmrk-mientras-que-el-cono\">Mientras que el conocimiento institucional debe ir a:</p>\n<pre id=\"bkmrk-wetopi-docs-1\"><code class=\"language-text\">wetopi-docs\n</code></pre>\n<p id=\"bkmrk-regla%3A\">Regla:</p>\n<blockquote id=\"bkmrk-readme-explica-el-co\">\n<p>README explica el componente.<br>\n<code>wetopi-docs</code> explica c\u00f3mo Wetopi lo utiliza, opera y gobierna.</p>\n</blockquote>\n<hr id=\"bkmrk--69\">\n<h1 id=\"bkmrk-16.-modelo-de-distri\">16. Modelo de distribuci\u00f3n de tools y skills</h1>\n<p id=\"bkmrk-flujo-propuesto%3A\">Flujo propuesto:</p>\n<pre id=\"bkmrk-wetopi-ai-hub-%E2%94%82-%E2%94%8C%E2%94%80%E2%94%80%E2%94%80\"><code class=\"language-text\">                  wetopi-ai-hub\n                         \u2502\n        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n        \u2502                \u2502                \u2502\n        \u25bc                \u25bc                \u25bc\n  Hermes Developer   Hermes SRE      ADK Fleet\n        \u2502                \u2502                \u2502\n        \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                         \u2502\n                   shared contracts\n</code></pre>\n<hr id=\"bkmrk--70\">\n<h2 id=\"bkmrk-16.1-no-entregar-70-\">16.1 No entregar 70 tools al LLM</h2>\n<p id=\"bkmrk-que-existan-70-tools\">Que existan 70 tools en el AI Hub no significa que un agente deba verlas todas.</p>\n<p id=\"bkmrk-cada-agente-declara-\">Cada agente declara sus capabilities.</p>\n<p id=\"bkmrk-ejemplo%3A-6\">Ejemplo:</p>\n<pre id=\"bkmrk-agent%3A-wordpress-db-\"><code class=\"language-yaml\">agent: wordpress-db-specialist\n\ntools:\n  - memory.search\n  - work.plan\n  - work.comment\n  - wordpress.inspect_db\n  - database.query_readonly\n</code></pre>\n<hr id=\"bkmrk--71\">\n<h2 id=\"bkmrk-16.2-capabilities-po\">16.2 Capabilities por rol</h2>\n<p id=\"bkmrk-el-bundle-se-constru\">El bundle se construye seg\u00fan:</p>\n<pre id=\"bkmrk-runtime-crew-role-en\"><code class=\"language-text\">runtime\ncrew\nrole\nenvironment\n</code></pre>\n<hr id=\"bkmrk--72\">\n<h2 id=\"bkmrk-16.3-testing-central\">16.3 Testing central</h2>\n<p id=\"bkmrk-cada-tool-reutilizab\">Cada tool reutilizable debe tener:</p>\n<ul id=\"bkmrk-unit-tests%3B-contract\">\n<li>unit tests;</li>\n<li>contract tests;</li>\n<li>permission tests;</li>\n<li>safe failure behavior;</li>\n<li>structured result schema.</li>\n</ul>\n<hr id=\"bkmrk--73\">\n<h1 id=\"bkmrk-17.-relaci%C3%B3n-entre-m\">17. Relaci\u00f3n entre memoria, documentaci\u00f3n y Work Ledger</h1>\n<p id=\"bkmrk-son-tres-sistemas-di\">Son tres sistemas diferentes.</p>\n<h2 id=\"bkmrk-work-ledger\">Work Ledger</h2>\n<pre id=\"bkmrk-%C2%BFqu%C3%A9-estamos-haciend\"><code class=\"language-text\">\u00bfQu\u00e9 estamos haciendo?\n</code></pre>\n<h2 id=\"bkmrk-memory-plane\">Memory Plane</h2>\n<pre id=\"bkmrk-%C2%BFqu%C3%A9-sabemos%3F\"><code class=\"language-text\">\u00bfQu\u00e9 sabemos?\n</code></pre>\n<h2 id=\"bkmrk-documentation\">Documentation</h2>\n<pre id=\"bkmrk-%C2%BFqu%C3%A9-conocimiento-he\"><code class=\"language-text\">\u00bfQu\u00e9 conocimiento hemos decidido formalizar?\n</code></pre>\n<p id=\"bkmrk-flujo%3A\">Flujo:</p>\n<pre id=\"bkmrk-incident-%E2%86%93-ticket-%E2%86%93-\"><code class=\"language-text\">incident\n  \u2193\nticket\n  \u2193\ninvestigation\n  \u2193\nvalidated facts\n  \u2193\nMemory Plane\n  \u2193\nprocedure / ADR / postmortem\n  \u2193\nwetopi-docs\n</code></pre>\n<hr id=\"bkmrk--74\">\n<h1 id=\"bkmrk-18.-modelo-de-identi\">18. Modelo de identidad y permisos</h1>\n<p id=\"bkmrk-la-identidad-debe-se\">La identidad debe ser transversal.</p>\n<p id=\"bkmrk-ejemplo%3A-7\">Ejemplo:</p>\n<pre id=\"bkmrk-actor_id-%3D-platform-\"><code class=\"language-text\">actor_id = platform-sre-manager\n</code></pre>\n<p id=\"bkmrk-debe-aparecer-en%3A\">Debe aparecer en:</p>\n<ul id=\"bkmrk-kubernetes%3B-openbao%3B\">\n<li>Kubernetes;</li>\n<li>OpenBao;</li>\n<li>Memory Plane;</li>\n<li>Work Ledger;</li>\n<li>audit logs;</li>\n<li>observability.</li>\n</ul>\n<p id=\"bkmrk-ideal%3A\">Ideal:</p>\n<pre id=\"bkmrk-actor_id%3A-actor_type-1\"><code class=\"language-yaml\">actor_id:\nactor_type:\nruntime:\ncrew:\nrole:\nuser:\nenvironment:\n</code></pre>\n<hr id=\"bkmrk--75\">\n<h1 id=\"bkmrk-19.-modelo-de-scopes\">19. Modelo de scopes y recursos</h1>\n<p id=\"bkmrk-convenci%C3%B3n-compartid\">Convenci\u00f3n compartida:</p>\n<pre id=\"bkmrk-company%3Awetopi-crew%3A-1\"><code class=\"language-text\">company:wetopi\ncrew:platform\ncrew:development\ncrew:marketing\ncustomer:123\nsite:456\ncluster:k3s01\nnamespace:agents\nrepo:wetopi-api\nservice:foo\n</code></pre>\n<p id=\"bkmrk-puede-utilizarse-par\">Puede utilizarse para:</p>\n<ul id=\"bkmrk-acl%3B-memoria%3B-locks%3B\">\n<li>ACL;</li>\n<li>memoria;</li>\n<li>locks;</li>\n<li>visibilidad;</li>\n<li>ownership;</li>\n<li>auditor\u00eda.</li>\n</ul>\n<hr id=\"bkmrk--76\">\n<h1 id=\"bkmrk-20.-observabilidad-y\">20. Observabilidad y auditor\u00eda</h1>\n<p id=\"bkmrk-campos-comunes%3A\">Campos comunes:</p>\n<pre id=\"bkmrk-actor_id-actor_type-\"><code class=\"language-text\">actor_id\nactor_type\nruntime\ncrew\ntask_id\nrun_id\nresource\ntool\naction\nresult\nduration\ntimestamp\n</code></pre>\n<p id=\"bkmrk-ejemplo%3A-8\">Ejemplo:</p>\n<pre id=\"bkmrk-actor_id%3A-platform-s\"><code class=\"language-yaml\">actor_id: platform-sre-manager\ntask_id: INC-492\nrun_id: run-492-03\ntool: kubernetes.restart\nresource: k8s:deployment:support-manager\nresult: success\n</code></pre>\n<p id=\"bkmrk-no-registrar%3A\">No registrar:</p>\n<ul id=\"bkmrk-secrets%3B-chain-of-th\">\n<li>secrets;</li>\n<li>chain-of-thought;</li>\n<li>informaci\u00f3n privada sin necesidad.</li>\n</ul>\n<hr id=\"bkmrk--77\">\n<h1 id=\"bkmrk-21.-flujos-operativo\">21. Flujos operativos de ejemplo</h1>\n<h2 id=\"bkmrk-21.1-adk-detecta-inc\">21.1 ADK detecta incidencia</h2>\n<pre id=\"bkmrk-observer-%E2%86%93-detect-%E2%86%93-\"><code class=\"language-text\">Observer\n \u2193\ndetect\n \u2193\nWork Ledger\n \u2193\nINC-492\n \u2193\nSpecialist claims\n \u2193\nresource lease\n \u2193\ninvestigation\n \u2193\njournal\n \u2193\nfix\n \u2193\nreview\n \u2193\ndone\n</code></pre>\n<hr id=\"bkmrk--78\">\n<h2 id=\"bkmrk-21.2-hermes-intenta-\">21.2 Hermes intenta tocar un recurso ocupado</h2>\n<pre id=\"bkmrk-hermes-sre-%E2%86%93-work.pl\"><code class=\"language-text\">Hermes SRE\n \u2193\nwork.plan()\n \u2193\nconflict detected\n \u2193\nresource owned by ADK specialist\n \u2193\nHermes waits / comments / coordinates\n</code></pre>\n<hr id=\"bkmrk--79\">\n<h2 id=\"bkmrk-21.3-hermes-aprende-\">21.3 Hermes aprende un hecho de Wetopi</h2>\n<pre id=\"bkmrk-hermes-%E2%86%93-propose_fac\"><code class=\"language-text\">Hermes\n \u2193\npropose_fact\n \u2193\nMemory Service\n \u2193\nvalidation\n \u2193\nFleet Memory\n \u2193\navailable to ADK + other Hermes\n</code></pre>\n<hr id=\"bkmrk--80\">\n<h2 id=\"bkmrk-21.4-agent-uses-secr\">21.4 Agent uses secret</h2>\n<pre id=\"bkmrk-agent-%E2%86%93-tool-call-%E2%86%93-\"><code class=\"language-text\">Agent\n \u2193\ntool call\n \u2193\nTool Runtime\n \u2193\nOpenBao\n \u2193\ncredential\n \u2193\nexternal service\n\nLLM does not see credential\n</code></pre>\n<hr id=\"bkmrk--81\">\n<h2 id=\"bkmrk-21.5-agent-updates-d\">21.5 Agent updates docs</h2>\n<pre id=\"bkmrk-agent-%E2%86%93-edit-markdow\"><code class=\"language-text\">Agent\n \u2193\nedit Markdown\n \u2193\nbranch / commit / PR\n \u2193\nreview\n \u2193\nmerge\n \u2193\nMkDocs build\n \u2193\nhuman-readable website\n</code></pre>\n<hr id=\"bkmrk--82\">\n<h1 id=\"bkmrk-22.-topolog%C3%ADa-inicia\">22. Topolog\u00eda inicial de Kubernetes</h1>\n<p id=\"bkmrk-propuesta-conceptual\">Propuesta conceptual:</p>\n<pre id=\"bkmrk-kubernetes-%E2%94%82-%E2%94%9C%E2%94%80%E2%94%80-age\"><code class=\"language-text\">Kubernetes\n\u2502\n\u251c\u2500\u2500 agent-runtime/\n\u2502   \u251c\u2500\u2500 hermes-*\n\u2502   \u2514\u2500\u2500 adk-*\n\u2502\n\u251c\u2500\u2500 agent-services/\n\u2502   \u251c\u2500\u2500 memory-service\n\u2502   \u251c\u2500\u2500 work-ledger\n\u2502   \u251c\u2500\u2500 openbao\n\u2502   \u2514\u2500\u2500 docs\n\u2502\n\u251c\u2500\u2500 shared-data/\n\u2502   \u251c\u2500\u2500 postgres\n\u2502   \u2514\u2500\u2500 object-storage\n\u2502\n\u251c\u2500\u2500 observability/\n\u2502\n\u2514\u2500\u2500 business-workloads/\n</code></pre>\n<p id=\"bkmrk-los-namespaces-defin\">Los namespaces definitivos deber\u00e1n dise\u00f1arse considerando:</p>\n<ul id=\"bkmrk-seguridad%3B-networkpo\">\n<li>seguridad;</li>\n<li>NetworkPolicies;</li>\n<li>lifecycle;</li>\n<li>RBAC;</li>\n<li>ownership.</li>\n</ul>\n<hr id=\"bkmrk--83\">\n<h1 id=\"bkmrk-23.-orden-recomendad\">23. Orden recomendado de despliegue</h1>\n<h2 id=\"bkmrk-fase-0-%E2%80%94-kubernetes-\">Fase 0 \u2014 Kubernetes foundation</h2>\n<ul id=\"bkmrk-cluster%3B-networking%3B\">\n<li>cluster;</li>\n<li>networking;</li>\n<li>ingress;</li>\n<li>DNS;</li>\n<li>TLS;</li>\n<li>storage;</li>\n<li>PostgreSQL;</li>\n<li>object storage si aplica;</li>\n<li>observabilidad b\u00e1sica;</li>\n<li>GitOps.</li>\n</ul>\n<hr id=\"bkmrk--84\">\n<h2 id=\"bkmrk-fase-1-%E2%80%94-identity-%2B-\">Fase 1 \u2014 Identity + Secrets</h2>\n<p id=\"bkmrk-desplegar-openbao.\">Desplegar OpenBao.</p>\n<p id=\"bkmrk-implementar%3A\">Implementar:</p>\n<ul id=\"bkmrk-kubernetes-auth%3B-wor\">\n<li>Kubernetes Auth;</li>\n<li>workload identities;</li>\n<li>policies;</li>\n<li>audit.</li>\n</ul>\n<hr id=\"bkmrk--85\">\n<h2 id=\"bkmrk-fase-2-%E2%80%94-git-foundat\">Fase 2 \u2014 Git foundations</h2>\n<p id=\"bkmrk-crear%3A\">Crear:</p>\n<pre id=\"bkmrk-wetopi-platform-weto\"><code class=\"language-text\">wetopi-platform\nwetopi-ai-hub\nwetopi-docs\nwetopi-agents\nwetopi-agent-services\n</code></pre>\n<p id=\"bkmrk-a%C3%B1adir%3A\">A\u00f1adir:</p>\n<ul id=\"bkmrk-branch-protection%3B-c\">\n<li>branch protection;</li>\n<li>CI;</li>\n<li>CODEOWNERS;</li>\n<li>versioning;</li>\n<li>release conventions.</li>\n</ul>\n<hr id=\"bkmrk--86\">\n<h2 id=\"bkmrk-fase-3-%E2%80%94-memory-plan\">Fase 3 \u2014 Memory Plane</h2>\n<p id=\"bkmrk-productivizar-la-mem\">Productivizar la memoria existente.</p>\n<p id=\"bkmrk-a%C3%B1adir%3A-1\">A\u00f1adir:</p>\n<ul id=\"bkmrk-api-estable%3B-scopes%3B\">\n<li>API estable;</li>\n<li>scopes;</li>\n<li>provenance;</li>\n<li>candidate facts;</li>\n<li>contradiction handling;</li>\n<li>TTL;</li>\n<li>history.</li>\n</ul>\n<hr id=\"bkmrk--87\">\n<h2 id=\"bkmrk-fase-4-%E2%80%94-work-coordi\">Fase 4 \u2014 Work Coordination Plane</h2>\n<p id=\"bkmrk-seleccionar-backend.\">Seleccionar backend.</p>\n<p id=\"bkmrk-crear-el-adapter%2Fcon\">Crear el adapter/contract propio.</p>\n<p id=\"bkmrk-implementar%3A-1\">Implementar:</p>\n<ul id=\"bkmrk-actors%3B-tickets%3B-res\">\n<li>actors;</li>\n<li>tickets;</li>\n<li>resource scopes;</li>\n<li>conflict detection;</li>\n<li>leases;</li>\n<li>intents;</li>\n<li>journal.</li>\n</ul>\n<hr id=\"bkmrk--88\">\n<h2 id=\"bkmrk-fase-5-%E2%80%94-documentati\">Fase 5 \u2014 Documentation Plane</h2>\n<p id=\"bkmrk-crear-wetopi-docs.\">Crear <code>wetopi-docs</code>.</p>\n<p id=\"bkmrk-desplegar%3A\">Desplegar:</p>\n<ul id=\"bkmrk-mkdocs-material%3B-ci%3B\">\n<li>MkDocs Material;</li>\n<li>CI;</li>\n<li>static hosting.</li>\n</ul>\n<p id=\"bkmrk-migrar-progresivamen-1\">Migrar progresivamente documentaci\u00f3n \u00fatil existente.</p>\n<hr id=\"bkmrk--89\">\n<h2 id=\"bkmrk-fase-6-%E2%80%94-wetopi-ai-h\">Fase 6 \u2014 wetopi-ai-hub</h2>\n<p id=\"bkmrk-consolidar-tools-hoy\">Consolidar tools hoy dispersas.</p>\n<p id=\"bkmrk-prioridad-inicial%3A\">Prioridad inicial:</p>\n<pre id=\"bkmrk-tools%2Fmemory-tools%2Fw\"><code class=\"language-text\">tools/memory\ntools/workledger\ntools/kubernetes\ntools/ceph\ntools/wordpress\n</code></pre>\n<p id=\"bkmrk-definir-contratos-es\">Definir contratos estables.</p>\n<hr id=\"bkmrk--90\">\n<h2 id=\"bkmrk-fase-7-%E2%80%94-hermes\">Fase 7 \u2014 Hermes</h2>\n<p id=\"bkmrk-desplegar-un-hermes-\">Desplegar un Hermes por empleado seg\u00fan necesidad.</p>\n<p id=\"bkmrk-cada-hermes-tendr%C3%A1%3A\">Cada Hermes tendr\u00e1:</p>\n<pre id=\"bkmrk-personal-local-memor\"><code class=\"language-text\">personal local memory\n+\nWetopi Fleet Memory\n+\nWork Ledger\n+\nrole-specific tools\n+\ndocumentation access\n</code></pre>\n<hr id=\"bkmrk--91\">\n<h2 id=\"bkmrk-fase-8-%E2%80%94-adk-fleet\">Fase 8 \u2014 ADK Fleet</h2>\n<p id=\"bkmrk-desplegar-observers%2C\">Desplegar observers, specialists y managers.</p>\n<p id=\"bkmrk-todos-usan%3A\">Todos usan:</p>\n<ul id=\"bkmrk-memory-plane%3B-work-l\">\n<li>Memory Plane;</li>\n<li>Work Ledger;</li>\n<li>OpenBao;</li>\n<li>Docs;</li>\n<li>AI Hub;</li>\n<li>observabilidad com\u00fan.</li>\n</ul>\n<hr id=\"bkmrk--92\">\n<h2 id=\"bkmrk-fase-9-%E2%80%94-construcci%C3%B3\">Fase 9 \u2014 Construcci\u00f3n del resto de Wetopi</h2>\n<p id=\"bkmrk-con-los-servicios-ag\">Con los servicios ag\u00e9nticos ya operativos, comenzar la migraci\u00f3n/reconstrucci\u00f3n de los dem\u00e1s workloads de Wetopi.</p>\n<p id=\"bkmrk-los-agentes-particip\">Los agentes participan desde el inicio en:</p>\n<ul id=\"bkmrk-discovery%3B-documenta\">\n<li>discovery;</li>\n<li>documentaci\u00f3n;</li>\n<li>migration;</li>\n<li>testing;</li>\n<li>operaciones;</li>\n<li>observabilidad;</li>\n<li>incident response.</li>\n</ul>\n<hr id=\"bkmrk--93\">\n<h1 id=\"bkmrk-24.-decisiones-conso\">24. Decisiones consolidadas</h1>\n<h2 id=\"bkmrk-d-001-%E2%80%94-kubernetes-c\">D-001 \u2014 Kubernetes como plataforma base</h2>\n<p id=\"bkmrk-la-nueva-wetopi-se-r\">La nueva Wetopi se reconstruir\u00e1 sobre Kubernetes.</p>\n<hr id=\"bkmrk--94\">\n<h2 id=\"bkmrk-d-002-%E2%80%94-ai-first\">D-002 \u2014 AI-first</h2>\n<p id=\"bkmrk-los-sistemas-ag%C3%A9ntic\">Los sistemas ag\u00e9nticos se despliegan antes que el resto de la plataforma.</p>\n<hr id=\"bkmrk--95\">\n<h2 id=\"bkmrk-d-003-%E2%80%94-hermes-%2B-adk\">D-003 \u2014 Hermes + ADK</h2>\n<p id=\"bkmrk-se-combinar%C3%A1n%3A\">Se combinar\u00e1n:</p>\n<ul id=\"bkmrk-hermes-como-asistent\">\n<li>Hermes como asistente personal/coding por empleado;</li>\n<li>ADK para la flota standalone y especializada.</li>\n</ul>\n<hr id=\"bkmrk--96\">\n<h2 id=\"bkmrk-d-004-%E2%80%94-memoria-dual\">D-004 \u2014 Memoria dual</h2>\n<p id=\"bkmrk-hermes-utilizar%C3%A1%3A\">Hermes utilizar\u00e1:</p>\n<pre id=\"bkmrk-local-personal-memor\"><code class=\"language-text\">local personal memory\n+\nWetopi Fleet Memory\n</code></pre>\n<p id=\"bkmrk-los-adk-utilizar%C3%A1n-p\">Los ADK utilizar\u00e1n principalmente Fleet Memory m\u00e1s state/session cuando corresponda.</p>\n<hr id=\"bkmrk--97\">\n<h2 id=\"bkmrk-d-005-%E2%80%94-memoria-inst\">D-005 \u2014 Memoria institucional propia</h2>\n<p id=\"bkmrk-se-reutilizar%C3%A1-y-evo\">Se reutilizar\u00e1 y evolucionar\u00e1 el Memory Plane ya creado internamente.</p>\n<hr id=\"bkmrk--98\">\n<h2 id=\"bkmrk-d-006-%E2%80%94-integraci%C3%B3n-\">D-006 \u2014 Integraci\u00f3n de Hermes mediante Memory Provider</h2>\n<p id=\"bkmrk-el-retrieval-global-\">El retrieval global se integrar\u00e1 mediante el mecanismo de provider de Hermes.</p>\n<p id=\"bkmrk-no-se-volcar%C3%A1n-conve\">No se volcar\u00e1n conversaciones personales autom\u00e1ticamente a Fleet Memory.</p>\n<hr id=\"bkmrk--99\">\n<h2 id=\"bkmrk-d-007-%E2%80%94-openbao\">D-007 \u2014 OpenBao</h2>\n<p id=\"bkmrk-openbao-ser%C3%A1-el-cand\">OpenBao ser\u00e1 el candidato principal como secrets manager central.</p>\n<hr id=\"bkmrk--100\">\n<h2 id=\"bkmrk-d-008-%E2%80%94-secret-isola\">D-008 \u2014 Secret isolation</h2>\n<p id=\"bkmrk-las-tools-consumir%C3%A1n\">Las tools consumir\u00e1n secrets sin introducirlos en contexto LLM siempre que sea posible.</p>\n<hr id=\"bkmrk--101\">\n<h2 id=\"bkmrk-d-009-%E2%80%94-work-ledger\">D-009 \u2014 Work Ledger</h2>\n<p id=\"bkmrk-el-kanban-se-conside\">El Kanban se considera UI de un sistema m\u00e1s profundo de coordinaci\u00f3n.</p>\n<hr id=\"bkmrk--102\">\n<h2 id=\"bkmrk-d-010-%E2%80%94-backend-desa\">D-010 \u2014 Backend desacoplado</h2>\n<p id=\"bkmrk-muster%2C-plane-o-alte\">Muster, Plane o alternativa se utilizar\u00e1n detr\u00e1s de un contrato Wetopi.</p>\n<hr id=\"bkmrk--103\">\n<h2 id=\"bkmrk-d-011-%E2%80%94-markdown-com\">D-011 \u2014 Markdown como formato documental</h2>\n<p id=\"bkmrk-markdown-%2B-git-ser%C3%A1-\">Markdown + Git ser\u00e1 el source of truth.</p>\n<hr id=\"bkmrk--104\">\n<h2 id=\"bkmrk-d-012-%E2%80%94-mkdocs-mater\">D-012 \u2014 MkDocs Material</h2>\n<p id=\"bkmrk-se-utilizar%C3%A1-como-pr\">Se utilizar\u00e1 como primera opci\u00f3n para renderizado web de documentaci\u00f3n.</p>\n<hr id=\"bkmrk--105\">\n<h2 id=\"bkmrk-d-013-%E2%80%94-ai-hub\">D-013 \u2014 AI Hub</h2>\n<p id=\"bkmrk-se-crear%C3%A1-wetopi-ai-\">Se crear\u00e1 <code>wetopi-ai-hub</code> como repositorio com\u00fan de:</p>\n<ul id=\"bkmrk-skills%3B-tools%3B-compo\">\n<li>skills;</li>\n<li>tools;</li>\n<li>components;</li>\n<li>contracts;</li>\n<li>schemas;</li>\n<li>SDKs;</li>\n<li>evals.</li>\n</ul>\n<hr id=\"bkmrk--106\">\n<h2 id=\"bkmrk-d-014-%E2%80%94-tools-compar\">D-014 \u2014 Tools compartidas centralizadas</h2>\n<p id=\"bkmrk-una-tool-usada-por-m\">Una tool usada por m\u00e1s de un agente debe trasladarse al AI Hub.</p>\n<hr id=\"bkmrk--107\">\n<h2 id=\"bkmrk-d-015-%E2%80%94-gitops\">D-015 \u2014 GitOps</h2>\n<p id=\"bkmrk-la-infraestructura-k\">La infraestructura Kubernetes debe quedar declarada en Git.</p>\n<hr id=\"bkmrk--108\">\n<h1 id=\"bkmrk-25.-riesgos-y-contro\">25. Riesgos y controles</h1>\n<h2 id=\"bkmrk-25.1-contaminaci%C3%B3n-d\">25.1 Contaminaci\u00f3n de memoria</h2>\n<p id=\"bkmrk-riesgo%3A-agentes-alma\"><strong>Riesgo:</strong> agentes almacenan hip\u00f3tesis como hechos.</p>\n<p id=\"bkmrk-control%3A\"><strong>Control:</strong></p>\n<ul id=\"bkmrk-candidate-facts%3B-pro\">\n<li>candidate facts;</li>\n<li>provenance;</li>\n<li>confidence;</li>\n<li>validation;</li>\n<li>source authority.</li>\n</ul>\n<hr id=\"bkmrk--109\">\n<h2 id=\"bkmrk-25.2-conflictos-entr\">25.2 Conflictos entre agentes</h2>\n<p id=\"bkmrk-riesgo%3A-varios-agent\"><strong>Riesgo:</strong> varios agentes modifican el mismo recurso.</p>\n<p id=\"bkmrk-control%3A-1\"><strong>Control:</strong></p>\n<ul id=\"bkmrk-work-ledger%3B-scopes%3B\">\n<li>Work Ledger;</li>\n<li>scopes;</li>\n<li>leases;</li>\n<li>intents;</li>\n<li>conflict check.</li>\n</ul>\n<hr id=\"bkmrk--110\">\n<h2 id=\"bkmrk-25.3-secrets-en-cont\">25.3 Secrets en contexto</h2>\n<p id=\"bkmrk-riesgo%3A-token-termin\"><strong>Riesgo:</strong> token termina en prompt/log/trace.</p>\n<p id=\"bkmrk-control%3A-2\"><strong>Control:</strong></p>\n<ul id=\"bkmrk-secret-retrieval-den\">\n<li>secret retrieval dentro de tool runtime;</li>\n<li>dynamic secrets;</li>\n<li>redaction;</li>\n<li>audit.</li>\n</ul>\n<hr id=\"bkmrk--111\">\n<h2 id=\"bkmrk-25.4-tool-sprawl\">25.4 Tool sprawl</h2>\n<p id=\"bkmrk-riesgo%3A-cada-agente-\"><strong>Riesgo:</strong> cada agente implementa su versi\u00f3n.</p>\n<p id=\"bkmrk-control%3A-3\"><strong>Control:</strong></p>\n<ul id=\"bkmrk-wetopi-ai-hub%3B-contr\">\n<li><code>wetopi-ai-hub</code>;</li>\n<li>contracts;</li>\n<li>versioning;</li>\n<li>tests.</li>\n</ul>\n<hr id=\"bkmrk--112\">\n<h2 id=\"bkmrk-25.5-context-overloa\">25.5 Context overload</h2>\n<p id=\"bkmrk-riesgo%3A-demasiadas-t\"><strong>Riesgo:</strong> demasiadas tools disponibles para un LLM.</p>\n<p id=\"bkmrk-control%3A-4\"><strong>Control:</strong></p>\n<ul id=\"bkmrk-bundles-por-rol%3B-too\">\n<li>bundles por rol;</li>\n<li>tool routing;</li>\n<li>APIs sem\u00e1nticas de alto nivel.</li>\n</ul>\n<hr id=\"bkmrk--113\">\n<h2 id=\"bkmrk-25.6-documentaci%C3%B3n-o\">25.6 Documentaci\u00f3n obsoleta</h2>\n<p id=\"bkmrk-riesgo%3A-los-agentes-\"><strong>Riesgo:</strong> los agentes conf\u00edan en docs antiguas.</p>\n<p id=\"bkmrk-control%3A-5\"><strong>Control:</strong></p>\n<ul id=\"bkmrk-git%3B-ownership%3B-revi\">\n<li>Git;</li>\n<li>ownership;</li>\n<li>review dates;</li>\n<li>links con fuentes autoritativas;</li>\n<li>documentaci\u00f3n vs factual memory claramente diferenciadas.</li>\n</ul>\n<hr id=\"bkmrk--114\">\n<h1 id=\"bkmrk-26.-cuestiones-abier\">26. Cuestiones abiertas</h1>\n<p id=\"bkmrk-a%C3%BAn-deben-decidirse%3A\">A\u00fan deben decidirse:</p>\n<ol id=\"bkmrk-backend-final-del-wo\">\n<li>\n<p>Backend final del Work Coordination Plane:</p>\n<ul>\n<li>Muster;</li>\n<li>Plane;</li>\n<li>It's a Plan;</li>\n<li>alternativa.</li>\n</ul>\n</li>\n<li>\n<p>Authentication humana central:</p>\n<ul>\n<li>OIDC provider existente;</li>\n<li>SSO objetivo.</li>\n</ul>\n</li>\n<li>\n<p>Modelo exacto de namespaces Kubernetes.</p>\n</li>\n<li>\n<p>Storage definitivo de Memory Plane.</p>\n</li>\n<li>\n<p>Estrategia de embeddings/reranking del Memory Plane.</p>\n</li>\n<li>\n<p>Convenci\u00f3n exacta de nombres de scopes.</p>\n</li>\n<li>\n<p>Si <code>wetopi-agents</code> permanece monorepo o se divide cuando crezca.</p>\n</li>\n<li>\n<p>Packaging definitivo de <code>wetopi-ai-hub</code>.</p>\n</li>\n<li>\n<p>CI/CD y GitOps concretos.</p>\n</li>\n<li>\n<p>Modelo de aprobaci\u00f3n humana para acciones de alto riesgo.</p>\n</li>\n</ol>\n<hr id=\"bkmrk--115\">\n<h1 id=\"bkmrk-27.-resultado-espera\">27. Resultado esperado</h1>\n<p id=\"bkmrk-al-finalizar-la-prim\">Al finalizar la primera etapa, Wetopi deber\u00eda disponer de una base parecida a:</p>\n<pre id=\"bkmrk-wetopi-ai-first-foun\"><code class=\"language-text\">                   WETOPI AI-FIRST FOUNDATION\n\n                           Humans\n                              \u2502\n                \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                \u25bc             \u25bc             \u25bc\n              Hermes       Kanban         Docs\n                \u2502             \u2502             \u2502\n                \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                       \u2502             \u2502\n                 Work Ledger     Fleet Memory\n                       \u2502             \u2502\n                       \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                              \u2502\n                         wetopi-ai-hub\n                              \u2502\n           \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n           \u25bc                  \u25bc                  \u25bc\n        Hermes Fleet      ADK Fleet        Future Agents\n           \u2502                  \u2502\n           \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                              \u2502\n                           OpenBao\n                              \u2502\n                           Kubernetes\n                              \u2502\n                   \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                   \u25bc          \u25bc          \u25bc\n                  Infra     Hosting    Business\n</code></pre>\n<p id=\"bkmrk-la-consecuencia-m%C3%A1s-\">La consecuencia m\u00e1s importante es que Wetopi no tendr\u00e1 simplemente \"algunos agentes\".</p>\n<p id=\"bkmrk-la-organizaci%C3%B3n-disp\">La organizaci\u00f3n dispondr\u00e1 de una <strong>infraestructura operacional dise\u00f1ada para humanos y agentes simult\u00e1neamente</strong>:</p>\n<ul id=\"bkmrk-conocimiento-institu\">\n<li>conocimiento institucional compartido;</li>\n<li>trabajo coordinado;</li>\n<li>secrets gobernados;</li>\n<li>documentaci\u00f3n agent-native;</li>\n<li>tools y skills reutilizables;</li>\n<li>identidad y auditor\u00eda comunes;</li>\n<li>repositorios definidos;</li>\n<li>capacidad de incorporar nuevos runtimes sin redise\u00f1ar la plataforma.</li>\n</ul>\n<p id=\"bkmrk-%C3%89sta-ser%C3%A1-la-base-so\">\u00c9sta ser\u00e1 la base sobre la que reconstruir el resto de Wetopi.</p>\n"
 },
 {
  "id": 55,
  "title": "bootstrap plan",
  "slug": "bootstrap-plan",
  "book_id": 8,
  "chapter_id": 29,
  "description": null,
  "created_at": "2026-09-06T13:39:01.000000Z",
  "updated_at": "2026-09-06T13:39:01.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (design/bootstrap-plan.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n# Wetopi Platform Bootstrap Plan\n## Plan de arranque de la nueva plataforma RKE2 y organizaci\u00f3n GitHub\n\n**Fecha:** 28 de agosto de 2026  \n**Estado:** Plan de bootstrap consolidado  \n**Organizaci\u00f3n GitHub:** `wetopi-platform`  \n**\u00c1mbito:** Creaci\u00f3n de repositorios, bootstrap de infraestructura, RKE2, servicios base y posterior incorporaci\u00f3n de agentes y workloads.\n\n---\n\n# \u00cdndice\n\n1. Resumen ejecutivo\n2. Objetivo del bootstrap\n3. Decisiones ya tomadas\n4. Organizaci\u00f3n GitHub\n5. Repositorios iniciales\n6. Git como source of truth\n7. OpenTofu vs Terraform\n8. Modelo de infraestructura inicial\n9. VM externa de operaciones: `wetopi-ops01`\n10. Control plane RKE2\n11. Platform workers\n12. Distribuci\u00f3n sobre Proxmox\n13. Networking de las VMs\n14. Red Ceph existente\n15. RKE2 y etcd\n16. Bootstrap desde fuera del cluster\n17. Rol inicial de Codex\n18. Cu\u00e1ndo desplegar Hermes\n19. Orden de despliegue de servicios base\n20. Cilium y Hubble\n21. Ceph CSI\n22. GitOps\n23. OpenBao\n24. Documentation Plane\n25. Memory Plane\n26. Work Coordination Plane\n27. AI Hub\n28. Agentes Hermes y ADK\n29. Edge nodes\n30. Hosting workers\n31. Migraci\u00f3n progresiva desde Swarm\n32. Permisos y credenciales\n33. Primeros encargos para Codex\n34. Fases del bootstrap\n35. Checklist de arranque\n36. Resultado esperado\n\n---\n\n# 1. Resumen ejecutivo\n\nLa nueva plataforma de Wetopi se construir\u00e1 desde cero, en paralelo a la infraestructura actual basada en Docker Swarm.\n\nNo se realizar\u00e1 una migraci\u00f3n in-place.\n\nLa nueva base ser\u00e1:\n\n```text\nProxmox\n  \u2193\nVMs Ubuntu\n  \u2193\nRKE2\n  \u2193\nCilium / Hubble\n  \u2193\nCeph CSI\n  \u2193\nGitOps\n  \u2193\nOpenBao\n  \u2193\nDocs / Memory / Work Ledger\n  \u2193\nHermes + ADK\n  \u2193\nEdge\n  \u2193\nHosting workloads\n```\n\nEl bootstrap inicial se har\u00e1 **desde fuera de Kubernetes**.\n\nDurante esta primera etapa:\n\n- Codex trabajar\u00e1 desde el laptop del operador;\n- GitHub ser\u00e1 el source of truth;\n- existir\u00e1 una VM externa `wetopi-ops01`;\n- se crear\u00e1n 3 control planes RKE2;\n- se crear\u00e1n 3 platform workers;\n- no se desplegar\u00e1n a\u00fan workloads de customers;\n- no se tocar\u00e1 la red Ceph existente `192.168.96.0/20`;\n- cada VM tendr\u00e1 una \u00fanica interfaz LAN;\n- Hermes se desplegar\u00e1 despu\u00e9s, ya dentro del cluster definitivo.\n\nLa organizaci\u00f3n GitHub ser\u00e1:\n\n```text\nwetopi-platform\n```\n\ny los repositorios iniciales ser\u00e1n:\n\n```text\nwetopi-platform/infrastructure\nwetopi-platform/agent-services\nwetopi-platform/ai-hub\nwetopi-platform/agents\nwetopi-platform/docs\n```\n\n---\n\n# 2. Objetivo del bootstrap\n\nEl objetivo de esta fase no es migrar inmediatamente los ~700 contenedores actuales.\n\nEl objetivo es construir una **foundation nueva, reproducible y segura** sobre la que posteriormente se pueda migrar Wetopi.\n\nEl milestone inicial ser\u00e1:\n\n> disponer de un cluster RKE2 HA vac\u00edo, estable, observable, con storage, secrets, GitOps y servicios ag\u00e9nticos comunes funcionando antes de mover tr\u00e1fico de clientes.\n\n---\n\n# 3. Decisiones ya tomadas\n\n## Plataforma Kubernetes\n\n`RKE2`\n\n## CNI\n\n`Cilium`\n\n## Observabilidad de red\n\n`Hubble`\n\n## Storage\n\n`Ceph existente mediante CSI`\n\n## Secrets\n\n`OpenBao`\n\n## Documentaci\u00f3n\n\n`Markdown + Git + MkDocs Material`\n\n## Infraestructura declarativa\n\n`OpenTofu + Ansible`\n\n## Bootstrap\n\n`Codex desde laptop + wetopi-ops01`\n\n## Agentes\n\n`Hermes + ADK`\n\nLos agentes se desplegar\u00e1n posteriormente dentro del cluster definitivo.\n\n---\n\n# 4. Organizaci\u00f3n GitHub\n\nSe adopta como organizaci\u00f3n:\n\n```text\ngithub.com/wetopi-platform\n```\n\nSe descartan nombres temporales como `wetopi3`, `wetopi-v3` o `wetopi-new` porque describen una migraci\u00f3n, no una plataforma duradera.\n\n`wetopi-platform` se considera adecuado como namespace t\u00e9cnico de largo plazo.\n\n---\n\n# 5. Repositorios iniciales\n\nSe crear\u00e1n inicialmente **cinco repositorios**.\n\n## 5.1 `infrastructure`\n\nResponsabilidad:\n\n- Proxmox;\n- OpenTofu;\n- Ansible;\n- RKE2;\n- networking base;\n- Cilium;\n- Hubble;\n- Ceph CSI;\n- OpenBao deployment;\n- GitOps;\n- observabilidad base;\n- edge;\n- NATGW;\n- bootstrap;\n- disaster recovery.\n\nEstructura propuesta:\n\n```text\ninfrastructure/\n\u251c\u2500\u2500 tofu/\n\u2502   \u2514\u2500\u2500 proxmox/\n\u2502       \u251c\u2500\u2500 modules/\n\u2502       \u2514\u2500\u2500 environments/\n\u2502           \u2514\u2500\u2500 prod/\n\u251c\u2500\u2500 ansible/\n\u2502   \u251c\u2500\u2500 inventories/\n\u2502   \u251c\u2500\u2500 roles/\n\u2502   \u2502   \u251c\u2500\u2500 common/\n\u2502   \u2502   \u251c\u2500\u2500 rke2-server/\n\u2502   \u2502   \u251c\u2500\u2500 rke2-agent/\n\u2502   \u2502   \u251c\u2500\u2500 edge/\n\u2502   \u2502   \u2514\u2500\u2500 natgw/\n\u2502   \u2514\u2500\u2500 playbooks/\n\u251c\u2500\u2500 kubernetes/\n\u2502   \u251c\u2500\u2500 bootstrap/\n\u2502   \u251c\u2500\u2500 cilium/\n\u2502   \u251c\u2500\u2500 hubble/\n\u2502   \u251c\u2500\u2500 ceph-csi/\n\u2502   \u251c\u2500\u2500 openbao/\n\u2502   \u251c\u2500\u2500 gitops/\n\u2502   \u251c\u2500\u2500 namespaces/\n\u2502   \u2514\u2500\u2500 observability/\n\u251c\u2500\u2500 scripts/\n\u2514\u2500\u2500 docs/\n    \u251c\u2500\u2500 bootstrap.md\n    \u251c\u2500\u2500 networking.md\n    \u2514\u2500\u2500 disaster-recovery.md\n```\n\n## 5.2 `agent-services`\n\nServicios compartidos de la capa ag\u00e9ntica:\n\n```text\nmemory-service/\nwork-ledger-adapter/\nhermes-memory-provider/\nidentity/\napproval/\n```\n\nInicialmente se priorizar\u00e1n `memory-service`, `hermes-memory-provider` y `work-ledger-adapter`.\n\n## 5.3 `ai-hub`\n\nRepositorio com\u00fan de:\n\n- skills;\n- tools;\n- components;\n- contracts;\n- schemas;\n- SDKs;\n- evals;\n- tests.\n\nRegla:\n\n> una tool utilizada por m\u00e1s de un agente debe vivir en `ai-hub`.\n\n## 5.4 `agents`\n\nMonorepo inicial de la flota:\n\n```text\nagents/\n\u251c\u2500\u2500 hermes/\n\u2502   \u251c\u2500\u2500 sre/\n\u2502   \u251c\u2500\u2500 developer/\n\u2502   \u2514\u2500\u2500 marketing/\n\u251c\u2500\u2500 adk/\n\u2502   \u251c\u2500\u2500 platform/\n\u2502   \u251c\u2500\u2500 wordpress/\n\u2502   \u2514\u2500\u2500 support/\n\u251c\u2500\u2500 base/\n\u251c\u2500\u2500 deployments/\n\u2514\u2500\u2500 tests/\n```\n\n## 5.5 `docs`\n\nSource of truth documental:\n\n```text\ndocs/\n\u251c\u2500\u2500 mkdocs.yml\n\u2514\u2500\u2500 docs/\n    \u251c\u2500\u2500 architecture/\n    \u251c\u2500\u2500 infrastructure/\n    \u251c\u2500\u2500 kubernetes/\n    \u251c\u2500\u2500 networking/\n    \u251c\u2500\u2500 wordpress/\n    \u251c\u2500\u2500 agents/\n    \u251c\u2500\u2500 procedures/\n    \u251c\u2500\u2500 runbooks/\n    \u251c\u2500\u2500 security/\n    \u2514\u2500\u2500 adr/\n```\n\n---\n\n# 6. Git como source of truth\n\nDesde el primer d\u00eda:\n\n```text\nGit\n  \u2193\nreview\n  \u2193\nautomation\n  \u2193\ninfrastructure\n```\n\nLa shell history no ser\u00e1 source of truth.\n\nTodo cambio relevante debe terminar en Git.\n\n---\n\n# 7. OpenTofu vs Terraform\n\nSe selecciona inicialmente `OpenTofu` por:\n\n- licencia open source;\n- gobernanza Linux Foundation;\n- evitar dependencia BSL;\n- compatibilidad con el ecosistema Terraform;\n- idoneidad para un proyecto interno de largo plazo.\n\nTerraform seguir\u00eda siendo t\u00e9cnicamente v\u00e1lido.\n\n---\n\n# 8. Modelo de infraestructura inicial\n\nSe crear\u00e1n inicialmente:\n\n```text\n1 x ops VM\n3 x RKE2 control plane\n3 x platform workers\n```\n\nTotal:\n\n```text\n7 VMs\n```\n\nNo se crear\u00e1n todav\u00eda hosting workers masivos ni edge productivos.\n\n---\n\n# 9. VM externa de operaciones: `wetopi-ops01`\n\nSe crear\u00e1 `wetopi-ops01` fuera del cluster Kubernetes.\n\nObjetivo:\n\n> bootstrap host, bastion administrativo y punto de recuperaci\u00f3n cuando Kubernetes no est\u00e9 disponible.\n\nSizing orientativo:\n\n```text\n4 vCPU\n8 GB RAM\n40-80 GB disk\nUbuntu 24.04\n1 NIC LAN\n```\n\nHerramientas:\n\n```text\ngit\nssh\nansible\nopentofu\nkubectl\nhelm\njq\nyq\nrke2 tooling\n```\n\nNo alojar\u00e1 workloads productivos.\n\nDebe permitir operar si fallan Cilium, GitOps, OpenBao o parte del cluster.\n\n---\n\n# 10. Control plane RKE2\n\nSe desplegar\u00e1n:\n\n```text\nrke-cp01\nrke-cp02\nrke-cp03\n```\n\nCada uno en un host Proxmox f\u00edsico diferente siempre que sea posible.\n\nSizing orientativo:\n\n```text\n8 vCPU\n16 GB RAM\n50-100 GB disk\nUbuntu 24.04\n1 NIC LAN\n```\n\n---\n\n# 11. Platform workers\n\nSe desplegar\u00e1n:\n\n```text\nrke-platform01\nrke-platform02\nrke-platform03\n```\n\nSizing orientativo:\n\n```text\n8-16 vCPU\n16-32 GB RAM\nUbuntu 24.04\n1 NIC LAN\n```\n\nLabel:\n\n```text\nwetopi.io/role=platform\n```\n\n---\n\n# 12. Distribuci\u00f3n sobre Proxmox\n\nEjemplo:\n\n```text\npve01 -> rke-cp01 + rke-platform01\npve02 -> rke-cp02 + rke-platform02\npve03 -> rke-cp03 + rke-platform03\n```\n\nLa distribuci\u00f3n real debe respetar failure domains y recursos disponibles.\n\n---\n\n# 13. Networking de las VMs\n\nCada VM tendr\u00e1 **una \u00fanica interfaz LAN**.\n\n```text\nVM\n \u2514\u2500\u2500 eth0\n      \u2514\u2500\u2500 Wetopi LAN / vRack\n```\n\nLos rangos Kubernetes ser\u00e1n independientes:\n\n```text\nVM LAN != Pod CIDR != Service CIDR != Ceph network\n```\n\nLos rangos concretos deben definirse tras inventariar las redes actuales.\n\n---\n\n# 14. Red Ceph existente\n\nExiste una red Ceph:\n\n```text\n192.168.96.0/20\n```\n\nque forma parte de la infraestructura hiperconvergida Proxmox + Ceph.\n\nDecisi\u00f3n:\n\n> NO tocar, renumerar ni redise\u00f1ar esta red durante el bootstrap RKE2.\n\nRKE2 consumir\u00e1 Ceph mediante CSI adapt\u00e1ndose a la infraestructura existente.\n\n---\n\n# 15. RKE2 y etcd\n\nSe utilizar\u00e1 `embedded etcd` sobre los tres server nodes.\n\n```text\ncp01 etcd\ncp02 etcd\ncp03 etcd\n```\n\nQuorum: 2 de 3.\n\nSe priorizar\u00e1 almacenamiento de baja latencia para etcd. Debe evaluarse local NVMe si la alternativa Ceph introduce latencia innecesaria.\n\nLos snapshots de etcd deber\u00e1n almacenarse fuera de los propios nodos y el procedimiento de restore deber\u00e1 probarse.\n\n---\n\n# 16. Bootstrap desde fuera del cluster\n\nModelo:\n\n```text\nLaptop\n  \u2502\nCodex\n  \u2502\nGitHub\n  \u2502\nops01\n  \u2502\nOpenTofu + Ansible\n  \u2502\nProxmox / RKE2\n```\n\nKubernetes no depender\u00e1 de agentes internos para poder crearse o repararse.\n\n---\n\n# 17. Rol inicial de Codex\n\nDurante el bootstrap Codex act\u00faa como `bootstrap engineer`.\n\nTrabajar\u00e1 contra:\n\n- GitHub;\n- repos;\n- `ops01`;\n- Proxmox con credenciales limitadas;\n- posteriormente Kubernetes.\n\nTodo cambio relevante debe quedar versionado.\n\n---\n\n# 18. Cu\u00e1ndo desplegar Hermes\n\nNo se desplegar\u00e1n varios Hermes temporales antes del cluster.\n\nLos Hermes definitivos se desplegar\u00e1n dentro de RKE2 cuando existan al menos:\n\n```text\nRKE2 estable\nCilium\nHubble\nCeph CSI\nGitOps\nOpenBao\nDocumentation\nMemory Plane\nWork Ledger\n```\n\nDespu\u00e9s:\n\n```text\nHermes SRE\nHermes Developer\nHermes Marketing\n```\n\n---\n\n# 19. Orden de despliegue de servicios base\n\n```text\nRKE2\n \u2193\nCilium\n \u2193\nHubble\n \u2193\nCeph CSI\n \u2193\nGitOps\n \u2193\nOpenBao\n \u2193\nDocumentation\n \u2193\nMemory Plane\n \u2193\nWork Ledger\n \u2193\nAI Hub integration\n \u2193\nHermes\n \u2193\nADK Fleet\n```\n\n---\n\n# 20. Cilium y Hubble\n\nCilium ser\u00e1 el CNI base.\n\nInicialmente:\n\n- networking;\n- NetworkPolicy;\n- Hubble;\n- observabilidad.\n\nNo se activar\u00e1n desde el d\u00eda uno BGP WAN, egress complejo o service mesh.\n\n---\n\n# 21. Ceph CSI\n\nPruebas m\u00ednimas antes de customer workloads:\n\n```text\ncrear PVC\nmontar PVC\nborrar Pod\nrecrear Pod\nvalidar persistencia\nsnapshot si aplica\n```\n\n---\n\n# 22. GitOps\n\nEl cluster evolucionar\u00e1 hacia:\n\n```text\nGit -> reconciler -> Kubernetes\n```\n\nLa herramienta concreta puede decidirse durante el bootstrap.\n\n---\n\n# 23. OpenBao\n\nOpenBao ser\u00e1 el secrets manager central.\n\nConfigurar:\n\n- HA;\n- persistence;\n- Kubernetes Auth;\n- audit;\n- policies;\n- backups.\n\n---\n\n# 24. Documentation Plane\n\nRepo:\n\n```text\nwetopi-platform/docs\n```\n\nRender:\n\n```text\nMkDocs Material\n```\n\nMarkdown ser\u00e1 el source of truth.\n\n---\n\n# 25. Memory Plane\n\nLa memoria institucional propia de Wetopi se productivizar\u00e1 como servicio central para Hermes, ADK y futuros agentes.\n\nDebe soportar:\n\n- factual memory;\n- scopes;\n- provenance;\n- confidence;\n- temporalidad;\n- contradictions;\n- candidate facts.\n\n---\n\n# 26. Work Coordination Plane\n\nSe desplegar\u00e1 un backend Kanban/ticketing desacoplado mediante contrato Wetopi.\n\nResponsabilidades:\n\n- tareas;\n- ownership;\n- resource scopes;\n- leases;\n- intent;\n- journal;\n- coordinaci\u00f3n entre humanos y agentes.\n\n---\n\n# 27. AI Hub\n\nRepo:\n\n```text\nwetopi-platform/ai-hub\n```\n\nPara tools, skills, contracts, components, SDKs, schemas y evals.\n\n---\n\n# 28. Agentes Hermes y ADK\n\nUna vez montada la base, Hermes y ADK participar\u00e1n en:\n\n- inventario;\n- migraci\u00f3n;\n- testing;\n- observabilidad;\n- documentaci\u00f3n;\n- automatizaci\u00f3n;\n- desarrollo;\n- operaci\u00f3n.\n\n---\n\n# 29. Edge nodes\n\nNo forman parte del bootstrap inicial.\n\nDespu\u00e9s se a\u00f1adir\u00e1n:\n\n```text\nrke-edge01\nrke-edge02\nrke-edge03\n...\n```\n\nintegrados con IPVS, VRRP, DSR y Gateway API.\n\n---\n\n# 30. Hosting workers\n\nDespu\u00e9s del edge se crear\u00e1n pocos workers de hosting:\n\n```text\nrke-host01\nrke-host02\nrke-host03\n```\n\nPrimero se migrar\u00e1n aproximadamente 10 sites y se validar\u00e1 la plataforma antes de escalar.\n\n---\n\n# 31. Migraci\u00f3n progresiva desde Swarm\n\nSwarm seguir\u00e1 produciendo durante el bootstrap.\n\nNo habr\u00e1 big-bang.\n\nLa capacidad se trasladar\u00e1 progresivamente hasta retirar Swarm.\n\n---\n\n# 32. Permisos y credenciales\n\nCodex puede tener permisos amplios de GitHub sobre la organizaci\u00f3n, pero:\n\n```text\nGitHub permissions != production credentials\n```\n\nNo entregar inicialmente:\n\n- OVH master credentials;\n- Proxmox root;\n- Ceph admin global;\n- OpenBao root permanente.\n\nCrear una identidad espec\u00edfica de automatizaci\u00f3n para Proxmox con m\u00ednimos permisos necesarios.\n\n---\n\n# 33. Primeros encargos para Codex\n\n## Encargo 1 \u2014 Repos\n\nPreparar los cinco repos con README, branch protection, CODEOWNERS y CI m\u00ednima.\n\n## Encargo 2 \u2014 Inventario Proxmox\n\nInventariar:\n\n- nodos;\n- CPU;\n- RAM;\n- storage;\n- bridges;\n- VLANs;\n- redes;\n- failure domains.\n\nNo inventar direccionamiento.\n\n## Encargo 3 \u2014 Ubuntu template\n\nCrear template Ubuntu 24.04 reproducible con cloud-init.\n\n## Encargo 4 \u2014 `ops01`\n\nCrear `wetopi-ops01` mediante OpenTofu y provisionarla con Ansible.\n\n## Encargo 5 \u2014 Control plane\n\nCrear `rke-cp01`, `rke-cp02`, `rke-cp03`.\n\n## Encargo 6 \u2014 Platform workers\n\nCrear `rke-platform01`, `rke-platform02`, `rke-platform03`.\n\n## Encargo 7 \u2014 RKE2 HA\n\nInstalar RKE2 con embedded etcd y endpoint API estable.\n\n## Encargo 8 \u2014 Failure testing\n\nValidar:\n\n```text\nreboot cp\nloss of one cp\nworker reboot\ndrain\nuncordon\netcd snapshot\nrestore procedure\n```\n\nantes de seguir.\n\n---\n\n# 34. Fases del bootstrap\n\n## Fase 0 \u2014 GitHub\n\nCrear organizaci\u00f3n y repos.\n\n## Fase 1 \u2014 Inventario\n\nProxmox + networking + storage.\n\n## Fase 2 \u2014 IaC\n\nOpenTofu + Ansible.\n\n## Fase 3 \u2014 Ops host\n\n`wetopi-ops01`.\n\n## Fase 4 \u2014 RKE2 HA\n\n3 control planes + 3 platform workers.\n\n## Fase 5 \u2014 Networking\n\nCilium + Hubble.\n\n## Fase 6 \u2014 Storage\n\nCeph CSI.\n\n## Fase 7 \u2014 GitOps\n\nReconciliaci\u00f3n desde Git.\n\n## Fase 8 \u2014 Secrets\n\nOpenBao.\n\n## Fase 9 \u2014 Docs / Memory / Work Ledger\n\nServicios ag\u00e9nticos compartidos.\n\n## Fase 10 \u2014 Hermes + ADK\n\nDespliegue definitivo.\n\n## Fase 11 \u2014 Edge\n\nPrimer tr\u00e1fico HTTP en RKE2.\n\n## Fase 12 \u2014 Hosting\n\nPrimeros WordPress.\n\n## Fase 13 \u2014 Migraci\u00f3n masiva\n\nSwarm -> RKE2.\n\n---\n\n# 35. Checklist de arranque\n\n## GitHub\n\n- [ ] organizaci\u00f3n `wetopi-platform`\n- [ ] repo `infrastructure`\n- [ ] repo `agent-services`\n- [ ] repo `ai-hub`\n- [ ] repo `agents`\n- [ ] repo `docs`\n- [ ] Codex autorizado\n- [ ] branch protection\n- [ ] CODEOWNERS\n\n## Proxmox\n\n- [ ] inventario\n- [ ] bridges\n- [ ] IP ranges\n- [ ] failure domains\n- [ ] Ubuntu template\n- [ ] automation account\n\n## Bootstrap\n\n- [ ] `wetopi-ops01`\n- [ ] `rke-cp01`\n- [ ] `rke-cp02`\n- [ ] `rke-cp03`\n- [ ] `rke-platform01`\n- [ ] `rke-platform02`\n- [ ] `rke-platform03`\n\n## Networking\n\n- [ ] una NIC LAN por VM\n- [ ] evitar overlaps\n- [ ] no modificar `192.168.96.0/20`\n- [ ] definir Pod CIDR\n- [ ] definir Service CIDR\n\n## RKE2\n\n- [ ] HA\n- [ ] embedded etcd\n- [ ] API stable endpoint\n- [ ] snapshots\n- [ ] restore tested\n\n## Platform\n\n- [ ] Cilium\n- [ ] Hubble\n- [ ] Ceph CSI\n- [ ] GitOps\n- [ ] OpenBao\n- [ ] MkDocs\n- [ ] Memory Plane\n- [ ] Work Ledger\n\n## Agents\n\n- [ ] Hermes SRE\n- [ ] Hermes Developer\n- [ ] Hermes Marketing\n- [ ] ADK Fleet\n\n## Migration\n\n- [ ] Edge nodes\n- [ ] Gateway API\n- [ ] first hosting workers\n- [ ] first 10 sites\n- [ ] production validation\n- [ ] progressive migration\n- [ ] Swarm retirement\n\n---\n\n# 36. Resultado esperado\n\nAl finalizar el bootstrap inicial:\n\n```text\n                    GitHub\n             wetopi-platform\n                    \u2502\n             infrastructure\n                    \u2502\n                 Codex\n                    \u2502\n               wetopi-ops01\n                    \u2502\n             OpenTofu/Ansible\n                    \u2502\n                    \u25bc\n             \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n             \u2502   RKE2 HA     \u2502\n             \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                    \u2502\n       \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n       \u2502            \u2502            \u2502\n   3 Control     Cilium      3 Platform\n    Planes        Hubble       Workers\n       \u2502            \u2502            \u2502\n       \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                    \u2502\n                 Ceph CSI\n                    \u2502\n                  GitOps\n                    \u2502\n                 OpenBao\n                    \u2502\n        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n        \u25bc           \u25bc           \u25bc\n      Docs       Memory      Work Ledger\n        \u2502           \u2502           \u2502\n        \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                    \u2502\n             Hermes + ADK\n```\n\nTodav\u00eda sin necesidad de haber movido un solo cliente.\n\nDespu\u00e9s:\n\n```text\nEdge\n \u2193\nfirst hosting workers\n \u2193\nfirst websites\n \u2193\nprogressive migration\n \u2193\nSwarm retirement\n```\n\n---\n\n# Conclusi\u00f3n\n\nLa estrategia de arranque queda fijada as\u00ed:\n\n1. Git primero.\n2. IaC desde el primer d\u00eda.\n3. Bootstrap desde fuera de Kubernetes.\n4. RKE2 greenfield.\n5. Tres control planes.\n6. Tres platform workers.\n7. Una sola NIC LAN por VM.\n8. No tocar Ceph `192.168.96.0/20`.\n9. Cilium/Hubble.\n10. Ceph CSI.\n11. GitOps.\n12. OpenBao.\n13. Docs, Memory y Work Ledger.\n14. Hermes y ADK ya dentro del cluster definitivo.\n15. Edge despu\u00e9s.\n16. Hosting despu\u00e9s.\n17. Migraci\u00f3n progresiva desde Swarm.\n\nLa prioridad es crear una foundation que pueda romperse, reconstruirse y probarse sin afectar a producci\u00f3n, y solo despu\u00e9s empezar a trasladar tr\u00e1fico y workloads reales.",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (design/bootstrap-plan.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<h1 id=\"bkmrk-wetopi-platform-boot\">Wetopi Platform Bootstrap Plan</h1>\n<h2 id=\"bkmrk-plan-de-arranque-de-\">Plan de arranque de la nueva plataforma RKE2 y organizaci\u00f3n GitHub</h2>\n<p id=\"bkmrk-fecha%3A-28-de-agosto-\"><strong>Fecha:</strong> 28 de agosto de 2026<br>\n<strong>Estado:</strong> Plan de bootstrap consolidado<br>\n<strong>Organizaci\u00f3n GitHub:</strong> <code>wetopi-platform</code><br>\n<strong>\u00c1mbito:</strong> Creaci\u00f3n de repositorios, bootstrap de infraestructura, RKE2, servicios base y posterior incorporaci\u00f3n de agentes y workloads.</p>\n<hr id=\"bkmrk-\">\n<h1 id=\"bkmrk-%C3%8Dndice\">\u00cdndice</h1>\n<ol id=\"bkmrk-resumen-ejecutivo-ob\">\n<li>Resumen ejecutivo</li>\n<li>Objetivo del bootstrap</li>\n<li>Decisiones ya tomadas</li>\n<li>Organizaci\u00f3n GitHub</li>\n<li>Repositorios iniciales</li>\n<li>Git como source of truth</li>\n<li>OpenTofu vs Terraform</li>\n<li>Modelo de infraestructura inicial</li>\n<li>VM externa de operaciones: <code>wetopi-ops01</code></li>\n<li>Control plane RKE2</li>\n<li>Platform workers</li>\n<li>Distribuci\u00f3n sobre Proxmox</li>\n<li>Networking de las VMs</li>\n<li>Red Ceph existente</li>\n<li>RKE2 y etcd</li>\n<li>Bootstrap desde fuera del cluster</li>\n<li>Rol inicial de Codex</li>\n<li>Cu\u00e1ndo desplegar Hermes</li>\n<li>Orden de despliegue de servicios base</li>\n<li>Cilium y Hubble</li>\n<li>Ceph CSI</li>\n<li>GitOps</li>\n<li>OpenBao</li>\n<li>Documentation Plane</li>\n<li>Memory Plane</li>\n<li>Work Coordination Plane</li>\n<li>AI Hub</li>\n<li>Agentes Hermes y ADK</li>\n<li>Edge nodes</li>\n<li>Hosting workers</li>\n<li>Migraci\u00f3n progresiva desde Swarm</li>\n<li>Permisos y credenciales</li>\n<li>Primeros encargos para Codex</li>\n<li>Fases del bootstrap</li>\n<li>Checklist de arranque</li>\n<li>Resultado esperado</li>\n</ol>\n<hr id=\"bkmrk--1\">\n<h1 id=\"bkmrk-1.-resumen-ejecutivo\">1. Resumen ejecutivo</h1>\n<p id=\"bkmrk-la-nueva-plataforma-\">La nueva plataforma de Wetopi se construir\u00e1 desde cero, en paralelo a la infraestructura actual basada en Docker Swarm.</p>\n<p id=\"bkmrk-no-se-realizar%C3%A1-una-\">No se realizar\u00e1 una migraci\u00f3n in-place.</p>\n<p id=\"bkmrk-la-nueva-base-ser%C3%A1%3A\">La nueva base ser\u00e1:</p>\n<pre id=\"bkmrk-proxmox-%E2%86%93-vms-ubuntu\"><code class=\"language-text\">Proxmox\n  \u2193\nVMs Ubuntu\n  \u2193\nRKE2\n  \u2193\nCilium / Hubble\n  \u2193\nCeph CSI\n  \u2193\nGitOps\n  \u2193\nOpenBao\n  \u2193\nDocs / Memory / Work Ledger\n  \u2193\nHermes + ADK\n  \u2193\nEdge\n  \u2193\nHosting workloads\n</code></pre>\n<p id=\"bkmrk-el-bootstrap-inicial\">El bootstrap inicial se har\u00e1 <strong>desde fuera de Kubernetes</strong>.</p>\n<p id=\"bkmrk-durante-esta-primera\">Durante esta primera etapa:</p>\n<ul id=\"bkmrk-codex-trabajar%C3%A1-desd\">\n<li>Codex trabajar\u00e1 desde el laptop del operador;</li>\n<li>GitHub ser\u00e1 el source of truth;</li>\n<li>existir\u00e1 una VM externa <code>wetopi-ops01</code>;</li>\n<li>se crear\u00e1n 3 control planes RKE2;</li>\n<li>se crear\u00e1n 3 platform workers;</li>\n<li>no se desplegar\u00e1n a\u00fan workloads de customers;</li>\n<li>no se tocar\u00e1 la red Ceph existente <code>192.168.96.0/20</code>;</li>\n<li>cada VM tendr\u00e1 una \u00fanica interfaz LAN;</li>\n<li>Hermes se desplegar\u00e1 despu\u00e9s, ya dentro del cluster definitivo.</li>\n</ul>\n<p id=\"bkmrk-la-organizaci%C3%B3n-gith\">La organizaci\u00f3n GitHub ser\u00e1:</p>\n<pre id=\"bkmrk-wetopi-platform\"><code class=\"language-text\">wetopi-platform\n</code></pre>\n<p id=\"bkmrk-y-los-repositorios-i\">y los repositorios iniciales ser\u00e1n:</p>\n<pre id=\"bkmrk-wetopi-platform%2Finfr\"><code class=\"language-text\">wetopi-platform/infrastructure\nwetopi-platform/agent-services\nwetopi-platform/ai-hub\nwetopi-platform/agents\nwetopi-platform/docs\n</code></pre>\n<hr id=\"bkmrk--2\">\n<h1 id=\"bkmrk-2.-objetivo-del-boot\">2. Objetivo del bootstrap</h1>\n<p id=\"bkmrk-el-objetivo-de-esta-\">El objetivo de esta fase no es migrar inmediatamente los ~700 contenedores actuales.</p>\n<p id=\"bkmrk-el-objetivo-es-const\">El objetivo es construir una <strong>foundation nueva, reproducible y segura</strong> sobre la que posteriormente se pueda migrar Wetopi.</p>\n<p id=\"bkmrk-el-milestone-inicial\">El milestone inicial ser\u00e1:</p>\n<blockquote id=\"bkmrk-disponer-de-un-clust\">\n<p>disponer de un cluster RKE2 HA vac\u00edo, estable, observable, con storage, secrets, GitOps y servicios ag\u00e9nticos comunes funcionando antes de mover tr\u00e1fico de clientes.</p>\n</blockquote>\n<hr id=\"bkmrk--3\">\n<h1 id=\"bkmrk-3.-decisiones-ya-tom\">3. Decisiones ya tomadas</h1>\n<h2 id=\"bkmrk-plataforma-kubernete\">Plataforma Kubernetes</h2>\n<p id=\"bkmrk-rke2\"><code>RKE2</code></p>\n<h2 id=\"bkmrk-cni\">CNI</h2>\n<p id=\"bkmrk-cilium\"><code>Cilium</code></p>\n<h2 id=\"bkmrk-observabilidad-de-re\">Observabilidad de red</h2>\n<p id=\"bkmrk-hubble\"><code>Hubble</code></p>\n<h2 id=\"bkmrk-storage\">Storage</h2>\n<p id=\"bkmrk-ceph-existente-media\"><code>Ceph existente mediante CSI</code></p>\n<h2 id=\"bkmrk-secrets\">Secrets</h2>\n<p id=\"bkmrk-openbao\"><code>OpenBao</code></p>\n<h2 id=\"bkmrk-documentaci%C3%B3n\">Documentaci\u00f3n</h2>\n<p id=\"bkmrk-markdown-%2B-git-%2B-mkd\"><code>Markdown + Git + MkDocs Material</code></p>\n<h2 id=\"bkmrk-infraestructura-decl\">Infraestructura declarativa</h2>\n<p id=\"bkmrk-opentofu-%2B-ansible\"><code>OpenTofu + Ansible</code></p>\n<h2 id=\"bkmrk-bootstrap\">Bootstrap</h2>\n<p id=\"bkmrk-codex-desde-laptop-%2B\"><code>Codex desde laptop + wetopi-ops01</code></p>\n<h2 id=\"bkmrk-agentes\">Agentes</h2>\n<p id=\"bkmrk-hermes-%2B-adk\"><code>Hermes + ADK</code></p>\n<p id=\"bkmrk-los-agentes-se-despl\">Los agentes se desplegar\u00e1n posteriormente dentro del cluster definitivo.</p>\n<hr id=\"bkmrk--4\">\n<h1 id=\"bkmrk-4.-organizaci%C3%B3n-gith\">4. Organizaci\u00f3n GitHub</h1>\n<p id=\"bkmrk-se-adopta-como-organ\">Se adopta como organizaci\u00f3n:</p>\n<pre id=\"bkmrk-github.com%2Fwetopi-pl\"><code class=\"language-text\">github.com/wetopi-platform\n</code></pre>\n<p id=\"bkmrk-se-descartan-nombres\">Se descartan nombres temporales como <code>wetopi3</code>, <code>wetopi-v3</code> o <code>wetopi-new</code> porque describen una migraci\u00f3n, no una plataforma duradera.</p>\n<p id=\"bkmrk-wetopi-platform-se-c\"><code>wetopi-platform</code> se considera adecuado como namespace t\u00e9cnico de largo plazo.</p>\n<hr id=\"bkmrk--5\">\n<h1 id=\"bkmrk-5.-repositorios-inic\">5. Repositorios iniciales</h1>\n<p id=\"bkmrk-se-crear%C3%A1n-inicialme\">Se crear\u00e1n inicialmente <strong>cinco repositorios</strong>.</p>\n<h2 id=\"bkmrk-5.1-infrastructure\">5.1 <code>infrastructure</code></h2>\n<p id=\"bkmrk-responsabilidad%3A\">Responsabilidad:</p>\n<ul id=\"bkmrk-proxmox%3B-opentofu%3B-a\">\n<li>Proxmox;</li>\n<li>OpenTofu;</li>\n<li>Ansible;</li>\n<li>RKE2;</li>\n<li>networking base;</li>\n<li>Cilium;</li>\n<li>Hubble;</li>\n<li>Ceph CSI;</li>\n<li>OpenBao deployment;</li>\n<li>GitOps;</li>\n<li>observabilidad base;</li>\n<li>edge;</li>\n<li>NATGW;</li>\n<li>bootstrap;</li>\n<li>disaster recovery.</li>\n</ul>\n<p id=\"bkmrk-estructura-propuesta\">Estructura propuesta:</p>\n<pre id=\"bkmrk-infrastructure%2F-%E2%94%9C%E2%94%80%E2%94%80-\"><code class=\"language-text\">infrastructure/\n\u251c\u2500\u2500 tofu/\n\u2502   \u2514\u2500\u2500 proxmox/\n\u2502       \u251c\u2500\u2500 modules/\n\u2502       \u2514\u2500\u2500 environments/\n\u2502           \u2514\u2500\u2500 prod/\n\u251c\u2500\u2500 ansible/\n\u2502   \u251c\u2500\u2500 inventories/\n\u2502   \u251c\u2500\u2500 roles/\n\u2502   \u2502   \u251c\u2500\u2500 common/\n\u2502   \u2502   \u251c\u2500\u2500 rke2-server/\n\u2502   \u2502   \u251c\u2500\u2500 rke2-agent/\n\u2502   \u2502   \u251c\u2500\u2500 edge/\n\u2502   \u2502   \u2514\u2500\u2500 natgw/\n\u2502   \u2514\u2500\u2500 playbooks/\n\u251c\u2500\u2500 kubernetes/\n\u2502   \u251c\u2500\u2500 bootstrap/\n\u2502   \u251c\u2500\u2500 cilium/\n\u2502   \u251c\u2500\u2500 hubble/\n\u2502   \u251c\u2500\u2500 ceph-csi/\n\u2502   \u251c\u2500\u2500 openbao/\n\u2502   \u251c\u2500\u2500 gitops/\n\u2502   \u251c\u2500\u2500 namespaces/\n\u2502   \u2514\u2500\u2500 observability/\n\u251c\u2500\u2500 scripts/\n\u2514\u2500\u2500 docs/\n    \u251c\u2500\u2500 bootstrap.md\n    \u251c\u2500\u2500 networking.md\n    \u2514\u2500\u2500 disaster-recovery.md\n</code></pre>\n<h2 id=\"bkmrk-5.2-agent-services\">5.2 <code>agent-services</code></h2>\n<p id=\"bkmrk-servicios-compartido\">Servicios compartidos de la capa ag\u00e9ntica:</p>\n<pre id=\"bkmrk-memory-service%2F-work\"><code class=\"language-text\">memory-service/\nwork-ledger-adapter/\nhermes-memory-provider/\nidentity/\napproval/\n</code></pre>\n<p id=\"bkmrk-inicialmente-se-prio\">Inicialmente se priorizar\u00e1n <code>memory-service</code>, <code>hermes-memory-provider</code> y <code>work-ledger-adapter</code>.</p>\n<h2 id=\"bkmrk-5.3-ai-hub\">5.3 <code>ai-hub</code></h2>\n<p id=\"bkmrk-repositorio-com%C3%BAn-de\">Repositorio com\u00fan de:</p>\n<ul id=\"bkmrk-skills%3B-tools%3B-compo\">\n<li>skills;</li>\n<li>tools;</li>\n<li>components;</li>\n<li>contracts;</li>\n<li>schemas;</li>\n<li>SDKs;</li>\n<li>evals;</li>\n<li>tests.</li>\n</ul>\n<p id=\"bkmrk-regla%3A\">Regla:</p>\n<blockquote id=\"bkmrk-una-tool-utilizada-p\">\n<p>una tool utilizada por m\u00e1s de un agente debe vivir en <code>ai-hub</code>.</p>\n</blockquote>\n<h2 id=\"bkmrk-5.4-agents\">5.4 <code>agents</code></h2>\n<p id=\"bkmrk-monorepo-inicial-de-\">Monorepo inicial de la flota:</p>\n<pre id=\"bkmrk-agents%2F-%E2%94%9C%E2%94%80%E2%94%80-hermes%2F-\"><code class=\"language-text\">agents/\n\u251c\u2500\u2500 hermes/\n\u2502   \u251c\u2500\u2500 sre/\n\u2502   \u251c\u2500\u2500 developer/\n\u2502   \u2514\u2500\u2500 marketing/\n\u251c\u2500\u2500 adk/\n\u2502   \u251c\u2500\u2500 platform/\n\u2502   \u251c\u2500\u2500 wordpress/\n\u2502   \u2514\u2500\u2500 support/\n\u251c\u2500\u2500 base/\n\u251c\u2500\u2500 deployments/\n\u2514\u2500\u2500 tests/\n</code></pre>\n<h2 id=\"bkmrk-5.5-docs\">5.5 <code>docs</code></h2>\n<p id=\"bkmrk-source-of-truth-docu\">Source of truth documental:</p>\n<pre id=\"bkmrk-docs%2F-%E2%94%9C%E2%94%80%E2%94%80-mkdocs.yml\"><code class=\"language-text\">docs/\n\u251c\u2500\u2500 mkdocs.yml\n\u2514\u2500\u2500 docs/\n    \u251c\u2500\u2500 architecture/\n    \u251c\u2500\u2500 infrastructure/\n    \u251c\u2500\u2500 kubernetes/\n    \u251c\u2500\u2500 networking/\n    \u251c\u2500\u2500 wordpress/\n    \u251c\u2500\u2500 agents/\n    \u251c\u2500\u2500 procedures/\n    \u251c\u2500\u2500 runbooks/\n    \u251c\u2500\u2500 security/\n    \u2514\u2500\u2500 adr/\n</code></pre>\n<hr id=\"bkmrk--6\">\n<h1 id=\"bkmrk-6.-git-como-source-o\">6. Git como source of truth</h1>\n<p id=\"bkmrk-desde-el-primer-d%C3%ADa%3A\">Desde el primer d\u00eda:</p>\n<pre id=\"bkmrk-git-%E2%86%93-review-%E2%86%93-autom\"><code class=\"language-text\">Git\n  \u2193\nreview\n  \u2193\nautomation\n  \u2193\ninfrastructure\n</code></pre>\n<p id=\"bkmrk-la-shell-history-no-\">La shell history no ser\u00e1 source of truth.</p>\n<p id=\"bkmrk-todo-cambio-relevant\">Todo cambio relevante debe terminar en Git.</p>\n<hr id=\"bkmrk--7\">\n<h1 id=\"bkmrk-7.-opentofu-vs-terra\">7. OpenTofu vs Terraform</h1>\n<p id=\"bkmrk-se-selecciona-inicia\">Se selecciona inicialmente <code>OpenTofu</code> por:</p>\n<ul id=\"bkmrk-licencia-open-source\">\n<li>licencia open source;</li>\n<li>gobernanza Linux Foundation;</li>\n<li>evitar dependencia BSL;</li>\n<li>compatibilidad con el ecosistema Terraform;</li>\n<li>idoneidad para un proyecto interno de largo plazo.</li>\n</ul>\n<p id=\"bkmrk-terraform-seguir%C3%ADa-s\">Terraform seguir\u00eda siendo t\u00e9cnicamente v\u00e1lido.</p>\n<hr id=\"bkmrk--8\">\n<h1 id=\"bkmrk-8.-modelo-de-infraes\">8. Modelo de infraestructura inicial</h1>\n<p id=\"bkmrk-se-crear%C3%A1n-inicialme-1\">Se crear\u00e1n inicialmente:</p>\n<pre id=\"bkmrk-1-x-ops-vm-3-x-rke2-\"><code class=\"language-text\">1 x ops VM\n3 x RKE2 control plane\n3 x platform workers\n</code></pre>\n<p id=\"bkmrk-total%3A\">Total:</p>\n<pre id=\"bkmrk-7-vms\"><code class=\"language-text\">7 VMs\n</code></pre>\n<p id=\"bkmrk-no-se-crear%C3%A1n-todav%C3%AD\">No se crear\u00e1n todav\u00eda hosting workers masivos ni edge productivos.</p>\n<hr id=\"bkmrk--9\">\n<h1 id=\"bkmrk-9.-vm-externa-de-ope\">9. VM externa de operaciones: <code>wetopi-ops01</code></h1>\n<p id=\"bkmrk-se-crear%C3%A1-wetopi-ops\">Se crear\u00e1 <code>wetopi-ops01</code> fuera del cluster Kubernetes.</p>\n<p id=\"bkmrk-objetivo%3A\">Objetivo:</p>\n<blockquote id=\"bkmrk-bootstrap-host%2C-bast\">\n<p>bootstrap host, bastion administrativo y punto de recuperaci\u00f3n cuando Kubernetes no est\u00e9 disponible.</p>\n</blockquote>\n<p id=\"bkmrk-sizing-orientativo%3A\">Sizing orientativo:</p>\n<pre id=\"bkmrk-4-vcpu-8-gb-ram-40-8\"><code class=\"language-text\">4 vCPU\n8 GB RAM\n40-80 GB disk\nUbuntu 24.04\n1 NIC LAN\n</code></pre>\n<p id=\"bkmrk-herramientas%3A\">Herramientas:</p>\n<pre id=\"bkmrk-git-ssh-ansible-open\"><code class=\"language-text\">git\nssh\nansible\nopentofu\nkubectl\nhelm\njq\nyq\nrke2 tooling\n</code></pre>\n<p id=\"bkmrk-no-alojar%C3%A1-workloads\">No alojar\u00e1 workloads productivos.</p>\n<p id=\"bkmrk-debe-permitir-operar\">Debe permitir operar si fallan Cilium, GitOps, OpenBao o parte del cluster.</p>\n<hr id=\"bkmrk--10\">\n<h1 id=\"bkmrk-10.-control-plane-rk\">10. Control plane RKE2</h1>\n<p id=\"bkmrk-se-desplegar%C3%A1n%3A\">Se desplegar\u00e1n:</p>\n<pre id=\"bkmrk-rke-cp01-rke-cp02-rk\"><code class=\"language-text\">rke-cp01\nrke-cp02\nrke-cp03\n</code></pre>\n<p id=\"bkmrk-cada-uno-en-un-host-\">Cada uno en un host Proxmox f\u00edsico diferente siempre que sea posible.</p>\n<p id=\"bkmrk-sizing-orientativo%3A-1\">Sizing orientativo:</p>\n<pre id=\"bkmrk-8-vcpu-16-gb-ram-50-\"><code class=\"language-text\">8 vCPU\n16 GB RAM\n50-100 GB disk\nUbuntu 24.04\n1 NIC LAN\n</code></pre>\n<hr id=\"bkmrk--11\">\n<h1 id=\"bkmrk-11.-platform-workers\">11. Platform workers</h1>\n<p id=\"bkmrk-se-desplegar%C3%A1n%3A-1\">Se desplegar\u00e1n:</p>\n<pre id=\"bkmrk-rke-platform01-rke-p\"><code class=\"language-text\">rke-platform01\nrke-platform02\nrke-platform03\n</code></pre>\n<p id=\"bkmrk-sizing-orientativo%3A-2\">Sizing orientativo:</p>\n<pre id=\"bkmrk-8-16-vcpu-16-32-gb-r\"><code class=\"language-text\">8-16 vCPU\n16-32 GB RAM\nUbuntu 24.04\n1 NIC LAN\n</code></pre>\n<p id=\"bkmrk-label%3A\">Label:</p>\n<pre id=\"bkmrk-wetopi.io%2Frole%3Dplatf\"><code class=\"language-text\">wetopi.io/role=platform\n</code></pre>\n<hr id=\"bkmrk--12\">\n<h1 id=\"bkmrk-12.-distribuci%C3%B3n-sob\">12. Distribuci\u00f3n sobre Proxmox</h1>\n<p id=\"bkmrk-ejemplo%3A\">Ejemplo:</p>\n<pre id=\"bkmrk-pve01--%3E-rke-cp01-%2B-\"><code class=\"language-text\">pve01 -&gt; rke-cp01 + rke-platform01\npve02 -&gt; rke-cp02 + rke-platform02\npve03 -&gt; rke-cp03 + rke-platform03\n</code></pre>\n<p id=\"bkmrk-la-distribuci%C3%B3n-real\">La distribuci\u00f3n real debe respetar failure domains y recursos disponibles.</p>\n<hr id=\"bkmrk--13\">\n<h1 id=\"bkmrk-13.-networking-de-la\">13. Networking de las VMs</h1>\n<p id=\"bkmrk-cada-vm-tendr%C3%A1-una-%C3%BA\">Cada VM tendr\u00e1 <strong>una \u00fanica interfaz LAN</strong>.</p>\n<pre id=\"bkmrk-vm-%E2%94%94%E2%94%80%E2%94%80-eth0-%E2%94%94%E2%94%80%E2%94%80-weto\"><code class=\"language-text\">VM\n \u2514\u2500\u2500 eth0\n      \u2514\u2500\u2500 Wetopi LAN / vRack\n</code></pre>\n<p id=\"bkmrk-los-rangos-kubernete\">Los rangos Kubernetes ser\u00e1n independientes:</p>\n<pre id=\"bkmrk-vm-lan-%21%3D-pod-cidr-%21\"><code class=\"language-text\">VM LAN != Pod CIDR != Service CIDR != Ceph network\n</code></pre>\n<p id=\"bkmrk-los-rangos-concretos\">Los rangos concretos deben definirse tras inventariar las redes actuales.</p>\n<hr id=\"bkmrk--14\">\n<h1 id=\"bkmrk-14.-red-ceph-existen\">14. Red Ceph existente</h1>\n<p id=\"bkmrk-existe-una-red-ceph%3A\">Existe una red Ceph:</p>\n<pre id=\"bkmrk-192.168.96.0%2F20\"><code class=\"language-text\">192.168.96.0/20\n</code></pre>\n<p id=\"bkmrk-que-forma-parte-de-l\">que forma parte de la infraestructura hiperconvergida Proxmox + Ceph.</p>\n<p id=\"bkmrk-decisi%C3%B3n%3A\">Decisi\u00f3n:</p>\n<blockquote id=\"bkmrk-no-tocar%2C-renumerar-\">\n<p>NO tocar, renumerar ni redise\u00f1ar esta red durante el bootstrap RKE2.</p>\n</blockquote>\n<p id=\"bkmrk-rke2-consumir%C3%A1-ceph-\">RKE2 consumir\u00e1 Ceph mediante CSI adapt\u00e1ndose a la infraestructura existente.</p>\n<hr id=\"bkmrk--15\">\n<h1 id=\"bkmrk-15.-rke2-y-etcd\">15. RKE2 y etcd</h1>\n<p id=\"bkmrk-se-utilizar%C3%A1-embedde\">Se utilizar\u00e1 <code>embedded etcd</code> sobre los tres server nodes.</p>\n<pre id=\"bkmrk-cp01-etcd-cp02-etcd-\"><code class=\"language-text\">cp01 etcd\ncp02 etcd\ncp03 etcd\n</code></pre>\n<p id=\"bkmrk-quorum%3A-2-de-3.\">Quorum: 2 de 3.</p>\n<p id=\"bkmrk-se-priorizar%C3%A1-almace\">Se priorizar\u00e1 almacenamiento de baja latencia para etcd. Debe evaluarse local NVMe si la alternativa Ceph introduce latencia innecesaria.</p>\n<p id=\"bkmrk-los-snapshots-de-etc\">Los snapshots de etcd deber\u00e1n almacenarse fuera de los propios nodos y el procedimiento de restore deber\u00e1 probarse.</p>\n<hr id=\"bkmrk--16\">\n<h1 id=\"bkmrk-16.-bootstrap-desde-\">16. Bootstrap desde fuera del cluster</h1>\n<p id=\"bkmrk-modelo%3A\">Modelo:</p>\n<pre id=\"bkmrk-laptop-%E2%94%82-codex-%E2%94%82-git\"><code class=\"language-text\">Laptop\n  \u2502\nCodex\n  \u2502\nGitHub\n  \u2502\nops01\n  \u2502\nOpenTofu + Ansible\n  \u2502\nProxmox / RKE2\n</code></pre>\n<p id=\"bkmrk-kubernetes-no-depend\">Kubernetes no depender\u00e1 de agentes internos para poder crearse o repararse.</p>\n<hr id=\"bkmrk--17\">\n<h1 id=\"bkmrk-17.-rol-inicial-de-c\">17. Rol inicial de Codex</h1>\n<p id=\"bkmrk-durante-el-bootstrap\">Durante el bootstrap Codex act\u00faa como <code>bootstrap engineer</code>.</p>\n<p id=\"bkmrk-trabajar%C3%A1-contra%3A\">Trabajar\u00e1 contra:</p>\n<ul id=\"bkmrk-github%3B-repos%3B-ops01\">\n<li>GitHub;</li>\n<li>repos;</li>\n<li><code>ops01</code>;</li>\n<li>Proxmox con credenciales limitadas;</li>\n<li>posteriormente Kubernetes.</li>\n</ul>\n<p id=\"bkmrk-todo-cambio-relevant-1\">Todo cambio relevante debe quedar versionado.</p>\n<hr id=\"bkmrk--18\">\n<h1 id=\"bkmrk-18.-cu%C3%A1ndo-desplegar\">18. Cu\u00e1ndo desplegar Hermes</h1>\n<p id=\"bkmrk-no-se-desplegar%C3%A1n-va\">No se desplegar\u00e1n varios Hermes temporales antes del cluster.</p>\n<p id=\"bkmrk-los-hermes-definitiv\">Los Hermes definitivos se desplegar\u00e1n dentro de RKE2 cuando existan al menos:</p>\n<pre id=\"bkmrk-rke2-estable-cilium-\"><code class=\"language-text\">RKE2 estable\nCilium\nHubble\nCeph CSI\nGitOps\nOpenBao\nDocumentation\nMemory Plane\nWork Ledger\n</code></pre>\n<p id=\"bkmrk-despu%C3%A9s%3A\">Despu\u00e9s:</p>\n<pre id=\"bkmrk-hermes-sre-hermes-de\"><code class=\"language-text\">Hermes SRE\nHermes Developer\nHermes Marketing\n</code></pre>\n<hr id=\"bkmrk--19\">\n<h1 id=\"bkmrk-19.-orden-de-desplie\">19. Orden de despliegue de servicios base</h1>\n<pre id=\"bkmrk-rke2-%E2%86%93-cilium-%E2%86%93-hubb\"><code class=\"language-text\">RKE2\n \u2193\nCilium\n \u2193\nHubble\n \u2193\nCeph CSI\n \u2193\nGitOps\n \u2193\nOpenBao\n \u2193\nDocumentation\n \u2193\nMemory Plane\n \u2193\nWork Ledger\n \u2193\nAI Hub integration\n \u2193\nHermes\n \u2193\nADK Fleet\n</code></pre>\n<hr id=\"bkmrk--20\">\n<h1 id=\"bkmrk-20.-cilium-y-hubble\">20. Cilium y Hubble</h1>\n<p id=\"bkmrk-cilium-ser%C3%A1-el-cni-b\">Cilium ser\u00e1 el CNI base.</p>\n<p id=\"bkmrk-inicialmente%3A\">Inicialmente:</p>\n<ul id=\"bkmrk-networking%3B-networkp\">\n<li>networking;</li>\n<li>NetworkPolicy;</li>\n<li>Hubble;</li>\n<li>observabilidad.</li>\n</ul>\n<p id=\"bkmrk-no-se-activar%C3%A1n-desd\">No se activar\u00e1n desde el d\u00eda uno BGP WAN, egress complejo o service mesh.</p>\n<hr id=\"bkmrk--21\">\n<h1 id=\"bkmrk-21.-ceph-csi\">21. Ceph CSI</h1>\n<p id=\"bkmrk-pruebas-m%C3%ADnimas-ante\">Pruebas m\u00ednimas antes de customer workloads:</p>\n<pre id=\"bkmrk-crear-pvc-montar-pvc\"><code class=\"language-text\">crear PVC\nmontar PVC\nborrar Pod\nrecrear Pod\nvalidar persistencia\nsnapshot si aplica\n</code></pre>\n<hr id=\"bkmrk--22\">\n<h1 id=\"bkmrk-22.-gitops\">22. GitOps</h1>\n<p id=\"bkmrk-el-cluster-evolucion\">El cluster evolucionar\u00e1 hacia:</p>\n<pre id=\"bkmrk-git--%3E-reconciler--%3E\"><code class=\"language-text\">Git -&gt; reconciler -&gt; Kubernetes\n</code></pre>\n<p id=\"bkmrk-la-herramienta-concr\">La herramienta concreta puede decidirse durante el bootstrap.</p>\n<hr id=\"bkmrk--23\">\n<h1 id=\"bkmrk-23.-openbao\">23. OpenBao</h1>\n<p id=\"bkmrk-openbao-ser%C3%A1-el-secr\">OpenBao ser\u00e1 el secrets manager central.</p>\n<p id=\"bkmrk-configurar%3A\">Configurar:</p>\n<ul id=\"bkmrk-ha%3B-persistence%3B-kub\">\n<li>HA;</li>\n<li>persistence;</li>\n<li>Kubernetes Auth;</li>\n<li>audit;</li>\n<li>policies;</li>\n<li>backups.</li>\n</ul>\n<hr id=\"bkmrk--24\">\n<h1 id=\"bkmrk-24.-documentation-pl\">24. Documentation Plane</h1>\n<p id=\"bkmrk-repo%3A\">Repo:</p>\n<pre id=\"bkmrk-wetopi-platform%2Fdocs\"><code class=\"language-text\">wetopi-platform/docs\n</code></pre>\n<p id=\"bkmrk-render%3A\">Render:</p>\n<pre id=\"bkmrk-mkdocs-material\"><code class=\"language-text\">MkDocs Material\n</code></pre>\n<p id=\"bkmrk-markdown-ser%C3%A1-el-sou\">Markdown ser\u00e1 el source of truth.</p>\n<hr id=\"bkmrk--25\">\n<h1 id=\"bkmrk-25.-memory-plane\">25. Memory Plane</h1>\n<p id=\"bkmrk-la-memoria-instituci\">La memoria institucional propia de Wetopi se productivizar\u00e1 como servicio central para Hermes, ADK y futuros agentes.</p>\n<p id=\"bkmrk-debe-soportar%3A\">Debe soportar:</p>\n<ul id=\"bkmrk-factual-memory%3B-scop\">\n<li>factual memory;</li>\n<li>scopes;</li>\n<li>provenance;</li>\n<li>confidence;</li>\n<li>temporalidad;</li>\n<li>contradictions;</li>\n<li>candidate facts.</li>\n</ul>\n<hr id=\"bkmrk--26\">\n<h1 id=\"bkmrk-26.-work-coordinatio\">26. Work Coordination Plane</h1>\n<p id=\"bkmrk-se-desplegar%C3%A1-un-bac\">Se desplegar\u00e1 un backend Kanban/ticketing desacoplado mediante contrato Wetopi.</p>\n<p id=\"bkmrk-responsabilidades%3A\">Responsabilidades:</p>\n<ul id=\"bkmrk-tareas%3B-ownership%3B-r\">\n<li>tareas;</li>\n<li>ownership;</li>\n<li>resource scopes;</li>\n<li>leases;</li>\n<li>intent;</li>\n<li>journal;</li>\n<li>coordinaci\u00f3n entre humanos y agentes.</li>\n</ul>\n<hr id=\"bkmrk--27\">\n<h1 id=\"bkmrk-27.-ai-hub\">27. AI Hub</h1>\n<p id=\"bkmrk-repo%3A-1\">Repo:</p>\n<pre id=\"bkmrk-wetopi-platform%2Fai-h\"><code class=\"language-text\">wetopi-platform/ai-hub\n</code></pre>\n<p id=\"bkmrk-para-tools%2C-skills%2C-\">Para tools, skills, contracts, components, SDKs, schemas y evals.</p>\n<hr id=\"bkmrk--28\">\n<h1 id=\"bkmrk-28.-agentes-hermes-y\">28. Agentes Hermes y ADK</h1>\n<p id=\"bkmrk-una-vez-montada-la-b\">Una vez montada la base, Hermes y ADK participar\u00e1n en:</p>\n<ul id=\"bkmrk-inventario%3B-migraci%C3%B3\">\n<li>inventario;</li>\n<li>migraci\u00f3n;</li>\n<li>testing;</li>\n<li>observabilidad;</li>\n<li>documentaci\u00f3n;</li>\n<li>automatizaci\u00f3n;</li>\n<li>desarrollo;</li>\n<li>operaci\u00f3n.</li>\n</ul>\n<hr id=\"bkmrk--29\">\n<h1 id=\"bkmrk-29.-edge-nodes\">29. Edge nodes</h1>\n<p id=\"bkmrk-no-forman-parte-del-\">No forman parte del bootstrap inicial.</p>\n<p id=\"bkmrk-despu%C3%A9s-se-a%C3%B1adir%C3%A1n%3A\">Despu\u00e9s se a\u00f1adir\u00e1n:</p>\n<pre id=\"bkmrk-rke-edge01-rke-edge0\"><code class=\"language-text\">rke-edge01\nrke-edge02\nrke-edge03\n...\n</code></pre>\n<p id=\"bkmrk-integrados-con-ipvs%2C\">integrados con IPVS, VRRP, DSR y Gateway API.</p>\n<hr id=\"bkmrk--30\">\n<h1 id=\"bkmrk-30.-hosting-workers\">30. Hosting workers</h1>\n<p id=\"bkmrk-despu%C3%A9s-del-edge-se-\">Despu\u00e9s del edge se crear\u00e1n pocos workers de hosting:</p>\n<pre id=\"bkmrk-rke-host01-rke-host0\"><code class=\"language-text\">rke-host01\nrke-host02\nrke-host03\n</code></pre>\n<p id=\"bkmrk-primero-se-migrar%C3%A1n-\">Primero se migrar\u00e1n aproximadamente 10 sites y se validar\u00e1 la plataforma antes de escalar.</p>\n<hr id=\"bkmrk--31\">\n<h1 id=\"bkmrk-31.-migraci%C3%B3n-progre\">31. Migraci\u00f3n progresiva desde Swarm</h1>\n<p id=\"bkmrk-swarm-seguir%C3%A1-produc\">Swarm seguir\u00e1 produciendo durante el bootstrap.</p>\n<p id=\"bkmrk-no-habr%C3%A1-big-bang.\">No habr\u00e1 big-bang.</p>\n<p id=\"bkmrk-la-capacidad-se-tras\">La capacidad se trasladar\u00e1 progresivamente hasta retirar Swarm.</p>\n<hr id=\"bkmrk--32\">\n<h1 id=\"bkmrk-32.-permisos-y-crede\">32. Permisos y credenciales</h1>\n<p id=\"bkmrk-codex-puede-tener-pe\">Codex puede tener permisos amplios de GitHub sobre la organizaci\u00f3n, pero:</p>\n<pre id=\"bkmrk-github-permissions-%21\"><code class=\"language-text\">GitHub permissions != production credentials\n</code></pre>\n<p id=\"bkmrk-no-entregar-inicialm\">No entregar inicialmente:</p>\n<ul id=\"bkmrk-ovh-master-credentia\">\n<li>OVH master credentials;</li>\n<li>Proxmox root;</li>\n<li>Ceph admin global;</li>\n<li>OpenBao root permanente.</li>\n</ul>\n<p id=\"bkmrk-crear-una-identidad-\">Crear una identidad espec\u00edfica de automatizaci\u00f3n para Proxmox con m\u00ednimos permisos necesarios.</p>\n<hr id=\"bkmrk--33\">\n<h1 id=\"bkmrk-33.-primeros-encargo\">33. Primeros encargos para Codex</h1>\n<h2 id=\"bkmrk-encargo-1-%E2%80%94-repos\">Encargo 1 \u2014 Repos</h2>\n<p id=\"bkmrk-preparar-los-cinco-r\">Preparar los cinco repos con README, branch protection, CODEOWNERS y CI m\u00ednima.</p>\n<h2 id=\"bkmrk-encargo-2-%E2%80%94-inventar\">Encargo 2 \u2014 Inventario Proxmox</h2>\n<p id=\"bkmrk-inventariar%3A\">Inventariar:</p>\n<ul id=\"bkmrk-nodos%3B-cpu%3B-ram%3B-sto\">\n<li>nodos;</li>\n<li>CPU;</li>\n<li>RAM;</li>\n<li>storage;</li>\n<li>bridges;</li>\n<li>VLANs;</li>\n<li>redes;</li>\n<li>failure domains.</li>\n</ul>\n<p id=\"bkmrk-no-inventar-direccio\">No inventar direccionamiento.</p>\n<h2 id=\"bkmrk-encargo-3-%E2%80%94-ubuntu-t\">Encargo 3 \u2014 Ubuntu template</h2>\n<p id=\"bkmrk-crear-template-ubunt\">Crear template Ubuntu 24.04 reproducible con cloud-init.</p>\n<h2 id=\"bkmrk-encargo-4-%E2%80%94-ops01\">Encargo 4 \u2014 <code>ops01</code></h2>\n<p id=\"bkmrk-crear-wetopi-ops01-m\">Crear <code>wetopi-ops01</code> mediante OpenTofu y provisionarla con Ansible.</p>\n<h2 id=\"bkmrk-encargo-5-%E2%80%94-control-\">Encargo 5 \u2014 Control plane</h2>\n<p id=\"bkmrk-crear-rke-cp01%2C-rke-\">Crear <code>rke-cp01</code>, <code>rke-cp02</code>, <code>rke-cp03</code>.</p>\n<h2 id=\"bkmrk-encargo-6-%E2%80%94-platform\">Encargo 6 \u2014 Platform workers</h2>\n<p id=\"bkmrk-crear-rke-platform01\">Crear <code>rke-platform01</code>, <code>rke-platform02</code>, <code>rke-platform03</code>.</p>\n<h2 id=\"bkmrk-encargo-7-%E2%80%94-rke2-ha\">Encargo 7 \u2014 RKE2 HA</h2>\n<p id=\"bkmrk-instalar-rke2-con-em\">Instalar RKE2 con embedded etcd y endpoint API estable.</p>\n<h2 id=\"bkmrk-encargo-8-%E2%80%94-failure-\">Encargo 8 \u2014 Failure testing</h2>\n<p id=\"bkmrk-validar%3A\">Validar:</p>\n<pre id=\"bkmrk-reboot-cp-loss-of-on\"><code class=\"language-text\">reboot cp\nloss of one cp\nworker reboot\ndrain\nuncordon\netcd snapshot\nrestore procedure\n</code></pre>\n<p id=\"bkmrk-antes-de-seguir.\">antes de seguir.</p>\n<hr id=\"bkmrk--34\">\n<h1 id=\"bkmrk-34.-fases-del-bootst\">34. Fases del bootstrap</h1>\n<h2 id=\"bkmrk-fase-0-%E2%80%94-github\">Fase 0 \u2014 GitHub</h2>\n<p id=\"bkmrk-crear-organizaci%C3%B3n-y\">Crear organizaci\u00f3n y repos.</p>\n<h2 id=\"bkmrk-fase-1-%E2%80%94-inventario\">Fase 1 \u2014 Inventario</h2>\n<p id=\"bkmrk-proxmox-%2B-networking\">Proxmox + networking + storage.</p>\n<h2 id=\"bkmrk-fase-2-%E2%80%94-iac\">Fase 2 \u2014 IaC</h2>\n<p id=\"bkmrk-opentofu-%2B-ansible.\">OpenTofu + Ansible.</p>\n<h2 id=\"bkmrk-fase-3-%E2%80%94-ops-host\">Fase 3 \u2014 Ops host</h2>\n<p id=\"bkmrk-wetopi-ops01.\"><code>wetopi-ops01</code>.</p>\n<h2 id=\"bkmrk-fase-4-%E2%80%94-rke2-ha\">Fase 4 \u2014 RKE2 HA</h2>\n<p id=\"bkmrk-3-control-planes-%2B-3\">3 control planes + 3 platform workers.</p>\n<h2 id=\"bkmrk-fase-5-%E2%80%94-networking\">Fase 5 \u2014 Networking</h2>\n<p id=\"bkmrk-cilium-%2B-hubble.\">Cilium + Hubble.</p>\n<h2 id=\"bkmrk-fase-6-%E2%80%94-storage\">Fase 6 \u2014 Storage</h2>\n<p id=\"bkmrk-ceph-csi.\">Ceph CSI.</p>\n<h2 id=\"bkmrk-fase-7-%E2%80%94-gitops\">Fase 7 \u2014 GitOps</h2>\n<p id=\"bkmrk-reconciliaci%C3%B3n-desde\">Reconciliaci\u00f3n desde Git.</p>\n<h2 id=\"bkmrk-fase-8-%E2%80%94-secrets\">Fase 8 \u2014 Secrets</h2>\n<p id=\"bkmrk-openbao.\">OpenBao.</p>\n<h2 id=\"bkmrk-fase-9-%E2%80%94-docs-%2F-memo\">Fase 9 \u2014 Docs / Memory / Work Ledger</h2>\n<p id=\"bkmrk-servicios-ag%C3%A9nticos-\">Servicios ag\u00e9nticos compartidos.</p>\n<h2 id=\"bkmrk-fase-10-%E2%80%94-hermes-%2B-a\">Fase 10 \u2014 Hermes + ADK</h2>\n<p id=\"bkmrk-despliegue-definitiv\">Despliegue definitivo.</p>\n<h2 id=\"bkmrk-fase-11-%E2%80%94-edge\">Fase 11 \u2014 Edge</h2>\n<p id=\"bkmrk-primer-tr%C3%A1fico-http-\">Primer tr\u00e1fico HTTP en RKE2.</p>\n<h2 id=\"bkmrk-fase-12-%E2%80%94-hosting\">Fase 12 \u2014 Hosting</h2>\n<p id=\"bkmrk-primeros-wordpress.\">Primeros WordPress.</p>\n<h2 id=\"bkmrk-fase-13-%E2%80%94-migraci%C3%B3n-\">Fase 13 \u2014 Migraci\u00f3n masiva</h2>\n<p id=\"bkmrk-swarm--%3E-rke2.\">Swarm -&gt; RKE2.</p>\n<hr id=\"bkmrk--35\">\n<h1 id=\"bkmrk-35.-checklist-de-arr\">35. Checklist de arranque</h1>\n<h2 id=\"bkmrk-github\">GitHub</h2>\n<ul id=\"bkmrk-organizaci%C3%B3n-wetopi-\">\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> organizaci\u00f3n <code>wetopi-platform</code></li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> repo <code>infrastructure</code></li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> repo <code>agent-services</code></li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> repo <code>ai-hub</code></li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> repo <code>agents</code></li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> repo <code>docs</code></li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Codex autorizado</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> branch protection</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> CODEOWNERS</li>\n</ul>\n<h2 id=\"bkmrk-proxmox\">Proxmox</h2>\n<ul id=\"bkmrk-inventario-bridges-i\">\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> inventario</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> bridges</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> IP ranges</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> failure domains</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Ubuntu template</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> automation account</li>\n</ul>\n<h2 id=\"bkmrk-bootstrap-1\">Bootstrap</h2>\n<ul id=\"bkmrk-wetopi-ops01-rke-cp0\">\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> <code>wetopi-ops01</code></li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> <code>rke-cp01</code></li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> <code>rke-cp02</code></li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> <code>rke-cp03</code></li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> <code>rke-platform01</code></li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> <code>rke-platform02</code></li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> <code>rke-platform03</code></li>\n</ul>\n<h2 id=\"bkmrk-networking\">Networking</h2>\n<ul id=\"bkmrk-una-nic-lan-por-vm-e\">\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> una NIC LAN por VM</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> evitar overlaps</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> no modificar <code>192.168.96.0/20</code></li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> definir Pod CIDR</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> definir Service CIDR</li>\n</ul>\n<h2 id=\"bkmrk-rke2-1\">RKE2</h2>\n<ul id=\"bkmrk-ha-embedded-etcd-api\">\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> HA</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> embedded etcd</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> API stable endpoint</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> snapshots</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> restore tested</li>\n</ul>\n<h2 id=\"bkmrk-platform\">Platform</h2>\n<ul id=\"bkmrk-cilium-hubble-ceph-c\">\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Cilium</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Hubble</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Ceph CSI</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> GitOps</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> OpenBao</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> MkDocs</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Memory Plane</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Work Ledger</li>\n</ul>\n<h2 id=\"bkmrk-agents\">Agents</h2>\n<ul id=\"bkmrk-hermes-sre-hermes-de-1\">\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Hermes SRE</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Hermes Developer</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Hermes Marketing</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> ADK Fleet</li>\n</ul>\n<h2 id=\"bkmrk-migration\">Migration</h2>\n<ul id=\"bkmrk-edge-nodes-gateway-a\">\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Edge nodes</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Gateway API</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> first hosting workers</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> first 10 sites</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> production validation</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> progressive migration</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Swarm retirement</li>\n</ul>\n<hr id=\"bkmrk--36\">\n<h1 id=\"bkmrk-36.-resultado-espera\">36. Resultado esperado</h1>\n<p id=\"bkmrk-al-finalizar-el-boot\">Al finalizar el bootstrap inicial:</p>\n<pre id=\"bkmrk-github-wetopi-platfo\"><code class=\"language-text\">                    GitHub\n             wetopi-platform\n                    \u2502\n             infrastructure\n                    \u2502\n                 Codex\n                    \u2502\n               wetopi-ops01\n                    \u2502\n             OpenTofu/Ansible\n                    \u2502\n                    \u25bc\n             \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n             \u2502   RKE2 HA     \u2502\n             \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                    \u2502\n       \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n       \u2502            \u2502            \u2502\n   3 Control     Cilium      3 Platform\n    Planes        Hubble       Workers\n       \u2502            \u2502            \u2502\n       \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                    \u2502\n                 Ceph CSI\n                    \u2502\n                  GitOps\n                    \u2502\n                 OpenBao\n                    \u2502\n        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n        \u25bc           \u25bc           \u25bc\n      Docs       Memory      Work Ledger\n        \u2502           \u2502           \u2502\n        \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                    \u2502\n             Hermes + ADK\n</code></pre>\n<p id=\"bkmrk-todav%C3%ADa-sin-necesida\">Todav\u00eda sin necesidad de haber movido un solo cliente.</p>\n<p id=\"bkmrk-despu%C3%A9s%3A-1\">Despu\u00e9s:</p>\n<pre id=\"bkmrk-edge-%E2%86%93-first-hosting\"><code class=\"language-text\">Edge\n \u2193\nfirst hosting workers\n \u2193\nfirst websites\n \u2193\nprogressive migration\n \u2193\nSwarm retirement\n</code></pre>\n<hr id=\"bkmrk--37\">\n<h1 id=\"bkmrk-conclusi%C3%B3n\">Conclusi\u00f3n</h1>\n<p id=\"bkmrk-la-estrategia-de-arr\">La estrategia de arranque queda fijada as\u00ed:</p>\n<ol id=\"bkmrk-git-primero.-iac-des\">\n<li>Git primero.</li>\n<li>IaC desde el primer d\u00eda.</li>\n<li>Bootstrap desde fuera de Kubernetes.</li>\n<li>RKE2 greenfield.</li>\n<li>Tres control planes.</li>\n<li>Tres platform workers.</li>\n<li>Una sola NIC LAN por VM.</li>\n<li>No tocar Ceph <code>192.168.96.0/20</code>.</li>\n<li>Cilium/Hubble.</li>\n<li>Ceph CSI.</li>\n<li>GitOps.</li>\n<li>OpenBao.</li>\n<li>Docs, Memory y Work Ledger.</li>\n<li>Hermes y ADK ya dentro del cluster definitivo.</li>\n<li>Edge despu\u00e9s.</li>\n<li>Hosting despu\u00e9s.</li>\n<li>Migraci\u00f3n progresiva desde Swarm.</li>\n</ol>\n<p id=\"bkmrk-la-prioridad-es-crea\">La prioridad es crear una foundation que pueda romperse, reconstruirse y probarse sin afectar a producci\u00f3n, y solo despu\u00e9s empezar a trasladar tr\u00e1fico y workloads reales.</p>\n"
 },
 {
  "id": 56,
  "title": "decisions log",
  "slug": "decisions-log",
  "book_id": 8,
  "chapter_id": 29,
  "description": null,
  "created_at": "2026-09-06T13:39:02.000000Z",
  "updated_at": "2026-09-06T13:39:02.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (design/decisions-log.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n# Wetopi Platform \u2014 Memoria t\u00e9cnica y ejecutiva de decisiones recientes\n## RKE2, networking, GitOps, Vault y bootstrap para Codex\n\n**Fecha:** 28 de agosto de 2026  \n**Estado:** Decisiones consolidadas para implementaci\u00f3n  \n**Organizaci\u00f3n GitHub:** `wetopi-platform`  \n**Audiencia principal:** Codex / agentes de implementaci\u00f3n / operadores SRE  \n**Objetivo:** preservar el contexto t\u00e9cnico y ejecutivo de las \u00faltimas decisiones para continuar el proyecto sin reabrir debates ya cerrados.\n\n---\n\n# \u00cdndice\n\n1. Resumen ejecutivo\n2. Contexto del proyecto\n3. Decisiones consolidadas\n4. RKE2 y topolog\u00eda inicial\n5. Bootstrap externo y `wetopi-ops01`\n6. Organizaci\u00f3n GitHub y repos\n7. OpenTofu + Ansible\n8. Networking de las VMs\n9. Ceph existente\n10. CIDRs propuestos\n11. Cilium + Hubble\n12. Ingress WAN: IPVS + VRRP + DSR\n13. Edge nodes\n14. Traefik + Gateway API\n15. Sustituci\u00f3n de Consul/Registrator/consul-template\n16. Egress simple con NATGW Ubuntu\n17. Selecci\u00f3n de egress por site/customer\n18. Argo CD\n19. Infisical como Wetopi Vault\n20. SOPS + age como bootstrap\n21. Kubernetes Secrets como materializaci\u00f3n runtime\n22. Modelo de acceso de agentes a secrets\n23. Tool runtimes y aislamiento de credenciales\n24. Hermes personales y coding agents\n25. ServiceAccounts, RBAC y namespaces\n26. Secrets encryption at rest\n27. Orden de bootstrap actualizado\n28. Primeras tareas para Codex\n29. Decisiones a\u00fan abiertas\n30. Criterios de aceptaci\u00f3n\n31. Resumen final para implementaci\u00f3n\n32. PLATFORM-018 \u2014 CNI Cilium desde el d\u00eda uno (sin Canal provisional)\n\n---\n\n# 1. Resumen ejecutivo\n\nWetopi est\u00e1 reconstruyendo su plataforma de Managed WordPress Hosting desde Docker Swarm hacia una nueva base RKE2 sobre su cloud privado Proxmox + Ceph.\n\nLa nueva plataforma debe ser AI-first y operable tanto por humanos como por agentes.\n\nLas decisiones ya cerradas son:\n\n```text\nKubernetes distribution:\nRKE2\n\nCNI:\nCilium\n\nNetwork observability:\nHubble\n\nGitOps:\nArgo CD\n\nVault / secrets manager:\nInfisical self-hosted\n\nBootstrap secret encryption:\nSOPS + age\n\nInfrastructure as Code:\nOpenTofu + Ansible\n\nIngress WAN:\nmantener IPVS + VRRP + DSR inicialmente\n\nProxy edge inicial:\nTraefik\n\nHTTP routing:\nGateway API\n\nEgress:\nNATGW Ubuntu simples con gateway interno estable y WAN IP cambiable\n```\n\nEl cluster inicial se construir\u00e1 greenfield, en paralelo al Docker Swarm actual. No habr\u00e1 big-bang.\n\nLa plataforma se levantar\u00e1 primero, se validar\u00e1, y despu\u00e9s se migrar\u00e1n edge nodes, hosting workers y sites de forma progresiva.\n\n---\n\n# 2. Contexto del proyecto\n\nWetopi es un Managed WordPress Hosting ejecutado actualmente sobre:\n\n```text\nOVH\n \u2193\nvRack\n \u2193\nProxmox\n \u2193\nCeph\n \u2193\nDocker Swarm\n```\n\nEl workload principal son `wepod`, aproximadamente un contenedor por website, con:\n\n```text\ns6\nnginx\nphp-fpm\nmysql\n```\n\nEl tr\u00e1fico WAN actual utiliza:\n\n```text\nVIP\n \u2193\nVRRP / Keepalived\n \u2193\nIPVS\n \u2193\nDSR\n \u2193\nWAN nginx proxies\n \u2193\nSwarm workloads\n```\n\nEl descubrimiento de backends se realiza mediante:\n\n```text\nRegistrator\n \u2193\nConsul\n \u2193\nconsul-template\n \u2193\nnginx.conf\n```\n\nLa nueva plataforma debe preservar las propiedades valiosas de esta arquitectura, especialmente DSR y la distribuci\u00f3n del ancho de banda de salida entre distintos hosts f\u00edsicos de OVH.\n\n---\n\n# 3. Decisiones consolidadas\n\n## PLATFORM-001 \u2014 RKE2\n\nRKE2 ser\u00e1 la distribuci\u00f3n Kubernetes de producci\u00f3n.\n\n## PLATFORM-002 \u2014 Greenfield\n\nEl cluster nuevo se construir\u00e1 en paralelo a Swarm.\n\n## PLATFORM-003 \u2014 3 control planes\n\nEl control plane inicial tendr\u00e1 tres nodos RKE2 server con embedded etcd.\n\n## PLATFORM-004 \u2014 3 platform workers\n\nInicialmente habr\u00e1 tres workers dedicados a servicios de plataforma.\n\n## PLATFORM-005 \u2014 Bootstrap externo\n\nKubernetes se construye desde fuera del cluster.\n\n## PLATFORM-006 \u2014 `wetopi-ops01`\n\nExistir\u00e1 una VM externa de operaciones, bootstrap y recovery.\n\n## PLATFORM-007 \u2014 Cilium\n\nCilium ser\u00e1 el CNI.\n\n## PLATFORM-008 \u2014 Hubble\n\nHubble ser\u00e1 la base de network observability.\n\n## PLATFORM-009 \u2014 Argo CD\n\nArgo CD ser\u00e1 el sistema GitOps.\n\n## PLATFORM-010 \u2014 Infisical\n\nInfisical self-hosted ser\u00e1 el Vault operativo de Wetopi.\n\n## PLATFORM-011 \u2014 SOPS + age\n\nSOPS + age se utilizar\u00e1 para bootstrap secrets, no como Vault principal.\n\n## PLATFORM-012 \u2014 IPVS/DSR\n\nSe preserva IPVS + VRRP + DSR inicialmente.\n\n## PLATFORM-013 \u2014 Traefik\n\nTraefik ser\u00e1 el proxy edge inicial.\n\n## PLATFORM-014 \u2014 Gateway API\n\nEl routing HTTP se expresar\u00e1 con Gateway API.\n\n## PLATFORM-015 \u2014 Egress simple\n\nSe usar\u00e1n NATGW Ubuntu simples con gateway interno estable y WAN IP cambiable.\n\n## PLATFORM-016 \u2014 No Istio inicialmente\n\nNo se desplegar\u00e1 service mesh sin necesidad concreta.\n\n## PLATFORM-017 \u2014 No BGP como requisito inicial\n\nLa plataforma no depender\u00e1 de BGP para arrancar.\n\n---\n\n# 4. RKE2 y topolog\u00eda inicial\n\nSe crear\u00e1n inicialmente:\n\n```text\n1 x wetopi-ops01\n3 x RKE2 control plane\n3 x platform workers\n```\n\nTotal inicial:\n\n```text\n7 VMs\n```\n\nNombres previstos:\n\n```text\nwetopi-ops01\n\nrke-cp01\nrke-cp02\nrke-cp03\n\nrke-platform01\nrke-platform02\nrke-platform03\n```\n\nLos control planes y workers deben distribuirse entre distintos hosts Proxmox.\n\nEjemplo:\n\n```text\npve01 -> rke-cp01 + rke-platform01\npve02 -> rke-cp02 + rke-platform02\npve03 -> rke-cp03 + rke-platform03\n```\n\nLa distribuci\u00f3n real deber\u00e1 validarse con el inventario.\n\n---\n\n# 5. Bootstrap externo y `wetopi-ops01`\n\nLa regla es:\n\n> Kubernetes no debe depender de servicios internos de Kubernetes para poder crearse o repararse.\n\nModelo:\n\n```text\nLaptop\n  \u2502\nCodex\n  \u2502\nGitHub\n  \u2502\nwetopi-ops01\n  \u2502\nOpenTofu + Ansible\n  \u2502\nProxmox\n  \u2502\nRKE2\n```\n\nCodex trabajar\u00e1 inicialmente desde el laptop del operador.\n\n`wetopi-ops01` ser\u00e1 la m\u00e1quina durable de bootstrap y disaster recovery.\n\nDebe tener, como m\u00ednimo:\n\n```text\ngit\nssh\nansible\nopentofu\nkubectl\nhelm\njq\nyq\n```\n\nNo alojar\u00e1 workloads productivos.\n\n---\n\n# 6. Organizaci\u00f3n GitHub y repos\n\nOrganizaci\u00f3n:\n\n```text\nwetopi-platform\n```\n\nRepos iniciales:\n\n```text\nwetopi-platform/infrastructure\nwetopi-platform/agent-services\nwetopi-platform/ai-hub\nwetopi-platform/agents\nwetopi-platform/docs\n```\n\nNo crear repos adicionales sin necesidad real.\n\n---\n\n# 7. OpenTofu + Ansible\n\nSe selecciona:\n\n```text\nOpenTofu\n+\nAnsible\n```\n\nOpenTofu:\n\n- crea VMs;\n- configura recursos Proxmox;\n- modela infraestructura declarativa.\n\nAnsible:\n\n- baseline Ubuntu;\n- sysctl;\n- paquetes;\n- RKE2;\n- configuraci\u00f3n de hosts;\n- operaciones bootstrap.\n\nGit es source of truth.\n\nNo se considera v\u00e1lido dejar cambios permanentes solo en shell history.\n\n---\n\n# 8. Networking de las VMs\n\nCada VM tendr\u00e1 inicialmente:\n\n```text\n1 NIC LAN\n```\n\nModelo:\n\n```text\nVM\n \u2514\u2500\u2500 eth0\n      \u2514\u2500\u2500 LAN / vRack\n```\n\nNo se introducir\u00e1n m\u00faltiples interfaces salvo necesidad demostrada.\n\n---\n\n# 9. Ceph existente\n\nLa red Ceph actual es:\n\n```text\n192.168.96.0/20\n```\n\nEs parte de la infraestructura hiperconvergida Proxmox + Ceph.\n\nDecisi\u00f3n:\n\n> NO modificar, renumerar ni redise\u00f1ar esta red.\n\nCeph se consumir\u00e1 desde Kubernetes mediante CSI.\n\nLa conectividad exacta entre RKE2 nodes y Ceph debe comprobarse, no asumirse.\n\n---\n\n# 10. CIDRs propuestos\n\nPropuesta actual:\n\n```text\nNode LAN:\npendiente de inventario real\n\nPod CIDR:\n10.64.0.0/14\n\nService CIDR:\n10.68.0.0/16\n\nCeph:\n192.168.96.0/20\nEXISTENTE / NO TOCAR\n```\n\nLos CIDRs definitivos deben validarse contra todas las redes Wetopi existentes.\n\n---\n\n# 11. Cilium + Hubble\n\nCilium ser\u00e1 el CNI base.\n\nPrimera versi\u00f3n:\n\n```text\nCilium\n+\nVXLAN encapsulation\n+\nHubble\n```\n\nRaz\u00f3n de VXLAN inicial:\n\n- m\u00ednima dependencia de underlay;\n- no requiere que la red f\u00edsica conozca PodCIDRs;\n- simplifica bootstrap;\n- una sola NIC por VM.\n\nNo activar inicialmente:\n\n- BGP WAN;\n- service mesh;\n- egress avanzado;\n- features experimentales innecesarias.\n\nHubble se utilizar\u00e1 para flows y correlaci\u00f3n de tr\u00e1fico con pods/namespaces/sites.\n\n---\n\n# 12. Ingress WAN: IPVS + VRRP + DSR\n\nEl frontend actual se conserva inicialmente.\n\n```text\nInternet\n \u2193\nVIP\n \u2193\nVRRP\n \u2193\nIPVS\n \u2193\nDSR\n \u2193\nRKE2 Edge\n```\n\nMotivo principal:\n\nDSR permite que cada edge node responda directamente a Internet usando el uplink/bandwidth domain del host f\u00edsico Proxmox/OVH donde corre.\n\nEsto permite agregar capacidad de salida entre varios hosts f\u00edsicos.\n\nEsta propiedad no debe perderse durante la migraci\u00f3n.\n\n---\n\n# 13. Edge nodes\n\nLos antiguos WAN proxies evolucionar\u00e1n a RKE2 edge workers.\n\nEjemplo:\n\n```text\nrke-edge-a01\nrke-edge-a02\nrke-edge-b01\nrke-edge-b02\n```\n\nLabels:\n\n```text\nwetopi.io/role=edge\nwetopi.io/wan-zone=a\nwetopi.io/proxmox-host=pve01\nwetopi.io/bandwidth-domain=pve01\n```\n\nTaint:\n\n```text\nwetopi.io/edge=true:NoSchedule\n```\n\nEl proxy se desplegar\u00e1 como DaemonSet cuando tenga sentido mantener una instancia por edge node.\n\n---\n\n# 14. Traefik + Gateway API\n\nSe elige Traefik inicialmente.\n\nRazones:\n\n- sencillo;\n- Kubernetes-native;\n- soporte de Gateway API;\n- TLS;\n- routing din\u00e1mico;\n- m\u00e9tricas;\n- separaci\u00f3n clara respecto a Cilium.\n\nModelo:\n\n```text\nhostname\n \u2193\nHTTPRoute\n \u2193\nService\n \u2193\nEndpointSlice\n \u2193\nPod\n```\n\nEnvoy podr\u00e1 reevaluarse m\u00e1s adelante si aparecen necesidades L7 m\u00e1s avanzadas.\n\n---\n\n# 15. Sustituci\u00f3n de Consul/Registrator/consul-template\n\nAntes:\n\n```text\nDocker\n \u2193\nRegistrator\n \u2193\nConsul\n \u2193\nconsul-template\n \u2193\nnginx.conf\n \u2193\nreload\n```\n\nDespu\u00e9s:\n\n```text\nPod\n \u2193\nService\n \u2193\nEndpointSlice\n \u2193\nGateway controller\n \u2193\ndynamic routing\n```\n\nKubernetes ser\u00e1 el service registry del hosting.\n\n---\n\n# 16. Egress simple con NATGW Ubuntu\n\nNo se construir\u00e1 inicialmente un egress control plane complejo.\n\nModelo base:\n\n```text\ngw100\nLAN: 192.168.88.100\nWAN: PUBLIC_IP_A\n\ngw101\nLAN: 192.168.88.101\nWAN: PUBLIC_IP_B\n\ngw102\nLAN: 192.168.88.102\nWAN: PUBLIC_IP_C\n```\n\nCada gateway:\n\n```text\nUbuntu\n+\nrouting\n+\nnftables/iptables\n+\nSNAT\n```\n\nObjetivo:\n\n- f\u00e1cil de operar;\n- f\u00e1cil de cambiar;\n- f\u00e1cil de observar;\n- f\u00e1cil de separar reputaci\u00f3n.\n\n---\n\n# 17. Selecci\u00f3n de egress por site/customer\n\nModelo l\u00f3gico:\n\n```text\nsite/customer\n \u2193\negress_gateway\n \u2193\ngw100 / gw101 / gw102\n```\n\nEjemplo:\n\n```yaml\nwetopi.io/egress-gateway: gw101\n```\n\nLa implementaci\u00f3n inicial puede usar:\n\n- policy routing;\n- automatizaci\u00f3n ligera;\n- helper/DaemonSet que observe labels y mantenga reglas.\n\nNo construir todav\u00eda un controlador complejo.\n\nFuturo:\n\n```text\nCiliumEgressGatewayPolicy\n```\n\npuede sustituir el mecanismo sin cambiar el modelo l\u00f3gico.\n\nSeparar desde el inicio:\n\n```text\nhosting-general\nhosting-premium\nplatform\nagents/crawlers\n```\n\nLos crawlers/agentes no deben compartir IP de salida con customers.\n\n---\n\n# 18. Argo CD\n\nSe ha decidido usar:\n\n```text\nArgo CD\n```\n\nRazones principales:\n\n- excelente Web UI;\n- estado Synced/OutOfSync;\n- health;\n- diffs;\n- historial;\n- visibilidad para operadores humanos;\n- modelo declarativo;\n- API/CLI;\n- RBAC/SSO/OIDC futuro.\n\nModelo:\n\n```text\nGit\n \u2193\nArgo CD\n \u2193\nKubernetes\n```\n\nNo se utilizar\u00e1 un pipeline CI basado en `kubectl apply` como mecanismo normal de producci\u00f3n.\n\nPendiente decidir el layout exacto:\n\n- App of Apps;\n- ApplicationSet;\n- AppProjects;\n- estructura por cluster/environment.\n\n---\n\n# 19. Infisical como Wetopi Vault\n\nEl Vault operativo de Wetopi ser\u00e1:\n\n```text\nInfisical self-hosted\n```\n\nEl requisito real no es solamente almacenar secrets declarativos.\n\nSe necesita un servicio vivo donde humanos y agentes puedan:\n\n- crear credentials;\n- guardar username/password;\n- guardar API tokens;\n- guardar SSH keys;\n- recuperar secrets;\n- rotarlos;\n- listarlos;\n- actualizarlos;\n- auditarlos;\n- compartirlos con servicios.\n\nEjemplo conceptual:\n\n```text\nvault.put(\n  path=\"/platform/proxmox/api\",\n  username=\"automation\",\n  password=\"...\"\n)\n```\n\nInfisical ser\u00e1 el source of truth operativo de credentials.\n\n---\n\n# 20. SOPS + age como bootstrap\n\nSOPS NO ser\u00e1 el Vault principal.\n\nSe utilizar\u00e1 \u00fanicamente para bootstrap.\n\nModelo:\n\n```text\nGit\n \u2193\nSOPS + age\n \u2193\nbootstrap secrets\n \u2193\nInfisical starts\n```\n\nSOPS protege los secretos necesarios para levantar:\n\n- Infisical;\n- integraci\u00f3n inicial de Argo CD;\n- bootstrap identities;\n- recovery material m\u00ednimo.\n\nUna vez Infisical funciona, los nuevos credentials se guardan en Infisical, no en Git.\n\nDebe existir al menos una recovery key `age` fuera del cluster y fuera de Git.\n\n---\n\n# 21. Kubernetes Secrets como materializaci\u00f3n runtime\n\nModelo:\n\n```text\nInfisical\n    \u2502\n    \u2502 sync/operator\n    \u25bc\nKubernetes Secret\n    \u2502\n    \u25bc\nDeployment / Pod\n```\n\nInfisical es source of truth.\n\nKubernetes Secret es materializaci\u00f3n runtime.\n\nEsto permite:\n\n- cambiar un secret centralmente;\n- sincronizarlo al cluster;\n- consumirlo con mecanismos nativos de Kubernetes.\n\n---\n\n# 22. Modelo de acceso de agentes a secrets\n\nLa regla principal es:\n\n> el agente debe poder USAR un secret sin necesidad de VER su valor cuando sea posible.\n\nEjemplo:\n\n```text\nHermes\n  \u2502\n  \u2502 tool call\n  \u25bc\nplatform-tools\n  \u2502\n  \u2502 reads secret from Infisical/K8s Secret\n  \u25bc\nProxmox / Cloudflare / OVH\n```\n\nEl LLM no necesita recibir el token.\n\n---\n\n# 23. Tool runtimes y aislamiento de credenciales\n\nNo montar todos los secrets directamente en el proceso LLM.\n\nAgrupar tools por security domain.\n\nEjemplo:\n\n```text\nplatform-tools\n\u251c\u2500\u2500 kubernetes\n\u251c\u2500\u2500 proxmox\n\u251c\u2500\u2500 ceph\n\u2514\u2500\u2500 networking\n\nwordpress-tools\n\u251c\u2500\u2500 wordpress\n\u251c\u2500\u2500 mysql\n\u2514\u2500\u2500 backups\n\nmarketing-tools\n\u251c\u2500\u2500 intercom\n\u251c\u2500\u2500 analytics\n\u2514\u2500\u2500 ...\n```\n\nCada runtime recibe \u00fanicamente los secrets necesarios.\n\nNo crear un sidecar por tool individual.\n\nEn `ai-hub`, las tools deber\u00edan declarar sus requirements.\n\nEjemplo:\n\n```yaml\ntool: proxmox.vm_restart\nruntime: platform-tools\nsecrets:\n  - proxmox-api-token\n```\n\nLos agentes deben consumir un contrato propio Wetopi, no acoplarse directamente a Infisical.\n\nAPI conceptual:\n\n```text\nvault.get()\nvault.put()\nvault.update()\nvault.delete()\nvault.list()\nvault.exists()\n```\n\n---\n\n# 24. Hermes personales y coding agents\n\nHermes personales/coding agents necesitar\u00e1n operar la plataforma.\n\nPodr\u00e1n:\n\n- llamar tools privilegiadas;\n- usar credentials;\n- escribir secrets en Vault;\n- recuperar credentials autorizados;\n- operar GitHub;\n- operar Proxmox;\n- operar Kubernetes.\n\nPero:\n\n- no recibir todas las credenciales en env;\n- no montar un vault global;\n- no dar acceso indiscriminado a todos los paths.\n\nEl permiso debe seguir el rol.\n\nEjemplo conceptual:\n\n```text\nHermes SRE\n  /platform/*\n  /infrastructure/*\n  /agents/hermes-sre/*\n\nHermes Developer\n  /development/*\n  /github/*\n  /agents/hermes-developer/*\n\nHermes Marketing\n  /marketing/*\n  /agents/hermes-marketing/*\n```\n\n---\n\n# 25. ServiceAccounts, RBAC y namespaces\n\nPara operar Kubernetes, los agentes dentro del cluster usar\u00e1n:\n\n```text\nServiceAccount\n+\nRBAC\n```\n\nNo kubeconfig est\u00e1tico ni password.\n\nEjemplo:\n\n```text\nHermes SRE\n -> permisos operacionales amplios\n\nHermes Developer\n -> namespaces dev + deployments\n\nADK observer\n -> readonly\n\nADK specialist\n -> permissions espec\u00edficas\n```\n\nNamespaces orientativos:\n\n```text\nwetopi-platform\nwetopi-edge\nwetopi-hosting\nwetopi-agents-sre\nwetopi-agents-development\nwetopi-agents-marketing\nwetopi-observability\nwetopi-security\n```\n\nLos namespaces forman parte de la frontera de secrets y RBAC.\n\nImportante:\n\nquien puede crear Pods en un namespace puede potencialmente montar Secrets de ese namespace.\n\nPor tanto, permisos de workload creation y secret access deben dise\u00f1arse conjuntamente.\n\n---\n\n# 26. Secrets encryption at rest\n\nSOPS protege Git.\n\nInfisical protege el Vault.\n\nKubernetes Secrets materializados deben protegerse en etcd.\n\nRKE2 deber\u00e1 tener:\n\n```text\nSecrets Encryption at Rest\n```\n\nhabilitado.\n\nModelo:\n\n```text\nInfisical\n \u2193\nKubernetes Secret\n \u2193\nRKE2 API server\n \u2193\nencrypted etcd storage\n```\n\n---\n\n# 27. Orden de bootstrap actualizado\n\nOrden actual recomendado:\n\n```text\n1. GitHub organization + repos\n2. Inventario Proxmox/redes\n3. OpenTofu + Ansible\n4. wetopi-ops01\n5. 3 x RKE2 control plane\n6. 3 x platform workers\n7. RKE2 HA + etcd\n8. Cilium\n9. Hubble\n10. Ceph CSI\n11. Argo CD\n12. SOPS + age bootstrap\n13. Infisical\n14. Kubernetes Secrets integration/operator\n15. Docs\n16. Memory Plane\n17. Work Ledger\n18. AI Hub\n19. Hermes\n20. ADK Fleet\n21. Edge nodes\n22. Gateway API + Traefik\n23. NATGW\n24. first hosting workers\n25. first websites\n26. progressive migration from Swarm\n```\n\n---\n\n# 28. Primeras tareas para Codex\n\n## Task 1 \u2014 Crear repos\n\nCrear:\n\n```text\ninfrastructure\nagent-services\nai-hub\nagents\ndocs\n```\n\nAplicar:\n\n- README;\n- CODEOWNERS;\n- branch protection;\n- CI m\u00ednima;\n- convenciones.\n\n## Task 2 \u2014 Inventario Proxmox\n\nRecoger:\n\n- nodos;\n- CPU;\n- RAM;\n- storage;\n- bridges;\n- VLANs;\n- redes;\n- IP ranges;\n- failure domains;\n- bandwidth domains.\n\nNo crear a\u00fan direccionamiento inventado.\n\n## Task 3 \u2014 Validar redes\n\nConfirmar que:\n\n```text\nPod CIDR 10.64.0.0/14\nService CIDR 10.68.0.0/16\n```\n\nno colisionan.\n\nConfirmar conectividad RKE2 node -> Ceph `192.168.96.0/20`.\n\n## Task 4 \u2014 Ubuntu template\n\nCrear template reproducible Ubuntu 24.04 + cloud-init.\n\n## Task 5 \u2014 `wetopi-ops01`\n\nCrear/provisionar `wetopi-ops01`.\n\n## Task 6 \u2014 Control plane\n\nCrear:\n\n```text\nrke-cp01\nrke-cp02\nrke-cp03\n```\n\n## Task 7 \u2014 Platform workers\n\nCrear:\n\n```text\nrke-platform01\nrke-platform02\nrke-platform03\n```\n\n## Task 8 \u2014 RKE2\n\nConfigurar:\n\n- embedded etcd;\n- HA;\n- API endpoint estable;\n- secrets encryption;\n- snapshots.\n\n## Task 9 \u2014 Cilium/Hubble\n\nDesplegar:\n\n- Cilium VXLAN;\n- Hubble;\n- networking tests.\n\n## Task 10 \u2014 Ceph CSI\n\nValidar:\n\n- PVC create;\n- mount;\n- pod restart;\n- persistence.\n\n## Task 11 \u2014 Argo CD\n\nDesplegar declarativamente.\n\n## Task 12 \u2014 SOPS bootstrap\n\nGenerar:\n\n- age production key;\n- age recovery key;\n- `.sops.yaml`.\n\nGuardar recovery key fuera del cluster y fuera de Git.\n\n## Task 13 \u2014 Infisical\n\nDesplegar self-hosted.\n\nConfigurar:\n\n- persistence;\n- backups;\n- admin bootstrap;\n- projects/environments;\n- machine identities m\u00ednimas;\n- Kubernetes integration.\n\n## Task 14 \u2014 Vault contract\n\nCrear en `ai-hub` una capa estable:\n\n```text\nvault.get()\nvault.put()\nvault.update()\nvault.delete()\nvault.list()\nvault.exists()\n```\n\nLos agentes no deben quedar acoplados a la API concreta de Infisical.\n\n---\n\n# 29. Decisiones a\u00fan abiertas\n\n## 29.1 Node LAN CIDR\n\nPendiente de inventario.\n\n## 29.2 Endpoint HA de RKE2\n\nPropuesta previa:\n\n```text\n2 x HAProxy + Keepalived\n+\nprivate VIP\n```\n\nDebe cerrarse antes del deploy final.\n\n## 29.3 DSR validation\n\nDebe hacerse PoC real con capture.\n\n## 29.4 Traefik deployment exacto\n\nDefinir:\n\n- hostNetwork;\n- DaemonSet;\n- VIP handling;\n- health endpoint.\n\n## 29.5 Egress enforcement exacto\n\nDefinir implementaci\u00f3n inicial de:\n\n```text\nsite label\n \u2193\ngateway selection\n \u2193\npolicy routing\n```\n\n## 29.6 Infisical OSS/licensing\n\nValidar exactamente:\n\n- features necesarias en self-hosted;\n- machine identities;\n- Kubernetes integration;\n- audit;\n- versioning;\n- posibles l\u00edmites comerciales.\n\n## 29.7 GitOps layout\n\nDefinir estructura exacta Argo CD:\n\n- Application;\n- AppProject;\n- App of Apps o ApplicationSet.\n\n## 29.8 OIDC\n\nPendiente.\n\n## 29.9 Backup externo\n\nPendiente destino definitivo de:\n\n- etcd snapshots;\n- Infisical DB;\n- SOPS recovery material;\n- Git.\n\n## 29.10 Observabilidad central\n\nPendiente arquitectura final.\n\n---\n\n# 30. Criterios de aceptaci\u00f3n\n\nAntes de desplegar Hermes o workloads reales:\n\n## Cluster\n\n- [ ] p\u00e9rdida de 1 control plane no interrumpe API;\n- [ ] etcd mantiene quorum;\n- [ ] snapshot etcd funcional;\n- [ ] restore documentado y probado;\n- [ ] workers pueden drain/uncordon.\n\n## Networking\n\n- [ ] pod-to-pod;\n- [ ] pod-to-service;\n- [ ] DNS;\n- [ ] Hubble flows visibles;\n- [ ] no collision CIDRs;\n- [ ] nodes alcanzan Ceph.\n\n## Storage\n\n- [ ] PVC;\n- [ ] delete/recreate pod;\n- [ ] persistencia;\n- [ ] failure test b\u00e1sico.\n\n## GitOps\n\n- [ ] Argo CD reconciles desde Git;\n- [ ] manual drift detectable;\n- [ ] rollback verificable.\n\n## Vault\n\n- [ ] agente/tool puede crear secret;\n- [ ] servicio puede consumir secret;\n- [ ] secret no queda plaintext en Git;\n- [ ] access control validado;\n- [ ] backup de Infisical probado;\n- [ ] recovery bootstrap documentado.\n\n## Secrets\n\n- [ ] SOPS bootstrap funciona;\n- [ ] recovery age key offline;\n- [ ] Kubernetes secrets cifrados at rest;\n- [ ] namespaces/RBAC limitan acceso.\n\n## Agentes\n\n- [ ] Hermes puede usar tool privilegiada sin recibir raw token;\n- [ ] ADK readonly no puede leer secrets ajenos;\n- [ ] coding agent solo accede a vault paths autorizados.\n\n---\n\n# 31. Resumen final para implementaci\u00f3n\n\nLa platform foundation actual queda as\u00ed:\n\n```text\n                      GitHub\n               wetopi-platform\n                       \u2502\n               infrastructure\n                       \u2502\n                    Codex\n                       \u2502\n                  wetopi-ops01\n                       \u2502\n              OpenTofu + Ansible\n                       \u2502\n                       \u25bc\n                \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                \u2502   RKE2 HA   \u2502\n                \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                       \u2502\n              Cilium + Hubble\n                       \u2502\n                    Ceph CSI\n                       \u2502\n                    Argo CD\n                       \u2502\n                 SOPS bootstrap\n                       \u2502\n                    Infisical\n                       \u2502\n        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n        \u2502              \u2502              \u2502\n   Kubernetes       Tool runtimes    Humans\n    Secrets             \u2502           Web UI\n        \u2502               \u2502\n        \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                        \u2502              \u2502\n                      Hermes          ADK\n```\n\nNetworking:\n\n```text\nINGRESS\n\nInternet\n \u2193\nVRRP\n \u2193\nIPVS\n \u2193\nDSR\n \u2193\nRKE2 Edge\n \u2193\nTraefik\n \u2193\nGateway API\n \u2193\nService\n \u2193\nWebsite\n```\n\n```text\nEGRESS\n\nSite / Customer\n \u2193\negress label/profile\n \u2193\ngw100 / gw101 / gw102\n \u2193\nWAN IP\n \u2193\nInternet\n```\n\nLa intenci\u00f3n global es mantener la primera versi\u00f3n simple:\n\n- no Istio;\n- no BGP obligatorio;\n- no controller avanzado de egress;\n- no OpenBao;\n- no service mesh complejo;\n- no redise\u00f1o simult\u00e1neo de Ceph.\n\nLa plataforma se estabiliza primero.\n\nDespu\u00e9s se despliegan:\n\n```text\nDocs\nMemory Plane\nWork Ledger\nAI Hub\nHermes\nADK\nEdge\nHosting\n```\n\ny finalmente se inicia la migraci\u00f3n progresiva de Swarm a RKE2.\n\n---\n\n# 32. PLATFORM-018 \u2014 CNI Cilium desde el d\u00eda uno (sin Canal provisional)\n\n**Fecha:** 29 de agosto de 2026\n**Estado:** Decidido (operador). Refina PLATFORM-007 (Cilium ser\u00e1 el CNI) y\nPLATFORM-008 (Hubble). Se graba como PLATFORM-018 \u2014 el siguiente identificador\nlibre \u2014 porque PLATFORM-008 ya estaba asignado a Hubble en el registro original.\n\nDecisi\u00f3n operativa para el primer addon del cluster RKE2:\n\n- No habr\u00e1 CNI provisional Canal. Cilium es el CNI desde el primer nodo y\n  sustituye al Canal que RKE2 instala por defecto: la configuraci\u00f3n de\n  rke2-server debe fijar `cni: none`, y el rol de RKE2 debe mantener\n  deshabilitado el Canal incluido de serie.\n- Chart oficial Cilium fijado a la versi\u00f3n `1.20.1`\n  (`oci://quay.io/cilium/charts/cilium`, namespace `kube-system`). Las\n  versiones de chart no llevan prefijo `v`.\n- Encapsulaci\u00f3n VXLAN (`routingMode: tunnel`, `tunnelProtocol: vxlan`); sin\n  BGP y sin native routing inicialmente, seg\u00fan el plan maestro (14.4) y\n  PLATFORM-017.\n- IPAM `cluster-pool` con `clusterPoolIPv4PodCIDRList: [10.64.0.0/14]` y\n  `clusterPoolIPv4MaskSize: 24`, coincidiendo exactamente con el pod CIDR de\n  RKE2. RKE2 es quien posee el pod CIDR y el service CIDR (10.68.0.0/16);\n  Cilium no los re-deriva y no toca el CIDR de servicios.\n- `kubeProxyReplacement: false` en v1: kube-proxy est\u00e1ndar convive con Cilium\n  VXLAN. La sustituci\u00f3n de kube-proxy queda documentada como opci\u00f3n futura.\n- Hubble habilitado con Hubble Relay (`hubble.enabled: true`,\n  `hubble.relay.enabled: true`); `hubble.ui.enabled: false` por defecto,\n  activaci\u00f3n opcional documentada.\n- La MTU queda por defecto (auto ~1450 sobre el LAN de nodos MTU 1500).\n- Puertos obligatorios entre nodos: UDP 8472 (VXLAN) y TCP 4244 (Hubble\n  Relay).\n\nM\u00e9todo de instalaci\u00f3n: Argo CD no existe todav\u00eda en el primer bootstrap, as\u00ed\nque el addon se instala desde `wetopi-ops01` con Helm durante la fase de\napply (playbook `ansible/playbooks/cilium.yml` en el repo infrastructure).\nEs una instalaci\u00f3n solo de bootstrap: cuando Argo CD exista, la gesti\u00f3n del\naddon pasa a \u00e9l y el playbook se retira. Los valores pinneados viven en\n`kubernetes/cilium/values.yaml`; la lista de verificaci\u00f3n de post-instalaci\u00f3n\nen `kubernetes/cilium/VALIDATION.md`.\n\nCIDRs: el ownership es de RKE2 (pod CIDR 10.64.0.0/14 y service CIDR\n10.68.0.0/16); el pool de Cilium se limita a consumir el rango de pods y\nnunca debe diverger de la configuraci\u00f3n de rke2-server.",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (design/decisions-log.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<h1 id=\"bkmrk-wetopi-platform-%E2%80%94-me\">Wetopi Platform \u2014 Memoria t\u00e9cnica y ejecutiva de decisiones recientes</h1>\n<h2 id=\"bkmrk-rke2%2C-networking%2C-gi\">RKE2, networking, GitOps, Vault y bootstrap para Codex</h2>\n<p id=\"bkmrk-fecha%3A-28-de-agosto-\"><strong>Fecha:</strong> 28 de agosto de 2026<br>\n<strong>Estado:</strong> Decisiones consolidadas para implementaci\u00f3n<br>\n<strong>Organizaci\u00f3n GitHub:</strong> <code>wetopi-platform</code><br>\n<strong>Audiencia principal:</strong> Codex / agentes de implementaci\u00f3n / operadores SRE<br>\n<strong>Objetivo:</strong> preservar el contexto t\u00e9cnico y ejecutivo de las \u00faltimas decisiones para continuar el proyecto sin reabrir debates ya cerrados.</p>\n<hr id=\"bkmrk-\">\n<h1 id=\"bkmrk-%C3%8Dndice\">\u00cdndice</h1>\n<ol id=\"bkmrk-resumen-ejecutivo-co\">\n<li>Resumen ejecutivo</li>\n<li>Contexto del proyecto</li>\n<li>Decisiones consolidadas</li>\n<li>RKE2 y topolog\u00eda inicial</li>\n<li>Bootstrap externo y <code>wetopi-ops01</code></li>\n<li>Organizaci\u00f3n GitHub y repos</li>\n<li>OpenTofu + Ansible</li>\n<li>Networking de las VMs</li>\n<li>Ceph existente</li>\n<li>CIDRs propuestos</li>\n<li>Cilium + Hubble</li>\n<li>Ingress WAN: IPVS + VRRP + DSR</li>\n<li>Edge nodes</li>\n<li>Traefik + Gateway API</li>\n<li>Sustituci\u00f3n de Consul/Registrator/consul-template</li>\n<li>Egress simple con NATGW Ubuntu</li>\n<li>Selecci\u00f3n de egress por site/customer</li>\n<li>Argo CD</li>\n<li>Infisical como Wetopi Vault</li>\n<li>SOPS + age como bootstrap</li>\n<li>Kubernetes Secrets como materializaci\u00f3n runtime</li>\n<li>Modelo de acceso de agentes a secrets</li>\n<li>Tool runtimes y aislamiento de credenciales</li>\n<li>Hermes personales y coding agents</li>\n<li>ServiceAccounts, RBAC y namespaces</li>\n<li>Secrets encryption at rest</li>\n<li>Orden de bootstrap actualizado</li>\n<li>Primeras tareas para Codex</li>\n<li>Decisiones a\u00fan abiertas</li>\n<li>Criterios de aceptaci\u00f3n</li>\n<li>Resumen final para implementaci\u00f3n</li>\n<li>PLATFORM-018 \u2014 CNI Cilium desde el d\u00eda uno (sin Canal provisional)</li>\n</ol>\n<hr id=\"bkmrk--1\">\n<h1 id=\"bkmrk-1.-resumen-ejecutivo\">1. Resumen ejecutivo</h1>\n<p id=\"bkmrk-wetopi-est%C3%A1-reconstr\">Wetopi est\u00e1 reconstruyendo su plataforma de Managed WordPress Hosting desde Docker Swarm hacia una nueva base RKE2 sobre su cloud privado Proxmox + Ceph.</p>\n<p id=\"bkmrk-la-nueva-plataforma-\">La nueva plataforma debe ser AI-first y operable tanto por humanos como por agentes.</p>\n<p id=\"bkmrk-las-decisiones-ya-ce\">Las decisiones ya cerradas son:</p>\n<pre id=\"bkmrk-kubernetes-distribut\"><code class=\"language-text\">Kubernetes distribution:\nRKE2\n\nCNI:\nCilium\n\nNetwork observability:\nHubble\n\nGitOps:\nArgo CD\n\nVault / secrets manager:\nInfisical self-hosted\n\nBootstrap secret encryption:\nSOPS + age\n\nInfrastructure as Code:\nOpenTofu + Ansible\n\nIngress WAN:\nmantener IPVS + VRRP + DSR inicialmente\n\nProxy edge inicial:\nTraefik\n\nHTTP routing:\nGateway API\n\nEgress:\nNATGW Ubuntu simples con gateway interno estable y WAN IP cambiable\n</code></pre>\n<p id=\"bkmrk-el-cluster-inicial-s\">El cluster inicial se construir\u00e1 greenfield, en paralelo al Docker Swarm actual. No habr\u00e1 big-bang.</p>\n<p id=\"bkmrk-la-plataforma-se-lev\">La plataforma se levantar\u00e1 primero, se validar\u00e1, y despu\u00e9s se migrar\u00e1n edge nodes, hosting workers y sites de forma progresiva.</p>\n<hr id=\"bkmrk--2\">\n<h1 id=\"bkmrk-2.-contexto-del-proy\">2. Contexto del proyecto</h1>\n<p id=\"bkmrk-wetopi-es-un-managed\">Wetopi es un Managed WordPress Hosting ejecutado actualmente sobre:</p>\n<pre id=\"bkmrk-ovh-%E2%86%93-vrack-%E2%86%93-proxmo\"><code class=\"language-text\">OVH\n \u2193\nvRack\n \u2193\nProxmox\n \u2193\nCeph\n \u2193\nDocker Swarm\n</code></pre>\n<p id=\"bkmrk-el-workload-principa\">El workload principal son <code>wepod</code>, aproximadamente un contenedor por website, con:</p>\n<pre id=\"bkmrk-s6-nginx-php-fpm-mys\"><code class=\"language-text\">s6\nnginx\nphp-fpm\nmysql\n</code></pre>\n<p id=\"bkmrk-el-tr%C3%A1fico-wan-actua\">El tr\u00e1fico WAN actual utiliza:</p>\n<pre id=\"bkmrk-vip-%E2%86%93-vrrp-%2F-keepali\"><code class=\"language-text\">VIP\n \u2193\nVRRP / Keepalived\n \u2193\nIPVS\n \u2193\nDSR\n \u2193\nWAN nginx proxies\n \u2193\nSwarm workloads\n</code></pre>\n<p id=\"bkmrk-el-descubrimiento-de\">El descubrimiento de backends se realiza mediante:</p>\n<pre id=\"bkmrk-registrator-%E2%86%93-consul\"><code class=\"language-text\">Registrator\n \u2193\nConsul\n \u2193\nconsul-template\n \u2193\nnginx.conf\n</code></pre>\n<p id=\"bkmrk-la-nueva-plataforma--1\">La nueva plataforma debe preservar las propiedades valiosas de esta arquitectura, especialmente DSR y la distribuci\u00f3n del ancho de banda de salida entre distintos hosts f\u00edsicos de OVH.</p>\n<hr id=\"bkmrk--3\">\n<h1 id=\"bkmrk-3.-decisiones-consol\">3. Decisiones consolidadas</h1>\n<h2 id=\"bkmrk-platform-001-%E2%80%94-rke2\">PLATFORM-001 \u2014 RKE2</h2>\n<p id=\"bkmrk-rke2-ser%C3%A1-la-distrib\">RKE2 ser\u00e1 la distribuci\u00f3n Kubernetes de producci\u00f3n.</p>\n<h2 id=\"bkmrk-platform-002-%E2%80%94-green\">PLATFORM-002 \u2014 Greenfield</h2>\n<p id=\"bkmrk-el-cluster-nuevo-se-\">El cluster nuevo se construir\u00e1 en paralelo a Swarm.</p>\n<h2 id=\"bkmrk-platform-003-%E2%80%94-3-con\">PLATFORM-003 \u2014 3 control planes</h2>\n<p id=\"bkmrk-el-control-plane-ini\">El control plane inicial tendr\u00e1 tres nodos RKE2 server con embedded etcd.</p>\n<h2 id=\"bkmrk-platform-004-%E2%80%94-3-pla\">PLATFORM-004 \u2014 3 platform workers</h2>\n<p id=\"bkmrk-inicialmente-habr%C3%A1-t\">Inicialmente habr\u00e1 tres workers dedicados a servicios de plataforma.</p>\n<h2 id=\"bkmrk-platform-005-%E2%80%94-boots\">PLATFORM-005 \u2014 Bootstrap externo</h2>\n<p id=\"bkmrk-kubernetes-se-constr\">Kubernetes se construye desde fuera del cluster.</p>\n<h2 id=\"bkmrk-platform-006-%E2%80%94-wetop\">PLATFORM-006 \u2014 <code>wetopi-ops01</code></h2>\n<p id=\"bkmrk-existir%C3%A1-una-vm-exte\">Existir\u00e1 una VM externa de operaciones, bootstrap y recovery.</p>\n<h2 id=\"bkmrk-platform-007-%E2%80%94-ciliu\">PLATFORM-007 \u2014 Cilium</h2>\n<p id=\"bkmrk-cilium-ser%C3%A1-el-cni.\">Cilium ser\u00e1 el CNI.</p>\n<h2 id=\"bkmrk-platform-008-%E2%80%94-hubbl\">PLATFORM-008 \u2014 Hubble</h2>\n<p id=\"bkmrk-hubble-ser%C3%A1-la-base-\">Hubble ser\u00e1 la base de network observability.</p>\n<h2 id=\"bkmrk-platform-009-%E2%80%94-argo-\">PLATFORM-009 \u2014 Argo CD</h2>\n<p id=\"bkmrk-argo-cd-ser%C3%A1-el-sist\">Argo CD ser\u00e1 el sistema GitOps.</p>\n<h2 id=\"bkmrk-platform-010-%E2%80%94-infis\">PLATFORM-010 \u2014 Infisical</h2>\n<p id=\"bkmrk-infisical-self-hoste\">Infisical self-hosted ser\u00e1 el Vault operativo de Wetopi.</p>\n<h2 id=\"bkmrk-platform-011-%E2%80%94-sops-\">PLATFORM-011 \u2014 SOPS + age</h2>\n<p id=\"bkmrk-sops-%2B-age-se-utiliz\">SOPS + age se utilizar\u00e1 para bootstrap secrets, no como Vault principal.</p>\n<h2 id=\"bkmrk-platform-012-%E2%80%94-ipvs%2F\">PLATFORM-012 \u2014 IPVS/DSR</h2>\n<p id=\"bkmrk-se-preserva-ipvs-%2B-v\">Se preserva IPVS + VRRP + DSR inicialmente.</p>\n<h2 id=\"bkmrk-platform-013-%E2%80%94-traef\">PLATFORM-013 \u2014 Traefik</h2>\n<p id=\"bkmrk-traefik-ser%C3%A1-el-prox\">Traefik ser\u00e1 el proxy edge inicial.</p>\n<h2 id=\"bkmrk-platform-014-%E2%80%94-gatew\">PLATFORM-014 \u2014 Gateway API</h2>\n<p id=\"bkmrk-el-routing-http-se-e\">El routing HTTP se expresar\u00e1 con Gateway API.</p>\n<h2 id=\"bkmrk-platform-015-%E2%80%94-egres\">PLATFORM-015 \u2014 Egress simple</h2>\n<p id=\"bkmrk-se-usar%C3%A1n-natgw-ubun\">Se usar\u00e1n NATGW Ubuntu simples con gateway interno estable y WAN IP cambiable.</p>\n<h2 id=\"bkmrk-platform-016-%E2%80%94-no-is\">PLATFORM-016 \u2014 No Istio inicialmente</h2>\n<p id=\"bkmrk-no-se-desplegar%C3%A1-ser\">No se desplegar\u00e1 service mesh sin necesidad concreta.</p>\n<h2 id=\"bkmrk-platform-017-%E2%80%94-no-bg\">PLATFORM-017 \u2014 No BGP como requisito inicial</h2>\n<p id=\"bkmrk-la-plataforma-no-dep\">La plataforma no depender\u00e1 de BGP para arrancar.</p>\n<hr id=\"bkmrk--4\">\n<h1 id=\"bkmrk-4.-rke2-y-topolog%C3%ADa-\">4. RKE2 y topolog\u00eda inicial</h1>\n<p id=\"bkmrk-se-crear%C3%A1n-inicialme\">Se crear\u00e1n inicialmente:</p>\n<pre id=\"bkmrk-1-x-wetopi-ops01-3-x\"><code class=\"language-text\">1 x wetopi-ops01\n3 x RKE2 control plane\n3 x platform workers\n</code></pre>\n<p id=\"bkmrk-total-inicial%3A\">Total inicial:</p>\n<pre id=\"bkmrk-7-vms\"><code class=\"language-text\">7 VMs\n</code></pre>\n<p id=\"bkmrk-nombres-previstos%3A\">Nombres previstos:</p>\n<pre id=\"bkmrk-wetopi-ops01-rke-cp0\"><code class=\"language-text\">wetopi-ops01\n\nrke-cp01\nrke-cp02\nrke-cp03\n\nrke-platform01\nrke-platform02\nrke-platform03\n</code></pre>\n<p id=\"bkmrk-los-control-planes-y\">Los control planes y workers deben distribuirse entre distintos hosts Proxmox.</p>\n<p id=\"bkmrk-ejemplo%3A\">Ejemplo:</p>\n<pre id=\"bkmrk-pve01--%3E-rke-cp01-%2B-\"><code class=\"language-text\">pve01 -&gt; rke-cp01 + rke-platform01\npve02 -&gt; rke-cp02 + rke-platform02\npve03 -&gt; rke-cp03 + rke-platform03\n</code></pre>\n<p id=\"bkmrk-la-distribuci%C3%B3n-real\">La distribuci\u00f3n real deber\u00e1 validarse con el inventario.</p>\n<hr id=\"bkmrk--5\">\n<h1 id=\"bkmrk-5.-bootstrap-externo\">5. Bootstrap externo y <code>wetopi-ops01</code></h1>\n<p id=\"bkmrk-la-regla-es%3A\">La regla es:</p>\n<blockquote id=\"bkmrk-kubernetes-no-debe-d\">\n<p>Kubernetes no debe depender de servicios internos de Kubernetes para poder crearse o repararse.</p>\n</blockquote>\n<p id=\"bkmrk-modelo%3A\">Modelo:</p>\n<pre id=\"bkmrk-laptop-%E2%94%82-codex-%E2%94%82-git\"><code class=\"language-text\">Laptop\n  \u2502\nCodex\n  \u2502\nGitHub\n  \u2502\nwetopi-ops01\n  \u2502\nOpenTofu + Ansible\n  \u2502\nProxmox\n  \u2502\nRKE2\n</code></pre>\n<p id=\"bkmrk-codex-trabajar%C3%A1-inic\">Codex trabajar\u00e1 inicialmente desde el laptop del operador.</p>\n<p id=\"bkmrk-wetopi-ops01-ser%C3%A1-la\"><code>wetopi-ops01</code> ser\u00e1 la m\u00e1quina durable de bootstrap y disaster recovery.</p>\n<p id=\"bkmrk-debe-tener%2C-como-m%C3%ADn\">Debe tener, como m\u00ednimo:</p>\n<pre id=\"bkmrk-git-ssh-ansible-open\"><code class=\"language-text\">git\nssh\nansible\nopentofu\nkubectl\nhelm\njq\nyq\n</code></pre>\n<p id=\"bkmrk-no-alojar%C3%A1-workloads\">No alojar\u00e1 workloads productivos.</p>\n<hr id=\"bkmrk--6\">\n<h1 id=\"bkmrk-6.-organizaci%C3%B3n-gith\">6. Organizaci\u00f3n GitHub y repos</h1>\n<p id=\"bkmrk-organizaci%C3%B3n%3A\">Organizaci\u00f3n:</p>\n<pre id=\"bkmrk-wetopi-platform\"><code class=\"language-text\">wetopi-platform\n</code></pre>\n<p id=\"bkmrk-repos-iniciales%3A\">Repos iniciales:</p>\n<pre id=\"bkmrk-wetopi-platform%2Finfr\"><code class=\"language-text\">wetopi-platform/infrastructure\nwetopi-platform/agent-services\nwetopi-platform/ai-hub\nwetopi-platform/agents\nwetopi-platform/docs\n</code></pre>\n<p id=\"bkmrk-no-crear-repos-adici\">No crear repos adicionales sin necesidad real.</p>\n<hr id=\"bkmrk--7\">\n<h1 id=\"bkmrk-7.-opentofu-%2B-ansibl\">7. OpenTofu + Ansible</h1>\n<p id=\"bkmrk-se-selecciona%3A\">Se selecciona:</p>\n<pre id=\"bkmrk-opentofu-%2B-ansible\"><code class=\"language-text\">OpenTofu\n+\nAnsible\n</code></pre>\n<p id=\"bkmrk-opentofu%3A\">OpenTofu:</p>\n<ul id=\"bkmrk-crea-vms%3B-configura-\">\n<li>crea VMs;</li>\n<li>configura recursos Proxmox;</li>\n<li>modela infraestructura declarativa.</li>\n</ul>\n<p id=\"bkmrk-ansible%3A\">Ansible:</p>\n<ul id=\"bkmrk-baseline-ubuntu%3B-sys\">\n<li>baseline Ubuntu;</li>\n<li>sysctl;</li>\n<li>paquetes;</li>\n<li>RKE2;</li>\n<li>configuraci\u00f3n de hosts;</li>\n<li>operaciones bootstrap.</li>\n</ul>\n<p id=\"bkmrk-git-es-source-of-tru\">Git es source of truth.</p>\n<p id=\"bkmrk-no-se-considera-v%C3%A1li\">No se considera v\u00e1lido dejar cambios permanentes solo en shell history.</p>\n<hr id=\"bkmrk--8\">\n<h1 id=\"bkmrk-8.-networking-de-las\">8. Networking de las VMs</h1>\n<p id=\"bkmrk-cada-vm-tendr%C3%A1-inici\">Cada VM tendr\u00e1 inicialmente:</p>\n<pre id=\"bkmrk-1-nic-lan\"><code class=\"language-text\">1 NIC LAN\n</code></pre>\n<p id=\"bkmrk-modelo%3A-1\">Modelo:</p>\n<pre id=\"bkmrk-vm-%E2%94%94%E2%94%80%E2%94%80-eth0-%E2%94%94%E2%94%80%E2%94%80-lan-\"><code class=\"language-text\">VM\n \u2514\u2500\u2500 eth0\n      \u2514\u2500\u2500 LAN / vRack\n</code></pre>\n<p id=\"bkmrk-no-se-introducir%C3%A1n-m\">No se introducir\u00e1n m\u00faltiples interfaces salvo necesidad demostrada.</p>\n<hr id=\"bkmrk--9\">\n<h1 id=\"bkmrk-9.-ceph-existente\">9. Ceph existente</h1>\n<p id=\"bkmrk-la-red-ceph-actual-e\">La red Ceph actual es:</p>\n<pre id=\"bkmrk-192.168.96.0%2F20\"><code class=\"language-text\">192.168.96.0/20\n</code></pre>\n<p id=\"bkmrk-es-parte-de-la-infra\">Es parte de la infraestructura hiperconvergida Proxmox + Ceph.</p>\n<p id=\"bkmrk-decisi%C3%B3n%3A\">Decisi\u00f3n:</p>\n<blockquote id=\"bkmrk-no-modificar%2C-renume\">\n<p>NO modificar, renumerar ni redise\u00f1ar esta red.</p>\n</blockquote>\n<p id=\"bkmrk-ceph-se-consumir%C3%A1-de\">Ceph se consumir\u00e1 desde Kubernetes mediante CSI.</p>\n<p id=\"bkmrk-la-conectividad-exac\">La conectividad exacta entre RKE2 nodes y Ceph debe comprobarse, no asumirse.</p>\n<hr id=\"bkmrk--10\">\n<h1 id=\"bkmrk-10.-cidrs-propuestos\">10. CIDRs propuestos</h1>\n<p id=\"bkmrk-propuesta-actual%3A\">Propuesta actual:</p>\n<pre id=\"bkmrk-node-lan%3A-pendiente-\"><code class=\"language-text\">Node LAN:\npendiente de inventario real\n\nPod CIDR:\n10.64.0.0/14\n\nService CIDR:\n10.68.0.0/16\n\nCeph:\n192.168.96.0/20\nEXISTENTE / NO TOCAR\n</code></pre>\n<p id=\"bkmrk-los-cidrs-definitivo\">Los CIDRs definitivos deben validarse contra todas las redes Wetopi existentes.</p>\n<hr id=\"bkmrk--11\">\n<h1 id=\"bkmrk-11.-cilium-%2B-hubble\">11. Cilium + Hubble</h1>\n<p id=\"bkmrk-cilium-ser%C3%A1-el-cni-b\">Cilium ser\u00e1 el CNI base.</p>\n<p id=\"bkmrk-primera-versi%C3%B3n%3A\">Primera versi\u00f3n:</p>\n<pre id=\"bkmrk-cilium-%2B-vxlan-encap\"><code class=\"language-text\">Cilium\n+\nVXLAN encapsulation\n+\nHubble\n</code></pre>\n<p id=\"bkmrk-raz%C3%B3n-de-vxlan-inici\">Raz\u00f3n de VXLAN inicial:</p>\n<ul id=\"bkmrk-m%C3%ADnima-dependencia-d\">\n<li>m\u00ednima dependencia de underlay;</li>\n<li>no requiere que la red f\u00edsica conozca PodCIDRs;</li>\n<li>simplifica bootstrap;</li>\n<li>una sola NIC por VM.</li>\n</ul>\n<p id=\"bkmrk-no-activar-inicialme\">No activar inicialmente:</p>\n<ul id=\"bkmrk-bgp-wan%3B-service-mes\">\n<li>BGP WAN;</li>\n<li>service mesh;</li>\n<li>egress avanzado;</li>\n<li>features experimentales innecesarias.</li>\n</ul>\n<p id=\"bkmrk-hubble-se-utilizar%C3%A1-\">Hubble se utilizar\u00e1 para flows y correlaci\u00f3n de tr\u00e1fico con pods/namespaces/sites.</p>\n<hr id=\"bkmrk--12\">\n<h1 id=\"bkmrk-12.-ingress-wan%3A-ipv\">12. Ingress WAN: IPVS + VRRP + DSR</h1>\n<p id=\"bkmrk-el-frontend-actual-s\">El frontend actual se conserva inicialmente.</p>\n<pre id=\"bkmrk-internet-%E2%86%93-vip-%E2%86%93-vrr\"><code class=\"language-text\">Internet\n \u2193\nVIP\n \u2193\nVRRP\n \u2193\nIPVS\n \u2193\nDSR\n \u2193\nRKE2 Edge\n</code></pre>\n<p id=\"bkmrk-motivo-principal%3A\">Motivo principal:</p>\n<p id=\"bkmrk-dsr-permite-que-cada\">DSR permite que cada edge node responda directamente a Internet usando el uplink/bandwidth domain del host f\u00edsico Proxmox/OVH donde corre.</p>\n<p id=\"bkmrk-esto-permite-agregar\">Esto permite agregar capacidad de salida entre varios hosts f\u00edsicos.</p>\n<p id=\"bkmrk-esta-propiedad-no-de\">Esta propiedad no debe perderse durante la migraci\u00f3n.</p>\n<hr id=\"bkmrk--13\">\n<h1 id=\"bkmrk-13.-edge-nodes\">13. Edge nodes</h1>\n<p id=\"bkmrk-los-antiguos-wan-pro\">Los antiguos WAN proxies evolucionar\u00e1n a RKE2 edge workers.</p>\n<p id=\"bkmrk-ejemplo%3A-1\">Ejemplo:</p>\n<pre id=\"bkmrk-rke-edge-a01-rke-edg\"><code class=\"language-text\">rke-edge-a01\nrke-edge-a02\nrke-edge-b01\nrke-edge-b02\n</code></pre>\n<p id=\"bkmrk-labels%3A\">Labels:</p>\n<pre id=\"bkmrk-wetopi.io%2Frole%3Dedge-\"><code class=\"language-text\">wetopi.io/role=edge\nwetopi.io/wan-zone=a\nwetopi.io/proxmox-host=pve01\nwetopi.io/bandwidth-domain=pve01\n</code></pre>\n<p id=\"bkmrk-taint%3A\">Taint:</p>\n<pre id=\"bkmrk-wetopi.io%2Fedge%3Dtrue%3A\"><code class=\"language-text\">wetopi.io/edge=true:NoSchedule\n</code></pre>\n<p id=\"bkmrk-el-proxy-se-desplega\">El proxy se desplegar\u00e1 como DaemonSet cuando tenga sentido mantener una instancia por edge node.</p>\n<hr id=\"bkmrk--14\">\n<h1 id=\"bkmrk-14.-traefik-%2B-gatewa\">14. Traefik + Gateway API</h1>\n<p id=\"bkmrk-se-elige-traefik-ini\">Se elige Traefik inicialmente.</p>\n<p id=\"bkmrk-razones%3A\">Razones:</p>\n<ul id=\"bkmrk-sencillo%3B-kubernetes\">\n<li>sencillo;</li>\n<li>Kubernetes-native;</li>\n<li>soporte de Gateway API;</li>\n<li>TLS;</li>\n<li>routing din\u00e1mico;</li>\n<li>m\u00e9tricas;</li>\n<li>separaci\u00f3n clara respecto a Cilium.</li>\n</ul>\n<p id=\"bkmrk-modelo%3A-2\">Modelo:</p>\n<pre id=\"bkmrk-hostname-%E2%86%93-httproute\"><code class=\"language-text\">hostname\n \u2193\nHTTPRoute\n \u2193\nService\n \u2193\nEndpointSlice\n \u2193\nPod\n</code></pre>\n<p id=\"bkmrk-envoy-podr%C3%A1-reevalua\">Envoy podr\u00e1 reevaluarse m\u00e1s adelante si aparecen necesidades L7 m\u00e1s avanzadas.</p>\n<hr id=\"bkmrk--15\">\n<h1 id=\"bkmrk-15.-sustituci%C3%B3n-de-c\">15. Sustituci\u00f3n de Consul/Registrator/consul-template</h1>\n<p id=\"bkmrk-antes%3A\">Antes:</p>\n<pre id=\"bkmrk-docker-%E2%86%93-registrator\"><code class=\"language-text\">Docker\n \u2193\nRegistrator\n \u2193\nConsul\n \u2193\nconsul-template\n \u2193\nnginx.conf\n \u2193\nreload\n</code></pre>\n<p id=\"bkmrk-despu%C3%A9s%3A\">Despu\u00e9s:</p>\n<pre id=\"bkmrk-pod-%E2%86%93-service-%E2%86%93-endp\"><code class=\"language-text\">Pod\n \u2193\nService\n \u2193\nEndpointSlice\n \u2193\nGateway controller\n \u2193\ndynamic routing\n</code></pre>\n<p id=\"bkmrk-kubernetes-ser%C3%A1-el-s\">Kubernetes ser\u00e1 el service registry del hosting.</p>\n<hr id=\"bkmrk--16\">\n<h1 id=\"bkmrk-16.-egress-simple-co\">16. Egress simple con NATGW Ubuntu</h1>\n<p id=\"bkmrk-no-se-construir%C3%A1-ini\">No se construir\u00e1 inicialmente un egress control plane complejo.</p>\n<p id=\"bkmrk-modelo-base%3A\">Modelo base:</p>\n<pre id=\"bkmrk-gw100-lan%3A-192.168.8\"><code class=\"language-text\">gw100\nLAN: 192.168.88.100\nWAN: PUBLIC_IP_A\n\ngw101\nLAN: 192.168.88.101\nWAN: PUBLIC_IP_B\n\ngw102\nLAN: 192.168.88.102\nWAN: PUBLIC_IP_C\n</code></pre>\n<p id=\"bkmrk-cada-gateway%3A\">Cada gateway:</p>\n<pre id=\"bkmrk-ubuntu-%2B-routing-%2B-n\"><code class=\"language-text\">Ubuntu\n+\nrouting\n+\nnftables/iptables\n+\nSNAT\n</code></pre>\n<p id=\"bkmrk-objetivo%3A\">Objetivo:</p>\n<ul id=\"bkmrk-f%C3%A1cil-de-operar%3B-f%C3%A1c\">\n<li>f\u00e1cil de operar;</li>\n<li>f\u00e1cil de cambiar;</li>\n<li>f\u00e1cil de observar;</li>\n<li>f\u00e1cil de separar reputaci\u00f3n.</li>\n</ul>\n<hr id=\"bkmrk--17\">\n<h1 id=\"bkmrk-17.-selecci%C3%B3n-de-egr\">17. Selecci\u00f3n de egress por site/customer</h1>\n<p id=\"bkmrk-modelo-l%C3%B3gico%3A\">Modelo l\u00f3gico:</p>\n<pre id=\"bkmrk-site%2Fcustomer-%E2%86%93-egre\"><code class=\"language-text\">site/customer\n \u2193\negress_gateway\n \u2193\ngw100 / gw101 / gw102\n</code></pre>\n<p id=\"bkmrk-ejemplo%3A-2\">Ejemplo:</p>\n<pre id=\"bkmrk-wetopi.io%2Fegress-gat\"><code class=\"language-yaml\">wetopi.io/egress-gateway: gw101\n</code></pre>\n<p id=\"bkmrk-la-implementaci%C3%B3n-in\">La implementaci\u00f3n inicial puede usar:</p>\n<ul id=\"bkmrk-policy-routing%3B-auto\">\n<li>policy routing;</li>\n<li>automatizaci\u00f3n ligera;</li>\n<li>helper/DaemonSet que observe labels y mantenga reglas.</li>\n</ul>\n<p id=\"bkmrk-no-construir-todav%C3%ADa\">No construir todav\u00eda un controlador complejo.</p>\n<p id=\"bkmrk-futuro%3A\">Futuro:</p>\n<pre id=\"bkmrk-ciliumegressgatewayp\"><code class=\"language-text\">CiliumEgressGatewayPolicy\n</code></pre>\n<p id=\"bkmrk-puede-sustituir-el-m\">puede sustituir el mecanismo sin cambiar el modelo l\u00f3gico.</p>\n<p id=\"bkmrk-separar-desde-el-ini\">Separar desde el inicio:</p>\n<pre id=\"bkmrk-hosting-general-host\"><code class=\"language-text\">hosting-general\nhosting-premium\nplatform\nagents/crawlers\n</code></pre>\n<p id=\"bkmrk-los-crawlers%2Fagentes\">Los crawlers/agentes no deben compartir IP de salida con customers.</p>\n<hr id=\"bkmrk--18\">\n<h1 id=\"bkmrk-18.-argo-cd\">18. Argo CD</h1>\n<p id=\"bkmrk-se-ha-decidido-usar%3A\">Se ha decidido usar:</p>\n<pre id=\"bkmrk-argo-cd\"><code class=\"language-text\">Argo CD\n</code></pre>\n<p id=\"bkmrk-razones-principales%3A\">Razones principales:</p>\n<ul id=\"bkmrk-excelente-web-ui%3B-es\">\n<li>excelente Web UI;</li>\n<li>estado Synced/OutOfSync;</li>\n<li>health;</li>\n<li>diffs;</li>\n<li>historial;</li>\n<li>visibilidad para operadores humanos;</li>\n<li>modelo declarativo;</li>\n<li>API/CLI;</li>\n<li>RBAC/SSO/OIDC futuro.</li>\n</ul>\n<p id=\"bkmrk-modelo%3A-3\">Modelo:</p>\n<pre id=\"bkmrk-git-%E2%86%93-argo-cd-%E2%86%93-kube\"><code class=\"language-text\">Git\n \u2193\nArgo CD\n \u2193\nKubernetes\n</code></pre>\n<p id=\"bkmrk-no-se-utilizar%C3%A1-un-p\">No se utilizar\u00e1 un pipeline CI basado en <code>kubectl apply</code> como mecanismo normal de producci\u00f3n.</p>\n<p id=\"bkmrk-pendiente-decidir-el\">Pendiente decidir el layout exacto:</p>\n<ul id=\"bkmrk-app-of-apps%3B-applica\">\n<li>App of Apps;</li>\n<li>ApplicationSet;</li>\n<li>AppProjects;</li>\n<li>estructura por cluster/environment.</li>\n</ul>\n<hr id=\"bkmrk--19\">\n<h1 id=\"bkmrk-19.-infisical-como-w\">19. Infisical como Wetopi Vault</h1>\n<p id=\"bkmrk-el-vault-operativo-d\">El Vault operativo de Wetopi ser\u00e1:</p>\n<pre id=\"bkmrk-infisical-self-hoste-1\"><code class=\"language-text\">Infisical self-hosted\n</code></pre>\n<p id=\"bkmrk-el-requisito-real-no\">El requisito real no es solamente almacenar secrets declarativos.</p>\n<p id=\"bkmrk-se-necesita-un-servi\">Se necesita un servicio vivo donde humanos y agentes puedan:</p>\n<ul id=\"bkmrk-crear-credentials%3B-g\">\n<li>crear credentials;</li>\n<li>guardar username/password;</li>\n<li>guardar API tokens;</li>\n<li>guardar SSH keys;</li>\n<li>recuperar secrets;</li>\n<li>rotarlos;</li>\n<li>listarlos;</li>\n<li>actualizarlos;</li>\n<li>auditarlos;</li>\n<li>compartirlos con servicios.</li>\n</ul>\n<p id=\"bkmrk-ejemplo-conceptual%3A\">Ejemplo conceptual:</p>\n<pre id=\"bkmrk-vault.put%28-path%3D%22%2Fpl\"><code class=\"language-text\">vault.put(\n  path=\"/platform/proxmox/api\",\n  username=\"automation\",\n  password=\"...\"\n)\n</code></pre>\n<p id=\"bkmrk-infisical-ser%C3%A1-el-so\">Infisical ser\u00e1 el source of truth operativo de credentials.</p>\n<hr id=\"bkmrk--20\">\n<h1 id=\"bkmrk-20.-sops-%2B-age-como-\">20. SOPS + age como bootstrap</h1>\n<p id=\"bkmrk-sops-no-ser%C3%A1-el-vaul\">SOPS NO ser\u00e1 el Vault principal.</p>\n<p id=\"bkmrk-se-utilizar%C3%A1-%C3%BAnicame\">Se utilizar\u00e1 \u00fanicamente para bootstrap.</p>\n<p id=\"bkmrk-modelo%3A-4\">Modelo:</p>\n<pre id=\"bkmrk-git-%E2%86%93-sops-%2B-age-%E2%86%93-b\"><code class=\"language-text\">Git\n \u2193\nSOPS + age\n \u2193\nbootstrap secrets\n \u2193\nInfisical starts\n</code></pre>\n<p id=\"bkmrk-sops-protege-los-sec\">SOPS protege los secretos necesarios para levantar:</p>\n<ul id=\"bkmrk-infisical%3B-integraci\">\n<li>Infisical;</li>\n<li>integraci\u00f3n inicial de Argo CD;</li>\n<li>bootstrap identities;</li>\n<li>recovery material m\u00ednimo.</li>\n</ul>\n<p id=\"bkmrk-una-vez-infisical-fu\">Una vez Infisical funciona, los nuevos credentials se guardan en Infisical, no en Git.</p>\n<p id=\"bkmrk-debe-existir-al-meno\">Debe existir al menos una recovery key <code>age</code> fuera del cluster y fuera de Git.</p>\n<hr id=\"bkmrk--21\">\n<h1 id=\"bkmrk-21.-kubernetes-secre\">21. Kubernetes Secrets como materializaci\u00f3n runtime</h1>\n<p id=\"bkmrk-modelo%3A-5\">Modelo:</p>\n<pre id=\"bkmrk-infisical-%E2%94%82-%E2%94%82-sync%2Fo\"><code class=\"language-text\">Infisical\n    \u2502\n    \u2502 sync/operator\n    \u25bc\nKubernetes Secret\n    \u2502\n    \u25bc\nDeployment / Pod\n</code></pre>\n<p id=\"bkmrk-infisical-es-source-\">Infisical es source of truth.</p>\n<p id=\"bkmrk-kubernetes-secret-es\">Kubernetes Secret es materializaci\u00f3n runtime.</p>\n<p id=\"bkmrk-esto-permite%3A\">Esto permite:</p>\n<ul id=\"bkmrk-cambiar-un-secret-ce\">\n<li>cambiar un secret centralmente;</li>\n<li>sincronizarlo al cluster;</li>\n<li>consumirlo con mecanismos nativos de Kubernetes.</li>\n</ul>\n<hr id=\"bkmrk--22\">\n<h1 id=\"bkmrk-22.-modelo-de-acceso\">22. Modelo de acceso de agentes a secrets</h1>\n<p id=\"bkmrk-la-regla-principal-e\">La regla principal es:</p>\n<blockquote id=\"bkmrk-el-agente-debe-poder\">\n<p>el agente debe poder USAR un secret sin necesidad de VER su valor cuando sea posible.</p>\n</blockquote>\n<p id=\"bkmrk-ejemplo%3A-3\">Ejemplo:</p>\n<pre id=\"bkmrk-hermes-%E2%94%82-%E2%94%82-tool-call\"><code class=\"language-text\">Hermes\n  \u2502\n  \u2502 tool call\n  \u25bc\nplatform-tools\n  \u2502\n  \u2502 reads secret from Infisical/K8s Secret\n  \u25bc\nProxmox / Cloudflare / OVH\n</code></pre>\n<p id=\"bkmrk-el-llm-no-necesita-r\">El LLM no necesita recibir el token.</p>\n<hr id=\"bkmrk--23\">\n<h1 id=\"bkmrk-23.-tool-runtimes-y-\">23. Tool runtimes y aislamiento de credenciales</h1>\n<p id=\"bkmrk-no-montar-todos-los-\">No montar todos los secrets directamente en el proceso LLM.</p>\n<p id=\"bkmrk-agrupar-tools-por-se\">Agrupar tools por security domain.</p>\n<p id=\"bkmrk-ejemplo%3A-4\">Ejemplo:</p>\n<pre id=\"bkmrk-platform-tools-%E2%94%9C%E2%94%80%E2%94%80-k\"><code class=\"language-text\">platform-tools\n\u251c\u2500\u2500 kubernetes\n\u251c\u2500\u2500 proxmox\n\u251c\u2500\u2500 ceph\n\u2514\u2500\u2500 networking\n\nwordpress-tools\n\u251c\u2500\u2500 wordpress\n\u251c\u2500\u2500 mysql\n\u2514\u2500\u2500 backups\n\nmarketing-tools\n\u251c\u2500\u2500 intercom\n\u251c\u2500\u2500 analytics\n\u2514\u2500\u2500 ...\n</code></pre>\n<p id=\"bkmrk-cada-runtime-recibe-\">Cada runtime recibe \u00fanicamente los secrets necesarios.</p>\n<p id=\"bkmrk-no-crear-un-sidecar-\">No crear un sidecar por tool individual.</p>\n<p id=\"bkmrk-en-ai-hub%2C-las-tools\">En <code>ai-hub</code>, las tools deber\u00edan declarar sus requirements.</p>\n<p id=\"bkmrk-ejemplo%3A-5\">Ejemplo:</p>\n<pre id=\"bkmrk-tool%3A-proxmox.vm_res\"><code class=\"language-yaml\">tool: proxmox.vm_restart\nruntime: platform-tools\nsecrets:\n  - proxmox-api-token\n</code></pre>\n<p id=\"bkmrk-los-agentes-deben-co\">Los agentes deben consumir un contrato propio Wetopi, no acoplarse directamente a Infisical.</p>\n<p id=\"bkmrk-api-conceptual%3A\">API conceptual:</p>\n<pre id=\"bkmrk-vault.get%28%29-vault.pu\"><code class=\"language-text\">vault.get()\nvault.put()\nvault.update()\nvault.delete()\nvault.list()\nvault.exists()\n</code></pre>\n<hr id=\"bkmrk--24\">\n<h1 id=\"bkmrk-24.-hermes-personale\">24. Hermes personales y coding agents</h1>\n<p id=\"bkmrk-hermes-personales%2Fco\">Hermes personales/coding agents necesitar\u00e1n operar la plataforma.</p>\n<p id=\"bkmrk-podr%C3%A1n%3A\">Podr\u00e1n:</p>\n<ul id=\"bkmrk-llamar-tools-privile\">\n<li>llamar tools privilegiadas;</li>\n<li>usar credentials;</li>\n<li>escribir secrets en Vault;</li>\n<li>recuperar credentials autorizados;</li>\n<li>operar GitHub;</li>\n<li>operar Proxmox;</li>\n<li>operar Kubernetes.</li>\n</ul>\n<p id=\"bkmrk-pero%3A\">Pero:</p>\n<ul id=\"bkmrk-no-recibir-todas-las\">\n<li>no recibir todas las credenciales en env;</li>\n<li>no montar un vault global;</li>\n<li>no dar acceso indiscriminado a todos los paths.</li>\n</ul>\n<p id=\"bkmrk-el-permiso-debe-segu\">El permiso debe seguir el rol.</p>\n<p id=\"bkmrk-ejemplo-conceptual%3A-1\">Ejemplo conceptual:</p>\n<pre id=\"bkmrk-hermes-sre-%2Fplatform\"><code class=\"language-text\">Hermes SRE\n  /platform/*\n  /infrastructure/*\n  /agents/hermes-sre/*\n\nHermes Developer\n  /development/*\n  /github/*\n  /agents/hermes-developer/*\n\nHermes Marketing\n  /marketing/*\n  /agents/hermes-marketing/*\n</code></pre>\n<hr id=\"bkmrk--25\">\n<h1 id=\"bkmrk-25.-serviceaccounts%2C\">25. ServiceAccounts, RBAC y namespaces</h1>\n<p id=\"bkmrk-para-operar-kubernet\">Para operar Kubernetes, los agentes dentro del cluster usar\u00e1n:</p>\n<pre id=\"bkmrk-serviceaccount-%2B-rba\"><code class=\"language-text\">ServiceAccount\n+\nRBAC\n</code></pre>\n<p id=\"bkmrk-no-kubeconfig-est%C3%A1ti\">No kubeconfig est\u00e1tico ni password.</p>\n<p id=\"bkmrk-ejemplo%3A-6\">Ejemplo:</p>\n<pre id=\"bkmrk-hermes-sre--%3E-permis\"><code class=\"language-text\">Hermes SRE\n -&gt; permisos operacionales amplios\n\nHermes Developer\n -&gt; namespaces dev + deployments\n\nADK observer\n -&gt; readonly\n\nADK specialist\n -&gt; permissions espec\u00edficas\n</code></pre>\n<p id=\"bkmrk-namespaces-orientati\">Namespaces orientativos:</p>\n<pre id=\"bkmrk-wetopi-platform-weto\"><code class=\"language-text\">wetopi-platform\nwetopi-edge\nwetopi-hosting\nwetopi-agents-sre\nwetopi-agents-development\nwetopi-agents-marketing\nwetopi-observability\nwetopi-security\n</code></pre>\n<p id=\"bkmrk-los-namespaces-forma\">Los namespaces forman parte de la frontera de secrets y RBAC.</p>\n<p id=\"bkmrk-importante%3A\">Importante:</p>\n<p id=\"bkmrk-quien-puede-crear-po\">quien puede crear Pods en un namespace puede potencialmente montar Secrets de ese namespace.</p>\n<p id=\"bkmrk-por-tanto%2C-permisos-\">Por tanto, permisos de workload creation y secret access deben dise\u00f1arse conjuntamente.</p>\n<hr id=\"bkmrk--26\">\n<h1 id=\"bkmrk-26.-secrets-encrypti\">26. Secrets encryption at rest</h1>\n<p id=\"bkmrk-sops-protege-git.\">SOPS protege Git.</p>\n<p id=\"bkmrk-infisical-protege-el\">Infisical protege el Vault.</p>\n<p id=\"bkmrk-kubernetes-secrets-m\">Kubernetes Secrets materializados deben protegerse en etcd.</p>\n<p id=\"bkmrk-rke2-deber%C3%A1-tener%3A\">RKE2 deber\u00e1 tener:</p>\n<pre id=\"bkmrk-secrets-encryption-a\"><code class=\"language-text\">Secrets Encryption at Rest\n</code></pre>\n<p id=\"bkmrk-habilitado.\">habilitado.</p>\n<p id=\"bkmrk-modelo%3A-6\">Modelo:</p>\n<pre id=\"bkmrk-infisical-%E2%86%93-kubernet\"><code class=\"language-text\">Infisical\n \u2193\nKubernetes Secret\n \u2193\nRKE2 API server\n \u2193\nencrypted etcd storage\n</code></pre>\n<hr id=\"bkmrk--27\">\n<h1 id=\"bkmrk-27.-orden-de-bootstr\">27. Orden de bootstrap actualizado</h1>\n<p id=\"bkmrk-orden-actual-recomen\">Orden actual recomendado:</p>\n<pre id=\"bkmrk-1.-github-organizati\"><code class=\"language-text\">1. GitHub organization + repos\n2. Inventario Proxmox/redes\n3. OpenTofu + Ansible\n4. wetopi-ops01\n5. 3 x RKE2 control plane\n6. 3 x platform workers\n7. RKE2 HA + etcd\n8. Cilium\n9. Hubble\n10. Ceph CSI\n11. Argo CD\n12. SOPS + age bootstrap\n13. Infisical\n14. Kubernetes Secrets integration/operator\n15. Docs\n16. Memory Plane\n17. Work Ledger\n18. AI Hub\n19. Hermes\n20. ADK Fleet\n21. Edge nodes\n22. Gateway API + Traefik\n23. NATGW\n24. first hosting workers\n25. first websites\n26. progressive migration from Swarm\n</code></pre>\n<hr id=\"bkmrk--28\">\n<h1 id=\"bkmrk-28.-primeras-tareas-\">28. Primeras tareas para Codex</h1>\n<h2 id=\"bkmrk-task-1-%E2%80%94-crear-repos\">Task 1 \u2014 Crear repos</h2>\n<p id=\"bkmrk-crear%3A\">Crear:</p>\n<pre id=\"bkmrk-infrastructure-agent\"><code class=\"language-text\">infrastructure\nagent-services\nai-hub\nagents\ndocs\n</code></pre>\n<p id=\"bkmrk-aplicar%3A\">Aplicar:</p>\n<ul id=\"bkmrk-readme%3B-codeowners%3B-\">\n<li>README;</li>\n<li>CODEOWNERS;</li>\n<li>branch protection;</li>\n<li>CI m\u00ednima;</li>\n<li>convenciones.</li>\n</ul>\n<h2 id=\"bkmrk-task-2-%E2%80%94-inventario-\">Task 2 \u2014 Inventario Proxmox</h2>\n<p id=\"bkmrk-recoger%3A\">Recoger:</p>\n<ul id=\"bkmrk-nodos%3B-cpu%3B-ram%3B-sto\">\n<li>nodos;</li>\n<li>CPU;</li>\n<li>RAM;</li>\n<li>storage;</li>\n<li>bridges;</li>\n<li>VLANs;</li>\n<li>redes;</li>\n<li>IP ranges;</li>\n<li>failure domains;</li>\n<li>bandwidth domains.</li>\n</ul>\n<p id=\"bkmrk-no-crear-a%C3%BAn-direcci\">No crear a\u00fan direccionamiento inventado.</p>\n<h2 id=\"bkmrk-task-3-%E2%80%94-validar-red\">Task 3 \u2014 Validar redes</h2>\n<p id=\"bkmrk-confirmar-que%3A\">Confirmar que:</p>\n<pre id=\"bkmrk-pod-cidr-10.64.0.0%2F1\"><code class=\"language-text\">Pod CIDR 10.64.0.0/14\nService CIDR 10.68.0.0/16\n</code></pre>\n<p id=\"bkmrk-no-colisionan.\">no colisionan.</p>\n<p id=\"bkmrk-confirmar-conectivid\">Confirmar conectividad RKE2 node -&gt; Ceph <code>192.168.96.0/20</code>.</p>\n<h2 id=\"bkmrk-task-4-%E2%80%94-ubuntu-temp\">Task 4 \u2014 Ubuntu template</h2>\n<p id=\"bkmrk-crear-template-repro\">Crear template reproducible Ubuntu 24.04 + cloud-init.</p>\n<h2 id=\"bkmrk-task-5-%E2%80%94-wetopi-ops0\">Task 5 \u2014 <code>wetopi-ops01</code></h2>\n<p id=\"bkmrk-crear%2Fprovisionar-we\">Crear/provisionar <code>wetopi-ops01</code>.</p>\n<h2 id=\"bkmrk-task-6-%E2%80%94-control-pla\">Task 6 \u2014 Control plane</h2>\n<p id=\"bkmrk-crear%3A-1\">Crear:</p>\n<pre id=\"bkmrk-rke-cp01-rke-cp02-rk\"><code class=\"language-text\">rke-cp01\nrke-cp02\nrke-cp03\n</code></pre>\n<h2 id=\"bkmrk-task-7-%E2%80%94-platform-wo\">Task 7 \u2014 Platform workers</h2>\n<p id=\"bkmrk-crear%3A-2\">Crear:</p>\n<pre id=\"bkmrk-rke-platform01-rke-p\"><code class=\"language-text\">rke-platform01\nrke-platform02\nrke-platform03\n</code></pre>\n<h2 id=\"bkmrk-task-8-%E2%80%94-rke2\">Task 8 \u2014 RKE2</h2>\n<p id=\"bkmrk-configurar%3A\">Configurar:</p>\n<ul id=\"bkmrk-embedded-etcd%3B-ha%3B-a\">\n<li>embedded etcd;</li>\n<li>HA;</li>\n<li>API endpoint estable;</li>\n<li>secrets encryption;</li>\n<li>snapshots.</li>\n</ul>\n<h2 id=\"bkmrk-task-9-%E2%80%94-cilium%2Fhubb\">Task 9 \u2014 Cilium/Hubble</h2>\n<p id=\"bkmrk-desplegar%3A\">Desplegar:</p>\n<ul id=\"bkmrk-cilium-vxlan%3B-hubble\">\n<li>Cilium VXLAN;</li>\n<li>Hubble;</li>\n<li>networking tests.</li>\n</ul>\n<h2 id=\"bkmrk-task-10-%E2%80%94-ceph-csi\">Task 10 \u2014 Ceph CSI</h2>\n<p id=\"bkmrk-validar%3A\">Validar:</p>\n<ul id=\"bkmrk-pvc-create%3B-mount%3B-p\">\n<li>PVC create;</li>\n<li>mount;</li>\n<li>pod restart;</li>\n<li>persistence.</li>\n</ul>\n<h2 id=\"bkmrk-task-11-%E2%80%94-argo-cd\">Task 11 \u2014 Argo CD</h2>\n<p id=\"bkmrk-desplegar-declarativ\">Desplegar declarativamente.</p>\n<h2 id=\"bkmrk-task-12-%E2%80%94-sops-boots\">Task 12 \u2014 SOPS bootstrap</h2>\n<p id=\"bkmrk-generar%3A\">Generar:</p>\n<ul id=\"bkmrk-age-production-key%3B-\">\n<li>age production key;</li>\n<li>age recovery key;</li>\n<li><code>.sops.yaml</code>.</li>\n</ul>\n<p id=\"bkmrk-guardar-recovery-key\">Guardar recovery key fuera del cluster y fuera de Git.</p>\n<h2 id=\"bkmrk-task-13-%E2%80%94-infisical\">Task 13 \u2014 Infisical</h2>\n<p id=\"bkmrk-desplegar-self-hoste\">Desplegar self-hosted.</p>\n<p id=\"bkmrk-configurar%3A-1\">Configurar:</p>\n<ul id=\"bkmrk-persistence%3B-backups\">\n<li>persistence;</li>\n<li>backups;</li>\n<li>admin bootstrap;</li>\n<li>projects/environments;</li>\n<li>machine identities m\u00ednimas;</li>\n<li>Kubernetes integration.</li>\n</ul>\n<h2 id=\"bkmrk-task-14-%E2%80%94-vault-cont\">Task 14 \u2014 Vault contract</h2>\n<p id=\"bkmrk-crear-en-ai-hub-una-\">Crear en <code>ai-hub</code> una capa estable:</p>\n<pre id=\"bkmrk-vault.get%28%29-vault.pu-1\"><code class=\"language-text\">vault.get()\nvault.put()\nvault.update()\nvault.delete()\nvault.list()\nvault.exists()\n</code></pre>\n<p id=\"bkmrk-los-agentes-no-deben\">Los agentes no deben quedar acoplados a la API concreta de Infisical.</p>\n<hr id=\"bkmrk--29\">\n<h1 id=\"bkmrk-29.-decisiones-a%C3%BAn-a\">29. Decisiones a\u00fan abiertas</h1>\n<h2 id=\"bkmrk-29.1-node-lan-cidr\">29.1 Node LAN CIDR</h2>\n<p id=\"bkmrk-pendiente-de-inventa\">Pendiente de inventario.</p>\n<h2 id=\"bkmrk-29.2-endpoint-ha-de-\">29.2 Endpoint HA de RKE2</h2>\n<p id=\"bkmrk-propuesta-previa%3A\">Propuesta previa:</p>\n<pre id=\"bkmrk-2-x-haproxy-%2B-keepal\"><code class=\"language-text\">2 x HAProxy + Keepalived\n+\nprivate VIP\n</code></pre>\n<p id=\"bkmrk-debe-cerrarse-antes-\">Debe cerrarse antes del deploy final.</p>\n<h2 id=\"bkmrk-29.3-dsr-validation\">29.3 DSR validation</h2>\n<p id=\"bkmrk-debe-hacerse-poc-rea\">Debe hacerse PoC real con capture.</p>\n<h2 id=\"bkmrk-29.4-traefik-deploym\">29.4 Traefik deployment exacto</h2>\n<p id=\"bkmrk-definir%3A\">Definir:</p>\n<ul id=\"bkmrk-hostnetwork%3B-daemons\">\n<li>hostNetwork;</li>\n<li>DaemonSet;</li>\n<li>VIP handling;</li>\n<li>health endpoint.</li>\n</ul>\n<h2 id=\"bkmrk-29.5-egress-enforcem\">29.5 Egress enforcement exacto</h2>\n<p id=\"bkmrk-definir-implementaci\">Definir implementaci\u00f3n inicial de:</p>\n<pre id=\"bkmrk-site-label-%E2%86%93-gateway\"><code class=\"language-text\">site label\n \u2193\ngateway selection\n \u2193\npolicy routing\n</code></pre>\n<h2 id=\"bkmrk-29.6-infisical-oss%2Fl\">29.6 Infisical OSS/licensing</h2>\n<p id=\"bkmrk-validar-exactamente%3A\">Validar exactamente:</p>\n<ul id=\"bkmrk-features-necesarias-\">\n<li>features necesarias en self-hosted;</li>\n<li>machine identities;</li>\n<li>Kubernetes integration;</li>\n<li>audit;</li>\n<li>versioning;</li>\n<li>posibles l\u00edmites comerciales.</li>\n</ul>\n<h2 id=\"bkmrk-29.7-gitops-layout\">29.7 GitOps layout</h2>\n<p id=\"bkmrk-definir-estructura-e\">Definir estructura exacta Argo CD:</p>\n<ul id=\"bkmrk-application%3B-appproj\">\n<li>Application;</li>\n<li>AppProject;</li>\n<li>App of Apps o ApplicationSet.</li>\n</ul>\n<h2 id=\"bkmrk-29.8-oidc\">29.8 OIDC</h2>\n<p id=\"bkmrk-pendiente.\">Pendiente.</p>\n<h2 id=\"bkmrk-29.9-backup-externo\">29.9 Backup externo</h2>\n<p id=\"bkmrk-pendiente-destino-de\">Pendiente destino definitivo de:</p>\n<ul id=\"bkmrk-etcd-snapshots%3B-infi\">\n<li>etcd snapshots;</li>\n<li>Infisical DB;</li>\n<li>SOPS recovery material;</li>\n<li>Git.</li>\n</ul>\n<h2 id=\"bkmrk-29.10-observabilidad\">29.10 Observabilidad central</h2>\n<p id=\"bkmrk-pendiente-arquitectu\">Pendiente arquitectura final.</p>\n<hr id=\"bkmrk--30\">\n<h1 id=\"bkmrk-30.-criterios-de-ace\">30. Criterios de aceptaci\u00f3n</h1>\n<p id=\"bkmrk-antes-de-desplegar-h\">Antes de desplegar Hermes o workloads reales:</p>\n<h2 id=\"bkmrk-cluster\">Cluster</h2>\n<ul id=\"bkmrk-p%C3%A9rdida-de-1-control\">\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> p\u00e9rdida de 1 control plane no interrumpe API;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> etcd mantiene quorum;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> snapshot etcd funcional;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> restore documentado y probado;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> workers pueden drain/uncordon.</li>\n</ul>\n<h2 id=\"bkmrk-networking\">Networking</h2>\n<ul id=\"bkmrk-pod-to-pod%3B-pod-to-s\">\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> pod-to-pod;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> pod-to-service;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> DNS;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Hubble flows visibles;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> no collision CIDRs;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> nodes alcanzan Ceph.</li>\n</ul>\n<h2 id=\"bkmrk-storage\">Storage</h2>\n<ul id=\"bkmrk-pvc%3B-delete%2Frecreate\">\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> PVC;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> delete/recreate pod;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> persistencia;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> failure test b\u00e1sico.</li>\n</ul>\n<h2 id=\"bkmrk-gitops\">GitOps</h2>\n<ul id=\"bkmrk-argo-cd-reconciles-d\">\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Argo CD reconciles desde Git;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> manual drift detectable;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> rollback verificable.</li>\n</ul>\n<h2 id=\"bkmrk-vault\">Vault</h2>\n<ul id=\"bkmrk-agente%2Ftool-puede-cr\">\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> agente/tool puede crear secret;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> servicio puede consumir secret;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> secret no queda plaintext en Git;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> access control validado;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> backup de Infisical probado;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> recovery bootstrap documentado.</li>\n</ul>\n<h2 id=\"bkmrk-secrets\">Secrets</h2>\n<ul id=\"bkmrk-sops-bootstrap-funci\">\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> SOPS bootstrap funciona;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> recovery age key offline;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Kubernetes secrets cifrados at rest;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> namespaces/RBAC limitan acceso.</li>\n</ul>\n<h2 id=\"bkmrk-agentes\">Agentes</h2>\n<ul id=\"bkmrk-hermes-puede-usar-to\">\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Hermes puede usar tool privilegiada sin recibir raw token;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> ADK readonly no puede leer secrets ajenos;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> coding agent solo accede a vault paths autorizados.</li>\n</ul>\n<hr id=\"bkmrk--31\">\n<h1 id=\"bkmrk-31.-resumen-final-pa\">31. Resumen final para implementaci\u00f3n</h1>\n<p id=\"bkmrk-la-platform-foundati\">La platform foundation actual queda as\u00ed:</p>\n<pre id=\"bkmrk-github-wetopi-platfo\"><code class=\"language-text\">                      GitHub\n               wetopi-platform\n                       \u2502\n               infrastructure\n                       \u2502\n                    Codex\n                       \u2502\n                  wetopi-ops01\n                       \u2502\n              OpenTofu + Ansible\n                       \u2502\n                       \u25bc\n                \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                \u2502   RKE2 HA   \u2502\n                \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                       \u2502\n              Cilium + Hubble\n                       \u2502\n                    Ceph CSI\n                       \u2502\n                    Argo CD\n                       \u2502\n                 SOPS bootstrap\n                       \u2502\n                    Infisical\n                       \u2502\n        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n        \u2502              \u2502              \u2502\n   Kubernetes       Tool runtimes    Humans\n    Secrets             \u2502           Web UI\n        \u2502               \u2502\n        \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                        \u2502              \u2502\n                      Hermes          ADK\n</code></pre>\n<p id=\"bkmrk-networking%3A\">Networking:</p>\n<pre id=\"bkmrk-ingress-internet-%E2%86%93-v\"><code class=\"language-text\">INGRESS\n\nInternet\n \u2193\nVRRP\n \u2193\nIPVS\n \u2193\nDSR\n \u2193\nRKE2 Edge\n \u2193\nTraefik\n \u2193\nGateway API\n \u2193\nService\n \u2193\nWebsite\n</code></pre>\n<pre id=\"bkmrk-egress-site-%2F-custom\"><code class=\"language-text\">EGRESS\n\nSite / Customer\n \u2193\negress label/profile\n \u2193\ngw100 / gw101 / gw102\n \u2193\nWAN IP\n \u2193\nInternet\n</code></pre>\n<p id=\"bkmrk-la-intenci%C3%B3n-global-\">La intenci\u00f3n global es mantener la primera versi\u00f3n simple:</p>\n<ul id=\"bkmrk-no-istio%3B-no-bgp-obl\">\n<li>no Istio;</li>\n<li>no BGP obligatorio;</li>\n<li>no controller avanzado de egress;</li>\n<li>no OpenBao;</li>\n<li>no service mesh complejo;</li>\n<li>no redise\u00f1o simult\u00e1neo de Ceph.</li>\n</ul>\n<p id=\"bkmrk-la-plataforma-se-est\">La plataforma se estabiliza primero.</p>\n<p id=\"bkmrk-despu%C3%A9s-se-despliega\">Despu\u00e9s se despliegan:</p>\n<pre id=\"bkmrk-docs-memory-plane-wo\"><code class=\"language-text\">Docs\nMemory Plane\nWork Ledger\nAI Hub\nHermes\nADK\nEdge\nHosting\n</code></pre>\n<p id=\"bkmrk-y-finalmente-se-inic\">y finalmente se inicia la migraci\u00f3n progresiva de Swarm a RKE2.</p>\n<hr id=\"bkmrk--32\">\n<h1 id=\"bkmrk-32.-platform-018-%E2%80%94-c\">32. PLATFORM-018 \u2014 CNI Cilium desde el d\u00eda uno (sin Canal provisional)</h1>\n<p id=\"bkmrk-fecha%3A-29-de-agosto-\"><strong>Fecha:</strong> 29 de agosto de 2026\n<strong>Estado:</strong> Decidido (operador). Refina PLATFORM-007 (Cilium ser\u00e1 el CNI) y\nPLATFORM-008 (Hubble). Se graba como PLATFORM-018 \u2014 el siguiente identificador\nlibre \u2014 porque PLATFORM-008 ya estaba asignado a Hubble en el registro original.</p>\n<p id=\"bkmrk-decisi%C3%B3n-operativa-p\">Decisi\u00f3n operativa para el primer addon del cluster RKE2:</p>\n<ul id=\"bkmrk-no-habr%C3%A1-cni-provisi\">\n<li>No habr\u00e1 CNI provisional Canal. Cilium es el CNI desde el primer nodo y\nsustituye al Canal que RKE2 instala por defecto: la configuraci\u00f3n de\nrke2-server debe fijar <code>cni: none</code>, y el rol de RKE2 debe mantener\ndeshabilitado el Canal incluido de serie.</li>\n<li>Chart oficial Cilium fijado a la versi\u00f3n <code>1.20.1</code>\n(<code>oci://quay.io/cilium/charts/cilium</code>, namespace <code>kube-system</code>). Las\nversiones de chart no llevan prefijo <code>v</code>.</li>\n<li>Encapsulaci\u00f3n VXLAN (<code>routingMode: tunnel</code>, <code>tunnelProtocol: vxlan</code>); sin\nBGP y sin native routing inicialmente, seg\u00fan el plan maestro (14.4) y\nPLATFORM-017.</li>\n<li>IPAM <code>cluster-pool</code> con <code>clusterPoolIPv4PodCIDRList: [10.64.0.0/14]</code> y\n<code>clusterPoolIPv4MaskSize: 24</code>, coincidiendo exactamente con el pod CIDR de\nRKE2. RKE2 es quien posee el pod CIDR y el service CIDR (10.68.0.0/16);\nCilium no los re-deriva y no toca el CIDR de servicios.</li>\n<li><code>kubeProxyReplacement: false</code> en v1: kube-proxy est\u00e1ndar convive con Cilium\nVXLAN. La sustituci\u00f3n de kube-proxy queda documentada como opci\u00f3n futura.</li>\n<li>Hubble habilitado con Hubble Relay (<code>hubble.enabled: true</code>,\n<code>hubble.relay.enabled: true</code>); <code>hubble.ui.enabled: false</code> por defecto,\nactivaci\u00f3n opcional documentada.</li>\n<li>La MTU queda por defecto (auto ~1450 sobre el LAN de nodos MTU 1500).</li>\n<li>Puertos obligatorios entre nodos: UDP 8472 (VXLAN) y TCP 4244 (Hubble\nRelay).</li>\n</ul>\n<p id=\"bkmrk-m%C3%A9todo-de-instalaci%C3%B3\">M\u00e9todo de instalaci\u00f3n: Argo CD no existe todav\u00eda en el primer bootstrap, as\u00ed\nque el addon se instala desde <code>wetopi-ops01</code> con Helm durante la fase de\napply (playbook <code>ansible/playbooks/cilium.yml</code> en el repo infrastructure).\nEs una instalaci\u00f3n solo de bootstrap: cuando Argo CD exista, la gesti\u00f3n del\naddon pasa a \u00e9l y el playbook se retira. Los valores pinneados viven en\n<code>kubernetes/cilium/values.yaml</code>; la lista de verificaci\u00f3n de post-instalaci\u00f3n\nen <code>kubernetes/cilium/VALIDATION.md</code>.</p>\n<p id=\"bkmrk-cidrs%3A-el-ownership-\">CIDRs: el ownership es de RKE2 (pod CIDR 10.64.0.0/14 y service CIDR\n10.68.0.0/16); el pool de Cilium se limita a consumir el rango de pods y\nnunca debe diverger de la configuraci\u00f3n de rke2-server.</p>\n"
 },
 {
  "id": 57,
  "title": "master architecture plan",
  "slug": "master-architecture-plan",
  "book_id": 8,
  "chapter_id": 29,
  "description": null,
  "created_at": "2026-09-06T13:39:02.000000Z",
  "updated_at": "2026-09-06T13:39:02.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (design/master-architecture-plan.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n# Wetopi Platform \u2014 Master Architecture, Operating Model & Implementation Plan\n## Memoria ejecutiva, arquitectura t\u00e9cnica general y plan de construcci\u00f3n\n\n**Fecha:** 28 de agosto de 2026  \n**Estado:** Documento maestro para revisi\u00f3n e implementaci\u00f3n  \n**Organizaci\u00f3n GitHub:** `wetopi-platform`  \n**Audiencia:** Platform Engineering, SRE, DevOps, Development, Security, Network Engineering y agentes Codex/Hermes/ADK  \n**Prop\u00f3sito:** servir como documento principal para explicar, revisar, construir y operar la nueva plataforma Wetopi.\n\n---\n\n# 0. C\u00f3mo usar este documento\n\nEste documento consolida y sustituye como referencia principal a las memorias parciales creadas durante el dise\u00f1o inicial:\n\n- `wetopi-ai-first-kubernetes-project-memory.md`\n- `wetopi-platform-bootstrap-plan.md`\n- `wetopi-rke2-network-hosting-architecture.md`\n- `wetopi-platform-decisions-argocd-infisical-rke2.md`\n\nCuando exista una contradicci\u00f3n con documentos anteriores, **prevalece este documento**, especialmente en decisiones posteriores como:\n\n- **Argo CD** como GitOps.\n- **Infisical self-hosted** como Wetopi Vault.\n- **SOPS + age** \u00fanicamente como bootstrap/recovery de secretos declarativos.\n- **OpenBao queda fuera de v1**.\n\nNo todas las decisiones est\u00e1n cerradas. Las cuestiones a\u00fan abiertas se marcan expl\u00edcitamente como `OPEN` y **no deben ser inventadas por Codex ni por un ingeniero durante la implementaci\u00f3n**.\n\n---\n\n# 1. Resumen ejecutivo\n\nWetopi va a reconstruir su plataforma de Managed WordPress Hosting sobre una nueva foundation basada en **RKE2**, desplegada en su cloud privado sobre **Proxmox + Ceph + OVH vRack**.\n\nLa migraci\u00f3n ser\u00e1 **greenfield y progresiva**. Docker Swarm seguir\u00e1 dando servicio mientras se construye, prueba y estabiliza la nueva plataforma.\n\nLa arquitectura inicial queda resumida en:\n\n```text\nProxmox + Ceph + OVH vRack\n        \u2502\n        \u25bc\n       RKE2\n        \u2502\n  Cilium + Hubble\n        \u2502\n      Ceph CSI\n        \u2502\n      Argo CD\n        \u2502\n SOPS bootstrap\n        \u2502\n    Infisical\n        \u2502\n Docs / Memory / Work Coordination\n        \u2502\n Hermes + ADK + Codex workflows\n        \u2502\n Edge RKE2 + Traefik + Gateway API\n        \u2502\n Hosting workers + WordPress sites\n```\n\nLa entrada WAN conservar\u00e1 inicialmente la arquitectura probada de Wetopi:\n\n```text\nInternet\n   \u2193\nVRRP VIP\n   \u2193\nIPVS\n   \u2193\nDSR\n   \u2193\nRKE2 Edge nodes\n   \u2193\nTraefik\n   \u2193\nGateway API\n   \u2193\nServices / EndpointSlices\n   \u2193\nWordPress workloads\n```\n\nLa salida ser\u00e1 deliberadamente sencilla:\n\n```text\nsite/customer/workload\n        \u2193\nselected internal gateway\n        \u2193\n192.168.88.100 / .101 / .102 / ...\n        \u2193\nUbuntu NATGW\n        \u2193\nselected WAN public IP\n        \u2193\nInternet\n```\n\nLa plataforma ser\u00e1 **AI-first**, pero no se permitir\u00e1 que la autonom\u00eda de los agentes degrade la operabilidad o seguridad. Git, Vault, Work Ledger, Memory Plane, documentaci\u00f3n y tools compartidas ser\u00e1n servicios de primera clase tanto para humanos como para agentes.\n\n---\n\n# 2. Objetivos de negocio y plataforma\n\n## 2.1 Objetivos de negocio\n\nLa nueva plataforma debe permitir a Wetopi:\n\n- migrar desde Docker Swarm sin big-bang;\n- operar cientos y posteriormente miles de workloads WordPress;\n- mantener la eficiencia y capacidad WAN actual;\n- reducir configuraci\u00f3n artesanal y componentes legacy;\n- ofrecer mejor aislamiento por cliente;\n- permitir IPs de salida dedicadas o f\u00e1cilmente rotables;\n- mejorar observabilidad y seguridad;\n- incorporar automatizaci\u00f3n y agentes de forma nativa;\n- mantener un modelo operativo entendible por ingenieros humanos.\n\n## 2.2 Objetivos t\u00e9cnicos\n\nLa plataforma debe proporcionar:\n\n- Kubernetes HA;\n- IaC reproducible;\n- GitOps declarativo;\n- CNI moderno;\n- observabilidad de flujos;\n- CSI para Ceph;\n- Vault operativo con API;\n- secrets materializados en Kubernetes;\n- documentaci\u00f3n Markdown versionada;\n- memoria institucional com\u00fan;\n- coordinaci\u00f3n de trabajo entre agentes y humanos;\n- trazabilidad;\n- separaci\u00f3n clara de security domains;\n- recuperaci\u00f3n desde p\u00e9rdida parcial o total del cluster.\n\n---\n\n# 3. Principios de dise\u00f1o\n\n## 3.1 Greenfield antes que migraci\u00f3n in-place\n\nLa nueva plataforma se construye en paralelo al Swarm existente.\n\n```text\nPRODUCCI\u00d3N ACTUAL                 NUEVA PLATAFORMA\nDocker Swarm                     RKE2 greenfield\n      \u2502                                \u2502\n      \u2502                                \u2502\n      \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 coexistencia \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\nNo se reutilizan nodos de producci\u00f3n hasta que RKE2 haya demostrado estabilidad.\n\n## 3.2 Git es source of truth del estado declarativo\n\n```text\nGit\n \u2193\nreview\n \u2193\nArgo CD / IaC\n \u2193\nreal infrastructure\n```\n\nLa shell history no es source of truth.\n\n## 3.3 La fuente viva gana a la memoria\n\nPara infraestructura real:\n\n```text\nAPI autoritativa / sistema vivo\n>\nFleet Memory\n>\nDocs hist\u00f3ricas\n>\nLLM inference\n```\n\n## 3.4 Simplicidad antes que sofisticaci\u00f3n\n\nNo desplegar v1 con:\n\n- Istio;\n- BGP WAN obligatorio;\n- service mesh global;\n- Cilium egress controller complejo;\n- Suricata inline global;\n- WAF distribuido avanzado;\n- PKI din\u00e1mica compleja;\n- automatizaci\u00f3n de reputaci\u00f3n completa.\n\nLa arquitectura debe permitir a\u00f1adirlo despu\u00e9s.\n\n## 3.5 Human-compatible, agent-native\n\nTodos los servicios importantes deben disponer de:\n\n- API/CLI/Git para agentes;\n- UI cuando aporte valor a humanos;\n- identidad y permisos;\n- auditabilidad.\n\n## 3.6 Least privilege\n\nCada workload, tool runtime y agente recibe \u00fanicamente los permisos necesarios.\n\n## 3.7 Secrets usados, no necesariamente vistos\n\nCuando sea posible:\n\n```text\nLLM\n \u2193 tool call\nTool Runtime\n \u2193 secret\nExternal API\n```\n\nEl modelo no necesita recibir el valor del secret.\n\n---\n\n# 4. Arquitectura general de componentes\n\n```mermaid\nflowchart TB\n    Internet((Internet))\n    OVH[OVH / vRack]\n    PVE[Proxmox Cluster]\n    Ceph[(Ceph)]\n\n    subgraph RKE2[Wetopi RKE2 Production]\n      CP[3x RKE2 Control Plane\\nembedded etcd]\n      Platform[Platform Workers]\n      Edge[Edge Workers]\n      Hosting[Hosting Workers]\n      Agents[Agent Workers / Platform Workers]\n\n      Cilium[Cilium CNI]\n      Hubble[Hubble]\n      Argo[Argo CD]\n      Vault[Infisical Vault]\n      Docs[MkDocs / Docs]\n      Memory[Memory Plane]\n      Work[Work Coordination Plane]\n      Hermes[Hermes Fleet]\n      ADK[ADK Fleet]\n      Traefik[Traefik / Gateway API]\n      CSI[Ceph CSI]\n    end\n\n    Internet --> OVH\n    OVH --> PVE\n    PVE --> RKE2\n    PVE --- Ceph\n    CSI --> Ceph\n\n    CP --- Cilium\n    Platform --- Cilium\n    Edge --- Cilium\n    Hosting --- Cilium\n    Agents --- Cilium\n    Cilium --> Hubble\n\n    Argo --> Platform\n    Vault --> Platform\n    Docs --> Hermes\n    Memory --> Hermes\n    Memory --> ADK\n    Work --> Hermes\n    Work --> ADK\n    Hermes --> Agents\n    ADK --> Agents\n    Traefik --> Hosting\n```\n\n---\n\n# 5. Operating Model: todos los \u201cOps\u201d de Wetopi\n\nLa plataforma se divide en dominios operativos. Cada dominio tiene un prop\u00f3sito, source of truth y responsabilidades.\n\n| \u00c1rea | Responsabilidad principal | Source of truth principal |\n|---|---|---|\n| BootstrapOps | crear/reparar la foundation | Git + `wetopi-ops01` |\n| InfraOps | VMs, Proxmox, RKE2 nodes | OpenTofu + Ansible |\n| GitOps | estado declarativo Kubernetes | Git + Argo CD |\n| NetOps | CNI, routing, CIDR, policies | Git + APIs de red |\n| EdgeOps | IPVS/DSR, proxies WAN, Gateway API | Git + IPVS state |\n| EgressOps | NATGW, rutas, WAN IPs | Git/config + gateway state |\n| StorageOps | Ceph CSI/PVC/storage | Ceph + Kubernetes |\n| VaultOps / SecretOps | credentials y secrets | Infisical |\n| SecOps | RBAC, policies, security controls | Git + audit logs |\n| ObservabilityOps | m\u00e9tricas, logs, traces, flows | observability stack |\n| DevOps | CI/build/release/app lifecycle | Git + CI + Argo CD |\n| AgentOps | runtimes Hermes/ADK y capability bundles | `agents` + `ai-hub` |\n| MemoryOps | memoria institucional | Memory Plane |\n| WorkOps | coordinaci\u00f3n, tickets, leases | Work Ledger |\n| DocOps | docs/runbooks/ADR | Markdown Git |\n| BackupOps | backups y restore | backup repositories |\n| RecoveryOps | cluster/Vault/etcd disaster recovery | runbooks + backups |\n| HostingOps | WordPress/customer workloads | Kubernetes + business control plane |\n\n---\n\n# 6. BootstrapOps\n\n## 6.1 Prop\u00f3sito\n\nBootstrapOps existe para que la plataforma pueda construirse y repararse **sin depender de la propia plataforma**.\n\n## 6.2 Componentes\n\n```mermaid\nflowchart LR\n    Laptop[Engineer Laptop\\nCodex]\n    GH[GitHub\\nwetopi-platform]\n    Ops[wetopi-ops01]\n    Tofu[OpenTofu]\n    Ansible[Ansible]\n    PVE[Proxmox API]\n    RKE[RKE2]\n\n    Laptop --> GH\n    Laptop --> Ops\n    GH --> Ops\n    Ops --> Tofu\n    Ops --> Ansible\n    Tofu --> PVE\n    Ansible --> RKE\n```\n\n## 6.3 `wetopi-ops01`\n\nVM externa al cluster Kubernetes.\n\nResponsabilidades:\n\n- bootstrap;\n- bastion operativo;\n- OpenTofu;\n- Ansible;\n- kubectl/helm;\n- herramientas de recovery;\n- acceso a backups;\n- reparaci\u00f3n cuando GitOps/Cilium/Vault est\u00e9n ca\u00eddos.\n\nNo ejecuta workloads productivos.\n\nSizing inicial orientativo:\n\n```text\n4 vCPU\n8 GB RAM\n40\u201380 GB disk\nUbuntu 24.04\n1 NIC LAN\n```\n\n---\n\n# 7. InfraOps\n\n## 7.1 RKE2 inicial\n\n```text\n3 x control-plane\n3 x platform worker\n1 x ops VM externa\n```\n\nNombres:\n\n```text\nwetopi-ops01\n\nrke-cp01\nrke-cp02\nrke-cp03\n\nrke-platform01\nrke-platform02\nrke-platform03\n```\n\n## 7.2 Distribuci\u00f3n f\u00edsica\n\nIdeal:\n\n```text\npve01 -> rke-cp01 + rke-platform01\npve02 -> rke-cp02 + rke-platform02\npve03 -> rke-cp03 + rke-platform03\n```\n\nDebe respetarse failure domain.\n\n## 7.3 Control plane\n\nRKE2 con:\n\n- embedded etcd;\n- quorum 2/3;\n- API endpoint estable;\n- snapshots;\n- restore probado;\n- sin customer workloads.\n\nSizing orientativo:\n\n```text\n8 vCPU\n16 GB RAM\n50\u2013100 GB disk\n```\n\nEl almacenamiento de etcd debe priorizar baja latencia.\n\n## 7.4 IaC\n\n```text\nOpenTofu -> Proxmox resources / VMs\nAnsible  -> OS / RKE2 / host configuration\nArgo CD  -> Kubernetes workloads\n```\n\nNo mezclar responsabilidades.\n\n---\n\n# 8. GitHub y repositorios\n\nOrganizaci\u00f3n:\n\n```text\nwetopi-platform\n```\n\nRepositorios iniciales:\n\n```text\nwetopi-platform/\n\u251c\u2500\u2500 infrastructure\n\u251c\u2500\u2500 agent-services\n\u251c\u2500\u2500 ai-hub\n\u251c\u2500\u2500 agents\n\u2514\u2500\u2500 docs\n```\n\n## 8.1 `infrastructure`\n\n```text\ninfrastructure/\n\u251c\u2500\u2500 tofu/\n\u2502   \u2514\u2500\u2500 proxmox/\n\u251c\u2500\u2500 ansible/\n\u2502   \u251c\u2500\u2500 inventories/\n\u2502   \u251c\u2500\u2500 roles/\n\u2502   \u2514\u2500\u2500 playbooks/\n\u251c\u2500\u2500 clusters/\n\u2502   \u2514\u2500\u2500 production/\n\u251c\u2500\u2500 apps/\n\u251c\u2500\u2500 platform/\n\u251c\u2500\u2500 networking/\n\u251c\u2500\u2500 storage/\n\u251c\u2500\u2500 security/\n\u251c\u2500\u2500 observability/\n\u251c\u2500\u2500 bootstrap/\n\u2514\u2500\u2500 docs/\n```\n\n## 8.2 `agent-services`\n\nServicios compartidos:\n\n- Memory Plane;\n- Work Ledger adapter;\n- Hermes Memory Provider;\n- identidad/approval cuando sea necesario.\n\n## 8.3 `ai-hub`\n\n```text\nskills/\ntools/\ncomponents/\ncontracts/\nschemas/\nsdk/\nevals/\ntests/\n```\n\nRegla:\n\n> Una tool utilizada por m\u00e1s de un agente deja de pertenecer al agente y pasa a `ai-hub`.\n\n## 8.4 `agents`\n\nMonorepo inicial de:\n\n- Hermes;\n- ADK;\n- configs;\n- capability bundles;\n- deployments;\n- tests.\n\n## 8.5 `docs`\n\nMarkdown + MkDocs Material.\n\n---\n\n# 9. GitOps \u2014 Argo CD\n\n## 9.1 Decisi\u00f3n\n\nArgo CD ser\u00e1 el sistema GitOps de Wetopi.\n\n## 9.2 Modelo\n\n```mermaid\nflowchart LR\n    Dev[Human / Agent]\n    Git[GitHub]\n    Argo[Argo CD]\n    K8s[RKE2 API]\n    Apps[Applications]\n\n    Dev -->|PR / merge| Git\n    Git --> Argo\n    Argo --> K8s\n    K8s --> Apps\n```\n\n## 9.3 Principios\n\n- CI no har\u00e1 `kubectl apply` en producci\u00f3n como mecanismo normal.\n- Git describe estado deseado.\n- Argo CD reconcilia.\n- Drift manual debe ser visible.\n- Aplicaciones deben agruparse por `AppProject`/dominio.\n- Human UI se considera una ventaja operativa importante.\n\n## 9.4 Layout GitOps\n\n**OPEN:** decidir entre:\n\n- App-of-Apps;\n- ApplicationSet;\n- combinaci\u00f3n por environments/domains.\n\nNo bloquear bootstrap del cluster por esta decisi\u00f3n, pero cerrarla antes de escalar apps.\n\n---\n\n# 10. SecretOps / VaultOps\n\n## 10.1 Decisi\u00f3n\n\nEl Vault operativo de Wetopi ser\u00e1:\n\n```text\nInfisical self-hosted\n```\n\nOpenBao no forma parte de v1.\n\n## 10.2 Requisito real\n\nEl Vault debe permitir que un agente o humano pueda:\n\n```text\ncreate\nread\nupdate\nrotate\ndelete\nlist\n```\n\ncredentials como:\n\n- username/password;\n- API token;\n- SSH key;\n- deploy token;\n- client secret;\n- customer credential.\n\nEjemplo conceptual:\n\n```text\n/platform/proxmox/api\n/customers/837/wp-admin\n/development/github/token\n/agents/hermes-sre/private\n```\n\n## 10.3 Source of truth\n\n```text\nInfisical = source of truth operativo\nKubernetes Secret = materializaci\u00f3n runtime\nSOPS = bootstrap/recovery declarativo\n```\n\n## 10.4 Arquitectura\n\n```mermaid\nflowchart TB\n    Humans[Humans\\nWeb UI / CLI]\n    Agents[Hermes / ADK / Codex]\n    Tools[Tool runtimes]\n    Vault[Infisical]\n    Operator[Infisical Kubernetes Integration]\n    KSec[Kubernetes Secrets]\n    Pods[Applications / Pods]\n\n    Humans --> Vault\n    Agents -->|vault contract| Vault\n    Agents --> Tools\n    Tools --> Vault\n    Vault --> Operator\n    Operator --> KSec\n    KSec --> Pods\n```\n\n## 10.5 Wetopi Vault Contract\n\nLos agentes no deben quedar fuertemente acoplados a la API concreta de Infisical.\n\n`ai-hub` debe exponer:\n\n```text\nvault.get()\nvault.put()\nvault.update()\nvault.delete()\nvault.list()\nvault.exists()\n```\n\n## 10.6 Infisical validation\n\n**OPEN / REQUIRED BEFORE PRODUCTION:** verificar:\n\n- features self-hosted requeridas;\n- modelo de licensing;\n- machine identities;\n- Kubernetes integration;\n- audit;\n- versioning;\n- backup/restore;\n- l\u00edmites que puedan afectar a una flota de agentes grande.\n\n---\n\n# 11. SOPS + age\n\nSOPS no ser\u00e1 el Vault.\n\nSu responsabilidad es resolver el problema inevitable de bootstrap:\n\n> \u00bfD\u00f3nde guardamos los secretos necesarios para arrancar el sistema que guarda secretos?\n\nModelo:\n\n```mermaid\nflowchart LR\n    Git[Git]\n    SOPS[SOPS encrypted files]\n    Age[age keys]\n    Bootstrap[Bootstrap process]\n    Vault[Infisical]\n\n    Git --> SOPS\n    Age --> Bootstrap\n    SOPS --> Bootstrap\n    Bootstrap --> Vault\n```\n\nSe utilizar\u00e1n al menos:\n\n- clave `age` de bootstrap/automation;\n- clave `age` de recovery offline.\n\nNunca almacenar claves privadas `age` en Git.\n\n---\n\n# 12. Kubernetes Secrets\n\nUna vez Infisical est\u00e1 operativo:\n\n```text\nInfisical\n \u2193\nKubernetes integration/operator\n \u2193\nKubernetes Secret\n \u2193\nPod/container\n```\n\nRKE2 debe tener **Secrets Encryption at Rest** habilitado para proteger la copia persistida en etcd.\n\n---\n\n# 13. Agent Secret Model\n\n## 13.1 Regla principal\n\nUn agente puede necesitar permiso para **usar** una credencial sin tener permiso para **leer su contenido**.\n\n## 13.2 Tool runtime model\n\n```mermaid\nsequenceDiagram\n    participant LLM as Hermes / ADK\n    participant Tool as Platform Tool Runtime\n    participant Vault as Infisical\n    participant API as External API\n\n    LLM->>Tool: proxmox.restart_vm(vm=123)\n    Tool->>Vault: get /platform/proxmox/api\n    Vault-->>Tool: credential\n    Tool->>API: authenticated request\n    API-->>Tool: result\n    Tool-->>LLM: sanitized result\n```\n\n## 13.3 Capability domains\n\nNo crear un sidecar por tool.\n\nAgrupar por dominio:\n\n```text\nplatform-tools\n\u251c\u2500\u2500 kubernetes\n\u251c\u2500\u2500 proxmox\n\u251c\u2500\u2500 ceph\n\u2514\u2500\u2500 networking\n\nwordpress-tools\n\u251c\u2500\u2500 wordpress\n\u251c\u2500\u2500 database\n\u2514\u2500\u2500 backups\n\nmarketing-tools\n\u251c\u2500\u2500 intercom\n\u2514\u2500\u2500 analytics\n```\n\n## 13.4 Coding agents\n\nHermes/Codex de desarrollo o SRE pueden necesitar acceso m\u00e1s amplio.\n\nDebe otorgarse por paths/roles, no con acceso global al Vault.\n\n---\n\n# 14. NetOps\n\n## 14.1 NICs\n\nCada VM tendr\u00e1 inicialmente una sola NIC LAN:\n\n```text\nVM\n \u2514\u2500\u2500 eth0\n      \u2514\u2500\u2500 LAN / OVH vRack\n```\n\n## 14.2 Ceph network\n\nRed existente:\n\n```text\n192.168.96.0/20\n```\n\nEstado:\n\n```text\nEXISTING\nDO NOT MODIFY\n```\n\nEst\u00e1 hiperconvergida con Proxmox/Ceph.\n\nNo renumerar ni redise\u00f1ar como parte de RKE2.\n\n## 14.3 CIDRs propuestos\n\n```text\nNode LAN:      OPEN \u2014 pendiente inventario\nPod CIDR:      10.64.0.0/14\nService CIDR:  10.68.0.0/16\nCeph:          192.168.96.0/20 existente\n```\n\nLos rangos Pod/Service deben verificarse contra todo el direccionamiento existente antes de aplicarse.\n\n## 14.4 Cilium\n\nCilium ser\u00e1 CNI.\n\nBaseline inicial:\n\n```text\nCilium\nVXLAN encapsulation\nHubble enabled\n```\n\nNo exigir native routing/BGP inicialmente.\n\n## 14.5 Hubble\n\nObjetivos:\n\n- flow visibility;\n- pod identity;\n- namespace identity;\n- source/destination;\n- policy verdicts;\n- futuras correlaciones con site/customer.\n\n---\n\n# 15. StorageOps\n\nCeph seguir\u00e1 siendo storage principal.\n\nRKE2 consumir\u00e1 Ceph mediante CSI.\n\n```mermaid\nflowchart LR\n    Pod[Pod]\n    PVC[PVC]\n    CSI[Ceph CSI]\n    Node[RKE2 Node]\n    Ceph[(Ceph Cluster)]\n\n    Pod --> PVC\n    PVC --> CSI\n    CSI --> Node\n    Node --> Ceph\n```\n\nTests obligatorios antes de hosting:\n\n- crear PVC;\n- mount;\n- pod delete/recreate;\n- persistencia;\n- failure b\u00e1sico;\n- snapshot si se adopta;\n- rendimiento aceptable.\n\n---\n\n# 16. Endpoint HA de la API RKE2\n\nRKE2 necesita un endpoint estable:\n\n```text\nrke2-api.<internal-domain>\n          \u2193\n        VIP\n          \u2193\ncp01 / cp02 / cp03\n```\n\nPropuesta actual:\n\n```text\n2 x HAProxy + Keepalived\nprivate VIP\n```\n\nEstado:\n\n**OPEN \u2014 debe cerrarse antes de instalar el cluster definitivo.**\n\nLa propuesta se considera conservadora y compatible con el conocimiento operativo existente, pero todav\u00eda no est\u00e1 aprobada como decisi\u00f3n final.\n\n---\n\n# 17. EdgeOps \u2014 ingress WAN\n\n## 17.1 Decisi\u00f3n\n\nConservar inicialmente:\n\n```text\nVRRP\n+\nIPVS\n+\nDSR\n```\n\n## 17.2 Motivo\n\nLos WAN nodes est\u00e1n distribuidos sobre varios hosts Proxmox, y cada host f\u00edsico dispone de capacidad de salida OVH propia.\n\nDSR permite:\n\n```text\nrequest:\nclient -> IPVS -> edge\n\nresponse:\nedge -> client DIRECT\n```\n\nPor tanto se preserva la agregaci\u00f3n de bandwidth domains.\n\n## 17.3 Arquitectura\n\n```mermaid\nflowchart TB\n    Client[Internet Client]\n    VIP[Public VIP / VRRP]\n    LVS[IPVS Directors]\n    Edge1[RKE2 Edge 1\\nTraefik]\n    Edge2[RKE2 Edge 2\\nTraefik]\n    Edge3[RKE2 Edge 3\\nTraefik]\n    Gateway[Gateway API]\n    Service[Kubernetes Service]\n    Site[WordPress Pod]\n\n    Client --> VIP\n    VIP --> LVS\n    LVS -->|DSR| Edge1\n    LVS -->|DSR| Edge2\n    LVS -->|DSR| Edge3\n    Edge1 --> Gateway\n    Edge2 --> Gateway\n    Edge3 --> Gateway\n    Gateway --> Service\n    Service --> Site\n\n    Edge1 -. direct response .-> Client\n    Edge2 -. direct response .-> Client\n    Edge3 -. direct response .-> Client\n```\n\n## 17.4 DSR PoC\n\nRequired before production:\n\n- packet capture request;\n- packet capture response;\n- verify response bypasses IPVS;\n- verify source public IP;\n- verify real client IP at proxy;\n- verify health removal/reinsertion;\n- verify node drain procedure.\n\n---\n\n# 18. Proxy \u2014 Traefik\n\nTraefik ser\u00e1 el proxy inicial.\n\nResponsabilidades:\n\n- TLS termination;\n- HTTP routing;\n- Gateway API;\n- client IP forwarding;\n- metrics;\n- health;\n- base futura para WAF integration.\n\nEstado de detalles:\n\n**OPEN:** definir deployment exacto:\n\n- `DaemonSet`;\n- `hostNetwork` s\u00ed/no seg\u00fan PoC DSR;\n- puertos;\n- VIP handling;\n- health endpoint;\n- trusted proxies.\n\n---\n\n# 19. Gateway API\n\nGateway API ser\u00e1 el modelo declarativo L7.\n\n```text\nDomain\n \u2193\nHTTPRoute\n \u2193\nService\n \u2193\nEndpointSlice\n \u2193\nPod\n```\n\nEsto elimina del path Kubernetes:\n\n- Registrator;\n- Consul service discovery de websites;\n- consul-template;\n- regeneraci\u00f3n/reload de nginx config.\n\n---\n\n# 20. EgressOps\n\n## 20.1 Filosof\u00eda\n\nEgress v1 debe ser simple.\n\nNo se construye inicialmente un controlador sofisticado de reputaci\u00f3n/IPAM.\n\n## 20.2 NATGW\n\nEjemplo:\n\n```text\ngw100\nLAN: 192.168.88.100\nWAN: PUBLIC_IP_A\n\ngw101\nLAN: 192.168.88.101\nWAN: PUBLIC_IP_B\n\ngw102\nLAN: 192.168.88.102\nWAN: PUBLIC_IP_C\n```\n\nVM Ubuntu con:\n\n- routing;\n- nftables/iptables;\n- SNAT;\n- logging;\n- health.\n\n## 20.3 Selecci\u00f3n l\u00f3gica\n\n```text\nsite/customer/workload\n   \u2193\negress profile / label\n   \u2193\ngw100 / gw101 / gw102\n```\n\nEjemplo l\u00f3gico:\n\n```yaml\nwetopi.io/egress-gateway: gw101\n```\n\n## 20.4 Enforcement\n\nEstado:\n\n**OPEN.**\n\nBaseline propuesto:\n\n- policy routing;\n- automatizaci\u00f3n ligera que observe Pods/labels;\n- no depender de Pod IP est\u00e1tica;\n- no construir todav\u00eda controller complejo.\n\nEvoluci\u00f3n posible:\n\n```text\nCilium Egress Gateway\n```\n\nsin cambiar el contrato l\u00f3gico.\n\n## 20.5 Pools iniciales\n\nSeparar al menos:\n\n```text\nhosting-general\nhosting-premium\nplatform\nagents-crawlers\n```\n\nLos crawlers nunca deben compartir egress WAN IP con hosting general.\n\n---\n\n# 21. ObservabilityOps\n\n## 21.1 Fuentes\n\nLa observabilidad futura debe combinar:\n\n```text\nKubernetes metrics\napplication logs\nHubble flows\nTraefik access logs\nNATGW logs\nKubernetes audit\nInfisical audit\nArgo CD events\nagent/tool audit\n```\n\nPosteriormente:\n\n```text\nSuricata\nWAF events\nDNS telemetry\nreputation signals\n```\n\n## 21.2 Correlaci\u00f3n deseada\n\nUna observaci\u00f3n de red deber\u00eda poder acabar enriquecida con:\n\n```text\ncustomer_id\nsite_id\npod\nnamespace\nnode\nproxmox_host\negress_gateway\negress_public_ip\ndestination\nprotocol\nverdict\n```\n\n---\n\n# 22. SecOps\n\nSecurity foundation v1:\n\n- Kubernetes RBAC;\n- ServiceAccounts;\n- namespace isolation;\n- NetworkPolicies;\n- Infisical paths/identities;\n- RKE2 Secrets Encryption at Rest;\n- SOPS bootstrap keys offline;\n- least privilege;\n- audit trails.\n\nNo asumir que `cannot get Secret` equivale a no poder acceder a secretos: un usuario con capacidad de crear Pods en un namespace puede potencialmente montar Secrets de ese namespace.\n\nPor tanto RBAC de Pods y Vault/Secrets debe dise\u00f1arse conjuntamente.\n\n---\n\n# 23. Namespaces\n\nPropuesta inicial orientativa:\n\n```text\nwetopi-platform\nwetopi-edge\nwetopi-hosting\nwetopi-agents-sre\nwetopi-agents-development\nwetopi-agents-marketing\nwetopi-observability\nwetopi-security\n```\n\nEstado:\n\n**OPEN:** modelo definitivo de namespaces y AppProjects/RBAC.\n\n---\n\n# 24. DevOps\n\nDevOps cubre lifecycle de software, no infraestructura f\u00edsica.\n\nPipeline conceptual:\n\n```mermaid\nflowchart LR\n    Code[Application Code]\n    CI[CI / Tests / Build]\n    Registry[Container Registry]\n    Config[GitOps Repo]\n    Argo[Argo CD]\n    RKE[RKE2]\n\n    Code --> CI\n    CI --> Registry\n    CI -->|version bump / PR| Config\n    Config --> Argo\n    Argo --> RKE\n```\n\nPrincipios:\n\n- build artifacts inmutables;\n- manifests/version changes en Git;\n- Argo CD despliega;\n- no despliegues manuales silenciosos.\n\n---\n\n# 25. AgentOps\n\n## 25.1 Actores\n\n```text\nActor\n\u251c\u2500\u2500 Human\n\u251c\u2500\u2500 Hermes\n\u251c\u2500\u2500 ADK Agent\n\u2514\u2500\u2500 future runtime\n```\n\nCampos de identidad:\n\n```text\nactor_id\nactor_type\nruntime\ncrew\nrole\nuser_id\nenvironment\n```\n\n## 25.2 Hermes\n\nHermes personales:\n\n- developer/coding;\n- SRE/DevOps;\n- marketing;\n- otros empleados cuando proceda.\n\nCada Hermes tendr\u00e1:\n\n```text\nlocal personal memory\n+\nFleet Memory\n+\nWork Ledger\n+\nrole-specific tools\n+\nVault permissions\n```\n\n## 25.3 ADK\n\nADK fleet:\n\n- observers;\n- specialists;\n- managers;\n- domain agents.\n\nTodos consumen servicios compartidos.\n\n---\n\n# 26. MemoryOps\n\nMemory Plane es memoria institucional, no source of truth de infraestructura viva.\n\nClases:\n\n- factual;\n- operational con TTL;\n- procedural;\n- decisions;\n- shared episodic.\n\nDebe soportar:\n\n- scopes;\n- provenance;\n- confidence;\n- temporalidad;\n- contradictions;\n- candidate facts.\n\nHermes combina:\n\n```text\nLocal Memory\n+\nWetopi Fleet Memory\n```\n\n---\n\n# 27. WorkOps\n\nWork Coordination Plane evita conflictos entre agentes/humanos.\n\nNo es solo un Kanban.\n\nDebe manejar:\n\n- tasks;\n- ownership;\n- claims;\n- resource scopes;\n- shared/exclusive locks;\n- TTL leases;\n- intents;\n- activity journal;\n- dependencies;\n- conflict detection.\n\nContrato m\u00ednimo:\n\n```text\nwork.list()\nwork.get()\nwork.create()\nwork.claim()\nwork.release()\nwork.update()\nwork.comment()\nwork.complete()\nwork.find_conflicts()\nwork.acquire()\nwork.renew()\nwork.release_resources()\n```\n\nBackend a\u00fan desacoplado.\n\n---\n\n# 28. DocOps\n\nDocumentaci\u00f3n:\n\n```text\nMarkdown\n+\nGit\n+\nMkDocs Material\n```\n\nRoles:\n\n```text\nREADME del repo\n= c\u00f3mo funciona el componente\n\nwetopi-platform/docs\n= c\u00f3mo Wetopi lo dise\u00f1a, opera y gobierna\n```\n\nFlujo:\n\n```text\nincident / project\n \u2193\nWork Ledger\n \u2193\nvalidated knowledge\n \u2193\nMemory\n \u2193\nADR / runbook / architecture doc\n \u2193\nDocs repo\n```\n\n---\n\n# 29. BackupOps y RecoveryOps\n\nDebe existir recuperaci\u00f3n fuera del cluster.\n\nObjetos a proteger:\n\n```text\nGit repos\netcd snapshots\nInfisical database/state\nSOPS/age recovery material\nCeph configuration/data policies\nArgo configuration (also in Git)\ncritical application databases\n```\n\n**OPEN:** destino externo definitivo de backups.\n\nPrincipio:\n\n> un backup no se considera v\u00e1lido hasta haber probado un restore.\n\n---\n\n# 30. HostingOps\n\n## 30.1 Primera migraci\u00f3n\n\nNo refactorizar todo `wepod` a la vez.\n\nInicialmente:\n\n```text\nDocker wepod\n \u2193\nPod / StatefulSet\n```\n\nmanteniendo cuando convenga:\n\n```text\ns6\nnginx\nphp-fpm\nmysql\n```\n\nLa separaci\u00f3n de DB/frontend puede llegar despu\u00e9s.\n\n## 30.2 Hosting workers\n\nSe crean despu\u00e9s de estabilizar Platform y Edge.\n\n```text\nrke-host01\nrke-host02\nrke-host03\n...\n```\n\nPrimera cohorte:\n\n```text\n~10 sites\n```\n\nDespu\u00e9s escalar progresivamente.\n\n## 30.3 Futuro `WordPressSite` CRD\n\nPosible modelo futuro:\n\n```yaml\napiVersion: wetopi.io/v1\nkind: WordPressSite\nmetadata:\n  name: site-12345\nspec:\n  domains:\n    - example.com\n  network:\n    egressGateway: gw100\n  storage:\n    size: 20Gi\n```\n\nNo es requisito de v1.\n\n---\n\n# 31. Arquitectura de data planes y control planes\n\n```mermaid\nflowchart TB\n    subgraph Control[CONTROL PLANES]\n      Git[GitHub]\n      Tofu[OpenTofu]\n      Ansible[Ansible]\n      Argo[Argo CD]\n      Vault[Infisical]\n      Work[Work Ledger]\n      Memory[Memory Plane]\n      Docs[Docs]\n    end\n\n    subgraph K8s[KUBERNETES CONTROL]\n      API[RKE2 API]\n      ETCD[(etcd)]\n      Cilium[Cilium control]\n    end\n\n    subgraph Data[DATA PLANES]\n      IPVS[IPVS / DSR]\n      Edge[Traefik Edge]\n      Pods[Hosting Pods]\n      NATGW[NAT Gateways]\n      Ceph[(Ceph)]\n    end\n\n    Git --> Tofu\n    Git --> Ansible\n    Git --> Argo\n    Tofu --> API\n    Ansible --> API\n    Argo --> API\n    API --> ETCD\n    API --> Cilium\n    Vault --> Pods\n    IPVS --> Edge\n    Edge --> Pods\n    Pods --> NATGW\n    Pods --> Ceph\n```\n\n---\n\n# 32. Dependency graph del bootstrap\n\n```mermaid\nflowchart TD\n    Git[GitHub org + repos]\n    Inventory[Proxmox / Network Inventory]\n    IaC[OpenTofu + Ansible]\n    Ops[wetopi-ops01]\n    VMs[3 CP + 3 Platform VMs]\n    RKE[RKE2 HA]\n    Cilium[Cilium]\n    Hubble[Hubble]\n    CSI[Ceph CSI]\n    Argo[Argo CD]\n    SOPS[SOPS + age bootstrap]\n    Vault[Infisical]\n    Docs[Docs]\n    Memory[Memory Plane]\n    Work[Work Ledger]\n    AI[AI Hub]\n    Agents[Hermes + ADK]\n    Edge[Edge workers + Traefik]\n    Egress[NATGW]\n    Hosting[Hosting workers]\n    Sites[First sites]\n\n    Git --> Inventory\n    Inventory --> IaC\n    IaC --> Ops\n    Ops --> VMs\n    VMs --> RKE\n    RKE --> Cilium\n    Cilium --> Hubble\n    Cilium --> CSI\n    CSI --> Argo\n    Argo --> SOPS\n    SOPS --> Vault\n    Vault --> Docs\n    Vault --> Memory\n    Vault --> Work\n    Docs --> AI\n    Memory --> Agents\n    Work --> Agents\n    AI --> Agents\n    Agents --> Edge\n    Edge --> Egress\n    Egress --> Hosting\n    Hosting --> Sites\n```\n\n---\n\n# 33. Plan de implementaci\u00f3n por fases\n\n## Fase 0 \u2014 Gobierno y repos\n\nDeliverables:\n\n- organizaci\u00f3n `wetopi-platform`;\n- repos iniciales;\n- README;\n- CODEOWNERS;\n- branch protection;\n- convenciones;\n- permisos de Codex.\n\n## Fase 1 \u2014 Discovery / inventario\n\nInventariar:\n\n- hosts Proxmox;\n- CPU/RAM;\n- bridges;\n- VLANs;\n- vRack;\n- direccionamiento;\n- storage;\n- Ceph;\n- IPVS;\n- VIPs;\n- NATGW;\n- failure domains;\n- bandwidth domains.\n\nNo crear ranges nuevos hasta completar este inventario.\n\n## Fase 2 \u2014 IaC foundation\n\n- Ubuntu 24.04 template/cloud-init;\n- OpenTofu Proxmox modules;\n- Ansible baseline;\n- automation account Proxmox.\n\n## Fase 3 \u2014 Bootstrap node\n\nCrear/provisionar `wetopi-ops01`.\n\n## Fase 4 \u2014 RKE2 control plane\n\nCrear:\n\n```text\nrke-cp01\nrke-cp02\nrke-cp03\n```\n\nCerrar previamente endpoint HA.\n\n## Fase 5 \u2014 Platform workers\n\nCrear:\n\n```text\nrke-platform01\nrke-platform02\nrke-platform03\n```\n\n## Fase 6 \u2014 RKE2 acceptance\n\n- quorum;\n- fail one CP;\n- snapshot etcd;\n- restore test;\n- node drain;\n- reboot tests.\n\n## Fase 7 \u2014 Cilium / Hubble\n\n- VXLAN;\n- Pod CIDR;\n- Service CIDR;\n- Hubble;\n- DNS;\n- NetworkPolicy baseline.\n\n## Fase 8 \u2014 Ceph CSI\n\n- connectivity;\n- CSI;\n- PVC acceptance.\n\n## Fase 9 \u2014 Argo CD\n\n- install;\n- projects;\n- first Git reconciled app;\n- drift test;\n- rollback test.\n\n## Fase 10 \u2014 SOPS bootstrap\n\n- age keys;\n- recovery key offline;\n- encrypted bootstrap values;\n- recovery procedure.\n\n## Fase 11 \u2014 Infisical Vault\n\n- self-hosted deploy;\n- persistence;\n- backup;\n- identities;\n- Kubernetes integration;\n- Wetopi `vault.*` contract.\n\n## Fase 12 \u2014 AI-first services\n\n- Docs;\n- Memory Plane;\n- Work Coordination Plane;\n- AI Hub.\n\n## Fase 13 \u2014 Hermes / ADK\n\nDeploy definitive agents after shared services are healthy.\n\n## Fase 14 \u2014 Edge PoC\n\n- edge RKE2 nodes;\n- Traefik;\n- Gateway API;\n- DSR PoC;\n- health integration with IPVS.\n\n## Fase 15 \u2014 Egress PoC\n\n- gw100/gw101/gw102;\n- policy routing selection;\n- WAN IP rotation;\n- crawler isolation.\n\n## Fase 16 \u2014 Hosting pilot\n\n- 3 initial hosting workers;\n- ~10 sites;\n- storage/network/performance validation.\n\n## Fase 17 \u2014 Progressive migration\n\nIncrease cohorts while shrinking Swarm.\n\n---\n\n# 34. Migration strategy\n\n```mermaid\nflowchart LR\n    Swarm[Docker Swarm\\nCurrent Production]\n    New[RKE2 Platform]\n    Pilot[10 Sites]\n    Batch1[50 Sites]\n    Batch2[100+ Sites]\n    Final[Swarm Retirement]\n\n    Swarm --> New\n    New --> Pilot\n    Pilot --> Batch1\n    Batch1 --> Batch2\n    Batch2 --> Final\n```\n\nPrincipio:\n\n```text\nRKE2 failure during early phases\n!=\ncustomer outage\n```\n\n---\n\n# 35. Criterios de aceptaci\u00f3n\n\n## 35.1 Infra / RKE2\n\n- [ ] p\u00e9rdida de un CP no rompe API;\n- [ ] etcd quorum correcto;\n- [ ] snapshot funciona;\n- [ ] restore probado;\n- [ ] nodes reproducibles desde IaC;\n- [ ] no customer workloads en CP.\n\n## 35.2 Network\n\n- [ ] Pod-to-Pod;\n- [ ] Pod-to-Service;\n- [ ] DNS;\n- [ ] Hubble visibility;\n- [ ] no CIDR collision;\n- [ ] Ceph reachable desde nodes;\n- [ ] DSR demostrado por packet capture.\n\n## 35.3 Storage\n\n- [ ] PVC create;\n- [ ] persistence;\n- [ ] pod recreation;\n- [ ] failure test;\n- [ ] rendimiento aceptable.\n\n## 35.4 GitOps\n\n- [ ] Argo reconcilia desde Git;\n- [ ] drift detectable;\n- [ ] rollback probado;\n- [ ] Git conserva estado deseado.\n\n## 35.5 Vault\n\n- [ ] agente puede crear credential;\n- [ ] humano puede consultar credential autorizado;\n- [ ] workload puede recibir secret;\n- [ ] secret no aparece plaintext en Git;\n- [ ] backup/restore Infisical probado;\n- [ ] path permissions validados;\n- [ ] raw secrets no llegan al LLM cuando se usa tool runtime.\n\n## 35.6 Egress\n\n- [ ] workload A sale por gw100;\n- [ ] workload B sale por gw101;\n- [ ] cambio de gateway no requiere mover workload;\n- [ ] cambio de WAN IP verificable;\n- [ ] crawlers aislados de hosting general.\n\n## 35.7 Edge\n\n- [ ] IPVS health funciona;\n- [ ] DSR funciona;\n- [ ] client IP preservada;\n- [ ] TLS correcto;\n- [ ] HTTPRoute din\u00e1mico;\n- [ ] drain seguro.\n\n## 35.8 Agents\n\n- [ ] Hermes usa Vault contract;\n- [ ] ADK usa scoped tools;\n- [ ] tool runtime consume credentials;\n- [ ] Work Ledger evita conflictos;\n- [ ] Fleet Memory consultable;\n- [ ] docs accesibles por Git.\n\n---\n\n# 36. Decisiones cerradas\n\n| ID | Decisi\u00f3n |\n|---|---|\n| ARC-001 | RKE2 |\n| ARC-002 | 3 CP + embedded etcd |\n| ARC-003 | greenfield paralelo a Swarm |\n| ARC-004 | `wetopi-ops01` externo |\n| ARC-005 | OpenTofu + Ansible |\n| ARC-006 | Cilium |\n| ARC-007 | Hubble |\n| ARC-008 | Ceph existente + CSI |\n| ARC-009 | Argo CD |\n| ARC-010 | Infisical self-hosted Vault |\n| ARC-011 | SOPS + age solo bootstrap/recovery |\n| ARC-012 | IPVS + VRRP + DSR inicial |\n| ARC-013 | Traefik inicial |\n| ARC-014 | Gateway API |\n| ARC-015 | NATGW Ubuntu simples |\n| ARC-016 | no Istio inicialmente |\n| ARC-017 | no BGP obligatorio inicialmente |\n| ARC-018 | agents/crawlers separados de customer egress |\n| ARC-019 | Markdown + MkDocs |\n| ARC-020 | AI Hub compartido |\n\n---\n\n# 37. Decisiones abiertas / review board\n\nEstas decisiones deben ser revisadas por Platform Engineering antes de implementaci\u00f3n o antes de su fase correspondiente.\n\n| Tema | Estado | Cu\u00e1ndo debe cerrarse |\n|---|---|---|\n| Node LAN CIDR | OPEN | antes de crear VMs RKE2 |\n| Pod CIDR `10.64.0.0/14` | Proposed | antes de RKE2 install |\n| Service CIDR `10.68.0.0/16` | Proposed | antes de RKE2 install |\n| API HA: HAProxy+Keepalived | Proposed | antes de CP bootstrap |\n| Traefik hostNetwork/DSR details | OPEN | antes de Edge PoC |\n| Egress enforcement implementation | OPEN | antes de Egress PoC |\n| Infisical licensing/feature validation | OPEN | antes de prod Vault |\n| Argo app layout | OPEN | antes de app scale-out |\n| OIDC provider | OPEN | antes de broad human access |\n| Namespace model final | OPEN | antes de production RBAC |\n| External backup destination | OPEN | antes de production data |\n| Central observability stack | OPEN | antes de migration scale |\n| Work Ledger backend | OPEN | antes de multi-agent operations |\n\n---\n\n# 38. Riesgos y mitigaciones\n\n## 38.1 Overengineering\n\n**Riesgo:** desplegar demasiado antes de tener workloads.\n\n**Mitigaci\u00f3n:** v1 simple, roadmap posterior.\n\n## 38.2 P\u00e9rdida de DSR\n\n**Riesgo:** degradar capacidad WAN agregada.\n\n**Mitigaci\u00f3n:** PoC + packet capture + no retirar IPVS hasta equivalencia demostrada.\n\n## 38.3 CIDR collision\n\n**Riesgo:** colisi\u00f3n con redes legacy/OVH/Ceph.\n\n**Mitigaci\u00f3n:** inventory first.\n\n## 38.4 Secrets expuestos a LLM\n\n**Riesgo:** tokens en prompts/logs.\n\n**Mitigaci\u00f3n:** tool runtimes + Infisical + least privilege.\n\n## 38.5 Vault lock-in/licensing\n\n**Riesgo:** features cr\u00edticas fuera de self-hosted assumptions.\n\n**Mitigaci\u00f3n:** validar Infisical y usar Wetopi `vault.*` contract.\n\n## 38.6 GitOps blast radius\n\n**Riesgo:** un merge despliega cambio err\u00f3neo global.\n\n**Mitigaci\u00f3n:** AppProjects, environments, PR review, health gates.\n\n## 38.7 Ceph dependency\n\n**Riesgo:** Kubernetes storage depende del cluster Ceph hiperconvergido.\n\n**Mitigaci\u00f3n:** no modificar Ceph en bootstrap, test de CSI y recovery independientes.\n\n## 38.8 Agent conflicts\n\n**Riesgo:** dos agentes modifican recurso simult\u00e1neamente.\n\n**Mitigaci\u00f3n:** Work Ledger + leases + resource scopes.\n\n---\n\n# 39. Responsabilidades por equipo\n\n## Platform Engineering\n\n- RKE2;\n- IaC;\n- Argo CD;\n- Cilium;\n- CSI;\n- node lifecycle;\n- Vault platform;\n- cluster reliability.\n\n## Network / SRE\n\n- CIDRs;\n- vRack;\n- IPVS;\n- VRRP;\n- DSR;\n- NATGW;\n- WAN IPs;\n- egress routing;\n- Hubble flow validation.\n\n## Development\n\n- app packaging;\n- containers;\n- GitOps manifests;\n- APIs;\n- WordPress migration tooling;\n- future CRDs/controllers.\n\n## Security\n\n- Infisical permissions;\n- RBAC;\n- NetworkPolicy;\n- secret usage;\n- audit;\n- future Suricata/WAF.\n\n## Agent Engineering\n\n- `ai-hub`;\n- Memory Plane integration;\n- Work Ledger;\n- Hermes/ADK capabilities;\n- tool runtimes;\n- safety boundaries.\n\n## Operations\n\n- runbooks;\n- incidents;\n- backups;\n- restore drills;\n- capacity;\n- production migrations.\n\n---\n\n# 40. Suggested first architecture review agenda\n\nPara presentar este documento a otros ingenieros, la primera sesi\u00f3n deber\u00eda revisar en este orden:\n\n1. RKE2 vs constraints de Proxmox/Ceph.\n2. Node LAN y CIDRs.\n3. API HA endpoint.\n4. Cilium VXLAN y Ceph connectivity.\n5. IPVS/DSR preservation.\n6. Traefik edge PoC.\n7. NATGW/eject routing model.\n8. Argo CD repository layout.\n9. Infisical self-hosted requirements/licensing.\n10. namespace/RBAC/security boundaries.\n11. backup/recovery design.\n12. migration pilot criteria.\n\nNo intentar resolver Work Ledger, WAF, BGP, CRDs y service mesh en esa primera reuni\u00f3n.\n\n---\n\n# 41. Immediate next actions\n\n## NOW-01\n\nCrear/inicializar los cinco repos en `wetopi-platform`.\n\n## NOW-02\n\nDar acceso a Codex a la organizaci\u00f3n GitHub con permisos suficientes para gestionar repos, PRs y workflows.\n\n## NOW-03\n\nCrear un inventario automatizado/read-only de Proxmox y networking.\n\n## NOW-04\n\nProducir un `network-inventory.md` con todos los rangos actuales antes de fijar Node/Pod/Service CIDRs.\n\n## NOW-05\n\nCerrar el dise\u00f1o del endpoint API HA.\n\n## NOW-06\n\nCrear plantilla Ubuntu 24.04 y `wetopi-ops01`.\n\n## NOW-07\n\nImplementar OpenTofu/Ansible de CP/workers sin desplegar todav\u00eda servicios de negocio.\n\n## NOW-08\n\nHacer acceptance test de RKE2 antes de Cilium/Ceph/Argo.\n\n---\n\n# 42. Arquitectura objetivo final \u2014 vista ejecutiva\n\n```mermaid\nflowchart TB\n    People[Engineers / Operators / Employees]\n    Git[GitHub wetopi-platform]\n    Codex[Codex]\n    Hermes[Hermes]\n    ADK[ADK Fleet]\n\n    subgraph OpsPlanes[Operational Planes]\n      IaC[InfraOps\\nOpenTofu + Ansible]\n      GitOps[GitOps\\nArgo CD]\n      VaultOps[VaultOps\\nInfisical]\n      NetOps[NetOps\\nCilium / IPVS / NATGW]\n      ObsOps[ObservabilityOps\\nHubble + logs/metrics]\n      MemoryOps[MemoryOps]\n      WorkOps[WorkOps]\n      DocsOps[DocOps]\n    end\n\n    subgraph Infra[Private Cloud]\n      PVE[Proxmox]\n      Ceph[(Ceph)]\n      RKE[RKE2]\n      Edge[Edge]\n      Host[Hosting]\n    end\n\n    People --> Git\n    People --> VaultOps\n    People --> DocsOps\n    Codex --> Git\n    Hermes --> WorkOps\n    Hermes --> MemoryOps\n    Hermes --> VaultOps\n    ADK --> WorkOps\n    ADK --> MemoryOps\n    ADK --> VaultOps\n\n    Git --> IaC\n    Git --> GitOps\n    IaC --> PVE\n    PVE --> RKE\n    Ceph --> RKE\n    GitOps --> RKE\n    VaultOps --> RKE\n    NetOps --> RKE\n    ObsOps --> RKE\n    RKE --> Edge\n    RKE --> Host\n```\n\n---\n\n# 43. Final implementation statement\n\nLa nueva Wetopi no debe construirse como \u201cun cluster Kubernetes con algunos agentes\u201d.\n\nDebe construirse como una **plataforma operativa integrada** donde:\n\n```text\nGit           = desired state\nOpenTofu      = virtual infrastructure provisioning\nAnsible       = host bootstrap/configuration\nRKE2          = orchestration platform\nCilium        = cluster network foundation\nHubble        = network visibility\nCeph          = persistent storage\nArgo CD       = continuous declarative reconciliation\nInfisical     = operational Vault\nSOPS + age    = Vault/bootstrap recovery bridge\nIPVS + DSR    = WAN ingress foundation v1\nTraefik       = HTTP/TLS edge v1\nNATGW         = simple controlled egress v1\nMemory Plane  = institutional knowledge\nWork Ledger   = coordination\nAI Hub        = shared capabilities\nHermes/ADK    = operational agents\nDocs          = institutional documentation\n```\n\nEl orden de implementaci\u00f3n debe preservar siempre dos propiedades:\n\n1. **La nueva plataforma puede fallar durante su construcci\u00f3n sin afectar al Swarm productivo.**\n2. **La plataforma puede ser reconstruida desde Git, backups y material de recovery sin depender de servicios que solo existen dentro del cluster destruido.**\n\n\u00c9sta es la base t\u00e9cnica y organizativa que debe revisarse con Platform Engineering y Development antes de iniciar la construcci\u00f3n del cluster definitivo.",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (design/master-architecture-plan.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<h1 id=\"bkmrk-wetopi-platform-%E2%80%94-ma\">Wetopi Platform \u2014 Master Architecture, Operating Model &amp; Implementation Plan</h1>\n<h2 id=\"bkmrk-memoria-ejecutiva%2C-a\">Memoria ejecutiva, arquitectura t\u00e9cnica general y plan de construcci\u00f3n</h2>\n<p id=\"bkmrk-fecha%3A-28-de-agosto-\"><strong>Fecha:</strong> 28 de agosto de 2026<br>\n<strong>Estado:</strong> Documento maestro para revisi\u00f3n e implementaci\u00f3n<br>\n<strong>Organizaci\u00f3n GitHub:</strong> <code>wetopi-platform</code><br>\n<strong>Audiencia:</strong> Platform Engineering, SRE, DevOps, Development, Security, Network Engineering y agentes Codex/Hermes/ADK<br>\n<strong>Prop\u00f3sito:</strong> servir como documento principal para explicar, revisar, construir y operar la nueva plataforma Wetopi.</p>\n<hr id=\"bkmrk-\">\n<h1 id=\"bkmrk-0.-c%C3%B3mo-usar-este-do\">0. C\u00f3mo usar este documento</h1>\n<p id=\"bkmrk-este-documento-conso\">Este documento consolida y sustituye como referencia principal a las memorias parciales creadas durante el dise\u00f1o inicial:</p>\n<ul id=\"bkmrk-wetopi-ai-first-kube\">\n<li><code>wetopi-ai-first-kubernetes-project-memory.md</code></li>\n<li><code>wetopi-platform-bootstrap-plan.md</code></li>\n<li><code>wetopi-rke2-network-hosting-architecture.md</code></li>\n<li><code>wetopi-platform-decisions-argocd-infisical-rke2.md</code></li>\n</ul>\n<p id=\"bkmrk-cuando-exista-una-co\">Cuando exista una contradicci\u00f3n con documentos anteriores, <strong>prevalece este documento</strong>, especialmente en decisiones posteriores como:</p>\n<ul id=\"bkmrk-argo-cd-como-gitops.\">\n<li><strong>Argo CD</strong> como GitOps.</li>\n<li><strong>Infisical self-hosted</strong> como Wetopi Vault.</li>\n<li><strong>SOPS + age</strong> \u00fanicamente como bootstrap/recovery de secretos declarativos.</li>\n<li><strong>OpenBao queda fuera de v1</strong>.</li>\n</ul>\n<p id=\"bkmrk-no-todas-las-decisio\">No todas las decisiones est\u00e1n cerradas. Las cuestiones a\u00fan abiertas se marcan expl\u00edcitamente como <code>OPEN</code> y <strong>no deben ser inventadas por Codex ni por un ingeniero durante la implementaci\u00f3n</strong>.</p>\n<hr id=\"bkmrk--1\">\n<h1 id=\"bkmrk-1.-resumen-ejecutivo\">1. Resumen ejecutivo</h1>\n<p id=\"bkmrk-wetopi-va-a-reconstr\">Wetopi va a reconstruir su plataforma de Managed WordPress Hosting sobre una nueva foundation basada en <strong>RKE2</strong>, desplegada en su cloud privado sobre <strong>Proxmox + Ceph + OVH vRack</strong>.</p>\n<p id=\"bkmrk-la-migraci%C3%B3n-ser%C3%A1-gr\">La migraci\u00f3n ser\u00e1 <strong>greenfield y progresiva</strong>. Docker Swarm seguir\u00e1 dando servicio mientras se construye, prueba y estabiliza la nueva plataforma.</p>\n<p id=\"bkmrk-la-arquitectura-inic\">La arquitectura inicial queda resumida en:</p>\n<pre id=\"bkmrk-proxmox-%2B-ceph-%2B-ovh\"><code class=\"language-text\">Proxmox + Ceph + OVH vRack\n        \u2502\n        \u25bc\n       RKE2\n        \u2502\n  Cilium + Hubble\n        \u2502\n      Ceph CSI\n        \u2502\n      Argo CD\n        \u2502\n SOPS bootstrap\n        \u2502\n    Infisical\n        \u2502\n Docs / Memory / Work Coordination\n        \u2502\n Hermes + ADK + Codex workflows\n        \u2502\n Edge RKE2 + Traefik + Gateway API\n        \u2502\n Hosting workers + WordPress sites\n</code></pre>\n<p id=\"bkmrk-la-entrada-wan-conse\">La entrada WAN conservar\u00e1 inicialmente la arquitectura probada de Wetopi:</p>\n<pre id=\"bkmrk-internet-%E2%86%93-vrrp-vip-\"><code class=\"language-text\">Internet\n   \u2193\nVRRP VIP\n   \u2193\nIPVS\n   \u2193\nDSR\n   \u2193\nRKE2 Edge nodes\n   \u2193\nTraefik\n   \u2193\nGateway API\n   \u2193\nServices / EndpointSlices\n   \u2193\nWordPress workloads\n</code></pre>\n<p id=\"bkmrk-la-salida-ser%C3%A1-delib\">La salida ser\u00e1 deliberadamente sencilla:</p>\n<pre id=\"bkmrk-site%2Fcustomer%2Fworklo\"><code class=\"language-text\">site/customer/workload\n        \u2193\nselected internal gateway\n        \u2193\n192.168.88.100 / .101 / .102 / ...\n        \u2193\nUbuntu NATGW\n        \u2193\nselected WAN public IP\n        \u2193\nInternet\n</code></pre>\n<p id=\"bkmrk-la-plataforma-ser%C3%A1-a\">La plataforma ser\u00e1 <strong>AI-first</strong>, pero no se permitir\u00e1 que la autonom\u00eda de los agentes degrade la operabilidad o seguridad. Git, Vault, Work Ledger, Memory Plane, documentaci\u00f3n y tools compartidas ser\u00e1n servicios de primera clase tanto para humanos como para agentes.</p>\n<hr id=\"bkmrk--2\">\n<h1 id=\"bkmrk-2.-objetivos-de-nego\">2. Objetivos de negocio y plataforma</h1>\n<h2 id=\"bkmrk-2.1-objetivos-de-neg\">2.1 Objetivos de negocio</h2>\n<p id=\"bkmrk-la-nueva-plataforma-\">La nueva plataforma debe permitir a Wetopi:</p>\n<ul id=\"bkmrk-migrar-desde-docker-\">\n<li>migrar desde Docker Swarm sin big-bang;</li>\n<li>operar cientos y posteriormente miles de workloads WordPress;</li>\n<li>mantener la eficiencia y capacidad WAN actual;</li>\n<li>reducir configuraci\u00f3n artesanal y componentes legacy;</li>\n<li>ofrecer mejor aislamiento por cliente;</li>\n<li>permitir IPs de salida dedicadas o f\u00e1cilmente rotables;</li>\n<li>mejorar observabilidad y seguridad;</li>\n<li>incorporar automatizaci\u00f3n y agentes de forma nativa;</li>\n<li>mantener un modelo operativo entendible por ingenieros humanos.</li>\n</ul>\n<h2 id=\"bkmrk-2.2-objetivos-t%C3%A9cnic\">2.2 Objetivos t\u00e9cnicos</h2>\n<p id=\"bkmrk-la-plataforma-debe-p\">La plataforma debe proporcionar:</p>\n<ul id=\"bkmrk-kubernetes-ha%3B-iac-r\">\n<li>Kubernetes HA;</li>\n<li>IaC reproducible;</li>\n<li>GitOps declarativo;</li>\n<li>CNI moderno;</li>\n<li>observabilidad de flujos;</li>\n<li>CSI para Ceph;</li>\n<li>Vault operativo con API;</li>\n<li>secrets materializados en Kubernetes;</li>\n<li>documentaci\u00f3n Markdown versionada;</li>\n<li>memoria institucional com\u00fan;</li>\n<li>coordinaci\u00f3n de trabajo entre agentes y humanos;</li>\n<li>trazabilidad;</li>\n<li>separaci\u00f3n clara de security domains;</li>\n<li>recuperaci\u00f3n desde p\u00e9rdida parcial o total del cluster.</li>\n</ul>\n<hr id=\"bkmrk--3\">\n<h1 id=\"bkmrk-3.-principios-de-dis\">3. Principios de dise\u00f1o</h1>\n<h2 id=\"bkmrk-3.1-greenfield-antes\">3.1 Greenfield antes que migraci\u00f3n in-place</h2>\n<p id=\"bkmrk-la-nueva-plataforma--1\">La nueva plataforma se construye en paralelo al Swarm existente.</p>\n<pre id=\"bkmrk-producci%C3%93n-actual-nu\"><code class=\"language-text\">PRODUCCI\u00d3N ACTUAL                 NUEVA PLATAFORMA\nDocker Swarm                     RKE2 greenfield\n      \u2502                                \u2502\n      \u2502                                \u2502\n      \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 coexistencia \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n</code></pre>\n<p id=\"bkmrk-no-se-reutilizan-nod\">No se reutilizan nodos de producci\u00f3n hasta que RKE2 haya demostrado estabilidad.</p>\n<h2 id=\"bkmrk-3.2-git-es-source-of\">3.2 Git es source of truth del estado declarativo</h2>\n<pre id=\"bkmrk-git-%E2%86%93-review-%E2%86%93-argo-\"><code class=\"language-text\">Git\n \u2193\nreview\n \u2193\nArgo CD / IaC\n \u2193\nreal infrastructure\n</code></pre>\n<p id=\"bkmrk-la-shell-history-no-\">La shell history no es source of truth.</p>\n<h2 id=\"bkmrk-3.3-la-fuente-viva-g\">3.3 La fuente viva gana a la memoria</h2>\n<p id=\"bkmrk-para-infraestructura\">Para infraestructura real:</p>\n<pre id=\"bkmrk-api-autoritativa-%2F-s\"><code class=\"language-text\">API autoritativa / sistema vivo\n&gt;\nFleet Memory\n&gt;\nDocs hist\u00f3ricas\n&gt;\nLLM inference\n</code></pre>\n<h2 id=\"bkmrk-3.4-simplicidad-ante\">3.4 Simplicidad antes que sofisticaci\u00f3n</h2>\n<p id=\"bkmrk-no-desplegar-v1-con%3A\">No desplegar v1 con:</p>\n<ul id=\"bkmrk-istio%3B-bgp-wan-oblig\">\n<li>Istio;</li>\n<li>BGP WAN obligatorio;</li>\n<li>service mesh global;</li>\n<li>Cilium egress controller complejo;</li>\n<li>Suricata inline global;</li>\n<li>WAF distribuido avanzado;</li>\n<li>PKI din\u00e1mica compleja;</li>\n<li>automatizaci\u00f3n de reputaci\u00f3n completa.</li>\n</ul>\n<p id=\"bkmrk-la-arquitectura-debe\">La arquitectura debe permitir a\u00f1adirlo despu\u00e9s.</p>\n<h2 id=\"bkmrk-3.5-human-compatible\">3.5 Human-compatible, agent-native</h2>\n<p id=\"bkmrk-todos-los-servicios-\">Todos los servicios importantes deben disponer de:</p>\n<ul id=\"bkmrk-api%2Fcli%2Fgit-para-age\">\n<li>API/CLI/Git para agentes;</li>\n<li>UI cuando aporte valor a humanos;</li>\n<li>identidad y permisos;</li>\n<li>auditabilidad.</li>\n</ul>\n<h2 id=\"bkmrk-3.6-least-privilege\">3.6 Least privilege</h2>\n<p id=\"bkmrk-cada-workload%2C-tool-\">Cada workload, tool runtime y agente recibe \u00fanicamente los permisos necesarios.</p>\n<h2 id=\"bkmrk-3.7-secrets-usados%2C-\">3.7 Secrets usados, no necesariamente vistos</h2>\n<p id=\"bkmrk-cuando-sea-posible%3A\">Cuando sea posible:</p>\n<pre id=\"bkmrk-llm-%E2%86%93-tool-call-tool\"><code class=\"language-text\">LLM\n \u2193 tool call\nTool Runtime\n \u2193 secret\nExternal API\n</code></pre>\n<p id=\"bkmrk-el-modelo-no-necesit\">El modelo no necesita recibir el valor del secret.</p>\n<hr id=\"bkmrk--4\">\n<h1 id=\"bkmrk-4.-arquitectura-gene\">4. Arquitectura general de componentes</h1>\n<pre id=\"bkmrk-flowchart-tb-interne\"><code class=\"language-mermaid\">flowchart TB\n    Internet((Internet))\n    OVH[OVH / vRack]\n    PVE[Proxmox Cluster]\n    Ceph[(Ceph)]\n\n    subgraph RKE2[Wetopi RKE2 Production]\n      CP[3x RKE2 Control Plane\\nembedded etcd]\n      Platform[Platform Workers]\n      Edge[Edge Workers]\n      Hosting[Hosting Workers]\n      Agents[Agent Workers / Platform Workers]\n\n      Cilium[Cilium CNI]\n      Hubble[Hubble]\n      Argo[Argo CD]\n      Vault[Infisical Vault]\n      Docs[MkDocs / Docs]\n      Memory[Memory Plane]\n      Work[Work Coordination Plane]\n      Hermes[Hermes Fleet]\n      ADK[ADK Fleet]\n      Traefik[Traefik / Gateway API]\n      CSI[Ceph CSI]\n    end\n\n    Internet --&gt; OVH\n    OVH --&gt; PVE\n    PVE --&gt; RKE2\n    PVE --- Ceph\n    CSI --&gt; Ceph\n\n    CP --- Cilium\n    Platform --- Cilium\n    Edge --- Cilium\n    Hosting --- Cilium\n    Agents --- Cilium\n    Cilium --&gt; Hubble\n\n    Argo --&gt; Platform\n    Vault --&gt; Platform\n    Docs --&gt; Hermes\n    Memory --&gt; Hermes\n    Memory --&gt; ADK\n    Work --&gt; Hermes\n    Work --&gt; ADK\n    Hermes --&gt; Agents\n    ADK --&gt; Agents\n    Traefik --&gt; Hosting\n</code></pre>\n<hr id=\"bkmrk--5\">\n<h1 id=\"bkmrk-5.-operating-model%3A-\">5. Operating Model: todos los \u201cOps\u201d de Wetopi</h1>\n<p id=\"bkmrk-la-plataforma-se-div\">La plataforma se divide en dominios operativos. Cada dominio tiene un prop\u00f3sito, source of truth y responsabilidades.</p>\n<table id=\"bkmrk-%C3%81rea-responsabilidad\">\n<thead>\n<tr>\n<th>\u00c1rea</th>\n<th>Responsabilidad principal</th>\n<th>Source of truth principal</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>BootstrapOps</td>\n<td>crear/reparar la foundation</td>\n<td>Git + <code>wetopi-ops01</code></td>\n</tr>\n<tr>\n<td>InfraOps</td>\n<td>VMs, Proxmox, RKE2 nodes</td>\n<td>OpenTofu + Ansible</td>\n</tr>\n<tr>\n<td>GitOps</td>\n<td>estado declarativo Kubernetes</td>\n<td>Git + Argo CD</td>\n</tr>\n<tr>\n<td>NetOps</td>\n<td>CNI, routing, CIDR, policies</td>\n<td>Git + APIs de red</td>\n</tr>\n<tr>\n<td>EdgeOps</td>\n<td>IPVS/DSR, proxies WAN, Gateway API</td>\n<td>Git + IPVS state</td>\n</tr>\n<tr>\n<td>EgressOps</td>\n<td>NATGW, rutas, WAN IPs</td>\n<td>Git/config + gateway state</td>\n</tr>\n<tr>\n<td>StorageOps</td>\n<td>Ceph CSI/PVC/storage</td>\n<td>Ceph + Kubernetes</td>\n</tr>\n<tr>\n<td>VaultOps / SecretOps</td>\n<td>credentials y secrets</td>\n<td>Infisical</td>\n</tr>\n<tr>\n<td>SecOps</td>\n<td>RBAC, policies, security controls</td>\n<td>Git + audit logs</td>\n</tr>\n<tr>\n<td>ObservabilityOps</td>\n<td>m\u00e9tricas, logs, traces, flows</td>\n<td>observability stack</td>\n</tr>\n<tr>\n<td>DevOps</td>\n<td>CI/build/release/app lifecycle</td>\n<td>Git + CI + Argo CD</td>\n</tr>\n<tr>\n<td>AgentOps</td>\n<td>runtimes Hermes/ADK y capability bundles</td>\n<td><code>agents</code> + <code>ai-hub</code></td>\n</tr>\n<tr>\n<td>MemoryOps</td>\n<td>memoria institucional</td>\n<td>Memory Plane</td>\n</tr>\n<tr>\n<td>WorkOps</td>\n<td>coordinaci\u00f3n, tickets, leases</td>\n<td>Work Ledger</td>\n</tr>\n<tr>\n<td>DocOps</td>\n<td>docs/runbooks/ADR</td>\n<td>Markdown Git</td>\n</tr>\n<tr>\n<td>BackupOps</td>\n<td>backups y restore</td>\n<td>backup repositories</td>\n</tr>\n<tr>\n<td>RecoveryOps</td>\n<td>cluster/Vault/etcd disaster recovery</td>\n<td>runbooks + backups</td>\n</tr>\n<tr>\n<td>HostingOps</td>\n<td>WordPress/customer workloads</td>\n<td>Kubernetes + business control plane</td>\n</tr>\n</tbody>\n</table>\n<hr id=\"bkmrk--6\">\n<h1 id=\"bkmrk-6.-bootstrapops\">6. BootstrapOps</h1>\n<h2 id=\"bkmrk-6.1-prop%C3%B3sito\">6.1 Prop\u00f3sito</h2>\n<p id=\"bkmrk-bootstrapops-existe-\">BootstrapOps existe para que la plataforma pueda construirse y repararse <strong>sin depender de la propia plataforma</strong>.</p>\n<h2 id=\"bkmrk-6.2-componentes\">6.2 Componentes</h2>\n<pre id=\"bkmrk-flowchart-lr-laptop%5B\"><code class=\"language-mermaid\">flowchart LR\n    Laptop[Engineer Laptop\\nCodex]\n    GH[GitHub\\nwetopi-platform]\n    Ops[wetopi-ops01]\n    Tofu[OpenTofu]\n    Ansible[Ansible]\n    PVE[Proxmox API]\n    RKE[RKE2]\n\n    Laptop --&gt; GH\n    Laptop --&gt; Ops\n    GH --&gt; Ops\n    Ops --&gt; Tofu\n    Ops --&gt; Ansible\n    Tofu --&gt; PVE\n    Ansible --&gt; RKE\n</code></pre>\n<h2 id=\"bkmrk-6.3-wetopi-ops01\">6.3 <code>wetopi-ops01</code></h2>\n<p id=\"bkmrk-vm-externa-al-cluste\">VM externa al cluster Kubernetes.</p>\n<p id=\"bkmrk-responsabilidades%3A\">Responsabilidades:</p>\n<ul id=\"bkmrk-bootstrap%3B-bastion-o\">\n<li>bootstrap;</li>\n<li>bastion operativo;</li>\n<li>OpenTofu;</li>\n<li>Ansible;</li>\n<li>kubectl/helm;</li>\n<li>herramientas de recovery;</li>\n<li>acceso a backups;</li>\n<li>reparaci\u00f3n cuando GitOps/Cilium/Vault est\u00e9n ca\u00eddos.</li>\n</ul>\n<p id=\"bkmrk-no-ejecuta-workloads\">No ejecuta workloads productivos.</p>\n<p id=\"bkmrk-sizing-inicial-orien\">Sizing inicial orientativo:</p>\n<pre id=\"bkmrk-4-vcpu-8-gb-ram-40%E2%80%938\"><code class=\"language-text\">4 vCPU\n8 GB RAM\n40\u201380 GB disk\nUbuntu 24.04\n1 NIC LAN\n</code></pre>\n<hr id=\"bkmrk--7\">\n<h1 id=\"bkmrk-7.-infraops\">7. InfraOps</h1>\n<h2 id=\"bkmrk-7.1-rke2-inicial\">7.1 RKE2 inicial</h2>\n<pre id=\"bkmrk-3-x-control-plane-3-\"><code class=\"language-text\">3 x control-plane\n3 x platform worker\n1 x ops VM externa\n</code></pre>\n<p id=\"bkmrk-nombres%3A\">Nombres:</p>\n<pre id=\"bkmrk-wetopi-ops01-rke-cp0\"><code class=\"language-text\">wetopi-ops01\n\nrke-cp01\nrke-cp02\nrke-cp03\n\nrke-platform01\nrke-platform02\nrke-platform03\n</code></pre>\n<h2 id=\"bkmrk-7.2-distribuci%C3%B3n-f%C3%ADs\">7.2 Distribuci\u00f3n f\u00edsica</h2>\n<p id=\"bkmrk-ideal%3A\">Ideal:</p>\n<pre id=\"bkmrk-pve01--%3E-rke-cp01-%2B-\"><code class=\"language-text\">pve01 -&gt; rke-cp01 + rke-platform01\npve02 -&gt; rke-cp02 + rke-platform02\npve03 -&gt; rke-cp03 + rke-platform03\n</code></pre>\n<p id=\"bkmrk-debe-respetarse-fail\">Debe respetarse failure domain.</p>\n<h2 id=\"bkmrk-7.3-control-plane\">7.3 Control plane</h2>\n<p id=\"bkmrk-rke2-con%3A\">RKE2 con:</p>\n<ul id=\"bkmrk-embedded-etcd%3B-quoru\">\n<li>embedded etcd;</li>\n<li>quorum 2/3;</li>\n<li>API endpoint estable;</li>\n<li>snapshots;</li>\n<li>restore probado;</li>\n<li>sin customer workloads.</li>\n</ul>\n<p id=\"bkmrk-sizing-orientativo%3A\">Sizing orientativo:</p>\n<pre id=\"bkmrk-8-vcpu-16-gb-ram-50%E2%80%93\"><code class=\"language-text\">8 vCPU\n16 GB RAM\n50\u2013100 GB disk\n</code></pre>\n<p id=\"bkmrk-el-almacenamiento-de\">El almacenamiento de etcd debe priorizar baja latencia.</p>\n<h2 id=\"bkmrk-7.4-iac\">7.4 IaC</h2>\n<pre id=\"bkmrk-opentofu--%3E-proxmox-\"><code class=\"language-text\">OpenTofu -&gt; Proxmox resources / VMs\nAnsible  -&gt; OS / RKE2 / host configuration\nArgo CD  -&gt; Kubernetes workloads\n</code></pre>\n<p id=\"bkmrk-no-mezclar-responsab\">No mezclar responsabilidades.</p>\n<hr id=\"bkmrk--8\">\n<h1 id=\"bkmrk-8.-github-y-reposito\">8. GitHub y repositorios</h1>\n<p id=\"bkmrk-organizaci%C3%B3n%3A\">Organizaci\u00f3n:</p>\n<pre id=\"bkmrk-wetopi-platform\"><code class=\"language-text\">wetopi-platform\n</code></pre>\n<p id=\"bkmrk-repositorios-inicial\">Repositorios iniciales:</p>\n<pre id=\"bkmrk-wetopi-platform%2F-%E2%94%9C%E2%94%80%E2%94%80\"><code class=\"language-text\">wetopi-platform/\n\u251c\u2500\u2500 infrastructure\n\u251c\u2500\u2500 agent-services\n\u251c\u2500\u2500 ai-hub\n\u251c\u2500\u2500 agents\n\u2514\u2500\u2500 docs\n</code></pre>\n<h2 id=\"bkmrk-8.1-infrastructure\">8.1 <code>infrastructure</code></h2>\n<pre id=\"bkmrk-infrastructure%2F-%E2%94%9C%E2%94%80%E2%94%80-\"><code class=\"language-text\">infrastructure/\n\u251c\u2500\u2500 tofu/\n\u2502   \u2514\u2500\u2500 proxmox/\n\u251c\u2500\u2500 ansible/\n\u2502   \u251c\u2500\u2500 inventories/\n\u2502   \u251c\u2500\u2500 roles/\n\u2502   \u2514\u2500\u2500 playbooks/\n\u251c\u2500\u2500 clusters/\n\u2502   \u2514\u2500\u2500 production/\n\u251c\u2500\u2500 apps/\n\u251c\u2500\u2500 platform/\n\u251c\u2500\u2500 networking/\n\u251c\u2500\u2500 storage/\n\u251c\u2500\u2500 security/\n\u251c\u2500\u2500 observability/\n\u251c\u2500\u2500 bootstrap/\n\u2514\u2500\u2500 docs/\n</code></pre>\n<h2 id=\"bkmrk-8.2-agent-services\">8.2 <code>agent-services</code></h2>\n<p id=\"bkmrk-servicios-compartido\">Servicios compartidos:</p>\n<ul id=\"bkmrk-memory-plane%3B-work-l\">\n<li>Memory Plane;</li>\n<li>Work Ledger adapter;</li>\n<li>Hermes Memory Provider;</li>\n<li>identidad/approval cuando sea necesario.</li>\n</ul>\n<h2 id=\"bkmrk-8.3-ai-hub\">8.3 <code>ai-hub</code></h2>\n<pre id=\"bkmrk-skills%2F-tools%2F-compo\"><code class=\"language-text\">skills/\ntools/\ncomponents/\ncontracts/\nschemas/\nsdk/\nevals/\ntests/\n</code></pre>\n<p id=\"bkmrk-regla%3A\">Regla:</p>\n<blockquote id=\"bkmrk-una-tool-utilizada-p\">\n<p>Una tool utilizada por m\u00e1s de un agente deja de pertenecer al agente y pasa a <code>ai-hub</code>.</p>\n</blockquote>\n<h2 id=\"bkmrk-8.4-agents\">8.4 <code>agents</code></h2>\n<p id=\"bkmrk-monorepo-inicial-de%3A\">Monorepo inicial de:</p>\n<ul id=\"bkmrk-hermes%3B-adk%3B-configs\">\n<li>Hermes;</li>\n<li>ADK;</li>\n<li>configs;</li>\n<li>capability bundles;</li>\n<li>deployments;</li>\n<li>tests.</li>\n</ul>\n<h2 id=\"bkmrk-8.5-docs\">8.5 <code>docs</code></h2>\n<p id=\"bkmrk-markdown-%2B-mkdocs-ma\">Markdown + MkDocs Material.</p>\n<hr id=\"bkmrk--9\">\n<h1 id=\"bkmrk-9.-gitops-%E2%80%94-argo-cd\">9. GitOps \u2014 Argo CD</h1>\n<h2 id=\"bkmrk-9.1-decisi%C3%B3n\">9.1 Decisi\u00f3n</h2>\n<p id=\"bkmrk-argo-cd-ser%C3%A1-el-sist\">Argo CD ser\u00e1 el sistema GitOps de Wetopi.</p>\n<h2 id=\"bkmrk-9.2-modelo\">9.2 Modelo</h2>\n<pre id=\"bkmrk-flowchart-lr-dev%5Bhum\"><code class=\"language-mermaid\">flowchart LR\n    Dev[Human / Agent]\n    Git[GitHub]\n    Argo[Argo CD]\n    K8s[RKE2 API]\n    Apps[Applications]\n\n    Dev --&gt;|PR / merge| Git\n    Git --&gt; Argo\n    Argo --&gt; K8s\n    K8s --&gt; Apps\n</code></pre>\n<h2 id=\"bkmrk-9.3-principios\">9.3 Principios</h2>\n<ul id=\"bkmrk-ci-no-har%C3%A1-kubectl-a\">\n<li>CI no har\u00e1 <code>kubectl apply</code> en producci\u00f3n como mecanismo normal.</li>\n<li>Git describe estado deseado.</li>\n<li>Argo CD reconcilia.</li>\n<li>Drift manual debe ser visible.</li>\n<li>Aplicaciones deben agruparse por <code>AppProject</code>/dominio.</li>\n<li>Human UI se considera una ventaja operativa importante.</li>\n</ul>\n<h2 id=\"bkmrk-9.4-layout-gitops\">9.4 Layout GitOps</h2>\n<p id=\"bkmrk-open%3A-decidir-entre%3A\"><strong>OPEN:</strong> decidir entre:</p>\n<ul id=\"bkmrk-app-of-apps%3B-applica\">\n<li>App-of-Apps;</li>\n<li>ApplicationSet;</li>\n<li>combinaci\u00f3n por environments/domains.</li>\n</ul>\n<p id=\"bkmrk-no-bloquear-bootstra\">No bloquear bootstrap del cluster por esta decisi\u00f3n, pero cerrarla antes de escalar apps.</p>\n<hr id=\"bkmrk--10\">\n<h1 id=\"bkmrk-10.-secretops-%2F-vaul\">10. SecretOps / VaultOps</h1>\n<h2 id=\"bkmrk-10.1-decisi%C3%B3n\">10.1 Decisi\u00f3n</h2>\n<p id=\"bkmrk-el-vault-operativo-d\">El Vault operativo de Wetopi ser\u00e1:</p>\n<pre id=\"bkmrk-infisical-self-hoste\"><code class=\"language-text\">Infisical self-hosted\n</code></pre>\n<p id=\"bkmrk-openbao-no-forma-par\">OpenBao no forma parte de v1.</p>\n<h2 id=\"bkmrk-10.2-requisito-real\">10.2 Requisito real</h2>\n<p id=\"bkmrk-el-vault-debe-permit\">El Vault debe permitir que un agente o humano pueda:</p>\n<pre id=\"bkmrk-create-read-update-r\"><code class=\"language-text\">create\nread\nupdate\nrotate\ndelete\nlist\n</code></pre>\n<p id=\"bkmrk-credentials-como%3A\">credentials como:</p>\n<ul id=\"bkmrk-username%2Fpassword%3B-a\">\n<li>username/password;</li>\n<li>API token;</li>\n<li>SSH key;</li>\n<li>deploy token;</li>\n<li>client secret;</li>\n<li>customer credential.</li>\n</ul>\n<p id=\"bkmrk-ejemplo-conceptual%3A\">Ejemplo conceptual:</p>\n<pre id=\"bkmrk-%2Fplatform%2Fproxmox%2Fap\"><code class=\"language-text\">/platform/proxmox/api\n/customers/837/wp-admin\n/development/github/token\n/agents/hermes-sre/private\n</code></pre>\n<h2 id=\"bkmrk-10.3-source-of-truth\">10.3 Source of truth</h2>\n<pre id=\"bkmrk-infisical-%3D-source-o\"><code class=\"language-text\">Infisical = source of truth operativo\nKubernetes Secret = materializaci\u00f3n runtime\nSOPS = bootstrap/recovery declarativo\n</code></pre>\n<h2 id=\"bkmrk-10.4-arquitectura\">10.4 Arquitectura</h2>\n<pre id=\"bkmrk-flowchart-tb-humans%5B\"><code class=\"language-mermaid\">flowchart TB\n    Humans[Humans\\nWeb UI / CLI]\n    Agents[Hermes / ADK / Codex]\n    Tools[Tool runtimes]\n    Vault[Infisical]\n    Operator[Infisical Kubernetes Integration]\n    KSec[Kubernetes Secrets]\n    Pods[Applications / Pods]\n\n    Humans --&gt; Vault\n    Agents --&gt;|vault contract| Vault\n    Agents --&gt; Tools\n    Tools --&gt; Vault\n    Vault --&gt; Operator\n    Operator --&gt; KSec\n    KSec --&gt; Pods\n</code></pre>\n<h2 id=\"bkmrk-10.5-wetopi-vault-co\">10.5 Wetopi Vault Contract</h2>\n<p id=\"bkmrk-los-agentes-no-deben\">Los agentes no deben quedar fuertemente acoplados a la API concreta de Infisical.</p>\n<p id=\"bkmrk-ai-hub-debe-exponer%3A\"><code>ai-hub</code> debe exponer:</p>\n<pre id=\"bkmrk-vault.get%28%29-vault.pu\"><code class=\"language-text\">vault.get()\nvault.put()\nvault.update()\nvault.delete()\nvault.list()\nvault.exists()\n</code></pre>\n<h2 id=\"bkmrk-10.6-infisical-valid\">10.6 Infisical validation</h2>\n<p id=\"bkmrk-open-%2F-required-befo\"><strong>OPEN / REQUIRED BEFORE PRODUCTION:</strong> verificar:</p>\n<ul id=\"bkmrk-features-self-hosted\">\n<li>features self-hosted requeridas;</li>\n<li>modelo de licensing;</li>\n<li>machine identities;</li>\n<li>Kubernetes integration;</li>\n<li>audit;</li>\n<li>versioning;</li>\n<li>backup/restore;</li>\n<li>l\u00edmites que puedan afectar a una flota de agentes grande.</li>\n</ul>\n<hr id=\"bkmrk--11\">\n<h1 id=\"bkmrk-11.-sops-%2B-age\">11. SOPS + age</h1>\n<p id=\"bkmrk-sops-no-ser%C3%A1-el-vaul\">SOPS no ser\u00e1 el Vault.</p>\n<p id=\"bkmrk-su-responsabilidad-e\">Su responsabilidad es resolver el problema inevitable de bootstrap:</p>\n<blockquote id=\"bkmrk-%C2%BFd%C3%B3nde-guardamos-los\">\n<p>\u00bfD\u00f3nde guardamos los secretos necesarios para arrancar el sistema que guarda secretos?</p>\n</blockquote>\n<p id=\"bkmrk-modelo%3A\">Modelo:</p>\n<pre id=\"bkmrk-flowchart-lr-git%5Bgit\"><code class=\"language-mermaid\">flowchart LR\n    Git[Git]\n    SOPS[SOPS encrypted files]\n    Age[age keys]\n    Bootstrap[Bootstrap process]\n    Vault[Infisical]\n\n    Git --&gt; SOPS\n    Age --&gt; Bootstrap\n    SOPS --&gt; Bootstrap\n    Bootstrap --&gt; Vault\n</code></pre>\n<p id=\"bkmrk-se-utilizar%C3%A1n-al-men\">Se utilizar\u00e1n al menos:</p>\n<ul id=\"bkmrk-clave-age-de-bootstr\">\n<li>clave <code>age</code> de bootstrap/automation;</li>\n<li>clave <code>age</code> de recovery offline.</li>\n</ul>\n<p id=\"bkmrk-nunca-almacenar-clav\">Nunca almacenar claves privadas <code>age</code> en Git.</p>\n<hr id=\"bkmrk--12\">\n<h1 id=\"bkmrk-12.-kubernetes-secre\">12. Kubernetes Secrets</h1>\n<p id=\"bkmrk-una-vez-infisical-es\">Una vez Infisical est\u00e1 operativo:</p>\n<pre id=\"bkmrk-infisical-%E2%86%93-kubernet\"><code class=\"language-text\">Infisical\n \u2193\nKubernetes integration/operator\n \u2193\nKubernetes Secret\n \u2193\nPod/container\n</code></pre>\n<p id=\"bkmrk-rke2-debe-tener-secr\">RKE2 debe tener <strong>Secrets Encryption at Rest</strong> habilitado para proteger la copia persistida en etcd.</p>\n<hr id=\"bkmrk--13\">\n<h1 id=\"bkmrk-13.-agent-secret-mod\">13. Agent Secret Model</h1>\n<h2 id=\"bkmrk-13.1-regla-principal\">13.1 Regla principal</h2>\n<p id=\"bkmrk-un-agente-puede-nece\">Un agente puede necesitar permiso para <strong>usar</strong> una credencial sin tener permiso para <strong>leer su contenido</strong>.</p>\n<h2 id=\"bkmrk-13.2-tool-runtime-mo\">13.2 Tool runtime model</h2>\n<pre id=\"bkmrk-sequencediagram-part\"><code class=\"language-mermaid\">sequenceDiagram\n    participant LLM as Hermes / ADK\n    participant Tool as Platform Tool Runtime\n    participant Vault as Infisical\n    participant API as External API\n\n    LLM-&gt;&gt;Tool: proxmox.restart_vm(vm=123)\n    Tool-&gt;&gt;Vault: get /platform/proxmox/api\n    Vault--&gt;&gt;Tool: credential\n    Tool-&gt;&gt;API: authenticated request\n    API--&gt;&gt;Tool: result\n    Tool--&gt;&gt;LLM: sanitized result\n</code></pre>\n<h2 id=\"bkmrk-13.3-capability-doma\">13.3 Capability domains</h2>\n<p id=\"bkmrk-no-crear-un-sidecar-\">No crear un sidecar por tool.</p>\n<p id=\"bkmrk-agrupar-por-dominio%3A\">Agrupar por dominio:</p>\n<pre id=\"bkmrk-platform-tools-%E2%94%9C%E2%94%80%E2%94%80-k\"><code class=\"language-text\">platform-tools\n\u251c\u2500\u2500 kubernetes\n\u251c\u2500\u2500 proxmox\n\u251c\u2500\u2500 ceph\n\u2514\u2500\u2500 networking\n\nwordpress-tools\n\u251c\u2500\u2500 wordpress\n\u251c\u2500\u2500 database\n\u2514\u2500\u2500 backups\n\nmarketing-tools\n\u251c\u2500\u2500 intercom\n\u2514\u2500\u2500 analytics\n</code></pre>\n<h2 id=\"bkmrk-13.4-coding-agents\">13.4 Coding agents</h2>\n<p id=\"bkmrk-hermes%2Fcodex-de-desa\">Hermes/Codex de desarrollo o SRE pueden necesitar acceso m\u00e1s amplio.</p>\n<p id=\"bkmrk-debe-otorgarse-por-p\">Debe otorgarse por paths/roles, no con acceso global al Vault.</p>\n<hr id=\"bkmrk--14\">\n<h1 id=\"bkmrk-14.-netops\">14. NetOps</h1>\n<h2 id=\"bkmrk-14.1-nics\">14.1 NICs</h2>\n<p id=\"bkmrk-cada-vm-tendr%C3%A1-inici\">Cada VM tendr\u00e1 inicialmente una sola NIC LAN:</p>\n<pre id=\"bkmrk-vm-%E2%94%94%E2%94%80%E2%94%80-eth0-%E2%94%94%E2%94%80%E2%94%80-lan-\"><code class=\"language-text\">VM\n \u2514\u2500\u2500 eth0\n      \u2514\u2500\u2500 LAN / OVH vRack\n</code></pre>\n<h2 id=\"bkmrk-14.2-ceph-network\">14.2 Ceph network</h2>\n<p id=\"bkmrk-red-existente%3A\">Red existente:</p>\n<pre id=\"bkmrk-192.168.96.0%2F20\"><code class=\"language-text\">192.168.96.0/20\n</code></pre>\n<p id=\"bkmrk-estado%3A\">Estado:</p>\n<pre id=\"bkmrk-existing-do-not-modi\"><code class=\"language-text\">EXISTING\nDO NOT MODIFY\n</code></pre>\n<p id=\"bkmrk-est%C3%A1-hiperconvergida\">Est\u00e1 hiperconvergida con Proxmox/Ceph.</p>\n<p id=\"bkmrk-no-renumerar-ni-redi\">No renumerar ni redise\u00f1ar como parte de RKE2.</p>\n<h2 id=\"bkmrk-14.3-cidrs-propuesto\">14.3 CIDRs propuestos</h2>\n<pre id=\"bkmrk-node-lan%3A-open-%E2%80%94-pen\"><code class=\"language-text\">Node LAN:      OPEN \u2014 pendiente inventario\nPod CIDR:      10.64.0.0/14\nService CIDR:  10.68.0.0/16\nCeph:          192.168.96.0/20 existente\n</code></pre>\n<p id=\"bkmrk-los-rangos-pod%2Fservi\">Los rangos Pod/Service deben verificarse contra todo el direccionamiento existente antes de aplicarse.</p>\n<h2 id=\"bkmrk-14.4-cilium\">14.4 Cilium</h2>\n<p id=\"bkmrk-cilium-ser%C3%A1-cni.\">Cilium ser\u00e1 CNI.</p>\n<p id=\"bkmrk-baseline-inicial%3A\">Baseline inicial:</p>\n<pre id=\"bkmrk-cilium-vxlan-encapsu\"><code class=\"language-text\">Cilium\nVXLAN encapsulation\nHubble enabled\n</code></pre>\n<p id=\"bkmrk-no-exigir-native-rou\">No exigir native routing/BGP inicialmente.</p>\n<h2 id=\"bkmrk-14.5-hubble\">14.5 Hubble</h2>\n<p id=\"bkmrk-objetivos%3A\">Objetivos:</p>\n<ul id=\"bkmrk-flow-visibility%3B-pod\">\n<li>flow visibility;</li>\n<li>pod identity;</li>\n<li>namespace identity;</li>\n<li>source/destination;</li>\n<li>policy verdicts;</li>\n<li>futuras correlaciones con site/customer.</li>\n</ul>\n<hr id=\"bkmrk--15\">\n<h1 id=\"bkmrk-15.-storageops\">15. StorageOps</h1>\n<p id=\"bkmrk-ceph-seguir%C3%A1-siendo-\">Ceph seguir\u00e1 siendo storage principal.</p>\n<p id=\"bkmrk-rke2-consumir%C3%A1-ceph-\">RKE2 consumir\u00e1 Ceph mediante CSI.</p>\n<pre id=\"bkmrk-flowchart-lr-pod%5Bpod\"><code class=\"language-mermaid\">flowchart LR\n    Pod[Pod]\n    PVC[PVC]\n    CSI[Ceph CSI]\n    Node[RKE2 Node]\n    Ceph[(Ceph Cluster)]\n\n    Pod --&gt; PVC\n    PVC --&gt; CSI\n    CSI --&gt; Node\n    Node --&gt; Ceph\n</code></pre>\n<p id=\"bkmrk-tests-obligatorios-a\">Tests obligatorios antes de hosting:</p>\n<ul id=\"bkmrk-crear-pvc%3B-mount%3B-po\">\n<li>crear PVC;</li>\n<li>mount;</li>\n<li>pod delete/recreate;</li>\n<li>persistencia;</li>\n<li>failure b\u00e1sico;</li>\n<li>snapshot si se adopta;</li>\n<li>rendimiento aceptable.</li>\n</ul>\n<hr id=\"bkmrk--16\">\n<h1 id=\"bkmrk-16.-endpoint-ha-de-l\">16. Endpoint HA de la API RKE2</h1>\n<p id=\"bkmrk-rke2-necesita-un-end\">RKE2 necesita un endpoint estable:</p>\n<pre id=\"bkmrk-rke2-api.%3Cinternal-d\"><code class=\"language-text\">rke2-api.&lt;internal-domain&gt;\n          \u2193\n        VIP\n          \u2193\ncp01 / cp02 / cp03\n</code></pre>\n<p id=\"bkmrk-propuesta-actual%3A\">Propuesta actual:</p>\n<pre id=\"bkmrk-2-x-haproxy-%2B-keepal\"><code class=\"language-text\">2 x HAProxy + Keepalived\nprivate VIP\n</code></pre>\n<p id=\"bkmrk-estado%3A-1\">Estado:</p>\n<p id=\"bkmrk-open-%E2%80%94-debe-cerrarse\"><strong>OPEN \u2014 debe cerrarse antes de instalar el cluster definitivo.</strong></p>\n<p id=\"bkmrk-la-propuesta-se-cons\">La propuesta se considera conservadora y compatible con el conocimiento operativo existente, pero todav\u00eda no est\u00e1 aprobada como decisi\u00f3n final.</p>\n<hr id=\"bkmrk--17\">\n<h1 id=\"bkmrk-17.-edgeops-%E2%80%94-ingres\">17. EdgeOps \u2014 ingress WAN</h1>\n<h2 id=\"bkmrk-17.1-decisi%C3%B3n\">17.1 Decisi\u00f3n</h2>\n<p id=\"bkmrk-conservar-inicialmen\">Conservar inicialmente:</p>\n<pre id=\"bkmrk-vrrp-%2B-ipvs-%2B-dsr\"><code class=\"language-text\">VRRP\n+\nIPVS\n+\nDSR\n</code></pre>\n<h2 id=\"bkmrk-17.2-motivo\">17.2 Motivo</h2>\n<p id=\"bkmrk-los-wan-nodes-est%C3%A1n-\">Los WAN nodes est\u00e1n distribuidos sobre varios hosts Proxmox, y cada host f\u00edsico dispone de capacidad de salida OVH propia.</p>\n<p id=\"bkmrk-dsr-permite%3A\">DSR permite:</p>\n<pre id=\"bkmrk-request%3A-client--%3E-i\"><code class=\"language-text\">request:\nclient -&gt; IPVS -&gt; edge\n\nresponse:\nedge -&gt; client DIRECT\n</code></pre>\n<p id=\"bkmrk-por-tanto-se-preserv\">Por tanto se preserva la agregaci\u00f3n de bandwidth domains.</p>\n<h2 id=\"bkmrk-17.3-arquitectura\">17.3 Arquitectura</h2>\n<pre id=\"bkmrk-flowchart-tb-client%5B\"><code class=\"language-mermaid\">flowchart TB\n    Client[Internet Client]\n    VIP[Public VIP / VRRP]\n    LVS[IPVS Directors]\n    Edge1[RKE2 Edge 1\\nTraefik]\n    Edge2[RKE2 Edge 2\\nTraefik]\n    Edge3[RKE2 Edge 3\\nTraefik]\n    Gateway[Gateway API]\n    Service[Kubernetes Service]\n    Site[WordPress Pod]\n\n    Client --&gt; VIP\n    VIP --&gt; LVS\n    LVS --&gt;|DSR| Edge1\n    LVS --&gt;|DSR| Edge2\n    LVS --&gt;|DSR| Edge3\n    Edge1 --&gt; Gateway\n    Edge2 --&gt; Gateway\n    Edge3 --&gt; Gateway\n    Gateway --&gt; Service\n    Service --&gt; Site\n\n    Edge1 -. direct response .-&gt; Client\n    Edge2 -. direct response .-&gt; Client\n    Edge3 -. direct response .-&gt; Client\n</code></pre>\n<h2 id=\"bkmrk-17.4-dsr-poc\">17.4 DSR PoC</h2>\n<p id=\"bkmrk-required-before-prod\">Required before production:</p>\n<ul id=\"bkmrk-packet-capture-reque\">\n<li>packet capture request;</li>\n<li>packet capture response;</li>\n<li>verify response bypasses IPVS;</li>\n<li>verify source public IP;</li>\n<li>verify real client IP at proxy;</li>\n<li>verify health removal/reinsertion;</li>\n<li>verify node drain procedure.</li>\n</ul>\n<hr id=\"bkmrk--18\">\n<h1 id=\"bkmrk-18.-proxy-%E2%80%94-traefik\">18. Proxy \u2014 Traefik</h1>\n<p id=\"bkmrk-traefik-ser%C3%A1-el-prox\">Traefik ser\u00e1 el proxy inicial.</p>\n<p id=\"bkmrk-responsabilidades%3A-1\">Responsabilidades:</p>\n<ul id=\"bkmrk-tls-termination%3B-htt\">\n<li>TLS termination;</li>\n<li>HTTP routing;</li>\n<li>Gateway API;</li>\n<li>client IP forwarding;</li>\n<li>metrics;</li>\n<li>health;</li>\n<li>base futura para WAF integration.</li>\n</ul>\n<p id=\"bkmrk-estado-de-detalles%3A\">Estado de detalles:</p>\n<p id=\"bkmrk-open%3A-definir-deploy\"><strong>OPEN:</strong> definir deployment exacto:</p>\n<ul id=\"bkmrk-daemonset%3B-hostnetwo\">\n<li><code>DaemonSet</code>;</li>\n<li><code>hostNetwork</code> s\u00ed/no seg\u00fan PoC DSR;</li>\n<li>puertos;</li>\n<li>VIP handling;</li>\n<li>health endpoint;</li>\n<li>trusted proxies.</li>\n</ul>\n<hr id=\"bkmrk--19\">\n<h1 id=\"bkmrk-19.-gateway-api\">19. Gateway API</h1>\n<p id=\"bkmrk-gateway-api-ser%C3%A1-el-\">Gateway API ser\u00e1 el modelo declarativo L7.</p>\n<pre id=\"bkmrk-domain-%E2%86%93-httproute-%E2%86%93\"><code class=\"language-text\">Domain\n \u2193\nHTTPRoute\n \u2193\nService\n \u2193\nEndpointSlice\n \u2193\nPod\n</code></pre>\n<p id=\"bkmrk-esto-elimina-del-pat\">Esto elimina del path Kubernetes:</p>\n<ul id=\"bkmrk-registrator%3B-consul-\">\n<li>Registrator;</li>\n<li>Consul service discovery de websites;</li>\n<li>consul-template;</li>\n<li>regeneraci\u00f3n/reload de nginx config.</li>\n</ul>\n<hr id=\"bkmrk--20\">\n<h1 id=\"bkmrk-20.-egressops\">20. EgressOps</h1>\n<h2 id=\"bkmrk-20.1-filosof%C3%ADa\">20.1 Filosof\u00eda</h2>\n<p id=\"bkmrk-egress-v1-debe-ser-s\">Egress v1 debe ser simple.</p>\n<p id=\"bkmrk-no-se-construye-inic\">No se construye inicialmente un controlador sofisticado de reputaci\u00f3n/IPAM.</p>\n<h2 id=\"bkmrk-20.2-natgw\">20.2 NATGW</h2>\n<p id=\"bkmrk-ejemplo%3A\">Ejemplo:</p>\n<pre id=\"bkmrk-gw100-lan%3A-192.168.8\"><code class=\"language-text\">gw100\nLAN: 192.168.88.100\nWAN: PUBLIC_IP_A\n\ngw101\nLAN: 192.168.88.101\nWAN: PUBLIC_IP_B\n\ngw102\nLAN: 192.168.88.102\nWAN: PUBLIC_IP_C\n</code></pre>\n<p id=\"bkmrk-vm-ubuntu-con%3A\">VM Ubuntu con:</p>\n<ul id=\"bkmrk-routing%3B-nftables%2Fip\">\n<li>routing;</li>\n<li>nftables/iptables;</li>\n<li>SNAT;</li>\n<li>logging;</li>\n<li>health.</li>\n</ul>\n<h2 id=\"bkmrk-20.3-selecci%C3%B3n-l%C3%B3gic\">20.3 Selecci\u00f3n l\u00f3gica</h2>\n<pre id=\"bkmrk-site%2Fcustomer%2Fworklo-1\"><code class=\"language-text\">site/customer/workload\n   \u2193\negress profile / label\n   \u2193\ngw100 / gw101 / gw102\n</code></pre>\n<p id=\"bkmrk-ejemplo-l%C3%B3gico%3A\">Ejemplo l\u00f3gico:</p>\n<pre id=\"bkmrk-wetopi.io%2Fegress-gat\"><code class=\"language-yaml\">wetopi.io/egress-gateway: gw101\n</code></pre>\n<h2 id=\"bkmrk-20.4-enforcement\">20.4 Enforcement</h2>\n<p id=\"bkmrk-estado%3A-2\">Estado:</p>\n<p id=\"bkmrk-open.\"><strong>OPEN.</strong></p>\n<p id=\"bkmrk-baseline-propuesto%3A\">Baseline propuesto:</p>\n<ul id=\"bkmrk-policy-routing%3B-auto\">\n<li>policy routing;</li>\n<li>automatizaci\u00f3n ligera que observe Pods/labels;</li>\n<li>no depender de Pod IP est\u00e1tica;</li>\n<li>no construir todav\u00eda controller complejo.</li>\n</ul>\n<p id=\"bkmrk-evoluci%C3%B3n-posible%3A\">Evoluci\u00f3n posible:</p>\n<pre id=\"bkmrk-cilium-egress-gatewa\"><code class=\"language-text\">Cilium Egress Gateway\n</code></pre>\n<p id=\"bkmrk-sin-cambiar-el-contr\">sin cambiar el contrato l\u00f3gico.</p>\n<h2 id=\"bkmrk-20.5-pools-iniciales\">20.5 Pools iniciales</h2>\n<p id=\"bkmrk-separar-al-menos%3A\">Separar al menos:</p>\n<pre id=\"bkmrk-hosting-general-host\"><code class=\"language-text\">hosting-general\nhosting-premium\nplatform\nagents-crawlers\n</code></pre>\n<p id=\"bkmrk-los-crawlers-nunca-d\">Los crawlers nunca deben compartir egress WAN IP con hosting general.</p>\n<hr id=\"bkmrk--21\">\n<h1 id=\"bkmrk-21.-observabilityops\">21. ObservabilityOps</h1>\n<h2 id=\"bkmrk-21.1-fuentes\">21.1 Fuentes</h2>\n<p id=\"bkmrk-la-observabilidad-fu\">La observabilidad futura debe combinar:</p>\n<pre id=\"bkmrk-kubernetes-metrics-a\"><code class=\"language-text\">Kubernetes metrics\napplication logs\nHubble flows\nTraefik access logs\nNATGW logs\nKubernetes audit\nInfisical audit\nArgo CD events\nagent/tool audit\n</code></pre>\n<p id=\"bkmrk-posteriormente%3A\">Posteriormente:</p>\n<pre id=\"bkmrk-suricata-waf-events-\"><code class=\"language-text\">Suricata\nWAF events\nDNS telemetry\nreputation signals\n</code></pre>\n<h2 id=\"bkmrk-21.2-correlaci%C3%B3n-des\">21.2 Correlaci\u00f3n deseada</h2>\n<p id=\"bkmrk-una-observaci%C3%B3n-de-r\">Una observaci\u00f3n de red deber\u00eda poder acabar enriquecida con:</p>\n<pre id=\"bkmrk-customer_id-site_id-\"><code class=\"language-text\">customer_id\nsite_id\npod\nnamespace\nnode\nproxmox_host\negress_gateway\negress_public_ip\ndestination\nprotocol\nverdict\n</code></pre>\n<hr id=\"bkmrk--22\">\n<h1 id=\"bkmrk-22.-secops\">22. SecOps</h1>\n<p id=\"bkmrk-security-foundation-\">Security foundation v1:</p>\n<ul id=\"bkmrk-kubernetes-rbac%3B-ser\">\n<li>Kubernetes RBAC;</li>\n<li>ServiceAccounts;</li>\n<li>namespace isolation;</li>\n<li>NetworkPolicies;</li>\n<li>Infisical paths/identities;</li>\n<li>RKE2 Secrets Encryption at Rest;</li>\n<li>SOPS bootstrap keys offline;</li>\n<li>least privilege;</li>\n<li>audit trails.</li>\n</ul>\n<p id=\"bkmrk-no-asumir-que-cannot\">No asumir que <code>cannot get Secret</code> equivale a no poder acceder a secretos: un usuario con capacidad de crear Pods en un namespace puede potencialmente montar Secrets de ese namespace.</p>\n<p id=\"bkmrk-por-tanto-rbac-de-po\">Por tanto RBAC de Pods y Vault/Secrets debe dise\u00f1arse conjuntamente.</p>\n<hr id=\"bkmrk--23\">\n<h1 id=\"bkmrk-23.-namespaces\">23. Namespaces</h1>\n<p id=\"bkmrk-propuesta-inicial-or\">Propuesta inicial orientativa:</p>\n<pre id=\"bkmrk-wetopi-platform-weto\"><code class=\"language-text\">wetopi-platform\nwetopi-edge\nwetopi-hosting\nwetopi-agents-sre\nwetopi-agents-development\nwetopi-agents-marketing\nwetopi-observability\nwetopi-security\n</code></pre>\n<p id=\"bkmrk-estado%3A-3\">Estado:</p>\n<p id=\"bkmrk-open%3A-modelo-definit\"><strong>OPEN:</strong> modelo definitivo de namespaces y AppProjects/RBAC.</p>\n<hr id=\"bkmrk--24\">\n<h1 id=\"bkmrk-24.-devops\">24. DevOps</h1>\n<p id=\"bkmrk-devops-cubre-lifecyc\">DevOps cubre lifecycle de software, no infraestructura f\u00edsica.</p>\n<p id=\"bkmrk-pipeline-conceptual%3A\">Pipeline conceptual:</p>\n<pre id=\"bkmrk-flowchart-lr-code%5Bap\"><code class=\"language-mermaid\">flowchart LR\n    Code[Application Code]\n    CI[CI / Tests / Build]\n    Registry[Container Registry]\n    Config[GitOps Repo]\n    Argo[Argo CD]\n    RKE[RKE2]\n\n    Code --&gt; CI\n    CI --&gt; Registry\n    CI --&gt;|version bump / PR| Config\n    Config --&gt; Argo\n    Argo --&gt; RKE\n</code></pre>\n<p id=\"bkmrk-principios%3A\">Principios:</p>\n<ul id=\"bkmrk-build-artifacts-inmu\">\n<li>build artifacts inmutables;</li>\n<li>manifests/version changes en Git;</li>\n<li>Argo CD despliega;</li>\n<li>no despliegues manuales silenciosos.</li>\n</ul>\n<hr id=\"bkmrk--25\">\n<h1 id=\"bkmrk-25.-agentops\">25. AgentOps</h1>\n<h2 id=\"bkmrk-25.1-actores\">25.1 Actores</h2>\n<pre id=\"bkmrk-actor-%E2%94%9C%E2%94%80%E2%94%80-human-%E2%94%9C%E2%94%80%E2%94%80-\"><code class=\"language-text\">Actor\n\u251c\u2500\u2500 Human\n\u251c\u2500\u2500 Hermes\n\u251c\u2500\u2500 ADK Agent\n\u2514\u2500\u2500 future runtime\n</code></pre>\n<p id=\"bkmrk-campos-de-identidad%3A\">Campos de identidad:</p>\n<pre id=\"bkmrk-actor_id-actor_type-\"><code class=\"language-text\">actor_id\nactor_type\nruntime\ncrew\nrole\nuser_id\nenvironment\n</code></pre>\n<h2 id=\"bkmrk-25.2-hermes\">25.2 Hermes</h2>\n<p id=\"bkmrk-hermes-personales%3A\">Hermes personales:</p>\n<ul id=\"bkmrk-developer%2Fcoding%3B-sr\">\n<li>developer/coding;</li>\n<li>SRE/DevOps;</li>\n<li>marketing;</li>\n<li>otros empleados cuando proceda.</li>\n</ul>\n<p id=\"bkmrk-cada-hermes-tendr%C3%A1%3A\">Cada Hermes tendr\u00e1:</p>\n<pre id=\"bkmrk-local-personal-memor\"><code class=\"language-text\">local personal memory\n+\nFleet Memory\n+\nWork Ledger\n+\nrole-specific tools\n+\nVault permissions\n</code></pre>\n<h2 id=\"bkmrk-25.3-adk\">25.3 ADK</h2>\n<p id=\"bkmrk-adk-fleet%3A\">ADK fleet:</p>\n<ul id=\"bkmrk-observers%3B-specialis\">\n<li>observers;</li>\n<li>specialists;</li>\n<li>managers;</li>\n<li>domain agents.</li>\n</ul>\n<p id=\"bkmrk-todos-consumen-servi\">Todos consumen servicios compartidos.</p>\n<hr id=\"bkmrk--26\">\n<h1 id=\"bkmrk-26.-memoryops\">26. MemoryOps</h1>\n<p id=\"bkmrk-memory-plane-es-memo\">Memory Plane es memoria institucional, no source of truth de infraestructura viva.</p>\n<p id=\"bkmrk-clases%3A\">Clases:</p>\n<ul id=\"bkmrk-factual%3B-operational\">\n<li>factual;</li>\n<li>operational con TTL;</li>\n<li>procedural;</li>\n<li>decisions;</li>\n<li>shared episodic.</li>\n</ul>\n<p id=\"bkmrk-debe-soportar%3A\">Debe soportar:</p>\n<ul id=\"bkmrk-scopes%3B-provenance%3B-\">\n<li>scopes;</li>\n<li>provenance;</li>\n<li>confidence;</li>\n<li>temporalidad;</li>\n<li>contradictions;</li>\n<li>candidate facts.</li>\n</ul>\n<p id=\"bkmrk-hermes-combina%3A\">Hermes combina:</p>\n<pre id=\"bkmrk-local-memory-%2B-wetop\"><code class=\"language-text\">Local Memory\n+\nWetopi Fleet Memory\n</code></pre>\n<hr id=\"bkmrk--27\">\n<h1 id=\"bkmrk-27.-workops\">27. WorkOps</h1>\n<p id=\"bkmrk-work-coordination-pl\">Work Coordination Plane evita conflictos entre agentes/humanos.</p>\n<p id=\"bkmrk-no-es-solo-un-kanban\">No es solo un Kanban.</p>\n<p id=\"bkmrk-debe-manejar%3A\">Debe manejar:</p>\n<ul id=\"bkmrk-tasks%3B-ownership%3B-cl\">\n<li>tasks;</li>\n<li>ownership;</li>\n<li>claims;</li>\n<li>resource scopes;</li>\n<li>shared/exclusive locks;</li>\n<li>TTL leases;</li>\n<li>intents;</li>\n<li>activity journal;</li>\n<li>dependencies;</li>\n<li>conflict detection.</li>\n</ul>\n<p id=\"bkmrk-contrato-m%C3%ADnimo%3A\">Contrato m\u00ednimo:</p>\n<pre id=\"bkmrk-work.list%28%29-work.get\"><code class=\"language-text\">work.list()\nwork.get()\nwork.create()\nwork.claim()\nwork.release()\nwork.update()\nwork.comment()\nwork.complete()\nwork.find_conflicts()\nwork.acquire()\nwork.renew()\nwork.release_resources()\n</code></pre>\n<p id=\"bkmrk-backend-a%C3%BAn-desacopl\">Backend a\u00fan desacoplado.</p>\n<hr id=\"bkmrk--28\">\n<h1 id=\"bkmrk-28.-docops\">28. DocOps</h1>\n<p id=\"bkmrk-documentaci%C3%B3n%3A\">Documentaci\u00f3n:</p>\n<pre id=\"bkmrk-markdown-%2B-git-%2B-mkd\"><code class=\"language-text\">Markdown\n+\nGit\n+\nMkDocs Material\n</code></pre>\n<p id=\"bkmrk-roles%3A\">Roles:</p>\n<pre id=\"bkmrk-readme-del-repo-%3D-c%C3%B3\"><code class=\"language-text\">README del repo\n= c\u00f3mo funciona el componente\n\nwetopi-platform/docs\n= c\u00f3mo Wetopi lo dise\u00f1a, opera y gobierna\n</code></pre>\n<p id=\"bkmrk-flujo%3A\">Flujo:</p>\n<pre id=\"bkmrk-incident-%2F-project-%E2%86%93\"><code class=\"language-text\">incident / project\n \u2193\nWork Ledger\n \u2193\nvalidated knowledge\n \u2193\nMemory\n \u2193\nADR / runbook / architecture doc\n \u2193\nDocs repo\n</code></pre>\n<hr id=\"bkmrk--29\">\n<h1 id=\"bkmrk-29.-backupops-y-reco\">29. BackupOps y RecoveryOps</h1>\n<p id=\"bkmrk-debe-existir-recuper\">Debe existir recuperaci\u00f3n fuera del cluster.</p>\n<p id=\"bkmrk-objetos-a-proteger%3A\">Objetos a proteger:</p>\n<pre id=\"bkmrk-git-repos-etcd-snaps\"><code class=\"language-text\">Git repos\netcd snapshots\nInfisical database/state\nSOPS/age recovery material\nCeph configuration/data policies\nArgo configuration (also in Git)\ncritical application databases\n</code></pre>\n<p id=\"bkmrk-open%3A-destino-extern\"><strong>OPEN:</strong> destino externo definitivo de backups.</p>\n<p id=\"bkmrk-principio%3A\">Principio:</p>\n<blockquote id=\"bkmrk-un-backup-no-se-cons\">\n<p>un backup no se considera v\u00e1lido hasta haber probado un restore.</p>\n</blockquote>\n<hr id=\"bkmrk--30\">\n<h1 id=\"bkmrk-30.-hostingops\">30. HostingOps</h1>\n<h2 id=\"bkmrk-30.1-primera-migraci\">30.1 Primera migraci\u00f3n</h2>\n<p id=\"bkmrk-no-refactorizar-todo\">No refactorizar todo <code>wepod</code> a la vez.</p>\n<p id=\"bkmrk-inicialmente%3A\">Inicialmente:</p>\n<pre id=\"bkmrk-docker-wepod-%E2%86%93-pod-%2F\"><code class=\"language-text\">Docker wepod\n \u2193\nPod / StatefulSet\n</code></pre>\n<p id=\"bkmrk-manteniendo-cuando-c\">manteniendo cuando convenga:</p>\n<pre id=\"bkmrk-s6-nginx-php-fpm-mys\"><code class=\"language-text\">s6\nnginx\nphp-fpm\nmysql\n</code></pre>\n<p id=\"bkmrk-la-separaci%C3%B3n-de-db%2F\">La separaci\u00f3n de DB/frontend puede llegar despu\u00e9s.</p>\n<h2 id=\"bkmrk-30.2-hosting-workers\">30.2 Hosting workers</h2>\n<p id=\"bkmrk-se-crean-despu%C3%A9s-de-\">Se crean despu\u00e9s de estabilizar Platform y Edge.</p>\n<pre id=\"bkmrk-rke-host01-rke-host0\"><code class=\"language-text\">rke-host01\nrke-host02\nrke-host03\n...\n</code></pre>\n<p id=\"bkmrk-primera-cohorte%3A\">Primera cohorte:</p>\n<pre id=\"bkmrk-%7E10-sites\"><code class=\"language-text\">~10 sites\n</code></pre>\n<p id=\"bkmrk-despu%C3%A9s-escalar-prog\">Despu\u00e9s escalar progresivamente.</p>\n<h2 id=\"bkmrk-30.3-futuro-wordpres\">30.3 Futuro <code>WordPressSite</code> CRD</h2>\n<p id=\"bkmrk-posible-modelo-futur\">Posible modelo futuro:</p>\n<pre id=\"bkmrk-apiversion%3A-wetopi.i\"><code class=\"language-yaml\">apiVersion: wetopi.io/v1\nkind: WordPressSite\nmetadata:\n  name: site-12345\nspec:\n  domains:\n    - example.com\n  network:\n    egressGateway: gw100\n  storage:\n    size: 20Gi\n</code></pre>\n<p id=\"bkmrk-no-es-requisito-de-v\">No es requisito de v1.</p>\n<hr id=\"bkmrk--31\">\n<h1 id=\"bkmrk-31.-arquitectura-de-\">31. Arquitectura de data planes y control planes</h1>\n<pre id=\"bkmrk-flowchart-tb-subgrap\"><code class=\"language-mermaid\">flowchart TB\n    subgraph Control[CONTROL PLANES]\n      Git[GitHub]\n      Tofu[OpenTofu]\n      Ansible[Ansible]\n      Argo[Argo CD]\n      Vault[Infisical]\n      Work[Work Ledger]\n      Memory[Memory Plane]\n      Docs[Docs]\n    end\n\n    subgraph K8s[KUBERNETES CONTROL]\n      API[RKE2 API]\n      ETCD[(etcd)]\n      Cilium[Cilium control]\n    end\n\n    subgraph Data[DATA PLANES]\n      IPVS[IPVS / DSR]\n      Edge[Traefik Edge]\n      Pods[Hosting Pods]\n      NATGW[NAT Gateways]\n      Ceph[(Ceph)]\n    end\n\n    Git --&gt; Tofu\n    Git --&gt; Ansible\n    Git --&gt; Argo\n    Tofu --&gt; API\n    Ansible --&gt; API\n    Argo --&gt; API\n    API --&gt; ETCD\n    API --&gt; Cilium\n    Vault --&gt; Pods\n    IPVS --&gt; Edge\n    Edge --&gt; Pods\n    Pods --&gt; NATGW\n    Pods --&gt; Ceph\n</code></pre>\n<hr id=\"bkmrk--32\">\n<h1 id=\"bkmrk-32.-dependency-graph\">32. Dependency graph del bootstrap</h1>\n<pre id=\"bkmrk-flowchart-td-git%5Bgit\"><code class=\"language-mermaid\">flowchart TD\n    Git[GitHub org + repos]\n    Inventory[Proxmox / Network Inventory]\n    IaC[OpenTofu + Ansible]\n    Ops[wetopi-ops01]\n    VMs[3 CP + 3 Platform VMs]\n    RKE[RKE2 HA]\n    Cilium[Cilium]\n    Hubble[Hubble]\n    CSI[Ceph CSI]\n    Argo[Argo CD]\n    SOPS[SOPS + age bootstrap]\n    Vault[Infisical]\n    Docs[Docs]\n    Memory[Memory Plane]\n    Work[Work Ledger]\n    AI[AI Hub]\n    Agents[Hermes + ADK]\n    Edge[Edge workers + Traefik]\n    Egress[NATGW]\n    Hosting[Hosting workers]\n    Sites[First sites]\n\n    Git --&gt; Inventory\n    Inventory --&gt; IaC\n    IaC --&gt; Ops\n    Ops --&gt; VMs\n    VMs --&gt; RKE\n    RKE --&gt; Cilium\n    Cilium --&gt; Hubble\n    Cilium --&gt; CSI\n    CSI --&gt; Argo\n    Argo --&gt; SOPS\n    SOPS --&gt; Vault\n    Vault --&gt; Docs\n    Vault --&gt; Memory\n    Vault --&gt; Work\n    Docs --&gt; AI\n    Memory --&gt; Agents\n    Work --&gt; Agents\n    AI --&gt; Agents\n    Agents --&gt; Edge\n    Edge --&gt; Egress\n    Egress --&gt; Hosting\n    Hosting --&gt; Sites\n</code></pre>\n<hr id=\"bkmrk--33\">\n<h1 id=\"bkmrk-33.-plan-de-implemen\">33. Plan de implementaci\u00f3n por fases</h1>\n<h2 id=\"bkmrk-fase-0-%E2%80%94-gobierno-y-\">Fase 0 \u2014 Gobierno y repos</h2>\n<p id=\"bkmrk-deliverables%3A\">Deliverables:</p>\n<ul id=\"bkmrk-organizaci%C3%B3n-wetopi-\">\n<li>organizaci\u00f3n <code>wetopi-platform</code>;</li>\n<li>repos iniciales;</li>\n<li>README;</li>\n<li>CODEOWNERS;</li>\n<li>branch protection;</li>\n<li>convenciones;</li>\n<li>permisos de Codex.</li>\n</ul>\n<h2 id=\"bkmrk-fase-1-%E2%80%94-discovery-%2F\">Fase 1 \u2014 Discovery / inventario</h2>\n<p id=\"bkmrk-inventariar%3A\">Inventariar:</p>\n<ul id=\"bkmrk-hosts-proxmox%3B-cpu%2Fr\">\n<li>hosts Proxmox;</li>\n<li>CPU/RAM;</li>\n<li>bridges;</li>\n<li>VLANs;</li>\n<li>vRack;</li>\n<li>direccionamiento;</li>\n<li>storage;</li>\n<li>Ceph;</li>\n<li>IPVS;</li>\n<li>VIPs;</li>\n<li>NATGW;</li>\n<li>failure domains;</li>\n<li>bandwidth domains.</li>\n</ul>\n<p id=\"bkmrk-no-crear-ranges-nuev\">No crear ranges nuevos hasta completar este inventario.</p>\n<h2 id=\"bkmrk-fase-2-%E2%80%94-iac-foundat\">Fase 2 \u2014 IaC foundation</h2>\n<ul id=\"bkmrk-ubuntu-24.04-templat\">\n<li>Ubuntu 24.04 template/cloud-init;</li>\n<li>OpenTofu Proxmox modules;</li>\n<li>Ansible baseline;</li>\n<li>automation account Proxmox.</li>\n</ul>\n<h2 id=\"bkmrk-fase-3-%E2%80%94-bootstrap-n\">Fase 3 \u2014 Bootstrap node</h2>\n<p id=\"bkmrk-crear%2Fprovisionar-we\">Crear/provisionar <code>wetopi-ops01</code>.</p>\n<h2 id=\"bkmrk-fase-4-%E2%80%94-rke2-contro\">Fase 4 \u2014 RKE2 control plane</h2>\n<p id=\"bkmrk-crear%3A\">Crear:</p>\n<pre id=\"bkmrk-rke-cp01-rke-cp02-rk\"><code class=\"language-text\">rke-cp01\nrke-cp02\nrke-cp03\n</code></pre>\n<p id=\"bkmrk-cerrar-previamente-e\">Cerrar previamente endpoint HA.</p>\n<h2 id=\"bkmrk-fase-5-%E2%80%94-platform-wo\">Fase 5 \u2014 Platform workers</h2>\n<p id=\"bkmrk-crear%3A-1\">Crear:</p>\n<pre id=\"bkmrk-rke-platform01-rke-p\"><code class=\"language-text\">rke-platform01\nrke-platform02\nrke-platform03\n</code></pre>\n<h2 id=\"bkmrk-fase-6-%E2%80%94-rke2-accept\">Fase 6 \u2014 RKE2 acceptance</h2>\n<ul id=\"bkmrk-quorum%3B-fail-one-cp%3B\">\n<li>quorum;</li>\n<li>fail one CP;</li>\n<li>snapshot etcd;</li>\n<li>restore test;</li>\n<li>node drain;</li>\n<li>reboot tests.</li>\n</ul>\n<h2 id=\"bkmrk-fase-7-%E2%80%94-cilium-%2F-hu\">Fase 7 \u2014 Cilium / Hubble</h2>\n<ul id=\"bkmrk-vxlan%3B-pod-cidr%3B-ser\">\n<li>VXLAN;</li>\n<li>Pod CIDR;</li>\n<li>Service CIDR;</li>\n<li>Hubble;</li>\n<li>DNS;</li>\n<li>NetworkPolicy baseline.</li>\n</ul>\n<h2 id=\"bkmrk-fase-8-%E2%80%94-ceph-csi\">Fase 8 \u2014 Ceph CSI</h2>\n<ul id=\"bkmrk-connectivity%3B-csi%3B-p\">\n<li>connectivity;</li>\n<li>CSI;</li>\n<li>PVC acceptance.</li>\n</ul>\n<h2 id=\"bkmrk-fase-9-%E2%80%94-argo-cd\">Fase 9 \u2014 Argo CD</h2>\n<ul id=\"bkmrk-install%3B-projects%3B-f\">\n<li>install;</li>\n<li>projects;</li>\n<li>first Git reconciled app;</li>\n<li>drift test;</li>\n<li>rollback test.</li>\n</ul>\n<h2 id=\"bkmrk-fase-10-%E2%80%94-sops-boots\">Fase 10 \u2014 SOPS bootstrap</h2>\n<ul id=\"bkmrk-age-keys%3B-recovery-k\">\n<li>age keys;</li>\n<li>recovery key offline;</li>\n<li>encrypted bootstrap values;</li>\n<li>recovery procedure.</li>\n</ul>\n<h2 id=\"bkmrk-fase-11-%E2%80%94-infisical-\">Fase 11 \u2014 Infisical Vault</h2>\n<ul id=\"bkmrk-self-hosted-deploy%3B-\">\n<li>self-hosted deploy;</li>\n<li>persistence;</li>\n<li>backup;</li>\n<li>identities;</li>\n<li>Kubernetes integration;</li>\n<li>Wetopi <code>vault.*</code> contract.</li>\n</ul>\n<h2 id=\"bkmrk-fase-12-%E2%80%94-ai-first-s\">Fase 12 \u2014 AI-first services</h2>\n<ul id=\"bkmrk-docs%3B-memory-plane%3B-\">\n<li>Docs;</li>\n<li>Memory Plane;</li>\n<li>Work Coordination Plane;</li>\n<li>AI Hub.</li>\n</ul>\n<h2 id=\"bkmrk-fase-13-%E2%80%94-hermes-%2F-a\">Fase 13 \u2014 Hermes / ADK</h2>\n<p id=\"bkmrk-deploy-definitive-ag\">Deploy definitive agents after shared services are healthy.</p>\n<h2 id=\"bkmrk-fase-14-%E2%80%94-edge-poc\">Fase 14 \u2014 Edge PoC</h2>\n<ul id=\"bkmrk-edge-rke2-nodes%3B-tra\">\n<li>edge RKE2 nodes;</li>\n<li>Traefik;</li>\n<li>Gateway API;</li>\n<li>DSR PoC;</li>\n<li>health integration with IPVS.</li>\n</ul>\n<h2 id=\"bkmrk-fase-15-%E2%80%94-egress-poc\">Fase 15 \u2014 Egress PoC</h2>\n<ul id=\"bkmrk-gw100%2Fgw101%2Fgw102%3B-p\">\n<li>gw100/gw101/gw102;</li>\n<li>policy routing selection;</li>\n<li>WAN IP rotation;</li>\n<li>crawler isolation.</li>\n</ul>\n<h2 id=\"bkmrk-fase-16-%E2%80%94-hosting-pi\">Fase 16 \u2014 Hosting pilot</h2>\n<ul id=\"bkmrk-3-initial-hosting-wo\">\n<li>3 initial hosting workers;</li>\n<li>~10 sites;</li>\n<li>storage/network/performance validation.</li>\n</ul>\n<h2 id=\"bkmrk-fase-17-%E2%80%94-progressiv\">Fase 17 \u2014 Progressive migration</h2>\n<p id=\"bkmrk-increase-cohorts-whi\">Increase cohorts while shrinking Swarm.</p>\n<hr id=\"bkmrk--34\">\n<h1 id=\"bkmrk-34.-migration-strate\">34. Migration strategy</h1>\n<pre id=\"bkmrk-flowchart-lr-swarm%5Bd\"><code class=\"language-mermaid\">flowchart LR\n    Swarm[Docker Swarm\\nCurrent Production]\n    New[RKE2 Platform]\n    Pilot[10 Sites]\n    Batch1[50 Sites]\n    Batch2[100+ Sites]\n    Final[Swarm Retirement]\n\n    Swarm --&gt; New\n    New --&gt; Pilot\n    Pilot --&gt; Batch1\n    Batch1 --&gt; Batch2\n    Batch2 --&gt; Final\n</code></pre>\n<p id=\"bkmrk-principio%3A-1\">Principio:</p>\n<pre id=\"bkmrk-rke2-failure-during-\"><code class=\"language-text\">RKE2 failure during early phases\n!=\ncustomer outage\n</code></pre>\n<hr id=\"bkmrk--35\">\n<h1 id=\"bkmrk-35.-criterios-de-ace\">35. Criterios de aceptaci\u00f3n</h1>\n<h2 id=\"bkmrk-35.1-infra-%2F-rke2\">35.1 Infra / RKE2</h2>\n<ul id=\"bkmrk-p%C3%A9rdida-de-un-cp-no-\">\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> p\u00e9rdida de un CP no rompe API;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> etcd quorum correcto;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> snapshot funciona;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> restore probado;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> nodes reproducibles desde IaC;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> no customer workloads en CP.</li>\n</ul>\n<h2 id=\"bkmrk-35.2-network\">35.2 Network</h2>\n<ul id=\"bkmrk-pod-to-pod%3B-pod-to-s\">\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Pod-to-Pod;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Pod-to-Service;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> DNS;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Hubble visibility;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> no CIDR collision;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Ceph reachable desde nodes;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> DSR demostrado por packet capture.</li>\n</ul>\n<h2 id=\"bkmrk-35.3-storage\">35.3 Storage</h2>\n<ul id=\"bkmrk-pvc-create%3B-persiste\">\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> PVC create;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> persistence;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> pod recreation;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> failure test;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> rendimiento aceptable.</li>\n</ul>\n<h2 id=\"bkmrk-35.4-gitops\">35.4 GitOps</h2>\n<ul id=\"bkmrk-argo-reconcilia-desd\">\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Argo reconcilia desde Git;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> drift detectable;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> rollback probado;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Git conserva estado deseado.</li>\n</ul>\n<h2 id=\"bkmrk-35.5-vault\">35.5 Vault</h2>\n<ul id=\"bkmrk-agente-puede-crear-c\">\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> agente puede crear credential;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> humano puede consultar credential autorizado;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> workload puede recibir secret;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> secret no aparece plaintext en Git;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> backup/restore Infisical probado;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> path permissions validados;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> raw secrets no llegan al LLM cuando se usa tool runtime.</li>\n</ul>\n<h2 id=\"bkmrk-35.6-egress\">35.6 Egress</h2>\n<ul id=\"bkmrk-workload-a-sale-por-\">\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> workload A sale por gw100;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> workload B sale por gw101;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> cambio de gateway no requiere mover workload;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> cambio de WAN IP verificable;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> crawlers aislados de hosting general.</li>\n</ul>\n<h2 id=\"bkmrk-35.7-edge\">35.7 Edge</h2>\n<ul id=\"bkmrk-ipvs-health-funciona\">\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> IPVS health funciona;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> DSR funciona;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> client IP preservada;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> TLS correcto;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> HTTPRoute din\u00e1mico;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> drain seguro.</li>\n</ul>\n<h2 id=\"bkmrk-35.8-agents\">35.8 Agents</h2>\n<ul id=\"bkmrk-hermes-usa-vault-con\">\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Hermes usa Vault contract;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> ADK usa scoped tools;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> tool runtime consume credentials;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Work Ledger evita conflictos;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> Fleet Memory consultable;</li>\n<li class=\"task-list-item\"><input disabled type=\"checkbox\"> docs accesibles por Git.</li>\n</ul>\n<hr id=\"bkmrk--36\">\n<h1 id=\"bkmrk-36.-decisiones-cerra\">36. Decisiones cerradas</h1>\n<table id=\"bkmrk-id-decisi%C3%B3n-arc-001-\">\n<thead>\n<tr>\n<th>ID</th>\n<th>Decisi\u00f3n</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>ARC-001</td>\n<td>RKE2</td>\n</tr>\n<tr>\n<td>ARC-002</td>\n<td>3 CP + embedded etcd</td>\n</tr>\n<tr>\n<td>ARC-003</td>\n<td>greenfield paralelo a Swarm</td>\n</tr>\n<tr>\n<td>ARC-004</td>\n<td><code>wetopi-ops01</code> externo</td>\n</tr>\n<tr>\n<td>ARC-005</td>\n<td>OpenTofu + Ansible</td>\n</tr>\n<tr>\n<td>ARC-006</td>\n<td>Cilium</td>\n</tr>\n<tr>\n<td>ARC-007</td>\n<td>Hubble</td>\n</tr>\n<tr>\n<td>ARC-008</td>\n<td>Ceph existente + CSI</td>\n</tr>\n<tr>\n<td>ARC-009</td>\n<td>Argo CD</td>\n</tr>\n<tr>\n<td>ARC-010</td>\n<td>Infisical self-hosted Vault</td>\n</tr>\n<tr>\n<td>ARC-011</td>\n<td>SOPS + age solo bootstrap/recovery</td>\n</tr>\n<tr>\n<td>ARC-012</td>\n<td>IPVS + VRRP + DSR inicial</td>\n</tr>\n<tr>\n<td>ARC-013</td>\n<td>Traefik inicial</td>\n</tr>\n<tr>\n<td>ARC-014</td>\n<td>Gateway API</td>\n</tr>\n<tr>\n<td>ARC-015</td>\n<td>NATGW Ubuntu simples</td>\n</tr>\n<tr>\n<td>ARC-016</td>\n<td>no Istio inicialmente</td>\n</tr>\n<tr>\n<td>ARC-017</td>\n<td>no BGP obligatorio inicialmente</td>\n</tr>\n<tr>\n<td>ARC-018</td>\n<td>agents/crawlers separados de customer egress</td>\n</tr>\n<tr>\n<td>ARC-019</td>\n<td>Markdown + MkDocs</td>\n</tr>\n<tr>\n<td>ARC-020</td>\n<td>AI Hub compartido</td>\n</tr>\n</tbody>\n</table>\n<hr id=\"bkmrk--37\">\n<h1 id=\"bkmrk-37.-decisiones-abier\">37. Decisiones abiertas / review board</h1>\n<p id=\"bkmrk-estas-decisiones-deb\">Estas decisiones deben ser revisadas por Platform Engineering antes de implementaci\u00f3n o antes de su fase correspondiente.</p>\n<table id=\"bkmrk-tema-estado-cu%C3%A1ndo-d\">\n<thead>\n<tr>\n<th>Tema</th>\n<th>Estado</th>\n<th>Cu\u00e1ndo debe cerrarse</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Node LAN CIDR</td>\n<td>OPEN</td>\n<td>antes de crear VMs RKE2</td>\n</tr>\n<tr>\n<td>Pod CIDR <code>10.64.0.0/14</code></td>\n<td>Proposed</td>\n<td>antes de RKE2 install</td>\n</tr>\n<tr>\n<td>Service CIDR <code>10.68.0.0/16</code></td>\n<td>Proposed</td>\n<td>antes de RKE2 install</td>\n</tr>\n<tr>\n<td>API HA: HAProxy+Keepalived</td>\n<td>Proposed</td>\n<td>antes de CP bootstrap</td>\n</tr>\n<tr>\n<td>Traefik hostNetwork/DSR details</td>\n<td>OPEN</td>\n<td>antes de Edge PoC</td>\n</tr>\n<tr>\n<td>Egress enforcement implementation</td>\n<td>OPEN</td>\n<td>antes de Egress PoC</td>\n</tr>\n<tr>\n<td>Infisical licensing/feature validation</td>\n<td>OPEN</td>\n<td>antes de prod Vault</td>\n</tr>\n<tr>\n<td>Argo app layout</td>\n<td>OPEN</td>\n<td>antes de app scale-out</td>\n</tr>\n<tr>\n<td>OIDC provider</td>\n<td>OPEN</td>\n<td>antes de broad human access</td>\n</tr>\n<tr>\n<td>Namespace model final</td>\n<td>OPEN</td>\n<td>antes de production RBAC</td>\n</tr>\n<tr>\n<td>External backup destination</td>\n<td>OPEN</td>\n<td>antes de production data</td>\n</tr>\n<tr>\n<td>Central observability stack</td>\n<td>OPEN</td>\n<td>antes de migration scale</td>\n</tr>\n<tr>\n<td>Work Ledger backend</td>\n<td>OPEN</td>\n<td>antes de multi-agent operations</td>\n</tr>\n</tbody>\n</table>\n<hr id=\"bkmrk--38\">\n<h1 id=\"bkmrk-38.-riesgos-y-mitiga\">38. Riesgos y mitigaciones</h1>\n<h2 id=\"bkmrk-38.1-overengineering\">38.1 Overengineering</h2>\n<p id=\"bkmrk-riesgo%3A-desplegar-de\"><strong>Riesgo:</strong> desplegar demasiado antes de tener workloads.</p>\n<p id=\"bkmrk-mitigaci%C3%B3n%3A-v1-simpl\"><strong>Mitigaci\u00f3n:</strong> v1 simple, roadmap posterior.</p>\n<h2 id=\"bkmrk-38.2-p%C3%A9rdida-de-dsr\">38.2 P\u00e9rdida de DSR</h2>\n<p id=\"bkmrk-riesgo%3A-degradar-cap\"><strong>Riesgo:</strong> degradar capacidad WAN agregada.</p>\n<p id=\"bkmrk-mitigaci%C3%B3n%3A-poc-%2B-pa\"><strong>Mitigaci\u00f3n:</strong> PoC + packet capture + no retirar IPVS hasta equivalencia demostrada.</p>\n<h2 id=\"bkmrk-38.3-cidr-collision\">38.3 CIDR collision</h2>\n<p id=\"bkmrk-riesgo%3A-colisi%C3%B3n-con\"><strong>Riesgo:</strong> colisi\u00f3n con redes legacy/OVH/Ceph.</p>\n<p id=\"bkmrk-mitigaci%C3%B3n%3A-inventor\"><strong>Mitigaci\u00f3n:</strong> inventory first.</p>\n<h2 id=\"bkmrk-38.4-secrets-expuest\">38.4 Secrets expuestos a LLM</h2>\n<p id=\"bkmrk-riesgo%3A-tokens-en-pr\"><strong>Riesgo:</strong> tokens en prompts/logs.</p>\n<p id=\"bkmrk-mitigaci%C3%B3n%3A-tool-run\"><strong>Mitigaci\u00f3n:</strong> tool runtimes + Infisical + least privilege.</p>\n<h2 id=\"bkmrk-38.5-vault-lock-in%2Fl\">38.5 Vault lock-in/licensing</h2>\n<p id=\"bkmrk-riesgo%3A-features-cr%C3%AD\"><strong>Riesgo:</strong> features cr\u00edticas fuera de self-hosted assumptions.</p>\n<p id=\"bkmrk-mitigaci%C3%B3n%3A-validar-\"><strong>Mitigaci\u00f3n:</strong> validar Infisical y usar Wetopi <code>vault.*</code> contract.</p>\n<h2 id=\"bkmrk-38.6-gitops-blast-ra\">38.6 GitOps blast radius</h2>\n<p id=\"bkmrk-riesgo%3A-un-merge-des\"><strong>Riesgo:</strong> un merge despliega cambio err\u00f3neo global.</p>\n<p id=\"bkmrk-mitigaci%C3%B3n%3A-appproje\"><strong>Mitigaci\u00f3n:</strong> AppProjects, environments, PR review, health gates.</p>\n<h2 id=\"bkmrk-38.7-ceph-dependency\">38.7 Ceph dependency</h2>\n<p id=\"bkmrk-riesgo%3A-kubernetes-s\"><strong>Riesgo:</strong> Kubernetes storage depende del cluster Ceph hiperconvergido.</p>\n<p id=\"bkmrk-mitigaci%C3%B3n%3A-no-modif\"><strong>Mitigaci\u00f3n:</strong> no modificar Ceph en bootstrap, test de CSI y recovery independientes.</p>\n<h2 id=\"bkmrk-38.8-agent-conflicts\">38.8 Agent conflicts</h2>\n<p id=\"bkmrk-riesgo%3A-dos-agentes-\"><strong>Riesgo:</strong> dos agentes modifican recurso simult\u00e1neamente.</p>\n<p id=\"bkmrk-mitigaci%C3%B3n%3A-work-led\"><strong>Mitigaci\u00f3n:</strong> Work Ledger + leases + resource scopes.</p>\n<hr id=\"bkmrk--39\">\n<h1 id=\"bkmrk-39.-responsabilidade\">39. Responsabilidades por equipo</h1>\n<h2 id=\"bkmrk-platform-engineering\">Platform Engineering</h2>\n<ul id=\"bkmrk-rke2%3B-iac%3B-argo-cd%3B-\">\n<li>RKE2;</li>\n<li>IaC;</li>\n<li>Argo CD;</li>\n<li>Cilium;</li>\n<li>CSI;</li>\n<li>node lifecycle;</li>\n<li>Vault platform;</li>\n<li>cluster reliability.</li>\n</ul>\n<h2 id=\"bkmrk-network-%2F-sre\">Network / SRE</h2>\n<ul id=\"bkmrk-cidrs%3B-vrack%3B-ipvs%3B-\">\n<li>CIDRs;</li>\n<li>vRack;</li>\n<li>IPVS;</li>\n<li>VRRP;</li>\n<li>DSR;</li>\n<li>NATGW;</li>\n<li>WAN IPs;</li>\n<li>egress routing;</li>\n<li>Hubble flow validation.</li>\n</ul>\n<h2 id=\"bkmrk-development\">Development</h2>\n<ul id=\"bkmrk-app-packaging%3B-conta\">\n<li>app packaging;</li>\n<li>containers;</li>\n<li>GitOps manifests;</li>\n<li>APIs;</li>\n<li>WordPress migration tooling;</li>\n<li>future CRDs/controllers.</li>\n</ul>\n<h2 id=\"bkmrk-security\">Security</h2>\n<ul id=\"bkmrk-infisical-permission\">\n<li>Infisical permissions;</li>\n<li>RBAC;</li>\n<li>NetworkPolicy;</li>\n<li>secret usage;</li>\n<li>audit;</li>\n<li>future Suricata/WAF.</li>\n</ul>\n<h2 id=\"bkmrk-agent-engineering\">Agent Engineering</h2>\n<ul id=\"bkmrk-ai-hub%3B-memory-plane\">\n<li><code>ai-hub</code>;</li>\n<li>Memory Plane integration;</li>\n<li>Work Ledger;</li>\n<li>Hermes/ADK capabilities;</li>\n<li>tool runtimes;</li>\n<li>safety boundaries.</li>\n</ul>\n<h2 id=\"bkmrk-operations\">Operations</h2>\n<ul id=\"bkmrk-runbooks%3B-incidents%3B\">\n<li>runbooks;</li>\n<li>incidents;</li>\n<li>backups;</li>\n<li>restore drills;</li>\n<li>capacity;</li>\n<li>production migrations.</li>\n</ul>\n<hr id=\"bkmrk--40\">\n<h1 id=\"bkmrk-40.-suggested-first-\">40. Suggested first architecture review agenda</h1>\n<p id=\"bkmrk-para-presentar-este-\">Para presentar este documento a otros ingenieros, la primera sesi\u00f3n deber\u00eda revisar en este orden:</p>\n<ol id=\"bkmrk-rke2-vs-constraints-\">\n<li>RKE2 vs constraints de Proxmox/Ceph.</li>\n<li>Node LAN y CIDRs.</li>\n<li>API HA endpoint.</li>\n<li>Cilium VXLAN y Ceph connectivity.</li>\n<li>IPVS/DSR preservation.</li>\n<li>Traefik edge PoC.</li>\n<li>NATGW/eject routing model.</li>\n<li>Argo CD repository layout.</li>\n<li>Infisical self-hosted requirements/licensing.</li>\n<li>namespace/RBAC/security boundaries.</li>\n<li>backup/recovery design.</li>\n<li>migration pilot criteria.</li>\n</ol>\n<p id=\"bkmrk-no-intentar-resolver\">No intentar resolver Work Ledger, WAF, BGP, CRDs y service mesh en esa primera reuni\u00f3n.</p>\n<hr id=\"bkmrk--41\">\n<h1 id=\"bkmrk-41.-immediate-next-a\">41. Immediate next actions</h1>\n<h2 id=\"bkmrk-now-01\">NOW-01</h2>\n<p id=\"bkmrk-crear%2Finicializar-lo\">Crear/inicializar los cinco repos en <code>wetopi-platform</code>.</p>\n<h2 id=\"bkmrk-now-02\">NOW-02</h2>\n<p id=\"bkmrk-dar-acceso-a-codex-a\">Dar acceso a Codex a la organizaci\u00f3n GitHub con permisos suficientes para gestionar repos, PRs y workflows.</p>\n<h2 id=\"bkmrk-now-03\">NOW-03</h2>\n<p id=\"bkmrk-crear-un-inventario-\">Crear un inventario automatizado/read-only de Proxmox y networking.</p>\n<h2 id=\"bkmrk-now-04\">NOW-04</h2>\n<p id=\"bkmrk-producir-un-network-\">Producir un <code>network-inventory.md</code> con todos los rangos actuales antes de fijar Node/Pod/Service CIDRs.</p>\n<h2 id=\"bkmrk-now-05\">NOW-05</h2>\n<p id=\"bkmrk-cerrar-el-dise%C3%B1o-del\">Cerrar el dise\u00f1o del endpoint API HA.</p>\n<h2 id=\"bkmrk-now-06\">NOW-06</h2>\n<p id=\"bkmrk-crear-plantilla-ubun\">Crear plantilla Ubuntu 24.04 y <code>wetopi-ops01</code>.</p>\n<h2 id=\"bkmrk-now-07\">NOW-07</h2>\n<p id=\"bkmrk-implementar-opentofu\">Implementar OpenTofu/Ansible de CP/workers sin desplegar todav\u00eda servicios de negocio.</p>\n<h2 id=\"bkmrk-now-08\">NOW-08</h2>\n<p id=\"bkmrk-hacer-acceptance-tes\">Hacer acceptance test de RKE2 antes de Cilium/Ceph/Argo.</p>\n<hr id=\"bkmrk--42\">\n<h1 id=\"bkmrk-42.-arquitectura-obj\">42. Arquitectura objetivo final \u2014 vista ejecutiva</h1>\n<pre id=\"bkmrk-flowchart-tb-people%5B\"><code class=\"language-mermaid\">flowchart TB\n    People[Engineers / Operators / Employees]\n    Git[GitHub wetopi-platform]\n    Codex[Codex]\n    Hermes[Hermes]\n    ADK[ADK Fleet]\n\n    subgraph OpsPlanes[Operational Planes]\n      IaC[InfraOps\\nOpenTofu + Ansible]\n      GitOps[GitOps\\nArgo CD]\n      VaultOps[VaultOps\\nInfisical]\n      NetOps[NetOps\\nCilium / IPVS / NATGW]\n      ObsOps[ObservabilityOps\\nHubble + logs/metrics]\n      MemoryOps[MemoryOps]\n      WorkOps[WorkOps]\n      DocsOps[DocOps]\n    end\n\n    subgraph Infra[Private Cloud]\n      PVE[Proxmox]\n      Ceph[(Ceph)]\n      RKE[RKE2]\n      Edge[Edge]\n      Host[Hosting]\n    end\n\n    People --&gt; Git\n    People --&gt; VaultOps\n    People --&gt; DocsOps\n    Codex --&gt; Git\n    Hermes --&gt; WorkOps\n    Hermes --&gt; MemoryOps\n    Hermes --&gt; VaultOps\n    ADK --&gt; WorkOps\n    ADK --&gt; MemoryOps\n    ADK --&gt; VaultOps\n\n    Git --&gt; IaC\n    Git --&gt; GitOps\n    IaC --&gt; PVE\n    PVE --&gt; RKE\n    Ceph --&gt; RKE\n    GitOps --&gt; RKE\n    VaultOps --&gt; RKE\n    NetOps --&gt; RKE\n    ObsOps --&gt; RKE\n    RKE --&gt; Edge\n    RKE --&gt; Host\n</code></pre>\n<hr id=\"bkmrk--43\">\n<h1 id=\"bkmrk-43.-final-implementa\">43. Final implementation statement</h1>\n<p id=\"bkmrk-la-nueva-wetopi-no-d\">La nueva Wetopi no debe construirse como \u201cun cluster Kubernetes con algunos agentes\u201d.</p>\n<p id=\"bkmrk-debe-construirse-com\">Debe construirse como una <strong>plataforma operativa integrada</strong> donde:</p>\n<pre id=\"bkmrk-git-%3D-desired-state-\"><code class=\"language-text\">Git           = desired state\nOpenTofu      = virtual infrastructure provisioning\nAnsible       = host bootstrap/configuration\nRKE2          = orchestration platform\nCilium        = cluster network foundation\nHubble        = network visibility\nCeph          = persistent storage\nArgo CD       = continuous declarative reconciliation\nInfisical     = operational Vault\nSOPS + age    = Vault/bootstrap recovery bridge\nIPVS + DSR    = WAN ingress foundation v1\nTraefik       = HTTP/TLS edge v1\nNATGW         = simple controlled egress v1\nMemory Plane  = institutional knowledge\nWork Ledger   = coordination\nAI Hub        = shared capabilities\nHermes/ADK    = operational agents\nDocs          = institutional documentation\n</code></pre>\n<p id=\"bkmrk-el-orden-de-implemen\">El orden de implementaci\u00f3n debe preservar siempre dos propiedades:</p>\n<ol id=\"bkmrk-la-nueva-plataforma--2\">\n<li><strong>La nueva plataforma puede fallar durante su construcci\u00f3n sin afectar al Swarm productivo.</strong></li>\n<li><strong>La plataforma puede ser reconstruida desde Git, backups y material de recovery sin depender de servicios que solo existen dentro del cluster destruido.</strong></li>\n</ol>\n<p id=\"bkmrk-%C3%89sta-es-la-base-t%C3%A9cn\">\u00c9sta es la base t\u00e9cnica y organizativa que debe revisarse con Platform Engineering y Development antes de iniciar la construcci\u00f3n del cluster definitivo.</p>\n"
 },
 {
  "id": 58,
  "title": "network hosting architecture",
  "slug": "network-hosting-architecture",
  "book_id": 8,
  "chapter_id": 29,
  "description": null,
  "created_at": "2026-09-06T13:39:03.000000Z",
  "updated_at": "2026-09-06T13:39:03.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (design/network-hosting-architecture.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n# Wetopi RKE2 Network & Hosting Architecture\n## Memoria ejecutiva y t\u00e9cnica para la migraci\u00f3n desde Docker Swarm\n\n**Fecha:** 28 de agosto de 2026  \n**Estado:** Dise\u00f1o base consolidado  \n**\u00c1mbito:** Red, ingress, egress, hosting WordPress y transici\u00f3n desde Docker Swarm hacia RKE2 sobre Proxmox + Ceph.\n\n---\n\n# \u00cdndice\n\n1. Resumen ejecutivo\n2. Contexto actual de Wetopi\n3. Objetivos del nuevo dise\u00f1o\n4. Principios de arquitectura\n5. Decisi\u00f3n de plataforma: RKE2\n6. Arquitectura global objetivo\n7. Control plane RKE2\n8. Workers y dominios de infraestructura\n9. Cilium como CNI base\n10. Hubble como observabilidad de red\n11. Ingress WAN: conservar IPVS + VRRP + DSR\n12. Edge nodes RKE2\n13. Proxy HTTP/TLS y Gateway API\n14. Sustituci\u00f3n de Consul, Registrator y consul-template\n15. Modelo inicial de WordPress workloads\n16. Egress: dise\u00f1o simple de NAT Gateways\n17. Asignaci\u00f3n de clientes a gateways de salida\n18. Rotaci\u00f3n y cambio de IPs de salida\n19. Separaci\u00f3n de reputaci\u00f3n de IPs\n20. Ingress IP dedicada por cliente\n21. Integraci\u00f3n con OVH vRack\n22. Proxmox y agregaci\u00f3n de ancho de banda\n23. Storage: Ceph\n24. Observabilidad y seguridad de red\n25. Suricata y WAF futuros\n26. Qu\u00e9 NO vamos a desplegar inicialmente\n27. Modelo de alta disponibilidad\n28. Mantenimiento y draining de edge nodes\n29. Modelo de namespaces y roles de nodos\n30. Flujos de tr\u00e1fico de ejemplo\n31. Migraci\u00f3n progresiva desde Docker Swarm\n32. Fases de implementaci\u00f3n\n33. Decisiones consolidadas\n34. Riesgos principales y mitigaciones\n35. Evoluci\u00f3n futura\n36. Arquitectura objetivo resumida\n\n---\n\n# 1. Resumen ejecutivo\n\nWetopi es un Managed WordPress Hosting ejecutado sobre un cloud privado basado en Proxmox + Ceph, con Docker Swarm como scheduler actual y del orden de 700 contenedores.\n\nLa arquitectura WAN existente tiene dos propiedades valiosas que se preservar\u00e1n inicialmente:\n\n- balanceo L3/L4 mediante IPVS;\n- VIPs con VRRP/Keepalived;\n- Direct Server Return (DSR);\n- proxies WAN repartidos entre varios hosts Proxmox.\n\nDSR no solo proporciona eficiencia y HA: permite que las respuestas salgan directamente por distintos nodos WAN y, por tanto, aprovechar la capacidad de salida de m\u00faltiples hosts f\u00edsicos de OVH sin hacer retornar todo el tr\u00e1fico por un \u00fanico balanceador.\n\nLa nueva plataforma se construir\u00e1 sobre:\n\n- RKE2;\n- Cilium;\n- Hubble;\n- Kubernetes Services;\n- EndpointSlices;\n- Gateway API;\n- edge proxies Kubernetes-native;\n- Ceph mediante CSI.\n\nNo se intentar\u00e1 transformar toda la red a la vez.\n\nLa primera generaci\u00f3n conservar\u00e1 IPVS + VRRP + DSR en la capa WAN y sustituir\u00e1, detr\u00e1s de ella:\n\n- Docker Swarm;\n- Registrator;\n- Consul como service registry de websites;\n- consul-template;\n- generaci\u00f3n din\u00e1mica de nginx.conf.\n\nEl egress tambi\u00e9n se mantendr\u00e1 deliberadamente simple.\n\nSe desplegar\u00e1n varios NAT Gateways Ubuntu con IP interna estable y una IP p\u00fablica asociada:\n\n```text\n192.168.88.100 -> WAN IP A\n192.168.88.101 -> WAN IP B\n192.168.88.102 -> WAN IP C\n```\n\nDiferentes clientes, sites o clases de workload podr\u00e1n usar gateways distintos.\n\nEsto permite:\n\n- aislar reputaci\u00f3n;\n- separar crawlers/agentes de customers;\n- rotar WAN IPs;\n- mover clientes entre gateways;\n- ofrecer IP de salida dedicada;\n- mantener una soluci\u00f3n f\u00e1cil de operar.\n\nCilium se adopta como foundation de networking y observabilidad, pero no se requiere utilizar desde el primer d\u00eda BGP, Egress Gateway avanzado, CRDs propios o automatizaci\u00f3n compleja.\n\nLa filosof\u00eda es:\n\n> construir una base moderna, observable y extensible sin sobrecomplicar la primera versi\u00f3n.\n\n---\n\n# 2. Contexto actual de Wetopi\n\nLa plataforma actual se ejecuta sobre:\n\n```text\nOVH\n \u2193\nProxmox\n \u2193\nCeph\n \u2193\nDocker Swarm\n```\n\nEl workload principal son websites WordPress.\n\nCada website se representa aproximadamente mediante un `wepod`, que contiene actualmente:\n\n```text\ns6\n\u251c\u2500\u2500 nginx\n\u251c\u2500\u2500 php-fpm\n\u2514\u2500\u2500 mysql\n```\n\nLa entrada WAN actual es aproximadamente:\n\n```text\nInternet\n   \u2193\nPublic VIP\n   \u2193\nVRRP / Keepalived\n   \u2193\nIPVS directors\n   \u2193\nDSR\n   \u2193\nWAN Nginx proxies\n   \u2193\nDocker workloads\n```\n\nEl descubrimiento y configuraci\u00f3n se realiza mediante:\n\n```text\nDocker\n \u2193\nRegistrator\n \u2193\nConsul\n \u2193\nconsul-template\n \u2193\nnginx.conf\n \u2193\nreload\n```\n\nEste modelo ha sido funcional, pero introduce piezas que Kubernetes puede sustituir de forma nativa.\n\n---\n\n# 3. Objetivos del nuevo dise\u00f1o\n\nLa nueva red debe:\n\n1. Migrar Docker Swarm a RKE2.\n2. Mantener Proxmox + Ceph.\n3. Preservar IPVS/DSR inicialmente.\n4. Preservar la agregaci\u00f3n distribuida de egress.\n5. Eliminar Registrator.\n6. Eliminar Consul como registry de websites.\n7. Eliminar consul-template del path HTTP.\n8. Utilizar Service + EndpointSlice.\n9. Adoptar Gateway API.\n10. Utilizar un CNI moderno.\n11. Observar tr\u00e1fico por workload.\n12. Poder separar clientes por egress gateway.\n13. Poder cambiar f\u00e1cilmente una IP de salida.\n14. Permitir IP dedicada de salida.\n15. Permitir ingress dedicado en el futuro.\n16. Dejar preparada la red para WAF/IDS.\n17. Mantener simplicidad operacional.\n18. Permitir migraci\u00f3n progresiva.\n19. No a\u00f1adir service mesh sin necesidad.\n20. No depender de BGP para el bootstrap.\n\n---\n\n# 4. Principios de arquitectura\n\n## 4.1 Conservar lo que resuelve un problema real\n\nIPVS + VRRP + DSR seguir\u00e1 siendo inicialmente la capa L3/L4 WAN.\n\n## 4.2 Kubernetes sustituye el service discovery artesanal\n\nKubernetes ya proporciona:\n\n- Services;\n- EndpointSlices;\n- DNS;\n- readiness;\n- labels;\n- selectors.\n\nNo se recrear\u00e1 Consul dentro de Kubernetes para registrar websites.\n\n## 4.3 Capas separadas\n\n```text\nL3/L4 WAN          -> IPVS / DSR\nHTTP/TLS           -> Gateway / proxy\nCluster networking -> Cilium\nFlow visibility    -> Hubble\nDeep IDS futuro    -> Suricata\nHTTP security      -> WAF futuro\n```\n\n## 4.4 Egress simple\n\nPrimera versi\u00f3n:\n\n```text\nUbuntu gateway\n+\nrouting\n+\nnftables/iptables\n+\nWAN IP\n```\n\n## 4.5 Evoluci\u00f3n progresiva\n\nLa base debe permitir a\u00f1adir capacidades avanzadas despu\u00e9s, no obligar a desplegarlas ya.\n\n---\n\n# 5. Decisi\u00f3n de plataforma: RKE2\n\nSe selecciona RKE2 como distribuci\u00f3n Kubernetes principal.\n\nRazones:\n\n- Kubernetes conformante;\n- control plane cercano a upstream;\n- etcd embebido;\n- static pods;\n- containerd;\n- HA sencilla;\n- soporte s\u00f3lido de CNI;\n- lifecycle m\u00e1s f\u00e1cil que kubeadm;\n- adecuada para plataforma productiva de largo plazo.\n\nRancher UI no es requisito.\n\n---\n\n# 6. Arquitectura global objetivo\n\n```text\n                              INTERNET\n                                 \u2502\n                     \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                     \u2502                       \u2502\n                   VIP A                   VIP B\n                     \u2502                       \u2502\n                   IPVS                    IPVS\n                     \u2502                       \u2502\n                    DSR                     DSR\n                     \u2502                       \u2502\n             \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510      \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n             \u2502 RKE2 EDGE A    \u2502      \u2502 RKE2 EDGE B    \u2502\n             \u2502 proxy HTTP/TLS \u2502      \u2502 proxy HTTP/TLS \u2502\n             \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518      \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                     \u2502                       \u2502\n                     \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                                \u2502\n                          Gateway API\n                                \u2502\n                     Service / EndpointSlice\n                                \u2502\n                              Pods\n                                \u2502\n                              Ceph\n\nEGRESS:\n\nPods / Sites / Agents\n        \u2502\n        \u25bc\ninternal gateway selection\n        \u2502\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u25bc      \u25bc        \u25bc\n.100   .101     .102\n GW A   GW B     GW C\n \u2502      \u2502        \u2502\nWAN A  WAN B    WAN C\n \u2502      \u2502        \u2502\n \u2514\u2500\u2500\u2500\u2500 Internet \u2500\u2518\n```\n\n---\n\n# 7. Control plane RKE2\n\nBaseline:\n\n```text\n3 x RKE2 server nodes\n+\nembedded etcd\n```\n\nModelo:\n\n```text\nAPI stable endpoint / VIP\n          \u2502\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u25bc        \u25bc        \u25bc\ncp01     cp02     cp03\netcd     etcd     etcd\n```\n\nLos control planes deben quedar separados de los workloads de customers.\n\nSe prioriza:\n\n- NVMe;\n- baja latencia;\n- backups de etcd;\n- restore probado;\n- endpoint API estable.\n\n---\n\n# 8. Workers y dominios de infraestructura\n\nSe definir\u00e1n roles expl\u00edcitos:\n\n```text\nwetopi.io/role=edge\nwetopi.io/role=hosting\nwetopi.io/role=agents\nwetopi.io/role=database\nwetopi.io/role=infra\n```\n\nAdem\u00e1s, cuando sea \u00fatil:\n\n```text\nwetopi.io/proxmox-host=pve01\nwetopi.io/bandwidth-domain=pve01\nwetopi.io/wan-zone=a\n```\n\nEl host Proxmox no es solo un failure domain: tambi\u00e9n puede ser un bandwidth domain.\n\n---\n\n# 9. Cilium como CNI base\n\nSe selecciona Cilium como CNI.\n\nObjetivos iniciales:\n\n- red de pods;\n- NetworkPolicy;\n- observabilidad;\n- Hubble;\n- integraci\u00f3n con Kubernetes;\n- base futura para capacidades avanzadas.\n\nNo es requisito inicial:\n\n- sustituir IPVS;\n- BGP WAN;\n- gestionar todas las IPs p\u00fablicas;\n- controlar todo egress mediante CRDs.\n\n---\n\n# 10. Hubble como observabilidad de red\n\nHubble ser\u00e1 la primera capa de visibilidad de flows.\n\nPermitir\u00e1 responder preguntas como:\n\n- qu\u00e9 pod abri\u00f3 una conexi\u00f3n;\n- qu\u00e9 site est\u00e1 haciendo egress;\n- qu\u00e9 namespace habla con qu\u00e9 destino;\n- qu\u00e9 tr\u00e1fico fue bloqueado;\n- qu\u00e9 workload est\u00e1 aumentando conexiones.\n\nEl objetivo a medio plazo es correlacionar:\n\n```text\npod\nnamespace\nsite\ncustomer\nnode\ngateway\negress IP\ndestination\n```\n\n---\n\n# 11. Ingress WAN: conservar IPVS + VRRP + DSR\n\nSe conserva el patr\u00f3n:\n\n```text\nInternet\n \u2193\nVIP\n \u2193\nVRRP\n \u2193\nIPVS\n \u2193\nDSR\n \u2193\nedge node\n```\n\n## Por qu\u00e9\n\nCon DSR:\n\n```text\nrequest:\nclient -> IPVS -> edge\n\nresponse:\nedge -> client\n```\n\nLa respuesta no vuelve por el director.\n\nEsto preserva la capacidad de salida distribuida entre hosts f\u00edsicos OVH.\n\n---\n\n# 12. Edge nodes RKE2\n\nLos antiguos WAN proxies pasar\u00e1n progresivamente a edge workers RKE2.\n\nEjemplo:\n\n```text\nedge-a01\nedge-a02\nedge-b01\nedge-b02\n```\n\nCada edge VM debe estar distribuida deliberadamente entre hosts Proxmox distintos.\n\nLabels ejemplo:\n\n```text\nwetopi.io/role=edge\nwetopi.io/wan-zone=a\nwetopi.io/proxmox-host=pve01\n```\n\nTaint recomendado:\n\n```text\nwetopi.io/edge=true:NoSchedule\n```\n\n---\n\n# 13. Proxy HTTP/TLS y Gateway API\n\nEl edge seguir\u00e1 terminando TLS.\n\nCandidatos:\n\n- Traefik;\n- Envoy.\n\nLa arquitectura no debe depender todav\u00eda de la elecci\u00f3n final.\n\nResponsabilidades:\n\n- TLS;\n- routing HTTP;\n- forwarding de IP real;\n- headers;\n- m\u00e9tricas;\n- readiness;\n- futura integraci\u00f3n WAF.\n\nRouting:\n\n```text\nhostname\n \u2193\nHTTPRoute\n \u2193\nService\n \u2193\nEndpointSlice\n \u2193\nPod\n```\n\n---\n\n# 14. Sustituci\u00f3n de Consul, Registrator y consul-template\n\nANTES:\n\n```text\ncontainer\n \u2193\nRegistrator\n \u2193\nConsul\n \u2193\nconsul-template\n \u2193\nnginx.conf\n \u2193\nreload\n```\n\nNUEVO:\n\n```text\nPod\n \u2193\nService selector\n \u2193\nEndpointSlice\n \u2193\nGateway controller\n \u2193\ndynamic route\n```\n\nConsul puede seguir existiendo para otros usos si hubiera necesidad, pero deja de ser necesario como service registry del hosting web.\n\n---\n\n# 15. Modelo inicial de WordPress workloads\n\nNo se recomienda refactorizar simult\u00e1neamente todo el `wepod`.\n\nPrimera migraci\u00f3n:\n\n```text\nDocker wepod\n    \u2193\nKubernetes Pod / StatefulSet\n```\n\nmanteniendo inicialmente:\n\n```text\ns6\nnginx\nphp-fpm\nmysql\n```\n\ncuando sea pr\u00e1ctico.\n\nLa separaci\u00f3n posterior de database/frontend debe tratarse como proyecto independiente.\n\n---\n\n# 16. Egress: dise\u00f1o simple de NAT Gateways\n\nPrimera versi\u00f3n:\n\n```text\ngw100\n  LAN: 192.168.88.100\n  WAN: PUBLIC_IP_A\n\ngw101\n  LAN: 192.168.88.101\n  WAN: PUBLIC_IP_B\n\ngw102\n  LAN: 192.168.88.102\n  WAN: PUBLIC_IP_C\n```\n\nCada gateway ser\u00e1 una VM Ubuntu sencilla.\n\nFunciones:\n\n- routing;\n- forwarding;\n- SNAT/MASQUERADE;\n- logging;\n- health;\n- m\u00e9tricas b\u00e1sicas.\n\nNo necesita initially un controller Kubernetes.\n\n---\n\n# 17. Asignaci\u00f3n de clientes a gateways de salida\n\nModelo:\n\n```text\nsite/customer\n \u2193\negress class\n \u2193\ngateway interno\n```\n\nEjemplo:\n\n```text\nsite-1001 -> 192.168.88.100\nsite-1002 -> 192.168.88.100\nsite-2001 -> 192.168.88.101\ncrawler   -> 192.168.88.102\n```\n\nLa implementaci\u00f3n puede empezar con routing/policy routing y evolucionar despu\u00e9s.\n\nLo importante es que sea:\n\n- determinista;\n- observable;\n- f\u00e1cil de cambiar.\n\n---\n\n# 18. Rotaci\u00f3n y cambio de IPs de salida\n\nCaso 1:\n\n```text\ngw100:\nPUBLIC_IP_A\n  \u2193\nPUBLIC_IP_B\n```\n\nCaso 2:\n\n```text\ncustomer/site\ngw100\n  \u2193\ngw101\n```\n\nLos workloads no deben necesitar conocer la WAN IP.\n\nSolo la pol\u00edtica/ruta de salida.\n\n---\n\n# 19. Separaci\u00f3n de reputaci\u00f3n de IPs\n\nDesde el principio se recomienda separar:\n\n```text\nhosting-general\nhosting-premium\nplatform\nagents/crawlers\n```\n\nEjemplo:\n\n```text\n192.168.88.100\n -> shared hosting\n -> 51.x.x.10\n\n192.168.88.101\n -> premium/dedicated pool\n -> 51.x.x.11\n\n192.168.88.102\n -> agents/crawlers\n -> 51.x.x.12\n```\n\nLos crawlers no deben compartir IP con customer hosting.\n\n---\n\n# 20. Ingress IP dedicada por cliente\n\nNo es prioridad inicial.\n\nEl sistema debe permitir en el futuro:\n\n```text\ncustomer premium\n  \u2193\ndedicated public VIP\n  \u2193\nIPVS / DSR\n  \u2193\nedge\n  \u2193\nHTTPRoute\n```\n\nNo requiere redise\u00f1ar el backend Kubernetes.\n\n---\n\n# 21. Integraci\u00f3n con OVH vRack\n\nvRack seguir\u00e1 siendo la red privada base.\n\nEl dise\u00f1o inicial no depende de BGP.\n\nLos bloques Additional IP podr\u00e1n utilizarse para:\n\n- VIPs;\n- IPs de NATGW;\n- futuras IPs dedicadas.\n\nBGP Service de OVH podr\u00e1 evaluarse cuando sea suficientemente maduro para producci\u00f3n.\n\n---\n\n# 22. Proxmox y agregaci\u00f3n de ancho de banda\n\nLa distribuci\u00f3n de edge nodes debe respetar la topolog\u00eda f\u00edsica.\n\nEjemplo:\n\n```text\npve01 -> edge01\npve02 -> edge02\npve03 -> edge03\n```\n\nEvitar:\n\n```text\npve01 -> edge01 + edge02 + edge03\n```\n\ncuando el objetivo es sumar failure/bandwidth domains.\n\n---\n\n# 23. Storage: Ceph\n\nCeph seguir\u00e1 siendo el storage principal.\n\nRKE2 consumir\u00e1 storage mediante CSI.\n\nObjetivos:\n\n- PVC nativo;\n- StorageClasses;\n- snapshots donde sea \u00fatil;\n- evitar dependencias de Docker volumes espec\u00edficos.\n\n---\n\n# 24. Observabilidad y seguridad de red\n\nPrimera capa:\n\n```text\nCilium\n \u2193\nHubble\n```\n\nM\u00e1s adelante:\n\n```text\nHubble flows\n+\nedge HTTP logs\n+\nNATGW logs\n+\nSuricata\n+\nWAF\n```\n\nTodos podr\u00e1n converger en la plataforma de observabilidad.\n\n---\n\n# 25. Suricata y WAF futuros\n\nNo son requisito de bootstrap.\n\n## Suricata\n\n\u00datil para:\n\n- IDS;\n- scans;\n- anomal\u00edas;\n- C2;\n- DNS;\n- metadata de protocolos.\n\nPosibles puntos:\n\n- edge;\n- egress gateway;\n- TAP/mirror.\n\n## WAF\n\nDebe operar despu\u00e9s de TLS termination para ver HTTP.\n\n```text\nInternet\n \u2193\nTLS termination\n \u2193\nWAF\n \u2193\nbackend\n```\n\n---\n\n# 26. Qu\u00e9 NO vamos a desplegar inicialmente\n\nNo se considera necesario de salida:\n\n- Istio;\n- service mesh global;\n- BGP WAN;\n- Cilium BGP externo;\n- controlador propio de egress;\n- IP reputation controller;\n- IPAM p\u00fablico din\u00e1mico;\n- Suricata inline global;\n- WAF sofisticado distribuido;\n- CRDs propios para toda la red.\n\nLa ausencia de estas piezas es deliberada.\n\n---\n\n# 27. Modelo de alta disponibilidad\n\n## Control plane\n\n```text\n3 x RKE2 server\n```\n\n## Ingress\n\n```text\nVRRP\n+\nIPVS\n+\nvarios edge nodes\n+\nDSR\n```\n\n## Egress\n\nVarios gateways:\n\n```text\ngw100\ngw101\ngw102\n```\n\nLa HA de una IP dedicada concreta podr\u00e1 implementarse cuando exista necesidad.\n\nNo es necesario resolver desde el primer d\u00eda todos los casos premium.\n\n---\n\n# 28. Mantenimiento y draining de edge nodes\n\nProcedimiento recomendado:\n\n```text\n1. sacar edge de IPVS\n2. dejar de enviar nuevas conexiones\n3. esperar conexiones existentes\n4. kubectl drain\n5. mantenimiento\n6. kubectl uncordon\n7. validar proxy\n8. reinsertar en IPVS\n```\n\nDespu\u00e9s podr\u00e1 automatizarse.\n\n---\n\n# 29. Modelo de namespaces y roles de nodos\n\nNamespaces orientativos:\n\n```text\nwetopi-edge\nwetopi-hosting\nwetopi-agents\nwetopi-platform\nwetopi-observability\nwetopi-security\n```\n\nRoles de nodo:\n\n```text\ncontrol-plane\nedge\nhosting\nagents\ninfra\n```\n\nSe utilizar\u00e1n:\n\n- labels;\n- taints;\n- affinity;\n- topology spread.\n\n---\n\n# 30. Flujos de tr\u00e1fico de ejemplo\n\n## Entrada HTTP\n\n```text\nClient\n \u2193\nVIP\n \u2193\nIPVS\n \u2193 DSR\nedge02\n \u2193\nTLS\n \u2193\nHTTPRoute\n \u2193\nService\n \u2193\nsite pod\n```\n\nRespuesta:\n\n```text\nedge02\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25ba Client\n```\n\n## Salida shared\n\n```text\nsite-1001\n \u2193\n192.168.88.100\n \u2193\nSNAT\n \u2193\n51.x.x.10\n \u2193\nInternet\n```\n\n## Salida premium\n\n```text\nsite-2001\n \u2193\n192.168.88.101\n \u2193\nSNAT\n \u2193\n51.x.x.11\n```\n\n## Crawler\n\n```text\nagent-crawler\n \u2193\n192.168.88.102\n \u2193\n51.x.x.12\n```\n\n## Rotaci\u00f3n\n\n```text\ncustomer X\n \u2193\ngw100\n```\n\npasa a:\n\n```text\ncustomer X\n \u2193\ngw101\n```\n\nsin mover el workload.\n\n---\n\n# 31. Migraci\u00f3n progresiva desde Docker Swarm\n\nNo big-bang.\n\nDurante la transici\u00f3n:\n\n```text\nIPVS\n \u251c\u2500\u2500 old WAN -> Swarm\n \u2514\u2500\u2500 new edge -> RKE2\n```\n\npueden coexistir.\n\nMigraci\u00f3n:\n\n```text\nsite 1\nsite 2\nsite 3\n...\n```\n\nde forma progresiva.\n\nIdealmente la VIP p\u00fablica permanece y el routing decide el backend.\n\n---\n\n# 32. Fases de implementaci\u00f3n\n\n## Fase 0 \u2014 Inventario\n\nDocumentar:\n\n- VIPs;\n- IPVS;\n- VRRP;\n- VLANs;\n- vRack;\n- Proxmox;\n- WAN bandwidth domains;\n- NATGW;\n- Ceph.\n\n## Fase 1 \u2014 RKE2\n\n- 3 control planes;\n- workers;\n- embedded etcd;\n- API VIP;\n- GitOps;\n- Ceph CSI.\n\n## Fase 2 \u2014 Cilium/Hubble\n\n- Cilium;\n- Hubble;\n- primeras NetworkPolicies;\n- flow visibility.\n\n## Fase 3 \u2014 Edge\n\n- edge workers;\n- proxy DaemonSet;\n- DSR;\n- health checks;\n- integraci\u00f3n con IPVS.\n\n## Fase 4 \u2014 Gateway API\n\n- Gateway;\n- HTTPRoute;\n- Services;\n- EndpointSlices.\n\n## Fase 5 \u2014 Primeros sites\n\nValidar:\n\n- TLS;\n- IP cliente;\n- headers;\n- storage;\n- health;\n- performance;\n- egress.\n\n## Fase 6 \u2014 NATGW simples\n\nDesplegar:\n\n```text\n192.168.88.100\n192.168.88.101\n192.168.88.102\n```\n\ncon WAN IPs separadas.\n\n## Fase 7 \u2014 Migraci\u00f3n masiva\n\nMover progresivamente el grueso de los workloads.\n\n## Fase 8 \u2014 Observabilidad avanzada\n\nEnriquecimiento de flows y dashboards.\n\n## Fase 9 \u2014 Seguridad avanzada opcional\n\n- Suricata;\n- WAF;\n- egress automation;\n- IP reputation;\n- dedicated IP automation.\n\n---\n\n# 33. Decisiones consolidadas\n\n## NET-001\nRKE2 ser\u00e1 la distribuci\u00f3n Kubernetes.\n\n## NET-002\nCilium ser\u00e1 el CNI inicial.\n\n## NET-003\nHubble ser\u00e1 la base de network observability.\n\n## NET-004\nIPVS + VRRP + DSR se mantienen inicialmente.\n\n## NET-005\nLos edge nodes seguir\u00e1n repartidos por hosts f\u00edsicos Proxmox.\n\n## NET-006\nRegistrator deja de ser necesario.\n\n## NET-007\nConsul deja de ser el registry de websites.\n\n## NET-008\nconsul-template deja de generar la configuraci\u00f3n HTTP de RKE2.\n\n## NET-009\nGateway API ser\u00e1 la abstracci\u00f3n de routing HTTP.\n\n## NET-010\nTraefik o Envoy actuar\u00e1n como proxy edge.\n\n## NET-011\nEl egress inicial usar\u00e1 NATGW Ubuntu simples.\n\n## NET-012\nCada NATGW tendr\u00e1 una IP interna estable.\n\nEjemplo:\n\n```text\n192.168.88.100\n192.168.88.101\n192.168.88.102\n```\n\n## NET-013\nLa WAN IP asociada a un gateway debe ser f\u00e1cil de cambiar.\n\n## NET-014\nAgents/crawlers no compartir\u00e1n IP de salida con customers.\n\n## NET-015\nSe podr\u00e1 asignar un cliente a un gateway espec\u00edfico.\n\n## NET-016\nNo se desplegar\u00e1 Istio inicialmente.\n\n## NET-017\nEl bootstrap no depender\u00e1 de BGP.\n\n## NET-018\nNo se construir\u00e1 inicialmente un egress control plane complejo si gateways est\u00e1ticos resuelven el problema.\n\n---\n\n# 34. Riesgos principales y mitigaciones\n\n## Cambio simult\u00e1neo excesivo\n\nMitigaci\u00f3n:\n\n- migraci\u00f3n por capas;\n- conservar IPVS;\n- conservar inicialmente wepod.\n\n## P\u00e9rdida accidental de DSR\n\nMitigaci\u00f3n:\n\n- edge nodes dedicados;\n- pruebas expl\u00edcitas de dataplane.\n\n## Mala distribuci\u00f3n f\u00edsica\n\nMitigaci\u00f3n:\n\n- labels de Proxmox host;\n- anti-affinity;\n- topology awareness.\n\n## NATGW como punto de fallo\n\nMitigaci\u00f3n:\n\n- varios gateways;\n- reasignaci\u00f3n sencilla;\n- HA premium cuando se requiera.\n\n## Reputaci\u00f3n compartida\n\nMitigaci\u00f3n:\n\n- separar pools desde el principio.\n\n## Overengineering\n\nMitigaci\u00f3n:\n\n- no desplegar capacidades avanzadas sin necesidad operacional concreta.\n\n---\n\n# 35. Evoluci\u00f3n futura\n\nUna vez estable la base se podr\u00e1 evaluar:\n\n## Cilium Egress Gateway\n\nPara seleccionar egress por labels de Kubernetes.\n\n## Egress pools\n\n```text\nshared\npremium\ncrawler\ndedicated\n```\n\n## Reputation monitoring\n\nMedici\u00f3n y rotaci\u00f3n de IPs.\n\n## Suricata\n\nIDS/IPS de red.\n\n## WAF\n\nInspecci\u00f3n HTTP tras TLS.\n\n## BGP\n\nCuando OVH ofrezca una opci\u00f3n madura y productiva.\n\n## Eliminaci\u00f3n de IPVS\n\nSolo si una alternativa BGP/ECMP conserva:\n\n- HA;\n- DSR/equivalente;\n- capacidad;\n- simplicidad.\n\n## WordPressSite CRD\n\nEn una fase posterior cada website podr\u00eda representarse como:\n\n```yaml\napiVersion: wetopi.io/v1\nkind: WordPressSite\n```\n\ny generar autom\u00e1ticamente:\n\n- workload;\n- Service;\n- HTTPRoute;\n- PVC;\n- monitoring;\n- policies.\n\nNo es requisito de la primera migraci\u00f3n.\n\n---\n\n# 36. Arquitectura objetivo resumida\n\n```text\n                        OVH / vRack\n                             \u2502\n                      Proxmox + Ceph\n                             \u2502\n                            RKE2\n                             \u2502\n              \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n              \u2502              \u2502              \u2502\n          Control Plane    Edge          Hosting\n              \u2502              \u2502              \u2502\n              \u2502           Cilium            \u2502\n              \u2502           Hubble            \u2502\n              \u2502              \u2502              \u2502\n              \u2502         Gateway API         \u2502\n              \u2502              \u2502              \u2502\n              \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                             \u2502\n                          Websites\n```\n\nINGRESS:\n\n```text\nInternet\n \u2193\nVIP / VRRP\n \u2193\nIPVS\n \u2193 DSR\nRKE2 Edge\n \u2193\nTLS / HTTP\n \u2193\nGateway API\n \u2193\nWebsite\n```\n\nEGRESS:\n\n```text\nWorkload\n \u2193\nselected internal GW\n\n \u251c\u2500\u2500 192.168.88.100 -> WAN IP A\n \u251c\u2500\u2500 192.168.88.101 -> WAN IP B\n \u2514\u2500\u2500 192.168.88.102 -> WAN IP C\n\n \u2193\nInternet\n```\n\nOBSERVABILITY:\n\n```text\nCilium\n \u2193\nHubble\n \u2193\nflows\n \u2193\nfuture security analytics\n```\n\nFUTURO:\n\n```text\nSuricata\nWAF\nreputation monitoring\nautomated egress\nBGP\ndedicated IP automation\n```\n\n---\n\n# Conclusi\u00f3n\n\nEl dise\u00f1o inicial de la nueva Wetopi debe optimizar robustez, visibilidad y capacidad de evoluci\u00f3n, no complejidad.\n\nLa base acordada es:\n\n```text\nRKE2\n+\nCilium/Hubble\n+\nIPVS/VRRP/DSR existente\n+\nGateway API\n+\nedge proxies Kubernetes-native\n+\nServices/EndpointSlices\n+\nNATGW Ubuntu simples\n```\n\nEste modelo conserva las propiedades valiosas de la plataforma actual:\n\n- DSR;\n- retorno directo;\n- capacidad WAN distribuida;\n- IPs movibles;\n- separaci\u00f3n de egress;\n\ny elimina progresivamente las piezas que Kubernetes puede resolver mejor:\n\n- Docker Swarm;\n- Registrator;\n- Consul como service registry web;\n- consul-template;\n- regeneraci\u00f3n de nginx.conf.\n\nSobre esta foundation se podr\u00e1n a\u00f1adir m\u00e1s adelante Cilium Egress Gateway, Suricata, WAF, reputaci\u00f3n autom\u00e1tica, BGP e IPs dedicadas gestionadas sin condicionar ni complicar el primer despliegue.",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (design/network-hosting-architecture.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<h1 id=\"bkmrk-wetopi-rke2-network-\">Wetopi RKE2 Network &amp; Hosting Architecture</h1>\n<h2 id=\"bkmrk-memoria-ejecutiva-y-\">Memoria ejecutiva y t\u00e9cnica para la migraci\u00f3n desde Docker Swarm</h2>\n<p id=\"bkmrk-fecha%3A-28-de-agosto-\"><strong>Fecha:</strong> 28 de agosto de 2026<br>\n<strong>Estado:</strong> Dise\u00f1o base consolidado<br>\n<strong>\u00c1mbito:</strong> Red, ingress, egress, hosting WordPress y transici\u00f3n desde Docker Swarm hacia RKE2 sobre Proxmox + Ceph.</p>\n<hr id=\"bkmrk-\">\n<h1 id=\"bkmrk-%C3%8Dndice\">\u00cdndice</h1>\n<ol id=\"bkmrk-resumen-ejecutivo-co\">\n<li>Resumen ejecutivo</li>\n<li>Contexto actual de Wetopi</li>\n<li>Objetivos del nuevo dise\u00f1o</li>\n<li>Principios de arquitectura</li>\n<li>Decisi\u00f3n de plataforma: RKE2</li>\n<li>Arquitectura global objetivo</li>\n<li>Control plane RKE2</li>\n<li>Workers y dominios de infraestructura</li>\n<li>Cilium como CNI base</li>\n<li>Hubble como observabilidad de red</li>\n<li>Ingress WAN: conservar IPVS + VRRP + DSR</li>\n<li>Edge nodes RKE2</li>\n<li>Proxy HTTP/TLS y Gateway API</li>\n<li>Sustituci\u00f3n de Consul, Registrator y consul-template</li>\n<li>Modelo inicial de WordPress workloads</li>\n<li>Egress: dise\u00f1o simple de NAT Gateways</li>\n<li>Asignaci\u00f3n de clientes a gateways de salida</li>\n<li>Rotaci\u00f3n y cambio de IPs de salida</li>\n<li>Separaci\u00f3n de reputaci\u00f3n de IPs</li>\n<li>Ingress IP dedicada por cliente</li>\n<li>Integraci\u00f3n con OVH vRack</li>\n<li>Proxmox y agregaci\u00f3n de ancho de banda</li>\n<li>Storage: Ceph</li>\n<li>Observabilidad y seguridad de red</li>\n<li>Suricata y WAF futuros</li>\n<li>Qu\u00e9 NO vamos a desplegar inicialmente</li>\n<li>Modelo de alta disponibilidad</li>\n<li>Mantenimiento y draining de edge nodes</li>\n<li>Modelo de namespaces y roles de nodos</li>\n<li>Flujos de tr\u00e1fico de ejemplo</li>\n<li>Migraci\u00f3n progresiva desde Docker Swarm</li>\n<li>Fases de implementaci\u00f3n</li>\n<li>Decisiones consolidadas</li>\n<li>Riesgos principales y mitigaciones</li>\n<li>Evoluci\u00f3n futura</li>\n<li>Arquitectura objetivo resumida</li>\n</ol>\n<hr id=\"bkmrk--1\">\n<h1 id=\"bkmrk-1.-resumen-ejecutivo\">1. Resumen ejecutivo</h1>\n<p id=\"bkmrk-wetopi-es-un-managed\">Wetopi es un Managed WordPress Hosting ejecutado sobre un cloud privado basado en Proxmox + Ceph, con Docker Swarm como scheduler actual y del orden de 700 contenedores.</p>\n<p id=\"bkmrk-la-arquitectura-wan-\">La arquitectura WAN existente tiene dos propiedades valiosas que se preservar\u00e1n inicialmente:</p>\n<ul id=\"bkmrk-balanceo-l3%2Fl4-media\">\n<li>balanceo L3/L4 mediante IPVS;</li>\n<li>VIPs con VRRP/Keepalived;</li>\n<li>Direct Server Return (DSR);</li>\n<li>proxies WAN repartidos entre varios hosts Proxmox.</li>\n</ul>\n<p id=\"bkmrk-dsr-no-solo-proporci\">DSR no solo proporciona eficiencia y HA: permite que las respuestas salgan directamente por distintos nodos WAN y, por tanto, aprovechar la capacidad de salida de m\u00faltiples hosts f\u00edsicos de OVH sin hacer retornar todo el tr\u00e1fico por un \u00fanico balanceador.</p>\n<p id=\"bkmrk-la-nueva-plataforma-\">La nueva plataforma se construir\u00e1 sobre:</p>\n<ul id=\"bkmrk-rke2%3B-cilium%3B-hubble\">\n<li>RKE2;</li>\n<li>Cilium;</li>\n<li>Hubble;</li>\n<li>Kubernetes Services;</li>\n<li>EndpointSlices;</li>\n<li>Gateway API;</li>\n<li>edge proxies Kubernetes-native;</li>\n<li>Ceph mediante CSI.</li>\n</ul>\n<p id=\"bkmrk-no-se-intentar%C3%A1-tran\">No se intentar\u00e1 transformar toda la red a la vez.</p>\n<p id=\"bkmrk-la-primera-generaci%C3%B3\">La primera generaci\u00f3n conservar\u00e1 IPVS + VRRP + DSR en la capa WAN y sustituir\u00e1, detr\u00e1s de ella:</p>\n<ul id=\"bkmrk-docker-swarm%3B-regist\">\n<li>Docker Swarm;</li>\n<li>Registrator;</li>\n<li>Consul como service registry de websites;</li>\n<li>consul-template;</li>\n<li>generaci\u00f3n din\u00e1mica de nginx.conf.</li>\n</ul>\n<p id=\"bkmrk-el-egress-tambi%C3%A9n-se\">El egress tambi\u00e9n se mantendr\u00e1 deliberadamente simple.</p>\n<p id=\"bkmrk-se-desplegar%C3%A1n-vario\">Se desplegar\u00e1n varios NAT Gateways Ubuntu con IP interna estable y una IP p\u00fablica asociada:</p>\n<pre id=\"bkmrk-192.168.88.100--%3E-wa\"><code class=\"language-text\">192.168.88.100 -&gt; WAN IP A\n192.168.88.101 -&gt; WAN IP B\n192.168.88.102 -&gt; WAN IP C\n</code></pre>\n<p id=\"bkmrk-diferentes-clientes%2C\">Diferentes clientes, sites o clases de workload podr\u00e1n usar gateways distintos.</p>\n<p id=\"bkmrk-esto-permite%3A\">Esto permite:</p>\n<ul id=\"bkmrk-aislar-reputaci%C3%B3n%3B-s\">\n<li>aislar reputaci\u00f3n;</li>\n<li>separar crawlers/agentes de customers;</li>\n<li>rotar WAN IPs;</li>\n<li>mover clientes entre gateways;</li>\n<li>ofrecer IP de salida dedicada;</li>\n<li>mantener una soluci\u00f3n f\u00e1cil de operar.</li>\n</ul>\n<p id=\"bkmrk-cilium-se-adopta-com\">Cilium se adopta como foundation de networking y observabilidad, pero no se requiere utilizar desde el primer d\u00eda BGP, Egress Gateway avanzado, CRDs propios o automatizaci\u00f3n compleja.</p>\n<p id=\"bkmrk-la-filosof%C3%ADa-es%3A\">La filosof\u00eda es:</p>\n<blockquote id=\"bkmrk-construir-una-base-m\">\n<p>construir una base moderna, observable y extensible sin sobrecomplicar la primera versi\u00f3n.</p>\n</blockquote>\n<hr id=\"bkmrk--2\">\n<h1 id=\"bkmrk-2.-contexto-actual-d\">2. Contexto actual de Wetopi</h1>\n<p id=\"bkmrk-la-plataforma-actual\">La plataforma actual se ejecuta sobre:</p>\n<pre id=\"bkmrk-ovh-%E2%86%93-proxmox-%E2%86%93-ceph\"><code class=\"language-text\">OVH\n \u2193\nProxmox\n \u2193\nCeph\n \u2193\nDocker Swarm\n</code></pre>\n<p id=\"bkmrk-el-workload-principa\">El workload principal son websites WordPress.</p>\n<p id=\"bkmrk-cada-website-se-repr\">Cada website se representa aproximadamente mediante un <code>wepod</code>, que contiene actualmente:</p>\n<pre id=\"bkmrk-s6-%E2%94%9C%E2%94%80%E2%94%80-nginx-%E2%94%9C%E2%94%80%E2%94%80-php\"><code class=\"language-text\">s6\n\u251c\u2500\u2500 nginx\n\u251c\u2500\u2500 php-fpm\n\u2514\u2500\u2500 mysql\n</code></pre>\n<p id=\"bkmrk-la-entrada-wan-actua\">La entrada WAN actual es aproximadamente:</p>\n<pre id=\"bkmrk-internet-%E2%86%93-public-vi\"><code class=\"language-text\">Internet\n   \u2193\nPublic VIP\n   \u2193\nVRRP / Keepalived\n   \u2193\nIPVS directors\n   \u2193\nDSR\n   \u2193\nWAN Nginx proxies\n   \u2193\nDocker workloads\n</code></pre>\n<p id=\"bkmrk-el-descubrimiento-y-\">El descubrimiento y configuraci\u00f3n se realiza mediante:</p>\n<pre id=\"bkmrk-docker-%E2%86%93-registrator\"><code class=\"language-text\">Docker\n \u2193\nRegistrator\n \u2193\nConsul\n \u2193\nconsul-template\n \u2193\nnginx.conf\n \u2193\nreload\n</code></pre>\n<p id=\"bkmrk-este-modelo-ha-sido-\">Este modelo ha sido funcional, pero introduce piezas que Kubernetes puede sustituir de forma nativa.</p>\n<hr id=\"bkmrk--3\">\n<h1 id=\"bkmrk-3.-objetivos-del-nue\">3. Objetivos del nuevo dise\u00f1o</h1>\n<p id=\"bkmrk-la-nueva-red-debe%3A\">La nueva red debe:</p>\n<ol id=\"bkmrk-migrar-docker-swarm-\">\n<li>Migrar Docker Swarm a RKE2.</li>\n<li>Mantener Proxmox + Ceph.</li>\n<li>Preservar IPVS/DSR inicialmente.</li>\n<li>Preservar la agregaci\u00f3n distribuida de egress.</li>\n<li>Eliminar Registrator.</li>\n<li>Eliminar Consul como registry de websites.</li>\n<li>Eliminar consul-template del path HTTP.</li>\n<li>Utilizar Service + EndpointSlice.</li>\n<li>Adoptar Gateway API.</li>\n<li>Utilizar un CNI moderno.</li>\n<li>Observar tr\u00e1fico por workload.</li>\n<li>Poder separar clientes por egress gateway.</li>\n<li>Poder cambiar f\u00e1cilmente una IP de salida.</li>\n<li>Permitir IP dedicada de salida.</li>\n<li>Permitir ingress dedicado en el futuro.</li>\n<li>Dejar preparada la red para WAF/IDS.</li>\n<li>Mantener simplicidad operacional.</li>\n<li>Permitir migraci\u00f3n progresiva.</li>\n<li>No a\u00f1adir service mesh sin necesidad.</li>\n<li>No depender de BGP para el bootstrap.</li>\n</ol>\n<hr id=\"bkmrk--4\">\n<h1 id=\"bkmrk-4.-principios-de-arq\">4. Principios de arquitectura</h1>\n<h2 id=\"bkmrk-4.1-conservar-lo-que\">4.1 Conservar lo que resuelve un problema real</h2>\n<p id=\"bkmrk-ipvs-%2B-vrrp-%2B-dsr-se\">IPVS + VRRP + DSR seguir\u00e1 siendo inicialmente la capa L3/L4 WAN.</p>\n<h2 id=\"bkmrk-4.2-kubernetes-susti\">4.2 Kubernetes sustituye el service discovery artesanal</h2>\n<p id=\"bkmrk-kubernetes-ya-propor\">Kubernetes ya proporciona:</p>\n<ul id=\"bkmrk-services%3B-endpointsl\">\n<li>Services;</li>\n<li>EndpointSlices;</li>\n<li>DNS;</li>\n<li>readiness;</li>\n<li>labels;</li>\n<li>selectors.</li>\n</ul>\n<p id=\"bkmrk-no-se-recrear%C3%A1-consu\">No se recrear\u00e1 Consul dentro de Kubernetes para registrar websites.</p>\n<h2 id=\"bkmrk-4.3-capas-separadas\">4.3 Capas separadas</h2>\n<pre id=\"bkmrk-l3%2Fl4-wan--%3E-ipvs-%2F-\"><code class=\"language-text\">L3/L4 WAN          -&gt; IPVS / DSR\nHTTP/TLS           -&gt; Gateway / proxy\nCluster networking -&gt; Cilium\nFlow visibility    -&gt; Hubble\nDeep IDS futuro    -&gt; Suricata\nHTTP security      -&gt; WAF futuro\n</code></pre>\n<h2 id=\"bkmrk-4.4-egress-simple\">4.4 Egress simple</h2>\n<p id=\"bkmrk-primera-versi%C3%B3n%3A\">Primera versi\u00f3n:</p>\n<pre id=\"bkmrk-ubuntu-gateway-%2B-rou\"><code class=\"language-text\">Ubuntu gateway\n+\nrouting\n+\nnftables/iptables\n+\nWAN IP\n</code></pre>\n<h2 id=\"bkmrk-4.5-evoluci%C3%B3n-progre\">4.5 Evoluci\u00f3n progresiva</h2>\n<p id=\"bkmrk-la-base-debe-permiti\">La base debe permitir a\u00f1adir capacidades avanzadas despu\u00e9s, no obligar a desplegarlas ya.</p>\n<hr id=\"bkmrk--5\">\n<h1 id=\"bkmrk-5.-decisi%C3%B3n-de-plata\">5. Decisi\u00f3n de plataforma: RKE2</h1>\n<p id=\"bkmrk-se-selecciona-rke2-c\">Se selecciona RKE2 como distribuci\u00f3n Kubernetes principal.</p>\n<p id=\"bkmrk-razones%3A\">Razones:</p>\n<ul id=\"bkmrk-kubernetes-conforman\">\n<li>Kubernetes conformante;</li>\n<li>control plane cercano a upstream;</li>\n<li>etcd embebido;</li>\n<li>static pods;</li>\n<li>containerd;</li>\n<li>HA sencilla;</li>\n<li>soporte s\u00f3lido de CNI;</li>\n<li>lifecycle m\u00e1s f\u00e1cil que kubeadm;</li>\n<li>adecuada para plataforma productiva de largo plazo.</li>\n</ul>\n<p id=\"bkmrk-rancher-ui-no-es-req\">Rancher UI no es requisito.</p>\n<hr id=\"bkmrk--6\">\n<h1 id=\"bkmrk-6.-arquitectura-glob\">6. Arquitectura global objetivo</h1>\n<pre id=\"bkmrk-internet-%E2%94%82-%E2%94%8C%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80\"><code class=\"language-text\">                              INTERNET\n                                 \u2502\n                     \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                     \u2502                       \u2502\n                   VIP A                   VIP B\n                     \u2502                       \u2502\n                   IPVS                    IPVS\n                     \u2502                       \u2502\n                    DSR                     DSR\n                     \u2502                       \u2502\n             \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510      \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n             \u2502 RKE2 EDGE A    \u2502      \u2502 RKE2 EDGE B    \u2502\n             \u2502 proxy HTTP/TLS \u2502      \u2502 proxy HTTP/TLS \u2502\n             \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518      \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                     \u2502                       \u2502\n                     \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                                \u2502\n                          Gateway API\n                                \u2502\n                     Service / EndpointSlice\n                                \u2502\n                              Pods\n                                \u2502\n                              Ceph\n\nEGRESS:\n\nPods / Sites / Agents\n        \u2502\n        \u25bc\ninternal gateway selection\n        \u2502\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u25bc      \u25bc        \u25bc\n.100   .101     .102\n GW A   GW B     GW C\n \u2502      \u2502        \u2502\nWAN A  WAN B    WAN C\n \u2502      \u2502        \u2502\n \u2514\u2500\u2500\u2500\u2500 Internet \u2500\u2518\n</code></pre>\n<hr id=\"bkmrk--7\">\n<h1 id=\"bkmrk-7.-control-plane-rke\">7. Control plane RKE2</h1>\n<p id=\"bkmrk-baseline%3A\">Baseline:</p>\n<pre id=\"bkmrk-3-x-rke2-server-node\"><code class=\"language-text\">3 x RKE2 server nodes\n+\nembedded etcd\n</code></pre>\n<p id=\"bkmrk-modelo%3A\">Modelo:</p>\n<pre id=\"bkmrk-api-stable-endpoint-\"><code class=\"language-text\">API stable endpoint / VIP\n          \u2502\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u25bc        \u25bc        \u25bc\ncp01     cp02     cp03\netcd     etcd     etcd\n</code></pre>\n<p id=\"bkmrk-los-control-planes-d\">Los control planes deben quedar separados de los workloads de customers.</p>\n<p id=\"bkmrk-se-prioriza%3A\">Se prioriza:</p>\n<ul id=\"bkmrk-nvme%3B-baja-latencia%3B\">\n<li>NVMe;</li>\n<li>baja latencia;</li>\n<li>backups de etcd;</li>\n<li>restore probado;</li>\n<li>endpoint API estable.</li>\n</ul>\n<hr id=\"bkmrk--8\">\n<h1 id=\"bkmrk-8.-workers-y-dominio\">8. Workers y dominios de infraestructura</h1>\n<p id=\"bkmrk-se-definir%C3%A1n-roles-e\">Se definir\u00e1n roles expl\u00edcitos:</p>\n<pre id=\"bkmrk-wetopi.io%2Frole%3Dedge-\"><code class=\"language-text\">wetopi.io/role=edge\nwetopi.io/role=hosting\nwetopi.io/role=agents\nwetopi.io/role=database\nwetopi.io/role=infra\n</code></pre>\n<p id=\"bkmrk-adem%C3%A1s%2C-cuando-sea-%C3%BA\">Adem\u00e1s, cuando sea \u00fatil:</p>\n<pre id=\"bkmrk-wetopi.io%2Fproxmox-ho\"><code class=\"language-text\">wetopi.io/proxmox-host=pve01\nwetopi.io/bandwidth-domain=pve01\nwetopi.io/wan-zone=a\n</code></pre>\n<p id=\"bkmrk-el-host-proxmox-no-e\">El host Proxmox no es solo un failure domain: tambi\u00e9n puede ser un bandwidth domain.</p>\n<hr id=\"bkmrk--9\">\n<h1 id=\"bkmrk-9.-cilium-como-cni-b\">9. Cilium como CNI base</h1>\n<p id=\"bkmrk-se-selecciona-cilium\">Se selecciona Cilium como CNI.</p>\n<p id=\"bkmrk-objetivos-iniciales%3A\">Objetivos iniciales:</p>\n<ul id=\"bkmrk-red-de-pods%3B-network\">\n<li>red de pods;</li>\n<li>NetworkPolicy;</li>\n<li>observabilidad;</li>\n<li>Hubble;</li>\n<li>integraci\u00f3n con Kubernetes;</li>\n<li>base futura para capacidades avanzadas.</li>\n</ul>\n<p id=\"bkmrk-no-es-requisito-inic\">No es requisito inicial:</p>\n<ul id=\"bkmrk-sustituir-ipvs%3B-bgp-\">\n<li>sustituir IPVS;</li>\n<li>BGP WAN;</li>\n<li>gestionar todas las IPs p\u00fablicas;</li>\n<li>controlar todo egress mediante CRDs.</li>\n</ul>\n<hr id=\"bkmrk--10\">\n<h1 id=\"bkmrk-10.-hubble-como-obse\">10. Hubble como observabilidad de red</h1>\n<p id=\"bkmrk-hubble-ser%C3%A1-la-prime\">Hubble ser\u00e1 la primera capa de visibilidad de flows.</p>\n<p id=\"bkmrk-permitir%C3%A1-responder-\">Permitir\u00e1 responder preguntas como:</p>\n<ul id=\"bkmrk-qu%C3%A9-pod-abri%C3%B3-una-co\">\n<li>qu\u00e9 pod abri\u00f3 una conexi\u00f3n;</li>\n<li>qu\u00e9 site est\u00e1 haciendo egress;</li>\n<li>qu\u00e9 namespace habla con qu\u00e9 destino;</li>\n<li>qu\u00e9 tr\u00e1fico fue bloqueado;</li>\n<li>qu\u00e9 workload est\u00e1 aumentando conexiones.</li>\n</ul>\n<p id=\"bkmrk-el-objetivo-a-medio-\">El objetivo a medio plazo es correlacionar:</p>\n<pre id=\"bkmrk-pod-namespace-site-c\"><code class=\"language-text\">pod\nnamespace\nsite\ncustomer\nnode\ngateway\negress IP\ndestination\n</code></pre>\n<hr id=\"bkmrk--11\">\n<h1 id=\"bkmrk-11.-ingress-wan%3A-con\">11. Ingress WAN: conservar IPVS + VRRP + DSR</h1>\n<p id=\"bkmrk-se-conserva-el-patr%C3%B3\">Se conserva el patr\u00f3n:</p>\n<pre id=\"bkmrk-internet-%E2%86%93-vip-%E2%86%93-vrr\"><code class=\"language-text\">Internet\n \u2193\nVIP\n \u2193\nVRRP\n \u2193\nIPVS\n \u2193\nDSR\n \u2193\nedge node\n</code></pre>\n<h2 id=\"bkmrk-por-qu%C3%A9\">Por qu\u00e9</h2>\n<p id=\"bkmrk-con-dsr%3A\">Con DSR:</p>\n<pre id=\"bkmrk-request%3A-client--%3E-i\"><code class=\"language-text\">request:\nclient -&gt; IPVS -&gt; edge\n\nresponse:\nedge -&gt; client\n</code></pre>\n<p id=\"bkmrk-la-respuesta-no-vuel\">La respuesta no vuelve por el director.</p>\n<p id=\"bkmrk-esto-preserva-la-cap\">Esto preserva la capacidad de salida distribuida entre hosts f\u00edsicos OVH.</p>\n<hr id=\"bkmrk--12\">\n<h1 id=\"bkmrk-12.-edge-nodes-rke2\">12. Edge nodes RKE2</h1>\n<p id=\"bkmrk-los-antiguos-wan-pro\">Los antiguos WAN proxies pasar\u00e1n progresivamente a edge workers RKE2.</p>\n<p id=\"bkmrk-ejemplo%3A\">Ejemplo:</p>\n<pre id=\"bkmrk-edge-a01-edge-a02-ed\"><code class=\"language-text\">edge-a01\nedge-a02\nedge-b01\nedge-b02\n</code></pre>\n<p id=\"bkmrk-cada-edge-vm-debe-es\">Cada edge VM debe estar distribuida deliberadamente entre hosts Proxmox distintos.</p>\n<p id=\"bkmrk-labels-ejemplo%3A\">Labels ejemplo:</p>\n<pre id=\"bkmrk-wetopi.io%2Frole%3Dedge--1\"><code class=\"language-text\">wetopi.io/role=edge\nwetopi.io/wan-zone=a\nwetopi.io/proxmox-host=pve01\n</code></pre>\n<p id=\"bkmrk-taint-recomendado%3A\">Taint recomendado:</p>\n<pre id=\"bkmrk-wetopi.io%2Fedge%3Dtrue%3A\"><code class=\"language-text\">wetopi.io/edge=true:NoSchedule\n</code></pre>\n<hr id=\"bkmrk--13\">\n<h1 id=\"bkmrk-13.-proxy-http%2Ftls-y\">13. Proxy HTTP/TLS y Gateway API</h1>\n<p id=\"bkmrk-el-edge-seguir%C3%A1-term\">El edge seguir\u00e1 terminando TLS.</p>\n<p id=\"bkmrk-candidatos%3A\">Candidatos:</p>\n<ul id=\"bkmrk-traefik%3B-envoy.\">\n<li>Traefik;</li>\n<li>Envoy.</li>\n</ul>\n<p id=\"bkmrk-la-arquitectura-no-d\">La arquitectura no debe depender todav\u00eda de la elecci\u00f3n final.</p>\n<p id=\"bkmrk-responsabilidades%3A\">Responsabilidades:</p>\n<ul id=\"bkmrk-tls%3B-routing-http%3B-f\">\n<li>TLS;</li>\n<li>routing HTTP;</li>\n<li>forwarding de IP real;</li>\n<li>headers;</li>\n<li>m\u00e9tricas;</li>\n<li>readiness;</li>\n<li>futura integraci\u00f3n WAF.</li>\n</ul>\n<p id=\"bkmrk-routing%3A\">Routing:</p>\n<pre id=\"bkmrk-hostname-%E2%86%93-httproute\"><code class=\"language-text\">hostname\n \u2193\nHTTPRoute\n \u2193\nService\n \u2193\nEndpointSlice\n \u2193\nPod\n</code></pre>\n<hr id=\"bkmrk--14\">\n<h1 id=\"bkmrk-14.-sustituci%C3%B3n-de-c\">14. Sustituci\u00f3n de Consul, Registrator y consul-template</h1>\n<p id=\"bkmrk-antes%3A\">ANTES:</p>\n<pre id=\"bkmrk-container-%E2%86%93-registra\"><code class=\"language-text\">container\n \u2193\nRegistrator\n \u2193\nConsul\n \u2193\nconsul-template\n \u2193\nnginx.conf\n \u2193\nreload\n</code></pre>\n<p id=\"bkmrk-nuevo%3A\">NUEVO:</p>\n<pre id=\"bkmrk-pod-%E2%86%93-service-select\"><code class=\"language-text\">Pod\n \u2193\nService selector\n \u2193\nEndpointSlice\n \u2193\nGateway controller\n \u2193\ndynamic route\n</code></pre>\n<p id=\"bkmrk-consul-puede-seguir-\">Consul puede seguir existiendo para otros usos si hubiera necesidad, pero deja de ser necesario como service registry del hosting web.</p>\n<hr id=\"bkmrk--15\">\n<h1 id=\"bkmrk-15.-modelo-inicial-d\">15. Modelo inicial de WordPress workloads</h1>\n<p id=\"bkmrk-no-se-recomienda-ref\">No se recomienda refactorizar simult\u00e1neamente todo el <code>wepod</code>.</p>\n<p id=\"bkmrk-primera-migraci%C3%B3n%3A\">Primera migraci\u00f3n:</p>\n<pre id=\"bkmrk-docker-wepod-%E2%86%93-kuber\"><code class=\"language-text\">Docker wepod\n    \u2193\nKubernetes Pod / StatefulSet\n</code></pre>\n<p id=\"bkmrk-manteniendo-inicialm\">manteniendo inicialmente:</p>\n<pre id=\"bkmrk-s6-nginx-php-fpm-mys\"><code class=\"language-text\">s6\nnginx\nphp-fpm\nmysql\n</code></pre>\n<p id=\"bkmrk-cuando-sea-pr%C3%A1ctico.\">cuando sea pr\u00e1ctico.</p>\n<p id=\"bkmrk-la-separaci%C3%B3n-poster\">La separaci\u00f3n posterior de database/frontend debe tratarse como proyecto independiente.</p>\n<hr id=\"bkmrk--16\">\n<h1 id=\"bkmrk-16.-egress%3A-dise%C3%B1o-s\">16. Egress: dise\u00f1o simple de NAT Gateways</h1>\n<p id=\"bkmrk-primera-versi%C3%B3n%3A-1\">Primera versi\u00f3n:</p>\n<pre id=\"bkmrk-gw100-lan%3A-192.168.8\"><code class=\"language-text\">gw100\n  LAN: 192.168.88.100\n  WAN: PUBLIC_IP_A\n\ngw101\n  LAN: 192.168.88.101\n  WAN: PUBLIC_IP_B\n\ngw102\n  LAN: 192.168.88.102\n  WAN: PUBLIC_IP_C\n</code></pre>\n<p id=\"bkmrk-cada-gateway-ser%C3%A1-un\">Cada gateway ser\u00e1 una VM Ubuntu sencilla.</p>\n<p id=\"bkmrk-funciones%3A\">Funciones:</p>\n<ul id=\"bkmrk-routing%3B-forwarding%3B\">\n<li>routing;</li>\n<li>forwarding;</li>\n<li>SNAT/MASQUERADE;</li>\n<li>logging;</li>\n<li>health;</li>\n<li>m\u00e9tricas b\u00e1sicas.</li>\n</ul>\n<p id=\"bkmrk-no-necesita-initiall\">No necesita initially un controller Kubernetes.</p>\n<hr id=\"bkmrk--17\">\n<h1 id=\"bkmrk-17.-asignaci%C3%B3n-de-cl\">17. Asignaci\u00f3n de clientes a gateways de salida</h1>\n<p id=\"bkmrk-modelo%3A-1\">Modelo:</p>\n<pre id=\"bkmrk-site%2Fcustomer-%E2%86%93-egre\"><code class=\"language-text\">site/customer\n \u2193\negress class\n \u2193\ngateway interno\n</code></pre>\n<p id=\"bkmrk-ejemplo%3A-1\">Ejemplo:</p>\n<pre id=\"bkmrk-site-1001--%3E-192.168\"><code class=\"language-text\">site-1001 -&gt; 192.168.88.100\nsite-1002 -&gt; 192.168.88.100\nsite-2001 -&gt; 192.168.88.101\ncrawler   -&gt; 192.168.88.102\n</code></pre>\n<p id=\"bkmrk-la-implementaci%C3%B3n-pu\">La implementaci\u00f3n puede empezar con routing/policy routing y evolucionar despu\u00e9s.</p>\n<p id=\"bkmrk-lo-importante-es-que\">Lo importante es que sea:</p>\n<ul id=\"bkmrk-determinista%3B-observ\">\n<li>determinista;</li>\n<li>observable;</li>\n<li>f\u00e1cil de cambiar.</li>\n</ul>\n<hr id=\"bkmrk--18\">\n<h1 id=\"bkmrk-18.-rotaci%C3%B3n-y-cambi\">18. Rotaci\u00f3n y cambio de IPs de salida</h1>\n<p id=\"bkmrk-caso-1%3A\">Caso 1:</p>\n<pre id=\"bkmrk-gw100%3A-public_ip_a-%E2%86%93\"><code class=\"language-text\">gw100:\nPUBLIC_IP_A\n  \u2193\nPUBLIC_IP_B\n</code></pre>\n<p id=\"bkmrk-caso-2%3A\">Caso 2:</p>\n<pre id=\"bkmrk-customer%2Fsite-gw100-\"><code class=\"language-text\">customer/site\ngw100\n  \u2193\ngw101\n</code></pre>\n<p id=\"bkmrk-los-workloads-no-deb\">Los workloads no deben necesitar conocer la WAN IP.</p>\n<p id=\"bkmrk-solo-la-pol%C3%ADtica%2Frut\">Solo la pol\u00edtica/ruta de salida.</p>\n<hr id=\"bkmrk--19\">\n<h1 id=\"bkmrk-19.-separaci%C3%B3n-de-re\">19. Separaci\u00f3n de reputaci\u00f3n de IPs</h1>\n<p id=\"bkmrk-desde-el-principio-s\">Desde el principio se recomienda separar:</p>\n<pre id=\"bkmrk-hosting-general-host\"><code class=\"language-text\">hosting-general\nhosting-premium\nplatform\nagents/crawlers\n</code></pre>\n<p id=\"bkmrk-ejemplo%3A-2\">Ejemplo:</p>\n<pre id=\"bkmrk-192.168.88.100--%3E-sh\"><code class=\"language-text\">192.168.88.100\n -&gt; shared hosting\n -&gt; 51.x.x.10\n\n192.168.88.101\n -&gt; premium/dedicated pool\n -&gt; 51.x.x.11\n\n192.168.88.102\n -&gt; agents/crawlers\n -&gt; 51.x.x.12\n</code></pre>\n<p id=\"bkmrk-los-crawlers-no-debe\">Los crawlers no deben compartir IP con customer hosting.</p>\n<hr id=\"bkmrk--20\">\n<h1 id=\"bkmrk-20.-ingress-ip-dedic\">20. Ingress IP dedicada por cliente</h1>\n<p id=\"bkmrk-no-es-prioridad-inic\">No es prioridad inicial.</p>\n<p id=\"bkmrk-el-sistema-debe-perm\">El sistema debe permitir en el futuro:</p>\n<pre id=\"bkmrk-customer-premium-%E2%86%93-d\"><code class=\"language-text\">customer premium\n  \u2193\ndedicated public VIP\n  \u2193\nIPVS / DSR\n  \u2193\nedge\n  \u2193\nHTTPRoute\n</code></pre>\n<p id=\"bkmrk-no-requiere-redise%C3%B1a\">No requiere redise\u00f1ar el backend Kubernetes.</p>\n<hr id=\"bkmrk--21\">\n<h1 id=\"bkmrk-21.-integraci%C3%B3n-con-\">21. Integraci\u00f3n con OVH vRack</h1>\n<p id=\"bkmrk-vrack-seguir%C3%A1-siendo\">vRack seguir\u00e1 siendo la red privada base.</p>\n<p id=\"bkmrk-el-dise%C3%B1o-inicial-no\">El dise\u00f1o inicial no depende de BGP.</p>\n<p id=\"bkmrk-los-bloques-addition\">Los bloques Additional IP podr\u00e1n utilizarse para:</p>\n<ul id=\"bkmrk-vips%3B-ips-de-natgw%3B-\">\n<li>VIPs;</li>\n<li>IPs de NATGW;</li>\n<li>futuras IPs dedicadas.</li>\n</ul>\n<p id=\"bkmrk-bgp-service-de-ovh-p\">BGP Service de OVH podr\u00e1 evaluarse cuando sea suficientemente maduro para producci\u00f3n.</p>\n<hr id=\"bkmrk--22\">\n<h1 id=\"bkmrk-22.-proxmox-y-agrega\">22. Proxmox y agregaci\u00f3n de ancho de banda</h1>\n<p id=\"bkmrk-la-distribuci%C3%B3n-de-e\">La distribuci\u00f3n de edge nodes debe respetar la topolog\u00eda f\u00edsica.</p>\n<p id=\"bkmrk-ejemplo%3A-3\">Ejemplo:</p>\n<pre id=\"bkmrk-pve01--%3E-edge01-pve0\"><code class=\"language-text\">pve01 -&gt; edge01\npve02 -&gt; edge02\npve03 -&gt; edge03\n</code></pre>\n<p id=\"bkmrk-evitar%3A\">Evitar:</p>\n<pre id=\"bkmrk-pve01--%3E-edge01-%2B-ed\"><code class=\"language-text\">pve01 -&gt; edge01 + edge02 + edge03\n</code></pre>\n<p id=\"bkmrk-cuando-el-objetivo-e\">cuando el objetivo es sumar failure/bandwidth domains.</p>\n<hr id=\"bkmrk--23\">\n<h1 id=\"bkmrk-23.-storage%3A-ceph\">23. Storage: Ceph</h1>\n<p id=\"bkmrk-ceph-seguir%C3%A1-siendo-\">Ceph seguir\u00e1 siendo el storage principal.</p>\n<p id=\"bkmrk-rke2-consumir%C3%A1-stora\">RKE2 consumir\u00e1 storage mediante CSI.</p>\n<p id=\"bkmrk-objetivos%3A\">Objetivos:</p>\n<ul id=\"bkmrk-pvc-nativo%3B-storagec\">\n<li>PVC nativo;</li>\n<li>StorageClasses;</li>\n<li>snapshots donde sea \u00fatil;</li>\n<li>evitar dependencias de Docker volumes espec\u00edficos.</li>\n</ul>\n<hr id=\"bkmrk--24\">\n<h1 id=\"bkmrk-24.-observabilidad-y\">24. Observabilidad y seguridad de red</h1>\n<p id=\"bkmrk-primera-capa%3A\">Primera capa:</p>\n<pre id=\"bkmrk-cilium-%E2%86%93-hubble\"><code class=\"language-text\">Cilium\n \u2193\nHubble\n</code></pre>\n<p id=\"bkmrk-m%C3%A1s-adelante%3A\">M\u00e1s adelante:</p>\n<pre id=\"bkmrk-hubble-flows-%2B-edge-\"><code class=\"language-text\">Hubble flows\n+\nedge HTTP logs\n+\nNATGW logs\n+\nSuricata\n+\nWAF\n</code></pre>\n<p id=\"bkmrk-todos-podr%C3%A1n-converg\">Todos podr\u00e1n converger en la plataforma de observabilidad.</p>\n<hr id=\"bkmrk--25\">\n<h1 id=\"bkmrk-25.-suricata-y-waf-f\">25. Suricata y WAF futuros</h1>\n<p id=\"bkmrk-no-son-requisito-de-\">No son requisito de bootstrap.</p>\n<h2 id=\"bkmrk-suricata\">Suricata</h2>\n<p id=\"bkmrk-%C3%9Atil-para%3A\">\u00datil para:</p>\n<ul id=\"bkmrk-ids%3B-scans%3B-anomal%C3%ADa\">\n<li>IDS;</li>\n<li>scans;</li>\n<li>anomal\u00edas;</li>\n<li>C2;</li>\n<li>DNS;</li>\n<li>metadata de protocolos.</li>\n</ul>\n<p id=\"bkmrk-posibles-puntos%3A\">Posibles puntos:</p>\n<ul id=\"bkmrk-edge%3B-egress-gateway\">\n<li>edge;</li>\n<li>egress gateway;</li>\n<li>TAP/mirror.</li>\n</ul>\n<h2 id=\"bkmrk-waf\">WAF</h2>\n<p id=\"bkmrk-debe-operar-despu%C3%A9s-\">Debe operar despu\u00e9s de TLS termination para ver HTTP.</p>\n<pre id=\"bkmrk-internet-%E2%86%93-tls-termi\"><code class=\"language-text\">Internet\n \u2193\nTLS termination\n \u2193\nWAF\n \u2193\nbackend\n</code></pre>\n<hr id=\"bkmrk--26\">\n<h1 id=\"bkmrk-26.-qu%C3%A9-no-vamos-a-d\">26. Qu\u00e9 NO vamos a desplegar inicialmente</h1>\n<p id=\"bkmrk-no-se-considera-nece\">No se considera necesario de salida:</p>\n<ul id=\"bkmrk-istio%3B-service-mesh-\">\n<li>Istio;</li>\n<li>service mesh global;</li>\n<li>BGP WAN;</li>\n<li>Cilium BGP externo;</li>\n<li>controlador propio de egress;</li>\n<li>IP reputation controller;</li>\n<li>IPAM p\u00fablico din\u00e1mico;</li>\n<li>Suricata inline global;</li>\n<li>WAF sofisticado distribuido;</li>\n<li>CRDs propios para toda la red.</li>\n</ul>\n<p id=\"bkmrk-la-ausencia-de-estas\">La ausencia de estas piezas es deliberada.</p>\n<hr id=\"bkmrk--27\">\n<h1 id=\"bkmrk-27.-modelo-de-alta-d\">27. Modelo de alta disponibilidad</h1>\n<h2 id=\"bkmrk-control-plane\">Control plane</h2>\n<pre id=\"bkmrk-3-x-rke2-server\"><code class=\"language-text\">3 x RKE2 server\n</code></pre>\n<h2 id=\"bkmrk-ingress\">Ingress</h2>\n<pre id=\"bkmrk-vrrp-%2B-ipvs-%2B-varios\"><code class=\"language-text\">VRRP\n+\nIPVS\n+\nvarios edge nodes\n+\nDSR\n</code></pre>\n<h2 id=\"bkmrk-egress\">Egress</h2>\n<p id=\"bkmrk-varios-gateways%3A\">Varios gateways:</p>\n<pre id=\"bkmrk-gw100-gw101-gw102\"><code class=\"language-text\">gw100\ngw101\ngw102\n</code></pre>\n<p id=\"bkmrk-la-ha-de-una-ip-dedi\">La HA de una IP dedicada concreta podr\u00e1 implementarse cuando exista necesidad.</p>\n<p id=\"bkmrk-no-es-necesario-reso\">No es necesario resolver desde el primer d\u00eda todos los casos premium.</p>\n<hr id=\"bkmrk--28\">\n<h1 id=\"bkmrk-28.-mantenimiento-y-\">28. Mantenimiento y draining de edge nodes</h1>\n<p id=\"bkmrk-procedimiento-recome\">Procedimiento recomendado:</p>\n<pre id=\"bkmrk-1.-sacar-edge-de-ipv\"><code class=\"language-text\">1. sacar edge de IPVS\n2. dejar de enviar nuevas conexiones\n3. esperar conexiones existentes\n4. kubectl drain\n5. mantenimiento\n6. kubectl uncordon\n7. validar proxy\n8. reinsertar en IPVS\n</code></pre>\n<p id=\"bkmrk-despu%C3%A9s-podr%C3%A1-automa\">Despu\u00e9s podr\u00e1 automatizarse.</p>\n<hr id=\"bkmrk--29\">\n<h1 id=\"bkmrk-29.-modelo-de-namesp\">29. Modelo de namespaces y roles de nodos</h1>\n<p id=\"bkmrk-namespaces-orientati\">Namespaces orientativos:</p>\n<pre id=\"bkmrk-wetopi-edge-wetopi-h\"><code class=\"language-text\">wetopi-edge\nwetopi-hosting\nwetopi-agents\nwetopi-platform\nwetopi-observability\nwetopi-security\n</code></pre>\n<p id=\"bkmrk-roles-de-nodo%3A\">Roles de nodo:</p>\n<pre id=\"bkmrk-control-plane-edge-h\"><code class=\"language-text\">control-plane\nedge\nhosting\nagents\ninfra\n</code></pre>\n<p id=\"bkmrk-se-utilizar%C3%A1n%3A\">Se utilizar\u00e1n:</p>\n<ul id=\"bkmrk-labels%3B-taints%3B-affi\">\n<li>labels;</li>\n<li>taints;</li>\n<li>affinity;</li>\n<li>topology spread.</li>\n</ul>\n<hr id=\"bkmrk--30\">\n<h1 id=\"bkmrk-30.-flujos-de-tr%C3%A1fic\">30. Flujos de tr\u00e1fico de ejemplo</h1>\n<h2 id=\"bkmrk-entrada-http\">Entrada HTTP</h2>\n<pre id=\"bkmrk-client-%E2%86%93-vip-%E2%86%93-ipvs-\"><code class=\"language-text\">Client\n \u2193\nVIP\n \u2193\nIPVS\n \u2193 DSR\nedge02\n \u2193\nTLS\n \u2193\nHTTPRoute\n \u2193\nService\n \u2193\nsite pod\n</code></pre>\n<p id=\"bkmrk-respuesta%3A\">Respuesta:</p>\n<pre id=\"bkmrk-edge02-%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80%E2%94%80\"><code class=\"language-text\">edge02\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25ba Client\n</code></pre>\n<h2 id=\"bkmrk-salida-shared\">Salida shared</h2>\n<pre id=\"bkmrk-site-1001-%E2%86%93-192.168.\"><code class=\"language-text\">site-1001\n \u2193\n192.168.88.100\n \u2193\nSNAT\n \u2193\n51.x.x.10\n \u2193\nInternet\n</code></pre>\n<h2 id=\"bkmrk-salida-premium\">Salida premium</h2>\n<pre id=\"bkmrk-site-2001-%E2%86%93-192.168.\"><code class=\"language-text\">site-2001\n \u2193\n192.168.88.101\n \u2193\nSNAT\n \u2193\n51.x.x.11\n</code></pre>\n<h2 id=\"bkmrk-crawler\">Crawler</h2>\n<pre id=\"bkmrk-agent-crawler-%E2%86%93-192.\"><code class=\"language-text\">agent-crawler\n \u2193\n192.168.88.102\n \u2193\n51.x.x.12\n</code></pre>\n<h2 id=\"bkmrk-rotaci%C3%B3n\">Rotaci\u00f3n</h2>\n<pre id=\"bkmrk-customer-x-%E2%86%93-gw100\"><code class=\"language-text\">customer X\n \u2193\ngw100\n</code></pre>\n<p id=\"bkmrk-pasa-a%3A\">pasa a:</p>\n<pre id=\"bkmrk-customer-x-%E2%86%93-gw101\"><code class=\"language-text\">customer X\n \u2193\ngw101\n</code></pre>\n<p id=\"bkmrk-sin-mover-el-workloa\">sin mover el workload.</p>\n<hr id=\"bkmrk--31\">\n<h1 id=\"bkmrk-31.-migraci%C3%B3n-progre\">31. Migraci\u00f3n progresiva desde Docker Swarm</h1>\n<p id=\"bkmrk-no-big-bang.\">No big-bang.</p>\n<p id=\"bkmrk-durante-la-transici%C3%B3\">Durante la transici\u00f3n:</p>\n<pre id=\"bkmrk-ipvs-%E2%94%9C%E2%94%80%E2%94%80-old-wan--%3E-\"><code class=\"language-text\">IPVS\n \u251c\u2500\u2500 old WAN -&gt; Swarm\n \u2514\u2500\u2500 new edge -&gt; RKE2\n</code></pre>\n<p id=\"bkmrk-pueden-coexistir.\">pueden coexistir.</p>\n<p id=\"bkmrk-migraci%C3%B3n%3A\">Migraci\u00f3n:</p>\n<pre id=\"bkmrk-site-1-site-2-site-3\"><code class=\"language-text\">site 1\nsite 2\nsite 3\n...\n</code></pre>\n<p id=\"bkmrk-de-forma-progresiva.\">de forma progresiva.</p>\n<p id=\"bkmrk-idealmente-la-vip-p%C3%BA\">Idealmente la VIP p\u00fablica permanece y el routing decide el backend.</p>\n<hr id=\"bkmrk--32\">\n<h1 id=\"bkmrk-32.-fases-de-impleme\">32. Fases de implementaci\u00f3n</h1>\n<h2 id=\"bkmrk-fase-0-%E2%80%94-inventario\">Fase 0 \u2014 Inventario</h2>\n<p id=\"bkmrk-documentar%3A\">Documentar:</p>\n<ul id=\"bkmrk-vips%3B-ipvs%3B-vrrp%3B-vl\">\n<li>VIPs;</li>\n<li>IPVS;</li>\n<li>VRRP;</li>\n<li>VLANs;</li>\n<li>vRack;</li>\n<li>Proxmox;</li>\n<li>WAN bandwidth domains;</li>\n<li>NATGW;</li>\n<li>Ceph.</li>\n</ul>\n<h2 id=\"bkmrk-fase-1-%E2%80%94-rke2\">Fase 1 \u2014 RKE2</h2>\n<ul id=\"bkmrk-3-control-planes%3B-wo\">\n<li>3 control planes;</li>\n<li>workers;</li>\n<li>embedded etcd;</li>\n<li>API VIP;</li>\n<li>GitOps;</li>\n<li>Ceph CSI.</li>\n</ul>\n<h2 id=\"bkmrk-fase-2-%E2%80%94-cilium%2Fhubb\">Fase 2 \u2014 Cilium/Hubble</h2>\n<ul id=\"bkmrk-cilium%3B-hubble%3B-prim\">\n<li>Cilium;</li>\n<li>Hubble;</li>\n<li>primeras NetworkPolicies;</li>\n<li>flow visibility.</li>\n</ul>\n<h2 id=\"bkmrk-fase-3-%E2%80%94-edge\">Fase 3 \u2014 Edge</h2>\n<ul id=\"bkmrk-edge-workers%3B-proxy-\">\n<li>edge workers;</li>\n<li>proxy DaemonSet;</li>\n<li>DSR;</li>\n<li>health checks;</li>\n<li>integraci\u00f3n con IPVS.</li>\n</ul>\n<h2 id=\"bkmrk-fase-4-%E2%80%94-gateway-api\">Fase 4 \u2014 Gateway API</h2>\n<ul id=\"bkmrk-gateway%3B-httproute%3B-\">\n<li>Gateway;</li>\n<li>HTTPRoute;</li>\n<li>Services;</li>\n<li>EndpointSlices.</li>\n</ul>\n<h2 id=\"bkmrk-fase-5-%E2%80%94-primeros-si\">Fase 5 \u2014 Primeros sites</h2>\n<p id=\"bkmrk-validar%3A\">Validar:</p>\n<ul id=\"bkmrk-tls%3B-ip-cliente%3B-hea\">\n<li>TLS;</li>\n<li>IP cliente;</li>\n<li>headers;</li>\n<li>storage;</li>\n<li>health;</li>\n<li>performance;</li>\n<li>egress.</li>\n</ul>\n<h2 id=\"bkmrk-fase-6-%E2%80%94-natgw-simpl\">Fase 6 \u2014 NATGW simples</h2>\n<p id=\"bkmrk-desplegar%3A\">Desplegar:</p>\n<pre id=\"bkmrk-192.168.88.100-192.1\"><code class=\"language-text\">192.168.88.100\n192.168.88.101\n192.168.88.102\n</code></pre>\n<p id=\"bkmrk-con-wan-ips-separada\">con WAN IPs separadas.</p>\n<h2 id=\"bkmrk-fase-7-%E2%80%94-migraci%C3%B3n-m\">Fase 7 \u2014 Migraci\u00f3n masiva</h2>\n<p id=\"bkmrk-mover-progresivament\">Mover progresivamente el grueso de los workloads.</p>\n<h2 id=\"bkmrk-fase-8-%E2%80%94-observabili\">Fase 8 \u2014 Observabilidad avanzada</h2>\n<p id=\"bkmrk-enriquecimiento-de-f\">Enriquecimiento de flows y dashboards.</p>\n<h2 id=\"bkmrk-fase-9-%E2%80%94-seguridad-a\">Fase 9 \u2014 Seguridad avanzada opcional</h2>\n<ul id=\"bkmrk-suricata%3B-waf%3B-egres\">\n<li>Suricata;</li>\n<li>WAF;</li>\n<li>egress automation;</li>\n<li>IP reputation;</li>\n<li>dedicated IP automation.</li>\n</ul>\n<hr id=\"bkmrk--33\">\n<h1 id=\"bkmrk-33.-decisiones-conso\">33. Decisiones consolidadas</h1>\n<h2 id=\"bkmrk-net-001\">NET-001</h2>\n<p id=\"bkmrk-rke2-ser%C3%A1-la-distrib\">RKE2 ser\u00e1 la distribuci\u00f3n Kubernetes.</p>\n<h2 id=\"bkmrk-net-002\">NET-002</h2>\n<p id=\"bkmrk-cilium-ser%C3%A1-el-cni-i\">Cilium ser\u00e1 el CNI inicial.</p>\n<h2 id=\"bkmrk-net-003\">NET-003</h2>\n<p id=\"bkmrk-hubble-ser%C3%A1-la-base-\">Hubble ser\u00e1 la base de network observability.</p>\n<h2 id=\"bkmrk-net-004\">NET-004</h2>\n<p id=\"bkmrk-ipvs-%2B-vrrp-%2B-dsr-se-1\">IPVS + VRRP + DSR se mantienen inicialmente.</p>\n<h2 id=\"bkmrk-net-005\">NET-005</h2>\n<p id=\"bkmrk-los-edge-nodes-segui\">Los edge nodes seguir\u00e1n repartidos por hosts f\u00edsicos Proxmox.</p>\n<h2 id=\"bkmrk-net-006\">NET-006</h2>\n<p id=\"bkmrk-registrator-deja-de-\">Registrator deja de ser necesario.</p>\n<h2 id=\"bkmrk-net-007\">NET-007</h2>\n<p id=\"bkmrk-consul-deja-de-ser-e\">Consul deja de ser el registry de websites.</p>\n<h2 id=\"bkmrk-net-008\">NET-008</h2>\n<p id=\"bkmrk-consul-template-deja\">consul-template deja de generar la configuraci\u00f3n HTTP de RKE2.</p>\n<h2 id=\"bkmrk-net-009\">NET-009</h2>\n<p id=\"bkmrk-gateway-api-ser%C3%A1-la-\">Gateway API ser\u00e1 la abstracci\u00f3n de routing HTTP.</p>\n<h2 id=\"bkmrk-net-010\">NET-010</h2>\n<p id=\"bkmrk-traefik-o-envoy-actu\">Traefik o Envoy actuar\u00e1n como proxy edge.</p>\n<h2 id=\"bkmrk-net-011\">NET-011</h2>\n<p id=\"bkmrk-el-egress-inicial-us\">El egress inicial usar\u00e1 NATGW Ubuntu simples.</p>\n<h2 id=\"bkmrk-net-012\">NET-012</h2>\n<p id=\"bkmrk-cada-natgw-tendr%C3%A1-un\">Cada NATGW tendr\u00e1 una IP interna estable.</p>\n<p id=\"bkmrk-ejemplo%3A-4\">Ejemplo:</p>\n<pre id=\"bkmrk-192.168.88.100-192.1-1\"><code class=\"language-text\">192.168.88.100\n192.168.88.101\n192.168.88.102\n</code></pre>\n<h2 id=\"bkmrk-net-013\">NET-013</h2>\n<p id=\"bkmrk-la-wan-ip-asociada-a\">La WAN IP asociada a un gateway debe ser f\u00e1cil de cambiar.</p>\n<h2 id=\"bkmrk-net-014\">NET-014</h2>\n<p id=\"bkmrk-agents%2Fcrawlers-no-c\">Agents/crawlers no compartir\u00e1n IP de salida con customers.</p>\n<h2 id=\"bkmrk-net-015\">NET-015</h2>\n<p id=\"bkmrk-se-podr%C3%A1-asignar-un-\">Se podr\u00e1 asignar un cliente a un gateway espec\u00edfico.</p>\n<h2 id=\"bkmrk-net-016\">NET-016</h2>\n<p id=\"bkmrk-no-se-desplegar%C3%A1-ist\">No se desplegar\u00e1 Istio inicialmente.</p>\n<h2 id=\"bkmrk-net-017\">NET-017</h2>\n<p id=\"bkmrk-el-bootstrap-no-depe\">El bootstrap no depender\u00e1 de BGP.</p>\n<h2 id=\"bkmrk-net-018\">NET-018</h2>\n<p id=\"bkmrk-no-se-construir%C3%A1-ini\">No se construir\u00e1 inicialmente un egress control plane complejo si gateways est\u00e1ticos resuelven el problema.</p>\n<hr id=\"bkmrk--34\">\n<h1 id=\"bkmrk-34.-riesgos-principa\">34. Riesgos principales y mitigaciones</h1>\n<h2 id=\"bkmrk-cambio-simult%C3%A1neo-ex\">Cambio simult\u00e1neo excesivo</h2>\n<p id=\"bkmrk-mitigaci%C3%B3n%3A\">Mitigaci\u00f3n:</p>\n<ul id=\"bkmrk-migraci%C3%B3n-por-capas%3B\">\n<li>migraci\u00f3n por capas;</li>\n<li>conservar IPVS;</li>\n<li>conservar inicialmente wepod.</li>\n</ul>\n<h2 id=\"bkmrk-p%C3%A9rdida-accidental-d\">P\u00e9rdida accidental de DSR</h2>\n<p id=\"bkmrk-mitigaci%C3%B3n%3A-1\">Mitigaci\u00f3n:</p>\n<ul id=\"bkmrk-edge-nodes-dedicados\">\n<li>edge nodes dedicados;</li>\n<li>pruebas expl\u00edcitas de dataplane.</li>\n</ul>\n<h2 id=\"bkmrk-mala-distribuci%C3%B3n-f%C3%AD\">Mala distribuci\u00f3n f\u00edsica</h2>\n<p id=\"bkmrk-mitigaci%C3%B3n%3A-2\">Mitigaci\u00f3n:</p>\n<ul id=\"bkmrk-labels-de-proxmox-ho\">\n<li>labels de Proxmox host;</li>\n<li>anti-affinity;</li>\n<li>topology awareness.</li>\n</ul>\n<h2 id=\"bkmrk-natgw-como-punto-de-\">NATGW como punto de fallo</h2>\n<p id=\"bkmrk-mitigaci%C3%B3n%3A-3\">Mitigaci\u00f3n:</p>\n<ul id=\"bkmrk-varios-gateways%3B-rea\">\n<li>varios gateways;</li>\n<li>reasignaci\u00f3n sencilla;</li>\n<li>HA premium cuando se requiera.</li>\n</ul>\n<h2 id=\"bkmrk-reputaci%C3%B3n-compartid\">Reputaci\u00f3n compartida</h2>\n<p id=\"bkmrk-mitigaci%C3%B3n%3A-4\">Mitigaci\u00f3n:</p>\n<ul id=\"bkmrk-separar-pools-desde-\">\n<li>separar pools desde el principio.</li>\n</ul>\n<h2 id=\"bkmrk-overengineering\">Overengineering</h2>\n<p id=\"bkmrk-mitigaci%C3%B3n%3A-5\">Mitigaci\u00f3n:</p>\n<ul id=\"bkmrk-no-desplegar-capacid\">\n<li>no desplegar capacidades avanzadas sin necesidad operacional concreta.</li>\n</ul>\n<hr id=\"bkmrk--35\">\n<h1 id=\"bkmrk-35.-evoluci%C3%B3n-futura\">35. Evoluci\u00f3n futura</h1>\n<p id=\"bkmrk-una-vez-estable-la-b\">Una vez estable la base se podr\u00e1 evaluar:</p>\n<h2 id=\"bkmrk-cilium-egress-gatewa\">Cilium Egress Gateway</h2>\n<p id=\"bkmrk-para-seleccionar-egr\">Para seleccionar egress por labels de Kubernetes.</p>\n<h2 id=\"bkmrk-egress-pools\">Egress pools</h2>\n<pre id=\"bkmrk-shared-premium-crawl\"><code class=\"language-text\">shared\npremium\ncrawler\ndedicated\n</code></pre>\n<h2 id=\"bkmrk-reputation-monitorin\">Reputation monitoring</h2>\n<p id=\"bkmrk-medici%C3%B3n-y-rotaci%C3%B3n-\">Medici\u00f3n y rotaci\u00f3n de IPs.</p>\n<h2 id=\"bkmrk-suricata-1\">Suricata</h2>\n<p id=\"bkmrk-ids%2Fips-de-red.\">IDS/IPS de red.</p>\n<h2 id=\"bkmrk-waf-1\">WAF</h2>\n<p id=\"bkmrk-inspecci%C3%B3n-http-tras\">Inspecci\u00f3n HTTP tras TLS.</p>\n<h2 id=\"bkmrk-bgp\">BGP</h2>\n<p id=\"bkmrk-cuando-ovh-ofrezca-u\">Cuando OVH ofrezca una opci\u00f3n madura y productiva.</p>\n<h2 id=\"bkmrk-eliminaci%C3%B3n-de-ipvs\">Eliminaci\u00f3n de IPVS</h2>\n<p id=\"bkmrk-solo-si-una-alternat\">Solo si una alternativa BGP/ECMP conserva:</p>\n<ul id=\"bkmrk-ha%3B-dsr%2Fequivalente%3B\">\n<li>HA;</li>\n<li>DSR/equivalente;</li>\n<li>capacidad;</li>\n<li>simplicidad.</li>\n</ul>\n<h2 id=\"bkmrk-wordpresssite-crd\">WordPressSite CRD</h2>\n<p id=\"bkmrk-en-una-fase-posterio\">En una fase posterior cada website podr\u00eda representarse como:</p>\n<pre id=\"bkmrk-apiversion%3A-wetopi.i\"><code class=\"language-yaml\">apiVersion: wetopi.io/v1\nkind: WordPressSite\n</code></pre>\n<p id=\"bkmrk-y-generar-autom%C3%A1tica\">y generar autom\u00e1ticamente:</p>\n<ul id=\"bkmrk-workload%3B-service%3B-h\">\n<li>workload;</li>\n<li>Service;</li>\n<li>HTTPRoute;</li>\n<li>PVC;</li>\n<li>monitoring;</li>\n<li>policies.</li>\n</ul>\n<p id=\"bkmrk-no-es-requisito-de-l\">No es requisito de la primera migraci\u00f3n.</p>\n<hr id=\"bkmrk--36\">\n<h1 id=\"bkmrk-36.-arquitectura-obj\">36. Arquitectura objetivo resumida</h1>\n<pre id=\"bkmrk-ovh-%2F-vrack-%E2%94%82-proxmo\"><code class=\"language-text\">                        OVH / vRack\n                             \u2502\n                      Proxmox + Ceph\n                             \u2502\n                            RKE2\n                             \u2502\n              \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n              \u2502              \u2502              \u2502\n          Control Plane    Edge          Hosting\n              \u2502              \u2502              \u2502\n              \u2502           Cilium            \u2502\n              \u2502           Hubble            \u2502\n              \u2502              \u2502              \u2502\n              \u2502         Gateway API         \u2502\n              \u2502              \u2502              \u2502\n              \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                             \u2502\n                          Websites\n</code></pre>\n<p id=\"bkmrk-ingress%3A\">INGRESS:</p>\n<pre id=\"bkmrk-internet-%E2%86%93-vip-%2F-vrr\"><code class=\"language-text\">Internet\n \u2193\nVIP / VRRP\n \u2193\nIPVS\n \u2193 DSR\nRKE2 Edge\n \u2193\nTLS / HTTP\n \u2193\nGateway API\n \u2193\nWebsite\n</code></pre>\n<p id=\"bkmrk-egress%3A\">EGRESS:</p>\n<pre id=\"bkmrk-workload-%E2%86%93-selected-\"><code class=\"language-text\">Workload\n \u2193\nselected internal GW\n\n \u251c\u2500\u2500 192.168.88.100 -&gt; WAN IP A\n \u251c\u2500\u2500 192.168.88.101 -&gt; WAN IP B\n \u2514\u2500\u2500 192.168.88.102 -&gt; WAN IP C\n\n \u2193\nInternet\n</code></pre>\n<p id=\"bkmrk-observability%3A\">OBSERVABILITY:</p>\n<pre id=\"bkmrk-cilium-%E2%86%93-hubble-%E2%86%93-fl\"><code class=\"language-text\">Cilium\n \u2193\nHubble\n \u2193\nflows\n \u2193\nfuture security analytics\n</code></pre>\n<p id=\"bkmrk-futuro%3A\">FUTURO:</p>\n<pre id=\"bkmrk-suricata-waf-reputat\"><code class=\"language-text\">Suricata\nWAF\nreputation monitoring\nautomated egress\nBGP\ndedicated IP automation\n</code></pre>\n<hr id=\"bkmrk--37\">\n<h1 id=\"bkmrk-conclusi%C3%B3n\">Conclusi\u00f3n</h1>\n<p id=\"bkmrk-el-dise%C3%B1o-inicial-de\">El dise\u00f1o inicial de la nueva Wetopi debe optimizar robustez, visibilidad y capacidad de evoluci\u00f3n, no complejidad.</p>\n<p id=\"bkmrk-la-base-acordada-es%3A\">La base acordada es:</p>\n<pre id=\"bkmrk-rke2-%2B-cilium%2Fhubble\"><code class=\"language-text\">RKE2\n+\nCilium/Hubble\n+\nIPVS/VRRP/DSR existente\n+\nGateway API\n+\nedge proxies Kubernetes-native\n+\nServices/EndpointSlices\n+\nNATGW Ubuntu simples\n</code></pre>\n<p id=\"bkmrk-este-modelo-conserva\">Este modelo conserva las propiedades valiosas de la plataforma actual:</p>\n<ul id=\"bkmrk-dsr%3B-retorno-directo\">\n<li>DSR;</li>\n<li>retorno directo;</li>\n<li>capacidad WAN distribuida;</li>\n<li>IPs movibles;</li>\n<li>separaci\u00f3n de egress;</li>\n</ul>\n<p id=\"bkmrk-y-elimina-progresiva\">y elimina progresivamente las piezas que Kubernetes puede resolver mejor:</p>\n<ul id=\"bkmrk-docker-swarm%3B-regist-1\">\n<li>Docker Swarm;</li>\n<li>Registrator;</li>\n<li>Consul como service registry web;</li>\n<li>consul-template;</li>\n<li>regeneraci\u00f3n de nginx.conf.</li>\n</ul>\n<p id=\"bkmrk-sobre-esta-foundatio\">Sobre esta foundation se podr\u00e1n a\u00f1adir m\u00e1s adelante Cilium Egress Gateway, Suricata, WAF, reputaci\u00f3n autom\u00e1tica, BGP e IPs dedicadas gestionadas sin condicionar ni complicar el primer despliegue.</p>\n"
 },
 {
  "id": 59,
  "title": "RUN-TOFU-STATE-LOSS \u2014 reimport del estado prod",
  "slug": "run-tofu-state-loss-reimport-del-estado-prod",
  "book_id": 6,
  "chapter_id": 22,
  "description": null,
  "created_at": "2026-09-06T13:39:03.000000Z",
  "updated_at": "2026-09-06T21:45:55.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (operations/runbooks/tofu-prod-state-loss-and-reimport.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n OpenTofu Proxmox PROD state loss and re-import recovery\n\n## Purpose\n\nRecover full OpenTofu control of the 8 Wetopi Proxmox PROD VMs after the\ntfstate is lost, WITHOUT touching the hypervisors: the import path is\nread-only against Proxmox (GET API calls + `tofu import` only \u2014 never an\napply), and the acceptance gate proves the recovered state matches live\nreality exactly.\n\nThe single executed instance of this runbook is task t_690e108f\n(wetopi-senior, 2026-08-30): 28 resources imported, `tofu plan` = No changes\n(0 add / 0 change / 0 destroy), verified twice. Evidence paths and the\npatch scripts it produced are listed under Evidence.\n\n## Trigger / symptoms\n\n- `tofu plan` for `tofu/proxmox/environments/prod` wants to CREATE all 28\n  resources (empty or absent `terraform.tfstate`), or errors with\n  \"state data does not exist\" / no state backend configured.\n- Nobody can answer \"which tfstate describes PROD?\" \u2014 the controller\n  location is unknown or its state file is missing/corrupt.\n\n## Impact\n\nWithout state, IaC has no claim on the live infrastructure: any speculative\n`tofu apply` would try to create duplicate VMs, template, network devices\nand storage. The RKE2 apply runbook (PROC-RKE2-APPLY) precondition \"render\nansible inventory from tofu output\" is blocked. Blast radius is contained as\nlong as no apply runs: Proxmox itself is unaffected.\n\n## Preconditions\n\n- SSH key `/opt/data/.ssh/bigardo` with access to the ops01 bastion\n  (`ubuntu@92.222.245.91`) \u2014 the secrets source.\n- `tofu` on PATH (controller: `/usr/local/bin/tofu`).\n- NO other operator is concurrently running tofu against PROD (single-writer\n  discipline; the state is local, not locked by a remote backend).\n- Read access to the infrastructure repo at current `main`.\n\n### Phase-1 tofu controller (PERMANENT location)\n\nThe phase-1 controller is this harness VM \u2014 NOT ops01 (operator decision\n2026-08-30). State root:\n\n`/opt/data/kanban/workspaces/t_690e108f/infrastructure/tofu/proxmox/environments/prod/`\n\nController artifacts (all mode 0600, all gitignored via `.gitignore`):\n\n| Artifact | Role |\n|---|---|\n| `terraform.tfstate` | recovered state \u2014 28 instances |\n| `terraform.tfvars` | regenerated from the live Proxmox API (never commit; `*.tfvars` is gitignored) |\n| provider env vars | session-only, pulled over SSH from ops01 (below) \u2014 never written to harness disk |\n\n## Concurrency model (phase 1)\n\nThe phase-1 backend is the LOCAL state file `terraform.tfstate` \u2014 it has NO\nlocking of any kind. Two concurrent tofu operations (even two `plan` runs)\ncan corrupt it. Therefore **only ONE tofu operation runs at a time**,\nperiod.\n\nEnforcement is BY CONVENTION, not by a technical gate:\n\n- All tofu-touching tasks are routed through the `wetopi-ops` lane.\n- Serialization is intended to be enforced by the dispatcher via\n  `max_in_progress_per_profile = 1` for tofu-touching tasks; the CURRENT\n  configured value in this harness is `3` (not yet 1), so today the ACTUAL\n  enforcement is that the **lead dispatches tofu tasks one-at-a-time by\n  design** \u2014 never two tofu-touching tasks `running` simultaneously.\n- Future hard gate: a per-board lock (only one tofu task claimable at a\n  time) \u2014 DEFERRED. Until it exists, the one-at-a-time dispatch convention\n  above is load-bearing.\n\nDeferred backend transition:\n\n> Remote backend (SOPS-encrypted or Infisical-backed) arrives with the\n> secrets phase \u2014 until then phase-1 local state with the serialization\n> convention above.\n\n## Safety checks\n\n1. Confirm you are in the phase-1 controller directory and the state file\n   is present, mode 0600, and NOT tracked by git\n   (`git check-ignore terraform.tfstate` \u2192 ignored).\n2. Confirm no credentials are on disk in the controller tree: tokens are\n   sourced over SSH per session only (see Procedure step 2).\n3. Read-only recon of the live Proxmox API (GET only) to know what will be\n   imported before importing: `/nodes`, `/nodes/<n>/qemu`,\n   `/nodes/<n>/qemu/<id>/config`, `/nodes/<n>/network`, `/storage`,\n   `/nodes/<n>/storage/<ds>/content`.\n\n## Procedure\n\n1. Clone infrastructure at current `main` and enter the environment:\n   `git clone https://github.com/wetopi-platform/infrastructure`,\n   `cd tofu/proxmox/environments/prod`.\n2. Source provider tokens (session-only, never on harness disk) and export:\n\n   ```bash\n   eval \"$(ssh -i /opt/data/.ssh/bigardo ubuntu@92.222.245.91 \\\n     'grep -E \"^PROXMOX\" /home/ubuntu/secrets/.env.proxmox; \\\n      grep -E \"CLOUDFLARE\" /home/ubuntu/secrets/.cloudflare-wetopi-net.env')\"\n   export PROXMOX_VE_ENDPOINT PROXMOX_VE_API_TOKEN PROXMOX_VE_INSECURE=true CLOUDFLARE_API_TOKEN\n   ```\n\n   `PROXMOX_VE_INSECURE=true` is REQUIRED when running from the harness VM\n   (untrusted PVE certificate). The ops01 copy of the secret says `false`\n   but ops01 trusts the cert \u2014 on the harness it must be `true`.\n3. Regenerate `terraform.tfvars` from the LIVE API data (shape:\n   `terraform.tfvars.example`; every VM field was verifiable 1:1 from\n   `/nodes/<n>/qemu/<id>/config`). Mode 0600.\n4. `tofu init`, then import the live resources (read-only vs Proxmox) with\n   the bpg/proxmox **0.111.1** import ID formats:\n\n   | Resource class | Import ID format | Example |\n   |---|---|---|\n   | VM (`module.vm`) | `<node>/<vmid>` | `ns3249468/8001` |\n   | network vlan/bridge (`proxmox_virtual_environment_network`) | `<node>:<iface>` | `ns3235424:vlan_rke` |\n   | download file (ISO) | `<node>/<datastore>:<content>/<file>` | `ns3243421/local:iso/ubuntu-24.04-20260826-server-cloudimg-amd64.img` |\n   | firewall options | `vm/<node>/<vmid>` | `vm/ns3249468/8001` |\n   | storage (`proxmox_storage_directory`) | `<storage-id>` | `local` |\n\n   The full 28-resource inventory with per-import return codes is in\n   Evidence. Declared-but-not-applied resources are NOT\n   imported: edge VMs 8041/8042 (F1 build-only, `create_vms=false`) and the\n   Cloudflare records edge-test/docs-new (F3/F5 pending \u2014\n   `cloudflare_dns_records` unset in tfvars).\n\n   **Exact resource addresses (the 28 imports, verbatim from the executed\n   run \u2014 `tofu import <ADDR> <IMPORT-ID>`):**\n\n   Address derivation (module source, quoted):\n\n   - `module \"vm\"` \u2014 `tofu/proxmox/environments/prod/main.tf` lines 93-97:\n     `module \"vm\" { for_each = { for name, vm in var.vms : name => vm if\n     try(var.create_vms[name], false) } \u2026 source = \"../../modules/vm\" }`;\n     the child resource is\n     `tofu/proxmox/modules/vm/main.tf` line 1:\n     `resource \"proxmox_virtual_environment_vm\" \"this\"` \u2192 per-VM address\n     `module.vm[\"<vm-name>\"].proxmox_virtual_environment_vm.this`.\n   - `module \"platform_network\"` \u2014 main.tf lines 1-3:\n     `module \"platform_network\" { for_each = var.manage_platform_network ?\n     var.proxmox_nodes : {} \u2026 source = \"../../modules/node-vlan\" }`.\n   - `proxmox_virtual_environment_firewall_options.bastion` \u2014\n     `tofu/proxmox/environments/prod/bastion.tf` lines 9-10:\n     `resource \"proxmox_virtual_environment_firewall_options\" \"bastion\" {\n     for_each = local.bastion_vms`.\n   - VM keys come from `var.vms` (the 8 live VM names); node placement\n     (ns*) was read live from the Proxmox API during the run.\n\n   Per-VM imports (8) \u2014 one per live VM, key = name in `var.vms`:\n\n   ```bash\n   tofu import 'module.vm[\"wetopi-ops01\"].proxmox_virtual_environment_vm.this'      ns3249468/8001\n   tofu import 'module.vm[\"rke-cp01\"].proxmox_virtual_environment_vm.this'          ns3243421/8011\n   tofu import 'module.vm[\"rke-cp02\"].proxmox_virtual_environment_vm.this'          ns3226939/8012\n   tofu import 'module.vm[\"rke-cp03\"].proxmox_virtual_environment_vm.this'          ns3214108/8013\n   tofu import 'module.vm[\"rke-platform01\"].proxmox_virtual_environment_vm.this'    ns31826929/8021\n   tofu import 'module.vm[\"rke-platform02\"].proxmox_virtual_environment_vm.this'    ns3226931/8022\n   tofu import 'module.vm[\"rke-platform03\"].proxmox_virtual_environment_vm.this'    ns31826925/8023\n   tofu import 'module.vm[\"wetopi-natgw01\"].proxmox_virtual_environment_vm.this'    ns3243421/8031\n   ```\n\n   Non-VM imports (20) \u2014 template, ISO download, firewall options, storage,\n   and the 16 per-node network devices:\n\n   ```bash\n   tofu import 'proxmox_virtual_environment_vm.ubuntu_noble_template[0]'            ns3243421/9000\n   tofu import 'proxmox_download_file.ubuntu_noble[0]'                              ns3243421/local:iso/ubuntu-24.04-20260826-server-cloudimg-amd64.img\n   tofu import 'proxmox_virtual_environment_firewall_options.bastion[\"wetopi-ops01\"]' vm/ns3249468/8001\n   tofu import 'proxmox_storage_directory.local'                                    local\n   tofu import 'module.platform_network[\"ns31826925\"].proxmox_network_linux_bridge.platform' ns31826925:vmbr4\n   tofu import 'module.platform_network[\"ns31826925\"].proxmox_network_linux_vlan.platform'   ns31826925:vlan_rke\n   tofu import 'module.platform_network[\"ns31826929\"].proxmox_network_linux_bridge.platform' ns31826929:vmbr4\n   tofu import 'module.platform_network[\"ns31826929\"].proxmox_network_linux_vlan.platform'   ns31826929:vlan_rke\n   tofu import 'module.platform_network[\"ns3214108\"].proxmox_network_linux_bridge.platform'  ns3214108:vmbr4\n   tofu import 'module.platform_network[\"ns3214108\"].proxmox_network_linux_vlan.platform'    ns3214108:vlan_rke\n   tofu import 'module.platform_network[\"ns3226931\"].proxmox_network_linux_bridge.platform'  ns3226931:vmbr4\n   tofu import 'module.platform_network[\"ns3226931\"].proxmox_network_linux_vlan.platform'    ns3226931:vlan_rke\n   tofu import 'module.platform_network[\"ns3226939\"].proxmox_network_linux_bridge.platform'  ns3226939:vmbr4\n   tofu import 'module.platform_network[\"ns3226939\"].proxmox_network_linux_vlan.platform'    ns3226939:vlan_rke\n   tofu import 'module.platform_network[\"ns3235424\"].proxmox_network_linux_bridge.platform'  ns3235424:vmbr4\n   tofu import 'module.platform_network[\"ns3235424\"].proxmox_network_linux_vlan.platform'    ns3235424:vlan_rke\n   tofu import 'module.platform_network[\"ns3243421\"].proxmox_network_linux_bridge.platform'  ns3243421:vmbr4\n   tofu import 'module.platform_network[\"ns3243421\"].proxmox_network_linux_vlan.platform'    ns3243421:vlan_rke\n   tofu import 'module.platform_network[\"ns3249468\"].proxmox_network_linux_bridge.platform'  ns3249468:vmbr4\n   tofu import 'module.platform_network[\"ns3249468\"].proxmox_network_linux_vlan.platform'    ns3249468:vlan_rke\n   ```\n\n   These 28 addresses are exactly the output of `tofu state list` after the\n   run (Evidence: `state-list.txt`), each with an rc=0 import receipt\n   (Evidence: `import-log.txt`).\n5. Patch the three known bpg 0.111.1 import gaps BEFORE gating. Each gap\n   makes the plan diverge from the pinned HCL and must be patched by\n   editing the LOCAL state JSON only (no Proxmox mutation):\n   - `proxmox_download_file`: import leaves `url`, `checksum`,\n     `checksum_algorithm`, `verify`, `overwrite`, `upload_timeout` NULL\n     (read() only refreshes `size` from the datastore listing) \u2192 the plan\n     wants to REPLACE the file AND cascade-replace template VM 9000 via its\n     chained `file_id`. Patch the pinned HCL values into the state.\n   - template VM `disk[0].file_id`: imports as `\"\"` \u2192 patch to the resolved\n     volid `local:iso/ubuntu-24.04-20260826-server-cloudimg-amd64.img`\n     (NOT the resource address \u2014 the address form also forces replacement).\n   - VMs: `stop_on_destroy` imports `false` (HCL says `true`) and\n     `agent.wait_for_ip` imports as an empty block (HCL: `disabled=true`)\n     \u2192 patch all 8 VMs.\n   After patching: bump `serial`, keep `lineage`.\n6. Acceptance gate \u2014 full `tofu plan` (no `-target`): MUST be\n   **No changes (0 add / 0 change / 0 destroy)**. Verify twice.\n\n## Validation\n\n- Final `tofu plan` output verbatim shows `No changes.` with the counts\n  line empty (0/0/0). ANY destroy line = STOP (see Escalation).\n- `tofu state list` returns the expected 28 instances.\n- A `tofu plan` re-run immediately after the gate reproduces \"No changes\"\n  (drift between the two runs means someone else is writing to Proxmox).\n\n## Rollback / recovery\n\n- State corruption mid-procedure: delete the bad local state file and\n  restart the export/import at step 4 \u2014 imports are idempotent and\n  read-only against Proxmox.\n- Every state write before the acceptance gate should keep a 0600 backup\n  (`terraform.tfstate.bak*`); restore from the newest backup if a patch\n  step goes wrong.\n- The procedure never applies, so there is no infrastructure rollback\n  surface \u2014 worst case is a clean restart of this runbook.\n\n## Failure modes\n\n- Plan shows replacement of `proxmox_download_file` cascading into template\n  VM 9000 \u2192 you skipped caveat 1 of step 5; patch the URL/checksum fields\n  into state, do NOT let the plan run as an apply.\n- Plan shows template disk file_id replacement even after patching \u2192 the\n  file_id was patched to the resource address instead of the resolved\n  volid (caveat 2).\n- Plan shows all 8 VMs modified (`stop_on_destroy`, `agent.wait_for_ip`)\n  \u2192 caveat 3 not applied to every VM.\n- `x509: certificate signed by unknown authority` against the Proxmox API\n  \u2192 `PROXMOX_VE_INSECURE=true` missing (harness requirement, step 2).\n- Import of a network device fails \u2192 ID uses node name + `:iface` form\n  (step 4 table), and VLAN/bridge must exist on that node (check recon).\n\n## Escalation conditions\n\n- The acceptance plan shows ANY destroy \u2192 STOP, do not apply, escalate to\n  the operator (wetopi-senior/lead) with the plan output.\n- Live API recon disagrees with the 28-resource inventory above (VMs exist\n  that are not declared, or declared VMs are absent) \u2192 STOP and reconcile\n  with the operator before writing tfvars.\n- Version gates \u2014 check the CLI and the provider as TWO SEPARATE\n  assertions (they are different version domains and must never be\n  compared to each other):\n  1. OpenTofu CLI: `tofu version` must satisfy\n     `required_version = \">= 1.6.0\"` (quoted from\n     `tofu/proxmox/environments/prod/versions.tf` line 2). The recovery\n     was executed with tofu 1.12.6.\n  2. bpg/proxmox provider: pinned `= 0.111.1` \u2014 quoted from\n     `tofu/proxmox/environments/prod/versions.tf` line 7\n     (`version = \"= 0.111.1\"`) and confirmed by\n     `tofu/proxmox/environments/prod/.terraform.lock.hcl` lines 4-6\n     (`provider \"registry.opentofu.org/bpg/proxmox\" { version = \"0.111.1\",\n     constraints = \"0.111.1\" }`).\n  Either assertion failing \u2192 STOP; the import ID formats and caveats are\n  version-specific and must be re-verified before use.\n\n## Evidence\n\nExecuted run: task `t_690e108f` (wetopi-senior, 2026-08-30), records in the\nKanban Memory Plane. Artifacts:\n\n| Path | Content |\n|---|---|\n| Per-import receipts: harness workspace `\u2026/t_690e108f/evidence/import-log.txt` | 28 import commands + rc=0 receipts |\n| `\u2026/t_690e108f/evidence/plan-final.txt` | acceptance gate: `tofu plan` = No changes (verified twice) |\n| `\u2026/t_690e108f/evidence/state-list.txt` | `tofu state list` \u2014 the 28 imported instances |\n| `\u2026/t_690e108f/patch-state.py`, `patch-state2.py` | the state-patch scripts that closed the step-5 caveats |\n| `\u2026/t_690e108f/recon.py` | reusable GET-only recon script (precondition check 3) |\n\n(the `\u2026` prefix is `/opt/data/kanban/workspaces/t_690e108f` \u2014 the phase-1\ncontroller workspace; all artifacts also attached to the t_690e108f board\ncard.)\n\n## Related incidents\n\n- 2026-08-30 state-loss incident and recovery: task t_690e108f\n  (Memory Plane). This runbook is its repeatable pattern.",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (operations/runbooks/tofu-prod-state-loss-and-reimport.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-opentofu-proxmox-pro\">OpenTofu Proxmox PROD state loss and re-import recovery</p>\n<h2 id=\"bkmrk-purpose\">Purpose</h2>\n<p id=\"bkmrk-recover-full-opentof\">Recover full OpenTofu control of the 8 Wetopi Proxmox PROD VMs after the\ntfstate is lost, WITHOUT touching the hypervisors: the import path is\nread-only against Proxmox (GET API calls + <code>tofu import</code> only \u2014 never an\napply), and the acceptance gate proves the recovered state matches live\nreality exactly.</p>\n<p id=\"bkmrk-the-single-executed-\">The single executed instance of this runbook is task t_690e108f\n(wetopi-senior, 2026-08-30): 28 resources imported, <code>tofu plan</code> = No changes\n(0 add / 0 change / 0 destroy), verified twice. Evidence paths and the\npatch scripts it produced are listed under Evidence.</p>\n<h2 id=\"bkmrk-trigger-%2F-symptoms\">Trigger / symptoms</h2>\n<ul id=\"bkmrk-tofu-plan-for-tofu%2Fp\">\n<li><code>tofu plan</code> for <code>tofu/proxmox/environments/prod</code> wants to CREATE all 28\nresources (empty or absent <code>terraform.tfstate</code>), or errors with\n\"state data does not exist\" / no state backend configured.</li>\n<li>Nobody can answer \"which tfstate describes PROD?\" \u2014 the controller\nlocation is unknown or its state file is missing/corrupt.</li>\n</ul>\n<h2 id=\"bkmrk-impact\">Impact</h2>\n<p id=\"bkmrk-without-state%2C-iac-h\">Without state, IaC has no claim on the live infrastructure: any speculative\n<code>tofu apply</code> would try to create duplicate VMs, template, network devices\nand storage. The RKE2 apply runbook (PROC-RKE2-APPLY) precondition \"render\nansible inventory from tofu output\" is blocked. Blast radius is contained as\nlong as no apply runs: Proxmox itself is unaffected.</p>\n<h2 id=\"bkmrk-preconditions\">Preconditions</h2>\n<ul id=\"bkmrk-ssh-key-%2Fopt%2Fdata%2F.s\">\n<li>SSH key <code>/opt/data/.ssh/bigardo</code> with access to the ops01 bastion\n(<code>ubuntu@92.222.245.91</code>) \u2014 the secrets source.</li>\n<li><code>tofu</code> on PATH (controller: <code>/usr/local/bin/tofu</code>).</li>\n<li>NO other operator is concurrently running tofu against PROD (single-writer\ndiscipline; the state is local, not locked by a remote backend).</li>\n<li>Read access to the infrastructure repo at current <code>main</code>.</li>\n</ul>\n<h3 id=\"bkmrk-phase-1-tofu-control\">Phase-1 tofu controller (PERMANENT location)</h3>\n<p id=\"bkmrk-the-phase-1-controll\">The phase-1 controller is this harness VM \u2014 NOT ops01 (operator decision\n2026-08-30). State root:</p>\n<p id=\"bkmrk-%2Fopt%2Fdata%2Fkanban%2Fwor\"><code>/opt/data/kanban/workspaces/t_690e108f/infrastructure/tofu/proxmox/environments/prod/</code></p>\n<p id=\"bkmrk-controller-artifacts\">Controller artifacts (all mode 0600, all gitignored via <code>.gitignore</code>):</p>\n<table id=\"bkmrk-artifact-role-terraf\">\n<thead>\n<tr>\n<th>Artifact</th>\n<th>Role</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>terraform.tfstate</code></td>\n<td>recovered state \u2014 28 instances</td>\n</tr>\n<tr>\n<td><code>terraform.tfvars</code></td>\n<td>regenerated from the live Proxmox API (never commit; <code>*.tfvars</code> is gitignored)</td>\n</tr>\n<tr>\n<td>provider env vars</td>\n<td>session-only, pulled over SSH from ops01 (below) \u2014 never written to harness disk</td>\n</tr>\n</tbody>\n</table>\n<h2 id=\"bkmrk-concurrency-model-%28p\">Concurrency model (phase 1)</h2>\n<p id=\"bkmrk-the-phase-1-backend-\">The phase-1 backend is the LOCAL state file <code>terraform.tfstate</code> \u2014 it has NO\nlocking of any kind. Two concurrent tofu operations (even two <code>plan</code> runs)\ncan corrupt it. Therefore <strong>only ONE tofu operation runs at a time</strong>,\nperiod.</p>\n<p id=\"bkmrk-enforcement-is-by-co\">Enforcement is BY CONVENTION, not by a technical gate:</p>\n<ul id=\"bkmrk-all-tofu-touching-ta\">\n<li>All tofu-touching tasks are routed through the <code>wetopi-ops</code> lane.</li>\n<li>Serialization is intended to be enforced by the dispatcher via\n<code>max_in_progress_per_profile = 1</code> for tofu-touching tasks; the CURRENT\nconfigured value in this harness is <code>3</code> (not yet 1), so today the ACTUAL\nenforcement is that the <strong>lead dispatches tofu tasks one-at-a-time by\ndesign</strong> \u2014 never two tofu-touching tasks <code>running</code> simultaneously.</li>\n<li>Future hard gate: a per-board lock (only one tofu task claimable at a\ntime) \u2014 DEFERRED. Until it exists, the one-at-a-time dispatch convention\nabove is load-bearing.</li>\n</ul>\n<p id=\"bkmrk-deferred-backend-tra\">Deferred backend transition:</p>\n<blockquote id=\"bkmrk-remote-backend-%28sops\">\n<p>Remote backend (SOPS-encrypted or Infisical-backed) arrives with the\nsecrets phase \u2014 until then phase-1 local state with the serialization\nconvention above.</p>\n</blockquote>\n<h2 id=\"bkmrk-safety-checks\">Safety checks</h2>\n<ol id=\"bkmrk-confirm-you-are-in-t\">\n<li>Confirm you are in the phase-1 controller directory and the state file\nis present, mode 0600, and NOT tracked by git\n(<code>git check-ignore terraform.tfstate</code> \u2192 ignored).</li>\n<li>Confirm no credentials are on disk in the controller tree: tokens are\nsourced over SSH per session only (see Procedure step 2).</li>\n<li>Read-only recon of the live Proxmox API (GET only) to know what will be\nimported before importing: <code>/nodes</code>, <code>/nodes/&lt;n&gt;/qemu</code>,\n<code>/nodes/&lt;n&gt;/qemu/&lt;id&gt;/config</code>, <code>/nodes/&lt;n&gt;/network</code>, <code>/storage</code>,\n<code>/nodes/&lt;n&gt;/storage/&lt;ds&gt;/content</code>.</li>\n</ol>\n<h2 id=\"bkmrk-procedure\">Procedure</h2>\n<ol id=\"bkmrk-clone-infrastructure\">\n<li>\n<p>Clone infrastructure at current <code>main</code> and enter the environment:\n<code>git clone https://github.com/wetopi-platform/infrastructure</code>,\n<code>cd tofu/proxmox/environments/prod</code>.</p>\n</li>\n<li>\n<p>Source provider tokens (session-only, never on harness disk) and export:</p>\n<pre><code class=\"language-bash\">eval \"$(ssh -i /opt/data/.ssh/bigardo ubuntu@92.222.245.91 \\\n  'grep -E \"^PROXMOX\" /home/ubuntu/secrets/.env.proxmox; \\\n   grep -E \"CLOUDFLARE\" /home/ubuntu/secrets/.cloudflare-wetopi-net.env')\"\nexport PROXMOX_VE_ENDPOINT PROXMOX_VE_API_TOKEN PROXMOX_VE_INSECURE=true CLOUDFLARE_API_TOKEN\n</code></pre>\n<p><code>PROXMOX_VE_INSECURE=true</code> is REQUIRED when running from the harness VM\n(untrusted PVE certificate). The ops01 copy of the secret says <code>false</code>\nbut ops01 trusts the cert \u2014 on the harness it must be <code>true</code>.</p>\n</li>\n<li>\n<p>Regenerate <code>terraform.tfvars</code> from the LIVE API data (shape:\n<code>terraform.tfvars.example</code>; every VM field was verifiable 1:1 from\n<code>/nodes/&lt;n&gt;/qemu/&lt;id&gt;/config</code>). Mode 0600.</p>\n</li>\n<li>\n<p><code>tofu init</code>, then import the live resources (read-only vs Proxmox) with\nthe bpg/proxmox <strong>0.111.1</strong> import ID formats:</p>\n<table>\n<thead>\n<tr>\n<th>Resource class</th>\n<th>Import ID format</th>\n<th>Example</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>VM (<code>module.vm</code>)</td>\n<td><code>&lt;node&gt;/&lt;vmid&gt;</code></td>\n<td><code>ns3249468/8001</code></td>\n</tr>\n<tr>\n<td>network vlan/bridge (<code>proxmox_virtual_environment_network</code>)</td>\n<td><code>&lt;node&gt;:&lt;iface&gt;</code></td>\n<td><code>ns3235424:vlan_rke</code></td>\n</tr>\n<tr>\n<td>download file (ISO)</td>\n<td><code>&lt;node&gt;/&lt;datastore&gt;:&lt;content&gt;/&lt;file&gt;</code></td>\n<td><code>ns3243421/local:iso/ubuntu-24.04-20260826-server-cloudimg-amd64.img</code></td>\n</tr>\n<tr>\n<td>firewall options</td>\n<td><code>vm/&lt;node&gt;/&lt;vmid&gt;</code></td>\n<td><code>vm/ns3249468/8001</code></td>\n</tr>\n<tr>\n<td>storage (<code>proxmox_storage_directory</code>)</td>\n<td><code>&lt;storage-id&gt;</code></td>\n<td><code>local</code></td>\n</tr>\n</tbody>\n</table>\n<p>The full 28-resource inventory with per-import return codes is in\nEvidence. Declared-but-not-applied resources are NOT\nimported: edge VMs 8041/8042 (F1 build-only, <code>create_vms=false</code>) and the\nCloudflare records edge-test/docs-new (F3/F5 pending \u2014\n<code>cloudflare_dns_records</code> unset in tfvars).</p>\n<p><strong>Exact resource addresses (the 28 imports, verbatim from the executed\nrun \u2014 <code>tofu import &lt;ADDR&gt; &lt;IMPORT-ID&gt;</code>):</strong></p>\n<p>Address derivation (module source, quoted):</p>\n<ul>\n<li><code>module \"vm\"</code> \u2014 <code>tofu/proxmox/environments/prod/main.tf</code> lines 93-97:\n<code>module \"vm\" { for_each = { for name, vm in var.vms : name =&gt; vm if try(var.create_vms[name], false) } \u2026 source = \"../../modules/vm\" }</code>;\nthe child resource is\n<code>tofu/proxmox/modules/vm/main.tf</code> line 1:\n<code>resource \"proxmox_virtual_environment_vm\" \"this\"</code> \u2192 per-VM address\n<code>module.vm[\"&lt;vm-name&gt;\"].proxmox_virtual_environment_vm.this</code>.</li>\n<li><code>module \"platform_network\"</code> \u2014 main.tf lines 1-3:\n<code>module \"platform_network\" { for_each = var.manage_platform_network ? var.proxmox_nodes : {} \u2026 source = \"../../modules/node-vlan\" }</code>.</li>\n<li><code>proxmox_virtual_environment_firewall_options.bastion</code> \u2014\n<code>tofu/proxmox/environments/prod/bastion.tf</code> lines 9-10:\n<code>resource \"proxmox_virtual_environment_firewall_options\" \"bastion\" { for_each = local.bastion_vms</code>.</li>\n<li>VM keys come from <code>var.vms</code> (the 8 live VM names); node placement\n(ns*) was read live from the Proxmox API during the run.</li>\n</ul>\n<p>Per-VM imports (8) \u2014 one per live VM, key = name in <code>var.vms</code>:</p>\n<pre><code class=\"language-bash\">tofu import 'module.vm[\"wetopi-ops01\"].proxmox_virtual_environment_vm.this'      ns3249468/8001\ntofu import 'module.vm[\"rke-cp01\"].proxmox_virtual_environment_vm.this'          ns3243421/8011\ntofu import 'module.vm[\"rke-cp02\"].proxmox_virtual_environment_vm.this'          ns3226939/8012\ntofu import 'module.vm[\"rke-cp03\"].proxmox_virtual_environment_vm.this'          ns3214108/8013\ntofu import 'module.vm[\"rke-platform01\"].proxmox_virtual_environment_vm.this'    ns31826929/8021\ntofu import 'module.vm[\"rke-platform02\"].proxmox_virtual_environment_vm.this'    ns3226931/8022\ntofu import 'module.vm[\"rke-platform03\"].proxmox_virtual_environment_vm.this'    ns31826925/8023\ntofu import 'module.vm[\"wetopi-natgw01\"].proxmox_virtual_environment_vm.this'    ns3243421/8031\n</code></pre>\n<p>Non-VM imports (20) \u2014 template, ISO download, firewall options, storage,\nand the 16 per-node network devices:</p>\n<pre><code class=\"language-bash\">tofu import 'proxmox_virtual_environment_vm.ubuntu_noble_template[0]'            ns3243421/9000\ntofu import 'proxmox_download_file.ubuntu_noble[0]'                              ns3243421/local:iso/ubuntu-24.04-20260826-server-cloudimg-amd64.img\ntofu import 'proxmox_virtual_environment_firewall_options.bastion[\"wetopi-ops01\"]' vm/ns3249468/8001\ntofu import 'proxmox_storage_directory.local'                                    local\ntofu import 'module.platform_network[\"ns31826925\"].proxmox_network_linux_bridge.platform' ns31826925:vmbr4\ntofu import 'module.platform_network[\"ns31826925\"].proxmox_network_linux_vlan.platform'   ns31826925:vlan_rke\ntofu import 'module.platform_network[\"ns31826929\"].proxmox_network_linux_bridge.platform' ns31826929:vmbr4\ntofu import 'module.platform_network[\"ns31826929\"].proxmox_network_linux_vlan.platform'   ns31826929:vlan_rke\ntofu import 'module.platform_network[\"ns3214108\"].proxmox_network_linux_bridge.platform'  ns3214108:vmbr4\ntofu import 'module.platform_network[\"ns3214108\"].proxmox_network_linux_vlan.platform'    ns3214108:vlan_rke\ntofu import 'module.platform_network[\"ns3226931\"].proxmox_network_linux_bridge.platform'  ns3226931:vmbr4\ntofu import 'module.platform_network[\"ns3226931\"].proxmox_network_linux_vlan.platform'    ns3226931:vlan_rke\ntofu import 'module.platform_network[\"ns3226939\"].proxmox_network_linux_bridge.platform'  ns3226939:vmbr4\ntofu import 'module.platform_network[\"ns3226939\"].proxmox_network_linux_vlan.platform'    ns3226939:vlan_rke\ntofu import 'module.platform_network[\"ns3235424\"].proxmox_network_linux_bridge.platform'  ns3235424:vmbr4\ntofu import 'module.platform_network[\"ns3235424\"].proxmox_network_linux_vlan.platform'    ns3235424:vlan_rke\ntofu import 'module.platform_network[\"ns3243421\"].proxmox_network_linux_bridge.platform'  ns3243421:vmbr4\ntofu import 'module.platform_network[\"ns3243421\"].proxmox_network_linux_vlan.platform'    ns3243421:vlan_rke\ntofu import 'module.platform_network[\"ns3249468\"].proxmox_network_linux_bridge.platform'  ns3249468:vmbr4\ntofu import 'module.platform_network[\"ns3249468\"].proxmox_network_linux_vlan.platform'    ns3249468:vlan_rke\n</code></pre>\n<p>These 28 addresses are exactly the output of <code>tofu state list</code> after the\nrun (Evidence: <code>state-list.txt</code>), each with an rc=0 import receipt\n(Evidence: <code>import-log.txt</code>).</p>\n</li>\n<li>\n<p>Patch the three known bpg 0.111.1 import gaps BEFORE gating. Each gap\nmakes the plan diverge from the pinned HCL and must be patched by\nediting the LOCAL state JSON only (no Proxmox mutation):</p>\n<ul>\n<li><code>proxmox_download_file</code>: import leaves <code>url</code>, <code>checksum</code>,\n<code>checksum_algorithm</code>, <code>verify</code>, <code>overwrite</code>, <code>upload_timeout</code> NULL\n(read() only refreshes <code>size</code> from the datastore listing) \u2192 the plan\nwants to REPLACE the file AND cascade-replace template VM 9000 via its\nchained <code>file_id</code>. Patch the pinned HCL values into the state.</li>\n<li>template VM <code>disk[0].file_id</code>: imports as <code>\"\"</code> \u2192 patch to the resolved\nvolid <code>local:iso/ubuntu-24.04-20260826-server-cloudimg-amd64.img</code>\n(NOT the resource address \u2014 the address form also forces replacement).</li>\n<li>VMs: <code>stop_on_destroy</code> imports <code>false</code> (HCL says <code>true</code>) and\n<code>agent.wait_for_ip</code> imports as an empty block (HCL: <code>disabled=true</code>)\n\u2192 patch all 8 VMs.\nAfter patching: bump <code>serial</code>, keep <code>lineage</code>.</li>\n</ul>\n</li>\n<li>\n<p>Acceptance gate \u2014 full <code>tofu plan</code> (no <code>-target</code>): MUST be\n<strong>No changes (0 add / 0 change / 0 destroy)</strong>. Verify twice.</p>\n</li>\n</ol>\n<h2 id=\"bkmrk-validation\">Validation</h2>\n<ul id=\"bkmrk-final-tofu-plan-outp\">\n<li>Final <code>tofu plan</code> output verbatim shows <code>No changes.</code> with the counts\nline empty (0/0/0). ANY destroy line = STOP (see Escalation).</li>\n<li><code>tofu state list</code> returns the expected 28 instances.</li>\n<li>A <code>tofu plan</code> re-run immediately after the gate reproduces \"No changes\"\n(drift between the two runs means someone else is writing to Proxmox).</li>\n</ul>\n<h2 id=\"bkmrk-rollback-%2F-recovery\">Rollback / recovery</h2>\n<ul id=\"bkmrk-state-corruption-mid\">\n<li>State corruption mid-procedure: delete the bad local state file and\nrestart the export/import at step 4 \u2014 imports are idempotent and\nread-only against Proxmox.</li>\n<li>Every state write before the acceptance gate should keep a 0600 backup\n(<code>terraform.tfstate.bak*</code>); restore from the newest backup if a patch\nstep goes wrong.</li>\n<li>The procedure never applies, so there is no infrastructure rollback\nsurface \u2014 worst case is a clean restart of this runbook.</li>\n</ul>\n<h2 id=\"bkmrk-failure-modes\">Failure modes</h2>\n<ul id=\"bkmrk-plan-shows-replaceme\">\n<li>Plan shows replacement of <code>proxmox_download_file</code> cascading into template\nVM 9000 \u2192 you skipped caveat 1 of step 5; patch the URL/checksum fields\ninto state, do NOT let the plan run as an apply.</li>\n<li>Plan shows template disk file_id replacement even after patching \u2192 the\nfile_id was patched to the resource address instead of the resolved\nvolid (caveat 2).</li>\n<li>Plan shows all 8 VMs modified (<code>stop_on_destroy</code>, <code>agent.wait_for_ip</code>)\n\u2192 caveat 3 not applied to every VM.</li>\n<li><code>x509: certificate signed by unknown authority</code> against the Proxmox API\n\u2192 <code>PROXMOX_VE_INSECURE=true</code> missing (harness requirement, step 2).</li>\n<li>Import of a network device fails \u2192 ID uses node name + <code>:iface</code> form\n(step 4 table), and VLAN/bridge must exist on that node (check recon).</li>\n</ul>\n<h2 id=\"bkmrk-escalation-condition\">Escalation conditions</h2>\n<ul id=\"bkmrk-the-acceptance-plan-\">\n<li>The acceptance plan shows ANY destroy \u2192 STOP, do not apply, escalate to\nthe operator (wetopi-senior/lead) with the plan output.</li>\n<li>Live API recon disagrees with the 28-resource inventory above (VMs exist\nthat are not declared, or declared VMs are absent) \u2192 STOP and reconcile\nwith the operator before writing tfvars.</li>\n<li>Version gates \u2014 check the CLI and the provider as TWO SEPARATE\nassertions (they are different version domains and must never be\ncompared to each other):\n<ol>\n<li>OpenTofu CLI: <code>tofu version</code> must satisfy\n<code>required_version = \"&gt;= 1.6.0\"</code> (quoted from\n<code>tofu/proxmox/environments/prod/versions.tf</code> line 2). The recovery\nwas executed with tofu 1.12.6.</li>\n<li>bpg/proxmox provider: pinned <code>= 0.111.1</code> \u2014 quoted from\n<code>tofu/proxmox/environments/prod/versions.tf</code> line 7\n(<code>version = \"= 0.111.1\"</code>) and confirmed by\n<code>tofu/proxmox/environments/prod/.terraform.lock.hcl</code> lines 4-6\n(<code>provider \"registry.opentofu.org/bpg/proxmox\" { version = \"0.111.1\", constraints = \"0.111.1\" }</code>).\nEither assertion failing \u2192 STOP; the import ID formats and caveats are\nversion-specific and must be re-verified before use.</li>\n</ol>\n</li>\n</ul>\n<h2 id=\"bkmrk-evidence\">Evidence</h2>\n<p id=\"bkmrk-executed-run%3A-task-t\">Executed run: task <code>t_690e108f</code> (wetopi-senior, 2026-08-30), records in the\nKanban Memory Plane. Artifacts:</p>\n<table id=\"bkmrk-path-content-per-imp\">\n<thead>\n<tr>\n<th>Path</th>\n<th>Content</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Per-import receipts: harness workspace <code>\u2026/t_690e108f/evidence/import-log.txt</code></td>\n<td>28 import commands + rc=0 receipts</td>\n</tr>\n<tr>\n<td><code>\u2026/t_690e108f/evidence/plan-final.txt</code></td>\n<td>acceptance gate: <code>tofu plan</code> = No changes (verified twice)</td>\n</tr>\n<tr>\n<td><code>\u2026/t_690e108f/evidence/state-list.txt</code></td>\n<td><code>tofu state list</code> \u2014 the 28 imported instances</td>\n</tr>\n<tr>\n<td><code>\u2026/t_690e108f/patch-state.py</code>, <code>patch-state2.py</code></td>\n<td>the state-patch scripts that closed the step-5 caveats</td>\n</tr>\n<tr>\n<td><code>\u2026/t_690e108f/recon.py</code></td>\n<td>reusable GET-only recon script (precondition check 3)</td>\n</tr>\n</tbody>\n</table>\n<p id=\"bkmrk-%28the-%E2%80%A6-prefix-is-%2Fop\">(the <code>\u2026</code> prefix is <code>/opt/data/kanban/workspaces/t_690e108f</code> \u2014 the phase-1\ncontroller workspace; all artifacts also attached to the t_690e108f board\ncard.)</p>\n<h2 id=\"bkmrk-related-incidents\">Related incidents</h2>\n<ul id=\"bkmrk-2026-08-30-state-los\">\n<li>2026-08-30 state-loss incident and recovery: task t_690e108f\n(Memory Plane). This runbook is its repeatable pattern.</li>\n</ul>\n"
 },
 {
  "id": 60,
  "title": "RUN-RKE2-APPLY \u2014 Aplicar cambios RKE2",
  "slug": "run-rke2-apply-aplicar-cambios-rke2",
  "book_id": 6,
  "chapter_id": 22,
  "description": null,
  "created_at": "2026-09-06T13:39:04.000000Z",
  "updated_at": "2026-09-06T21:45:52.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (platform/rke2-apply-runbook.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n# RKE2 apply runbook \u2014 phases 0-G\n\nThis is the single reviewed procedure a human follows to bring the RKE2 cluster up, with validation gates between phases. Every phase ends in an explicit STOP gate: **do not proceed past a failing gate**. Each gate result is recorded in the apply log with a date.\n\n> **Status (2026-08-30, post-Phase-D update)**: the live apply has RESUMED and executed through **Phase D**. Executed 2026-08-29/30, fully green: **B** (cp01 bootstrap, infra PR #6 fix d553643), **C** (cp02+cp03 quorum, PR #7), **F6-1..3** (CP-only acceptance: quorum recorded, one-CP failure drill via VIP failover, etcd snapshot + real restore), **E** (Cilium 1.20.1 + Hubble, PR #8), and **D** (all 3 platform workers joined via `rke2-workers.yml` at infra PR #9 f841ae7 \u2014 serial gates green, per-worker `kubectl wait` + exact-label asserts passed incl. the `/var/lib/rancher/rke2/agent/kubelet.kubeconfig` data-dir gate; live-verified on rke-platform01). Cluster state: **6/6 nodes Ready**, CP taints enforced (`node-role.kubernetes.io/control-plane:NoSchedule`), workers labelled `wetopi.io/role=platform`, kube-system pods Running, in-cluster DNS resolving (10.68.0.0/16), scheduling test landed on a platform worker, CPs workload-free (`rke2-workers-validate.yml` rack green \u2014 details its runbook section describes for D2 remain next). **Next in executable order: F (Ceph, blocked on operator-held facts, t_80ca13af); G (Argo CD) has since APPLIED with recorded deviations (see the post-Phase-G update below and the Phase G section).** Earlier halt notice (retained for history): the live apply (task t_a88bb36b) was HALTED BEFORE PHASE B and FROZEN per operator directive #38 \u2014 the 2026-08-29 23:17-23:26 UTC attempt failed at `rke2-server : Create the RKE2 server configuration` with `Destination directory /etc/rancher/rke2 does not exist` on rke-cp01 (neither rke2 role created the config dir); fix was infrastructure PR #6 (merged main d553643). cp01 carried keepalived (active+enabled) + RKE2 binaries from that pre-halt attempt \u2014 roles proved idempotent (RKE2 install guarded by `creates:`) when B/C re-ran clean on 2026-08-30 (tasks t_b0eebfac, t_58bd66bb, t_f7099d9d, t_b3841920). Verified against merged infrastructure `main` @ f841ae7 (PRs #1-#9) and docs `main` @ 7cc387a.\n\n**Phase order (the authorized executable order \u2014 stated IDENTICALLY in this banner, the F6 heading + STOP rule, the Phase W heading, the D2 handoff, the DoD mapping, and the provenance section): B \u2192 C \u2192 STOP \u2192 F6-1..3 \u2192 E \u2192 D \u2192 D2 \u2192 W \u2192 F \u2192 G.** Concretely: A (VIP) is folded into the B/C gates (no standalone A apply) \u2192 B (cp01 bootstrap + VIP bring-up) \u2192 C (quorum cp02+cp03 incl. VIP failover) \u2192 **STOP** (mandatory pause after the Phase C quorum gate: before any F6-2/F6-3 server stop, every F6 STOP prerequisite below must be PASS and dated in the apply log; historically this was the frozen-apply boundary of the 2026-08-29 halt \u2014 that freeze is superseded history, see the retained halt notice in the status banner above) \u2192 **Fase-6 acceptance, CP-only portion: F6-1 (quorum recorded), F6-2 (one-CP failure drill), F6-3 (etcd snapshot + REAL restore test)** \u2014 these three execute on the CP-only cluster (3 nodes, no workers) BEFORE any addon, per master plan \u00a733 (Fase 6 before Fase 7), and are gated by the hard F6 STOP prerequisites (external verified snapshot copy, SHA-256 match, quorum evidence, explicit human authorization) \u2192 E (Cilium install + basic install validation ONLY) \u2192 D (workers join, registration + exact-label gates only) \u2192 D2 (full post-Cilium cluster validation on the FINAL 6-node topology, incl. the full Cilium acceptance) \u2192 **W = Phase W \u2014 the worker lifecycle drill (drain + reboot; separately named; former F6-4/F6-5)** \u2014 worker drain/reboot happens HERE, after D2, NEVER before addons \u2192 F (Ceph, PR-only, default-off) \u2192 G (Argo CD). **Known tension with master plan \u00a733, resolved EXPLICITLY**: \u00a733 orders Fase 5 (workers) and Fase 6 (which includes node drain/reboot) BEFORE Fase 7 (Cilium), but ADR-0005 requires Cilium as the IMMEDIATE first addon with NO pre-Cilium worker-Ready/DNS gates \u2014 with `cni: none`, workers can never pass Ready/drain/reboot gates before Cilium exists. Resolution: the CP-only gates of Fase 6 (quorum, CP failure, snapshot + real restore) run before the first addon, and the worker-lifecycle drill is the separately-named Phase W landing after D2 on the final 6-node topology; the DoD maps drain/reboot to that POST-D2 evidence, never to pre-addon acceptance. Do NOT count Phase W as completed pre-addon acceptance. With `cni: none` at the RKE2 level, **no node can be Ready before Cilium exists**: health before E = `/readyz` + etcd member counts ONLY; nodes NotReady is EXPECTED before E and there are NO worker-Ready/CoreDNS/DNS expectations before Cilium. The FULL Cilium acceptance (connectivity test, Hubble flows, DNS, NetworkPolicy, CiliumNode per node) runs in D2 on the final 6-node topology, not in E.\n\n## 0. Preconditions (before Phase B)\n\nWork through this checklist in order. Each item has its concrete verification.\n\n1. **ADRs accepted** \u2014 `docs/decisions/ADR-0004-rke2-api-endpoint.md` and `docs/decisions/ADR-0005-cilium-first-addon.md` merged in docs main (merge commit 5740404).\n2. **Implementation PRs merged to infrastructure `main`** \u2014 verified at **d553643** (current main, 2026-08-30):\n   - PR #1 (Argo CD bootstrap, merged as 8f3e0e8)\n   - PR #2 (`feature/cilium-hubble-addon`: `ansible/playbooks/cilium.yml`, `kubernetes/cilium/*` \u2014 merged as e283e24)\n   - PR #3 (Ceph routing + CSI, merged as 76782dc)\n   - PR #4 (`feature/rke2-server-role`: rke2-server + keepalived roles, `ansible/playbooks/rke2-cp.yml`, `scripts/gen-rke2-token.sh` \u2014 merged as b15a087)\n   - PR #5 (`feature/rke2-agent-role`: rke2-agent role, `ansible/playbooks/rke2-workers.yml`, `ansible/playbooks/rke2-workers-validate.yml` \u2014 merged as 8d836e8)\n   - PR #6 (`fix/rke2-config-dir-create`, head 3ff2ee8 \u2014 merged as d553643): creates `/etc/rancher/rke2` (root:root 0750) before templating in BOTH rke2 roles. **This is the fix for the config-directory defect that halted the 2026-08-29 apply attempt** (`Destination directory /etc/rancher/rke2 does not exist` at `rke2-server : Create the RKE2 server configuration`; byte-identical defect in rke2-agent). Without it, phases B and D fail at config templating.\n\n   STEP 0 gate: `git -C infrastructure log --oneline -1` on the checkout used for apply must show **d553643 or a verified descendant** \u2014 8d836e8 (pre-PR#6) is NO LONGER sufficient: resuming phases B/C against a checkout without the config-dir fix reproduces the halt. Verify the fix is present: `git -C infrastructure show d553643 --stat` mentions both `rke2-server` and `rke2-agent` tasks. `git grep secrets-encryption` still returns nothing on d553643 (the explicit hardening line is still absent \u2014 see item 8 for the current measured-evidence gate rule).\n3. **Branch protection** \u2014 `main` is the reviewed line; all changes land via PR with wetopi-approver review; never commit directly to main.\n4. **Token** \u2014 on the controller (the infrastructure checkout that will run the playbooks \u2014 the evidenced controller is the WORKSTATION, not ops01; see item 9), run `scripts/gen-rke2-token.sh` (umask 077, `openssl rand -hex 16`, 32-char hex). It writes `ansible/files/rke2/node-token`, mode 0600, gitignored.\n   Verify: `stat -c '%a' ansible/files/rke2/node-token` \u2192 `600`. **NEVER commit it.**\n   Both `rke2-cp.yml` and `rke2-workers.yml` pre-flight plays hard-fail if the file is missing or not 0600.\n5. **Inventory** \u2014 `./scripts/render-ansible-inventory.sh` regenerates `ansible/inventories/prod/hosts.json` from the tofu `ansible_inventory` output (mode 600, gitignored); the generated hostvars set `ansible_ssh_common_args = \"-o ProxyJump=ubuntu@92.222.245.91\"` for every non-bastion VM \u2014 that ProxyJump form is the inventory's internal contract consumed by Ansible. Bastion is `wetopi-ops01` = 92.222.245.91 (WAN) / 10.72.0.10, SSH account `ubuntu` (bastion SSH policy allows only `ubuntu`; the workstation keeps all private keys; ops01 is a ProxyJump target only). Human reachability check from the workstation uses the documented double-key form:\n   `ssh -i /opt/data/.ssh/bigardo -o ProxyCommand=\"ssh -i /opt/data/.ssh/bigardo -W %h:%p ubuntu@92.222.245.91\" <user>@<host>`, or plain `ping 10.72.0.21` from ops01.\n6. **Proxmox snapshots (rollback plane)** \u2014 before Phase B, snapshot every VM that phases B-G will touch, from the Proxmox node hosting it (standard Proxmox CLI `qm snapshot <vmid> pre-rke2-apply`):\n\n   | VM | vm_id | Proxmox node | IP |\n   |---|---|---|---|\n   | wetopi-ops01 | 8001 | ns3249468 | 10.72.0.10 |\n   | rke-cp01 | 8011 | ns3243421 | 10.72.0.21 |\n   | rke-cp02 | 8012 | ns3226939 | 10.72.0.22 |\n   | rke-cp03 | 8013 | ns3214108 | 10.72.0.23 |\n   | rke-platform01 | 8021 | ns31826929 | 10.72.0.41 |\n   | rke-platform02 | 8022 | ns3226931 | 10.72.0.42 |\n   | rke-platform03 | 8023 | ns31826925 | 10.72.0.43 |\n   | wetopi-natgw01 | 8031 | ns3243421 | 10.72.0.2 |\n\n   Rollback = `qm rollback <vmid> pre-rke2-apply` (VM must be stopped). These are the Proxmox-level safety net; the per-phase rollbacks below are the first line.\n7. **Cluster facts (from ADR-0004)** \u2014 API VIP 10.72.0.20 (vmbr4/VLAN 4 node LAN 10.72.0.0/23, never Internet); server URL `https://10.72.0.20:9345`; API `https://10.72.0.20:6443`; tls-san = 10.72.0.20/.21/.22/.23 (IP-only, no DNS SAN); cluster-cidr 10.64.0.0/14; service-cidr 10.68.0.0/16; CNI `none` at RKE2 level (Cilium is the first addon, ADR-0005).\n8. **At-rest encryption gate \u2014 ONE current statement (reconciled 2026-08-30, Phase G live evidence, card t_e0cace4b).** The earlier \"honest gap\" assumption (that a missing `secrets-encryption` line in the config template means Secrets sit PLAINTEXT in etcd) is **superseded: it was wrong for this RKE2 build**. Measured fact: RKE2 v1.36.3+rke2r1 enables **AES-CBC at-rest encryption BY DEFAULT** \u2014 the config template renders no `secrets-encryption` line, yet `rke2 secrets-encrypt status` measured on ALL THREE CPs shows `Encryption Status: Enabled` / `Current Rotation Stage: start` / `Server Encryption Hashes: All hashes match` / Active `* AES-CBC aescbckey`, and raw etcd probes (approved kubectl-exec etcdctl form, below) of `argocd/argocd-initial-admin-secret` (316B) and `argocd/repo-infra-https` (685B) returned `k8s:enc:aescbc:v1:aescbckey:<opaque ciphertext>` \u2014 NOT plaintext.\n   **CURRENT GATE RULE (this is the operative rule; it supersedes every older wording elsewhere in this runbook, including any residual plaintext framing):** current protection is determined by MEASURED EVIDENCE, not by config-template contents \u2014 for any Secret at any gate, the measured checks below. **What the explicit `secrets-encryption: true` line is:** a SEPARATE, tracked, future reviewed hardening/rotation prerequisite (explicit declarative intent in the rke2-server role template, needed BEFORE any future key rotation). It is NOT an unlock condition for any gate while the measured checks pass \u2014 a build with default-on encryption and no explicit line is PROTECTED, not blocked.\n   **Measured-protection checks (the form used at any [encryption-gate]):**\n   - (i) `rke2 secrets-encrypt status` on all three CPs, each showing:\n     ```\n     Encryption Status: Enabled\n     Current Rotation Stage: start\n     Server Encryption Hashes: All hashes match\n     ```\n     plus an Active key row of the shape `* AES-CBC aescbckey...` (the `*` marks the active key);\n   - (ii) an exact raw-etcd ciphertext check, reusing the approved kubectl-exec etcdctl form (Phase B gate) with the 17-arg pattern:\n     ```\n     /var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml -n kube-system exec etcd-rke-cp01 -- env ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 --cacert=/var/lib/rancher/rke2/server/tls/etcd/server-ca.crt --cert=/var/lib/rancher/rke2/server/tls/etcd/client.crt --key=/var/lib/rancher/rke2/server/tls/etcd/client.key get /registry/secrets/<namespace>/<NAME>\n     ```\n     Expected: the value shows the encrypted form (`k8s:enc:aescbc` ciphertext \u2014 opaque), **NOT** the plaintext returned by `kubectl get secret <name> -n <namespace> -o jsonpath='{.data.<KEY>}' | base64 -d`. **STOP: plaintext visible \u2192 protection NOT confirmed \u2192 the [encryption-gate] in question stays STOPped.**\n   **Status of this cluster (2026-08-30, Phase G):** measured PROTECTED \u2014 (i) verified on all 3 CPs, (ii) verified by raw etcd ciphertext probes on both argocd Secrets. The future hardening/rotation prerequisite (reviewed `secrets-encryption: true` merged to infrastructure main, then sequential restart cp01\u2192cp02\u2192cp03 one at a time with each returning Ready before the next, re-verifying (i) and (ii)) is **NOT yet merged** (`git grep secrets-encryption` on infrastructure main still returns empty) \u2014 it must land via its own reviewed PR BEFORE any future encryption-key rotation; it does not block F2/G while the measured checks pass.\n   Future shape: a reviewed `secrets-encryption: true` setting in `/etc/rancher/rke2/config.yaml` (rendered by the rke2-server role), merged to infrastructure main. NOTE: there is **no manually-managed key file** \u2014 RKE2's documented behavior (rancher/rke2-docs, docs/security/secrets_encryption.md) is that once secrets encryption is enabled, RKE2 **automatically generates an AES-CBC key and the encryption-provider config file** at `/var/lib/rancher/rke2/server/cred/encryption-config.json` and passes it to the kube-apiserver as `--encryption-provider-config`. The config file is node-local state auto-generated and managed by RKE2 itself \u2014 never commit it to Git.\n9. **Controller and execution environment \u2014 define and verify BEFORE the first phase command.** Direct evidence from the 2026-08-29 apply (t_a88bb36b): wetopi-ops01 had NO Ansible venv, NO infrastructure checkout, and NO kubeconfig; the playbooks ran from the WORKSTATION, which holds the SSH private keys (`/opt/data/.ssh/bigardo`). ops01 is a ProxyJump target only (item 5): it holds no private keys and cannot SSH to the LAN VMs by itself. This runbook therefore fixes the execution context explicitly instead of assuming it:\n   - **Controller = the workstation** (the Ansible phases B, C, D, D2 \u2014 playbooks `rke2-cp.yml`, `rke2-workers.yml`, `rke2-workers-validate.yml` run from the controller and reach the VMs over SSH through the bastion). Verify on the controller BEFORE phases B/C (record each check with a date):\n     - clean infrastructure checkout at **d553643 or a verified descendant** (item 2 STEP 0 gate): `git -C infrastructure log --oneline -1` shows d553643 or a descendant, and `git -C infrastructure status --porcelain` is empty;\n     - Ansible venv per `requirements-dev.txt` (pins `ansible-core==2.21.3`): `python3 -m venv .venv` then `.venv/bin/pip install -r requirements-dev.txt`; verify `.venv/bin/ansible-playbook --version` reports ansible-core 2.21.3;\n     - inventory: `./scripts/render-ansible-inventory.sh` regenerates `ansible/inventories/prod/hosts.json` (item 5) \u2014 or use the synthesized gitignored inventory shape the 2026-08-29 apply verified against `outputs.tf`; EITHER form must include the ungrouped `localhost` host with `ansible_connection: local, ansible_become: false` (the roles' `delegate_to: localhost` token checks fail on a sudo-less controller without it \u2014 evidenced 2026-08-29);\n     - token file (item 4): `stat -c '%a' ansible/files/rke2/node-token` \u2192 `600`;\n     - SSH path: the double-key ProxyCommand form (item 5) opens a sudo-capable shell on cp01 (`sudo -v` succeeds there).\n   - **Ops01 provisioning \u2014 verify BEFORE Phase E** (ops01 hosts the `connection: local` Helm op of `cilium.yml`, the cilium/hubble CLI acceptance in D2, and the F2/G commands; NONE of this existed at apply time):\n     - infrastructure checkout on ops01 at **d553643 or a verified descendant**: `git clone https://github.com/wetopi-platform/infrastructure.git ~/wetopi-infrastructure` then `git checkout main` and verify the head;\n     - Ansible venv on ops01: `python3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt` (Phase E invokes `.venv/bin/ansible-playbook` there);\n     - inventory on ops01: `ansible/inventories/prod/hosts.json` is gitignored \u2014 either render it on ops01 (requires the tofu state) or copy the controller's verified copy over (same relay as the kubeconfig below); record which was done;\n     - binaries on ops01: `helm version --short` (v3.x; the chart reference was validated with Helm 3.19), `kubectl version --client`, and the `cilium` + `hubble` CLIs (GitHub releases cilium/cilium-cli and cilium/hubble; verify with `cilium version` and `hubble version`; record exact versions \u2014 the cilium CLI must be compatible with the pinned chart 1.20.1);\n     - **current admin kubeconfig, transferred AFTER the F6-3 restore test** (Phase E is the first consumer; a pre-restore transfer is stale):\n       1. on cp01: `sudo cat /etc/rancher/rke2/rke2.yaml > /tmp/rke2-admin-kubeconfig && chmod 600 /tmp/rke2-admin-kubeconfig`;\n       2. from the workstation (relay \u2014 ops01 cannot pull from cp01): `scp -i /opt/data/.ssh/bigardo -o ProxyCommand=\"ssh -i /opt/data/.ssh/bigardo -W %h:%p ubuntu@92.222.245.91\" ubuntu@10.72.0.21:/tmp/rke2-admin-kubeconfig /tmp/rke2-admin-kubeconfig`;\n       3. workstation \u2192 ops01 (direct to the bastion): `scp -i /opt/data/.ssh/bigardo /tmp/rke2-admin-kubeconfig ubuntu@92.222.245.91:~/secrets/rke2-admin-kubeconfig`;\n       4. remove the `/tmp` copies on cp01 and the workstation;\n       5. on ops01: `sed -i 's|https://127.0.0.1:6443|https://10.72.0.20:6443|' ~/secrets/rke2-admin-kubeconfig && chmod 600 ~/secrets/rke2-admin-kubeconfig` (the RKE2 admin kubeconfig points at 127.0.0.1 by default; the VIP is in tls-san per ADR-0004, so the apiserver cert validates), then verify: `KUBECONFIG=~/secrets/rke2-admin-kubeconfig kubectl get nodes` lists the 3 CPs. **NEVER commit the kubeconfig** \u2014 it is cluster-admin credentials.\n   - **Two canonical kubectl invocation forms** (every command block below uses one of these):\n     - **CP-local** (executed ON a control-plane node): `/var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml ...` \u2014 plain `kubectl` is NOT in PATH on CP nodes; only the RKE2-bundled binary is (same form the playbooks use, e.g. the `rke2_kubectl` fact in `rke2-workers-validate.yml`);\n     - **Remote** (ops01 or the workstation): `kubectl ...` with `KUBECONFIG` pointing at the transferred admin kubeconfig (ops01 provisioning above).\n\n## Phase B \u2014 first control plane (cp01 bootstrap, incl. VIP bring-up)\n\nKeepalived is NOT a standalone phase: it is the **first role of both RKE2 plays** inside `ansible/playbooks/rke2-cp.yml` (role order per CP: keepalived, then rke2-server). Play 1 pre-flights all CPs (asserts VIP+addresses in tls-san, CIDRs defined, VIP == 10.72.0.20, token file 0600). The bootstrap play (play 2, hosts `rke-cp01`, `serial: 1`, `any_errors_fatal: true`) renders `/etc/rancher/rke2/config.yaml` via the rke2-server template (token; `server:` only on non-bootstrap nodes; tls-san list; cluster-cidr; service-cidr; `cni: none`).\n\n**Keepalived design facts** (from `ansible/roles/keepalived/`): interface eth0; VIP 10.72.0.20/23; VRID 51; unicast peers = the other two CPs; priorities cp01=150 (initial MASTER), cp02=100, cp03=50; advert_int 1; health-check script `/etc/keepalived/check_rke2_api.sh` probes the LOCAL API only: `curl -sf --cacert /var/lib/rancher/rke2/server/tls/server-ca.crt --cert .../client-kube-apiserver.crt --key .../client-kube-apiserver.key https://127.0.0.1:6443/readyz` plus a TCP probe of 127.0.0.1:9345; interval 2, fall 3, rise 2.\n\n**Consequence (fail-safe by design)**: a CP only holds the VIP while its own local API is healthy \u2014 before cp01's API is up there is deliberately NO VIP holder, so `ping 10.72.0.20` failing before Phase B completes is EXPECTED, not a bug. There is NO standalone Phase-A apply; VIP acceptance is folded into the B/C gates below.\n\nCommand (from the CONTROLLER \u2014 the workstation per \u00a70 item 9, in its infrastructure checkout):\n\n```\nANSIBLE_CONFIG=ansible/ansible.cfg \\\n  .venv/bin/ansible-playbook -i ansible/inventories/prod/hosts.json \\\n  ansible/playbooks/rke2-cp.yml\n```\n\nThe playbook is one invocation for B AND C \u2014 play 1 pre-flights all CPs, play 2 bootstraps cp01, play 3 joins cp02/cp03. Phase B is complete when play 2's built-in gates pass: `wait_for` 127.0.0.1:6443 (600s timeout), `/readyz` via the VIP (Ansible uri with client certs, retries 30 \u00d7 10s, expects status 200), and etcd member list == 1 (see gate B below).\n\n### Validation gate B (includes former \"Phase A\" VIP acceptance)\n\n- `curl -k https://10.72.0.20:6443/readyz` \u2192 `ok` (API via VIP works; VIP now held by cp01)\n- `systemctl is-active keepalived` on **cp01 ONLY** (Phase B applies cp01; the all-three-CP keepalived check belongs to gate C, where cp02/cp03 are applied) \u2192 `active`\n- `ip -4 addr show eth0` on each CP: exactly one node lists `10.72.0.20/23` (the current owner; cp01 when healthy)\n- `ping -c 3 10.72.0.20` from wetopi-ops01 \u2192 0% loss\n- etcd member list (EXACT form the playbook uses, executed as a kubectl exec into the etcd pod on cp01):\n  ```\n  /var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml -n kube-system exec etcd-rke-cp01 -- env ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 --cacert=/var/lib/rancher/rke2/server/tls/etcd/server-ca.crt --cert=/var/lib/rancher/rke2/server/tls/etcd/client.crt --key=/var/lib/rancher/rke2/server/tls/etcd/client.key member list\n  ```\n  \u2192 exactly 1 member, started.\n- **Node Ready state is NOT a Phase B gate**: with `cni: none`, the CP-local form (\u00a70 item 9) `/var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml get nodes` on cp01 shows cp01 **NotReady \u2014 EXPECTED**. CP health at this stage = `/readyz` via VIP + etcd member count only.\n\n**STOP**: any playbook play fails (`any_errors_fatal` aborts the whole run \u2014 do not re-run blindly; read the failure first); `/readyz` via VIP fails; etcd member count != 1; keepalived not active on cp01.\n\n## Phase C \u2014 quorum (cp02 + cp03, incl. VIP failover test)\n\nSame playbook, play 3 (hosts `rke-cp02:rke-cp03`, `serial: 1`, roles keepalived + rke2-server, joining via `server: https://10.72.0.20:9345`; per-node expected etcd member counts: after cp02 = 2, after cp03 = 3). It continues automatically once Phase B gates pass.\n\n### Validation gate C\n\n- etcd member list (exact kubectl-exec form, run from cp01):\n  ```\n  /var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml -n kube-system exec etcd-rke-cp01 -- env ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 --cacert=/var/lib/rancher/rke2/server/tls/etcd/server-ca.crt --cert=/var/lib/rancher/rke2/server/tls/etcd/client.crt --key=/var/lib/rancher/rke2/server/tls/etcd/client.key member list\n  ```\n  \u2192 exactly 3 members, all started; quorum = 2 of 3\n- `curl -k https://10.72.0.20:6443/readyz` \u2192 `ok`\n- `systemctl is-active keepalived` on **all three CPs** (the all-three-CP keepalived check lives HERE, in Phase C where cp02/cp03 are applied) \u2192 `active`\n- `/var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml get nodes` (CP-local form, on cp01) \u2192 rke-cp01..03 all **NotReady \u2014 EXPECTED (pre-CNI)**; do NOT treat this as a failure\n- **VIP failover test** (only now, with quorum): on the current owner (cp01; find via `ip -4 addr show eth0` on each CP) run `sudo systemctl stop keepalived`; expect the VIP to move to the highest-priority healthy peer (cp02) within roughly 10 s (advert_int 1 \u00d7 fall 3, plus check rise 2 \u00d7 interval 2 on the receiver); verify from ops01: `ping -c 3 10.72.0.20` still 0% loss and `curl -k https://10.72.0.20:6443/readyz` still `ok`; then `sudo systemctl start keepalived` on cp01 \u2014 cp01 (priority 150) reclaims the VIP by preemption; confirm with `ip -4 addr` again.\n- **Documented one-CP-outage test**: on ONE non-VIP-owner CP (cp02 or cp03): `sudo systemctl stop rke2-server`; then from ops01 `curl -k https://10.72.0.20:6443/readyz` must STILL return `ok` (2 members keep quorum; API served via VIP on remaining CPs) and the etcd member list (kubectl-exec form, from cp01) still lists 3 members; restart with `sudo systemctl start rke2-server`. STOP the whole run if the API via VIP fails during this test (that is a quorum/VIP defect, not a node defect).\n- **CP workload check** (CP-local form, on each CP): `/var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml describe node rke-cp0X | grep -i taint` \u2014 record the taint state on all three CPs in the apply log. The no-platform-workloads-on-CPs guarantee is enforced by the workers playbook play 2, which applies `node-role.kubernetes.io/control-plane=true:NoSchedule` on all three CPs (idempotency decided from the API pre-state, not kubectl stdout), so ordinary pods can never schedule onto a CP; system components tolerate the taint by upstream default. Do not proceed if `/var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml -n kube-system get pods -o wide` (same form) shows any platform-labeled pod scheduled on a CP.\n\n**STOP**: member count != 3; /readyz fails; keepalived not active on all three CPs; API via VIP fails with one CP stopped or during VIP failover; any platform workload found on a CP.\n\n## Phase F6 / Fase-6 acceptance \u2014 CP-only portion (F6-1..3), gated by hard STOP prerequisites\n\nPart of the authorized executable order (identical to the banner, the Phase W heading, the D2 handoff, the DoD mapping, and the provenance section): **B \u2192 C \u2192 STOP \u2192 F6-1..3 \u2192 E \u2192 D \u2192 D2 \u2192 W \u2192 F \u2192 G.** This phase is the **CP-only portion of the Fase-6 acceptance**: it runs immediately after the Phase C quorum gate and **BEFORE any addon (Cilium E, workers D, Ceph F, Argo CD G)**, on the CP-only cluster (3 nodes, `cni: none`, no workers joined). Master plan \u00a733 places Fase 6 before Fase 7 (Cilium/Hubble); its CP-only gates \u2014 **F6-1 (quorum recorded), F6-2 (one-CP failure drill), F6-3 (etcd snapshot + REAL restore test)** \u2014 run here, gated by the hard F6 STOP prerequisites below. The **worker-lifecycle drill (drain + reboot) is NOT part of this phase**: it is the separately-named **Phase W** and executes AFTER D2 on the final 6-node topology (ADR-0005 forbids pre-Cilium worker-Ready gates; see the banner for the \u00a733 tension and its explicit resolution). Do NOT count Phase W as completed pre-addon acceptance. Each test has a concrete command, expected output, and STOP condition; record every result with a date in the apply log. Node Ready state is NOT a gate in this phase (CPs are NotReady pre-CNI \u2014 EXPECTED). See the execution notes per test.\n\n### F6-1 \u2014 quorum recorded\n- Command: etcd member list via the kubectl-exec form (Phase B gate), from cp01.\n- Expected: exactly 3 members, all `started`.\n- STOP: member count != 3 or any member not started.\n\n### F6 STOP prerequisites \u2014 HARD GATES (ALL must be PASS, dated in the apply log, BEFORE F6-2/F6-3 stop any rke2-server)\n\nThese are GATES, not suggestions. F6-3's restore test stops ALL THREE servers \u2014 full cluster API downtime \u2014 and replaces the etcd peer databases; F6-2 stops one server. Until every prerequisite below is PASS, this phase halts after the read-only F6-1 and NO `systemctl stop rke2-server` may be issued.\n\n- **P1 \u2014 Quorum and API evidence (immediately before any stop)**: F6-1 recorded with today's date (member list == 3 members, all started), plus `curl -k https://10.72.0.20:6443/readyz` \u2192 `ok`; record both outputs.\n- **P2 \u2014 Snapshot saved AND copied OUT of cp01's failure domain** (on cp01, as root):\n  ```\n  rke2 etcd-snapshot save --name pre-addons-fase6\n  rke2 etcd-snapshot ls\n  ```\n  Expected: `save` reports the snapshot file written under `/var/lib/rancher/rke2/server/db/snapshots/` (on-demand snapshots default to `on-demand-<node>-<unix-ts>`; `--name` sets the base name to `pre-addons-fase6`); `ls` lists the new snapshot with a size and timestamp. Record both outputs. Then copy the snapshot file OFF cp01 to external storage \u2014 e.g. wetopi-ops01 `~/rke2-snapshots/` or the workstation's disk \u2014 using the relay documented in \u00a70 item 9 (on cp01 place it under `/tmp` readable by `ubuntu`; pull it with the workstation's ProxyCommand form; ops01 cannot pull from cp01 itself because it holds no private keys). The external copy MUST live **outside `/var/lib/rancher/rke2/server/db` AND outside cp01's failure domain**: NOT on cp01, NOT on any CP node, NOT inside the live db tree, and preferably not on cp01's Proxmox node (ns3243421) either \u2014 wetopi-ops01 (ns3249468) or the workstation satisfies this. cp01 keeps the copy the reset consumes; the external copy is the recovery plane if the reset damages cp01's db.\n- **P3 \u2014 SHA-256 digests match, independently recomputed**: `sha256sum /var/lib/rancher/rke2/server/db/snapshots/<snapshot-file>` on cp01, and \u2014 independently, ON the host holding the external copy (not via a shell relayed through cp01) \u2014 `sha256sum <external-path>/<snapshot-file>`. The two digests MUST be identical; record both in the apply log. Mismatch \u2192 re-copy and re-verify before anything else.\n- **P4 \u2014 EXPLICIT human authorization for full API downtime**: a named human explicitly authorizes the F6-3 restore window (all three servers stopped, cluster API fully down; plan tens of minutes for reset + peer rejoin), recorded in the apply log with name, date, scope, and the expected window \u2014 BEFORE the first `systemctl stop rke2-server` of F6-3. No recorded authorization line = gate NOT passed.\n\n**STOP (prerequisites)**: any P1-P4 item not PASS \u2192 do NOT run F6-2/F6-3; the cluster stays up (read-only F6-1 is the only permitted test); escalate to the lead.\n\n### F6-2 \u2014 one-CP failure (recorded acceptance test; requires the F6 STOP prerequisites P1-P4 ALL PASS \u2014 this test stops a server)\n- Command: `sudo systemctl stop rke2-server` on cp02, then `curl -k https://10.72.0.20:6443/readyz` from ops01, then `sudo systemctl start rke2-server` on cp02.\n- Expected: `/readyz` returns `ok` throughout (2 members keep quorum); API served via VIP on remaining CPs; after restart, member list still 3.\n- STOP: API via VIP fails while one CP is stopped (quorum/VIP defect \u2014 stop the whole apply).\n\n### F6-3 \u2014 **REAL restore test (restore probado)** \u2014 requires the F6 STOP prerequisites P1-P4 ALL PASS; snapshot per P2\n\n> This is a **REAL, EXECUTED acceptance test** \u2014 not a documented-only procedure. Master plan \u00a735.1 requires \"restore probado\"; a snapshot-save-only check does NOT satisfy it. The restore test must be executed and recorded HERE, before Phase E (Cilium). If the restore test cannot be executed, the DoD marks it UNMET and the runbook mandates STOP before all subsequent phases (E onward). The snapshot itself was saved and externally checksum-verified in the F6 STOP prerequisites P2/P3 above; this test consumes the cp01-local copy.\n\nThe snapshot file is the one saved and externally verified in prerequisites P2/P3 above (cp01-local path `/var/lib/rancher/rke2/server/db/snapshots/<file>`; the verified external copy is the recovery plane \u2014 if the cp01-local file is damaged, re-copy from the external copy and re-verify its SHA-256 before use). **EXECUTE the restore procedure below as the acceptance test** and record the evidence (expected-output lines, member list 3/3, `/readyz`, and the post-restore journal checks in step 6).\n\n#### Restore procedure \u2014 `rke2 server --cluster-reset` (multi-server, 3 CPs)\n\n> **CRITICAL**: there is NO `rke2 etcd-snapshot restore` subcommand. Verified from the authoritative RKE2 docs (rancher/rke2-docs, docs/datastore/backup_restore.md): the `etcd-snapshot` subcommands are `delete` / `ls` (`list`, `l`) / `prune` / `save` ONLY. Restore is the server **cluster-reset** flow.\n\nSnapshot on N1 = cp01; peers N2/N3 = cp02/cp03:\n\n1. **Stop RKE2 on ALL server nodes**: `systemctl stop rke2-server` on cp01, cp02, cp03.\n2. **On N1 (cp01)**: `rke2 server --cluster-reset --cluster-reset-restore-path=<PATH-TO-SNAPSHOT>` (path = the snapshot file recorded by `rke2 etcd-snapshot ls`, under `/var/lib/rancher/rke2/server/db/snapshots/`).\n   Expected output on success:\n   ```\n   Managed etcd cluster membership has been reset, restart without --cluster-reset flag now.\n   Backup and delete ${datadir}/server/db on each peer etcd server and rejoin the nodes.\n   ```\n   (datadir = /var/lib/rancher/rke2)\n3. **On N1 (cp01)**: `systemctl start rke2-server`.\n4. **On N2/N3 (cp02/cp03)**: `rm -rf /var/lib/rancher/rke2/server/db/` then `systemctl start rke2-server` \u2014 each peer rejoins the restored cluster by re-downloading the db.\n5. **Re-verify**: etcd member list (kubectl-exec form, from cp01) == 3 members started; `curl -k https://10.72.0.20:6443/readyz` \u2192 `ok`; the CP-local form `/var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml get nodes` (on cp01) shows 3/3 CPs registered.\n6. **Post-restore journal checks on EVERY CP** (explicit gate \u2014 record all three outputs with a date in the apply log):\n   ```\n   sudo journalctl -u rke2-server --since \"-30 min\" --no-pager | grep -E 'level=(error|fatal)' || echo CLEAN\n   ```\n   **Failure criteria (explicit)**: on ANY of the three CPs, any `level=error`/`level=fatal` line mentioning corruption, corrupt, checksum, panic, or db/data-dir damage \u2014 OR a recurring `failed to start` persisting more than ~2 minutes after that node's start \u2014 means the restore test is **FAILED**: apply the STOP-and-escalate rule below; do NOT retry. Transient retry/election/timeout lines during peer rejoin that stop once quorum re-forms are acceptable. Required to PASS: `CLEAN` output (or only transient lines that cease) on all three CPs.\n\nMechanics (from the RKE2 docs): the current etcd db files are moved to `${data-dir}/server/db/etcd-old-$TIMESTAMP/`; the snapshot checksum is verified; all etcd members except the restore node are removed from the cluster, and peers rejoin by re-downloading the db.\n\n**Reset-flag safety mechanism** (rancher/rke2-docs, docs/datastore/backup_restore.md; semantics verified 2026-08-30 against the k3s source vendored by the pinned `rke2_version: v1.36.3+rke2r1` \u2014 k3s commit 4ca29faac89e, `pkg/cluster/managed.go` + `pkg/etcd/etcd.go`): when RKE2 resets the cluster it creates an empty file at `/var/lib/rancher/rke2/server/db/reset-flag` to prevent accidental repeated cluster resets; the flag is **deleted only when RKE2 starts normally** (a normal start of the managed database clears it). Version-exact consequences: (a) the flag is written **after the snapshot-restore step succeeds** \u2014 an early failure (bad snapshot path, download/checksum/restore error) happens with NO flag present; a late failure (post-reset etcd startup) happens WITH the flag present; (b) the flag blocks only a **bare** `rke2 server --cluster-reset` (no restore path) \u2014 the pinned version does NOT consult the flag when `--cluster-reset-restore-path` is set (source comment: \"If we're restoring from a snapshot, don't check the reset-flag - just reset and restore\"); (c) the pre-restore db is moved aside to `db/etcd-old-<timestamp>/` by the restore step, so a failed attempt may leave the db dir absent or partially restored.\n\n- **STOP \u2014 reset/restore failure is an immediate STOP-and-escalate condition.** If the reset invocation fails, does not print the expected output, the db dir is not re-provisioned, peers fail to rejoin, quorum is not re-established, or `/readyz` is not OK: do NOT blindly retry \u2014 the failure state is flag- and db-dependent per (a)-(c) above, and all CPs are stopped. Escalate to the lead with the full output; the cluster stays STOPped. No addons or workloads until recovery is complete and quorum is re-verified.\n- **Only documented, version-supported recovery** \u2014 every option below is a lead-approved, apply-log-recorded escalated action, never an operator default; a second reset attempt is permitted ONLY after a successful normal cp01 start or a coordinated rollback decision:\n  1. **Normal start on cp01**: `systemctl start rke2-server` \u2014 the documented flag-clearing mechanism. It starts from whatever db state the failed attempt left: if the snapshot-restore step succeeded and only the startup failed, the node comes up as the restored single member; if the restore step itself failed (db moved aside, restore incomplete), the node may fail to start or come up uninitialized. Record the actual observed state (flag present? `db/etcd-old-*` present? member list, `/readyz`) and STOP again if anything is unexpected. **A successful normal start is the precondition for considering any second reset attempt.**\n  2. **Second attempt, only after (1) succeeded or a coordinated rollback decision**: re-attempt the documented restore with a KNOWN-GOOD snapshot (stop rke2-server on all CPs \u2192 `rke2 server --cluster-reset --cluster-reset-restore-path=<PATH>` on cp01 \u2192 normal start cp01 \u2192 peers `rm -rf /var/lib/rancher/rke2/server/db/` + start). Version fact for the record: the flag check applies only to a bare `--cluster-reset`; the pinned version does not consult the flag when a restore path is set. This runbook never performs a bare reset \u2014 do not remove `reset-flag` manually; if it is ever encountered blocking a start, that is itself a STOP-and-escalate condition.\n  3. **Coordinated rollback**: Proxmox `qm rollback <vmid> pre-rke2-apply` applied to ALL CPs together \u2014 the snapshot predates Phase B, so this returns the cluster to the pre-B state and phases B/C must be re-run.\n  4. **Escalate to Rancher support** with the exact rke2 version and full logs.\n- A restore is a STOP-and-reassess event: after the test, re-run F6-1/F6-2 gates and only then proceed to Phase E.\n\n**STOP (F6 overall)**: any F6 STOP prerequisite P1-P4 not PASS (no server may be stopped); any F6-1/F6-2/F6-3 gate failing \u2014 the cluster is not accepted; fix before Phase E (these three are the pre-addon CP-only gates and are the ONLY Fase-6 gates blocking E). The worker-lifecycle drill is the separately-named Phase W of the same executable order (**B \u2192 C \u2192 STOP \u2192 F6-1..3 \u2192 E \u2192 D \u2192 D2 \u2192 W \u2192 F \u2192 G**); it runs after D2 and its gates are defined there. The Fase-6 acceptance ledger is COMPLETE only when F6-1..3 AND Phase W's W-1/W-2 all have dated PASS entries; W passes POST-D2 by design (ADR-0005 \u2014 see banner), never pre-addon.\n\n## Phase E \u2014 Cilium install + basic install validation (first addon)\n\n> Runs as the **immediate first addon after the CP-only Fase-6 gates (F6-1..3)** \u2014 and BEFORE workers join: with Cilium already present, newly joined workers can reach Ready as soon as their Cilium agent starts. (The worker-lifecycle drill \u2014 drain/reboot \u2014 is the separately-named **Phase W** and executes later in the executable order, AFTER D2; it does not block E.) This runbook splits Cilium work in two: **Phase E = install + basic install validation ONLY**; the **full Cilium acceptance** (connectivity test, Hubble flows, DNS, NetworkPolicy, one CiliumNode per node) runs in **Phase D2 on the FINAL 6-node topology** (see D2).\n\nPlaybook `ansible/playbooks/cilium.yml` (infrastructure PR #2, merged as e283e24): runs from the `operations` host (ops01) with `connection: local` so the Helm op executes exactly once; chart pinned `oci://quay.io/cilium/charts/cilium --version 1.20.1` (full OCI reference; the `--repo oci://` form is NOT valid); values `kubernetes/cilium/values.yaml` (VXLAN tunnel, kubeProxyReplacement false, Hubble enabled).\n\n**Gate semantics are FAIL-CLOSED**: every kubectl probe uses `--ignore-not-found` and requires rc 0 \u2014 any rc != 0 (API error, connection refused, auth/TLS failure) ABORTS the run; absence of a resource is proven ONLY by rc 0 with EMPTY stdout. An error is never \"absent\": it is a STOP.\n\nOrdering gates (fail-closed, per ADR-0005): the play refuses to run if a CNI is already deployed (Canal would fight Cilium), requires kube-proxy present (RKE2 with `cni: none` provides it), and aborts unless the Cilium agent is absent (first-install check).\n\nCommand (from ops01 \u2014 AFTER the \u00a70 item 9 ops01 provisioning is verified: checkout at d553643 or a verified descendant, Ansible venv, inventory, helm, and the current transferred kubeconfig in `KUBECONFIG`; this playbook targets the `operations` group with `connection: local`, so the Helm op runs on ops01 itself):\n\n```\nANSIBLE_CONFIG=ansible/ansible.cfg \\\n  .venv/bin/ansible-playbook -i ansible/inventories/prod/hosts.json \\\n  ansible/playbooks/cilium.yml\n```\n\nFirewall prerequisites (node LAN must allow): UDP 8472 (VXLAN) between all nodes; TCP 4244 (Hubble Relay flow scraping). MTU stays Cilium default (~1450) on the MTU-1500 LAN.\n\n### Validation gate E (install-only)\n\nBasic install validation on the CP-only (3-node) cluster, from ops01 with `KUBECONFIG` set to the transferred admin kubeconfig (\u00a70 item 9):\n\n```\ncilium status --wait     # agents Ready on the 3 CPs; KVStore/Conntrack/ControllerStatus Ok, Hubble Ok/Enabled\nkubectl -n kube-system get pods -l k8s-app=cilium    # cilium-ds Running+Ready on all 3 CPs (one per CP)\nkubectl -n kube-system get pods -l io.cilium/app=operator   # cilium-operator Running+Ready\n```\n\nThe following are deliberately **NOT Phase E gates** (they are D2 gates on the final 6-node topology): `cilium connectivity test`, Hubble flow observation, DNS resolution, NetworkPolicy enforcement, `CiliumNode` per-node completeness.\n\nAlso note (VALIDATION.md \u00a77): pods that existed before Cilium must be restarted once to fall under the eBPF datapath (in this greenfield order the only such pods are the control-plane static/system pods; kube-proxy static pods and CoreDNS are handled by rollout/restart once nodes go Ready).\n\nRollback: `helm uninstall cilium --namespace kube-system` \u2014 documented in VALIDATION.md \u00a78 with the warning that the cluster has NO other CNI after this (`cni: none` means RKE2 will not reinstall Canal), so only do this with a replacement plan.\n\n**STOP**: any fail-closed probe rc != 0; cilium-ds pod not Running+Ready on each of the 3 CPs; cilium-operator not Running+Ready.\n\n## Phase D \u2014 platform workers join (registration + label gates only)\n\nPlaybook `ansible/playbooks/rke2-workers.yml` (infrastructure PR #5, merged as 8d836e8): play 1 pre-flights `rke_platform` (asserts `rke2_server_url == https://10.72.0.20:9345`, CIDRs inherited from shared group_vars, token file exists 0600 \u2014 no_log), play 2 (hosts `rke-cp01`) applies/verifies the control-plane NoSchedule taint `node-role.kubernetes.io/control-plane=true:NoSchedule` on all 3 CPs (idempotency decided from API pre-state), play 3 joins rke-platform01..03 (`serial: 1`, `any_errors_fatal: true`, rke2-agent role; config: token from the gitignored 0600 file, `server: https://10.72.0.20:9345`, `node-label: [wetopi.io/role=platform]`, no taints).\n\nCommand (from the CONTROLLER \u2014 the workstation per \u00a70 item 9):\n\n```\nANSIBLE_CONFIG=ansible/ansible.cfg \\\n  .venv/bin/ansible-playbook -i ansible/inventories/prod/hosts.json \\\n  ansible/playbooks/rke2-workers.yml\n```\n\n**Per-worker join gate (inside the serial join play)**: for each worker, the gate is control-plane-OBSERVED registration + exact label \u2014 `kubectl get node <name> -o json` delegated to rke-cp01 (retries 60 \u00d7 10s), then assert `metadata.name == inventory_hostname` and `labels['wetopi.io/role'] == 'platform'`. A failed gate aborts before the next worker joins. **Phase D gate = registration + label ONLY.**\n\n**Ready state after join (Cilium-before-workers)**: Cilium was installed in Phase E, so a newly joined worker **SHOULD reach Ready once its Cilium agent (the cilium-ds DaemonSet pod) starts on it** \u2014 `NotReady` is a **TRANSIENT join state, NOT indefinitely expected**. Ready/pod/DNS/scheduling validation is deferred to `rke2-workers-validate.yml` (Phase D2), which must prove: 6/6 nodes Ready, a cilium-ds pod per node, and a CiliumNode object per node, plus the full Cilium acceptance. **A worker that stays NotReady indefinitely is a FAILURE to investigate (Cilium agent attachment issue), never \"expected\".**\n\nAfter the join play, the playbook reports the result; workers are expected to transition to Ready as their Cilium agents come up. Full validation runs next in D2.\n\n## Phase D2 \u2014 full post-Cilium cluster validation (rke2-workers-validate.yml + FULL Cilium acceptance, on the FINAL 6-node topology)\n\nPlaybook `ansible/playbooks/rke2-workers-validate.yml` (PR #5, merged as 8d836e8; hosts `rke-cp01`, `become: true`, `any_errors_fatal: true`, read-only queries + two short-lived uniquely-named test pods, always deleted). Run from the CONTROLLER (\u00a70 item 9 \u2014 the play reaches cp01 over SSH) AFTER Cilium (Phase E) and after the workers have joined (Phase D). This phase ALSO carries the **full Cilium acceptance** (moved from former Phase E) on the final 6-node topology:\n\n```\nANSIBLE_CONFIG=ansible/ansible.cfg \\\n  .venv/bin/ansible-playbook -i ansible/inventories/prod/hosts.json \\\n  ansible/playbooks/rke2-workers-validate.yml\n```\n\nBuilt-in gates (manual equivalents \u2014 run ON cp01 using the CP-local kubectl form, \u00a70 item 9; define once for the shell: `RKE2_KUBECTL=\"/var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml\"`):\n\n```\n$RKE2_KUBECTL get nodes                          # 6 nodes, ALL Ready (3 CP + 3 platform)\n$RKE2_KUBECTL get nodes -l wetopi.io/role=platform   # exactly 3, each carrying the exact label\n$RKE2_KUBECTL -n kube-system get pods -l k8s-app=kube-dns            # Running+Ready\n$RKE2_KUBECTL -n kube-system get pods -l k8s-app=kube-dns-autoscaler # Running+Ready\n$RKE2_KUBECTL -n kube-system get pods -l app=rke2-metrics-server     # Running+Ready\n$RKE2_KUBECTL -n kube-system get pods -l k8s-app=cilium              # Running+Ready, one per node\n$RKE2_KUBECTL -n kube-system get pods -l io.cilium/app=operator      # Running+Ready\n$RKE2_KUBECTL -n kube-system get pods -l k8s-app=hubble-relay         # Running+Ready\n$RKE2_KUBECTL -n kube-system get pods -l component=kube-proxy         # Running+Ready, one per node\n$RKE2_KUBECTL run rke2-dns-check-<suffix> --restart=Never \\\n  --image=registry.k8s.io/e2e-test-images/jessie-dnsutils:1.7 \\\n  --command -- sleep 300\n$RKE2_KUBECTL wait --for=condition=Ready pod/rke2-dns-check-<suffix> --timeout=300s\n$RKE2_KUBECTL exec rke2-dns-check-<suffix> -- nslookup kubernetes.default.svc.cluster.local\n$RKE2_KUBECTL run rke2-sched-check-<suffix> --restart=Never \\\n  --image=registry.k8s.io/e2e-test-images/busybox:1.36.1-1 \\\n  --overrides='{\"spec\":{\"nodeSelector\":{\"wetopi.io/role\":\"platform\"}}}' \\\n  --command -- sleep 300\n```\n\nExpected success:\n\n- 6/6 nodes Ready (the playbook retries up to 60 \u00d7 10s);\n- every platform worker carries the exact label `wetopi.io/role=platform` (count == 3);\n- all required kube-system components Running+Ready with the selectors above (CoreDNS `k8s-app=kube-dns` and autoscaler `k8s-app=kube-dns-autoscaler` min 1 each; metrics-server `app=rke2-metrics-server` min 1; Cilium agent `k8s-app=cilium` one per node; cilium-operator `io.cilium/app=operator` min 2; hubble-relay `k8s-app=hubble-relay` min 1; kube-proxy `component=kube-proxy` one per node);\n- DNS gate: nslookup resolves `kubernetes.default.svc.cluster.local` to answer address(es) in 10.68.0.0/16 \u2014 the playbook parses the ANSWER addresses (the Address lines AFTER the `Name: kubernetes.default...` line) into a CSV string and requires every entry to match the octet-bounded regex `^10\\.68\\.\\d{1,3}\\.\\d{1,3}$`; the resolver's own `Server: 10.68.0.10` line appears BEFORE the Name line and must never satisfy the gate;\n- scheduling gate: the test pod lands on a node of group `rke_platform` (placement on a CP = FAIL \u2014 investigate taints/labels).\n\n### D2 full Cilium acceptance (final 6-node topology, from kubernetes/cilium/VALIDATION.md; cilium/hubble CLIs on ops01 \u2014 \u00a70 item 9 ops01 provisioning \u2014 with `KUBECONFIG` set to the transferred admin kubeconfig; the `kubectl` lines in THIS block are the ops01 remote form, not CP-local)\n\n```\ncilium status --wait            # all agents ready, KVStore/Conntrack/ControllerStatus Ok, Hubble Ok/Enabled\ncilium connectivity test        # all tests pass, incl. cross-node pod-to-pod, pod-to-service, north-south\nhubble status                   # flows counters increasing, sane uptime\nhubble observe --since 5m --from-cluster   # real flows with identities and VERDICTs, not empty\nkubectl run dns-utils --image=busybox:1.36 --restart=Never -- sleep 60\nkubectl wait --for=condition=Ready pod/dns-utils --timeout=120s\nkubectl exec dns-utils -- nslookup kubernetes.default    # resolves into 10.68.0.0/16\nkubectl delete pod dns-utils\nkubectl get ciliumnodes.cilium.io    # one CiliumNode object per node (6/6); podCIDR /24s inside 10.64.0.0/14\n```\n\nNetworkPolicy sample (VALIDATION.md \u00a75):\n\n```\nkubectl create namespace cilium-validation\nkubectl -n cilium-validation run web --image=nginx:1.27 --restart=Never\nkubectl -n cilium-validation expose pod web --port 80\nkubectl apply -f kubernetes/cilium/test-default-deny.yaml     # default-deny Ingress in that namespace\nkubectl -n cilium-validation run probe --image=busybox:1.36 --restart=Never \\\n  -- wget -qO- --timeout=5 http://web                          # MUST fail (dropped)\nhubble observe --namespace cilium-validation --since 5m        # shows VERDICT Dropped\nkubectl delete -f kubernetes/cilium/test-default-deny.yaml\nkubectl delete namespace cilium-validation\n```\n\n- **One cilium-ds pod per node**: `kubectl -n kube-system get pods -l k8s-app=cilium -o wide` \u2192 exactly 6 Running+Ready pods, one on each of the 6 nodes (3 CP + 3 platform).\n- **One CiliumNode object per node**: `kubectl get ciliumnodes.cilium.io` \u2192 exactly 6, one per node, each podCIDR inside 10.64.0.0/14.\n\n**STOP (D2 overall)**: any node not Ready after the retry window; any selector returning pods that are not all Running+Ready (or missing entirely); DNS gate failing (rc != 0, zero parsed answers, or any answer outside 10.68.0.0/16); sched-check not placed on a platform-labeled node; cilium connectivity test failures; hubble observe empty; NetworkPolicy probe NOT dropped; any podCIDR outside 10.64.0.0/14; fewer than 6 CiliumNode objects or missing cilium-ds pod on any node.\n\nThe executable order (identical to the banner, the F6 heading + STOP rule, the Phase W heading, the DoD mapping, and the provenance section) is **B \u2192 C \u2192 STOP \u2192 F6-1..3 \u2192 E \u2192 D \u2192 D2 \u2192 W \u2192 F \u2192 G**: the CP-only Fase-6 gates (F6-1 quorum, F6-2 one-CP failure, F6-3 snapshot + REAL restore test) sat BEFORE this phase; **Phase W \u2014 the worker lifecycle drill (drain + reboot) \u2014 runs NEXT**, after these D2 gates pass, on the final 6-node topology. Do NOT count Phase W as completed pre-addon acceptance.\n\n## Phase W \u2014 worker lifecycle drill (drain + reboot; former F6-4/F6-5)\n\nPart of the authorized executable order (identical to the banner, the F6 heading + STOP rule, the D2 handoff, the DoD mapping, and the provenance section): **B \u2192 C \u2192 STOP \u2192 F6-1..3 \u2192 E \u2192 D \u2192 D2 \u2192 W \u2192 F \u2192 G.** This is a **SEPARATE named phase** \u2014 the worker-lifecycle portion of the Fase-6 acceptance \u2014 running **AFTER the D2 gates pass** on the final 6-node topology, **BEFORE Phase F**, and **NEVER before the addons**: drain/reboot evidence is POST-D2 by design (ADR-0005: with `cni: none`, workers cannot pass Ready \u2014 and therefore cannot pass drain/reboot gates \u2014 before Cilium exists; see the banner for the \u00a733 tension and its explicit resolution). Results are recorded in the Fase-6 acceptance ledger under their historical aliases F6-4 (drain) / F6-5 (reboot), which map to master plan \u00a735.1/\u00a735.2 drain/reboot recovery. Do NOT count this phase as completed pre-addon acceptance.\n\n### W-1 \u2014 drain one worker (former F6-4)\n\n- Command (CP-local form on cp01 \u2014 or the ops01 remote form with the transferred kubeconfig, \u00a70 item 9):\n  ```\n  /var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml drain rke-platform01 --ignore-daemonsets --delete-emptydir-data\n  /var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml uncordon rke-platform01\n  ```\n- Expected: drain evicts/reschedules eligible pods (kube-system DaemonSet pods stay \u2014 expected); node shows `SchedulingDisabled` during the drain; after uncordon the node returns to Ready/Schedulable.\n- STOP: drain hangs on un-evictable pods (investigate PodDisruptionBudgets), node not Schedulable after uncordon.\n\n### W-2 \u2014 reboot one worker + Ready return (former F6-5)\n\n- Command: `sudo reboot` on rke-platform02 (via the documented SSH form, \u00a70 item 5), then on cp01 (CP-local form):\n  ```\n  /var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml get node rke-platform02 -w    # wait for Ready\n  ```\n- Expected: node cycles NotReady \u2192 Ready within ~2-3 min of boot (kubelet re-registers, Cilium agent re-attaches); label `wetopi.io/role=platform` still present; no NotReady residue after ~5 min.\n- STOP: node not Ready after 10 min, label lost, or the Cilium agent on that node not Running+Ready after reboot.\n\n**STOP (W overall)**: either W-1/W-2 gate failing \u2014 fix before Phase F. With W-1/W-2 recorded PASS (post-D2) alongside F6-1..3 (pre-addon), the Fase-6 acceptance ledger is COMPLETE; the DoD maps drain/reboot recovery to this POST-D2 evidence, never to pre-addon acceptance.\n\n## Phase F \u2014 Ceph routing + RBD CSI\n\nSource artifacts (all on merged main): `ansible/roles/nat_gateway` (vars `natgw_ceph_routing_enabled: false`, `natgw_ceph_address: \"\"` \u2014 default-off until gates), `kubernetes/ceph-csi/` (chart `ceph-csi-rbd` pinned 3.17.1, StorageClass `ceph-rbd`, `secret.example.yaml`), `ansible/playbooks/ceph-csi.yml`, `kubernetes/ceph-csi/VALIDATION.md`, `kubernetes/ceph-csi/README.md`.\n\nPhase F has TWO independent sub-phases; the CSI part does not block the cluster (RKE2 installs with no storage class).\n\n### F0 \u2014 tofu NIC change (NEW precondition for F1)\n\nThe third NIC for the Ceph leg lives in `tofu/proxmox/environments/prod/terraform.tfvars.example`: a commented-out block at the end of the wetopi-natgw01 nic list with the ADR-0004 comment above it (VLAN 2 / vmbr2, address from 192.168.96.0/20; guest-side config via the nat_gateway role's `natgw_ceph_*` vars, NOT cloud-init). F1 requires it enabled via a **reviewed PR**, not an apply-time edit:\n\n1. Open a PR uncommenting the block with the allocated address:\n   ```\n   # {\n   #   bridge       = \"vmbr2\"\n   #   ipv4_address = \"192.168.96.<allocated>/20\"\n   #   firewall     = false\n   # }\n   ```\n   \u2192 `{ bridge = \"vmbr2\", ipv4_address = \"192.168.96.<allocated>/20\", firewall = false }`.\n2. Merge it (wetopi-approver review), then on ops01 check out a **clean main** and run:\n   ```\n   tofu -chdir=tofu/proxmox/environments/prod plan\n   ```\n3. **tofu plan gate (record the output)**: the plan must show ADD of **exactly one NIC on wetopi-natgw01** and **ZERO destroys** (no other resource touched). Any destroy = STOP.\n4. `tofu apply` = **HUMAN gate** \u2014 a human explicitly reviews the plan and runs the apply; record evidence (plan summary + apply output) in the apply log.\n\n**STOP (F0)**: plan shows any destroy; plan touches any other VM/resource; apply fails.\n\n### F1 \u2014 Routing\n\nADR-0004 shape: route, not NAT; third Nic on wetopi-natgw01, VLAN 2, into Ceph LAN 192.168.96.0/20.\n\nPreconditions (human, from VALIDATION.md \u00a71 \u2014 each gate verified and recorded BEFORE flipping the vars):\n\n| Gate | Check |\n|---|---|\n| (a) VLAN 2 address allocation | pick a free address in 192.168.96.0/20 \u2014 check live allocations (`ip -4 route`, Ceph-side `ceph mon dump`) |\n| (b) Ceph-side return-route owner | who installs `ip route 10.72.0.0/23 via <natgw VLAN 2 addr>` on Ceph hosts (not managed by us \u2014 confirm with Ceph owner, record mechanism) |\n| (c) firewall path both directions | `nft list chain inet wetopi_filter forward` shows the two ceph_net accept rules; `nft list table ip wetopi_nat` still scopes NAT to oifname WAN only |\n| (d) rp_filter=1 behavior | `sysctl net.ipv4.conf.all.rp_filter net.ipv4.conf.default.rp_filter`; asymmetric drops = missing gate b |\n| (e) MTU 1500 on VLAN 2 | from a CP: `ping -c 3 -M do -s 1472 <natgw-ceph-addr>` passes, `-s 1473` fails |\n| (f) no CIDR overlap | 10.64.0.0/14 + 10.68.0.0/16 vs 192.168.96.0/20 + 10.72.0.0/23 (`ip -4 route` review; `ip route get <ceph-host>` takes the gateway) |\n\nApply: **via reviewed PR, not an apply-time edit** \u2014 open a PR setting `natgw_ceph_routing_enabled: true` + `natgw_ceph_address: <chosen>` in `ansible/inventories/prod/group_vars/all.yml`, merge it, then check out a clean main and re-run the baseline playbook (idempotent; its first play applies nat_gateway to wetopi-natgw01):\n\n```\nANSIBLE_CONFIG=ansible/ansible.cfg \\\n  .venv/bin/ansible-playbook -i ansible/inventories/prod/hosts.json \\\n  ansible/playbooks/bootstrap.yml\n```\n\nGateway checks (on wetopi-natgw01, VALIDATION.md \u00a72): `ip -4 route` shows `192.168.96.0/20 dev eth2 ... src <natgw_ceph_address>`; `ip route get 192.168.96.10` via eth2; `ip route get 10.72.0.21` still via eth0 src 10.72.0.2. Ceph-side (prerequisite, VALIDATION.md \u00a73): `ip route get 10.72.0.21` on a Ceph host must go via the natgw VLAN 2 address.\n\nCP-node connectivity (from 10.72.0.21, VALIDATION.md \u00a74):\n\n```\nping -c 3 192.168.96.10          # Ceph monitor; 0% loss\ntraceroute -n 192.168.96.10      # one hop 10.72.0.1 (gateway), then the mon host\nnc -vz -w 3 192.168.96.10 3300   # msgr2 open\nnc -vz -w 3 192.168.96.10 6789   # msgr1 open\ntimeout 3 bash -c 'echo > /dev/tcp/192.168.96.10/3300' && echo OK   # fallback if nc absent\n```\n\n### F2 \u2014 CSI install\n\nHuman prerequisites (README OPEN list): dedicated Ceph pool (suggested `kubernetes`); dedicated Ceph user + caps (e.g. `client.kubernetes-csi`, never admin); live `clusterID` (fsid) + monitor addresses replacing the placeholders in `kubernetes/ceph-csi/values.yaml` + `storageclass.yaml` \u2014 **via reviewed PR, then merge and a clean main checkout** (placeholders are tracked files; never edit them at apply time); `csi-rbd-secret` in `kube-system` created BY HAND from that user's keyring per `secret.example.yaml` (key material never enters Git; Vault flow later).\n\n**STOP [encryption-gate] before creating `csi-rbd-secret`**: the Ceph key stored in a Kubernetes Secret must be confirmed PROTECTED at rest by the **measured-protection checks of \u00a70 item 8** (the operative gate rule \u2014 current protection is determined by measured evidence, not by config-template contents; this build's default-on AES-CBC state is the baseline, re-verify, do not assume). Before creating the Secret: `rke2 secrets-encrypt status` on all three CPs showing `Encryption Status: Enabled` / `Current Rotation Stage: start` / `Server Encryption Hashes: All hashes match` and an Active `* AES-CBC aescbckey...` key (\u00a70 item 8 check (i)). Then create `csi-rbd-secret` per the prerequisites above and immediately run check (ii) on it: `etcdctl ... get /registry/secrets/kube-system/csi-rbd-secret` via the approved kubectl-exec 17-arg form (\u00a70 item 8) showing `k8s:enc:aescbc` ciphertext, NOT the plaintext from `kubectl get secret csi-rbd-secret -n kube-system -o jsonpath='{.data.<KEY>}' | base64 -d`. **STOP if either check fails or shows plaintext \u2014 do not run the csi playbook.** (The explicit reviewed `secrets-encryption: true` line is the separately tracked future hardening/rotation prerequisite per \u00a70 item 8 \u2014 it is not an unlock condition here while the measured checks pass.) Then:\n\n```\nANSIBLE_CONFIG=ansible/ansible.cfg \\\n  .venv/bin/ansible-playbook -i ansible/inventories/prod/hosts.json \\\n  ansible/playbooks/ceph-csi.yml\n```\n\n(Run from the controller or ops01 \u2014 the play targets `rke_control_plane` with `connection: local` + `run_once: true`, so helm/kubectl talk to the cluster API from wherever ansible runs; kubeconfig resolution: `-e kubeconfig=` \u2192 `$KUBECONFIG` \u2192 `/etc/rancher/rke2/rke2.yaml` \u2014 on a non-CP host the transferred kubeconfig must be in `KUBECONFIG`, \u00a70 item 9.)\n\nThe playbook fails early on placeholders or missing secret (by design); it targets `rke_control_plane` with `connection: local`, `run_once: true`, kubeconfig resolution: `-e kubeconfig=` \u2192 `$KUBECONFIG` \u2192 `/etc/rancher/rke2/rke2.yaml`; helm `upgrade --install ceph-csi-rbd ceph-csi-rbd --repo https://ceph.github.io/csi-charts --version 3.17.1 -n kube-system --values kubernetes/ceph-csi/values.yaml`; then applies `storageclass.yaml`.\n\nPVC acceptance (VALIDATION.md \u00a75, maps master plan \u00a735.3):\n\n```\nkubectl -n kube-system get pods -l app=ceph-csi-rbd     # all Running\nkubectl get storageclass ceph-rbd                       # provisioner rbd.csi.ceph.com, allowVolumeExpansion true\nkubectl apply -f - <<'EOF'\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: ceph-rbd-test\nspec:\n  accessModes: [\"ReadWriteOnce\"]\n  storageClassName: ceph-rbd\n  resources:\n    requests:\n      storage: 1Gi\nEOF\nkubectl get pvc ceph-rbd-test -w     # Bound within ~1 min\n# (b) mount+write: kubectl run ceph-rbd-test --rm -it --restart=Never --image=nginx:alpine \\\n#   --overrides='{\"spec\":{\"containers\":[{\"name\":\"ceph-rbd-test\",\"image\":\"nginx:alpine\",\"command\":[\"sh\"],\"stdin\":true,\"tty\":true,\"volumeMounts\":[{\"name\":\"data\",\"mountPath\":\"/mnt/data\"}]}],\"volumes\":[{\"name\":\"data\",\"persistentVolumeClaim\":{\"claimName\":\"ceph-rbd-test\"}}]}}'\n#   inside: date > /mnt/data/marker.txt && cat /mnt/data/marker.txt && exit\n# (c) persistence: delete the pod, re-run the SAME kubectl run, marker still readable with the same timestamp\n# (d) failure test: kubectl delete pvc ceph-rbd-test; on a Ceph host: rbd ls kubernetes  \u2192 no csi-vol- image for the PVC\n# (e) performance: no CSI errors/timeouts in kubectl -n kube-system logs deploy/ceph-csi-rbd-provisioner (and node plugin logs) across (a)-(d); provisioning within ~1 min\n```\n\nRollback F: routing \u2014 revert `natgw_ceph_routing_enabled: false` (and clear the address) **via reviewed PR** and re-run `bootstrap.yml` (the role removes the leg; the default-off state is the reviewed baseline). CSI \u2014 inverse of the install: `helm uninstall ceph-csi-rbd -n kube-system`, `kubectl delete -f kubernetes/ceph-csi/storageclass.yaml`, delete test PVCs first; keep `csi-rbd-secret` out of Git always. *(This rollback line is runbook-authored inverse of the playbook install; the parent README documents no uninstall procedure.)*\n\n**STOP (F0)**: plan shows destroys or extra resource churn.\n**STOP (F1)**: missing connected route, wrong next-hop, ping loss, TCP 3300/6789 closed, MTU probe failing.\n**STOP (F2)**: [encryption-gate] measured-protection checks failing (\u00a70 item 8); PVC Pending (describe it; check provisioner logs \u2014 usual causes: placeholder clusterID/monitors, wrong userKey, routing regression), any acceptance step (b)-(e) failing.\n\n## Phase G \u2014 Argo CD\n\nSource: infrastructure PR #1 (MERGED, main): `kubernetes/gitops/` + `docs/gitops.md`. Pinned Argo CD v3.4.8, non-HA, vendored manifest, namespace `argocd`. Run from ops01 with a clean checkout of infrastructure main at d553643 or a verified descendant (\u00a70 item 9 ops01 provisioning; every `kubectl` below is the ops01 remote form with `KUBECONFIG` set to the transferred admin kubeconfig). **`docs/gitops.md` (infrastructure repo) is the authoritative step-by-step procedure and has been reconciled to the deployed state \u2014 its steps 1-19 now match what was applied: HTTPS repository Secret, split/server-side CRD apply, current encryption status, drift deferral. The commands below are the same procedure restated; where wording differs, follow `docs/gitops.md`.**\n\nInstall (docs/gitops.md steps 1-6); SSH to ops01 (the bastion itself, 92.222.245.91) is DIRECT \u2014 no ProxyCommand (reserve the ProxyCommand form for LAN targets behind ops01):\n\n```\nssh -i /opt/data/.ssh/bigardo ubuntu@92.222.245.91\ncd ~/wetopi-infrastructure\ngit fetch origin && git checkout main && git pull --ff-only\nsha256sum kubernetes/gitops/bootstrap/argocd-install-v3.4.8/install.yaml   # optional integrity check\nkubectl apply -f kubernetes/gitops/bootstrap/argocd-install-v3.4.8/00-namespace.yaml\n# The pinned install.yaml is a 59-document manifest. The CRD\n# applicationsets.argoproj.io (1385776 bytes raw; ~341KB serialized) exceeds\n# kubectl's 262144-byte client-side last-applied-configuration annotation\n# cap, so a broad client-side apply of the whole manifest FAILS on that\n# object (\"metadata.annotations: Too long\"). The other large CRD,\n# applications.argoproj.io, serializes to ~191KB and stays under the cap \u2014\n# only applicationsets needs special handling. REQUIRED METHOD (validated\n# for both first install and re-apply; no CRD-presence branch, no extra\n# tools \u2014 plain awk): split the CRD out, apply it SERVER-SIDE (idempotent\n# whether the CRD is absent or already present), apply the remaining 58\n# objects client-side. Same form for installs, re-applies after upgrades,\n# and the Phase G rollback path.\nawk '\n  function flush() {\n    if (buf == \"\") { return }\n    if (want) { print buf > \"/tmp/argocd-install-appsets-crd.yaml\" }\n    else {\n      if (n++) print \"---\" > \"/tmp/argocd-install-rest.yaml\"\n      print buf > \"/tmp/argocd-install-rest.yaml\"\n    }\n    buf = \"\"; want = 0; kind = 0\n  }\n  /^---$/ { flush(); next }\n  /^kind: CustomResourceDefinition$/ { kind = 1 }\n  /^  name: applicationsets\\.argoproj\\.io$/ { if (kind) want = 1 }\n  { buf = buf $0 \"\\n\" }\n  END { flush() }\n' kubernetes/gitops/bootstrap/argocd-install-v3.4.8/install.yaml\n# sanity check before applying (expected counts on the pinned manifest):\ngrep -c '^apiVersion:' /tmp/argocd-install-appsets-crd.yaml   # 1\ngrep -c '^apiVersion:' /tmp/argocd-install-rest.yaml           # 58\nkubectl apply --server-side -f /tmp/argocd-install-appsets-crd.yaml\nkubectl apply -n argocd -f /tmp/argocd-install-rest.yaml\nrm /tmp/argocd-install-appsets-crd.yaml /tmp/argocd-install-rest.yaml\nkubectl apply -f kubernetes/gitops/bootstrap/argocd-install-v3.4.8/01-cmd-params.yaml\n```\n\nValidation (steps 7-12): `kubectl -n argocd get pods` (application-controller StatefulSet 1 replica by pin, applicationset-controller, dex (idle; SSO OPEN), redis, repo-server, server \u2014 all Running/Ready); port-forward `kubectl -n argocd port-forward svc/argocd-server 9090:443 --address=127.0.0.1`; install CLI `curl -sSLf -o /tmp/argocd https://github.com/argoproj/argo-cd/releases/download/v3.4.8/argocd-linux-amd64` + `chmod +x` + `sudo mv /usr/local/bin` + `argocd version --short` (expect v3.4.8 client+server); initial password `kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d; echo` (never commit); `argocd login 127.0.0.1:9090 --username admin --insecure`. UI from workstation via:\n`ssh -i /opt/data/.ssh/bigardo -L 8443:127.0.0.1:9090 ubuntu@92.222.245.91` (direct to the bastion, NO ProxyCommand \u2014 ops01 IS the bastion; the ProxyCommand form stays reserved for LAN targets behind ops01).\n\n**[encryption-gate] on the repo credential (steps 13-17) \u2014 ORDERED GATE PROCEDURE (status-check BEFORE, ciphertext-verify AFTER, cleanup STOP on failure).** The Argo CD repo credential is stored as a Kubernetes Secret, and \u2014 key semantic \u2014 **creating the labeled repository Secret IS the registration**: Argo CD auto-discovers any Secret carrying the label `argocd.argoproj.io/secret-type=repository`, so a create-without-registering sequence does not exist. The gate therefore does not \"hold registration until after the check\"; it verifies the at-rest protection of the Secret that already exists, per the **measured-protection checks of \u00a70 item 8** (the operative gate rule: current protection = measured evidence, not config-template contents). Execute in this exact order:\n\n1. **Check (i) FIRST \u2014 before creating the Secret**: `rke2 secrets-encrypt status` on all three CPs, each showing `Encryption Status: Enabled` / `Current Rotation Stage: start` / `Server Encryption Hashes: All hashes match` / Active `* AES-CBC aescbckey` key (\u00a70 item 8 check (i)). Fails \u2192 STOP here, before any credential material enters the cluster; re-verify before continuing.\n2. **Create/update the labeled Secret** `argocd/repo-infra-https` exactly per the \"Repo credential\" block below (protected `read -rs` token input; stdin-only `kubectl -n argocd apply -f -` heredoc; label `argocd.argoproj.io/secret-type=repository`; fields `type: git`, `url`, `username`, `password`). Creation auto-registers the credential (Argo CD label auto-discovery).\n3. **Immediately run check (ii) on the created Secret**: raw-etcd ciphertext probe via the approved kubectl-exec etcdctl form (\u00a70 item 8) \u2014 `get /registry/secrets/argocd/repo-infra-https` must return `k8s:enc:aescbc` ciphertext, NOT the plaintext/base64 of the token.\n4. **Check (ii) fails or shows plaintext \u2192 CLEANUP STOP**: `kubectl -n argocd delete secret repo-infra-https` (this also removes the auto-registered credential), `unset ARGOCD_REPO_TOKEN`, then STOP \u2014 do not proceed to the root Application \u2014 and **revoke/rotate the token on GitHub as appropriate** (a credential that sat readable in etcd must be treated as exposed). Do not re-attempt from step 2 until the at-rest encryption fault is found, fixed, and re-measured (checks (i) and (ii) green again).\n5. **Only after check (ii) passes**: the GATE of step 17 \u2014 `argocd repo list` shows `https://github.com/wetopi-platform/infrastructure.git` CONNECTION STATUS `Successful` \u2014 and only then proceed to the root Application (steps 18-19).\n\nThis gate does NOT block the Argo CD install itself (steps 1-12 above), only the private-repo credential.\n   **APPLY-TIME VERDICT (2026-08-30, card t_e0cace4b)**: gate PASSED on measured evidence, in exactly the ordered sequence above \u2014 check (i) verified on all 3 CPs BEFORE the credential Secret existed; the labeled Secret `argocd/repo-infra-https` was then created, which auto-registered the credential via Argo CD's label auto-discovery; raw-etcd check (ii) immediately probed the created Secret and returned `k8s:enc:aescbc:v1:aescbckey:` ciphertext (685B total, NOT plaintext; `argocd/argocd-initial-admin-secret` also probed: 316B ciphertext); only after that verification did execution proceed to the repo-list gate (`argocd repo list` \u2192 Successful) and the root Application. The cleanup STOP path was not needed.\n\nRepo credential (steps 13-17; private repo \u2192 mandatory before first sync). **APPLY-TIME DEVIATION (lead decision, card t_e0cace4b) \u2014 the deploy-key path is SUPERSEDED**: GitHub org policy DISABLES deploy keys on wetopi-platform/infrastructure (API returns HTTP 422 \"Deploy keys are disabled for this repository\", org-level setting), so the deployed mechanism is an **HTTPS read credential** \u2014 the token-based repository Secret below, auto-discovered by Argo CD. The exact step-by-step procedure (steps 13-16, secret-safe form) lives in `docs/gitops.md` in the infrastructure repo, \"Private repository credential (apply-phase, before first sync)\" \u2014 read token from GitHub into a shell variable via `read -rs` (protected input, never typed into a command or history); create/update Secret `argocd/repo-infra-https` in the `argocd` namespace by `kubectl -n argocd apply -f -` heredoc referencing `${ARGOCD_REPO_TOKEN}` (the manifest exists only on stdin \u2014 never in Git, never a file on disk) with the required label `argocd.argoproj.io/secret-type=repository` and required stringData fields `type: git`, `url: https://github.com/wetopi-platform/infrastructure.git`, `username: box-daxter`, `password: <the variable>`; `unset ARGOCD_REPO_TOKEN` after; rotation = re-run the same block, cleanup = `kubectl -n argocd delete secret repo-infra-https` + revoke the token on GitHub. Do NOT instead use inline `argocd repo add --password ...` \u2014 an inline add was tried at apply time and registered an empty credential (STATUS Failed); the labeled Secret is the only registration. When Infisical lands, this credential migrates to the vault contract.\n\n**GATE (step 17, mandatory)**: `argocd repo list` shows `https://github.com/wetopi-platform/infrastructure.git` CONNECTION STATUS `Successful`. Do not proceed to the root Application until this passes. **PASSED at apply time (t_e0cace4b)** via the Secret auto-discovery above (one recorded correction: an initial inline-password `argocd repo add` registered an empty credential, STATUS Failed \u2014 entry removed; the labeled Secret is now the only registration).\n\nFirst app sync (steps 18-19):\n\n```\nkubectl apply -f kubernetes/gitops/app.project.yaml -f kubernetes/gitops/app.root.yaml\nargocd app list       # one row: root, project=platform, in-cluster, Synced/Healthy\nargocd app get root   # STATUS Synced, HEALTH Healthy, source infrastructure main @ kubernetes/platform\nargocd app sync root  # no-op sync (target path holds placeholders only)\nargocd app wait root --health --sync --timeout 120\nkubectl get ns platform    # created by the sync via CreateNamespace=true\n```\n\nDrift check (documented expectations): Git is desired state; manual kubectl edit/apply/delete inside the cluster is drift and shows `OutOfSync`; `root` runs `automated.prune=true, selfHeal=true` so drift self-heals within the reconcile interval and every correction lands in `argocd app history root`. Drift procedure to record: make a manual change in the platform namespace, confirm `argocd app get root` shows OutOfSync then self-heals back, confirm the correction in `argocd app history root`; preferred rollback is always a Git revert (Argo reconciles it) \u2014 `argocd app rollback <id>` exists for emergencies but must be followed by a Git fix or selfHeal re-applies Git state. Note the Argo CD control plane itself is apply-only from pinned manifests (procedural exclusion, re-apply `install.yaml` after reviewed upgrades).\n   **APPLY-TIME DEVIATION \u2014 drift check DEFERRED (card t_e0cace4b)**: structurally untestable at bootstrap. `kubernetes/platform/` holds a README placeholder only (0 tracked resources), so the Application has no desired state to diverge from: a probe ConfigMap created manually in the `platform` ns kept `Synced/Healthy` (Argo cannot flag an object outside its tree; OutOfSync/selfHeal/prune have nothing to act on). Probe deleted; cluster left clean. The `automated.prune=true, selfHeal=true` manifest evidence IS factual (verified in the app JSON spec). The check is **trivially testable once \u22651 real workload exists under `kubernetes/platform/`** (e.g. the first app-of-apps child of a later phase) \u2014 re-run the drift procedure above then, and test drift by EDITING or DELETING one of the Git-managed objects in the cluster (never by creating another unmanaged object outside the Application's tree, which is invisible to Argo by design).\n\n**Apply-time deviations (2026-08-30, card t_e0cace4b \u2014 reconciled into this runbook and the repo):**\n1. **Server-side CRD apply (required method)**: `applicationsets.argoproj.io` CRD (1385776 bytes raw; ~341KB serialized) exceeds kubectl client-side apply's 262144-byte `last-applied-configuration` annotation cap \u2014 a broad client-side apply of the whole install manifest fails with `metadata.annotations: Too long`. **Required apply method: awk-split that single CRD out of the pinned manifest and `kubectl apply --server-side -f` it** (recorded `serverside-applied`; idempotent on first install and re-apply \u2014 no CRD-presence branch needed); the remaining 58 objects stay client-side apply per this runbook. Any re-apply of the pinned manifest must use the same split form.\n2. **HTTPS credential instead of SSH deploy key** (lead decision): both `kubernetes/gitops/app.project.yaml` and `app.root.yaml` were applied with `repoURL`/`sourceRepos` `git@github.com:wetopi-platform/infrastructure.git` \u2192 `https://github.com/wetopi-platform/infrastructure.git` (deploy keys disabled at org level); credential = Secret `argocd/repo-infra-https` (see \"Repo credential\" above). The infrastructure repo is reconciled to the applied state by PR (infrastructure) \u2014 repoURL lines updated to HTTPS; the app yamls now match what is deployed.\n3. **Drift self-heal check deferred** \u2014 structural reason and re-test condition as recorded above.\n\n**Execution evidence (t_e0cace4b, 2026-08-30)**: Argo CD v3.4.8 installed (ops01 checkout at main f841ae7, PIN SHA-256 e223122b\u2026 matched docs/gitops.md); 7/7 pods Running/Ready; CLI v3.4.8 client+server match; encryption gate PASSED on measured AES-CBC evidence in the ordered sequence (check (i) pre-credential, check (ii) ciphertext on the created `repo-infra-https` Secret, cleanup path not needed); repo credential Successful; `argocd app list` \u2192 one row `root`, project `platform`, in-cluster, `https://github.com/wetopi-platform/infrastructure.git` @ `kubernetes/platform` @ main, SYNCPOLICY Auto-Prune; first sync Succeeded (no-op, placeholders only), root Synced to main (b610209) Healthy; ns `platform` Active; history ID 0.\n\n**STOP**: pods not all Running; [encryption-gate] measured-protection checks failing per the ordered procedure (check (i) failing at step 1, or check (ii) failing/showing plaintext at step 3 \u2192 cleanup STOP: delete `argocd/repo-infra-https`, `unset ARGOCD_REPO_TOKEN`, revoke/rotate the token, stop \u2014 \u00a70 item 8 and the ordered gate procedure above); `argocd repo list` not Successful; root app not Synced+Healthy after `argocd app wait`. (Drift-check STOP superseded by the deferral above: no drift gate applies while `kubernetes/platform/` tracks 0 resources \u2014 re-test when the first workload lands.)\n\n## Rollback story (general, per phase)\n\nOn ANY phase failure \u2014 stop, do not proceed; read the playbook/task failure output first (all plays run `any_errors_fatal`); the run is serial, so a failed phase leaves the cluster in a known partial state. Order of escalation: (1) fix forward and re-run the phase playbooks (roles are idempotent, serial-safe); (2) per-node rollback (below); (3) etcd snapshot restore via the **cluster-reset flow** (`systemctl stop rke2-server` on all CPs \u2192 `rke2 server --cluster-reset --cluster-reset-restore-path=<PATH-TO-SNAPSHOT>` on cp01 \u2192 start cp01 \u2192 `rm -rf /var/lib/rancher/rke2/server/db/` + start on cp02/cp03 \u2192 re-verify quorum + `/readyz` \u2014 see Phase F6-3, INCLUDING its reset-failure rule: a failed reset is an immediate STOP-and-escalate condition, NOT a retry \u2014 `reset-flag` blocks repeated resets; there is NO `rke2 etcd-snapshot restore` subcommand \u2014 and the F6 STOP prerequisites P1-P4 gate ANY cluster-reset restore, including from this rollback path) for cluster-state corruption; (4) Proxmox `qm rollback <vmid> pre-rke2-apply` (last resort \u2014 cluster-level state like etcd membership makes node-level rollback of ONE CP risky once quorum exists: prefer etcd member removal for a node you are rebuilding rather than snapshot rollback of a single CP mid-quorum; snapshot rollback is safest applied to ALL CPs together or before Phase B).\n\nPer-phase specifics (from parent docs; the per-phase STOP blocks above remain the authoritative inline version):\n\n- **B/C** (from the CP runbook): stop RKE2 `systemctl stop rke2-server`; per node serially remove `/etc/rancher/rke2`, `/var/lib/rancher/rke2`, `/var/lib/kubelet`; keepalived may stay (role idempotent) or be removed; re-run the playbook from scratch after regenerating the token; do NOT manually delete etcd members unless removing a node permanently \u2014 then remove the member with the same kubectl-exec etcdctl form (Phase B gate) with `member remove <ID>`. Rebuild-a-node criterion: node whose config/state is unrecoverable (repeated join failures, corrupt etcd data dir) \u2014 snapshot-rollback or recreate via tofu, then re-run the CP join play.\n- **D** (from the worker runbook): per node serially `systemctl stop rke2-agent`, remove `/etc/rancher/rke2`, `/var/lib/rancher/rke2`, `/var/lib/kubelet`, then `kubectl delete node rke-platform0X`; re-run the workers playbook after fixing the root cause (idempotent).\n- **E**: `helm uninstall cilium --namespace kube-system` (documented; no other CNI behind it \u2014 replacement plan required).\n- **W**: no install state to roll back \u2014 if interrupted mid-drill, uncordon (CP-local form) and re-verify the node Ready; a worker that fails W-2 recovery follows the **D** bullet (agent wipe + node delete + re-join).\n- **F**: F0 \u2014 revert the tfvars PR (reviewed); routing \u2014 revert the natgw vars PR to default-off + re-run `bootstrap.yml`; CSI inverse (uninstall chart, delete StorageClass) as authored above.\n- **G**: GitOps plane state is rebuildable from Git \u2014 re-apply the pinned manifests using the SAME split form as the install (awk-split the `applicationsets.argoproj.io` CRD out, `kubectl apply --server-side -f` the CRD file, `kubectl apply -n argocd -f` the remaining-objects file; a broad client-side apply of the whole `install.yaml` fails on that CRD by design), re-register the repo credential (the `argocd/repo-infra-https` labeled Secret per `docs/gitops.md` steps 13-16), re-sync; app-level rollback = Git revert (`argocd app history root` records syncs). Full cluster loss = rebuild from the tofu/ansible phases, then this runbook top to bottom.\n\n## Definition of done \u2014 master plan \u00a735.1/35.2 mapped to concrete commands\n\n| Plan item | Concrete check in this runbook |\n|---|---|\n| \u00a735.1 p\u00e9rdida de un CP no rompe API | Phase C one-CP-outage test + Phase F6-2 (`systemctl stop rke2-server` on cp02, `curl -k https://10.72.0.20:6443/readyz` \u2192 ok) |\n| \u00a735.1 etcd quorum correcto | kubectl-exec etcdctl member list == 3 members (Phase B/C gates + F6-1) |\n| \u00a735.1 nodes reproducibles desde IaC | phases B-E run purely from tofu-generated inventory + ansible roles (re-run of the same playbooks reproduces the state; snapshot rollback to pre-rke2 then re-run = full reproduction test) |\n| \u00a735.1 no customer workloads en CP | workers playbook play 2 taint enforcement (`node-role.kubernetes.io/control-plane=true:NoSchedule` on all CPs) + Phase C taint record + `get pods -o wide` scan + D2 scheduling gate placement |\n| \u00a735.1 snapshot funciona / restore probado | F6 STOP prerequisite P2 (`rke2 etcd-snapshot save --name pre-addons-fase6` + `rke2 etcd-snapshot ls`, external copy outside cp01's failure domain) + P3 (SHA-256 match on cp01 and the external copy), then F6-3: the **EXECUTED cluster-reset restore test** (`systemctl stop rke2-server` all CPs \u2192 `rke2 server --cluster-reset --cluster-reset-restore-path=<PATH-TO-SNAPSHOT>` on cp01 \u2192 start cp01 \u2192 `rm -rf /var/lib/rancher/rke2/server/db/` + start on cp02/cp03 \u2192 re-verify quorum 3/3 + `/readyz` + the post-restore journal checks on all three CPs). DoD maps \"restore probado\" to THIS executed-test evidence \u2014 snapshot-save-only is NOT restore probado. If the restore test cannot be executed, this DoD item is UNMET and the runbook mandates STOP before all subsequent phases (E onward). |\n| \u00a735.2 Pod-to-Pod + Pod-to-Service | `cilium connectivity test` (Phase D2, final 6-node topology) |\n| \u00a735.2 DNS | Phase D2 DNS gate (answer parsed into 10.68.0.0/16) + Phase D2 `dns-utils` nslookup (final topology) |\n| \u00a735.2 Hubble visibility | `hubble status` + `hubble observe` (Phase D2, final 6-node topology \u2014 real flows + VERDICTs) |\n| \u00a735.2 no CIDR collision | gate (f) `ip -4 route` review + `kubectl get ciliumnodes.cilium.io` podCIDR \u2282 10.64.0.0/14 (Phase D2) |\n| \u00a735.2 Ceph reachable desde nodes | Phase F1 connectivity block (ping/traceroute/nc 3300+6789) + PVC Bound |\n| \u00a735.2 DSR demostrado por packet capture | **NOT covered by phases 0-G** (edge/DSR is later work per the master plan); recorded as an explicit gap |\n| \u00a735.3 Storage: PVC create/persistence/pod recreation/failure test | Phase F2 steps (a)-(d) |\n| \u00a735.3 Storage: performance | Phase F2 step (e) |\n| \u00a735.4 GitOps: Argo reconcilia desde Git | first sync block |\n| \u00a735.4 GitOps: drift detectable | drift procedure |\n| \u00a735.4 GitOps: Git conserva estado deseado | selfHeal + Git-revert rollback rule |\n| \u00a735.4 GitOps: rollback probado | Git revert is exercised only in a real incident \u2014 recorded as \"procedure documented, not yet exercised\" |\n| \u00a735.1/\u00a735.2 node drain + reboot recovery | **Phase W** (W-1 drain, W-2 reboot + Ready return; former F6-4/F6-5) \u2014 the separately-named post-D2 phase, executed on the final 6-node topology in the executable order **B \u2192 C \u2192 STOP \u2192 F6-1..3 \u2192 E \u2192 D \u2192 D2 \u2192 W \u2192 F \u2192 G**. The DoD maps drain/reboot to this POST-D2 evidence, NOT to pre-addon acceptance: \u00a733 places these Fase-6 items before Fase 7 (Cilium), but ADR-0005 requires Cilium as the immediate first addon with no pre-Cilium worker-Ready gates \u2014 with `cni: none` the drain/reboot gates are unexecutable before Cilium exists (see the banner for the \u00a733/ADR-0005 tension and its explicit resolution). F6-1..3 (quorum, CP failure, snapshot + REAL restore test) remain the PRE-addon CP-only evidence. |\n\n**Runbook complete when every phase gate above has a dated PASS entry in the apply log.**\n\n## Provenance\n\n- **Execution status (2026-08-30, post-Phase-D)**: the live apply has RESUMED and executed B, C, F6-1..3, E, and D fully green (details in the status banner; cluster 6/6 nodes Ready). **Historical note (superseded):** the 2026-08-29 23:17-23:26 UTC attempt (task t_a88bb36b, wetopi-ops) was **HALTED BEFORE PHASE B and FROZEN** (operator directive #38) after failing at `rke2-server : Create the RKE2 server configuration` with **`Destination directory /etc/rancher/rke2 does not exist`** on rke-cp01 \u2014 neither rke2 role created the config dir (byte-identical defect in rke2-agent). Infrastructure **PR #6** (`fix/rke2-config-dir-create`, head 3ff2ee8, merged main d553643, 2026-08-29 23:56 UTC) is the fix: config-dir creation (root:root 0750) before templating in both roles. Residual pre-halt state on cp01 (keepalived installed+active, RKE2 binaries present, `/etc/rancher` absent, rke2-server inactive) was absorbed cleanly by the resumed run \u2014 the roles are idempotent (RKE2 install guarded by `creates:`). The former resume gates (infrastructure checkout at d553643 or verified descendant per the \u00a70 STEP 0 gate, harness V2 validation, explicit lead unblock) were all satisfied on 2026-08-30 before resume; they remain historical prerequisites of that resume, not open conditions.\n- Verified against merged heads on 2026-08-30 (~00:15Z): infrastructure `main` @ **d553643** (PR #6 merge \u2014 config-dir fix for both RKE2 roles; prior PRs #1 8f3e0e8, #2 e283e24, #3 76782dc, #4 b15a087, #5 8d836e8 all merged \u2014 no open-PR references remain), docs `main` @ 5740404 (ADR-0005 and platform runbooks in main).\n- **Executable order (identical in the banner, the F6 heading + STOP rule, the Phase W heading, the D2 handoff, the DoD mapping, and here): B \u2192 C \u2192 STOP \u2192 F6-1..3 \u2192 E \u2192 D \u2192 D2 \u2192 W \u2192 F \u2192 G** (STOP = the mandatory post-Phase-C pause before any F6-2/F6-3 server stop, gated by the F6 STOP prerequisites \u2014 historically the frozen-apply boundary of the superseded 2026-08-29 halt; W = Phase W, the separately-named worker lifecycle drill \u2014 former F6-4/F6-5). Master plan \u00a733 orders Fase 5 (workers) and Fase 6 (incl. node drain/reboot) before Fase 7 (Cilium); ADR-0005 requires Cilium as the immediate first addon with NO pre-Cilium worker-Ready/DNS gates. **Tension and resolution**: with `cni: none`, workers cannot pass Ready (and therefore drain/reboot) gates before Cilium exists; the resolution adopted by this runbook is that Fase-6's CP-only gates (F6-1..3) run before the first addon per \u00a733, and the worker-lifecycle drill is the separately-named Phase W landing after D2 on the final 6-node topology; the DoD maps drain/reboot to that post-D2 evidence, never to pre-addon acceptance. Phase W is not counted as completed pre-addon acceptance.\n- Playbook/role facts re-read on final main: `ansible/playbooks/{rke2-cp,rke2-workers,rke2-workers-validate,cilium,ceph-csi,bootstrap}.yml`, `ansible/roles/{keepalived,rke2-server,rke2-agent,nat_gateway}/`, `kubernetes/{cilium,ceph-csi,gitops}/`, `docs/gitops.md`, `scripts/{gen-rke2-token.sh,render-ansible-inventory.sh}`, `tofu/proxmox/environments/prod/terraform.tfvars.example`.\n- **Execution environment (\u00a70 item 9)**: the controller is the workstation (direct evidence from the t_a88bb36b apply: ops01 had no Ansible venv/repository; the playbooks ran from the workstation over the bastion); ops01 requires explicit provisioning (checkout, venv, inventory, helm/kubectl/cilium/hubble CLIs, and a transferred admin kubeconfig with the server endpoint rewritten to the VIP) before Phase E \u2014 needed because `cilium.yml` runs its Helm op on the `operations` host with `connection: local`. CP-local kubectl invocations use the RKE2-bundled binary path `/var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml`, matching the playbooks' own `rke2_kubectl` form.\n- secrets-encryption: **MEASURED PROTECTED, per the single operative gate rule (\u00a70 item 8)** \u2014 current protection is determined by measured evidence (default-on AES-CBC on this RKE2 build: `secrets-encrypt status` Enabled on all 3 CPs; raw etcd probes of both argocd Secrets returned `k8s:enc:aescbc` ciphertext), NOT by config-template contents. The explicit `secrets-encryption: true` role-template line is still absent from merged main (`git grep secrets-encryption` \u2192 empty) \u2014 it is the separately tracked future hardening/rotation prerequisite (must merge via its own reviewed PR before any key rotation), not an unlock condition for F2/G while the measured checks pass. F2's csi-rbd-secret gate (when Phase F executes) re-verifies the same measured checks on that Secret.\n- Design: `docs/design/master-architecture-plan.md` \u00a733/\u00a735 (Fase 6 = acceptance; CP-only portion BEFORE Fase 7 Cilium; worker-lifecycle portion after D2 per ADR-0005 \u2014 see the executable-order bullet above); `docs/decisions/ADR-0004`, `docs/decisions/ADR-0005`.\n- Restore procedure and reset-flag semantics: RKE2 docs, rancher/rke2-docs `docs/datastore/backup_restore.md` (etcd-snapshot subcommands = delete/ls/prune/save ONLY; restore = `rke2 server --cluster-reset --cluster-reset-restore-path=<PATH>` multi-server flow; reset creates `/var/lib/rancher/rke2/server/db/reset-flag` preventing repeated resets in succession, deleted only by a normal start \u2014 verified 2026-08-30 against the `main` branch of rancher/rke2-docs). Flag lifecycle additionally verified 2026-08-30 against the k3s source vendored by the pinned `rke2_version: v1.36.3+rke2r1` (k3s commit 4ca29faac89e via rke2 go.mod; `pkg/cluster/managed.go`: the flag check is bypassed when `--cluster-reset-restore-path` is set and the flag blocks only a bare reset; `pkg/etcd/etcd.go` Reset(): the flag is written only after the snapshot-restore step succeeds).\n- Secrets-encryption behavior: RKE2 docs, rancher/rke2-docs `docs/security/secrets_encryption.md` (auto-generated AES-CBC key + `/var/lib/rancher/rke2/server/cred/encryption-config.json`; `rke2 secrets-encrypt status` output shape: Encryption Status / Current Rotation Stage / Server Encryption Hashes / key table).",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (platform/rke2-apply-runbook.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<h1 id=\"bkmrk-rke2-apply-runbook-%E2%80%94\">RKE2 apply runbook \u2014 phases 0-G</h1>\n<p id=\"bkmrk-this-is-the-single-r\">This is the single reviewed procedure a human follows to bring the RKE2 cluster up, with validation gates between phases. Every phase ends in an explicit STOP gate: <strong>do not proceed past a failing gate</strong>. Each gate result is recorded in the apply log with a date.</p>\n<blockquote id=\"bkmrk-status-%282026-08-30%2C-\">\n<p><strong>Status (2026-08-30, post-Phase-D update)</strong>: the live apply has RESUMED and executed through <strong>Phase D</strong>. Executed 2026-08-29/30, fully green: <strong>B</strong> (cp01 bootstrap, infra PR #6 fix d553643), <strong>C</strong> (cp02+cp03 quorum, PR #7), <strong>F6-1..3</strong> (CP-only acceptance: quorum recorded, one-CP failure drill via VIP failover, etcd snapshot + real restore), <strong>E</strong> (Cilium 1.20.1 + Hubble, PR #8), and <strong>D</strong> (all 3 platform workers joined via <code>rke2-workers.yml</code> at infra PR #9 f841ae7 \u2014 serial gates green, per-worker <code>kubectl wait</code> + exact-label asserts passed incl. the <code>/var/lib/rancher/rke2/agent/kubelet.kubeconfig</code> data-dir gate; live-verified on rke-platform01). Cluster state: <strong>6/6 nodes Ready</strong>, CP taints enforced (<code>node-role.kubernetes.io/control-plane:NoSchedule</code>), workers labelled <code>wetopi.io/role=platform</code>, kube-system pods Running, in-cluster DNS resolving (10.68.0.0/16), scheduling test landed on a platform worker, CPs workload-free (<code>rke2-workers-validate.yml</code> rack green \u2014 details its runbook section describes for D2 remain next). <strong>Next in executable order: F (Ceph, blocked on operator-held facts, t_80ca13af); G (Argo CD) has since APPLIED with recorded deviations (see the post-Phase-G update below and the Phase G section).</strong> Earlier halt notice (retained for history): the live apply (task t_a88bb36b) was HALTED BEFORE PHASE B and FROZEN per operator directive #38 \u2014 the 2026-08-29 23:17-23:26 UTC attempt failed at <code>rke2-server : Create the RKE2 server configuration</code> with <code>Destination directory /etc/rancher/rke2 does not exist</code> on rke-cp01 (neither rke2 role created the config dir); fix was infrastructure PR #6 (merged main d553643). cp01 carried keepalived (active+enabled) + RKE2 binaries from that pre-halt attempt \u2014 roles proved idempotent (RKE2 install guarded by <code>creates:</code>) when B/C re-ran clean on 2026-08-30 (tasks t_b0eebfac, t_58bd66bb, t_f7099d9d, t_b3841920). Verified against merged infrastructure <code>main</code> @ f841ae7 (PRs #1-#9) and docs <code>main</code> @ 7cc387a.</p>\n</blockquote>\n<p id=\"bkmrk-phase-order-%28the-aut\"><strong>Phase order (the authorized executable order \u2014 stated IDENTICALLY in this banner, the F6 heading + STOP rule, the Phase W heading, the D2 handoff, the DoD mapping, and the provenance section): B \u2192 C \u2192 STOP \u2192 F6-1..3 \u2192 E \u2192 D \u2192 D2 \u2192 W \u2192 F \u2192 G.</strong> Concretely: A (VIP) is folded into the B/C gates (no standalone A apply) \u2192 B (cp01 bootstrap + VIP bring-up) \u2192 C (quorum cp02+cp03 incl. VIP failover) \u2192 <strong>STOP</strong> (mandatory pause after the Phase C quorum gate: before any F6-2/F6-3 server stop, every F6 STOP prerequisite below must be PASS and dated in the apply log; historically this was the frozen-apply boundary of the 2026-08-29 halt \u2014 that freeze is superseded history, see the retained halt notice in the status banner above) \u2192 <strong>Fase-6 acceptance, CP-only portion: F6-1 (quorum recorded), F6-2 (one-CP failure drill), F6-3 (etcd snapshot + REAL restore test)</strong> \u2014 these three execute on the CP-only cluster (3 nodes, no workers) BEFORE any addon, per master plan \u00a733 (Fase 6 before Fase 7), and are gated by the hard F6 STOP prerequisites (external verified snapshot copy, SHA-256 match, quorum evidence, explicit human authorization) \u2192 E (Cilium install + basic install validation ONLY) \u2192 D (workers join, registration + exact-label gates only) \u2192 D2 (full post-Cilium cluster validation on the FINAL 6-node topology, incl. the full Cilium acceptance) \u2192 <strong>W = Phase W \u2014 the worker lifecycle drill (drain + reboot; separately named; former F6-4/F6-5)</strong> \u2014 worker drain/reboot happens HERE, after D2, NEVER before addons \u2192 F (Ceph, PR-only, default-off) \u2192 G (Argo CD). <strong>Known tension with master plan \u00a733, resolved EXPLICITLY</strong>: \u00a733 orders Fase 5 (workers) and Fase 6 (which includes node drain/reboot) BEFORE Fase 7 (Cilium), but ADR-0005 requires Cilium as the IMMEDIATE first addon with NO pre-Cilium worker-Ready/DNS gates \u2014 with <code>cni: none</code>, workers can never pass Ready/drain/reboot gates before Cilium exists. Resolution: the CP-only gates of Fase 6 (quorum, CP failure, snapshot + real restore) run before the first addon, and the worker-lifecycle drill is the separately-named Phase W landing after D2 on the final 6-node topology; the DoD maps drain/reboot to that POST-D2 evidence, never to pre-addon acceptance. Do NOT count Phase W as completed pre-addon acceptance. With <code>cni: none</code> at the RKE2 level, <strong>no node can be Ready before Cilium exists</strong>: health before E = <code>/readyz</code> + etcd member counts ONLY; nodes NotReady is EXPECTED before E and there are NO worker-Ready/CoreDNS/DNS expectations before Cilium. The FULL Cilium acceptance (connectivity test, Hubble flows, DNS, NetworkPolicy, CiliumNode per node) runs in D2 on the final 6-node topology, not in E.</p>\n<h2 id=\"bkmrk-0.-preconditions-%28be\">0. Preconditions (before Phase B)</h2>\n<p id=\"bkmrk-work-through-this-ch\">Work through this checklist in order. Each item has its concrete verification.</p>\n<ol id=\"bkmrk-adrs-accepted-%E2%80%94-docs\">\n<li>\n<p><strong>ADRs accepted</strong> \u2014 <code>docs/decisions/ADR-0004-rke2-api-endpoint.md</code> and <code>docs/decisions/ADR-0005-cilium-first-addon.md</code> merged in docs main (merge commit 5740404).</p>\n</li>\n<li>\n<p><strong>Implementation PRs merged to infrastructure <code>main</code></strong> \u2014 verified at <strong>d553643</strong> (current main, 2026-08-30):</p>\n<ul>\n<li>PR #1 (Argo CD bootstrap, merged as 8f3e0e8)</li>\n<li>PR #2 (<code>feature/cilium-hubble-addon</code>: <code>ansible/playbooks/cilium.yml</code>, <code>kubernetes/cilium/*</code> \u2014 merged as e283e24)</li>\n<li>PR #3 (Ceph routing + CSI, merged as 76782dc)</li>\n<li>PR #4 (<code>feature/rke2-server-role</code>: rke2-server + keepalived roles, <code>ansible/playbooks/rke2-cp.yml</code>, <code>scripts/gen-rke2-token.sh</code> \u2014 merged as b15a087)</li>\n<li>PR #5 (<code>feature/rke2-agent-role</code>: rke2-agent role, <code>ansible/playbooks/rke2-workers.yml</code>, <code>ansible/playbooks/rke2-workers-validate.yml</code> \u2014 merged as 8d836e8)</li>\n<li>PR #6 (<code>fix/rke2-config-dir-create</code>, head 3ff2ee8 \u2014 merged as d553643): creates <code>/etc/rancher/rke2</code> (root:root 0750) before templating in BOTH rke2 roles. <strong>This is the fix for the config-directory defect that halted the 2026-08-29 apply attempt</strong> (<code>Destination directory /etc/rancher/rke2 does not exist</code> at <code>rke2-server : Create the RKE2 server configuration</code>; byte-identical defect in rke2-agent). Without it, phases B and D fail at config templating.</li>\n</ul>\n<p>STEP 0 gate: <code>git -C infrastructure log --oneline -1</code> on the checkout used for apply must show <strong>d553643 or a verified descendant</strong> \u2014 8d836e8 (pre-PR#6) is NO LONGER sufficient: resuming phases B/C against a checkout without the config-dir fix reproduces the halt. Verify the fix is present: <code>git -C infrastructure show d553643 --stat</code> mentions both <code>rke2-server</code> and <code>rke2-agent</code> tasks. <code>git grep secrets-encryption</code> still returns nothing on d553643 (the explicit hardening line is still absent \u2014 see item 8 for the current measured-evidence gate rule).</p>\n</li>\n<li>\n<p><strong>Branch protection</strong> \u2014 <code>main</code> is the reviewed line; all changes land via PR with wetopi-approver review; never commit directly to main.</p>\n</li>\n<li>\n<p><strong>Token</strong> \u2014 on the controller (the infrastructure checkout that will run the playbooks \u2014 the evidenced controller is the WORKSTATION, not ops01; see item 9), run <code>scripts/gen-rke2-token.sh</code> (umask 077, <code>openssl rand -hex 16</code>, 32-char hex). It writes <code>ansible/files/rke2/node-token</code>, mode 0600, gitignored.\nVerify: <code>stat -c '%a' ansible/files/rke2/node-token</code> \u2192 <code>600</code>. <strong>NEVER commit it.</strong>\nBoth <code>rke2-cp.yml</code> and <code>rke2-workers.yml</code> pre-flight plays hard-fail if the file is missing or not 0600.</p>\n</li>\n<li>\n<p><strong>Inventory</strong> \u2014 <code>./scripts/render-ansible-inventory.sh</code> regenerates <code>ansible/inventories/prod/hosts.json</code> from the tofu <code>ansible_inventory</code> output (mode 600, gitignored); the generated hostvars set <code>ansible_ssh_common_args = \"-o ProxyJump=ubuntu@92.222.245.91\"</code> for every non-bastion VM \u2014 that ProxyJump form is the inventory's internal contract consumed by Ansible. Bastion is <code>wetopi-ops01</code> = 92.222.245.91 (WAN) / 10.72.0.10, SSH account <code>ubuntu</code> (bastion SSH policy allows only <code>ubuntu</code>; the workstation keeps all private keys; ops01 is a ProxyJump target only). Human reachability check from the workstation uses the documented double-key form:\n<code>ssh -i /opt/data/.ssh/bigardo -o ProxyCommand=\"ssh -i /opt/data/.ssh/bigardo -W %h:%p ubuntu@92.222.245.91\" &lt;user&gt;@&lt;host&gt;</code>, or plain <code>ping 10.72.0.21</code> from ops01.</p>\n</li>\n<li>\n<p><strong>Proxmox snapshots (rollback plane)</strong> \u2014 before Phase B, snapshot every VM that phases B-G will touch, from the Proxmox node hosting it (standard Proxmox CLI <code>qm snapshot &lt;vmid&gt; pre-rke2-apply</code>):</p>\n<table>\n<thead>\n<tr>\n<th>VM</th>\n<th>vm_id</th>\n<th>Proxmox node</th>\n<th>IP</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>wetopi-ops01</td>\n<td>8001</td>\n<td>ns3249468</td>\n<td>10.72.0.10</td>\n</tr>\n<tr>\n<td>rke-cp01</td>\n<td>8011</td>\n<td>ns3243421</td>\n<td>10.72.0.21</td>\n</tr>\n<tr>\n<td>rke-cp02</td>\n<td>8012</td>\n<td>ns3226939</td>\n<td>10.72.0.22</td>\n</tr>\n<tr>\n<td>rke-cp03</td>\n<td>8013</td>\n<td>ns3214108</td>\n<td>10.72.0.23</td>\n</tr>\n<tr>\n<td>rke-platform01</td>\n<td>8021</td>\n<td>ns31826929</td>\n<td>10.72.0.41</td>\n</tr>\n<tr>\n<td>rke-platform02</td>\n<td>8022</td>\n<td>ns3226931</td>\n<td>10.72.0.42</td>\n</tr>\n<tr>\n<td>rke-platform03</td>\n<td>8023</td>\n<td>ns31826925</td>\n<td>10.72.0.43</td>\n</tr>\n<tr>\n<td>wetopi-natgw01</td>\n<td>8031</td>\n<td>ns3243421</td>\n<td>10.72.0.2</td>\n</tr>\n</tbody>\n</table>\n<p>Rollback = <code>qm rollback &lt;vmid&gt; pre-rke2-apply</code> (VM must be stopped). These are the Proxmox-level safety net; the per-phase rollbacks below are the first line.</p>\n</li>\n<li>\n<p><strong>Cluster facts (from ADR-0004)</strong> \u2014 API VIP 10.72.0.20 (vmbr4/VLAN 4 node LAN 10.72.0.0/23, never Internet); server URL <code>https://10.72.0.20:9345</code>; API <code>https://10.72.0.20:6443</code>; tls-san = 10.72.0.20/.21/.22/.23 (IP-only, no DNS SAN); cluster-cidr 10.64.0.0/14; service-cidr 10.68.0.0/16; CNI <code>none</code> at RKE2 level (Cilium is the first addon, ADR-0005).</p>\n</li>\n<li>\n<p><strong>At-rest encryption gate \u2014 ONE current statement (reconciled 2026-08-30, Phase G live evidence, card t_e0cace4b).</strong> The earlier \"honest gap\" assumption (that a missing <code>secrets-encryption</code> line in the config template means Secrets sit PLAINTEXT in etcd) is <strong>superseded: it was wrong for this RKE2 build</strong>. Measured fact: RKE2 v1.36.3+rke2r1 enables <strong>AES-CBC at-rest encryption BY DEFAULT</strong> \u2014 the config template renders no <code>secrets-encryption</code> line, yet <code>rke2 secrets-encrypt status</code> measured on ALL THREE CPs shows <code>Encryption Status: Enabled</code> / <code>Current Rotation Stage: start</code> / <code>Server Encryption Hashes: All hashes match</code> / Active <code>* AES-CBC aescbckey</code>, and raw etcd probes (approved kubectl-exec etcdctl form, below) of <code>argocd/argocd-initial-admin-secret</code> (316B) and <code>argocd/repo-infra-https</code> (685B) returned <code>k8s:enc:aescbc:v1:aescbckey:&lt;opaque ciphertext&gt;</code> \u2014 NOT plaintext.\n<strong>CURRENT GATE RULE (this is the operative rule; it supersedes every older wording elsewhere in this runbook, including any residual plaintext framing):</strong> current protection is determined by MEASURED EVIDENCE, not by config-template contents \u2014 for any Secret at any gate, the measured checks below. <strong>What the explicit <code>secrets-encryption: true</code> line is:</strong> a SEPARATE, tracked, future reviewed hardening/rotation prerequisite (explicit declarative intent in the rke2-server role template, needed BEFORE any future key rotation). It is NOT an unlock condition for any gate while the measured checks pass \u2014 a build with default-on encryption and no explicit line is PROTECTED, not blocked.\n<strong>Measured-protection checks (the form used at any [encryption-gate]):</strong></p>\n<ul>\n<li>(i) <code>rke2 secrets-encrypt status</code> on all three CPs, each showing:\n<pre><code>Encryption Status: Enabled\nCurrent Rotation Stage: start\nServer Encryption Hashes: All hashes match\n</code></pre>\nplus an Active key row of the shape <code>* AES-CBC aescbckey...</code> (the <code>*</code> marks the active key);</li>\n<li>(ii) an exact raw-etcd ciphertext check, reusing the approved kubectl-exec etcdctl form (Phase B gate) with the 17-arg pattern:\n<pre><code>/var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml -n kube-system exec etcd-rke-cp01 -- env ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 --cacert=/var/lib/rancher/rke2/server/tls/etcd/server-ca.crt --cert=/var/lib/rancher/rke2/server/tls/etcd/client.crt --key=/var/lib/rancher/rke2/server/tls/etcd/client.key get /registry/secrets/&lt;namespace&gt;/&lt;NAME&gt;\n</code></pre>\nExpected: the value shows the encrypted form (<code>k8s:enc:aescbc</code> ciphertext \u2014 opaque), <strong>NOT</strong> the plaintext returned by <code>kubectl get secret &lt;name&gt; -n &lt;namespace&gt; -o jsonpath='{.data.&lt;KEY&gt;}' | base64 -d</code>. <strong>STOP: plaintext visible \u2192 protection NOT confirmed \u2192 the [encryption-gate] in question stays STOPped.</strong>\n<strong>Status of this cluster (2026-08-30, Phase G):</strong> measured PROTECTED \u2014 (i) verified on all 3 CPs, (ii) verified by raw etcd ciphertext probes on both argocd Secrets. The future hardening/rotation prerequisite (reviewed <code>secrets-encryption: true</code> merged to infrastructure main, then sequential restart cp01\u2192cp02\u2192cp03 one at a time with each returning Ready before the next, re-verifying (i) and (ii)) is <strong>NOT yet merged</strong> (<code>git grep secrets-encryption</code> on infrastructure main still returns empty) \u2014 it must land via its own reviewed PR BEFORE any future encryption-key rotation; it does not block F2/G while the measured checks pass.\nFuture shape: a reviewed <code>secrets-encryption: true</code> setting in <code>/etc/rancher/rke2/config.yaml</code> (rendered by the rke2-server role), merged to infrastructure main. NOTE: there is <strong>no manually-managed key file</strong> \u2014 RKE2's documented behavior (rancher/rke2-docs, docs/security/secrets_encryption.md) is that once secrets encryption is enabled, RKE2 <strong>automatically generates an AES-CBC key and the encryption-provider config file</strong> at <code>/var/lib/rancher/rke2/server/cred/encryption-config.json</code> and passes it to the kube-apiserver as <code>--encryption-provider-config</code>. The config file is node-local state auto-generated and managed by RKE2 itself \u2014 never commit it to Git.</li>\n</ul>\n</li>\n<li>\n<p><strong>Controller and execution environment \u2014 define and verify BEFORE the first phase command.</strong> Direct evidence from the 2026-08-29 apply (t_a88bb36b): wetopi-ops01 had NO Ansible venv, NO infrastructure checkout, and NO kubeconfig; the playbooks ran from the WORKSTATION, which holds the SSH private keys (<code>/opt/data/.ssh/bigardo</code>). ops01 is a ProxyJump target only (item 5): it holds no private keys and cannot SSH to the LAN VMs by itself. This runbook therefore fixes the execution context explicitly instead of assuming it:</p>\n<ul>\n<li><strong>Controller = the workstation</strong> (the Ansible phases B, C, D, D2 \u2014 playbooks <code>rke2-cp.yml</code>, <code>rke2-workers.yml</code>, <code>rke2-workers-validate.yml</code> run from the controller and reach the VMs over SSH through the bastion). Verify on the controller BEFORE phases B/C (record each check with a date):\n<ul>\n<li>clean infrastructure checkout at <strong>d553643 or a verified descendant</strong> (item 2 STEP 0 gate): <code>git -C infrastructure log --oneline -1</code> shows d553643 or a descendant, and <code>git -C infrastructure status --porcelain</code> is empty;</li>\n<li>Ansible venv per <code>requirements-dev.txt</code> (pins <code>ansible-core==2.21.3</code>): <code>python3 -m venv .venv</code> then <code>.venv/bin/pip install -r requirements-dev.txt</code>; verify <code>.venv/bin/ansible-playbook --version</code> reports ansible-core 2.21.3;</li>\n<li>inventory: <code>./scripts/render-ansible-inventory.sh</code> regenerates <code>ansible/inventories/prod/hosts.json</code> (item 5) \u2014 or use the synthesized gitignored inventory shape the 2026-08-29 apply verified against <code>outputs.tf</code>; EITHER form must include the ungrouped <code>localhost</code> host with <code>ansible_connection: local, ansible_become: false</code> (the roles' <code>delegate_to: localhost</code> token checks fail on a sudo-less controller without it \u2014 evidenced 2026-08-29);</li>\n<li>token file (item 4): <code>stat -c '%a' ansible/files/rke2/node-token</code> \u2192 <code>600</code>;</li>\n<li>SSH path: the double-key ProxyCommand form (item 5) opens a sudo-capable shell on cp01 (<code>sudo -v</code> succeeds there).</li>\n</ul>\n</li>\n<li><strong>Ops01 provisioning \u2014 verify BEFORE Phase E</strong> (ops01 hosts the <code>connection: local</code> Helm op of <code>cilium.yml</code>, the cilium/hubble CLI acceptance in D2, and the F2/G commands; NONE of this existed at apply time):\n<ul>\n<li>infrastructure checkout on ops01 at <strong>d553643 or a verified descendant</strong>: <code>git clone https://github.com/wetopi-platform/infrastructure.git ~/wetopi-infrastructure</code> then <code>git checkout main</code> and verify the head;</li>\n<li>Ansible venv on ops01: <code>python3 -m venv .venv &amp;&amp; .venv/bin/pip install -r requirements-dev.txt</code> (Phase E invokes <code>.venv/bin/ansible-playbook</code> there);</li>\n<li>inventory on ops01: <code>ansible/inventories/prod/hosts.json</code> is gitignored \u2014 either render it on ops01 (requires the tofu state) or copy the controller's verified copy over (same relay as the kubeconfig below); record which was done;</li>\n<li>binaries on ops01: <code>helm version --short</code> (v3.x; the chart reference was validated with Helm 3.19), <code>kubectl version --client</code>, and the <code>cilium</code> + <code>hubble</code> CLIs (GitHub releases cilium/cilium-cli and cilium/hubble; verify with <code>cilium version</code> and <code>hubble version</code>; record exact versions \u2014 the cilium CLI must be compatible with the pinned chart 1.20.1);</li>\n<li><strong>current admin kubeconfig, transferred AFTER the F6-3 restore test</strong> (Phase E is the first consumer; a pre-restore transfer is stale):\n<ol>\n<li>on cp01: <code>sudo cat /etc/rancher/rke2/rke2.yaml &gt; /tmp/rke2-admin-kubeconfig &amp;&amp; chmod 600 /tmp/rke2-admin-kubeconfig</code>;</li>\n<li>from the workstation (relay \u2014 ops01 cannot pull from cp01): <code>scp -i /opt/data/.ssh/bigardo -o ProxyCommand=\"ssh -i /opt/data/.ssh/bigardo -W %h:%p ubuntu@92.222.245.91\" ubuntu@10.72.0.21:/tmp/rke2-admin-kubeconfig /tmp/rke2-admin-kubeconfig</code>;</li>\n<li>workstation \u2192 ops01 (direct to the bastion): <code>scp -i /opt/data/.ssh/bigardo /tmp/rke2-admin-kubeconfig ubuntu@92.222.245.91:~/secrets/rke2-admin-kubeconfig</code>;</li>\n<li>remove the <code>/tmp</code> copies on cp01 and the workstation;</li>\n<li>on ops01: <code>sed -i 's|https://127.0.0.1:6443|https://10.72.0.20:6443|' ~/secrets/rke2-admin-kubeconfig &amp;&amp; chmod 600 ~/secrets/rke2-admin-kubeconfig</code> (the RKE2 admin kubeconfig points at 127.0.0.1 by default; the VIP is in tls-san per ADR-0004, so the apiserver cert validates), then verify: <code>KUBECONFIG=~/secrets/rke2-admin-kubeconfig kubectl get nodes</code> lists the 3 CPs. <strong>NEVER commit the kubeconfig</strong> \u2014 it is cluster-admin credentials.</li>\n</ol>\n</li>\n</ul>\n</li>\n<li><strong>Two canonical kubectl invocation forms</strong> (every command block below uses one of these):\n<ul>\n<li><strong>CP-local</strong> (executed ON a control-plane node): <code>/var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml ...</code> \u2014 plain <code>kubectl</code> is NOT in PATH on CP nodes; only the RKE2-bundled binary is (same form the playbooks use, e.g. the <code>rke2_kubectl</code> fact in <code>rke2-workers-validate.yml</code>);</li>\n<li><strong>Remote</strong> (ops01 or the workstation): <code>kubectl ...</code> with <code>KUBECONFIG</code> pointing at the transferred admin kubeconfig (ops01 provisioning above).</li>\n</ul>\n</li>\n</ul>\n</li>\n</ol>\n<h2 id=\"bkmrk-phase-b-%E2%80%94-first-cont\">Phase B \u2014 first control plane (cp01 bootstrap, incl. VIP bring-up)</h2>\n<p id=\"bkmrk-keepalived-is-not-a-\">Keepalived is NOT a standalone phase: it is the <strong>first role of both RKE2 plays</strong> inside <code>ansible/playbooks/rke2-cp.yml</code> (role order per CP: keepalived, then rke2-server). Play 1 pre-flights all CPs (asserts VIP+addresses in tls-san, CIDRs defined, VIP == 10.72.0.20, token file 0600). The bootstrap play (play 2, hosts <code>rke-cp01</code>, <code>serial: 1</code>, <code>any_errors_fatal: true</code>) renders <code>/etc/rancher/rke2/config.yaml</code> via the rke2-server template (token; <code>server:</code> only on non-bootstrap nodes; tls-san list; cluster-cidr; service-cidr; <code>cni: none</code>).</p>\n<p id=\"bkmrk-keepalived-design-fa\"><strong>Keepalived design facts</strong> (from <code>ansible/roles/keepalived/</code>): interface eth0; VIP 10.72.0.20/23; VRID 51; unicast peers = the other two CPs; priorities cp01=150 (initial MASTER), cp02=100, cp03=50; advert_int 1; health-check script <code>/etc/keepalived/check_rke2_api.sh</code> probes the LOCAL API only: <code>curl -sf --cacert /var/lib/rancher/rke2/server/tls/server-ca.crt --cert .../client-kube-apiserver.crt --key .../client-kube-apiserver.key https://127.0.0.1:6443/readyz</code> plus a TCP probe of 127.0.0.1:9345; interval 2, fall 3, rise 2.</p>\n<p id=\"bkmrk-consequence-%28fail-sa\"><strong>Consequence (fail-safe by design)</strong>: a CP only holds the VIP while its own local API is healthy \u2014 before cp01's API is up there is deliberately NO VIP holder, so <code>ping 10.72.0.20</code> failing before Phase B completes is EXPECTED, not a bug. There is NO standalone Phase-A apply; VIP acceptance is folded into the B/C gates below.</p>\n<p id=\"bkmrk-command-%28from-the-co\">Command (from the CONTROLLER \u2014 the workstation per \u00a70 item 9, in its infrastructure checkout):</p>\n<pre id=\"bkmrk-ansible_config%3Dansib\"><code>ANSIBLE_CONFIG=ansible/ansible.cfg \\\n  .venv/bin/ansible-playbook -i ansible/inventories/prod/hosts.json \\\n  ansible/playbooks/rke2-cp.yml\n</code></pre>\n<p id=\"bkmrk-the-playbook-is-one-\">The playbook is one invocation for B AND C \u2014 play 1 pre-flights all CPs, play 2 bootstraps cp01, play 3 joins cp02/cp03. Phase B is complete when play 2's built-in gates pass: <code>wait_for</code> 127.0.0.1:6443 (600s timeout), <code>/readyz</code> via the VIP (Ansible uri with client certs, retries 30 \u00d7 10s, expects status 200), and etcd member list == 1 (see gate B below).</p>\n<h3 id=\"bkmrk-validation-gate-b-%28i\">Validation gate B (includes former \"Phase A\" VIP acceptance)</h3>\n<ul id=\"bkmrk-curl--k-https%3A%2F%2F10.7\">\n<li><code>curl -k https://10.72.0.20:6443/readyz</code> \u2192 <code>ok</code> (API via VIP works; VIP now held by cp01)</li>\n<li><code>systemctl is-active keepalived</code> on <strong>cp01 ONLY</strong> (Phase B applies cp01; the all-three-CP keepalived check belongs to gate C, where cp02/cp03 are applied) \u2192 <code>active</code></li>\n<li><code>ip -4 addr show eth0</code> on each CP: exactly one node lists <code>10.72.0.20/23</code> (the current owner; cp01 when healthy)</li>\n<li><code>ping -c 3 10.72.0.20</code> from wetopi-ops01 \u2192 0% loss</li>\n<li>etcd member list (EXACT form the playbook uses, executed as a kubectl exec into the etcd pod on cp01):\n<pre><code>/var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml -n kube-system exec etcd-rke-cp01 -- env ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 --cacert=/var/lib/rancher/rke2/server/tls/etcd/server-ca.crt --cert=/var/lib/rancher/rke2/server/tls/etcd/client.crt --key=/var/lib/rancher/rke2/server/tls/etcd/client.key member list\n</code></pre>\n\u2192 exactly 1 member, started.</li>\n<li><strong>Node Ready state is NOT a Phase B gate</strong>: with <code>cni: none</code>, the CP-local form (\u00a70 item 9) <code>/var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml get nodes</code> on cp01 shows cp01 <strong>NotReady \u2014 EXPECTED</strong>. CP health at this stage = <code>/readyz</code> via VIP + etcd member count only.</li>\n</ul>\n<p id=\"bkmrk-stop%3A-any-playbook-p\"><strong>STOP</strong>: any playbook play fails (<code>any_errors_fatal</code> aborts the whole run \u2014 do not re-run blindly; read the failure first); <code>/readyz</code> via VIP fails; etcd member count != 1; keepalived not active on cp01.</p>\n<h2 id=\"bkmrk-phase-c-%E2%80%94-quorum-%28cp\">Phase C \u2014 quorum (cp02 + cp03, incl. VIP failover test)</h2>\n<p id=\"bkmrk-same-playbook%2C-play-\">Same playbook, play 3 (hosts <code>rke-cp02:rke-cp03</code>, <code>serial: 1</code>, roles keepalived + rke2-server, joining via <code>server: https://10.72.0.20:9345</code>; per-node expected etcd member counts: after cp02 = 2, after cp03 = 3). It continues automatically once Phase B gates pass.</p>\n<h3 id=\"bkmrk-validation-gate-c\">Validation gate C</h3>\n<ul id=\"bkmrk-etcd-member-list-%28ex\">\n<li>etcd member list (exact kubectl-exec form, run from cp01):\n<pre><code>/var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml -n kube-system exec etcd-rke-cp01 -- env ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 --cacert=/var/lib/rancher/rke2/server/tls/etcd/server-ca.crt --cert=/var/lib/rancher/rke2/server/tls/etcd/client.crt --key=/var/lib/rancher/rke2/server/tls/etcd/client.key member list\n</code></pre>\n\u2192 exactly 3 members, all started; quorum = 2 of 3</li>\n<li><code>curl -k https://10.72.0.20:6443/readyz</code> \u2192 <code>ok</code></li>\n<li><code>systemctl is-active keepalived</code> on <strong>all three CPs</strong> (the all-three-CP keepalived check lives HERE, in Phase C where cp02/cp03 are applied) \u2192 <code>active</code></li>\n<li><code>/var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml get nodes</code> (CP-local form, on cp01) \u2192 rke-cp01..03 all <strong>NotReady \u2014 EXPECTED (pre-CNI)</strong>; do NOT treat this as a failure</li>\n<li><strong>VIP failover test</strong> (only now, with quorum): on the current owner (cp01; find via <code>ip -4 addr show eth0</code> on each CP) run <code>sudo systemctl stop keepalived</code>; expect the VIP to move to the highest-priority healthy peer (cp02) within roughly 10 s (advert_int 1 \u00d7 fall 3, plus check rise 2 \u00d7 interval 2 on the receiver); verify from ops01: <code>ping -c 3 10.72.0.20</code> still 0% loss and <code>curl -k https://10.72.0.20:6443/readyz</code> still <code>ok</code>; then <code>sudo systemctl start keepalived</code> on cp01 \u2014 cp01 (priority 150) reclaims the VIP by preemption; confirm with <code>ip -4 addr</code> again.</li>\n<li><strong>Documented one-CP-outage test</strong>: on ONE non-VIP-owner CP (cp02 or cp03): <code>sudo systemctl stop rke2-server</code>; then from ops01 <code>curl -k https://10.72.0.20:6443/readyz</code> must STILL return <code>ok</code> (2 members keep quorum; API served via VIP on remaining CPs) and the etcd member list (kubectl-exec form, from cp01) still lists 3 members; restart with <code>sudo systemctl start rke2-server</code>. STOP the whole run if the API via VIP fails during this test (that is a quorum/VIP defect, not a node defect).</li>\n<li><strong>CP workload check</strong> (CP-local form, on each CP): <code>/var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml describe node rke-cp0X | grep -i taint</code> \u2014 record the taint state on all three CPs in the apply log. The no-platform-workloads-on-CPs guarantee is enforced by the workers playbook play 2, which applies <code>node-role.kubernetes.io/control-plane=true:NoSchedule</code> on all three CPs (idempotency decided from the API pre-state, not kubectl stdout), so ordinary pods can never schedule onto a CP; system components tolerate the taint by upstream default. Do not proceed if <code>/var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml -n kube-system get pods -o wide</code> (same form) shows any platform-labeled pod scheduled on a CP.</li>\n</ul>\n<p id=\"bkmrk-stop%3A-member-count-%21\"><strong>STOP</strong>: member count != 3; /readyz fails; keepalived not active on all three CPs; API via VIP fails with one CP stopped or during VIP failover; any platform workload found on a CP.</p>\n<h2 id=\"bkmrk-phase-f6-%2F-fase-6-ac\">Phase F6 / Fase-6 acceptance \u2014 CP-only portion (F6-1..3), gated by hard STOP prerequisites</h2>\n<p id=\"bkmrk-part-of-the-authoriz\">Part of the authorized executable order (identical to the banner, the Phase W heading, the D2 handoff, the DoD mapping, and the provenance section): <strong>B \u2192 C \u2192 STOP \u2192 F6-1..3 \u2192 E \u2192 D \u2192 D2 \u2192 W \u2192 F \u2192 G.</strong> This phase is the <strong>CP-only portion of the Fase-6 acceptance</strong>: it runs immediately after the Phase C quorum gate and <strong>BEFORE any addon (Cilium E, workers D, Ceph F, Argo CD G)</strong>, on the CP-only cluster (3 nodes, <code>cni: none</code>, no workers joined). Master plan \u00a733 places Fase 6 before Fase 7 (Cilium/Hubble); its CP-only gates \u2014 <strong>F6-1 (quorum recorded), F6-2 (one-CP failure drill), F6-3 (etcd snapshot + REAL restore test)</strong> \u2014 run here, gated by the hard F6 STOP prerequisites below. The <strong>worker-lifecycle drill (drain + reboot) is NOT part of this phase</strong>: it is the separately-named <strong>Phase W</strong> and executes AFTER D2 on the final 6-node topology (ADR-0005 forbids pre-Cilium worker-Ready gates; see the banner for the \u00a733 tension and its explicit resolution). Do NOT count Phase W as completed pre-addon acceptance. Each test has a concrete command, expected output, and STOP condition; record every result with a date in the apply log. Node Ready state is NOT a gate in this phase (CPs are NotReady pre-CNI \u2014 EXPECTED). See the execution notes per test.</p>\n<h3 id=\"bkmrk-f6-1-%E2%80%94-quorum-record\">F6-1 \u2014 quorum recorded</h3>\n<ul id=\"bkmrk-command%3A-etcd-member\">\n<li>Command: etcd member list via the kubectl-exec form (Phase B gate), from cp01.</li>\n<li>Expected: exactly 3 members, all <code>started</code>.</li>\n<li>STOP: member count != 3 or any member not started.</li>\n</ul>\n<h3 id=\"bkmrk-f6-stop-prerequisite\">F6 STOP prerequisites \u2014 HARD GATES (ALL must be PASS, dated in the apply log, BEFORE F6-2/F6-3 stop any rke2-server)</h3>\n<p id=\"bkmrk-these-are-gates%2C-not\">These are GATES, not suggestions. F6-3's restore test stops ALL THREE servers \u2014 full cluster API downtime \u2014 and replaces the etcd peer databases; F6-2 stops one server. Until every prerequisite below is PASS, this phase halts after the read-only F6-1 and NO <code>systemctl stop rke2-server</code> may be issued.</p>\n<ul id=\"bkmrk-p1-%E2%80%94-quorum-and-api-\">\n<li><strong>P1 \u2014 Quorum and API evidence (immediately before any stop)</strong>: F6-1 recorded with today's date (member list == 3 members, all started), plus <code>curl -k https://10.72.0.20:6443/readyz</code> \u2192 <code>ok</code>; record both outputs.</li>\n<li><strong>P2 \u2014 Snapshot saved AND copied OUT of cp01's failure domain</strong> (on cp01, as root):\n<pre><code>rke2 etcd-snapshot save --name pre-addons-fase6\nrke2 etcd-snapshot ls\n</code></pre>\nExpected: <code>save</code> reports the snapshot file written under <code>/var/lib/rancher/rke2/server/db/snapshots/</code> (on-demand snapshots default to <code>on-demand-&lt;node&gt;-&lt;unix-ts&gt;</code>; <code>--name</code> sets the base name to <code>pre-addons-fase6</code>); <code>ls</code> lists the new snapshot with a size and timestamp. Record both outputs. Then copy the snapshot file OFF cp01 to external storage \u2014 e.g. wetopi-ops01 <code>~/rke2-snapshots/</code> or the workstation's disk \u2014 using the relay documented in \u00a70 item 9 (on cp01 place it under <code>/tmp</code> readable by <code>ubuntu</code>; pull it with the workstation's ProxyCommand form; ops01 cannot pull from cp01 itself because it holds no private keys). The external copy MUST live <strong>outside <code>/var/lib/rancher/rke2/server/db</code> AND outside cp01's failure domain</strong>: NOT on cp01, NOT on any CP node, NOT inside the live db tree, and preferably not on cp01's Proxmox node (ns3243421) either \u2014 wetopi-ops01 (ns3249468) or the workstation satisfies this. cp01 keeps the copy the reset consumes; the external copy is the recovery plane if the reset damages cp01's db.</li>\n<li><strong>P3 \u2014 SHA-256 digests match, independently recomputed</strong>: <code>sha256sum /var/lib/rancher/rke2/server/db/snapshots/&lt;snapshot-file&gt;</code> on cp01, and \u2014 independently, ON the host holding the external copy (not via a shell relayed through cp01) \u2014 <code>sha256sum &lt;external-path&gt;/&lt;snapshot-file&gt;</code>. The two digests MUST be identical; record both in the apply log. Mismatch \u2192 re-copy and re-verify before anything else.</li>\n<li><strong>P4 \u2014 EXPLICIT human authorization for full API downtime</strong>: a named human explicitly authorizes the F6-3 restore window (all three servers stopped, cluster API fully down; plan tens of minutes for reset + peer rejoin), recorded in the apply log with name, date, scope, and the expected window \u2014 BEFORE the first <code>systemctl stop rke2-server</code> of F6-3. No recorded authorization line = gate NOT passed.</li>\n</ul>\n<p id=\"bkmrk-stop-%28prerequisites%29\"><strong>STOP (prerequisites)</strong>: any P1-P4 item not PASS \u2192 do NOT run F6-2/F6-3; the cluster stays up (read-only F6-1 is the only permitted test); escalate to the lead.</p>\n<h3 id=\"bkmrk-f6-2-%E2%80%94-one-cp-failur\">F6-2 \u2014 one-CP failure (recorded acceptance test; requires the F6 STOP prerequisites P1-P4 ALL PASS \u2014 this test stops a server)</h3>\n<ul id=\"bkmrk-command%3A-sudo-system\">\n<li>Command: <code>sudo systemctl stop rke2-server</code> on cp02, then <code>curl -k https://10.72.0.20:6443/readyz</code> from ops01, then <code>sudo systemctl start rke2-server</code> on cp02.</li>\n<li>Expected: <code>/readyz</code> returns <code>ok</code> throughout (2 members keep quorum); API served via VIP on remaining CPs; after restart, member list still 3.</li>\n<li>STOP: API via VIP fails while one CP is stopped (quorum/VIP defect \u2014 stop the whole apply).</li>\n</ul>\n<h3 id=\"bkmrk-f6-3-%E2%80%94-real-restore-\">F6-3 \u2014 <strong>REAL restore test (restore probado)</strong> \u2014 requires the F6 STOP prerequisites P1-P4 ALL PASS; snapshot per P2</h3>\n<blockquote id=\"bkmrk-this-is-a-real%2C-exec\">\n<p>This is a <strong>REAL, EXECUTED acceptance test</strong> \u2014 not a documented-only procedure. Master plan \u00a735.1 requires \"restore probado\"; a snapshot-save-only check does NOT satisfy it. The restore test must be executed and recorded HERE, before Phase E (Cilium). If the restore test cannot be executed, the DoD marks it UNMET and the runbook mandates STOP before all subsequent phases (E onward). The snapshot itself was saved and externally checksum-verified in the F6 STOP prerequisites P2/P3 above; this test consumes the cp01-local copy.</p>\n</blockquote>\n<p id=\"bkmrk-the-snapshot-file-is\">The snapshot file is the one saved and externally verified in prerequisites P2/P3 above (cp01-local path <code>/var/lib/rancher/rke2/server/db/snapshots/&lt;file&gt;</code>; the verified external copy is the recovery plane \u2014 if the cp01-local file is damaged, re-copy from the external copy and re-verify its SHA-256 before use). <strong>EXECUTE the restore procedure below as the acceptance test</strong> and record the evidence (expected-output lines, member list 3/3, <code>/readyz</code>, and the post-restore journal checks in step 6).</p>\n<h4 id=\"bkmrk-restore-procedure-%E2%80%94-\">Restore procedure \u2014 <code>rke2 server --cluster-reset</code> (multi-server, 3 CPs)</h4>\n<blockquote id=\"bkmrk-critical%3A-there-is-n\">\n<p><strong>CRITICAL</strong>: there is NO <code>rke2 etcd-snapshot restore</code> subcommand. Verified from the authoritative RKE2 docs (rancher/rke2-docs, docs/datastore/backup_restore.md): the <code>etcd-snapshot</code> subcommands are <code>delete</code> / <code>ls</code> (<code>list</code>, <code>l</code>) / <code>prune</code> / <code>save</code> ONLY. Restore is the server <strong>cluster-reset</strong> flow.</p>\n</blockquote>\n<p id=\"bkmrk-snapshot-on-n1-%3D-cp0\">Snapshot on N1 = cp01; peers N2/N3 = cp02/cp03:</p>\n<ol id=\"bkmrk-stop-rke2-on-all-ser\">\n<li><strong>Stop RKE2 on ALL server nodes</strong>: <code>systemctl stop rke2-server</code> on cp01, cp02, cp03.</li>\n<li><strong>On N1 (cp01)</strong>: <code>rke2 server --cluster-reset --cluster-reset-restore-path=&lt;PATH-TO-SNAPSHOT&gt;</code> (path = the snapshot file recorded by <code>rke2 etcd-snapshot ls</code>, under <code>/var/lib/rancher/rke2/server/db/snapshots/</code>).\nExpected output on success:\n<pre><code>Managed etcd cluster membership has been reset, restart without --cluster-reset flag now.\nBackup and delete ${datadir}/server/db on each peer etcd server and rejoin the nodes.\n</code></pre>\n(datadir = /var/lib/rancher/rke2)</li>\n<li><strong>On N1 (cp01)</strong>: <code>systemctl start rke2-server</code>.</li>\n<li><strong>On N2/N3 (cp02/cp03)</strong>: <code>rm -rf /var/lib/rancher/rke2/server/db/</code> then <code>systemctl start rke2-server</code> \u2014 each peer rejoins the restored cluster by re-downloading the db.</li>\n<li><strong>Re-verify</strong>: etcd member list (kubectl-exec form, from cp01) == 3 members started; <code>curl -k https://10.72.0.20:6443/readyz</code> \u2192 <code>ok</code>; the CP-local form <code>/var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml get nodes</code> (on cp01) shows 3/3 CPs registered.</li>\n<li><strong>Post-restore journal checks on EVERY CP</strong> (explicit gate \u2014 record all three outputs with a date in the apply log):\n<pre><code>sudo journalctl -u rke2-server --since \"-30 min\" --no-pager | grep -E 'level=(error|fatal)' || echo CLEAN\n</code></pre>\n<strong>Failure criteria (explicit)</strong>: on ANY of the three CPs, any <code>level=error</code>/<code>level=fatal</code> line mentioning corruption, corrupt, checksum, panic, or db/data-dir damage \u2014 OR a recurring <code>failed to start</code> persisting more than ~2 minutes after that node's start \u2014 means the restore test is <strong>FAILED</strong>: apply the STOP-and-escalate rule below; do NOT retry. Transient retry/election/timeout lines during peer rejoin that stop once quorum re-forms are acceptable. Required to PASS: <code>CLEAN</code> output (or only transient lines that cease) on all three CPs.</li>\n</ol>\n<p id=\"bkmrk-mechanics-%28from-the-\">Mechanics (from the RKE2 docs): the current etcd db files are moved to <code>${data-dir}/server/db/etcd-old-$TIMESTAMP/</code>; the snapshot checksum is verified; all etcd members except the restore node are removed from the cluster, and peers rejoin by re-downloading the db.</p>\n<p id=\"bkmrk-reset-flag-safety-me\"><strong>Reset-flag safety mechanism</strong> (rancher/rke2-docs, docs/datastore/backup_restore.md; semantics verified 2026-08-30 against the k3s source vendored by the pinned <code>rke2_version: v1.36.3+rke2r1</code> \u2014 k3s commit 4ca29faac89e, <code>pkg/cluster/managed.go</code> + <code>pkg/etcd/etcd.go</code>): when RKE2 resets the cluster it creates an empty file at <code>/var/lib/rancher/rke2/server/db/reset-flag</code> to prevent accidental repeated cluster resets; the flag is <strong>deleted only when RKE2 starts normally</strong> (a normal start of the managed database clears it). Version-exact consequences: (a) the flag is written <strong>after the snapshot-restore step succeeds</strong> \u2014 an early failure (bad snapshot path, download/checksum/restore error) happens with NO flag present; a late failure (post-reset etcd startup) happens WITH the flag present; (b) the flag blocks only a <strong>bare</strong> <code>rke2 server --cluster-reset</code> (no restore path) \u2014 the pinned version does NOT consult the flag when <code>--cluster-reset-restore-path</code> is set (source comment: \"If we're restoring from a snapshot, don't check the reset-flag - just reset and restore\"); (c) the pre-restore db is moved aside to <code>db/etcd-old-&lt;timestamp&gt;/</code> by the restore step, so a failed attempt may leave the db dir absent or partially restored.</p>\n<ul id=\"bkmrk-stop-%E2%80%94-reset%2Frestore\">\n<li><strong>STOP \u2014 reset/restore failure is an immediate STOP-and-escalate condition.</strong> If the reset invocation fails, does not print the expected output, the db dir is not re-provisioned, peers fail to rejoin, quorum is not re-established, or <code>/readyz</code> is not OK: do NOT blindly retry \u2014 the failure state is flag- and db-dependent per (a)-(c) above, and all CPs are stopped. Escalate to the lead with the full output; the cluster stays STOPped. No addons or workloads until recovery is complete and quorum is re-verified.</li>\n<li><strong>Only documented, version-supported recovery</strong> \u2014 every option below is a lead-approved, apply-log-recorded escalated action, never an operator default; a second reset attempt is permitted ONLY after a successful normal cp01 start or a coordinated rollback decision:\n<ol>\n<li><strong>Normal start on cp01</strong>: <code>systemctl start rke2-server</code> \u2014 the documented flag-clearing mechanism. It starts from whatever db state the failed attempt left: if the snapshot-restore step succeeded and only the startup failed, the node comes up as the restored single member; if the restore step itself failed (db moved aside, restore incomplete), the node may fail to start or come up uninitialized. Record the actual observed state (flag present? <code>db/etcd-old-*</code> present? member list, <code>/readyz</code>) and STOP again if anything is unexpected. <strong>A successful normal start is the precondition for considering any second reset attempt.</strong></li>\n<li><strong>Second attempt, only after (1) succeeded or a coordinated rollback decision</strong>: re-attempt the documented restore with a KNOWN-GOOD snapshot (stop rke2-server on all CPs \u2192 <code>rke2 server --cluster-reset --cluster-reset-restore-path=&lt;PATH&gt;</code> on cp01 \u2192 normal start cp01 \u2192 peers <code>rm -rf /var/lib/rancher/rke2/server/db/</code> + start). Version fact for the record: the flag check applies only to a bare <code>--cluster-reset</code>; the pinned version does not consult the flag when a restore path is set. This runbook never performs a bare reset \u2014 do not remove <code>reset-flag</code> manually; if it is ever encountered blocking a start, that is itself a STOP-and-escalate condition.</li>\n<li><strong>Coordinated rollback</strong>: Proxmox <code>qm rollback &lt;vmid&gt; pre-rke2-apply</code> applied to ALL CPs together \u2014 the snapshot predates Phase B, so this returns the cluster to the pre-B state and phases B/C must be re-run.</li>\n<li><strong>Escalate to Rancher support</strong> with the exact rke2 version and full logs.</li>\n</ol>\n</li>\n<li>A restore is a STOP-and-reassess event: after the test, re-run F6-1/F6-2 gates and only then proceed to Phase E.</li>\n</ul>\n<p id=\"bkmrk-stop-%28f6-overall%29%3A-a\"><strong>STOP (F6 overall)</strong>: any F6 STOP prerequisite P1-P4 not PASS (no server may be stopped); any F6-1/F6-2/F6-3 gate failing \u2014 the cluster is not accepted; fix before Phase E (these three are the pre-addon CP-only gates and are the ONLY Fase-6 gates blocking E). The worker-lifecycle drill is the separately-named Phase W of the same executable order (<strong>B \u2192 C \u2192 STOP \u2192 F6-1..3 \u2192 E \u2192 D \u2192 D2 \u2192 W \u2192 F \u2192 G</strong>); it runs after D2 and its gates are defined there. The Fase-6 acceptance ledger is COMPLETE only when F6-1..3 AND Phase W's W-1/W-2 all have dated PASS entries; W passes POST-D2 by design (ADR-0005 \u2014 see banner), never pre-addon.</p>\n<h2 id=\"bkmrk-phase-e-%E2%80%94-cilium-ins\">Phase E \u2014 Cilium install + basic install validation (first addon)</h2>\n<blockquote id=\"bkmrk-runs-as-the-immediat\">\n<p>Runs as the <strong>immediate first addon after the CP-only Fase-6 gates (F6-1..3)</strong> \u2014 and BEFORE workers join: with Cilium already present, newly joined workers can reach Ready as soon as their Cilium agent starts. (The worker-lifecycle drill \u2014 drain/reboot \u2014 is the separately-named <strong>Phase W</strong> and executes later in the executable order, AFTER D2; it does not block E.) This runbook splits Cilium work in two: <strong>Phase E = install + basic install validation ONLY</strong>; the <strong>full Cilium acceptance</strong> (connectivity test, Hubble flows, DNS, NetworkPolicy, one CiliumNode per node) runs in <strong>Phase D2 on the FINAL 6-node topology</strong> (see D2).</p>\n</blockquote>\n<p id=\"bkmrk-playbook-ansible%2Fpla\">Playbook <code>ansible/playbooks/cilium.yml</code> (infrastructure PR #2, merged as e283e24): runs from the <code>operations</code> host (ops01) with <code>connection: local</code> so the Helm op executes exactly once; chart pinned <code>oci://quay.io/cilium/charts/cilium --version 1.20.1</code> (full OCI reference; the <code>--repo oci://</code> form is NOT valid); values <code>kubernetes/cilium/values.yaml</code> (VXLAN tunnel, kubeProxyReplacement false, Hubble enabled).</p>\n<p id=\"bkmrk-gate-semantics-are-f\"><strong>Gate semantics are FAIL-CLOSED</strong>: every kubectl probe uses <code>--ignore-not-found</code> and requires rc 0 \u2014 any rc != 0 (API error, connection refused, auth/TLS failure) ABORTS the run; absence of a resource is proven ONLY by rc 0 with EMPTY stdout. An error is never \"absent\": it is a STOP.</p>\n<p id=\"bkmrk-ordering-gates-%28fail\">Ordering gates (fail-closed, per ADR-0005): the play refuses to run if a CNI is already deployed (Canal would fight Cilium), requires kube-proxy present (RKE2 with <code>cni: none</code> provides it), and aborts unless the Cilium agent is absent (first-install check).</p>\n<p id=\"bkmrk-command-%28from-ops01-\">Command (from ops01 \u2014 AFTER the \u00a70 item 9 ops01 provisioning is verified: checkout at d553643 or a verified descendant, Ansible venv, inventory, helm, and the current transferred kubeconfig in <code>KUBECONFIG</code>; this playbook targets the <code>operations</code> group with <code>connection: local</code>, so the Helm op runs on ops01 itself):</p>\n<pre id=\"bkmrk-ansible_config%3Dansib-1\"><code>ANSIBLE_CONFIG=ansible/ansible.cfg \\\n  .venv/bin/ansible-playbook -i ansible/inventories/prod/hosts.json \\\n  ansible/playbooks/cilium.yml\n</code></pre>\n<p id=\"bkmrk-firewall-prerequisit\">Firewall prerequisites (node LAN must allow): UDP 8472 (VXLAN) between all nodes; TCP 4244 (Hubble Relay flow scraping). MTU stays Cilium default (~1450) on the MTU-1500 LAN.</p>\n<h3 id=\"bkmrk-validation-gate-e-%28i\">Validation gate E (install-only)</h3>\n<p id=\"bkmrk-basic-install-valida\">Basic install validation on the CP-only (3-node) cluster, from ops01 with <code>KUBECONFIG</code> set to the transferred admin kubeconfig (\u00a70 item 9):</p>\n<pre id=\"bkmrk-cilium-status---wait\"><code>cilium status --wait     # agents Ready on the 3 CPs; KVStore/Conntrack/ControllerStatus Ok, Hubble Ok/Enabled\nkubectl -n kube-system get pods -l k8s-app=cilium    # cilium-ds Running+Ready on all 3 CPs (one per CP)\nkubectl -n kube-system get pods -l io.cilium/app=operator   # cilium-operator Running+Ready\n</code></pre>\n<p id=\"bkmrk-the-following-are-de\">The following are deliberately <strong>NOT Phase E gates</strong> (they are D2 gates on the final 6-node topology): <code>cilium connectivity test</code>, Hubble flow observation, DNS resolution, NetworkPolicy enforcement, <code>CiliumNode</code> per-node completeness.</p>\n<p id=\"bkmrk-also-note-%28validatio\">Also note (VALIDATION.md \u00a77): pods that existed before Cilium must be restarted once to fall under the eBPF datapath (in this greenfield order the only such pods are the control-plane static/system pods; kube-proxy static pods and CoreDNS are handled by rollout/restart once nodes go Ready).</p>\n<p id=\"bkmrk-rollback%3A-helm-unins\">Rollback: <code>helm uninstall cilium --namespace kube-system</code> \u2014 documented in VALIDATION.md \u00a78 with the warning that the cluster has NO other CNI after this (<code>cni: none</code> means RKE2 will not reinstall Canal), so only do this with a replacement plan.</p>\n<p id=\"bkmrk-stop%3A-any-fail-close\"><strong>STOP</strong>: any fail-closed probe rc != 0; cilium-ds pod not Running+Ready on each of the 3 CPs; cilium-operator not Running+Ready.</p>\n<h2 id=\"bkmrk-phase-d-%E2%80%94-platform-w\">Phase D \u2014 platform workers join (registration + label gates only)</h2>\n<p id=\"bkmrk-playbook-ansible%2Fpla-1\">Playbook <code>ansible/playbooks/rke2-workers.yml</code> (infrastructure PR #5, merged as 8d836e8): play 1 pre-flights <code>rke_platform</code> (asserts <code>rke2_server_url == https://10.72.0.20:9345</code>, CIDRs inherited from shared group_vars, token file exists 0600 \u2014 no_log), play 2 (hosts <code>rke-cp01</code>) applies/verifies the control-plane NoSchedule taint <code>node-role.kubernetes.io/control-plane=true:NoSchedule</code> on all 3 CPs (idempotency decided from API pre-state), play 3 joins rke-platform01..03 (<code>serial: 1</code>, <code>any_errors_fatal: true</code>, rke2-agent role; config: token from the gitignored 0600 file, <code>server: https://10.72.0.20:9345</code>, <code>node-label: [wetopi.io/role=platform]</code>, no taints).</p>\n<p id=\"bkmrk-command-%28from-the-co-1\">Command (from the CONTROLLER \u2014 the workstation per \u00a70 item 9):</p>\n<pre id=\"bkmrk-ansible_config%3Dansib-2\"><code>ANSIBLE_CONFIG=ansible/ansible.cfg \\\n  .venv/bin/ansible-playbook -i ansible/inventories/prod/hosts.json \\\n  ansible/playbooks/rke2-workers.yml\n</code></pre>\n<p id=\"bkmrk-per-worker-join-gate\"><strong>Per-worker join gate (inside the serial join play)</strong>: for each worker, the gate is control-plane-OBSERVED registration + exact label \u2014 <code>kubectl get node &lt;name&gt; -o json</code> delegated to rke-cp01 (retries 60 \u00d7 10s), then assert <code>metadata.name == inventory_hostname</code> and <code>labels['wetopi.io/role'] == 'platform'</code>. A failed gate aborts before the next worker joins. <strong>Phase D gate = registration + label ONLY.</strong></p>\n<p id=\"bkmrk-ready-state-after-jo\"><strong>Ready state after join (Cilium-before-workers)</strong>: Cilium was installed in Phase E, so a newly joined worker <strong>SHOULD reach Ready once its Cilium agent (the cilium-ds DaemonSet pod) starts on it</strong> \u2014 <code>NotReady</code> is a <strong>TRANSIENT join state, NOT indefinitely expected</strong>. Ready/pod/DNS/scheduling validation is deferred to <code>rke2-workers-validate.yml</code> (Phase D2), which must prove: 6/6 nodes Ready, a cilium-ds pod per node, and a CiliumNode object per node, plus the full Cilium acceptance. <strong>A worker that stays NotReady indefinitely is a FAILURE to investigate (Cilium agent attachment issue), never \"expected\".</strong></p>\n<p id=\"bkmrk-after-the-join-play%2C\">After the join play, the playbook reports the result; workers are expected to transition to Ready as their Cilium agents come up. Full validation runs next in D2.</p>\n<h2 id=\"bkmrk-phase-d2-%E2%80%94-full-post\">Phase D2 \u2014 full post-Cilium cluster validation (rke2-workers-validate.yml + FULL Cilium acceptance, on the FINAL 6-node topology)</h2>\n<p id=\"bkmrk-playbook-ansible%2Fpla-2\">Playbook <code>ansible/playbooks/rke2-workers-validate.yml</code> (PR #5, merged as 8d836e8; hosts <code>rke-cp01</code>, <code>become: true</code>, <code>any_errors_fatal: true</code>, read-only queries + two short-lived uniquely-named test pods, always deleted). Run from the CONTROLLER (\u00a70 item 9 \u2014 the play reaches cp01 over SSH) AFTER Cilium (Phase E) and after the workers have joined (Phase D). This phase ALSO carries the <strong>full Cilium acceptance</strong> (moved from former Phase E) on the final 6-node topology:</p>\n<pre id=\"bkmrk-ansible_config%3Dansib-3\"><code>ANSIBLE_CONFIG=ansible/ansible.cfg \\\n  .venv/bin/ansible-playbook -i ansible/inventories/prod/hosts.json \\\n  ansible/playbooks/rke2-workers-validate.yml\n</code></pre>\n<p id=\"bkmrk-built-in-gates-%28manu\">Built-in gates (manual equivalents \u2014 run ON cp01 using the CP-local kubectl form, \u00a70 item 9; define once for the shell: <code>RKE2_KUBECTL=\"/var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml\"</code>):</p>\n<pre id=\"bkmrk-%24rke2_kubectl-get-no\"><code>$RKE2_KUBECTL get nodes                          # 6 nodes, ALL Ready (3 CP + 3 platform)\n$RKE2_KUBECTL get nodes -l wetopi.io/role=platform   # exactly 3, each carrying the exact label\n$RKE2_KUBECTL -n kube-system get pods -l k8s-app=kube-dns            # Running+Ready\n$RKE2_KUBECTL -n kube-system get pods -l k8s-app=kube-dns-autoscaler # Running+Ready\n$RKE2_KUBECTL -n kube-system get pods -l app=rke2-metrics-server     # Running+Ready\n$RKE2_KUBECTL -n kube-system get pods -l k8s-app=cilium              # Running+Ready, one per node\n$RKE2_KUBECTL -n kube-system get pods -l io.cilium/app=operator      # Running+Ready\n$RKE2_KUBECTL -n kube-system get pods -l k8s-app=hubble-relay         # Running+Ready\n$RKE2_KUBECTL -n kube-system get pods -l component=kube-proxy         # Running+Ready, one per node\n$RKE2_KUBECTL run rke2-dns-check-&lt;suffix&gt; --restart=Never \\\n  --image=registry.k8s.io/e2e-test-images/jessie-dnsutils:1.7 \\\n  --command -- sleep 300\n$RKE2_KUBECTL wait --for=condition=Ready pod/rke2-dns-check-&lt;suffix&gt; --timeout=300s\n$RKE2_KUBECTL exec rke2-dns-check-&lt;suffix&gt; -- nslookup kubernetes.default.svc.cluster.local\n$RKE2_KUBECTL run rke2-sched-check-&lt;suffix&gt; --restart=Never \\\n  --image=registry.k8s.io/e2e-test-images/busybox:1.36.1-1 \\\n  --overrides='{\"spec\":{\"nodeSelector\":{\"wetopi.io/role\":\"platform\"}}}' \\\n  --command -- sleep 300\n</code></pre>\n<p id=\"bkmrk-expected-success%3A\">Expected success:</p>\n<ul id=\"bkmrk-6%2F6-nodes-ready-%28the\">\n<li>6/6 nodes Ready (the playbook retries up to 60 \u00d7 10s);</li>\n<li>every platform worker carries the exact label <code>wetopi.io/role=platform</code> (count == 3);</li>\n<li>all required kube-system components Running+Ready with the selectors above (CoreDNS <code>k8s-app=kube-dns</code> and autoscaler <code>k8s-app=kube-dns-autoscaler</code> min 1 each; metrics-server <code>app=rke2-metrics-server</code> min 1; Cilium agent <code>k8s-app=cilium</code> one per node; cilium-operator <code>io.cilium/app=operator</code> min 2; hubble-relay <code>k8s-app=hubble-relay</code> min 1; kube-proxy <code>component=kube-proxy</code> one per node);</li>\n<li>DNS gate: nslookup resolves <code>kubernetes.default.svc.cluster.local</code> to answer address(es) in 10.68.0.0/16 \u2014 the playbook parses the ANSWER addresses (the Address lines AFTER the <code>Name: kubernetes.default...</code> line) into a CSV string and requires every entry to match the octet-bounded regex <code>^10\\.68\\.\\d{1,3}\\.\\d{1,3}$</code>; the resolver's own <code>Server: 10.68.0.10</code> line appears BEFORE the Name line and must never satisfy the gate;</li>\n<li>scheduling gate: the test pod lands on a node of group <code>rke_platform</code> (placement on a CP = FAIL \u2014 investigate taints/labels).</li>\n</ul>\n<h3 id=\"bkmrk-d2-full-cilium-accep\">D2 full Cilium acceptance (final 6-node topology, from kubernetes/cilium/VALIDATION.md; cilium/hubble CLIs on ops01 \u2014 \u00a70 item 9 ops01 provisioning \u2014 with <code>KUBECONFIG</code> set to the transferred admin kubeconfig; the <code>kubectl</code> lines in THIS block are the ops01 remote form, not CP-local)</h3>\n<pre id=\"bkmrk-cilium-status---wait-1\"><code>cilium status --wait            # all agents ready, KVStore/Conntrack/ControllerStatus Ok, Hubble Ok/Enabled\ncilium connectivity test        # all tests pass, incl. cross-node pod-to-pod, pod-to-service, north-south\nhubble status                   # flows counters increasing, sane uptime\nhubble observe --since 5m --from-cluster   # real flows with identities and VERDICTs, not empty\nkubectl run dns-utils --image=busybox:1.36 --restart=Never -- sleep 60\nkubectl wait --for=condition=Ready pod/dns-utils --timeout=120s\nkubectl exec dns-utils -- nslookup kubernetes.default    # resolves into 10.68.0.0/16\nkubectl delete pod dns-utils\nkubectl get ciliumnodes.cilium.io    # one CiliumNode object per node (6/6); podCIDR /24s inside 10.64.0.0/14\n</code></pre>\n<p id=\"bkmrk-networkpolicy-sample\">NetworkPolicy sample (VALIDATION.md \u00a75):</p>\n<pre id=\"bkmrk-kubectl-create-names\"><code>kubectl create namespace cilium-validation\nkubectl -n cilium-validation run web --image=nginx:1.27 --restart=Never\nkubectl -n cilium-validation expose pod web --port 80\nkubectl apply -f kubernetes/cilium/test-default-deny.yaml     # default-deny Ingress in that namespace\nkubectl -n cilium-validation run probe --image=busybox:1.36 --restart=Never \\\n  -- wget -qO- --timeout=5 http://web                          # MUST fail (dropped)\nhubble observe --namespace cilium-validation --since 5m        # shows VERDICT Dropped\nkubectl delete -f kubernetes/cilium/test-default-deny.yaml\nkubectl delete namespace cilium-validation\n</code></pre>\n<ul id=\"bkmrk-one-cilium-ds-pod-pe\">\n<li><strong>One cilium-ds pod per node</strong>: <code>kubectl -n kube-system get pods -l k8s-app=cilium -o wide</code> \u2192 exactly 6 Running+Ready pods, one on each of the 6 nodes (3 CP + 3 platform).</li>\n<li><strong>One CiliumNode object per node</strong>: <code>kubectl get ciliumnodes.cilium.io</code> \u2192 exactly 6, one per node, each podCIDR inside 10.64.0.0/14.</li>\n</ul>\n<p id=\"bkmrk-stop-%28d2-overall%29%3A-a\"><strong>STOP (D2 overall)</strong>: any node not Ready after the retry window; any selector returning pods that are not all Running+Ready (or missing entirely); DNS gate failing (rc != 0, zero parsed answers, or any answer outside 10.68.0.0/16); sched-check not placed on a platform-labeled node; cilium connectivity test failures; hubble observe empty; NetworkPolicy probe NOT dropped; any podCIDR outside 10.64.0.0/14; fewer than 6 CiliumNode objects or missing cilium-ds pod on any node.</p>\n<p id=\"bkmrk-the-executable-order\">The executable order (identical to the banner, the F6 heading + STOP rule, the Phase W heading, the DoD mapping, and the provenance section) is <strong>B \u2192 C \u2192 STOP \u2192 F6-1..3 \u2192 E \u2192 D \u2192 D2 \u2192 W \u2192 F \u2192 G</strong>: the CP-only Fase-6 gates (F6-1 quorum, F6-2 one-CP failure, F6-3 snapshot + REAL restore test) sat BEFORE this phase; <strong>Phase W \u2014 the worker lifecycle drill (drain + reboot) \u2014 runs NEXT</strong>, after these D2 gates pass, on the final 6-node topology. Do NOT count Phase W as completed pre-addon acceptance.</p>\n<h2 id=\"bkmrk-phase-w-%E2%80%94-worker-lif\">Phase W \u2014 worker lifecycle drill (drain + reboot; former F6-4/F6-5)</h2>\n<p id=\"bkmrk-part-of-the-authoriz-1\">Part of the authorized executable order (identical to the banner, the F6 heading + STOP rule, the D2 handoff, the DoD mapping, and the provenance section): <strong>B \u2192 C \u2192 STOP \u2192 F6-1..3 \u2192 E \u2192 D \u2192 D2 \u2192 W \u2192 F \u2192 G.</strong> This is a <strong>SEPARATE named phase</strong> \u2014 the worker-lifecycle portion of the Fase-6 acceptance \u2014 running <strong>AFTER the D2 gates pass</strong> on the final 6-node topology, <strong>BEFORE Phase F</strong>, and <strong>NEVER before the addons</strong>: drain/reboot evidence is POST-D2 by design (ADR-0005: with <code>cni: none</code>, workers cannot pass Ready \u2014 and therefore cannot pass drain/reboot gates \u2014 before Cilium exists; see the banner for the \u00a733 tension and its explicit resolution). Results are recorded in the Fase-6 acceptance ledger under their historical aliases F6-4 (drain) / F6-5 (reboot), which map to master plan \u00a735.1/\u00a735.2 drain/reboot recovery. Do NOT count this phase as completed pre-addon acceptance.</p>\n<h3 id=\"bkmrk-w-1-%E2%80%94-drain-one-work\">W-1 \u2014 drain one worker (former F6-4)</h3>\n<ul id=\"bkmrk-command-%28cp-local-fo\">\n<li>Command (CP-local form on cp01 \u2014 or the ops01 remote form with the transferred kubeconfig, \u00a70 item 9):\n<pre><code>/var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml drain rke-platform01 --ignore-daemonsets --delete-emptydir-data\n/var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml uncordon rke-platform01\n</code></pre>\n</li>\n<li>Expected: drain evicts/reschedules eligible pods (kube-system DaemonSet pods stay \u2014 expected); node shows <code>SchedulingDisabled</code> during the drain; after uncordon the node returns to Ready/Schedulable.</li>\n<li>STOP: drain hangs on un-evictable pods (investigate PodDisruptionBudgets), node not Schedulable after uncordon.</li>\n</ul>\n<h3 id=\"bkmrk-w-2-%E2%80%94-reboot-one-wor\">W-2 \u2014 reboot one worker + Ready return (former F6-5)</h3>\n<ul id=\"bkmrk-command%3A-sudo-reboot\">\n<li>Command: <code>sudo reboot</code> on rke-platform02 (via the documented SSH form, \u00a70 item 5), then on cp01 (CP-local form):\n<pre><code>/var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml get node rke-platform02 -w    # wait for Ready\n</code></pre>\n</li>\n<li>Expected: node cycles NotReady \u2192 Ready within ~2-3 min of boot (kubelet re-registers, Cilium agent re-attaches); label <code>wetopi.io/role=platform</code> still present; no NotReady residue after ~5 min.</li>\n<li>STOP: node not Ready after 10 min, label lost, or the Cilium agent on that node not Running+Ready after reboot.</li>\n</ul>\n<p id=\"bkmrk-stop-%28w-overall%29%3A-ei\"><strong>STOP (W overall)</strong>: either W-1/W-2 gate failing \u2014 fix before Phase F. With W-1/W-2 recorded PASS (post-D2) alongside F6-1..3 (pre-addon), the Fase-6 acceptance ledger is COMPLETE; the DoD maps drain/reboot recovery to this POST-D2 evidence, never to pre-addon acceptance.</p>\n<h2 id=\"bkmrk-phase-f-%E2%80%94-ceph-routi\">Phase F \u2014 Ceph routing + RBD CSI</h2>\n<p id=\"bkmrk-source-artifacts-%28al\">Source artifacts (all on merged main): <code>ansible/roles/nat_gateway</code> (vars <code>natgw_ceph_routing_enabled: false</code>, <code>natgw_ceph_address: \"\"</code> \u2014 default-off until gates), <code>kubernetes/ceph-csi/</code> (chart <code>ceph-csi-rbd</code> pinned 3.17.1, StorageClass <code>ceph-rbd</code>, <code>secret.example.yaml</code>), <code>ansible/playbooks/ceph-csi.yml</code>, <code>kubernetes/ceph-csi/VALIDATION.md</code>, <code>kubernetes/ceph-csi/README.md</code>.</p>\n<p id=\"bkmrk-phase-f-has-two-inde\">Phase F has TWO independent sub-phases; the CSI part does not block the cluster (RKE2 installs with no storage class).</p>\n<h3 id=\"bkmrk-f0-%E2%80%94-tofu-nic-change\">F0 \u2014 tofu NIC change (NEW precondition for F1)</h3>\n<p id=\"bkmrk-the-third-nic-for-th\">The third NIC for the Ceph leg lives in <code>tofu/proxmox/environments/prod/terraform.tfvars.example</code>: a commented-out block at the end of the wetopi-natgw01 nic list with the ADR-0004 comment above it (VLAN 2 / vmbr2, address from 192.168.96.0/20; guest-side config via the nat_gateway role's <code>natgw_ceph_*</code> vars, NOT cloud-init). F1 requires it enabled via a <strong>reviewed PR</strong>, not an apply-time edit:</p>\n<ol id=\"bkmrk-open-a-pr-uncommenti\">\n<li>Open a PR uncommenting the block with the allocated address:\n<pre><code># {\n#   bridge       = \"vmbr2\"\n#   ipv4_address = \"192.168.96.&lt;allocated&gt;/20\"\n#   firewall     = false\n# }\n</code></pre>\n\u2192 <code>{ bridge = \"vmbr2\", ipv4_address = \"192.168.96.&lt;allocated&gt;/20\", firewall = false }</code>.</li>\n<li>Merge it (wetopi-approver review), then on ops01 check out a <strong>clean main</strong> and run:\n<pre><code>tofu -chdir=tofu/proxmox/environments/prod plan\n</code></pre>\n</li>\n<li><strong>tofu plan gate (record the output)</strong>: the plan must show ADD of <strong>exactly one NIC on wetopi-natgw01</strong> and <strong>ZERO destroys</strong> (no other resource touched). Any destroy = STOP.</li>\n<li><code>tofu apply</code> = <strong>HUMAN gate</strong> \u2014 a human explicitly reviews the plan and runs the apply; record evidence (plan summary + apply output) in the apply log.</li>\n</ol>\n<p id=\"bkmrk-stop-%28f0%29%3A-plan-show\"><strong>STOP (F0)</strong>: plan shows any destroy; plan touches any other VM/resource; apply fails.</p>\n<h3 id=\"bkmrk-f1-%E2%80%94-routing\">F1 \u2014 Routing</h3>\n<p id=\"bkmrk-adr-0004-shape%3A-rout\">ADR-0004 shape: route, not NAT; third Nic on wetopi-natgw01, VLAN 2, into Ceph LAN 192.168.96.0/20.</p>\n<p id=\"bkmrk-preconditions-%28human\">Preconditions (human, from VALIDATION.md \u00a71 \u2014 each gate verified and recorded BEFORE flipping the vars):</p>\n<table id=\"bkmrk-gate-check-%28a%29-vlan-\">\n<thead>\n<tr>\n<th>Gate</th>\n<th>Check</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>(a) VLAN 2 address allocation</td>\n<td>pick a free address in 192.168.96.0/20 \u2014 check live allocations (<code>ip -4 route</code>, Ceph-side <code>ceph mon dump</code>)</td>\n</tr>\n<tr>\n<td>(b) Ceph-side return-route owner</td>\n<td>who installs <code>ip route 10.72.0.0/23 via &lt;natgw VLAN 2 addr&gt;</code> on Ceph hosts (not managed by us \u2014 confirm with Ceph owner, record mechanism)</td>\n</tr>\n<tr>\n<td>(c) firewall path both directions</td>\n<td><code>nft list chain inet wetopi_filter forward</code> shows the two ceph_net accept rules; <code>nft list table ip wetopi_nat</code> still scopes NAT to oifname WAN only</td>\n</tr>\n<tr>\n<td>(d) rp_filter=1 behavior</td>\n<td><code>sysctl net.ipv4.conf.all.rp_filter net.ipv4.conf.default.rp_filter</code>; asymmetric drops = missing gate b</td>\n</tr>\n<tr>\n<td>(e) MTU 1500 on VLAN 2</td>\n<td>from a CP: <code>ping -c 3 -M do -s 1472 &lt;natgw-ceph-addr&gt;</code> passes, <code>-s 1473</code> fails</td>\n</tr>\n<tr>\n<td>(f) no CIDR overlap</td>\n<td>10.64.0.0/14 + 10.68.0.0/16 vs 192.168.96.0/20 + 10.72.0.0/23 (<code>ip -4 route</code> review; <code>ip route get &lt;ceph-host&gt;</code> takes the gateway)</td>\n</tr>\n</tbody>\n</table>\n<p id=\"bkmrk-apply%3A-via-reviewed-\">Apply: <strong>via reviewed PR, not an apply-time edit</strong> \u2014 open a PR setting <code>natgw_ceph_routing_enabled: true</code> + <code>natgw_ceph_address: &lt;chosen&gt;</code> in <code>ansible/inventories/prod/group_vars/all.yml</code>, merge it, then check out a clean main and re-run the baseline playbook (idempotent; its first play applies nat_gateway to wetopi-natgw01):</p>\n<pre id=\"bkmrk-ansible_config%3Dansib-4\"><code>ANSIBLE_CONFIG=ansible/ansible.cfg \\\n  .venv/bin/ansible-playbook -i ansible/inventories/prod/hosts.json \\\n  ansible/playbooks/bootstrap.yml\n</code></pre>\n<p id=\"bkmrk-gateway-checks-%28on-w\">Gateway checks (on wetopi-natgw01, VALIDATION.md \u00a72): <code>ip -4 route</code> shows <code>192.168.96.0/20 dev eth2 ... src &lt;natgw_ceph_address&gt;</code>; <code>ip route get 192.168.96.10</code> via eth2; <code>ip route get 10.72.0.21</code> still via eth0 src 10.72.0.2. Ceph-side (prerequisite, VALIDATION.md \u00a73): <code>ip route get 10.72.0.21</code> on a Ceph host must go via the natgw VLAN 2 address.</p>\n<p id=\"bkmrk-cp-node-connectivity\">CP-node connectivity (from 10.72.0.21, VALIDATION.md \u00a74):</p>\n<pre id=\"bkmrk-ping--c-3-192.168.96\"><code>ping -c 3 192.168.96.10          # Ceph monitor; 0% loss\ntraceroute -n 192.168.96.10      # one hop 10.72.0.1 (gateway), then the mon host\nnc -vz -w 3 192.168.96.10 3300   # msgr2 open\nnc -vz -w 3 192.168.96.10 6789   # msgr1 open\ntimeout 3 bash -c 'echo &gt; /dev/tcp/192.168.96.10/3300' &amp;&amp; echo OK   # fallback if nc absent\n</code></pre>\n<h3 id=\"bkmrk-f2-%E2%80%94-csi-install\">F2 \u2014 CSI install</h3>\n<p id=\"bkmrk-human-prerequisites-\">Human prerequisites (README OPEN list): dedicated Ceph pool (suggested <code>kubernetes</code>); dedicated Ceph user + caps (e.g. <code>client.kubernetes-csi</code>, never admin); live <code>clusterID</code> (fsid) + monitor addresses replacing the placeholders in <code>kubernetes/ceph-csi/values.yaml</code> + <code>storageclass.yaml</code> \u2014 <strong>via reviewed PR, then merge and a clean main checkout</strong> (placeholders are tracked files; never edit them at apply time); <code>csi-rbd-secret</code> in <code>kube-system</code> created BY HAND from that user's keyring per <code>secret.example.yaml</code> (key material never enters Git; Vault flow later).</p>\n<p id=\"bkmrk-stop-%5Bencryption-gat\"><strong>STOP [encryption-gate] before creating <code>csi-rbd-secret</code></strong>: the Ceph key stored in a Kubernetes Secret must be confirmed PROTECTED at rest by the <strong>measured-protection checks of \u00a70 item 8</strong> (the operative gate rule \u2014 current protection is determined by measured evidence, not by config-template contents; this build's default-on AES-CBC state is the baseline, re-verify, do not assume). Before creating the Secret: <code>rke2 secrets-encrypt status</code> on all three CPs showing <code>Encryption Status: Enabled</code> / <code>Current Rotation Stage: start</code> / <code>Server Encryption Hashes: All hashes match</code> and an Active <code>* AES-CBC aescbckey...</code> key (\u00a70 item 8 check (i)). Then create <code>csi-rbd-secret</code> per the prerequisites above and immediately run check (ii) on it: <code>etcdctl ... get /registry/secrets/kube-system/csi-rbd-secret</code> via the approved kubectl-exec 17-arg form (\u00a70 item 8) showing <code>k8s:enc:aescbc</code> ciphertext, NOT the plaintext from <code>kubectl get secret csi-rbd-secret -n kube-system -o jsonpath='{.data.&lt;KEY&gt;}' | base64 -d</code>. <strong>STOP if either check fails or shows plaintext \u2014 do not run the csi playbook.</strong> (The explicit reviewed <code>secrets-encryption: true</code> line is the separately tracked future hardening/rotation prerequisite per \u00a70 item 8 \u2014 it is not an unlock condition here while the measured checks pass.) Then:</p>\n<pre id=\"bkmrk-ansible_config%3Dansib-5\"><code>ANSIBLE_CONFIG=ansible/ansible.cfg \\\n  .venv/bin/ansible-playbook -i ansible/inventories/prod/hosts.json \\\n  ansible/playbooks/ceph-csi.yml\n</code></pre>\n<p id=\"bkmrk-%28run-from-the-contro\">(Run from the controller or ops01 \u2014 the play targets <code>rke_control_plane</code> with <code>connection: local</code> + <code>run_once: true</code>, so helm/kubectl talk to the cluster API from wherever ansible runs; kubeconfig resolution: <code>-e kubeconfig=</code> \u2192 <code>$KUBECONFIG</code> \u2192 <code>/etc/rancher/rke2/rke2.yaml</code> \u2014 on a non-CP host the transferred kubeconfig must be in <code>KUBECONFIG</code>, \u00a70 item 9.)</p>\n<p id=\"bkmrk-the-playbook-fails-e\">The playbook fails early on placeholders or missing secret (by design); it targets <code>rke_control_plane</code> with <code>connection: local</code>, <code>run_once: true</code>, kubeconfig resolution: <code>-e kubeconfig=</code> \u2192 <code>$KUBECONFIG</code> \u2192 <code>/etc/rancher/rke2/rke2.yaml</code>; helm <code>upgrade --install ceph-csi-rbd ceph-csi-rbd --repo https://ceph.github.io/csi-charts --version 3.17.1 -n kube-system --values kubernetes/ceph-csi/values.yaml</code>; then applies <code>storageclass.yaml</code>.</p>\n<p id=\"bkmrk-pvc-acceptance-%28vali\">PVC acceptance (VALIDATION.md \u00a75, maps master plan \u00a735.3):</p>\n<pre id=\"bkmrk-kubectl--n-kube-syst\"><code>kubectl -n kube-system get pods -l app=ceph-csi-rbd     # all Running\nkubectl get storageclass ceph-rbd                       # provisioner rbd.csi.ceph.com, allowVolumeExpansion true\nkubectl apply -f - &lt;&lt;'EOF'\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: ceph-rbd-test\nspec:\n  accessModes: [\"ReadWriteOnce\"]\n  storageClassName: ceph-rbd\n  resources:\n    requests:\n      storage: 1Gi\nEOF\nkubectl get pvc ceph-rbd-test -w     # Bound within ~1 min\n# (b) mount+write: kubectl run ceph-rbd-test --rm -it --restart=Never --image=nginx:alpine \\\n#   --overrides='{\"spec\":{\"containers\":[{\"name\":\"ceph-rbd-test\",\"image\":\"nginx:alpine\",\"command\":[\"sh\"],\"stdin\":true,\"tty\":true,\"volumeMounts\":[{\"name\":\"data\",\"mountPath\":\"/mnt/data\"}]}],\"volumes\":[{\"name\":\"data\",\"persistentVolumeClaim\":{\"claimName\":\"ceph-rbd-test\"}}]}}'\n#   inside: date &gt; /mnt/data/marker.txt &amp;&amp; cat /mnt/data/marker.txt &amp;&amp; exit\n# (c) persistence: delete the pod, re-run the SAME kubectl run, marker still readable with the same timestamp\n# (d) failure test: kubectl delete pvc ceph-rbd-test; on a Ceph host: rbd ls kubernetes  \u2192 no csi-vol- image for the PVC\n# (e) performance: no CSI errors/timeouts in kubectl -n kube-system logs deploy/ceph-csi-rbd-provisioner (and node plugin logs) across (a)-(d); provisioning within ~1 min\n</code></pre>\n<p id=\"bkmrk-rollback-f%3A-routing-\">Rollback F: routing \u2014 revert <code>natgw_ceph_routing_enabled: false</code> (and clear the address) <strong>via reviewed PR</strong> and re-run <code>bootstrap.yml</code> (the role removes the leg; the default-off state is the reviewed baseline). CSI \u2014 inverse of the install: <code>helm uninstall ceph-csi-rbd -n kube-system</code>, <code>kubectl delete -f kubernetes/ceph-csi/storageclass.yaml</code>, delete test PVCs first; keep <code>csi-rbd-secret</code> out of Git always. <em>(This rollback line is runbook-authored inverse of the playbook install; the parent README documents no uninstall procedure.)</em></p>\n<p id=\"bkmrk-stop-%28f0%29%3A-plan-show-1\"><strong>STOP (F0)</strong>: plan shows destroys or extra resource churn.\n<strong>STOP (F1)</strong>: missing connected route, wrong next-hop, ping loss, TCP 3300/6789 closed, MTU probe failing.\n<strong>STOP (F2)</strong>: [encryption-gate] measured-protection checks failing (\u00a70 item 8); PVC Pending (describe it; check provisioner logs \u2014 usual causes: placeholder clusterID/monitors, wrong userKey, routing regression), any acceptance step (b)-(e) failing.</p>\n<h2 id=\"bkmrk-phase-g-%E2%80%94-argo-cd\">Phase G \u2014 Argo CD</h2>\n<p id=\"bkmrk-source%3A-infrastructu\">Source: infrastructure PR #1 (MERGED, main): <code>kubernetes/gitops/</code> + <code>docs/gitops.md</code>. Pinned Argo CD v3.4.8, non-HA, vendored manifest, namespace <code>argocd</code>. Run from ops01 with a clean checkout of infrastructure main at d553643 or a verified descendant (\u00a70 item 9 ops01 provisioning; every <code>kubectl</code> below is the ops01 remote form with <code>KUBECONFIG</code> set to the transferred admin kubeconfig). <strong><code>docs/gitops.md</code> (infrastructure repo) is the authoritative step-by-step procedure and has been reconciled to the deployed state \u2014 its steps 1-19 now match what was applied: HTTPS repository Secret, split/server-side CRD apply, current encryption status, drift deferral. The commands below are the same procedure restated; where wording differs, follow <code>docs/gitops.md</code>.</strong></p>\n<p id=\"bkmrk-install-%28docs%2Fgitops\">Install (docs/gitops.md steps 1-6); SSH to ops01 (the bastion itself, 92.222.245.91) is DIRECT \u2014 no ProxyCommand (reserve the ProxyCommand form for LAN targets behind ops01):</p>\n<pre id=\"bkmrk-ssh--i-%2Fopt%2Fdata%2F.ss\"><code>ssh -i /opt/data/.ssh/bigardo ubuntu@92.222.245.91\ncd ~/wetopi-infrastructure\ngit fetch origin &amp;&amp; git checkout main &amp;&amp; git pull --ff-only\nsha256sum kubernetes/gitops/bootstrap/argocd-install-v3.4.8/install.yaml   # optional integrity check\nkubectl apply -f kubernetes/gitops/bootstrap/argocd-install-v3.4.8/00-namespace.yaml\n# The pinned install.yaml is a 59-document manifest. The CRD\n# applicationsets.argoproj.io (1385776 bytes raw; ~341KB serialized) exceeds\n# kubectl's 262144-byte client-side last-applied-configuration annotation\n# cap, so a broad client-side apply of the whole manifest FAILS on that\n# object (\"metadata.annotations: Too long\"). The other large CRD,\n# applications.argoproj.io, serializes to ~191KB and stays under the cap \u2014\n# only applicationsets needs special handling. REQUIRED METHOD (validated\n# for both first install and re-apply; no CRD-presence branch, no extra\n# tools \u2014 plain awk): split the CRD out, apply it SERVER-SIDE (idempotent\n# whether the CRD is absent or already present), apply the remaining 58\n# objects client-side. Same form for installs, re-applies after upgrades,\n# and the Phase G rollback path.\nawk '\n  function flush() {\n    if (buf == \"\") { return }\n    if (want) { print buf &gt; \"/tmp/argocd-install-appsets-crd.yaml\" }\n    else {\n      if (n++) print \"---\" &gt; \"/tmp/argocd-install-rest.yaml\"\n      print buf &gt; \"/tmp/argocd-install-rest.yaml\"\n    }\n    buf = \"\"; want = 0; kind = 0\n  }\n  /^---$/ { flush(); next }\n  /^kind: CustomResourceDefinition$/ { kind = 1 }\n  /^  name: applicationsets\\.argoproj\\.io$/ { if (kind) want = 1 }\n  { buf = buf $0 \"\\n\" }\n  END { flush() }\n' kubernetes/gitops/bootstrap/argocd-install-v3.4.8/install.yaml\n# sanity check before applying (expected counts on the pinned manifest):\ngrep -c '^apiVersion:' /tmp/argocd-install-appsets-crd.yaml   # 1\ngrep -c '^apiVersion:' /tmp/argocd-install-rest.yaml           # 58\nkubectl apply --server-side -f /tmp/argocd-install-appsets-crd.yaml\nkubectl apply -n argocd -f /tmp/argocd-install-rest.yaml\nrm /tmp/argocd-install-appsets-crd.yaml /tmp/argocd-install-rest.yaml\nkubectl apply -f kubernetes/gitops/bootstrap/argocd-install-v3.4.8/01-cmd-params.yaml\n</code></pre>\n<p id=\"bkmrk-validation-%28steps-7-\">Validation (steps 7-12): <code>kubectl -n argocd get pods</code> (application-controller StatefulSet 1 replica by pin, applicationset-controller, dex (idle; SSO OPEN), redis, repo-server, server \u2014 all Running/Ready); port-forward <code>kubectl -n argocd port-forward svc/argocd-server 9090:443 --address=127.0.0.1</code>; install CLI <code>curl -sSLf -o /tmp/argocd https://github.com/argoproj/argo-cd/releases/download/v3.4.8/argocd-linux-amd64</code> + <code>chmod +x</code> + <code>sudo mv /usr/local/bin</code> + <code>argocd version --short</code> (expect v3.4.8 client+server); initial password <code>kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d; echo</code> (never commit); <code>argocd login 127.0.0.1:9090 --username admin --insecure</code>. UI from workstation via:\n<code>ssh -i /opt/data/.ssh/bigardo -L 8443:127.0.0.1:9090 ubuntu@92.222.245.91</code> (direct to the bastion, NO ProxyCommand \u2014 ops01 IS the bastion; the ProxyCommand form stays reserved for LAN targets behind ops01).</p>\n<p id=\"bkmrk-%5Bencryption-gate%5D-on\"><strong>[encryption-gate] on the repo credential (steps 13-17) \u2014 ORDERED GATE PROCEDURE (status-check BEFORE, ciphertext-verify AFTER, cleanup STOP on failure).</strong> The Argo CD repo credential is stored as a Kubernetes Secret, and \u2014 key semantic \u2014 <strong>creating the labeled repository Secret IS the registration</strong>: Argo CD auto-discovers any Secret carrying the label <code>argocd.argoproj.io/secret-type=repository</code>, so a create-without-registering sequence does not exist. The gate therefore does not \"hold registration until after the check\"; it verifies the at-rest protection of the Secret that already exists, per the <strong>measured-protection checks of \u00a70 item 8</strong> (the operative gate rule: current protection = measured evidence, not config-template contents). Execute in this exact order:</p>\n<ol id=\"bkmrk-check-%28i%29-first-%E2%80%94-be\">\n<li><strong>Check (i) FIRST \u2014 before creating the Secret</strong>: <code>rke2 secrets-encrypt status</code> on all three CPs, each showing <code>Encryption Status: Enabled</code> / <code>Current Rotation Stage: start</code> / <code>Server Encryption Hashes: All hashes match</code> / Active <code>* AES-CBC aescbckey</code> key (\u00a70 item 8 check (i)). Fails \u2192 STOP here, before any credential material enters the cluster; re-verify before continuing.</li>\n<li><strong>Create/update the labeled Secret</strong> <code>argocd/repo-infra-https</code> exactly per the \"Repo credential\" block below (protected <code>read -rs</code> token input; stdin-only <code>kubectl -n argocd apply -f -</code> heredoc; label <code>argocd.argoproj.io/secret-type=repository</code>; fields <code>type: git</code>, <code>url</code>, <code>username</code>, <code>password</code>). Creation auto-registers the credential (Argo CD label auto-discovery).</li>\n<li><strong>Immediately run check (ii) on the created Secret</strong>: raw-etcd ciphertext probe via the approved kubectl-exec etcdctl form (\u00a70 item 8) \u2014 <code>get /registry/secrets/argocd/repo-infra-https</code> must return <code>k8s:enc:aescbc</code> ciphertext, NOT the plaintext/base64 of the token.</li>\n<li><strong>Check (ii) fails or shows plaintext \u2192 CLEANUP STOP</strong>: <code>kubectl -n argocd delete secret repo-infra-https</code> (this also removes the auto-registered credential), <code>unset ARGOCD_REPO_TOKEN</code>, then STOP \u2014 do not proceed to the root Application \u2014 and <strong>revoke/rotate the token on GitHub as appropriate</strong> (a credential that sat readable in etcd must be treated as exposed). Do not re-attempt from step 2 until the at-rest encryption fault is found, fixed, and re-measured (checks (i) and (ii) green again).</li>\n<li><strong>Only after check (ii) passes</strong>: the GATE of step 17 \u2014 <code>argocd repo list</code> shows <code>https://github.com/wetopi-platform/infrastructure.git</code> CONNECTION STATUS <code>Successful</code> \u2014 and only then proceed to the root Application (steps 18-19).</li>\n</ol>\n<p id=\"bkmrk-this-gate-does-not-b\">This gate does NOT block the Argo CD install itself (steps 1-12 above), only the private-repo credential.\n<strong>APPLY-TIME VERDICT (2026-08-30, card t_e0cace4b)</strong>: gate PASSED on measured evidence, in exactly the ordered sequence above \u2014 check (i) verified on all 3 CPs BEFORE the credential Secret existed; the labeled Secret <code>argocd/repo-infra-https</code> was then created, which auto-registered the credential via Argo CD's label auto-discovery; raw-etcd check (ii) immediately probed the created Secret and returned <code>k8s:enc:aescbc:v1:aescbckey:</code> ciphertext (685B total, NOT plaintext; <code>argocd/argocd-initial-admin-secret</code> also probed: 316B ciphertext); only after that verification did execution proceed to the repo-list gate (<code>argocd repo list</code> \u2192 Successful) and the root Application. The cleanup STOP path was not needed.</p>\n<p id=\"bkmrk-repo-credential-%28ste\">Repo credential (steps 13-17; private repo \u2192 mandatory before first sync). <strong>APPLY-TIME DEVIATION (lead decision, card t_e0cace4b) \u2014 the deploy-key path is SUPERSEDED</strong>: GitHub org policy DISABLES deploy keys on wetopi-platform/infrastructure (API returns HTTP 422 \"Deploy keys are disabled for this repository\", org-level setting), so the deployed mechanism is an <strong>HTTPS read credential</strong> \u2014 the token-based repository Secret below, auto-discovered by Argo CD. The exact step-by-step procedure (steps 13-16, secret-safe form) lives in <code>docs/gitops.md</code> in the infrastructure repo, \"Private repository credential (apply-phase, before first sync)\" \u2014 read token from GitHub into a shell variable via <code>read -rs</code> (protected input, never typed into a command or history); create/update Secret <code>argocd/repo-infra-https</code> in the <code>argocd</code> namespace by <code>kubectl -n argocd apply -f -</code> heredoc referencing <code>${ARGOCD_REPO_TOKEN}</code> (the manifest exists only on stdin \u2014 never in Git, never a file on disk) with the required label <code>argocd.argoproj.io/secret-type=repository</code> and required stringData fields <code>type: git</code>, <code>url: https://github.com/wetopi-platform/infrastructure.git</code>, <code>username: box-daxter</code>, <code>password: &lt;the variable&gt;</code>; <code>unset ARGOCD_REPO_TOKEN</code> after; rotation = re-run the same block, cleanup = <code>kubectl -n argocd delete secret repo-infra-https</code> + revoke the token on GitHub. Do NOT instead use inline <code>argocd repo add --password ...</code> \u2014 an inline add was tried at apply time and registered an empty credential (STATUS Failed); the labeled Secret is the only registration. When Infisical lands, this credential migrates to the vault contract.</p>\n<p id=\"bkmrk-gate-%28step-17%2C-manda\"><strong>GATE (step 17, mandatory)</strong>: <code>argocd repo list</code> shows <code>https://github.com/wetopi-platform/infrastructure.git</code> CONNECTION STATUS <code>Successful</code>. Do not proceed to the root Application until this passes. <strong>PASSED at apply time (t_e0cace4b)</strong> via the Secret auto-discovery above (one recorded correction: an initial inline-password <code>argocd repo add</code> registered an empty credential, STATUS Failed \u2014 entry removed; the labeled Secret is now the only registration).</p>\n<p id=\"bkmrk-first-app-sync-%28step\">First app sync (steps 18-19):</p>\n<pre id=\"bkmrk-kubectl-apply--f-kub\"><code>kubectl apply -f kubernetes/gitops/app.project.yaml -f kubernetes/gitops/app.root.yaml\nargocd app list       # one row: root, project=platform, in-cluster, Synced/Healthy\nargocd app get root   # STATUS Synced, HEALTH Healthy, source infrastructure main @ kubernetes/platform\nargocd app sync root  # no-op sync (target path holds placeholders only)\nargocd app wait root --health --sync --timeout 120\nkubectl get ns platform    # created by the sync via CreateNamespace=true\n</code></pre>\n<p id=\"bkmrk-drift-check-%28documen\">Drift check (documented expectations): Git is desired state; manual kubectl edit/apply/delete inside the cluster is drift and shows <code>OutOfSync</code>; <code>root</code> runs <code>automated.prune=true, selfHeal=true</code> so drift self-heals within the reconcile interval and every correction lands in <code>argocd app history root</code>. Drift procedure to record: make a manual change in the platform namespace, confirm <code>argocd app get root</code> shows OutOfSync then self-heals back, confirm the correction in <code>argocd app history root</code>; preferred rollback is always a Git revert (Argo reconciles it) \u2014 <code>argocd app rollback &lt;id&gt;</code> exists for emergencies but must be followed by a Git fix or selfHeal re-applies Git state. Note the Argo CD control plane itself is apply-only from pinned manifests (procedural exclusion, re-apply <code>install.yaml</code> after reviewed upgrades).\n<strong>APPLY-TIME DEVIATION \u2014 drift check DEFERRED (card t_e0cace4b)</strong>: structurally untestable at bootstrap. <code>kubernetes/platform/</code> holds a README placeholder only (0 tracked resources), so the Application has no desired state to diverge from: a probe ConfigMap created manually in the <code>platform</code> ns kept <code>Synced/Healthy</code> (Argo cannot flag an object outside its tree; OutOfSync/selfHeal/prune have nothing to act on). Probe deleted; cluster left clean. The <code>automated.prune=true, selfHeal=true</code> manifest evidence IS factual (verified in the app JSON spec). The check is <strong>trivially testable once \u22651 real workload exists under <code>kubernetes/platform/</code></strong> (e.g. the first app-of-apps child of a later phase) \u2014 re-run the drift procedure above then, and test drift by EDITING or DELETING one of the Git-managed objects in the cluster (never by creating another unmanaged object outside the Application's tree, which is invisible to Argo by design).</p>\n<p id=\"bkmrk-apply-time-deviation\"><strong>Apply-time deviations (2026-08-30, card t_e0cace4b \u2014 reconciled into this runbook and the repo):</strong></p>\n<ol id=\"bkmrk-server-side-crd-appl\">\n<li><strong>Server-side CRD apply (required method)</strong>: <code>applicationsets.argoproj.io</code> CRD (1385776 bytes raw; ~341KB serialized) exceeds kubectl client-side apply's 262144-byte <code>last-applied-configuration</code> annotation cap \u2014 a broad client-side apply of the whole install manifest fails with <code>metadata.annotations: Too long</code>. <strong>Required apply method: awk-split that single CRD out of the pinned manifest and <code>kubectl apply --server-side -f</code> it</strong> (recorded <code>serverside-applied</code>; idempotent on first install and re-apply \u2014 no CRD-presence branch needed); the remaining 58 objects stay client-side apply per this runbook. Any re-apply of the pinned manifest must use the same split form.</li>\n<li><strong>HTTPS credential instead of SSH deploy key</strong> (lead decision): both <code>kubernetes/gitops/app.project.yaml</code> and <code>app.root.yaml</code> were applied with <code>repoURL</code>/<code>sourceRepos</code> <code>git@github.com:wetopi-platform/infrastructure.git</code> \u2192 <code>https://github.com/wetopi-platform/infrastructure.git</code> (deploy keys disabled at org level); credential = Secret <code>argocd/repo-infra-https</code> (see \"Repo credential\" above). The infrastructure repo is reconciled to the applied state by PR (infrastructure) \u2014 repoURL lines updated to HTTPS; the app yamls now match what is deployed.</li>\n<li><strong>Drift self-heal check deferred</strong> \u2014 structural reason and re-test condition as recorded above.</li>\n</ol>\n<p id=\"bkmrk-execution-evidence-%28\"><strong>Execution evidence (t_e0cace4b, 2026-08-30)</strong>: Argo CD v3.4.8 installed (ops01 checkout at main f841ae7, PIN SHA-256 e223122b\u2026 matched docs/gitops.md); 7/7 pods Running/Ready; CLI v3.4.8 client+server match; encryption gate PASSED on measured AES-CBC evidence in the ordered sequence (check (i) pre-credential, check (ii) ciphertext on the created <code>repo-infra-https</code> Secret, cleanup path not needed); repo credential Successful; <code>argocd app list</code> \u2192 one row <code>root</code>, project <code>platform</code>, in-cluster, <code>https://github.com/wetopi-platform/infrastructure.git</code> @ <code>kubernetes/platform</code> @ main, SYNCPOLICY Auto-Prune; first sync Succeeded (no-op, placeholders only), root Synced to main (b610209) Healthy; ns <code>platform</code> Active; history ID 0.</p>\n<p id=\"bkmrk-stop%3A-pods-not-all-r\"><strong>STOP</strong>: pods not all Running; [encryption-gate] measured-protection checks failing per the ordered procedure (check (i) failing at step 1, or check (ii) failing/showing plaintext at step 3 \u2192 cleanup STOP: delete <code>argocd/repo-infra-https</code>, <code>unset ARGOCD_REPO_TOKEN</code>, revoke/rotate the token, stop \u2014 \u00a70 item 8 and the ordered gate procedure above); <code>argocd repo list</code> not Successful; root app not Synced+Healthy after <code>argocd app wait</code>. (Drift-check STOP superseded by the deferral above: no drift gate applies while <code>kubernetes/platform/</code> tracks 0 resources \u2014 re-test when the first workload lands.)</p>\n<h2 id=\"bkmrk-rollback-story-%28gene\">Rollback story (general, per phase)</h2>\n<p id=\"bkmrk-on-any-phase-failure\">On ANY phase failure \u2014 stop, do not proceed; read the playbook/task failure output first (all plays run <code>any_errors_fatal</code>); the run is serial, so a failed phase leaves the cluster in a known partial state. Order of escalation: (1) fix forward and re-run the phase playbooks (roles are idempotent, serial-safe); (2) per-node rollback (below); (3) etcd snapshot restore via the <strong>cluster-reset flow</strong> (<code>systemctl stop rke2-server</code> on all CPs \u2192 <code>rke2 server --cluster-reset --cluster-reset-restore-path=&lt;PATH-TO-SNAPSHOT&gt;</code> on cp01 \u2192 start cp01 \u2192 <code>rm -rf /var/lib/rancher/rke2/server/db/</code> + start on cp02/cp03 \u2192 re-verify quorum + <code>/readyz</code> \u2014 see Phase F6-3, INCLUDING its reset-failure rule: a failed reset is an immediate STOP-and-escalate condition, NOT a retry \u2014 <code>reset-flag</code> blocks repeated resets; there is NO <code>rke2 etcd-snapshot restore</code> subcommand \u2014 and the F6 STOP prerequisites P1-P4 gate ANY cluster-reset restore, including from this rollback path) for cluster-state corruption; (4) Proxmox <code>qm rollback &lt;vmid&gt; pre-rke2-apply</code> (last resort \u2014 cluster-level state like etcd membership makes node-level rollback of ONE CP risky once quorum exists: prefer etcd member removal for a node you are rebuilding rather than snapshot rollback of a single CP mid-quorum; snapshot rollback is safest applied to ALL CPs together or before Phase B).</p>\n<p id=\"bkmrk-per-phase-specifics-\">Per-phase specifics (from parent docs; the per-phase STOP blocks above remain the authoritative inline version):</p>\n<ul id=\"bkmrk-b%2Fc-%28from-the-cp-run\">\n<li><strong>B/C</strong> (from the CP runbook): stop RKE2 <code>systemctl stop rke2-server</code>; per node serially remove <code>/etc/rancher/rke2</code>, <code>/var/lib/rancher/rke2</code>, <code>/var/lib/kubelet</code>; keepalived may stay (role idempotent) or be removed; re-run the playbook from scratch after regenerating the token; do NOT manually delete etcd members unless removing a node permanently \u2014 then remove the member with the same kubectl-exec etcdctl form (Phase B gate) with <code>member remove &lt;ID&gt;</code>. Rebuild-a-node criterion: node whose config/state is unrecoverable (repeated join failures, corrupt etcd data dir) \u2014 snapshot-rollback or recreate via tofu, then re-run the CP join play.</li>\n<li><strong>D</strong> (from the worker runbook): per node serially <code>systemctl stop rke2-agent</code>, remove <code>/etc/rancher/rke2</code>, <code>/var/lib/rancher/rke2</code>, <code>/var/lib/kubelet</code>, then <code>kubectl delete node rke-platform0X</code>; re-run the workers playbook after fixing the root cause (idempotent).</li>\n<li><strong>E</strong>: <code>helm uninstall cilium --namespace kube-system</code> (documented; no other CNI behind it \u2014 replacement plan required).</li>\n<li><strong>W</strong>: no install state to roll back \u2014 if interrupted mid-drill, uncordon (CP-local form) and re-verify the node Ready; a worker that fails W-2 recovery follows the <strong>D</strong> bullet (agent wipe + node delete + re-join).</li>\n<li><strong>F</strong>: F0 \u2014 revert the tfvars PR (reviewed); routing \u2014 revert the natgw vars PR to default-off + re-run <code>bootstrap.yml</code>; CSI inverse (uninstall chart, delete StorageClass) as authored above.</li>\n<li><strong>G</strong>: GitOps plane state is rebuildable from Git \u2014 re-apply the pinned manifests using the SAME split form as the install (awk-split the <code>applicationsets.argoproj.io</code> CRD out, <code>kubectl apply --server-side -f</code> the CRD file, <code>kubectl apply -n argocd -f</code> the remaining-objects file; a broad client-side apply of the whole <code>install.yaml</code> fails on that CRD by design), re-register the repo credential (the <code>argocd/repo-infra-https</code> labeled Secret per <code>docs/gitops.md</code> steps 13-16), re-sync; app-level rollback = Git revert (<code>argocd app history root</code> records syncs). Full cluster loss = rebuild from the tofu/ansible phases, then this runbook top to bottom.</li>\n</ul>\n<h2 id=\"bkmrk-definition-of-done-%E2%80%94\">Definition of done \u2014 master plan \u00a735.1/35.2 mapped to concrete commands</h2>\n<table id=\"bkmrk-plan-item-concrete-c\">\n<thead>\n<tr>\n<th>Plan item</th>\n<th>Concrete check in this runbook</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>\u00a735.1 p\u00e9rdida de un CP no rompe API</td>\n<td>Phase C one-CP-outage test + Phase F6-2 (<code>systemctl stop rke2-server</code> on cp02, <code>curl -k https://10.72.0.20:6443/readyz</code> \u2192 ok)</td>\n</tr>\n<tr>\n<td>\u00a735.1 etcd quorum correcto</td>\n<td>kubectl-exec etcdctl member list == 3 members (Phase B/C gates + F6-1)</td>\n</tr>\n<tr>\n<td>\u00a735.1 nodes reproducibles desde IaC</td>\n<td>phases B-E run purely from tofu-generated inventory + ansible roles (re-run of the same playbooks reproduces the state; snapshot rollback to pre-rke2 then re-run = full reproduction test)</td>\n</tr>\n<tr>\n<td>\u00a735.1 no customer workloads en CP</td>\n<td>workers playbook play 2 taint enforcement (<code>node-role.kubernetes.io/control-plane=true:NoSchedule</code> on all CPs) + Phase C taint record + <code>get pods -o wide</code> scan + D2 scheduling gate placement</td>\n</tr>\n<tr>\n<td>\u00a735.1 snapshot funciona / restore probado</td>\n<td>F6 STOP prerequisite P2 (<code>rke2 etcd-snapshot save --name pre-addons-fase6</code> + <code>rke2 etcd-snapshot ls</code>, external copy outside cp01's failure domain) + P3 (SHA-256 match on cp01 and the external copy), then F6-3: the <strong>EXECUTED cluster-reset restore test</strong> (<code>systemctl stop rke2-server</code> all CPs \u2192 <code>rke2 server --cluster-reset --cluster-reset-restore-path=&lt;PATH-TO-SNAPSHOT&gt;</code> on cp01 \u2192 start cp01 \u2192 <code>rm -rf /var/lib/rancher/rke2/server/db/</code> + start on cp02/cp03 \u2192 re-verify quorum 3/3 + <code>/readyz</code> + the post-restore journal checks on all three CPs). DoD maps \"restore probado\" to THIS executed-test evidence \u2014 snapshot-save-only is NOT restore probado. If the restore test cannot be executed, this DoD item is UNMET and the runbook mandates STOP before all subsequent phases (E onward).</td>\n</tr>\n<tr>\n<td>\u00a735.2 Pod-to-Pod + Pod-to-Service</td>\n<td><code>cilium connectivity test</code> (Phase D2, final 6-node topology)</td>\n</tr>\n<tr>\n<td>\u00a735.2 DNS</td>\n<td>Phase D2 DNS gate (answer parsed into 10.68.0.0/16) + Phase D2 <code>dns-utils</code> nslookup (final topology)</td>\n</tr>\n<tr>\n<td>\u00a735.2 Hubble visibility</td>\n<td><code>hubble status</code> + <code>hubble observe</code> (Phase D2, final 6-node topology \u2014 real flows + VERDICTs)</td>\n</tr>\n<tr>\n<td>\u00a735.2 no CIDR collision</td>\n<td>gate (f) <code>ip -4 route</code> review + <code>kubectl get ciliumnodes.cilium.io</code> podCIDR \u2282 10.64.0.0/14 (Phase D2)</td>\n</tr>\n<tr>\n<td>\u00a735.2 Ceph reachable desde nodes</td>\n<td>Phase F1 connectivity block (ping/traceroute/nc 3300+6789) + PVC Bound</td>\n</tr>\n<tr>\n<td>\u00a735.2 DSR demostrado por packet capture</td>\n<td><strong>NOT covered by phases 0-G</strong> (edge/DSR is later work per the master plan); recorded as an explicit gap</td>\n</tr>\n<tr>\n<td>\u00a735.3 Storage: PVC create/persistence/pod recreation/failure test</td>\n<td>Phase F2 steps (a)-(d)</td>\n</tr>\n<tr>\n<td>\u00a735.3 Storage: performance</td>\n<td>Phase F2 step (e)</td>\n</tr>\n<tr>\n<td>\u00a735.4 GitOps: Argo reconcilia desde Git</td>\n<td>first sync block</td>\n</tr>\n<tr>\n<td>\u00a735.4 GitOps: drift detectable</td>\n<td>drift procedure</td>\n</tr>\n<tr>\n<td>\u00a735.4 GitOps: Git conserva estado deseado</td>\n<td>selfHeal + Git-revert rollback rule</td>\n</tr>\n<tr>\n<td>\u00a735.4 GitOps: rollback probado</td>\n<td>Git revert is exercised only in a real incident \u2014 recorded as \"procedure documented, not yet exercised\"</td>\n</tr>\n<tr>\n<td>\u00a735.1/\u00a735.2 node drain + reboot recovery</td>\n<td><strong>Phase W</strong> (W-1 drain, W-2 reboot + Ready return; former F6-4/F6-5) \u2014 the separately-named post-D2 phase, executed on the final 6-node topology in the executable order <strong>B \u2192 C \u2192 STOP \u2192 F6-1..3 \u2192 E \u2192 D \u2192 D2 \u2192 W \u2192 F \u2192 G</strong>. The DoD maps drain/reboot to this POST-D2 evidence, NOT to pre-addon acceptance: \u00a733 places these Fase-6 items before Fase 7 (Cilium), but ADR-0005 requires Cilium as the immediate first addon with no pre-Cilium worker-Ready gates \u2014 with <code>cni: none</code> the drain/reboot gates are unexecutable before Cilium exists (see the banner for the \u00a733/ADR-0005 tension and its explicit resolution). F6-1..3 (quorum, CP failure, snapshot + REAL restore test) remain the PRE-addon CP-only evidence.</td>\n</tr>\n</tbody>\n</table>\n<p id=\"bkmrk-runbook-complete-whe\"><strong>Runbook complete when every phase gate above has a dated PASS entry in the apply log.</strong></p>\n<h2 id=\"bkmrk-provenance\">Provenance</h2>\n<ul id=\"bkmrk-execution-status-%2820\">\n<li><strong>Execution status (2026-08-30, post-Phase-D)</strong>: the live apply has RESUMED and executed B, C, F6-1..3, E, and D fully green (details in the status banner; cluster 6/6 nodes Ready). <strong>Historical note (superseded):</strong> the 2026-08-29 23:17-23:26 UTC attempt (task t_a88bb36b, wetopi-ops) was <strong>HALTED BEFORE PHASE B and FROZEN</strong> (operator directive #38) after failing at <code>rke2-server : Create the RKE2 server configuration</code> with <strong><code>Destination directory /etc/rancher/rke2 does not exist</code></strong> on rke-cp01 \u2014 neither rke2 role created the config dir (byte-identical defect in rke2-agent). Infrastructure <strong>PR #6</strong> (<code>fix/rke2-config-dir-create</code>, head 3ff2ee8, merged main d553643, 2026-08-29 23:56 UTC) is the fix: config-dir creation (root:root 0750) before templating in both roles. Residual pre-halt state on cp01 (keepalived installed+active, RKE2 binaries present, <code>/etc/rancher</code> absent, rke2-server inactive) was absorbed cleanly by the resumed run \u2014 the roles are idempotent (RKE2 install guarded by <code>creates:</code>). The former resume gates (infrastructure checkout at d553643 or verified descendant per the \u00a70 STEP 0 gate, harness V2 validation, explicit lead unblock) were all satisfied on 2026-08-30 before resume; they remain historical prerequisites of that resume, not open conditions.</li>\n<li>Verified against merged heads on 2026-08-30 (~00:15Z): infrastructure <code>main</code> @ <strong>d553643</strong> (PR #6 merge \u2014 config-dir fix for both RKE2 roles; prior PRs #1 8f3e0e8, #2 e283e24, #3 76782dc, #4 b15a087, #5 8d836e8 all merged \u2014 no open-PR references remain), docs <code>main</code> @ 5740404 (ADR-0005 and platform runbooks in main).</li>\n<li><strong>Executable order (identical in the banner, the F6 heading + STOP rule, the Phase W heading, the D2 handoff, the DoD mapping, and here): B \u2192 C \u2192 STOP \u2192 F6-1..3 \u2192 E \u2192 D \u2192 D2 \u2192 W \u2192 F \u2192 G</strong> (STOP = the mandatory post-Phase-C pause before any F6-2/F6-3 server stop, gated by the F6 STOP prerequisites \u2014 historically the frozen-apply boundary of the superseded 2026-08-29 halt; W = Phase W, the separately-named worker lifecycle drill \u2014 former F6-4/F6-5). Master plan \u00a733 orders Fase 5 (workers) and Fase 6 (incl. node drain/reboot) before Fase 7 (Cilium); ADR-0005 requires Cilium as the immediate first addon with NO pre-Cilium worker-Ready/DNS gates. <strong>Tension and resolution</strong>: with <code>cni: none</code>, workers cannot pass Ready (and therefore drain/reboot) gates before Cilium exists; the resolution adopted by this runbook is that Fase-6's CP-only gates (F6-1..3) run before the first addon per \u00a733, and the worker-lifecycle drill is the separately-named Phase W landing after D2 on the final 6-node topology; the DoD maps drain/reboot to that post-D2 evidence, never to pre-addon acceptance. Phase W is not counted as completed pre-addon acceptance.</li>\n<li>Playbook/role facts re-read on final main: <code>ansible/playbooks/{rke2-cp,rke2-workers,rke2-workers-validate,cilium,ceph-csi,bootstrap}.yml</code>, <code>ansible/roles/{keepalived,rke2-server,rke2-agent,nat_gateway}/</code>, <code>kubernetes/{cilium,ceph-csi,gitops}/</code>, <code>docs/gitops.md</code>, <code>scripts/{gen-rke2-token.sh,render-ansible-inventory.sh}</code>, <code>tofu/proxmox/environments/prod/terraform.tfvars.example</code>.</li>\n<li><strong>Execution environment (\u00a70 item 9)</strong>: the controller is the workstation (direct evidence from the t_a88bb36b apply: ops01 had no Ansible venv/repository; the playbooks ran from the workstation over the bastion); ops01 requires explicit provisioning (checkout, venv, inventory, helm/kubectl/cilium/hubble CLIs, and a transferred admin kubeconfig with the server endpoint rewritten to the VIP) before Phase E \u2014 needed because <code>cilium.yml</code> runs its Helm op on the <code>operations</code> host with <code>connection: local</code>. CP-local kubectl invocations use the RKE2-bundled binary path <code>/var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml</code>, matching the playbooks' own <code>rke2_kubectl</code> form.</li>\n<li>secrets-encryption: <strong>MEASURED PROTECTED, per the single operative gate rule (\u00a70 item 8)</strong> \u2014 current protection is determined by measured evidence (default-on AES-CBC on this RKE2 build: <code>secrets-encrypt status</code> Enabled on all 3 CPs; raw etcd probes of both argocd Secrets returned <code>k8s:enc:aescbc</code> ciphertext), NOT by config-template contents. The explicit <code>secrets-encryption: true</code> role-template line is still absent from merged main (<code>git grep secrets-encryption</code> \u2192 empty) \u2014 it is the separately tracked future hardening/rotation prerequisite (must merge via its own reviewed PR before any key rotation), not an unlock condition for F2/G while the measured checks pass. F2's csi-rbd-secret gate (when Phase F executes) re-verifies the same measured checks on that Secret.</li>\n<li>Design: <code>docs/design/master-architecture-plan.md</code> \u00a733/\u00a735 (Fase 6 = acceptance; CP-only portion BEFORE Fase 7 Cilium; worker-lifecycle portion after D2 per ADR-0005 \u2014 see the executable-order bullet above); <code>docs/decisions/ADR-0004</code>, <code>docs/decisions/ADR-0005</code>.</li>\n<li>Restore procedure and reset-flag semantics: RKE2 docs, rancher/rke2-docs <code>docs/datastore/backup_restore.md</code> (etcd-snapshot subcommands = delete/ls/prune/save ONLY; restore = <code>rke2 server --cluster-reset --cluster-reset-restore-path=&lt;PATH&gt;</code> multi-server flow; reset creates <code>/var/lib/rancher/rke2/server/db/reset-flag</code> preventing repeated resets in succession, deleted only by a normal start \u2014 verified 2026-08-30 against the <code>main</code> branch of rancher/rke2-docs). Flag lifecycle additionally verified 2026-08-30 against the k3s source vendored by the pinned <code>rke2_version: v1.36.3+rke2r1</code> (k3s commit 4ca29faac89e via rke2 go.mod; <code>pkg/cluster/managed.go</code>: the flag check is bypassed when <code>--cluster-reset-restore-path</code> is set and the flag blocks only a bare reset; <code>pkg/etcd/etcd.go</code> Reset(): the flag is written only after the snapshot-restore step succeeds).</li>\n<li>Secrets-encryption behavior: RKE2 docs, rancher/rke2-docs <code>docs/security/secrets_encryption.md</code> (auto-generated AES-CBC key + <code>/var/lib/rancher/rke2/server/cred/encryption-config.json</code>; <code>rke2 secrets-encrypt status</code> output shape: Encryption Status / Current Rotation Stage / Server Encryption Hashes / key table).</li>\n</ul>\n"
 },
 {
  "id": 61,
  "title": "taking over the platform",
  "slug": "taking-over-the-platform",
  "book_id": 1,
  "chapter_id": null,
  "description": null,
  "created_at": "2026-09-06T13:39:04.000000Z",
  "updated_at": "2026-09-06T13:39:04.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (project/taking-over-the-platform.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n PROC-PLATFORM-TAKEOVER: Taking over the platform\n\nThe platform map is\n[ARCH-PLATFORM-TOPOLOGY](../architecture/overview-topology.md) \u2014 read it\nfirst. This guide is the *procedure*: access, truth sources, reading order,\nday-1 checks, the operations you will run most often, where to escalate, and\nwhat is never allowed. It links out for everything it does not need to\nduplicate.\n\n## Purpose and when to use\n\nUse this when you are newly taking over operations of the platform \u2014 on a\nshift, a rotation, or an emergency handover. It is deliberately a composed\nguide of steps (the one allowed exception to the one-concept-per-pill rule);\nevery fact it does not restate lives in a linked pill or runbook.\n\n## (a) Access \u2014 who has which credential, by location\n\nNever search for a credential's *value* in this repo, the infrastructure\nrepo, a ticket or a chat \u2014 only its **location** is documented (DocOps \u00a78).\n\n| Access | Who | Credential location (never the value) | Notes |\n|---|---|---|---|\n| SSH to the fleet | operator holding the `bigardo` key | the workstation's `~/.ssh/bigardo` (key pair issued out-of-band; the public side is pushed to VMs by the `ssh_keys` role) | ops01 (`ubuntu@92.222.245.91`) is the bastion; all LAN VMs are reached *through* it. ops01 itself holds **no private keys** \u2014 it cannot SSH to the LAN by itself. |\n| Vault (human UI) | the taking-over operator | native Infisical account (username + password + TOTP) \u2014 the account is provisioned by the current operator at handover time; no value is documented | `https://vault.wetopi.net` \u2192 project `wetopi-harness`. The UI's own login+TOTP is the gate (no basic-auth). |\n| Vault (machine identities / Universal Auth) | agent profiles / automation only | client credentials live under project `wetopi-harness` in the Vault itself | **private-network-only**: `http://vault.vault.svc.cluster.local` \u2014 never the public route, which fail-closes by design (ADR-0009). |\n| `kubectl` on the cluster | operator via ops01 | kubeconfig fetched at apply-time from `rke-cp01` (the `ops_kubectl` role; nothing persisted in Git) | see [ARCH-PLATFORM-RKE2](../architecture/platform/rke2-cluster.md) for the API VIP. |\n| OpenTofu (PROD) | single writer, by explicit task | `.env.proxmox` on the phase-1 controller (0600); Cloudflare token on ops01 at `/home/ubuntu/secrets/.cloudflare-wetopi-net.env` (0600) | pulled over SSH per session, never written to disk in the repo. State: `terraform.tfstate` 0600, **never in Git** ([ARCH-INFRA-PROVIDERS](../architecture/infrastructure/providers.md)). |\n| Proxmox API | the same `.env.proxmox` | Proxmox VE HTTPS API token (env var) | no SSH to hypervisors \u2014 API only. |\n| Cloudflare DNS | the Cloudflare token above | Zone DNS Edit + Zone Read (both required) | records are tofu-managed \u2014 do not edit in the dashboard. |\n\nHandover checklist for credentials: at the handover meeting the outgoing\noperator must (1) verify you can reach ops01 and one LAN node with your\n`bigardo` key, (2) hand you a working Vault human account (or provision one\nyou can then rotate), (3) confirm the single-writer tofu lock is with you.\nIf any of these fail, stop and escalate \u2014 do not improvise credentials.\n\n## (b) Sources of truth\n\nAuthority order (DocOps \u00a71) \u2014 when sources disagree, the live one wins:\n\n1. **Live state** \u2014 `kubectl` (via ops01), Proxmox API, Cloudflare API,\n   Ceph. \"What IS happening.\"\n2. **Desired state** \u2014 the\n   [`infrastructure`](https://github.com/wetopi-platform/infrastructure)\n   repo (IaC: `tofu/`, `ansible/`, `kubernetes/`). A merge is a deploy.\n3. **Curated docs** \u2014 this repo (`docs/`). The map:\n   [ARCH-PLATFORM-TOPOLOGY](../architecture/overview-topology.md).\n4. **Kanban / work ledger** \u2014 execution state of tasks (who is working\n   what, which card a change belongs to). Live board, not documentation.\n5. **tfstate** \u2014 `terraform.tfstate` (0600, phase-1 controller) is the\n   OpenTofu claim on PROD. Lost state \u2192\n   [RUN-TOFU-STATE-LOSS](../operations/runbooks/tofu-prod-state-loss-and-reimport.md),\n   never a speculative `apply`.\n\n## (c) What to read, in order\n\n1. **[ARCH-PLATFORM-TOPOLOGY](../architecture/overview-topology.md)** \u2014 the\n   three-layer map; every node links to its pill.\n2. **Decisions** \u2014 [ADR index](../decisions/index.md); read ADR-0003 (tofu vs\n   Ansible split), ADR-0004 (API endpoint), ADR-0005 (Cilium first),\n   ADR-0006 (edge v1), ADR-0007 (Ceph storage-native), ADR-0009 (vault).\n3. **Layer pills** \u2014 [infrastructure section](../architecture/infrastructure/index.md)\n   (OVH/network \u2192 Proxmox \u2192 Ceph \u2192 providers), then\n   [platform section](../architecture/platform/index.md) (RKE2 \u2192 Cilium \u2192\n   edge \u2192 GitOps).\n4. **Service fiches** \u2014 [services section](../services/index.md): one per\n   deployed service with deploy path, dependencies and honest live state.\n5. **Runbooks** \u2014 [runbooks index](../operations/runbooks/index.md); the two\n   load-bearing ones:\n   [RKE2 apply & restore](../platform/rke2-apply-runbook.md) (cluster\n   bring-up/DR) and [RUN-TOFU-STATE-LOSS](../operations/runbooks/tofu-prod-state-loss-and-reimport.md)\n   (IaC state recovery).\n\n## (d) Day-1 operations \u2014 verify the platform is healthy\n\nAll from ops01 (kubeconfig in `~/.kube/config` or fetched per the\n`ops_kubectl` model \u2014 never copied into Git):\n\n1. **Cluster nodes**: `kubectl get nodes -o wide` \u2014 expect 8/8 `Ready`\n   (cp01/02/03, platform01-03, wan01/02; IPs per\n   [ARCH-PLATFORM-RKE2](../architecture/platform/rke2-cluster.md)).\n2. **GitOps plane**: `kubectl -n argocd get applications` \u2014 expect\n   `root`, `cert-manager`, `docs-site`, `argo-edge` Healthy+Synced; `vault`\n   Synced \u2014 its `Degraded` health is the documented **false positive** of\n   the fail-closed deny rules (verify pods 3/3 Running in `vault` ns and\n   check [SVC-VAULT](../services/vault.md) \"estado honesto\" before\n   treating it as a fault).\n3. **Edge**: `kubectl -n kube-system get gateway,httproute` \u2014 Gateway\n   `wetopi-edge` Accepted+Programmed, listeners 8000/8443;\n   `kubectl get certificate -A` \u2014 `argo-tls`, `docs-tls`, `vault-tls`\n   (and `edge-test-tls`) Ready. Public probe: `curl -sI\n   https://docs.wetopi.net` and `https://argo.wetopi.net` return 200/302\n   with a valid cert chain.\n4. **DNS**: the Cloudflare records for `docs/argo/vault/edge-test\n   .wetopi.net` \u2192 `79.137.114.193/.194` (grey-cloud, TTL 300) \u2014 tofu-owned;\n   verify with `dig` + the zone, don't trust the dashboard screenshot.\n5. **IaC sanity**: a `tofu plan` on PROD should be **no-change**; a plan\n   wanting to CREATE everything means lost state \u2192 the state-loss runbook.\n6. **Board**: scan the kanban board for in-flight/`blocked` cards \u2014\n   understand what is mid-flight before touching anything.\n\n## (e) Frequent operations\n\n- **DNS record change** \u2014 never the Cloudflare dashboard: change the\n  `tofu/proxmox/environments/prod/` tfvars (0600) \u2192 PR in the\n  `infrastructure` repo \u2192 merge is the deploy. The provider needs the\n  Cloudflare token env (ops01, 0600). Records are\n  `prevent_destroy`-annotated \u2014 deleting one is a recorded decision.\n- **Deploy / change a service** \u2014 GitOps only: edit `kubernetes/<app>/` in\n  the `infrastructure` repo \u2192 PR \u2192 review (blast-radius: a merge deploys)\n  \u2192 merge; Argo CD syncs (`prune + selfHeal`). No `kubectl apply` of\n  manifests from a workstation; no CI `kubectl apply` pipeline exists by\n  design ([ARCH-PLATFORM-GITOPS](../architecture/platform/gitops.md)).\n- **Secret rotation** \u2014 via the Vault human UI (`vault.wetopi.net`,\n  project `wetopi-harness`): rotate in Infisical, let the\n  [Infisical Operator](../services/vault.md) sync to the cluster; SOPS+age\n  is bootstrap/recovery only, not the daily vault (ADR-0009). The vault\n  datastore backup/restore contract (pg_dump + age, rehearsed) is in the\n  ADR AC8 \u2014 rehearse before calling it production.\n- **Read-only inspection** \u2014 any `kubectl get/describe/logs` via ops01.\n  Read-only cluster reads are always allowed; anything mutating is a card\n  with a gate.\n\n## (f) Escalation \u2014 existing runbooks\n\n| Situation | Runbook / owner |\n|---|---|\n| Cluster node down, CP quorum loss, DR | [RKE2 apply & restore runbook](../platform/rke2-apply-runbook.md) |\n| `terraform.tfstate` lost / empty plan wants to create everything | [RUN-TOFU-STATE-LOSS](../operations/runbooks/tofu-prod-state-loss-and-reimport.md) |\n| A service is unhealthy | its service fiche (deploy path + dependencies + honest state) |\n| Something that matches no runbook | file a kanban card for the right lane (ops/senior/lead); the board is the queue of truth |\n\n## (g) No-go rules (NO-PO)\n\nThese are non-negotiable; violations are incidents, not \"creative\ntroubleshooting\":\n\n1. **Never `tofu apply` / `kubectl apply` from a card or on a hunch** \u2014\n   mutations run only when the task body explicitly authorizes that\n   specific apply with its gates; the board is the gate.\n2. **Never put secrets in Git** \u2014 no values, no keys, no htpasswd, no\n   tfstate, no tokens: reference locations (vault path / Infisical path /\n   0600 file) only (DocOps \u00a78).\n3. **Never change PROD without the gate** \u2014 a reviewed merge (or an\n   explicitly gated, explicitly authorized apply). Single-writer discipline\n   on the tofu state.\n4. Never edit Cloudflare DNS in the dashboard (tofu owns the records).\n5. Never assume a doc over live state \u2014 investigate the drift instead.\n\n## Validation\n\nYou have successfully taken over when: day-1 checks (d) all pass and you\ncan name, for any node in the\n[topology map](../architecture/overview-topology.md), its pill, its\ncredential location, and the runbook that applies to its failure mode.",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (project/taking-over-the-platform.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-proc-platform-takeov\">PROC-PLATFORM-TAKEOVER: Taking over the platform</p>\n<p id=\"bkmrk-the-platform-map-is-\">The platform map is\n<a href=\"https://book.wetopi.net/architecture/overview-topology.md\">ARCH-PLATFORM-TOPOLOGY</a> \u2014 read it\nfirst. This guide is the <em>procedure</em>: access, truth sources, reading order,\nday-1 checks, the operations you will run most often, where to escalate, and\nwhat is never allowed. It links out for everything it does not need to\nduplicate.</p>\n<h2 id=\"bkmrk-purpose-and-when-to-\">Purpose and when to use</h2>\n<p id=\"bkmrk-use-this-when-you-ar\">Use this when you are newly taking over operations of the platform \u2014 on a\nshift, a rotation, or an emergency handover. It is deliberately a composed\nguide of steps (the one allowed exception to the one-concept-per-pill rule);\nevery fact it does not restate lives in a linked pill or runbook.</p>\n<h2 id=\"bkmrk-%28a%29-access-%E2%80%94-who-has\">(a) Access \u2014 who has which credential, by location</h2>\n<p id=\"bkmrk-never-search-for-a-c\">Never search for a credential's <em>value</em> in this repo, the infrastructure\nrepo, a ticket or a chat \u2014 only its <strong>location</strong> is documented (DocOps \u00a78).</p>\n<table id=\"bkmrk-access-who-credentia\">\n<thead>\n<tr>\n<th>Access</th>\n<th>Who</th>\n<th>Credential location (never the value)</th>\n<th>Notes</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>SSH to the fleet</td>\n<td>operator holding the <code>bigardo</code> key</td>\n<td>the workstation's <code>~/.ssh/bigardo</code> (key pair issued out-of-band; the public side is pushed to VMs by the <code>ssh_keys</code> role)</td>\n<td>ops01 (<code>ubuntu@92.222.245.91</code>) is the bastion; all LAN VMs are reached <em>through</em> it. ops01 itself holds <strong>no private keys</strong> \u2014 it cannot SSH to the LAN by itself.</td>\n</tr>\n<tr>\n<td>Vault (human UI)</td>\n<td>the taking-over operator</td>\n<td>native Infisical account (username + password + TOTP) \u2014 the account is provisioned by the current operator at handover time; no value is documented</td>\n<td><code>https://vault.wetopi.net</code> \u2192 project <code>wetopi-harness</code>. The UI's own login+TOTP is the gate (no basic-auth).</td>\n</tr>\n<tr>\n<td>Vault (machine identities / Universal Auth)</td>\n<td>agent profiles / automation only</td>\n<td>client credentials live under project <code>wetopi-harness</code> in the Vault itself</td>\n<td><strong>private-network-only</strong>: <code>http://vault.vault.svc.cluster.local</code> \u2014 never the public route, which fail-closes by design (ADR-0009).</td>\n</tr>\n<tr>\n<td><code>kubectl</code> on the cluster</td>\n<td>operator via ops01</td>\n<td>kubeconfig fetched at apply-time from <code>rke-cp01</code> (the <code>ops_kubectl</code> role; nothing persisted in Git)</td>\n<td>see <a href=\"https://book.wetopi.net/architecture/platform/rke2-cluster.md\">ARCH-PLATFORM-RKE2</a> for the API VIP.</td>\n</tr>\n<tr>\n<td>OpenTofu (PROD)</td>\n<td>single writer, by explicit task</td>\n<td><code>.env.proxmox</code> on the phase-1 controller (0600); Cloudflare token on ops01 at <code>/home/ubuntu/secrets/.cloudflare-wetopi-net.env</code> (0600)</td>\n<td>pulled over SSH per session, never written to disk in the repo. State: <code>terraform.tfstate</code> 0600, <strong>never in Git</strong> (<a href=\"https://book.wetopi.net/architecture/infrastructure/providers.md\">ARCH-INFRA-PROVIDERS</a>).</td>\n</tr>\n<tr>\n<td>Proxmox API</td>\n<td>the same <code>.env.proxmox</code></td>\n<td>Proxmox VE HTTPS API token (env var)</td>\n<td>no SSH to hypervisors \u2014 API only.</td>\n</tr>\n<tr>\n<td>Cloudflare DNS</td>\n<td>the Cloudflare token above</td>\n<td>Zone DNS Edit + Zone Read (both required)</td>\n<td>records are tofu-managed \u2014 do not edit in the dashboard.</td>\n</tr>\n</tbody>\n</table>\n<p id=\"bkmrk-handover-checklist-f\">Handover checklist for credentials: at the handover meeting the outgoing\noperator must (1) verify you can reach ops01 and one LAN node with your\n<code>bigardo</code> key, (2) hand you a working Vault human account (or provision one\nyou can then rotate), (3) confirm the single-writer tofu lock is with you.\nIf any of these fail, stop and escalate \u2014 do not improvise credentials.</p>\n<h2 id=\"bkmrk-%28b%29-sources-of-truth\">(b) Sources of truth</h2>\n<p id=\"bkmrk-authority-order-%28doc\">Authority order (DocOps \u00a71) \u2014 when sources disagree, the live one wins:</p>\n<ol id=\"bkmrk-live-state-%E2%80%94-kubectl\">\n<li><strong>Live state</strong> \u2014 <code>kubectl</code> (via ops01), Proxmox API, Cloudflare API,\nCeph. \"What IS happening.\"</li>\n<li><strong>Desired state</strong> \u2014 the\n<a href=\"https://github.com/wetopi-platform/infrastructure\"><code>infrastructure</code></a>\nrepo (IaC: <code>tofu/</code>, <code>ansible/</code>, <code>kubernetes/</code>). A merge is a deploy.</li>\n<li><strong>Curated docs</strong> \u2014 this repo (<code>docs/</code>). The map:\n<a href=\"https://book.wetopi.net/architecture/overview-topology.md\">ARCH-PLATFORM-TOPOLOGY</a>.</li>\n<li><strong>Kanban / work ledger</strong> \u2014 execution state of tasks (who is working\nwhat, which card a change belongs to). Live board, not documentation.</li>\n<li><strong>tfstate</strong> \u2014 <code>terraform.tfstate</code> (0600, phase-1 controller) is the\nOpenTofu claim on PROD. Lost state \u2192\n<a href=\"https://book.wetopi.net/operations/runbooks/tofu-prod-state-loss-and-reimport.md\">RUN-TOFU-STATE-LOSS</a>,\nnever a speculative <code>apply</code>.</li>\n</ol>\n<h2 id=\"bkmrk-%28c%29-what-to-read%2C-in\">(c) What to read, in order</h2>\n<ol id=\"bkmrk-arch-platform-topolo\">\n<li><strong><a href=\"https://book.wetopi.net/architecture/overview-topology.md\">ARCH-PLATFORM-TOPOLOGY</a></strong> \u2014 the\nthree-layer map; every node links to its pill.</li>\n<li><strong>Decisions</strong> \u2014 <a href=\"https://book.wetopi.net/decisions/index.md\">ADR index</a>; read ADR-0003 (tofu vs\nAnsible split), ADR-0004 (API endpoint), ADR-0005 (Cilium first),\nADR-0006 (edge v1), ADR-0007 (Ceph storage-native), ADR-0009 (vault).</li>\n<li><strong>Layer pills</strong> \u2014 <a href=\"https://book.wetopi.net/architecture/infrastructure/index.md\">infrastructure section</a>\n(OVH/network \u2192 Proxmox \u2192 Ceph \u2192 providers), then\n<a href=\"https://book.wetopi.net/architecture/platform/index.md\">platform section</a> (RKE2 \u2192 Cilium \u2192\nedge \u2192 GitOps).</li>\n<li><strong>Service fiches</strong> \u2014 <a href=\"https://book.wetopi.net/services/index.md\">services section</a>: one per\ndeployed service with deploy path, dependencies and honest live state.</li>\n<li><strong>Runbooks</strong> \u2014 <a href=\"https://book.wetopi.net/operations/runbooks/index.md\">runbooks index</a>; the two\nload-bearing ones:\n<a href=\"https://book.wetopi.net/platform/rke2-apply-runbook.md\">RKE2 apply &amp; restore</a> (cluster\nbring-up/DR) and <a href=\"https://book.wetopi.net/operations/runbooks/tofu-prod-state-loss-and-reimport.md\">RUN-TOFU-STATE-LOSS</a>\n(IaC state recovery).</li>\n</ol>\n<h2 id=\"bkmrk-%28d%29-day-1-operations\">(d) Day-1 operations \u2014 verify the platform is healthy</h2>\n<p id=\"bkmrk-all-from-ops01-%28kube\">All from ops01 (kubeconfig in <code>~/.kube/config</code> or fetched per the\n<code>ops_kubectl</code> model \u2014 never copied into Git):</p>\n<ol id=\"bkmrk-cluster-nodes%3A-kubec\">\n<li><strong>Cluster nodes</strong>: <code>kubectl get nodes -o wide</code> \u2014 expect 8/8 <code>Ready</code>\n(cp01/02/03, platform01-03, wan01/02; IPs per\n<a href=\"https://book.wetopi.net/architecture/platform/rke2-cluster.md\">ARCH-PLATFORM-RKE2</a>).</li>\n<li><strong>GitOps plane</strong>: <code>kubectl -n argocd get applications</code> \u2014 expect\n<code>root</code>, <code>cert-manager</code>, <code>docs-site</code>, <code>argo-edge</code> Healthy+Synced; <code>vault</code>\nSynced \u2014 its <code>Degraded</code> health is the documented <strong>false positive</strong> of\nthe fail-closed deny rules (verify pods 3/3 Running in <code>vault</code> ns and\ncheck <a href=\"https://book.wetopi.net/services/vault.md\">SVC-VAULT</a> \"estado honesto\" before\ntreating it as a fault).</li>\n<li><strong>Edge</strong>: <code>kubectl -n kube-system get gateway,httproute</code> \u2014 Gateway\n<code>wetopi-edge</code> Accepted+Programmed, listeners 8000/8443;\n<code>kubectl get certificate -A</code> \u2014 <code>argo-tls</code>, <code>docs-tls</code>, <code>vault-tls</code>\n(and <code>edge-test-tls</code>) Ready. Public probe: <code>curl -sI https://docs.wetopi.net</code> and <code>https://argo.wetopi.net</code> return 200/302\nwith a valid cert chain.</li>\n<li><strong>DNS</strong>: the Cloudflare records for <code>docs/argo/vault/edge-test .wetopi.net</code> \u2192 <code>79.137.114.193/.194</code> (grey-cloud, TTL 300) \u2014 tofu-owned;\nverify with <code>dig</code> + the zone, don't trust the dashboard screenshot.</li>\n<li><strong>IaC sanity</strong>: a <code>tofu plan</code> on PROD should be <strong>no-change</strong>; a plan\nwanting to CREATE everything means lost state \u2192 the state-loss runbook.</li>\n<li><strong>Board</strong>: scan the kanban board for in-flight/<code>blocked</code> cards \u2014\nunderstand what is mid-flight before touching anything.</li>\n</ol>\n<h2 id=\"bkmrk-%28e%29-frequent-operati\">(e) Frequent operations</h2>\n<ul id=\"bkmrk-dns-record-change-%E2%80%94-\">\n<li><strong>DNS record change</strong> \u2014 never the Cloudflare dashboard: change the\n<code>tofu/proxmox/environments/prod/</code> tfvars (0600) \u2192 PR in the\n<code>infrastructure</code> repo \u2192 merge is the deploy. The provider needs the\nCloudflare token env (ops01, 0600). Records are\n<code>prevent_destroy</code>-annotated \u2014 deleting one is a recorded decision.</li>\n<li><strong>Deploy / change a service</strong> \u2014 GitOps only: edit <code>kubernetes/&lt;app&gt;/</code> in\nthe <code>infrastructure</code> repo \u2192 PR \u2192 review (blast-radius: a merge deploys)\n\u2192 merge; Argo CD syncs (<code>prune + selfHeal</code>). No <code>kubectl apply</code> of\nmanifests from a workstation; no CI <code>kubectl apply</code> pipeline exists by\ndesign (<a href=\"https://book.wetopi.net/architecture/platform/gitops.md\">ARCH-PLATFORM-GITOPS</a>).</li>\n<li><strong>Secret rotation</strong> \u2014 via the Vault human UI (<code>vault.wetopi.net</code>,\nproject <code>wetopi-harness</code>): rotate in Infisical, let the\n<a href=\"https://book.wetopi.net/services/vault.md\">Infisical Operator</a> sync to the cluster; SOPS+age\nis bootstrap/recovery only, not the daily vault (ADR-0009). The vault\ndatastore backup/restore contract (pg_dump + age, rehearsed) is in the\nADR AC8 \u2014 rehearse before calling it production.</li>\n<li><strong>Read-only inspection</strong> \u2014 any <code>kubectl get/describe/logs</code> via ops01.\nRead-only cluster reads are always allowed; anything mutating is a card\nwith a gate.</li>\n</ul>\n<h2 id=\"bkmrk-%28f%29-escalation-%E2%80%94-exi\">(f) Escalation \u2014 existing runbooks</h2>\n<table id=\"bkmrk-situation-runbook-%2F-\">\n<thead>\n<tr>\n<th>Situation</th>\n<th>Runbook / owner</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Cluster node down, CP quorum loss, DR</td>\n<td><a href=\"https://book.wetopi.net/platform/rke2-apply-runbook.md\">RKE2 apply &amp; restore runbook</a></td>\n</tr>\n<tr>\n<td><code>terraform.tfstate</code> lost / empty plan wants to create everything</td>\n<td><a href=\"https://book.wetopi.net/operations/runbooks/tofu-prod-state-loss-and-reimport.md\">RUN-TOFU-STATE-LOSS</a></td>\n</tr>\n<tr>\n<td>A service is unhealthy</td>\n<td>its service fiche (deploy path + dependencies + honest state)</td>\n</tr>\n<tr>\n<td>Something that matches no runbook</td>\n<td>file a kanban card for the right lane (ops/senior/lead); the board is the queue of truth</td>\n</tr>\n</tbody>\n</table>\n<h2 id=\"bkmrk-%28g%29-no-go-rules-%28no-\">(g) No-go rules (NO-PO)</h2>\n<p id=\"bkmrk-these-are-non-negoti\">These are non-negotiable; violations are incidents, not \"creative\ntroubleshooting\":</p>\n<ol id=\"bkmrk-never-tofu-apply-%2F-k\">\n<li><strong>Never <code>tofu apply</code> / <code>kubectl apply</code> from a card or on a hunch</strong> \u2014\nmutations run only when the task body explicitly authorizes that\nspecific apply with its gates; the board is the gate.</li>\n<li><strong>Never put secrets in Git</strong> \u2014 no values, no keys, no htpasswd, no\ntfstate, no tokens: reference locations (vault path / Infisical path /\n0600 file) only (DocOps \u00a78).</li>\n<li><strong>Never change PROD without the gate</strong> \u2014 a reviewed merge (or an\nexplicitly gated, explicitly authorized apply). Single-writer discipline\non the tofu state.</li>\n<li>Never edit Cloudflare DNS in the dashboard (tofu owns the records).</li>\n<li>Never assume a doc over live state \u2014 investigate the drift instead.</li>\n</ol>\n<h2 id=\"bkmrk-validation\">Validation</h2>\n<p id=\"bkmrk-you-have-successfull\">You have successfully taken over when: day-1 checks (d) all pass and you\ncan name, for any node in the\n<a href=\"https://book.wetopi.net/architecture/overview-topology.md\">topology map</a>, its pill, its\ncredential location, and the runbook that applies to its failure mode.</p>\n"
 },
 {
  "id": 62,
  "title": "SVC-ARGOCD \u2014 GitOps plane",
  "slug": "svc-argocd-gitops-plane",
  "book_id": 3,
  "chapter_id": 21,
  "description": null,
  "created_at": "2026-09-06T13:39:04.000000Z",
  "updated_at": "2026-09-06T21:47:08.000000Z",
  "markdown": "> **Mirror de lectura** \u00b7 Mirror de Git: `docs/services/argocd.md`. Git es el SSOT.\n\n> test banner",
  "html": "<blockquote id=\"bkmrk-mirror-de-lectura-%C2%B7-\">\n<p><strong>Mirror de lectura</strong> \u00b7 Mirror de Git: <code>docs/services/argocd.md</code>. Git es el SSOT.</p>\n</blockquote>\n<blockquote id=\"bkmrk-test-banner\">\n<p>test banner</p>\n</blockquote>\n"
 },
 {
  "id": 63,
  "title": "SVC-CEPH-CSI \u2014 storage driver",
  "slug": "svc-ceph-csi-storage-driver",
  "book_id": 3,
  "chapter_id": 21,
  "description": null,
  "created_at": "2026-09-06T13:39:05.000000Z",
  "updated_at": "2026-09-06T21:45:54.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (services/ceph-csi.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n Ceph CSI (RBD) \u2014 cluster storage\n\n## What is it?\n\nThe Ceph CSI driver gives the RKE2 cluster RBD-backed `PersistentVolumeClaim`s\nfrom the wetopi Ceph cluster (192.168.96.0/20). It is the first real storage\nclass on the platform: stateful workloads (the Vault datastores today,\nstateful services later) persist on Ceph instead of node-local disks.\n\n## How is it mounted?\n\n- **Chart:** `ceph-csi-rbd` **v3.17.1** (repository\n  `https://ceph.github.io/csi-charts`), image\n  `quay.io/cephcsi/cephcsi:v3.17.1` (chart-pinned), installed in\n  `kube-system` via `ansible/playbooks/ceph-csi.yml` (helm).\n  `provisioner.replicaCount: 2` (controller Deployment); the node plugin is\n  a DaemonSet (one pod per node \u2014 chart default).\n- **StorageClass:** `ceph-rbd` (`kubernetes/ceph-csi/storageclass.yaml`) \u2014\n  provisioner `rbd.csi.ceph.com`, pool `kubernetes` (DEDICATED pool to be\n  created on the Ceph side \u2014 never reuse existing pools),\n  `imageFeatures: layering`, `reclaimPolicy: Delete`,\n  `allowVolumeExpansion: true`, fstype ext4.\n- **Secret (reference, never value):** `csi-rbd-secret` in `kube-system` \u2014\n  a dedicated Ceph user (e.g. `client.kubernetes-csi`, NOT admin) with\n  `userID`/`userKey`; shape in `secret.example.yaml`, values created\n  manually at apply time (long term: from the Vault flow, ADR-0009).\n- **Values placeholders:** `clusterID` (Ceph fsid) and `monitors`\n  (host:3300 msgr2) are `<REPLACE_ME>` in Git \u2014 filled at apply time from\n  live read-only lookups (`ceph fsid`, `ceph mon dump`).\n\n## Diagram\n\n```mermaid\nflowchart TD\n  subgraph CEPH[\"Ceph cluster (192.168.96.0/20)\"]\n    MON[\"monitors (msgr2 :3300)\"]\n    OSDS[\"OSDs / pool 'kubernetes'\"]\n  end\n  subgraph K8S[\"cluster wetopi3\"]\n    subgraph KSYS[\"kube-system\"]\n      PROV[\"csi-rbd-provisioner x2 (Deployment)\"]\n      NODE[\"csi-rbd-node (DaemonSet, 1 pod/node)\"]\n      SC[\"StorageClass ceph-rbd\"]\n      SEC[\"Secret csi-rbd-secret (dedicated Ceph user keyring)\"]\n    end\n    PVC[\"PVCs (e.g. vault-postgres-data 5Gi, vault-redis-data 1Gi)\"]\n  end\n  NET[\"storage NIC VLAN 2 / vmbr2 \u2014 192.168.98.10/.11/.12 /25 (workers only)\"]\n  PROV --> SC\n  PVC --> SC\n  PROV -->|\"RBD ops\"| OSDS\n  NODE -->|\"mounts RBD images\"| OSDS\n  PROV & NODE --> SEC\n  K8S --> NET --> MON\n```\n\n## How is it deployed?\n\nBootstrap-time, **human-gated** (Phase F of the apply runbook): the Ansible\nplaybook `ceph-csi.yml` installs the chart + applies the StorageClass and\nfails early while placeholders or the Secret are missing. Prerequisites\n(all human, per `kubernetes/ceph-csi/README.md`): (a) worker storage-NIC\naddresses DONE by the operator 2026-08-30 (192.168.98.10/.11/.12, /25 \u2014\nNIC add + firewall runs as a reviewed mutating apply, Phase W pattern,\nserial drain/reboot per worker); (b) Ceph pool `kubernetes` + dedicated\nuser + caps; (c) live fsid/monitors read; (d) `csi-rbd-secret` created.\nMigration to Argo CD ownership (values/StorageClass under Git sync) is a\nfuture reviewed change \u2014 today the playbook is the installer.\n\n## Dependencies\n\n**Depends on:** the wetopi Ceph cluster itself (monitors + a dedicated\n`kubernetes` pool); the storage-native network path \u2014 third NIC on VLAN 2\n/ `vmbr2` on the three platform workers with **no gateway and no return\nroutes** (ADR-0007 supersedes the ADR-0004 gateway-routing hypothesis;\nroute-not-NAT, Ceph subnet never renumbered); a dedicated Ceph user keyring\n(operator input, card `t_8eee26e4`); Cilium for pod networking.\n\n**Depended on by:** every PVC on the platform \u2014 concretely the Vault\ndatastores today (`vault-postgres-data`, `vault-redis-data`,\nStorageClass `ceph-rbd`, [SVC-VAULT](vault.md)); any future stateful\nworkload.\n\n## Operation\n\n- **Health:** `kubectl -n kube-system get pods | grep csi-rbd`\n  (provisioners 2/2, node plugin Ready on each worker);\n  `kubectl get sc ceph-rbd` (PROVISIONER shown); a scratch PVC binding test\n  (create small PVC \u2192 Bound \u2192 delete) is the end-to-end proof.\n- **Observability:** ceph-csi metrics via its Prometheus exporter\n  (chart-enabled); Ceph-side health is owned by the Ceph operators\n  (separate plane \u2014 this service is the client).\n- **Failure note:** if a pod sits in `ContainerCreating` waiting on\n  configmaps `ceph-config` / `ceph-csi-encryption-kms-config`, the driver's\n  configuration objects are missing \u2014 the live symptom recorded 2026-08-31\n  (inputs operator, card `t_8eee26e4`); do not work around by hand-creating\n  manifests outside the reviewed flow.\n- **Backup:** RBD images are Ceph objects \u2014 durability comes from the Ceph\n  replication (mon/OSD layer); volume-level backup of a specific PVC is a\n  Ceph-side operation (snapshot/clone), out of scope of this driver.\n\n## Estado honesto (live, 2026-08-31)\n\n**PENDING \u2014 not serving PVCs.** The chart is installed in `kube-system`\nbut the pods are in `ContainerCreating` waiting on the operator-input\nconfigmaps (`ceph-config` / `ceph-csi-encryption-kms-config` \u2014 inputs\noperator, card `t_8eee26e4`); `clusterID`/`monitors` and the\n`csi-rbd-secret` are still placeholders pending the Ceph-side inputs.\nThe StorageClass `ceph-rbd` object exists in Git as the target shape but\nis NOT yet usable: no PVC should be created against it until the pods are\nRunning and a scratch-PVC bind test passes. The Vault datastores depend on\nexactly this \u2014 that dependency is why the Vault is also PENDING.\n\n## Relay links (enlaces de relevo)\n\n- **Manifests (GitHub):** [kubernetes/ceph-csi](https://github.com/wetopi-platform/infrastructure/tree/main/kubernetes/ceph-csi) (values, StorageClass, secret shape, VALIDATION.md)\n- **Vault (https://vault.wetopi.net \u2192 project `wetopi-harness`):** long-term\n  home of the `csi-rbd-secret` values (ADR-0009 \u2014 \"these values come from\n  the Vault when it exists\"); no Ceph secret material in this repo today.\n- **Related docs:** [ADR-0007](../decisions/ADR-0007-ceph-storage-native.md) (storage-native access \u2014 THE decision), [ADR-0004](../decisions/ADR-0004-rke2-api-endpoint.md) (Ceph-routing section superseded), [storage index](../architecture/storage/index.md), [services index](index.md).",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (services/ceph-csi.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-ceph-csi-%28rbd%29-%E2%80%94-clu\">Ceph CSI (RBD) \u2014 cluster storage</p>\n<h2 id=\"bkmrk-what-is-it%3F\">What is it?</h2>\n<p id=\"bkmrk-the-ceph-csi-driver-\">The Ceph CSI driver gives the RKE2 cluster RBD-backed <code>PersistentVolumeClaim</code>s\nfrom the wetopi Ceph cluster (192.168.96.0/20). It is the first real storage\nclass on the platform: stateful workloads (the Vault datastores today,\nstateful services later) persist on Ceph instead of node-local disks.</p>\n<h2 id=\"bkmrk-how-is-it-mounted%3F\">How is it mounted?</h2>\n<ul id=\"bkmrk-chart%3A-ceph-csi-rbd-\">\n<li><strong>Chart:</strong> <code>ceph-csi-rbd</code> <strong>v3.17.1</strong> (repository\n<code>https://ceph.github.io/csi-charts</code>), image\n<code>quay.io/cephcsi/cephcsi:v3.17.1</code> (chart-pinned), installed in\n<code>kube-system</code> via <code>ansible/playbooks/ceph-csi.yml</code> (helm).\n<code>provisioner.replicaCount: 2</code> (controller Deployment); the node plugin is\na DaemonSet (one pod per node \u2014 chart default).</li>\n<li><strong>StorageClass:</strong> <code>ceph-rbd</code> (<code>kubernetes/ceph-csi/storageclass.yaml</code>) \u2014\nprovisioner <code>rbd.csi.ceph.com</code>, pool <code>kubernetes</code> (DEDICATED pool to be\ncreated on the Ceph side \u2014 never reuse existing pools),\n<code>imageFeatures: layering</code>, <code>reclaimPolicy: Delete</code>,\n<code>allowVolumeExpansion: true</code>, fstype ext4.</li>\n<li><strong>Secret (reference, never value):</strong> <code>csi-rbd-secret</code> in <code>kube-system</code> \u2014\na dedicated Ceph user (e.g. <code>client.kubernetes-csi</code>, NOT admin) with\n<code>userID</code>/<code>userKey</code>; shape in <code>secret.example.yaml</code>, values created\nmanually at apply time (long term: from the Vault flow, ADR-0009).</li>\n<li><strong>Values placeholders:</strong> <code>clusterID</code> (Ceph fsid) and <code>monitors</code>\n(host:3300 msgr2) are <code>&lt;REPLACE_ME&gt;</code> in Git \u2014 filled at apply time from\nlive read-only lookups (<code>ceph fsid</code>, <code>ceph mon dump</code>).</li>\n</ul>\n<h2 id=\"bkmrk-diagram\">Diagram</h2>\n<pre id=\"bkmrk-flowchart-td-subgrap\"><code class=\"language-mermaid\">flowchart TD\n  subgraph CEPH[\"Ceph cluster (192.168.96.0/20)\"]\n    MON[\"monitors (msgr2 :3300)\"]\n    OSDS[\"OSDs / pool 'kubernetes'\"]\n  end\n  subgraph K8S[\"cluster wetopi3\"]\n    subgraph KSYS[\"kube-system\"]\n      PROV[\"csi-rbd-provisioner x2 (Deployment)\"]\n      NODE[\"csi-rbd-node (DaemonSet, 1 pod/node)\"]\n      SC[\"StorageClass ceph-rbd\"]\n      SEC[\"Secret csi-rbd-secret (dedicated Ceph user keyring)\"]\n    end\n    PVC[\"PVCs (e.g. vault-postgres-data 5Gi, vault-redis-data 1Gi)\"]\n  end\n  NET[\"storage NIC VLAN 2 / vmbr2 \u2014 192.168.98.10/.11/.12 /25 (workers only)\"]\n  PROV --&gt; SC\n  PVC --&gt; SC\n  PROV --&gt;|\"RBD ops\"| OSDS\n  NODE --&gt;|\"mounts RBD images\"| OSDS\n  PROV &amp; NODE --&gt; SEC\n  K8S --&gt; NET --&gt; MON\n</code></pre>\n<h2 id=\"bkmrk-how-is-it-deployed%3F\">How is it deployed?</h2>\n<p id=\"bkmrk-bootstrap-time%2C-huma\">Bootstrap-time, <strong>human-gated</strong> (Phase F of the apply runbook): the Ansible\nplaybook <code>ceph-csi.yml</code> installs the chart + applies the StorageClass and\nfails early while placeholders or the Secret are missing. Prerequisites\n(all human, per <code>kubernetes/ceph-csi/README.md</code>): (a) worker storage-NIC\naddresses DONE by the operator 2026-08-30 (192.168.98.10/.11/.12, /25 \u2014\nNIC add + firewall runs as a reviewed mutating apply, Phase W pattern,\nserial drain/reboot per worker); (b) Ceph pool <code>kubernetes</code> + dedicated\nuser + caps; (c) live fsid/monitors read; (d) <code>csi-rbd-secret</code> created.\nMigration to Argo CD ownership (values/StorageClass under Git sync) is a\nfuture reviewed change \u2014 today the playbook is the installer.</p>\n<h2 id=\"bkmrk-dependencies\">Dependencies</h2>\n<p id=\"bkmrk-depends-on%3A-the-weto\"><strong>Depends on:</strong> the wetopi Ceph cluster itself (monitors + a dedicated\n<code>kubernetes</code> pool); the storage-native network path \u2014 third NIC on VLAN 2\n/ <code>vmbr2</code> on the three platform workers with <strong>no gateway and no return\nroutes</strong> (ADR-0007 supersedes the ADR-0004 gateway-routing hypothesis;\nroute-not-NAT, Ceph subnet never renumbered); a dedicated Ceph user keyring\n(operator input, card <code>t_8eee26e4</code>); Cilium for pod networking.</p>\n<p id=\"bkmrk-depended-on-by%3A-ever\"><strong>Depended on by:</strong> every PVC on the platform \u2014 concretely the Vault\ndatastores today (<code>vault-postgres-data</code>, <code>vault-redis-data</code>,\nStorageClass <code>ceph-rbd</code>, <a href=\"https://book.wetopi.net/vault.md\">SVC-VAULT</a>); any future stateful\nworkload.</p>\n<h2 id=\"bkmrk-operation\">Operation</h2>\n<ul id=\"bkmrk-health%3A-kubectl--n-k\">\n<li><strong>Health:</strong> <code>kubectl -n kube-system get pods | grep csi-rbd</code>\n(provisioners 2/2, node plugin Ready on each worker);\n<code>kubectl get sc ceph-rbd</code> (PROVISIONER shown); a scratch PVC binding test\n(create small PVC \u2192 Bound \u2192 delete) is the end-to-end proof.</li>\n<li><strong>Observability:</strong> ceph-csi metrics via its Prometheus exporter\n(chart-enabled); Ceph-side health is owned by the Ceph operators\n(separate plane \u2014 this service is the client).</li>\n<li><strong>Failure note:</strong> if a pod sits in <code>ContainerCreating</code> waiting on\nconfigmaps <code>ceph-config</code> / <code>ceph-csi-encryption-kms-config</code>, the driver's\nconfiguration objects are missing \u2014 the live symptom recorded 2026-08-31\n(inputs operator, card <code>t_8eee26e4</code>); do not work around by hand-creating\nmanifests outside the reviewed flow.</li>\n<li><strong>Backup:</strong> RBD images are Ceph objects \u2014 durability comes from the Ceph\nreplication (mon/OSD layer); volume-level backup of a specific PVC is a\nCeph-side operation (snapshot/clone), out of scope of this driver.</li>\n</ul>\n<h2 id=\"bkmrk-estado-honesto-%28live\">Estado honesto (live, 2026-08-31)</h2>\n<p id=\"bkmrk-pending-%E2%80%94-not-servin\"><strong>PENDING \u2014 not serving PVCs.</strong> The chart is installed in <code>kube-system</code>\nbut the pods are in <code>ContainerCreating</code> waiting on the operator-input\nconfigmaps (<code>ceph-config</code> / <code>ceph-csi-encryption-kms-config</code> \u2014 inputs\noperator, card <code>t_8eee26e4</code>); <code>clusterID</code>/<code>monitors</code> and the\n<code>csi-rbd-secret</code> are still placeholders pending the Ceph-side inputs.\nThe StorageClass <code>ceph-rbd</code> object exists in Git as the target shape but\nis NOT yet usable: no PVC should be created against it until the pods are\nRunning and a scratch-PVC bind test passes. The Vault datastores depend on\nexactly this \u2014 that dependency is why the Vault is also PENDING.</p>\n<h2 id=\"bkmrk-relay-links-%28enlaces\">Relay links (enlaces de relevo)</h2>\n<ul id=\"bkmrk-manifests-%28github%29%3A-\">\n<li><strong>Manifests (GitHub):</strong> <a href=\"https://github.com/wetopi-platform/infrastructure/tree/main/kubernetes/ceph-csi\">kubernetes/ceph-csi</a> (values, StorageClass, secret shape, VALIDATION.md)</li>\n<li><strong>Vault (https://vault.wetopi.net \u2192 project <code>wetopi-harness</code>):</strong> long-term\nhome of the <code>csi-rbd-secret</code> values (ADR-0009 \u2014 \"these values come from\nthe Vault when it exists\"); no Ceph secret material in this repo today.</li>\n<li><strong>Related docs:</strong> <a href=\"https://book.wetopi.net/decisions/ADR-0007-ceph-storage-native.md\">ADR-0007</a> (storage-native access \u2014 THE decision), <a href=\"https://book.wetopi.net/decisions/ADR-0004-rke2-api-endpoint.md\">ADR-0004</a> (Ceph-routing section superseded), <a href=\"https://book.wetopi.net/architecture/storage/index.md\">storage index</a>, <a href=\"https://book.wetopi.net/index.md\">services index</a>.</li>\n</ul>\n"
 },
 {
  "id": 64,
  "title": "SVC-CERT-MANAGER \u2014 LE DNS-01",
  "slug": "svc-cert-manager-le-dns-01",
  "book_id": 3,
  "chapter_id": 21,
  "description": null,
  "created_at": "2026-09-06T13:39:05.000000Z",
  "updated_at": "2026-09-06T21:45:54.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (services/cert-manager.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n cert-manager (ACME / Let's Encrypt)\n\n## What is it?\n\ncert-manager is the cluster's ACME client: it issues and automatically\nrenews the TLS certificates that the edge Gateway terminates on. DNS-01\nchallenges are answered through the Cloudflare API, so issuance is\nport-mapping-immune (no open 80/8080 HTTP-01 surface on the edge). It is\nthe service that turned the F2 bootstrap self-signed cert into real LE\ncertificates for every public hostname.\n\n## How is it mounted?\n\n- **Namespace:** `cert-manager` (Git-declared `namespace.yaml`, single\n  creator \u2014 sync runs with `CreateNamespace=false`).\n- **Version pin:** Helm chart `cert-manager` **v1.21.1** from\n  `oci://quay.io/jetstack/charts` (OCI is the source of truth; the legacy\n  index lags). appVersion v1.21.1 \u2014 controller/webhook/cainjector/acmesolver\n  are one released set; no per-image tag overrides. Cluster requirement\n  `kubeVersion >= 1.22.0-0` satisfied by RKE2 v1.36.3 (K8s 1.36).\n- **Objects (one Argo-owned unit, multi-source Application\n  `cert-manager`):** source 1 = the pinned chart (values from\n  `kubernetes/cert-manager/values.yaml` via `$values`; `crds.enabled: true`\n  so the CRDs are part of the release and owned/pruned by Argo); source 2 =\n  this repo at `kubernetes/cert-manager` rendering EXACTLY the Namespace,\n  both ClusterIssuers and the staging Certificate (include/exclude filter\n  documented in `platform/cert-manager.yaml`). The Application is anchored\n  in the root path (`kubernetes/platform/cert-manager.yaml`) and carries the\n  cascading-deletion finalizer \u2014 a `git revert` of the anchoring merge rolls\n  the full F3 deployment back as a foreground cascade.\n- **Issuers:** `letsencrypt-staging` and `letsencrypt-prod`\n  (ClusterIssuers, DNS-01 Cloudflare solver). Both reference the SAME\n  operator Secret `cloudflare-dns-api-token` (key `api-token`) in\n  namespace `cert-manager` \u2014 BY NAME ONLY; the token itself is operator\n  material (0600 env file on ops01, shared with OpenTofu's cloudflare\n  provider), never in Git.\n- **Certificates (all in `kube-system` \u2014 the Gateway's namespace, so the\n  listener `certificateRefs` are same-namespace, no ReferenceGrant):**\n  `edge-test-tls` (staging, the F3 acceptance object), `argo-tls`,\n  `docs-tls`, `vault-tls` (all LE PROD, single-name, no wildcards).\n\n## Diagram\n\n```mermaid\nflowchart TD\n  CF[\"Cloudflare API (wetopi.net zone: DNS-Edit + Zone-Read)\"]\n  subgraph CM[\"namespace cert-manager\"]\n    CTL[\"cert-manager controller v1.21.1 (+ webhook, cainjector)\"]\n    ISP[\"ClusterIssuer letsencrypt-prod\"]\n    ISS[\"ClusterIssuer letsencrypt-staging\"]\n    TOK[\"Secret cloudflare-dns-api-token (operator)\"]\n  end\n  subgraph KSYS[\"kube-system (Gateway namespace)\"]\n    CE1[\"Certificate edge-test-tls (staging)\"]\n    CA[\"Certificate argo-tls (PROD)\"]\n    CD[\"Certificate docs-tls (PROD)\"]\n    CV[\"Certificate vault-tls (PROD)\"]\n    GW[\"Gateway wetopi-edge https listener certificateRefs\"]\n  end\n  LE[\"Let's Encrypt ACME (staging + prod)\"]\n  CF -->|\"_acme-challenge TXT\"| CTL\n  CTL -->|ACME orders| LE\n  ISP --> CTL\n  ISS --> CTL\n  TOK --> ISP\n  TOK --> ISS\n  CE1 --> CA & CD & CV\n  CA & CD & CV --> GW\n```\n\n## How is it deployed?\n\nGitOps: Application **`cert-manager`** (anchored in\n`kubernetes/platform/cert-manager.yaml`, synced by the root app) \u2014\n`prune + selfHeal`. One-time bootstrap from ops01 (runbook:\n`kubernetes/cert-manager/README.md`): apply `namespace.yaml` by hand \u2192\ncreate the `cloudflare-dns-api-token` Secret from the ops01 0600 env file\n(`kubectl create secret ... --from-literal`, shape-only verification) \u2192\nthe root app creates the Application and syncs. **Prod cutover gate:**\nnothing may reference `letsencrypt-prod` until the F3 staging gate\n(edge-test certificate issued + full chain verified) is signed off \u2014 LE\nprod rate limits burn real budget. Upgrades = a reviewed PR changing\n`targetRevision` + `values.yaml` together.\n\n## Dependencies\n\n**Depends on:** the Cloudflare API token (Zone - DNS - Edit AND Zone - Zone\n- Read on `wetopi.net`) \u2014 operator-managed, ops01-only canonical file; the\nedge Gateway as the certificate consumer\n([SVC-TRAEFIK-EDGE](traefik-edge.md)); DNS records for the issued names\n(tofu-owned, so the `_acme-challenge` zone exists); the root Argo\nApplication ([SVC-ARGOCD](argocd.md)) for lifecycle.\n\n**Depended on by:** every public hostname on the edge \u2014 `argo-tls`\n(argo.wetopi.net), `docs-tls` (docs.wetopi.net), `vault-tls`\n(vault.wetopi.net), `edge-test-tls` (F3 acceptance). Without renewals, the\nwhole https listener degrades at 90-day boundaries.\n\n## Operation\n\n- **Health:** `kubectl -n cert-manager get pods` (controller/webhook/\n  cainjector Ready); `kubectl -n kube-system get certificates` \u2014 all four\n  `Ready=True`; controller events show ACME order \u2192 challenge \u2192 issue\n  chain. Expiry: `kubectl get certificate <name> -o jsonpath='{.status.notAfter}'`.\n- **Solver troubleshooting:** a zone-discovery failure means the token\n  lacks Zone-Read (the v1.21 solver always resolves the zone first \u2014 see\n  README \"Solver troubleshooting\" + the read-only token probe).\n- **Rollback of a name:** delete the `certificateRef` entry from the\n  Gateway (git revert); the Secret stays and keeps renewing unless the\n  Certificate is reverted too.\n- **Runbook:** infra repo `kubernetes/cert-manager/README.md` (ownership,\n  token provisioning, prod cutover gate).\n\n## Estado honesto (live, 2026-08-31)\n\n**DEPLOYED.** Live since the F3 apply: controller/webhook/cainjector\nRunning, both issuers configured. Certificates live-verified 2026-08-31 \u2014\n4/4 `Ready=True`: `edge-test-tls` (staging), `argo-tls`, `docs-tls`,\n`vault-tls` (LE PROD). Renewals run automatically (renewBefore default\n30 days for 90-day ACME certs).\n\n## Relay links (enlaces de relevo)\n\n- **Manifests (GitHub):** [kubernetes/cert-manager](https://github.com/wetopi-platform/infrastructure/tree/main/kubernetes/cert-manager) (issuers, certificates, values, README runbook), [kubernetes/platform/cert-manager.yaml](https://github.com/wetopi-platform/infrastructure/blob/main/kubernetes/platform/cert-manager.yaml) (the Argo Application)\n- **Vault (https://vault.wetopi.net \u2192 project `wetopi-harness`):** no\n  certificate material lives in the Vault; the ACME account keys\n  (`letsencrypt-*-account-key` Secrets) and the Cloudflare token are\n  operator-managed (ops01 + cert-manager namespace Secrets) \u2014 migration\n  into the Vault is part of ADR-0009's long-term picture, not yet done.\n- **Related docs:** [ADR-0006](../decisions/ADR-0006-edge-v1-wan-exception.md) (edge + LE posture), [services index](index.md).",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (services/cert-manager.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-cert-manager-%28acme-%2F\">cert-manager (ACME / Let's Encrypt)</p>\n<h2 id=\"bkmrk-what-is-it%3F\">What is it?</h2>\n<p id=\"bkmrk-cert-manager-is-the-\">cert-manager is the cluster's ACME client: it issues and automatically\nrenews the TLS certificates that the edge Gateway terminates on. DNS-01\nchallenges are answered through the Cloudflare API, so issuance is\nport-mapping-immune (no open 80/8080 HTTP-01 surface on the edge). It is\nthe service that turned the F2 bootstrap self-signed cert into real LE\ncertificates for every public hostname.</p>\n<h2 id=\"bkmrk-how-is-it-mounted%3F\">How is it mounted?</h2>\n<ul id=\"bkmrk-namespace%3A-cert-mana\">\n<li><strong>Namespace:</strong> <code>cert-manager</code> (Git-declared <code>namespace.yaml</code>, single\ncreator \u2014 sync runs with <code>CreateNamespace=false</code>).</li>\n<li><strong>Version pin:</strong> Helm chart <code>cert-manager</code> <strong>v1.21.1</strong> from\n<code>oci://quay.io/jetstack/charts</code> (OCI is the source of truth; the legacy\nindex lags). appVersion v1.21.1 \u2014 controller/webhook/cainjector/acmesolver\nare one released set; no per-image tag overrides. Cluster requirement\n<code>kubeVersion &gt;= 1.22.0-0</code> satisfied by RKE2 v1.36.3 (K8s 1.36).</li>\n<li><strong>Objects (one Argo-owned unit, multi-source Application\n<code>cert-manager</code>):</strong> source 1 = the pinned chart (values from\n<code>kubernetes/cert-manager/values.yaml</code> via <code>$values</code>; <code>crds.enabled: true</code>\nso the CRDs are part of the release and owned/pruned by Argo); source 2 =\nthis repo at <code>kubernetes/cert-manager</code> rendering EXACTLY the Namespace,\nboth ClusterIssuers and the staging Certificate (include/exclude filter\ndocumented in <code>platform/cert-manager.yaml</code>). The Application is anchored\nin the root path (<code>kubernetes/platform/cert-manager.yaml</code>) and carries the\ncascading-deletion finalizer \u2014 a <code>git revert</code> of the anchoring merge rolls\nthe full F3 deployment back as a foreground cascade.</li>\n<li><strong>Issuers:</strong> <code>letsencrypt-staging</code> and <code>letsencrypt-prod</code>\n(ClusterIssuers, DNS-01 Cloudflare solver). Both reference the SAME\noperator Secret <code>cloudflare-dns-api-token</code> (key <code>api-token</code>) in\nnamespace <code>cert-manager</code> \u2014 BY NAME ONLY; the token itself is operator\nmaterial (0600 env file on ops01, shared with OpenTofu's cloudflare\nprovider), never in Git.</li>\n<li><strong>Certificates (all in <code>kube-system</code> \u2014 the Gateway's namespace, so the\nlistener <code>certificateRefs</code> are same-namespace, no ReferenceGrant):</strong>\n<code>edge-test-tls</code> (staging, the F3 acceptance object), <code>argo-tls</code>,\n<code>docs-tls</code>, <code>vault-tls</code> (all LE PROD, single-name, no wildcards).</li>\n</ul>\n<h2 id=\"bkmrk-diagram\">Diagram</h2>\n<pre id=\"bkmrk-flowchart-td-cf%5B%22clo\"><code class=\"language-mermaid\">flowchart TD\n  CF[\"Cloudflare API (wetopi.net zone: DNS-Edit + Zone-Read)\"]\n  subgraph CM[\"namespace cert-manager\"]\n    CTL[\"cert-manager controller v1.21.1 (+ webhook, cainjector)\"]\n    ISP[\"ClusterIssuer letsencrypt-prod\"]\n    ISS[\"ClusterIssuer letsencrypt-staging\"]\n    TOK[\"Secret cloudflare-dns-api-token (operator)\"]\n  end\n  subgraph KSYS[\"kube-system (Gateway namespace)\"]\n    CE1[\"Certificate edge-test-tls (staging)\"]\n    CA[\"Certificate argo-tls (PROD)\"]\n    CD[\"Certificate docs-tls (PROD)\"]\n    CV[\"Certificate vault-tls (PROD)\"]\n    GW[\"Gateway wetopi-edge https listener certificateRefs\"]\n  end\n  LE[\"Let's Encrypt ACME (staging + prod)\"]\n  CF --&gt;|\"_acme-challenge TXT\"| CTL\n  CTL --&gt;|ACME orders| LE\n  ISP --&gt; CTL\n  ISS --&gt; CTL\n  TOK --&gt; ISP\n  TOK --&gt; ISS\n  CE1 --&gt; CA &amp; CD &amp; CV\n  CA &amp; CD &amp; CV --&gt; GW\n</code></pre>\n<h2 id=\"bkmrk-how-is-it-deployed%3F\">How is it deployed?</h2>\n<p id=\"bkmrk-gitops%3A-application-\">GitOps: Application <strong><code>cert-manager</code></strong> (anchored in\n<code>kubernetes/platform/cert-manager.yaml</code>, synced by the root app) \u2014\n<code>prune + selfHeal</code>. One-time bootstrap from ops01 (runbook:\n<code>kubernetes/cert-manager/README.md</code>): apply <code>namespace.yaml</code> by hand \u2192\ncreate the <code>cloudflare-dns-api-token</code> Secret from the ops01 0600 env file\n(<code>kubectl create secret ... --from-literal</code>, shape-only verification) \u2192\nthe root app creates the Application and syncs. <strong>Prod cutover gate:</strong>\nnothing may reference <code>letsencrypt-prod</code> until the F3 staging gate\n(edge-test certificate issued + full chain verified) is signed off \u2014 LE\nprod rate limits burn real budget. Upgrades = a reviewed PR changing\n<code>targetRevision</code> + <code>values.yaml</code> together.</p>\n<h2 id=\"bkmrk-dependencies\">Dependencies</h2>\n<p id=\"bkmrk-depends-on%3A-the-clou\"><strong>Depends on:</strong> the Cloudflare API token (Zone - DNS - Edit AND Zone - Zone</p>\n<ul id=\"bkmrk-read-on-wetopi.net%29-\">\n<li>Read on <code>wetopi.net</code>) \u2014 operator-managed, ops01-only canonical file; the\nedge Gateway as the certificate consumer\n(<a href=\"https://book.wetopi.net/traefik-edge.md\">SVC-TRAEFIK-EDGE</a>); DNS records for the issued names\n(tofu-owned, so the <code>_acme-challenge</code> zone exists); the root Argo\nApplication (<a href=\"https://book.wetopi.net/argocd.md\">SVC-ARGOCD</a>) for lifecycle.</li>\n</ul>\n<p id=\"bkmrk-depended-on-by%3A-ever\"><strong>Depended on by:</strong> every public hostname on the edge \u2014 <code>argo-tls</code>\n(argo.wetopi.net), <code>docs-tls</code> (docs.wetopi.net), <code>vault-tls</code>\n(vault.wetopi.net), <code>edge-test-tls</code> (F3 acceptance). Without renewals, the\nwhole https listener degrades at 90-day boundaries.</p>\n<h2 id=\"bkmrk-operation\">Operation</h2>\n<ul id=\"bkmrk-health%3A-kubectl--n-c\">\n<li><strong>Health:</strong> <code>kubectl -n cert-manager get pods</code> (controller/webhook/\ncainjector Ready); <code>kubectl -n kube-system get certificates</code> \u2014 all four\n<code>Ready=True</code>; controller events show ACME order \u2192 challenge \u2192 issue\nchain. Expiry: <code>kubectl get certificate &lt;name&gt; -o jsonpath='{.status.notAfter}'</code>.</li>\n<li><strong>Solver troubleshooting:</strong> a zone-discovery failure means the token\nlacks Zone-Read (the v1.21 solver always resolves the zone first \u2014 see\nREADME \"Solver troubleshooting\" + the read-only token probe).</li>\n<li><strong>Rollback of a name:</strong> delete the <code>certificateRef</code> entry from the\nGateway (git revert); the Secret stays and keeps renewing unless the\nCertificate is reverted too.</li>\n<li><strong>Runbook:</strong> infra repo <code>kubernetes/cert-manager/README.md</code> (ownership,\ntoken provisioning, prod cutover gate).</li>\n</ul>\n<h2 id=\"bkmrk-estado-honesto-%28live\">Estado honesto (live, 2026-08-31)</h2>\n<p id=\"bkmrk-deployed.-live-since\"><strong>DEPLOYED.</strong> Live since the F3 apply: controller/webhook/cainjector\nRunning, both issuers configured. Certificates live-verified 2026-08-31 \u2014\n4/4 <code>Ready=True</code>: <code>edge-test-tls</code> (staging), <code>argo-tls</code>, <code>docs-tls</code>,\n<code>vault-tls</code> (LE PROD). Renewals run automatically (renewBefore default\n30 days for 90-day ACME certs).</p>\n<h2 id=\"bkmrk-relay-links-%28enlaces\">Relay links (enlaces de relevo)</h2>\n<ul id=\"bkmrk-manifests-%28github%29%3A-\">\n<li><strong>Manifests (GitHub):</strong> <a href=\"https://github.com/wetopi-platform/infrastructure/tree/main/kubernetes/cert-manager\">kubernetes/cert-manager</a> (issuers, certificates, values, README runbook), <a href=\"https://github.com/wetopi-platform/infrastructure/blob/main/kubernetes/platform/cert-manager.yaml\">kubernetes/platform/cert-manager.yaml</a> (the Argo Application)</li>\n<li><strong>Vault (https://vault.wetopi.net \u2192 project <code>wetopi-harness</code>):</strong> no\ncertificate material lives in the Vault; the ACME account keys\n(<code>letsencrypt-*-account-key</code> Secrets) and the Cloudflare token are\noperator-managed (ops01 + cert-manager namespace Secrets) \u2014 migration\ninto the Vault is part of ADR-0009's long-term picture, not yet done.</li>\n<li><strong>Related docs:</strong> <a href=\"https://book.wetopi.net/decisions/ADR-0006-edge-v1-wan-exception.md\">ADR-0006</a> (edge + LE posture), <a href=\"https://book.wetopi.net/index.md\">services index</a>.</li>\n</ul>\n"
 },
 {
  "id": 65,
  "title": "SVC-DOCS-SITE \u2014 docs.wetopi.net",
  "slug": "svc-docs-site-docswetopinet",
  "book_id": 3,
  "chapter_id": 21,
  "description": null,
  "created_at": "2026-09-06T13:39:06.000000Z",
  "updated_at": "2026-09-06T21:45:54.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (services/docs-site.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n Docs site (docs.wetopi.net)\n\n## What is it?\n\nThe public (operator-authenticated) home of Wetopi's institutional\ntechnical knowledge \u2014 this documentation. It is a static MkDocs build of\n`wetopi-platform/docs` packed into an nginx image and served behind the\nedge; it is also the platform's first end-to-end Git \u2192 CI \u2192 image \u2192\nreviewed promotion \u2192 GitOps deployment chain (\"la \u00faltima build NUNCA se\nauto-despliega\").\n\n## How is it mounted?\n\n- **Namespace:** `docs-site` (Git-declared, Argo-created:\n  `CreateNamespace=true`).\n- **Deployment `docs-site`:** 2 replicas (the edge v1 node side is already\n  documented as reduced availability \u2014 the workload must not add a\n  single-pod failure mode). Image\n  `ghcr.io/wetopi-platform/docs-site@sha256:5c67cab5\u202603b23`\n  (digest-pinned promotion, PR #23 = docs main `7e0d0050`, CI run\n  33349505431) \u2014 **no mutable tags anywhere**. Non-root uid 101 (the image's\n  nginx user), caps ALL dropped, read-only rootfs, one writable emptyDir at\n  `/var/cache/nginx`, `imagePullSecrets: ghcr-pull-secret` (pod-scoped; the\n  GHCR package is PRIVATE \u2014 anonymous pull returns 401, live-verified).\n- **Service:** `docs-site:80` (ClusterIP) \u2192 targetPort 8080.\n- **Edge:** HTTPRoute `docs-site` (ns `docs-site`) \u2192 Gateway `wetopi-edge`\n  https listener (cross-namespace, `from: All`), with Basic-Auth\n  Middleware `docs-basic-auth` FIRST (htpasswd, realm `wetopi-docs`);\n  `http://docs.wetopi.net` 308-redirects to HTTPS (Gateway `web`\n  listener). TLS: Certificate `docs-tls` (LE PROD DNS-01, kube-system) on\n  the Gateway listener.\n- **Secrets (references, never values):** `docs-site-basic-auth`\n  (htpasswd \u2014 operator-created on the cluster, MUST exist before the first\n  sync; shape `secret.example.yaml`) and `ghcr-pull-secret` (GHCR pull\n  credential \u2014 shape `secret.ghcr-pull.example.yaml`).\n\n## Diagram\n\n```mermaid\nflowchart TD\n  REPO[\"GitHub wetopi-platform/docs (main + PRs)\"]\n  CI[\"docs CI: mkdocs build --strict -> nginx image -> ghcr.io/.../docs-site:sha-<commit>\"]\n  PROMO[\"human-reviewed PR in infrastructure: pin @sha256:<digest>\"]\n  subgraph K8S[\"cluster wetopi3\"]\n    ARGODEPS[\"Application docs-site (anchored in kubernetes/platform/, prune+selfHeal)\"]\n    subgraph NS[\"namespace docs-site\"]\n      DEP[\"Deployment docs-site x2 (nginx uid 101, digest-pinned)\"]\n      SVC[\"Service docs-site:80\"]\n      MW[\"Middleware docs-basic-auth (htpasswd)\"]\n      RT[\"HTTPRoute docs-site -> Gateway \u00a7https\"]\n    end\n    GW[\"Gateway wetopi-edge (kube-system) + cert docs-tls\"]\n  end\n  USER[\"operator: https://docs.wetopi.net (basic auth 401 realm wetopi-docs, INTENCIONAL)\"]\n  REPO --> CI --> PROMO --> ARGODEPS --> DEP\n  DEP --> SVC\n  USER --> GW --> RT --> MW --> SVC\n```\n\n## How is it deployed?\n\nPromotion-only chain (no auto-deploy of the latest build):\n1. **Build:** docs CI (`.github/workflows/docs-build.yml`) runs\n   `mkdocs build --strict` on main pushes AND PRs, then packs `site/` into\n   the nginx image and pushes `ghcr.io/wetopi-platform/docs-site:sha-<commit>`\n   (main pushes only; `GITHUB_TOKEN`, `packages: write`; CI NEVER touches\n   the cluster).\n2. **Promote:** a human-reviewed PR to `infrastructure` swaps the digest in\n   `kubernetes/docs-site/deployment.yaml` to the next reviewed\n   `@sha256:<digest>` (digest, not tag \u2014 a digest cannot be re-pushed).\n3. **Sync:** the Argo CD Application **`docs-site`**\n   (anchored in `kubernetes/platform/docs-site.yaml`; include-only filter\n   over the six workload manifests; `prune + selfHeal`; cascading-deletion\n   finalizer) deploys exactly the reviewed digest. One-time bootstrap from\n   ops01 (runbook: `kubernetes/docs-site/README.md`): create\n   `docs-site-basic-auth` + `ghcr-pull-secret` BEFORE the first sync.\n\n## Dependencies\n\n**Depends on:** the docs repo CI + GHCR (private package \u2192 pull Secret);\nthe edge Gateway + its `docs-tls` certificate ([SVC-TRAEFIK-EDGE](traefik-edge.md),\n[SVC-CERT-MANAGER](cert-manager.md)); DNS A records `docs.wetopi.net` \u2192\nedge IPs .193/.194 (specific A records beat the `*.wetopi.net` wildcard \u2192\n.76 in resolution order); the root Argo Application\n([SVC-ARGOCD](argocd.md)) for lifecycle.\n\n**Depended on by:** humans and agents retrieving institutional knowledge;\nthe docs runtime rebuild (OBJ-DOCSRUNTIME-001) is migrating the build to a\nGitHub-webhook-driven flow \u2014 the promotion chain and this deployment shape\nstay.\n\n## Operation\n\n- **Access:** `https://docs.wetopi.net` \u2014 HTTP 401 with realm\n  `wetopi-docs` WITHOUT credentials is **intentional** (ADR-0006: the v1\n  window publishes operator-only services behind basic auth; this is NOT a\n  broken site). With credentials: MkDocs site + search.\n- **Health:** `kubectl -n docs-site get pods` (2/2 Ready),\n  `HTTPRoute docs-site` `Accepted=True` with parent resolved to\n  `kube-system/wetopi-edge`; edge probe `curl -I https://docs.wetopi.net`\n  \u2192 401 (auth challenge = healthy), 200 with credentials.\n- **Image promotion flow:** `kubernetes/docs-site/README.md` (infra repo)\n  \u2014 build, verify the pushed digest, review the pin PR.\n\n## Estado honesto (live, 2026-08-31)\n\n**DEPLOYED.** The first real end-to-end chain is live: promoted digest\n(`sha256:5c67cab5\u202603b23` = docs main `7e0d0050`) deployed by Argo,\n2/2 pods Ready, `docs.wetopi.net` serving the LE PROD chain after the DNS\ncutover (APPLY `t_dde33585` AC5), basic auth active by design. **In\nflight:** OBJ-DOCSRUNTIME-001 (docs runtime rebuild via GitHub webhook,\ninfra PR #28 \u2014 pinning decision pending) will change HOW the image is\nbuilt/served; the current digest-pinned promotion chain above remains the\ndeployed reality until that lands.\n\n## Relay links (enlaces de relevo)\n\n- **Manifests (GitHub):** [kubernetes/docs-site](https://github.com/wetopi-platform/infrastructure/tree/main/kubernetes/docs-site) (Deployment, Service, HTTPRoute, Middleware, Certificate, Argo app anchor, README runbook), docs CI: [docs/.github/workflows/docs-build.yml](https://github.com/wetopi-platform/docs/blob/main/.github/workflows/docs-build.yml)\n- **Vault (https://vault.wetopi.net \u2192 project `wetopi-harness`):** the\n  `docs-site-basic-auth` htpasswd and the `ghcr-pull-secret` are\n  operator-managed cluster Secrets today; the Vault (ADR-0009) is their\n  long-term home \u2014 no values in this repo.\n- **Related docs:** [ADR-0006](../decisions/ADR-0006-edge-v1-wan-exception.md) (v1 window + basic-auth posture), [OBJ-EDGE-DOCS-001](../objectives/active/OBJ-EDGE-DOCS-001.md) (publish docs.wetopi.net), [OBJ-DOCSRUNTIME-001](../objectives/active/OBJ-DOCSRUNTIME-001.md) (runtime rebuild in flight), [services index](index.md).",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (services/docs-site.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-docs-site-%28docs.weto\">Docs site (docs.wetopi.net)</p>\n<h2 id=\"bkmrk-what-is-it%3F\">What is it?</h2>\n<p id=\"bkmrk-the-public-%28operator\">The public (operator-authenticated) home of Wetopi's institutional\ntechnical knowledge \u2014 this documentation. It is a static MkDocs build of\n<code>wetopi-platform/docs</code> packed into an nginx image and served behind the\nedge; it is also the platform's first end-to-end Git \u2192 CI \u2192 image \u2192\nreviewed promotion \u2192 GitOps deployment chain (\"la \u00faltima build NUNCA se\nauto-despliega\").</p>\n<h2 id=\"bkmrk-how-is-it-mounted%3F\">How is it mounted?</h2>\n<ul id=\"bkmrk-namespace%3A-docs-site\">\n<li><strong>Namespace:</strong> <code>docs-site</code> (Git-declared, Argo-created:\n<code>CreateNamespace=true</code>).</li>\n<li><strong>Deployment <code>docs-site</code>:</strong> 2 replicas (the edge v1 node side is already\ndocumented as reduced availability \u2014 the workload must not add a\nsingle-pod failure mode). Image\n<code>ghcr.io/wetopi-platform/docs-site@sha256:5c67cab5\u202603b23</code>\n(digest-pinned promotion, PR #23 = docs main <code>7e0d0050</code>, CI run\n33349505431) \u2014 <strong>no mutable tags anywhere</strong>. Non-root uid 101 (the image's\nnginx user), caps ALL dropped, read-only rootfs, one writable emptyDir at\n<code>/var/cache/nginx</code>, <code>imagePullSecrets: ghcr-pull-secret</code> (pod-scoped; the\nGHCR package is PRIVATE \u2014 anonymous pull returns 401, live-verified).</li>\n<li><strong>Service:</strong> <code>docs-site:80</code> (ClusterIP) \u2192 targetPort 8080.</li>\n<li><strong>Edge:</strong> HTTPRoute <code>docs-site</code> (ns <code>docs-site</code>) \u2192 Gateway <code>wetopi-edge</code>\nhttps listener (cross-namespace, <code>from: All</code>), with Basic-Auth\nMiddleware <code>docs-basic-auth</code> FIRST (htpasswd, realm <code>wetopi-docs</code>);\n<code>http://docs.wetopi.net</code> 308-redirects to HTTPS (Gateway <code>web</code>\nlistener). TLS: Certificate <code>docs-tls</code> (LE PROD DNS-01, kube-system) on\nthe Gateway listener.</li>\n<li><strong>Secrets (references, never values):</strong> <code>docs-site-basic-auth</code>\n(htpasswd \u2014 operator-created on the cluster, MUST exist before the first\nsync; shape <code>secret.example.yaml</code>) and <code>ghcr-pull-secret</code> (GHCR pull\ncredential \u2014 shape <code>secret.ghcr-pull.example.yaml</code>).</li>\n</ul>\n<h2 id=\"bkmrk-diagram\">Diagram</h2>\n<pre id=\"bkmrk-flowchart-td-repo%5B%22g\"><code class=\"language-mermaid\">flowchart TD\n  REPO[\"GitHub wetopi-platform/docs (main + PRs)\"]\n  CI[\"docs CI: mkdocs build --strict -&gt; nginx image -&gt; ghcr.io/.../docs-site:sha-&lt;commit&gt;\"]\n  PROMO[\"human-reviewed PR in infrastructure: pin @sha256:&lt;digest&gt;\"]\n  subgraph K8S[\"cluster wetopi3\"]\n    ARGODEPS[\"Application docs-site (anchored in kubernetes/platform/, prune+selfHeal)\"]\n    subgraph NS[\"namespace docs-site\"]\n      DEP[\"Deployment docs-site x2 (nginx uid 101, digest-pinned)\"]\n      SVC[\"Service docs-site:80\"]\n      MW[\"Middleware docs-basic-auth (htpasswd)\"]\n      RT[\"HTTPRoute docs-site -&gt; Gateway \u00a7https\"]\n    end\n    GW[\"Gateway wetopi-edge (kube-system) + cert docs-tls\"]\n  end\n  USER[\"operator: https://docs.wetopi.net (basic auth 401 realm wetopi-docs, INTENCIONAL)\"]\n  REPO --&gt; CI --&gt; PROMO --&gt; ARGODEPS --&gt; DEP\n  DEP --&gt; SVC\n  USER --&gt; GW --&gt; RT --&gt; MW --&gt; SVC\n</code></pre>\n<h2 id=\"bkmrk-how-is-it-deployed%3F\">How is it deployed?</h2>\n<p id=\"bkmrk-promotion-only-chain\">Promotion-only chain (no auto-deploy of the latest build):</p>\n<ol id=\"bkmrk-build%3A-docs-ci-%28.git\">\n<li><strong>Build:</strong> docs CI (<code>.github/workflows/docs-build.yml</code>) runs\n<code>mkdocs build --strict</code> on main pushes AND PRs, then packs <code>site/</code> into\nthe nginx image and pushes <code>ghcr.io/wetopi-platform/docs-site:sha-&lt;commit&gt;</code>\n(main pushes only; <code>GITHUB_TOKEN</code>, <code>packages: write</code>; CI NEVER touches\nthe cluster).</li>\n<li><strong>Promote:</strong> a human-reviewed PR to <code>infrastructure</code> swaps the digest in\n<code>kubernetes/docs-site/deployment.yaml</code> to the next reviewed\n<code>@sha256:&lt;digest&gt;</code> (digest, not tag \u2014 a digest cannot be re-pushed).</li>\n<li><strong>Sync:</strong> the Argo CD Application <strong><code>docs-site</code></strong>\n(anchored in <code>kubernetes/platform/docs-site.yaml</code>; include-only filter\nover the six workload manifests; <code>prune + selfHeal</code>; cascading-deletion\nfinalizer) deploys exactly the reviewed digest. One-time bootstrap from\nops01 (runbook: <code>kubernetes/docs-site/README.md</code>): create\n<code>docs-site-basic-auth</code> + <code>ghcr-pull-secret</code> BEFORE the first sync.</li>\n</ol>\n<h2 id=\"bkmrk-dependencies\">Dependencies</h2>\n<p id=\"bkmrk-depends-on%3A-the-docs\"><strong>Depends on:</strong> the docs repo CI + GHCR (private package \u2192 pull Secret);\nthe edge Gateway + its <code>docs-tls</code> certificate (<a href=\"https://book.wetopi.net/traefik-edge.md\">SVC-TRAEFIK-EDGE</a>,\n<a href=\"https://book.wetopi.net/cert-manager.md\">SVC-CERT-MANAGER</a>); DNS A records <code>docs.wetopi.net</code> \u2192\nedge IPs .193/.194 (specific A records beat the <code>*.wetopi.net</code> wildcard \u2192\n.76 in resolution order); the root Argo Application\n(<a href=\"https://book.wetopi.net/argocd.md\">SVC-ARGOCD</a>) for lifecycle.</p>\n<p id=\"bkmrk-depended-on-by%3A-huma\"><strong>Depended on by:</strong> humans and agents retrieving institutional knowledge;\nthe docs runtime rebuild (OBJ-DOCSRUNTIME-001) is migrating the build to a\nGitHub-webhook-driven flow \u2014 the promotion chain and this deployment shape\nstay.</p>\n<h2 id=\"bkmrk-operation\">Operation</h2>\n<ul id=\"bkmrk-access%3A-https%3A%2F%2Fdocs\">\n<li><strong>Access:</strong> <code>https://docs.wetopi.net</code> \u2014 HTTP 401 with realm\n<code>wetopi-docs</code> WITHOUT credentials is <strong>intentional</strong> (ADR-0006: the v1\nwindow publishes operator-only services behind basic auth; this is NOT a\nbroken site). With credentials: MkDocs site + search.</li>\n<li><strong>Health:</strong> <code>kubectl -n docs-site get pods</code> (2/2 Ready),\n<code>HTTPRoute docs-site</code> <code>Accepted=True</code> with parent resolved to\n<code>kube-system/wetopi-edge</code>; edge probe <code>curl -I https://docs.wetopi.net</code>\n\u2192 401 (auth challenge = healthy), 200 with credentials.</li>\n<li><strong>Image promotion flow:</strong> <code>kubernetes/docs-site/README.md</code> (infra repo)\n\u2014 build, verify the pushed digest, review the pin PR.</li>\n</ul>\n<h2 id=\"bkmrk-estado-honesto-%28live\">Estado honesto (live, 2026-08-31)</h2>\n<p id=\"bkmrk-deployed.-the-first-\"><strong>DEPLOYED.</strong> The first real end-to-end chain is live: promoted digest\n(<code>sha256:5c67cab5\u202603b23</code> = docs main <code>7e0d0050</code>) deployed by Argo,\n2/2 pods Ready, <code>docs.wetopi.net</code> serving the LE PROD chain after the DNS\ncutover (APPLY <code>t_dde33585</code> AC5), basic auth active by design. <strong>In\nflight:</strong> OBJ-DOCSRUNTIME-001 (docs runtime rebuild via GitHub webhook,\ninfra PR #28 \u2014 pinning decision pending) will change HOW the image is\nbuilt/served; the current digest-pinned promotion chain above remains the\ndeployed reality until that lands.</p>\n<h2 id=\"bkmrk-relay-links-%28enlaces\">Relay links (enlaces de relevo)</h2>\n<ul id=\"bkmrk-manifests-%28github%29%3A-\">\n<li><strong>Manifests (GitHub):</strong> <a href=\"https://github.com/wetopi-platform/infrastructure/tree/main/kubernetes/docs-site\">kubernetes/docs-site</a> (Deployment, Service, HTTPRoute, Middleware, Certificate, Argo app anchor, README runbook), docs CI: <a href=\"https://github.com/wetopi-platform/docs/blob/main/.github/workflows/docs-build.yml\">docs/.github/workflows/docs-build.yml</a></li>\n<li><strong>Vault (https://vault.wetopi.net \u2192 project <code>wetopi-harness</code>):</strong> the\n<code>docs-site-basic-auth</code> htpasswd and the <code>ghcr-pull-secret</code> are\noperator-managed cluster Secrets today; the Vault (ADR-0009) is their\nlong-term home \u2014 no values in this repo.</li>\n<li><strong>Related docs:</strong> <a href=\"https://book.wetopi.net/decisions/ADR-0006-edge-v1-wan-exception.md\">ADR-0006</a> (v1 window + basic-auth posture), <a href=\"https://book.wetopi.net/objectives/active/OBJ-EDGE-DOCS-001.md\">OBJ-EDGE-DOCS-001</a> (publish docs.wetopi.net), <a href=\"https://book.wetopi.net/objectives/active/OBJ-DOCSRUNTIME-001.md\">OBJ-DOCSRUNTIME-001</a> (runtime rebuild in flight), <a href=\"https://book.wetopi.net/index.md\">services index</a>.</li>\n</ul>\n"
 },
 {
  "id": 66,
  "title": "SVC-LITELLM-LEGACY \u2014 ficha migrada (ver SVC-LITELLM live)",
  "slug": "svc-litellm-legacy-ficha-migrada-ver-svc-litellm-live",
  "book_id": 3,
  "chapter_id": 21,
  "description": null,
  "created_at": "2026-09-06T13:39:06.000000Z",
  "updated_at": "2026-09-06T21:45:54.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (services/litellm.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n LiteLLM inference proxy (ns litellm)\n\n## What is it?\n\nLiteLLM is the platform's LLM inference proxy. Clients call an\nOpenAI-compatible endpoint at **https://infer.wetopi.net** and the proxy\nforwards every request to a single upstream: **https://llm.wetopi.net/v1**\n(same API key as the `infer01` deployment). The proxy runs LiteLLM in\n**full mode** \u2014 with a Postgres backend for virtual keys, budgets and usage\ntracking \u2014 *not* the simple no-DB mode.\n\nTopology: `client \u2192 edge Gateway (infer.wetopi.net) \u2192 ns litellm (LiteLLM)\n\u2192 llm.wetopi.net/v1`.\n\nAccess model (operator directive 2026-09-01, twin of the vault route \u2014\n[ADR-0014](../decisions/ADR-0014-edge-source-ip-allowlist.md)): the edge\ngate is a **source-IP allowlist** (`ipAllowList` middleware, range\n`51.254.106.87/32` = operator VPN egress). Only sources inside that range\nreach LiteLLM; everything else is rejected at the edge with `403` and never\nreaches the app. Behind that network gate sits LiteLLM's own auth: a\n**master key** for administration and **virtual keys** (per-client) for API\ntraffic. There is **no HTTP Basic Auth** anywhere on the route.\n\n## How is it mounted?\n\nAll objects are Git-owned in the **`platform-litellm`** repo (per-service\nrepo; desired state is the source of truth \u2014 this page only explains it).\n\n- **Namespace `litellm`** \u2014 dedicated; holds both the proxy and its\n  Postgres (verified topology decision).\n- **Deployment `litellm`** \u2014 `ghcr.io/berriai/litellm:v1.99.0` (tag-pinned,\n  never a floating tag), 1 replica, strategy `Recreate`. Requests\n  250m CPU / **512Mi** memory, limit **2Gi** memory (sized after the T12\n  go-live OOMKill at 1Gi). Config via `CONFIG_FILE_PATH=/app/config/config.yaml`\n  (ConfigMap `litellm-config`). Secrets resolved at runtime from env\n  (`os.environ/\u2026`) sourced by the `litellm-auth` Secret \u2014 **no secret value\n  is in this file**. Health endpoints `/health/liveness` +\n  `/health/readiness` (both unauthenticated) drive the startup/liveness/\n  readiness probes.\n- **Deployment `postgres`** \u2014 `postgres:16`, 1 replica, strategy `Recreate`\n  (RWO single-writer). Limits 1Gi / requests 256Mi. Probes are\n  `pg_isready -U litellm -d litellm` (auth-gated check, so the probe verifies\n  what the app actually needs). Data on PVC `postgres-data` **10Gi**,\n  StorageClass `ceph-rbd` \u2192 depends on Ceph CSI.\n- **Services** \u2014 `litellm` (ClusterIP :4000) and `postgres` (ClusterIP\n  :5432). In-cluster address: `litellm.litellm.svc.cluster.local:4000`.\n- **Edge** \u2014 HTTPRoute `litellm` (ns `litellm`) \u2192 Gateway\n  `kube-system/wetopi-edge` `https` listener; host `infer.wetopi.net`; a\n  single `PathPrefix /` rule carrying the `allowlist-middleware-litellm`\n  ExtensionRef filter and the single backendRef `litellm:4000`. TLS\n  terminates at the Gateway (the route is TLS-free); Certificate\n  `kube-system/infer-tls` (LE PROD, DNS-01) backs the https listener via an\n  `infer-tls` certificateRef.\n- **Middleware `allowlist-middleware-litellm`** (ns `litellm`) \u2014 Traefik\n  `ipAllowList`, `sourceRange: 51.254.106.87/32`. It ships in the SAME\n  directory as the HTTPRoute (Law 4 \u2014 a desynced ExtensionRef would\n  fail-closed with `500` while Argo shows Healthy).\n- **Secrets (references only, never values \u2014 apply-time only):**\n  - `services/litellm/litellm` \u2014 `LITELLM_MASTER_KEY`, `LITELLM_SALT_KEY`,\n    `LLM_WETOPI_API_KEY`\n  - `services/litellm/postgres` \u2014 `POSTGRES_LITELLM_PASSWORD`\n  - `services/external/llm-wetopi` \u2014 `LLM_WETOPI_API_KEY` (upstream key),\n    `LLM_WETOPI_URI`\n  - k8s Secrets `litellm-auth` (4 keys, incl. `DATABASE_URL` built in\n    memory from the Postgres password) and `postgres-auth` (3 keys) are\n    created at **apply-time only** from Infisical. Only\n    `secret.example.yaml` (placeholders) lives in Git.\n\n## Diagram\n\n```mermaid\nflowchart TD\n  OP[\"allowlisted client (51.254.106.87/32 \u2014 operator VPN egress)\"]\n  OTH[\"any other source\"]\n  subgraph CL[\"cluster wetopi3\"]\n    subgraph NSL[\"namespace litellm\"]\n      APP[\"Deployment litellm \u2014 LiteLLM v1.99.0 (full mode)\"]\n      PG[\"PostgreSQL 16 \u2014 PVC 10Gi (ceph-rbd)\"]\n      SVC[\"Service litellm :4000\"]\n      MW[\"Middleware allowlist-middleware-litellm (ipAllowList)\"]\n    end\n    GW[\"Gateway wetopi-edge (kube-system) + cert infer-tls (LE PROD)\"]\n    SC[\"StorageClass ceph-rbd (Ceph CSI)\"]\n  end\n  UP[\"upstream llm.wetopi.net/v1 (OpenAI-compatible)\"]\n  OP -->|\"https://infer.wetopi.net\"| GW -->|\"http (PathPrefix /) + ipAllowList\"| SVC\n  OTH -.->|\"rejected at edge: 403\"| GW\n  SVC --> APP\n  APP -->|\"database_url / virtual keys / usage\"| PG\n  APP -->|\"forward (model_name wetopi-llm)\"| UP\n  PG --> SC\n```\n\n## How is it deployed?\n\nGitOps, repo-per-service. Two Argo CD **anchor Applications** in\n`platform-litellm/kubernetes/argo-anchor/` sync the repo's manifests under\nthe root app:\n\n- **`litellm`** \u2014 sources `kubernetes/litellm/`, include-only glob\n  `{configmap,deployment,service,httproute,middleware-allowlist}` (the\n  `secret.example.yaml` is excluded on purpose \u2014 applying the placeholder\n  would clobber the real apply-time `litellm-auth` Secret; Argo would stay\n  green, i.e. silent).\n- **`postgres`** \u2014 sources `kubernetes/postgres/`, include-only glob\n  `{namespace,pvc,deployment,service}` (same example-secret exclusion).\n\nBootstrap sync policy is **`prune: false, selfHeal: false`** (RC4/Law 3 \u2014\nmanual sync during bootstrap, no automatic prune). Both apps are\n**manually synced** via\n`kubectl -n argocd patch application <app> --type merge -p\n'{\"operation\":{\"sync\":{\"revisions\":[\"main\"]}}}'` (order: `postgres` then\n`litellm`). The first full apply of the whole service (namespace, k8s\nSecrets from Infisical, the 9 manifests, AppProject + repo credential\nadmission, Argo anchors, the `infer-tls` Certificate wait, the Gateway\ndelta, and readiness verification) is performed by the apply-block script\nexecuted at T12 go-live \u2014 see *Estado honesto* for the run that went green.\n\n## Dependencies\n\n**Depends on:** Ceph CSI / StorageClass `ceph-rbd` for the Postgres PVC\n([SVC-CEPH-CSI](ceph-csi.md)); the edge Gateway `wetopi-edge` + the\nnftables 443\u21928443 redirect ([SVC-TRAEFIK-EDGE](traefik-edge.md));\ncert-manager for `infer-tls` ([SVC-CERT-MANAGER](cert-manager.md)); DNS A\nrecords `infer.wetopi.net` \u2192 edge IPs (tofu-owned, Cloudflare); Infisical\n([SVC-VAULT](vault.md)) for the apply-time Secrets; and the single upstream\n`llm.wetopi.net/v1` for actual inference.\n\n**Depended on by:** anything on the platform that needs a controlled,\nmetered OpenAI-compatible LLM endpoint (virtual keys per consumer, budgets\nand usage tracked in Postgres).\n\n## Operation\n\nkubectl is run on a control-plane node (no local kubectl); every command\nbelow is via `ssh rke-cp01 sudo /var/lib/rancher/rke2/bin/kubectl\n--kubeconfig /etc/rancher/rke2/rke2.yaml`.\n\n- **Health:** `kubectl -n litellm get pods` (both `1/1 Running`, restarts\n  low), `kubectl -n argocd get applications litellm postgres`\n  (`Synced`/`Healthy`), HTTPRoute `Accepted`+`ResolvedRefs`, Certificate\n  `infer-tls` `Ready`. In-cluster probe:\n  `http://litellm.litellm.svc.cluster.local:4000/health/liveness` \u2192 `200`.\n- **Master key vs virtual keys:** the **master key**\n  (`LITELLM_MASTER_KEY`) is an admin credential \u2014 it authenticates to the\n  proxy and to the key-management endpoints; it is not meant to be handed\n  out. **Virtual keys** are per-client API keys created by the proxy:\n  `POST /key/generate` (with the master key) returns a virtual key;\n  `POST /key/info` and `POST /key/delete` list and revoke them. Clients call\n  the API with a **virtual key** (`Authorization: Bearer <virtual key>`),\n  never with the master key.\n- **Runbook (minimal):**\n  - **Restart / rolling update the proxy:**\n    `kubectl -n litellm rollout restart deployment/litellm` (single\n    replica + `Recreate`, so it is a brief stop-the-world). Track with\n    `kubectl -n litellm rollout status deployment/litellm`.\n  - **Logs:** `kubectl -n litellm logs -l app.kubernetes.io/name=litellm`\n    (add `--tail=200 -f` / `-p` for previous container). Postgres:\n    `kubectl -n litellm logs -l app=postgres`.\n  - **Add a virtual key:** from inside the cluster (bypasses the edge\n    allowlist) call `POST /key/generate` on `:4000` with the master key as\n    `Authorization: Bearer <master key>` and a JSON body such as\n    `{\"key\": \"client-<name>\", \"max_budget\": \"50\", \"models\": [\"wetopi-llm\"]}`.\n    The response carries the new virtual key \u2014 distribute it to the client\n    and store only its identity, not the value, in this repo.\n  - **Add a new upstream:** edit the `model_list` in\n    `kubernetes/litellm/configmap.yaml` (a new `model_name` + `litellm_params`\n    with the upstream `api_base` and an `os.environ/<VAR>` key), add the new\n    secret key to the `litellm-auth` Secret from Infisical (apply-time),\n    and re-sync (manual Argo sync). Keep the single-valid-backendRef\n    invariant on the HTTPRoute.\n- **Rotating keys:**\n  - *Master key / Postgres password:* rotate the value in Infisical at its\n    path, then recreate the k8s Secret and roll the deployment\n    (re-apply `litellm-auth` / `postgres-auth` and\n    `rollout restart`). `DATABASE_URL` embeds the Postgres password.\n  - *Virtual keys:* revoke + reissue through the proxy\n    (`/key/delete` then `/key/generate`) \u2014 no cluster mutation, no\n    deployment roll.\n  - *Upstream key:* rotate `LLM_WETOPI_API_KEY` in Infisical, refresh the\n    `litellm-auth` Secret and roll `litellm`.\n  - *Salt key (`LITELLM_SALT_KEY`):* used to **hash** stored virtual keys \u2014\n    changing it invalidates every stored virtual key. Rotate only as a\n    deliberate break-the-keys operation, coordinated with the clients.\n\n## Estado honesto (live)\n\n**GO-LIVE 2026-09-02 (UTC). The proxy is deployed and serving the public\ncontract; the only unclosed item is the end-to-end upstream leg (network\nroute), not the proxy or the apply.**\n\n- **Go-live:** executed 2026-09-02. The authoritative run of the apply-block\n  (T12) finished with all phases `PASS` (`exit 0`) after the sizing fix was\n  merged to `platform-litellm` **main @ `f9d5afe41`** (PR #6 \u2014 litellm\n  `requests 512Mi` / `limits 2Gi`, which resolved the OOMKill at 1Gi seen in\n  the earlier go-live attempt). The service has been live at that revision\n  since; re-verified live **2026-09-02 07:01Z**.\n- **Live state (re-verified 2026-09-02 07:01Z):** pods `litellm-\u2026` and\n  `postgres-\u2026` `1/1 Running`, **restarts=0** (no OOM recurrence); Argo apps\n  `litellm` + `postgres` **Synced / Healthy @ `f9d5afe41`**; HTTPRoute\n  `litellm` `Accepted=True` + `ResolvedRefs=True`; Certificate `infer-tls`\n  `Ready=True` (\"Certificate is up to date and has not expired\").\n- **Public contract probes (evidenced at go-live):**\n\n  | Check | Result |\n  |---|---|\n  | DNS `infer.wetopi.net` | **PASS** \u2192 `79.137.114.193` + `79.137.114.194` (system + `@1.1.1.1` + `@9.9.9.9`); wildcard `*.wetopi.net` untouched |\n  | TLS cert | **PASS** \u2014 `CN=infer.wetopi.net`, issuer **Let's Encrypt (YR1)**, valid `2026-09-02 \u2192 2026-12-01`, `Verify=0`; both edge IPs serve it |\n  | In-cluster `/health/liveness` | **PASS** \u2192 `200` (\"I'm alive!\") |\n  | In-cluster `/health/readiness` | **PASS** \u2192 `200` |\n  | Public edge, non-allowlisted source | **`403`** (by design) \u2014 ops01 `92.222.245.91` and workstation `92.222.245.89` are outside `51.254.106.87/32`, so `/health/liveness` and `POST /v1` return `403` at the edge (TLS terminates before the 403) |\n  | `/v1/` without `Bearer` (in-cluster) | **`401`** (app auth) |\n  | `/v1/models` with master key (in-cluster) | **`200`** \u2192 model `wetopi-llm` (master key authenticates) |\n  | e2e `POST /v1/chat/completions` with master key | **`500` \u2014 BLOCKED-UPSTREAM (network)** \u2014 see below |\n\n- **Known limitations (honest):**\n  - **e2e chat completion is blocked by a network route, not by the proxy or\n    the credentials.** From the proxy's own pod,\n    `socket.create_connection((\"llm.wetopi.net\", 443))` \u2192\n    `OSError [Errno 113] No route to host`; `POST /v1/chat/completions` \u2192\n    `500 OpenAIException - Connection error`. The master key authenticates\n    fine (`/v1/models` \u2192 `200`), so this is the **cluster \u2192 llm.wetopi.net:443\n    route/egress**, an external-network item outside the apply/proxy. The same\n    key + model is LIVE at `infer01.hermeshaven.cloud:8000` (control: 200).\n    Tracked as a follow-up for the operator/infra (route/egress to\n    `llm.wetopi.net`). **The `200` happy path for a real completion is not yet\n    evidenced** until that route exists.\n  - **Positive allowlist probe (a `200` from an in-range `51.254.106.87/32`\n    source) was not measurable** from the available infra (workstation/ops01\n    egress is outside that range; no in-range source was available). The\n    *negative* probe (out-of-range \u2192 `403`) is evidenced from two sources and\n    is the correct negative. The `200`/`401` app contracts were verified\n    in-cluster instead.\n  - **Argo sync is manual** (`prune:false, selfHeal:false` bootstrap policy);\n    after a main change the apps must be synced manually (order\n    `postgres` \u2192 `litellm`). An initial `OutOfSync` is only the\n    `last-applied` kubectl annotation \u2014 specs == git, resolved by the manual\n    sync.\n\n## Relay links (enlaces de relevo)\n\n- **Manifests (GitHub):**\n  [platform-litellm/kubernetes/litellm](https://github.com/wetopi-platform/platform-litellm/tree/main/kubernetes/litellm),\n  [kubernetes/postgres](https://github.com/wetopi-platform/platform-litellm/tree/main/kubernetes/postgres),\n  [kubernetes/argo-anchor](https://github.com/wetopi-platform/platform-litellm/tree/main/kubernetes/argo-anchor)\n- **Edge + TLS (GitHub, infrastructure):**\n  [kubernetes/cert-manager/certificate-infer.yaml](https://github.com/wetopi-platform/infrastructure/blob/main/kubernetes/cert-manager/certificate-infer.yaml),\n  [kubernetes/edge/gateway-wetopi-edge.yaml](https://github.com/wetopi-platform/infrastructure/blob/main/kubernetes/edge/gateway-wetopi-edge.yaml)\n- **Secrets (paths only \u2014 DocOps \u00a78, never values):** Infisical project\n  `wetopi-harness` \u2192 `services/litellm/litellm`,\n  `services/litellm/postgres`, `services/external/llm-wetopi`.\n- **Related docs:** [ADR-0006](../decisions/ADR-0006-edge-v1-wan-exception.md)\n  (edge exposure), [ADR-0009](../decisions/ADR-0009-secrets-vault-harness.md)\n  (the secret store), [ADR-0014](../decisions/ADR-0014-edge-source-ip-allowlist.md)\n  (source-IP allowlist + no basic-auth), [SVC-TRAEFIK-EDGE](traefik-edge.md),\n  [SVC-CERT-MANAGER](cert-manager.md), [SVC-CEPH-CSI](ceph-csi.md),\n  [SVC-VAULT](vault.md), [services index](index.md).",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (services/litellm.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-litellm-inference-pr\">LiteLLM inference proxy (ns litellm)</p>\n<h2 id=\"bkmrk-what-is-it%3F\">What is it?</h2>\n<p id=\"bkmrk-litellm-is-the-platf\">LiteLLM is the platform's LLM inference proxy. Clients call an\nOpenAI-compatible endpoint at <strong>https://infer.wetopi.net</strong> and the proxy\nforwards every request to a single upstream: <strong>https://llm.wetopi.net/v1</strong>\n(same API key as the <code>infer01</code> deployment). The proxy runs LiteLLM in\n<strong>full mode</strong> \u2014 with a Postgres backend for virtual keys, budgets and usage\ntracking \u2014 <em>not</em> the simple no-DB mode.</p>\n<p id=\"bkmrk-topology%3A-client-%E2%86%92-e\">Topology: <code>client \u2192 edge Gateway (infer.wetopi.net) \u2192 ns litellm (LiteLLM) \u2192 llm.wetopi.net/v1</code>.</p>\n<p id=\"bkmrk-access-model-%28operat\">Access model (operator directive 2026-09-01, twin of the vault route \u2014\n<a href=\"https://book.wetopi.net/decisions/ADR-0014-edge-source-ip-allowlist.md\">ADR-0014</a>): the edge\ngate is a <strong>source-IP allowlist</strong> (<code>ipAllowList</code> middleware, range\n<code>51.254.106.87/32</code> = operator VPN egress). Only sources inside that range\nreach LiteLLM; everything else is rejected at the edge with <code>403</code> and never\nreaches the app. Behind that network gate sits LiteLLM's own auth: a\n<strong>master key</strong> for administration and <strong>virtual keys</strong> (per-client) for API\ntraffic. There is <strong>no HTTP Basic Auth</strong> anywhere on the route.</p>\n<h2 id=\"bkmrk-how-is-it-mounted%3F\">How is it mounted?</h2>\n<p id=\"bkmrk-all-objects-are-git-\">All objects are Git-owned in the <strong><code>platform-litellm</code></strong> repo (per-service\nrepo; desired state is the source of truth \u2014 this page only explains it).</p>\n<ul id=\"bkmrk-namespace-litellm-%E2%80%94-\">\n<li><strong>Namespace <code>litellm</code></strong> \u2014 dedicated; holds both the proxy and its\nPostgres (verified topology decision).</li>\n<li><strong>Deployment <code>litellm</code></strong> \u2014 <code>ghcr.io/berriai/litellm:v1.99.0</code> (tag-pinned,\nnever a floating tag), 1 replica, strategy <code>Recreate</code>. Requests\n250m CPU / <strong>512Mi</strong> memory, limit <strong>2Gi</strong> memory (sized after the T12\ngo-live OOMKill at 1Gi). Config via <code>CONFIG_FILE_PATH=/app/config/config.yaml</code>\n(ConfigMap <code>litellm-config</code>). Secrets resolved at runtime from env\n(<code>os.environ/\u2026</code>) sourced by the <code>litellm-auth</code> Secret \u2014 <strong>no secret value\nis in this file</strong>. Health endpoints <code>/health/liveness</code> +\n<code>/health/readiness</code> (both unauthenticated) drive the startup/liveness/\nreadiness probes.</li>\n<li><strong>Deployment <code>postgres</code></strong> \u2014 <code>postgres:16</code>, 1 replica, strategy <code>Recreate</code>\n(RWO single-writer). Limits 1Gi / requests 256Mi. Probes are\n<code>pg_isready -U litellm -d litellm</code> (auth-gated check, so the probe verifies\nwhat the app actually needs). Data on PVC <code>postgres-data</code> <strong>10Gi</strong>,\nStorageClass <code>ceph-rbd</code> \u2192 depends on Ceph CSI.</li>\n<li><strong>Services</strong> \u2014 <code>litellm</code> (ClusterIP :4000) and <code>postgres</code> (ClusterIP\n:5432). In-cluster address: <code>litellm.litellm.svc.cluster.local:4000</code>.</li>\n<li><strong>Edge</strong> \u2014 HTTPRoute <code>litellm</code> (ns <code>litellm</code>) \u2192 Gateway\n<code>kube-system/wetopi-edge</code> <code>https</code> listener; host <code>infer.wetopi.net</code>; a\nsingle <code>PathPrefix /</code> rule carrying the <code>allowlist-middleware-litellm</code>\nExtensionRef filter and the single backendRef <code>litellm:4000</code>. TLS\nterminates at the Gateway (the route is TLS-free); Certificate\n<code>kube-system/infer-tls</code> (LE PROD, DNS-01) backs the https listener via an\n<code>infer-tls</code> certificateRef.</li>\n<li><strong>Middleware <code>allowlist-middleware-litellm</code></strong> (ns <code>litellm</code>) \u2014 Traefik\n<code>ipAllowList</code>, <code>sourceRange: 51.254.106.87/32</code>. It ships in the SAME\ndirectory as the HTTPRoute (Law 4 \u2014 a desynced ExtensionRef would\nfail-closed with <code>500</code> while Argo shows Healthy).</li>\n<li><strong>Secrets (references only, never values \u2014 apply-time only):</strong>\n<ul>\n<li><code>services/litellm/litellm</code> \u2014 <code>LITELLM_MASTER_KEY</code>, <code>LITELLM_SALT_KEY</code>,\n<code>LLM_WETOPI_API_KEY</code></li>\n<li><code>services/litellm/postgres</code> \u2014 <code>POSTGRES_LITELLM_PASSWORD</code></li>\n<li><code>services/external/llm-wetopi</code> \u2014 <code>LLM_WETOPI_API_KEY</code> (upstream key),\n<code>LLM_WETOPI_URI</code></li>\n<li>k8s Secrets <code>litellm-auth</code> (4 keys, incl. <code>DATABASE_URL</code> built in\nmemory from the Postgres password) and <code>postgres-auth</code> (3 keys) are\ncreated at <strong>apply-time only</strong> from Infisical. Only\n<code>secret.example.yaml</code> (placeholders) lives in Git.</li>\n</ul>\n</li>\n</ul>\n<h2 id=\"bkmrk-diagram\">Diagram</h2>\n<pre id=\"bkmrk-flowchart-td-op%5B%22all\"><code class=\"language-mermaid\">flowchart TD\n  OP[\"allowlisted client (51.254.106.87/32 \u2014 operator VPN egress)\"]\n  OTH[\"any other source\"]\n  subgraph CL[\"cluster wetopi3\"]\n    subgraph NSL[\"namespace litellm\"]\n      APP[\"Deployment litellm \u2014 LiteLLM v1.99.0 (full mode)\"]\n      PG[\"PostgreSQL 16 \u2014 PVC 10Gi (ceph-rbd)\"]\n      SVC[\"Service litellm :4000\"]\n      MW[\"Middleware allowlist-middleware-litellm (ipAllowList)\"]\n    end\n    GW[\"Gateway wetopi-edge (kube-system) + cert infer-tls (LE PROD)\"]\n    SC[\"StorageClass ceph-rbd (Ceph CSI)\"]\n  end\n  UP[\"upstream llm.wetopi.net/v1 (OpenAI-compatible)\"]\n  OP --&gt;|\"https://infer.wetopi.net\"| GW --&gt;|\"http (PathPrefix /) + ipAllowList\"| SVC\n  OTH -.-&gt;|\"rejected at edge: 403\"| GW\n  SVC --&gt; APP\n  APP --&gt;|\"database_url / virtual keys / usage\"| PG\n  APP --&gt;|\"forward (model_name wetopi-llm)\"| UP\n  PG --&gt; SC\n</code></pre>\n<h2 id=\"bkmrk-how-is-it-deployed%3F\">How is it deployed?</h2>\n<p id=\"bkmrk-gitops%2C-repo-per-ser\">GitOps, repo-per-service. Two Argo CD <strong>anchor Applications</strong> in\n<code>platform-litellm/kubernetes/argo-anchor/</code> sync the repo's manifests under\nthe root app:</p>\n<ul id=\"bkmrk-litellm-%E2%80%94-sources-ku\">\n<li><strong><code>litellm</code></strong> \u2014 sources <code>kubernetes/litellm/</code>, include-only glob\n<code>{configmap,deployment,service,httproute,middleware-allowlist}</code> (the\n<code>secret.example.yaml</code> is excluded on purpose \u2014 applying the placeholder\nwould clobber the real apply-time <code>litellm-auth</code> Secret; Argo would stay\ngreen, i.e. silent).</li>\n<li><strong><code>postgres</code></strong> \u2014 sources <code>kubernetes/postgres/</code>, include-only glob\n<code>{namespace,pvc,deployment,service}</code> (same example-secret exclusion).</li>\n</ul>\n<p id=\"bkmrk-bootstrap-sync-polic\">Bootstrap sync policy is <strong><code>prune: false, selfHeal: false</code></strong> (RC4/Law 3 \u2014\nmanual sync during bootstrap, no automatic prune). Both apps are\n<strong>manually synced</strong> via\n<code>kubectl -n argocd patch application &lt;app&gt; --type merge -p '{\"operation\":{\"sync\":{\"revisions\":[\"main\"]}}}'</code> (order: <code>postgres</code> then\n<code>litellm</code>). The first full apply of the whole service (namespace, k8s\nSecrets from Infisical, the 9 manifests, AppProject + repo credential\nadmission, Argo anchors, the <code>infer-tls</code> Certificate wait, the Gateway\ndelta, and readiness verification) is performed by the apply-block script\nexecuted at T12 go-live \u2014 see <em>Estado honesto</em> for the run that went green.</p>\n<h2 id=\"bkmrk-dependencies\">Dependencies</h2>\n<p id=\"bkmrk-depends-on%3A-ceph-csi\"><strong>Depends on:</strong> Ceph CSI / StorageClass <code>ceph-rbd</code> for the Postgres PVC\n(<a href=\"https://book.wetopi.net/ceph-csi.md\">SVC-CEPH-CSI</a>); the edge Gateway <code>wetopi-edge</code> + the\nnftables 443\u21928443 redirect (<a href=\"https://book.wetopi.net/traefik-edge.md\">SVC-TRAEFIK-EDGE</a>);\ncert-manager for <code>infer-tls</code> (<a href=\"https://book.wetopi.net/cert-manager.md\">SVC-CERT-MANAGER</a>); DNS A\nrecords <code>infer.wetopi.net</code> \u2192 edge IPs (tofu-owned, Cloudflare); Infisical\n(<a href=\"https://book.wetopi.net/vault.md\">SVC-VAULT</a>) for the apply-time Secrets; and the single upstream\n<code>llm.wetopi.net/v1</code> for actual inference.</p>\n<p id=\"bkmrk-depended-on-by%3A-anyt\"><strong>Depended on by:</strong> anything on the platform that needs a controlled,\nmetered OpenAI-compatible LLM endpoint (virtual keys per consumer, budgets\nand usage tracked in Postgres).</p>\n<h2 id=\"bkmrk-operation\">Operation</h2>\n<p id=\"bkmrk-kubectl-is-run-on-a-\">kubectl is run on a control-plane node (no local kubectl); every command\nbelow is via <code>ssh rke-cp01 sudo /var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml</code>.</p>\n<ul id=\"bkmrk-health%3A-kubectl--n-l\">\n<li><strong>Health:</strong> <code>kubectl -n litellm get pods</code> (both <code>1/1 Running</code>, restarts\nlow), <code>kubectl -n argocd get applications litellm postgres</code>\n(<code>Synced</code>/<code>Healthy</code>), HTTPRoute <code>Accepted</code>+<code>ResolvedRefs</code>, Certificate\n<code>infer-tls</code> <code>Ready</code>. In-cluster probe:\n<code>http://litellm.litellm.svc.cluster.local:4000/health/liveness</code> \u2192 <code>200</code>.</li>\n<li><strong>Master key vs virtual keys:</strong> the <strong>master key</strong>\n(<code>LITELLM_MASTER_KEY</code>) is an admin credential \u2014 it authenticates to the\nproxy and to the key-management endpoints; it is not meant to be handed\nout. <strong>Virtual keys</strong> are per-client API keys created by the proxy:\n<code>POST /key/generate</code> (with the master key) returns a virtual key;\n<code>POST /key/info</code> and <code>POST /key/delete</code> list and revoke them. Clients call\nthe API with a <strong>virtual key</strong> (<code>Authorization: Bearer &lt;virtual key&gt;</code>),\nnever with the master key.</li>\n<li><strong>Runbook (minimal):</strong>\n<ul>\n<li><strong>Restart / rolling update the proxy:</strong>\n<code>kubectl -n litellm rollout restart deployment/litellm</code> (single\nreplica + <code>Recreate</code>, so it is a brief stop-the-world). Track with\n<code>kubectl -n litellm rollout status deployment/litellm</code>.</li>\n<li><strong>Logs:</strong> <code>kubectl -n litellm logs -l app.kubernetes.io/name=litellm</code>\n(add <code>--tail=200 -f</code> / <code>-p</code> for previous container). Postgres:\n<code>kubectl -n litellm logs -l app=postgres</code>.</li>\n<li><strong>Add a virtual key:</strong> from inside the cluster (bypasses the edge\nallowlist) call <code>POST /key/generate</code> on <code>:4000</code> with the master key as\n<code>Authorization: Bearer &lt;master key&gt;</code> and a JSON body such as\n<code>{\"key\": \"client-&lt;name&gt;\", \"max_budget\": \"50\", \"models\": [\"wetopi-llm\"]}</code>.\nThe response carries the new virtual key \u2014 distribute it to the client\nand store only its identity, not the value, in this repo.</li>\n<li><strong>Add a new upstream:</strong> edit the <code>model_list</code> in\n<code>kubernetes/litellm/configmap.yaml</code> (a new <code>model_name</code> + <code>litellm_params</code>\nwith the upstream <code>api_base</code> and an <code>os.environ/&lt;VAR&gt;</code> key), add the new\nsecret key to the <code>litellm-auth</code> Secret from Infisical (apply-time),\nand re-sync (manual Argo sync). Keep the single-valid-backendRef\ninvariant on the HTTPRoute.</li>\n</ul>\n</li>\n<li><strong>Rotating keys:</strong>\n<ul>\n<li><em>Master key / Postgres password:</em> rotate the value in Infisical at its\npath, then recreate the k8s Secret and roll the deployment\n(re-apply <code>litellm-auth</code> / <code>postgres-auth</code> and\n<code>rollout restart</code>). <code>DATABASE_URL</code> embeds the Postgres password.</li>\n<li><em>Virtual keys:</em> revoke + reissue through the proxy\n(<code>/key/delete</code> then <code>/key/generate</code>) \u2014 no cluster mutation, no\ndeployment roll.</li>\n<li><em>Upstream key:</em> rotate <code>LLM_WETOPI_API_KEY</code> in Infisical, refresh the\n<code>litellm-auth</code> Secret and roll <code>litellm</code>.</li>\n<li><em>Salt key (<code>LITELLM_SALT_KEY</code>):</em> used to <strong>hash</strong> stored virtual keys \u2014\nchanging it invalidates every stored virtual key. Rotate only as a\ndeliberate break-the-keys operation, coordinated with the clients.</li>\n</ul>\n</li>\n</ul>\n<h2 id=\"bkmrk-estado-honesto-%28live\">Estado honesto (live)</h2>\n<p id=\"bkmrk-go-live-2026-09-02-%28\"><strong>GO-LIVE 2026-09-02 (UTC). The proxy is deployed and serving the public\ncontract; the only unclosed item is the end-to-end upstream leg (network\nroute), not the proxy or the apply.</strong></p>\n<ul id=\"bkmrk-go-live%3A-executed-20\">\n<li>\n<p><strong>Go-live:</strong> executed 2026-09-02. The authoritative run of the apply-block\n(T12) finished with all phases <code>PASS</code> (<code>exit 0</code>) after the sizing fix was\nmerged to <code>platform-litellm</code> <strong>main @ <code>f9d5afe41</code></strong> (PR #6 \u2014 litellm\n<code>requests 512Mi</code> / <code>limits 2Gi</code>, which resolved the OOMKill at 1Gi seen in\nthe earlier go-live attempt). The service has been live at that revision\nsince; re-verified live <strong>2026-09-02 07:01Z</strong>.</p>\n</li>\n<li>\n<p><strong>Live state (re-verified 2026-09-02 07:01Z):</strong> pods <code>litellm-\u2026</code> and\n<code>postgres-\u2026</code> <code>1/1 Running</code>, <strong>restarts=0</strong> (no OOM recurrence); Argo apps\n<code>litellm</code> + <code>postgres</code> <strong>Synced / Healthy @ <code>f9d5afe41</code></strong>; HTTPRoute\n<code>litellm</code> <code>Accepted=True</code> + <code>ResolvedRefs=True</code>; Certificate <code>infer-tls</code>\n<code>Ready=True</code> (\"Certificate is up to date and has not expired\").</p>\n</li>\n<li>\n<p><strong>Public contract probes (evidenced at go-live):</strong></p>\n<table>\n<thead>\n<tr>\n<th>Check</th>\n<th>Result</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>DNS <code>infer.wetopi.net</code></td>\n<td><strong>PASS</strong> \u2192 <code>79.137.114.193</code> + <code>79.137.114.194</code> (system + <code>@1.1.1.1</code> + <code>@9.9.9.9</code>); wildcard <code>*.wetopi.net</code> untouched</td>\n</tr>\n<tr>\n<td>TLS cert</td>\n<td><strong>PASS</strong> \u2014 <code>CN=infer.wetopi.net</code>, issuer <strong>Let's Encrypt (YR1)</strong>, valid <code>2026-09-02 \u2192 2026-12-01</code>, <code>Verify=0</code>; both edge IPs serve it</td>\n</tr>\n<tr>\n<td>In-cluster <code>/health/liveness</code></td>\n<td><strong>PASS</strong> \u2192 <code>200</code> (\"I'm alive!\")</td>\n</tr>\n<tr>\n<td>In-cluster <code>/health/readiness</code></td>\n<td><strong>PASS</strong> \u2192 <code>200</code></td>\n</tr>\n<tr>\n<td>Public edge, non-allowlisted source</td>\n<td><strong><code>403</code></strong> (by design) \u2014 ops01 <code>92.222.245.91</code> and workstation <code>92.222.245.89</code> are outside <code>51.254.106.87/32</code>, so <code>/health/liveness</code> and <code>POST /v1</code> return <code>403</code> at the edge (TLS terminates before the 403)</td>\n</tr>\n<tr>\n<td><code>/v1/</code> without <code>Bearer</code> (in-cluster)</td>\n<td><strong><code>401</code></strong> (app auth)</td>\n</tr>\n<tr>\n<td><code>/v1/models</code> with master key (in-cluster)</td>\n<td><strong><code>200</code></strong> \u2192 model <code>wetopi-llm</code> (master key authenticates)</td>\n</tr>\n<tr>\n<td>e2e <code>POST /v1/chat/completions</code> with master key</td>\n<td><strong><code>500</code> \u2014 BLOCKED-UPSTREAM (network)</strong> \u2014 see below</td>\n</tr>\n</tbody>\n</table>\n</li>\n<li>\n<p><strong>Known limitations (honest):</strong></p>\n<ul>\n<li><strong>e2e chat completion is blocked by a network route, not by the proxy or\nthe credentials.</strong> From the proxy's own pod,\n<code>socket.create_connection((\"llm.wetopi.net\", 443))</code> \u2192\n<code>OSError [Errno 113] No route to host</code>; <code>POST /v1/chat/completions</code> \u2192\n<code>500 OpenAIException - Connection error</code>. The master key authenticates\nfine (<code>/v1/models</code> \u2192 <code>200</code>), so this is the <strong>cluster \u2192 llm.wetopi.net:443\nroute/egress</strong>, an external-network item outside the apply/proxy. The same\nkey + model is LIVE at <code>infer01.hermeshaven.cloud:8000</code> (control: 200).\nTracked as a follow-up for the operator/infra (route/egress to\n<code>llm.wetopi.net</code>). <strong>The <code>200</code> happy path for a real completion is not yet\nevidenced</strong> until that route exists.</li>\n<li><strong>Positive allowlist probe (a <code>200</code> from an in-range <code>51.254.106.87/32</code>\nsource) was not measurable</strong> from the available infra (workstation/ops01\negress is outside that range; no in-range source was available). The\n<em>negative</em> probe (out-of-range \u2192 <code>403</code>) is evidenced from two sources and\nis the correct negative. The <code>200</code>/<code>401</code> app contracts were verified\nin-cluster instead.</li>\n<li><strong>Argo sync is manual</strong> (<code>prune:false, selfHeal:false</code> bootstrap policy);\nafter a main change the apps must be synced manually (order\n<code>postgres</code> \u2192 <code>litellm</code>). An initial <code>OutOfSync</code> is only the\n<code>last-applied</code> kubectl annotation \u2014 specs == git, resolved by the manual\nsync.</li>\n</ul>\n</li>\n</ul>\n<h2 id=\"bkmrk-relay-links-%28enlaces\">Relay links (enlaces de relevo)</h2>\n<ul id=\"bkmrk-manifests-%28github%29%3A-\">\n<li><strong>Manifests (GitHub):</strong>\n<a href=\"https://github.com/wetopi-platform/platform-litellm/tree/main/kubernetes/litellm\">platform-litellm/kubernetes/litellm</a>,\n<a href=\"https://github.com/wetopi-platform/platform-litellm/tree/main/kubernetes/postgres\">kubernetes/postgres</a>,\n<a href=\"https://github.com/wetopi-platform/platform-litellm/tree/main/kubernetes/argo-anchor\">kubernetes/argo-anchor</a></li>\n<li><strong>Edge + TLS (GitHub, infrastructure):</strong>\n<a href=\"https://github.com/wetopi-platform/infrastructure/blob/main/kubernetes/cert-manager/certificate-infer.yaml\">kubernetes/cert-manager/certificate-infer.yaml</a>,\n<a href=\"https://github.com/wetopi-platform/infrastructure/blob/main/kubernetes/edge/gateway-wetopi-edge.yaml\">kubernetes/edge/gateway-wetopi-edge.yaml</a></li>\n<li><strong>Secrets (paths only \u2014 DocOps \u00a78, never values):</strong> Infisical project\n<code>wetopi-harness</code> \u2192 <code>services/litellm/litellm</code>,\n<code>services/litellm/postgres</code>, <code>services/external/llm-wetopi</code>.</li>\n<li><strong>Related docs:</strong> <a href=\"https://book.wetopi.net/decisions/ADR-0006-edge-v1-wan-exception.md\">ADR-0006</a>\n(edge exposure), <a href=\"https://book.wetopi.net/decisions/ADR-0009-secrets-vault-harness.md\">ADR-0009</a>\n(the secret store), <a href=\"https://book.wetopi.net/decisions/ADR-0014-edge-source-ip-allowlist.md\">ADR-0014</a>\n(source-IP allowlist + no basic-auth), <a href=\"https://book.wetopi.net/traefik-edge.md\">SVC-TRAEFIK-EDGE</a>,\n<a href=\"https://book.wetopi.net/cert-manager.md\">SVC-CERT-MANAGER</a>, <a href=\"https://book.wetopi.net/ceph-csi.md\">SVC-CEPH-CSI</a>,\n<a href=\"https://book.wetopi.net/vault.md\">SVC-VAULT</a>, <a href=\"https://book.wetopi.net/index.md\">services index</a>.</li>\n</ul>\n"
 },
 {
  "id": 67,
  "title": "SVC-OBSERVABILITY \u2014 suite grafana/prometheus/osd",
  "slug": "svc-observability-suite-grafanaprometheusosd",
  "book_id": 3,
  "chapter_id": 21,
  "description": null,
  "created_at": "2026-09-06T13:39:07.000000Z",
  "updated_at": "2026-09-06T21:45:55.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (services/observability-suite.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n Observability suite (ns observability)\n\n## What is it?\n\nThe platform's full observability stack: one Git repo\n(`platform-observability`), one Argo anchor, one namespace\n(`observability`), and three pipelines \u2014\n\n- **Metrics** \u2014 node/kube/service exporters scraped by Prometheus\n  (15d in-server retention); alert rules feed Alertmanager.\n- **Logs** \u2014 an OTel Collector DaemonSet on every node tails\n  `/var/log/pods` (filelog + k8s_attributes enrichment) and ships OTLP/gRPC\n  to Data Prepper, which indexes into OpenSearch (14-day ISM retention on\n  `logs-*`).\n- **Traces** \u2014 apps export OTLP to the collector\n  (gRPC :4317 / HTTP :4318); a traces pipeline forwards them to Tempo\n  (14d retention), browsable in Grafana.\n\nPresentation: Grafana (metrics + logs + traces in one UI) and\n**cerebro.wetopi.net** (OpenSearch Dashboards \u2014 the log search entry point).\n\nGo-live: **2026-09-05** (F7.2 apply gate + F7.3 public-contract probes,\nall green \u2014 see *Estado honesto*).\n\n## Public hostnames, TLS and auth posture\n\nFour hostnames, all served by the WAN edge (Gateway `kube-system/wetopi-edge`,\nRKE2 Traefik, `https` listener) and all gated by ONE shared source-IP\nallowlist middleware before the app's own auth \u2014 no HTTP Basic Auth at the\nedge (same posture as litellm / vault / docs routes,\n[ADR-0014](../decisions/ADR-0014-edge-source-ip-allowlist.md)):\n\n| Hostname | Backend | App-level auth behind the IP gate |\n|---|---|---|\n| `grafana.wetopi.net` | `grafana:3000` | Grafana admin login (Infisical `services/observability/grafana`) |\n| `prometheus.wetopi.net` | `prometheus:9090` | none \u2014 the IP gate IS the auth (as argo/vault) |\n| `alertmanager.wetopi.net` | `alertmanager:9093` | none \u2014 the IP gate IS the auth |\n| `cerebro.wetopi.net` | `opensearch-dashboards:5601` | OpenSearch Dashboards login with the DFM credential (Infisical `services/observability/opensearch-dashboards`) |\n\n- **Allowlist:** `51.254.106.87/32` (ops01 / operator VPN egress) +\n  `92.222.245.89/32` (agents01 egress). Any other source gets `403` at the\n  edge and never reaches a backend.\n- **TLS:** four Let's Encrypt PROD certificates (DNS-01) \u2014\n  `kube-system/grafana-tls`, `prometheus-tls`, `alertmanager-tls`,\n  `cerebro-tls` \u2014 declared in the `infrastructure` repo\n  (`kubernetes/cert-manager/certificate-{grafana,prometheus,alertmanager,cerebro}.yaml`,\n  merged 2026-09-04, infra PR #44) and wired into the Gateway's https\n  listener at the F1.5 edge apply (operator-gated).\n- **DNS:** 8 A-records (4 hosts \u00d7 `.193`/`.194`, Cloudflare, TTL 300) in\n  `infrastructure` (tofu; merged 2026-09-04, infra PR #43; apply gate F1.6).\n\n## Components and pinned versions\n\nAll objects Git-owned in **`platform-observability`**\n(`kubernetes/observability/<component>/`), ns `observability`.\nDesired state is the source of truth \u2014 this page only explains it.\nEvery image is **tag-pinned** (never a floating tag):\n\n| Component | Image (pinned) | Shape | PVC (ceph-rbd, RWO) |\n|---|---|---|---|\n| Prometheus | `prom/prometheus:v3.9.1` | Deployment, Recreate; 1Gi/2Gi | `prometheus` 20Gi (retention 15d in-server) |\n| Alertmanager | `quay.io/prometheus/alertmanager:v0.34.0` | Deployment, 256Mi/512Mi | `data-alertmanager-0` 2Gi |\n| node-exporter | `quay.io/prometheus/node-exporter:v1.12.1` | DaemonSet, 64Mi/128Mi | \u2014 |\n| kube-state-metrics | `registry.k8s.io/kube-state-metrics/kube-state-metrics:v2.9.2` | Deployment, 128Mi/256Mi | \u2014 |\n| Tempo | `grafana/tempo:3.0.3` | StatefulSet, 1Gi/2Gi | `tempo-data` 10Gi (14d retention) |\n| Grafana | `grafana/grafana:13.0.8` | Deployment, 512Mi/1Gi | `grafana-data` 2Gi |\n| OTel Collector | `otel/opentelemetry-collector-contrib:0.160.0` | DaemonSet (1/node), 256Mi/512Mi | \u2014 |\n| Data Prepper | `opensearchproject/data-prepper:2.16.0` | Deployment, 512Mi/1Gi | \u2014 |\n| OpenSearch | `opensearchproject/opensearch:3.8.0` | StatefulSet (single node, raw manifests), 2Gi/3Gi (heap 1Gi) | `opensearch-data` 50Gi |\n| OpenSearch Dashboards | `opensearchproject/opensearch-dashboards:3.8.0` | Deployment, 512Mi/1Gi | \u2014 |\n\nStorageClass: **`ceph-rbd` only** (there is no `local-path` on RKE2); every\nPVC pins it explicitly. No NodePort anywhere \u2014 all Services are ClusterIP;\npublic exposure goes through the edge only.\n\nNotable v1 postures (documented in-manifest, not duplicated here):\n\n- **OpenSearch 3.8.0** runs the image's **demo-installer security posture**:\n  self-signed demo transport+HTTP TLS generated at pod boot, admin seeded\n  from `OPENSEARCH_INITIAL_ADMIN_PASSWORD` (Secret `opensearch`), and a\n  `bootstrap-security.sh` (in-pod, apply-time) that creates the app users\n  (OSD `dfm`, `data-prepper`) with minimal roles. `:9200` is HTTPS + auth;\n  probes are `tcpSocket` accordingly. ISM policy `logs-retention-14d` +\n  composable template on `logs-*` applied out-of-band\n  (`scripts/ism-apply.sh`).\n- **Data Prepper 2.16.0** does not interpolate env vars in pipeline YAML \u2014\n  its initContainer `render` generates the runtime pipeline at startup from\n  the two `data-prepper-creds` keys (single Secret consumer).\n- **Grafana** installs the standalone plugin\n  `grafana-opensearch-datasource@2.34.3` (pinned) so the OpenSearch\n  datasource can load.\n- **NetworkPolicies (ingress-only v1):** default-deny ingress + explicit\n  allows (traefik \u2192 UIs, intra-namespace full, remote node-UIs); scrape\n  egress stays open in v1.\n\n## Edge wiring (repo layout)\n\n`kubernetes/observability/edge/` carries the four HTTPRoutes\n(`grafana` / `prometheus` / `alertmanager` / `cerebro`, ns `observability`)\nand the single shared Middleware `allowlist-middleware-observability`\n(`ipAllowList`, the two ranges above). Law 4 (PR #36 lesson): the middleware\nships in the SAME directory/PR as its routes \u2014 a desynced ExtensionRef would\nfail-closed with `500` while Argo shows Healthy. TLS terminates at the\nGateway; the routes are TLS-free.\n\n## Pipelines\n\n```mermaid\nflowchart TD\n  subgraph EDGE[\"WAN edge (Gateway wetopi-edge, LE TLS)\"]\n    R[\"4 HTTPRoutes + Middleware ipAllowList (51.254.106.87/32, 92.222.245.89/32)\"]\n  end\n  subgraph NS[\"namespace observability\"]\n    subgraph M[\"metrics\"]\n      EX[\"node-exporter DS + kube-state-metrics + apps /metrics\"]\n      PR[\"Prometheus v3.9.1 (PVC 20Gi, ret 15d)\"]\n      AM[\"Alertmanager v0.34.0 (UI-only v1)\"]\n    end\n    subgraph L[\"logs\"]\n      OC[\"OTel Collector contrib 0.160.0 (DaemonSet) \u2014 filelog + k8s_attributes\"]\n      DP[\"Data Prepper 2.16.0 (render-at-startup)\"]\n      OS[\"OpenSearch 3.8.0 (PVC 50Gi, ISM 14d)\"]\n      OSD[\"OSD 3.8.0 \u2014 cerebro.wetopi.net\"]\n    end\n    subgraph T[\"traces\"]\n      TMP[\"Tempo 3.0.3 (PVC 10Gi, ret 14d)\"]\n    end\n    GR[\"Grafana 13.0.8 \u2014 grafana.wetopi.net (datasources: prometheus, alertmanager, tempo, opensearch)\"]\n  end\n  OP[\"allowlisted clients (.87 / .89)\"]\n  APPS[\"apps exporting OTLP (litellm = first real traces producer)\"]\n  OP -->|HTTPS| R -->|403 for all other sources| EDGE\n  R --> GR & PR & AM & OSD\n  EX --> PR -->|rules| AM\n  OC -->|OTLP/gRPC :2189| DP -->|sink| OS\n  APPS -->|OTLP 4317/4318| OC -->|otlp/traces| TMP\n  OS -->|datasource| GR\n  TMP --> GR\n  PR --> GR\n```\n\n## Dashboards and datasources\n\n- **Datasources** (provisioned at Grafana start): `prometheus`\n  (default, `http://prometheus:9090`), `alertmanager`\n  (`http://alertmanager:9093`), `tempo` (`http://tempo:3200`), `opensearch`\n  (`https://opensearch.observability.svc:9200`, type\n  `grafana-opensearch-datasource`, Basic-Auth with the DFM credential).\n- **Dashboards** (\"Wetopi Obs\" folder, file provider): `k8s-overview`,\n  `prometheus-self`, `litellm`, `argocd`, `opensearch-health` (F6.3) +\n  `edge-traefik`, `alertmanager-fireboard` (F6.4).\n- **Out-of-band rendering (load-bearing):** the `grafana-datasources`\n  ConfigMap in Git is a TEMPLATE OF RECORD carrying the Argo skip marker\n  `+argocd:skip-file-rendering`; `scripts/apply-observability.sh` renders it\n  out-of-band at apply time (`envsubst` of `${OSD_DFM_USERNAME}` /\n  `${OSD_DFM_PASSWORD}` from Infisical) because Grafana's provisioner does\n  not expand `${...}` \u2014 a synced placeholder CM would give a 401 datasource.\n  Being outside the Argo tracked set, selfHeal can neither create the\n  placeholder nor revert the rendered CM.\n\n## Alerting \u2014 UI-only v1\n\nOperator decision for v1: **no external alert channels**. Alertmanager's\ndefault receiver is the no-op `null` receiver (no email, webhook or pager);\nAlertmanager only stores and displays alerts in its UI. The v1 rule set\n(`prometheus-rules-configmap.yaml`) covers node health (`NodeNotReady`,\n`NodeFilesystemAlmostFull`, `NodeClockSkew`), pod health\n(`PodCrashLooping`, `PodNotReady`, `ContainerOOMKilled`), storage\n(`PVCAlmostFull`) and self-monitoring (`PrometheusTargetDown`), each with a\n`severity` label (warning|critical). Adding a real receiver later = appending\nto `receivers:` and pointing a route sub-tree at it \u2014 no image or PVC\nchange. (Backlog v1.1: Telegram/email channels.)\n\n## Secrets (Infisical paths only \u2014 never values)\n\nProject `harness`, env `prod`. Four **out-of-band** k8s Secrets, created at\napply time by `scripts/apply-observability.sh` from Infisical (the single\nsource of truth for the contract is its `secret_contract()`):\n\n| k8s Secret (ns observability) | Infisical path | Keys | Consumed by |\n|---|---|---|---|\n| `opensearch` | `services/observability/opensearch` | `OPENSEARCH_ADMIN_PASSWORD`, `OPENSEARCH_ADMIN_HASH` | OpenSearch STS (admin seed), bootstrap-security, ism-apply |\n| `osd-dfm` | `services/observability/opensearch-dashboards` | `OSD_ADMIN_USER`, `OSD_ADMIN_PASSWORD` | OSD Deployment; rendered into the grafana-datasources CM at apply |\n| `grafana-admin` | `services/observability/grafana` | `GF_ADMIN_USER`, `GF_ADMIN_PASSWORD` | Grafana Deployment (admin login) |\n| `data-prepper-creds` | `services/observability/data-prepper` | `DATA_PREPPER_OPENSEARCH_PASSWORD`, `OPENSEARCH_ENDPOINTS` (non-secret) | Data Prepper initContainer `render` |\n\nOnly `secret.example.yaml` (placeholders) lives in Git \u2014 root\n`kubernetes/observability/secret.example.yaml` (consolidated shape) plus the\nper-component examples. These are EXCLUDED from the Argo include glob and\nfrom any dir-wide apply (litellm-gate RC-2: an example Secret inside an\nArgo-synced tree clobbers the real one while Argo stays green \u2014 the 2026-09-04\nincident that fixed the exclude glob to a single brace glob).\n\n## How is it deployed?\n\nGitOps with two layers:\n\n1. **Argo anchor** \u2014 `platform-gitops/apps/observability.yaml`: Application\n   `observability` (project `platform`) syncs\n   `platform-observability:main` path `kubernetes/observability`, directory\n   mode `recurse: true` with\n   `exclude: '{*example*,*.json,README.md}'` (single brace glob \u2014 Argo\n   exclude takes ONE glob), `automated: prune: true, selfHeal: true`,\n   `CreateNamespace=false` (the namespace is created by the apply script /\n   `namespace.yaml` first), `PruneLast=true` (the anchor was hardened in\n   F7.4). The anchor `argocd/application.yaml` inside\n   `platform-observability` is a legacy placeholder \u2014 the live anchor is the\n   platform-gitops one.\n2. **Apply script** \u2014 `scripts/apply-observability.sh`\n   (OPERATOR-GATED; the worker never runs it). It mirrors the proven\n   `apply-litellm.sh` pattern and runs on the harness host with no local\n   kubectl \u2014 every cluster op goes through\n   `ssh rke-cp01 sudo /var/lib/rancher/rke2/bin/kubectl --kubeconfig\n   /etc/rancher/rke2/rke2.yaml`. Phases: preflight \u2192 Infisical fetch \u2192\n   create the 4 Secrets \u2192 **file-by-file apply in a fixed order** (never\n   dir-wide; never touches `*example*` or `*.json`) \u2192 OpenSearch security\n   bootstrap + ISM policy (in-pod, out-of-band) \u2192 WAIT gates (all pods\n   Ready; certs verified) \u2192 in-cluster probes + summary. `--allow-existing`\n   makes re-runs idempotent (re-fetch from Infisical and overwrite the\n   Secrets). `scripts/ism-apply.sh` (separately re-runnable) loads the\n   14-day ISM policy + `logs-*` template with the OpenSearch 3.8.0 schema\n   fixes live-verified at F7.2.\n\n## Dependencies\n\n**Depends on:** Ceph CSI / StorageClass `ceph-rbd` for all five PVCs\n([SVC-CEPH-CSI](ceph-csi.md)); the edge Gateway `wetopi-edge` + nftables\n443\u21928443 redirect ([SVC-TRAEFIK-EDGE](traefik-edge.md)); cert-manager for\nthe four obs certificates ([SVC-CERT-MANAGER](cert-manager.md)); DNS A\nrecords in `infrastructure` (tofu-owned, Cloudflare); Infisical\n([SVC-VAULT](vault.md)) for the four apply-time Secrets; the scrape\ntargets across the cluster (kubernetes-SD) and OTLP producers for\nlogs/traces (litellm is the first real traces producer \u2014\n[SVC-LITELLM](litellm.md)).\n\n**Depended on by:** every platform operation that needs metrics, log search\nor traces \u2014 incident response, OOM sizing decisions (litellm/vault lessons),\nE2E acceptance gates (F8.2 logs / F8.3 traces / F8.4 metrics).\n\n## Operation\n\nkubectl is run on the control plane (no local kubectl); every command is via\n`ssh rke-cp01 sudo /var/lib/rancher/rke2/bin/kubectl --kubeconfig\n/etc/rancher/rke2/rke2.yaml`.\n\n- **Health:** `kubectl -n observability get pods` (all `1/1`, restarts low,\n  no `OOMKilled` in `lastState`); `kubectl -n argocd get application\n  observability` (`Synced`/`Healthy`); the four public probes (DNS \u2192 2 IPs,\n  TLS LE CN, `403` from a non-allowlisted source, `200`/`302` from\n  `.89`/`.87`).\n- **Logs:** `kubectl -n observability logs -l app.kubernetes.io/name=<component>`;\n  for node-level filelog ingestion check the collector DaemonSet pods\n  (group-0 read on `/var/log/pods` is what makes pod logs readable).\n- **Adding a dashboard:** drop the JSON into the right ConfigMap under\n  `kubernetes/observability/grafana/` (see the repo README \"Adding a\n  dashboard\" section), PR it; the `wetopi-obs` file provider picks it up\n  from its mounted path on the next Grafana start (or the provider rescan).\n  CM and mount changes are coupled (Law 4 \u2014 ship both in the same PR).\n- **Adding a datasource:** same pattern via\n  `grafana-datasources-configmap.yaml` \u2014 but remember that file is the\n  out-of-band template of record: a datasource needing credentials must\n  keep the `${VAR}` placeholder, gain a matching render step in\n  `apply-observability.sh`, and a new Infisical key; plain http datasources\n  are safe to edit directly (still: re-run the apply render so the live CM\n  matches).\n- **Rotating credentials:** rotate the value in Infisical at its path, then\n  re-run `scripts/apply-observability.sh --allow-existing` (re-fetches and\n  overwrites the Secrets), then roll the consuming workloads\n  (`kubectl -n observability rollout restart deployment/<name>` /\n  `statefulset/opensearch` only when the OpenSearch admin password changes).\n  The OpenSearch admin password is seeded at **first boot only**\n  (`OPENSEARCH_INITIAL_ADMIN_PASSWORD`) \u2014 changing it later requires a\n  security plugin user change, not a PVC wipe.\n- **OpenSearch re-bootstrap (new PVC / DR):** `opensearch/bootstrap-security.sh`\n  (users + roles) and `scripts/ism-apply.sh` (retention) are the two\n  out-of-band steps that re-run after a fresh OpenSearch data volume; both\n  carry the live-verified OpenSearch 3.8.0 schema fixes (policy body wrapped\n  as `{\"policy\": {...}}`, no `enabled` field in `ism_template`).\n\n## Runbook \u2014 apply (F7.2, operator-gated)\n\nThe full go-live apply is `scripts/apply-observability.sh` \u2014 see its header\nfor the phase list and the Wetopi laws baked in (explicit file list,\nnever dir-wide; Secrets out-of-band; name must match `secretKeyRef`; no\nNodePort; `set -e`). Operator steps:\n\n1. Seed Infisical (project `harness`, env `prod`) at the four vault paths\n   above with the listed keys.\n2. From the repo root:\n   `VAULT_API_CLIENTID=*** VAULT_API_KEY=*** \\\n   scripts/apply-observability.sh`\n   (idempotent re-runs: add `--allow-existing`).\n3. Wait for the script's WAIT gates (all pods Ready, 15m budget; OpenSearch\n   bootstrap 10m budget), then verify the F7.3 public-contract probes.\n\nPreconditions already in place at go-live: the four LE certificates\nReady, the Gateway delta applied (F1.5), DNS A-records live (F1.6),\nthe Argo anchor merged (F2.3/F7.4).\n\n## Rollback plan (plan v1 \u00a77)\n\n- **Edge:** git-revert the Gateway certRef delta in `platform-edge` \u2014 the\n  https listener falls back to the remaining certs; the four `*-tls`\n  Secrets stay and cert-manager keeps renewing them. (Invariant R1: a\n  certRef whose Secret is missing takes down the ENTIRE https listener,\n  so edge changes are always fail-safe ordered and individually reversible.)\n- **Whole stack:** the manifests live under the Argo app with\n  `prune: true` \u2014 revert the anchor merge or `argocd app delete\n  observability`, then `kubectl delete ns observability` manually (the\n  namespace is not Argo-created). PVCs carry the Delete policy, so\n  **metrics/logs/traces data is lost on rollback** \u2014 accepted for v1,\n  everything is re-derivable (scrapes and logs re-ingest; retention is\n  by config, not by recreating PVCs \u2014 R6: never delete PVCs from a PR).\n- **DNS:** record removal is a tofu PR (the records carry `prevent_destroy`\n  deliberately).\n- **Blast radius:** nothing outside ns `observability` except the shared\n  https listener (mitigated by R1).\n\n## Estado honesto (live)\n\n**GO-LIVE 2026-09-05 (UTC). The suite is deployed, serving, and\nend-to-end operational.**\n\n- **Apply gate (F7.2):** `apply-observability.sh` executed; all pods\n  Running, Argo `observability` Synced/Healthy.\n- **Public-contract probes (F7.3): 21/21 green** \u2014 DNS 4\u00d73 resolvers\n  (`.193`/`.194`), TLS LE certs \u00d74 (correct CN per host), positive `200`\n  from `92.222.245.89` (agents01 egress) via both edge IPs, negative\n  `403` from `92.222.245.91` (ops01, outside the allowlist) on all four\n  hostnames, in-cluster probes on all four backends, pods at 0 restarts.\n- **Incident (resolved pre-gate):** cerebro returned `503` at the\n  OpenSearch Dashboards system-index step \u2014 root cause: the OSD config\n  index fell under the demo-installer's `system-index` guard. Fix: OSD\n  config index pointed to `observability-osd-config`\n  (platform-observability PR #38 + hotfix commits; `opensearchDashboards.index`\n  schema-verified from the 3.8.0 image) plus the `dfm` role PUT to its\n  final live-verified shape (commit `bea78ae`).\n- **Known limitations (honest):**\n  - **Alerting is display-only** \u2014 no external channels in v1 (operator\n    decision; backlogged for v1.1).\n  - **OpenSearch security posture is the demo installer** (self-signed\n    transport/HTTP certs generated at boot, single node) \u2014 acceptable v1,\n    not a production-grade TLS story.\n  - **Logs e2e evidence** (a pod line visible in cerebro) is the F8.2 lead\n    gate \u2014 the pipeline is green (collector \u2192 prepper \u2192 OpenSearch) but\n    the public-facing screenshot evidence lands with F8.2.\n  - **Hubble flow-metrics and egress NP hardening** are out of scope v1\n    (plan \u00a78).\n\n## Relay links (enlaces de relevo)\n\n- **Manifests (GitHub):**\n  [platform-observability/kubernetes/observability](https://github.com/wetopi-platform/platform-observability/tree/main/kubernetes/observability),\n  [edge wiring](https://github.com/wetopi-platform/platform-observability/tree/main/kubernetes/observability/edge),\n  [apply script](https://github.com/wetopi-platform/platform-observability/blob/main/scripts/apply-observability.sh)\n- **Argo anchor (GitHub):**\n  [platform-gitops/apps/observability.yaml](https://github.com/wetopi-platform/platform-gitops/blob/main/apps/observability.yaml)\n- **Edge + TLS (GitHub, infrastructure):**\n  [certificate-grafana](https://github.com/wetopi-platform/infrastructure/blob/main/kubernetes/cert-manager/certificate-grafana.yaml),\n  [certificate-prometheus](https://github.com/wetopi-platform/infrastructure/blob/main/kubernetes/cert-manager/certificate-prometheus.yaml),\n  [certificate-alertmanager](https://github.com/wetopi-platform/infrastructure/blob/main/kubernetes/cert-manager/certificate-alertmanager.yaml),\n  [certificate-cerebro](https://github.com/wetopi-platform/infrastructure/blob/main/kubernetes/cert-manager/certificate-cerebro.yaml)\n- **Secrets (paths only \u2014 DocOps \u00a78, never values):** Infisical project\n  `harness` (env `prod`) \u2192 `services/observability/{opensearch,\n  opensearch-dashboards, grafana, data-prepper}`.\n- **Related docs:**\n  [ADR-0014](../decisions/ADR-0014-edge-source-ip-allowlist.md) (source-IP\n  allowlist posture), [SVC-TRAEFIK-EDGE](traefik-edge.md),\n  [SVC-CERT-MANAGER](cert-manager.md), [SVC-CEPH-CSI](ceph-csi.md),\n  [SVC-VAULT](vault.md), [SVC-LITELLM](litellm.md) (first traces producer),\n  [services index](index.md).",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (services/observability-suite.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-observability-suite-\">Observability suite (ns observability)</p>\n<h2 id=\"bkmrk-what-is-it%3F\">What is it?</h2>\n<p id=\"bkmrk-the-platform%27s-full-\">The platform's full observability stack: one Git repo\n(<code>platform-observability</code>), one Argo anchor, one namespace\n(<code>observability</code>), and three pipelines \u2014</p>\n<ul id=\"bkmrk-metrics-%E2%80%94-node%2Fkube%2F\">\n<li><strong>Metrics</strong> \u2014 node/kube/service exporters scraped by Prometheus\n(15d in-server retention); alert rules feed Alertmanager.</li>\n<li><strong>Logs</strong> \u2014 an OTel Collector DaemonSet on every node tails\n<code>/var/log/pods</code> (filelog + k8s_attributes enrichment) and ships OTLP/gRPC\nto Data Prepper, which indexes into OpenSearch (14-day ISM retention on\n<code>logs-*</code>).</li>\n<li><strong>Traces</strong> \u2014 apps export OTLP to the collector\n(gRPC :4317 / HTTP :4318); a traces pipeline forwards them to Tempo\n(14d retention), browsable in Grafana.</li>\n</ul>\n<p id=\"bkmrk-presentation%3A-grafan\">Presentation: Grafana (metrics + logs + traces in one UI) and\n<strong>cerebro.wetopi.net</strong> (OpenSearch Dashboards \u2014 the log search entry point).</p>\n<p id=\"bkmrk-go-live%3A-2026-09-05-\">Go-live: <strong>2026-09-05</strong> (F7.2 apply gate + F7.3 public-contract probes,\nall green \u2014 see <em>Estado honesto</em>).</p>\n<h2 id=\"bkmrk-public-hostnames%2C-tl\">Public hostnames, TLS and auth posture</h2>\n<p id=\"bkmrk-four-hostnames%2C-all-\">Four hostnames, all served by the WAN edge (Gateway <code>kube-system/wetopi-edge</code>,\nRKE2 Traefik, <code>https</code> listener) and all gated by ONE shared source-IP\nallowlist middleware before the app's own auth \u2014 no HTTP Basic Auth at the\nedge (same posture as litellm / vault / docs routes,\n<a href=\"https://book.wetopi.net/decisions/ADR-0014-edge-source-ip-allowlist.md\">ADR-0014</a>):</p>\n<table id=\"bkmrk-hostname-backend-app\">\n<thead>\n<tr>\n<th>Hostname</th>\n<th>Backend</th>\n<th>App-level auth behind the IP gate</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>grafana.wetopi.net</code></td>\n<td><code>grafana:3000</code></td>\n<td>Grafana admin login (Infisical <code>services/observability/grafana</code>)</td>\n</tr>\n<tr>\n<td><code>prometheus.wetopi.net</code></td>\n<td><code>prometheus:9090</code></td>\n<td>none \u2014 the IP gate IS the auth (as argo/vault)</td>\n</tr>\n<tr>\n<td><code>alertmanager.wetopi.net</code></td>\n<td><code>alertmanager:9093</code></td>\n<td>none \u2014 the IP gate IS the auth</td>\n</tr>\n<tr>\n<td><code>cerebro.wetopi.net</code></td>\n<td><code>opensearch-dashboards:5601</code></td>\n<td>OpenSearch Dashboards login with the DFM credential (Infisical <code>services/observability/opensearch-dashboards</code>)</td>\n</tr>\n</tbody>\n</table>\n<ul id=\"bkmrk-allowlist%3A-51.254.10\">\n<li><strong>Allowlist:</strong> <code>51.254.106.87/32</code> (ops01 / operator VPN egress) +\n<code>92.222.245.89/32</code> (agents01 egress). Any other source gets <code>403</code> at the\nedge and never reaches a backend.</li>\n<li><strong>TLS:</strong> four Let's Encrypt PROD certificates (DNS-01) \u2014\n<code>kube-system/grafana-tls</code>, <code>prometheus-tls</code>, <code>alertmanager-tls</code>,\n<code>cerebro-tls</code> \u2014 declared in the <code>infrastructure</code> repo\n(<code>kubernetes/cert-manager/certificate-{grafana,prometheus,alertmanager,cerebro}.yaml</code>,\nmerged 2026-09-04, infra PR #44) and wired into the Gateway's https\nlistener at the F1.5 edge apply (operator-gated).</li>\n<li><strong>DNS:</strong> 8 A-records (4 hosts \u00d7 <code>.193</code>/<code>.194</code>, Cloudflare, TTL 300) in\n<code>infrastructure</code> (tofu; merged 2026-09-04, infra PR #43; apply gate F1.6).</li>\n</ul>\n<h2 id=\"bkmrk-components-and-pinne\">Components and pinned versions</h2>\n<p id=\"bkmrk-all-objects-git-owne\">All objects Git-owned in <strong><code>platform-observability</code></strong>\n(<code>kubernetes/observability/&lt;component&gt;/</code>), ns <code>observability</code>.\nDesired state is the source of truth \u2014 this page only explains it.\nEvery image is <strong>tag-pinned</strong> (never a floating tag):</p>\n<table id=\"bkmrk-component-image-%28pin\">\n<thead>\n<tr>\n<th>Component</th>\n<th>Image (pinned)</th>\n<th>Shape</th>\n<th>PVC (ceph-rbd, RWO)</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Prometheus</td>\n<td><code>prom/prometheus:v3.9.1</code></td>\n<td>Deployment, Recreate; 1Gi/2Gi</td>\n<td><code>prometheus</code> 20Gi (retention 15d in-server)</td>\n</tr>\n<tr>\n<td>Alertmanager</td>\n<td><code>quay.io/prometheus/alertmanager:v0.34.0</code></td>\n<td>Deployment, 256Mi/512Mi</td>\n<td><code>data-alertmanager-0</code> 2Gi</td>\n</tr>\n<tr>\n<td>node-exporter</td>\n<td><code>quay.io/prometheus/node-exporter:v1.12.1</code></td>\n<td>DaemonSet, 64Mi/128Mi</td>\n<td>\u2014</td>\n</tr>\n<tr>\n<td>kube-state-metrics</td>\n<td><code>registry.k8s.io/kube-state-metrics/kube-state-metrics:v2.9.2</code></td>\n<td>Deployment, 128Mi/256Mi</td>\n<td>\u2014</td>\n</tr>\n<tr>\n<td>Tempo</td>\n<td><code>grafana/tempo:3.0.3</code></td>\n<td>StatefulSet, 1Gi/2Gi</td>\n<td><code>tempo-data</code> 10Gi (14d retention)</td>\n</tr>\n<tr>\n<td>Grafana</td>\n<td><code>grafana/grafana:13.0.8</code></td>\n<td>Deployment, 512Mi/1Gi</td>\n<td><code>grafana-data</code> 2Gi</td>\n</tr>\n<tr>\n<td>OTel Collector</td>\n<td><code>otel/opentelemetry-collector-contrib:0.160.0</code></td>\n<td>DaemonSet (1/node), 256Mi/512Mi</td>\n<td>\u2014</td>\n</tr>\n<tr>\n<td>Data Prepper</td>\n<td><code>opensearchproject/data-prepper:2.16.0</code></td>\n<td>Deployment, 512Mi/1Gi</td>\n<td>\u2014</td>\n</tr>\n<tr>\n<td>OpenSearch</td>\n<td><code>opensearchproject/opensearch:3.8.0</code></td>\n<td>StatefulSet (single node, raw manifests), 2Gi/3Gi (heap 1Gi)</td>\n<td><code>opensearch-data</code> 50Gi</td>\n</tr>\n<tr>\n<td>OpenSearch Dashboards</td>\n<td><code>opensearchproject/opensearch-dashboards:3.8.0</code></td>\n<td>Deployment, 512Mi/1Gi</td>\n<td>\u2014</td>\n</tr>\n</tbody>\n</table>\n<p id=\"bkmrk-storageclass%3A-ceph-r\">StorageClass: <strong><code>ceph-rbd</code> only</strong> (there is no <code>local-path</code> on RKE2); every\nPVC pins it explicitly. No NodePort anywhere \u2014 all Services are ClusterIP;\npublic exposure goes through the edge only.</p>\n<p id=\"bkmrk-notable-v1-postures-\">Notable v1 postures (documented in-manifest, not duplicated here):</p>\n<ul id=\"bkmrk-opensearch-3.8.0-run\">\n<li><strong>OpenSearch 3.8.0</strong> runs the image's <strong>demo-installer security posture</strong>:\nself-signed demo transport+HTTP TLS generated at pod boot, admin seeded\nfrom <code>OPENSEARCH_INITIAL_ADMIN_PASSWORD</code> (Secret <code>opensearch</code>), and a\n<code>bootstrap-security.sh</code> (in-pod, apply-time) that creates the app users\n(OSD <code>dfm</code>, <code>data-prepper</code>) with minimal roles. <code>:9200</code> is HTTPS + auth;\nprobes are <code>tcpSocket</code> accordingly. ISM policy <code>logs-retention-14d</code> +\ncomposable template on <code>logs-*</code> applied out-of-band\n(<code>scripts/ism-apply.sh</code>).</li>\n<li><strong>Data Prepper 2.16.0</strong> does not interpolate env vars in pipeline YAML \u2014\nits initContainer <code>render</code> generates the runtime pipeline at startup from\nthe two <code>data-prepper-creds</code> keys (single Secret consumer).</li>\n<li><strong>Grafana</strong> installs the standalone plugin\n<code>grafana-opensearch-datasource@2.34.3</code> (pinned) so the OpenSearch\ndatasource can load.</li>\n<li><strong>NetworkPolicies (ingress-only v1):</strong> default-deny ingress + explicit\nallows (traefik \u2192 UIs, intra-namespace full, remote node-UIs); scrape\negress stays open in v1.</li>\n</ul>\n<h2 id=\"bkmrk-edge-wiring-%28repo-la\">Edge wiring (repo layout)</h2>\n<p id=\"bkmrk-kubernetes%2Fobservabi\"><code>kubernetes/observability/edge/</code> carries the four HTTPRoutes\n(<code>grafana</code> / <code>prometheus</code> / <code>alertmanager</code> / <code>cerebro</code>, ns <code>observability</code>)\nand the single shared Middleware <code>allowlist-middleware-observability</code>\n(<code>ipAllowList</code>, the two ranges above). Law 4 (PR #36 lesson): the middleware\nships in the SAME directory/PR as its routes \u2014 a desynced ExtensionRef would\nfail-closed with <code>500</code> while Argo shows Healthy. TLS terminates at the\nGateway; the routes are TLS-free.</p>\n<h2 id=\"bkmrk-pipelines\">Pipelines</h2>\n<pre id=\"bkmrk-flowchart-td-subgrap\"><code class=\"language-mermaid\">flowchart TD\n  subgraph EDGE[\"WAN edge (Gateway wetopi-edge, LE TLS)\"]\n    R[\"4 HTTPRoutes + Middleware ipAllowList (51.254.106.87/32, 92.222.245.89/32)\"]\n  end\n  subgraph NS[\"namespace observability\"]\n    subgraph M[\"metrics\"]\n      EX[\"node-exporter DS + kube-state-metrics + apps /metrics\"]\n      PR[\"Prometheus v3.9.1 (PVC 20Gi, ret 15d)\"]\n      AM[\"Alertmanager v0.34.0 (UI-only v1)\"]\n    end\n    subgraph L[\"logs\"]\n      OC[\"OTel Collector contrib 0.160.0 (DaemonSet) \u2014 filelog + k8s_attributes\"]\n      DP[\"Data Prepper 2.16.0 (render-at-startup)\"]\n      OS[\"OpenSearch 3.8.0 (PVC 50Gi, ISM 14d)\"]\n      OSD[\"OSD 3.8.0 \u2014 cerebro.wetopi.net\"]\n    end\n    subgraph T[\"traces\"]\n      TMP[\"Tempo 3.0.3 (PVC 10Gi, ret 14d)\"]\n    end\n    GR[\"Grafana 13.0.8 \u2014 grafana.wetopi.net (datasources: prometheus, alertmanager, tempo, opensearch)\"]\n  end\n  OP[\"allowlisted clients (.87 / .89)\"]\n  APPS[\"apps exporting OTLP (litellm = first real traces producer)\"]\n  OP --&gt;|HTTPS| R --&gt;|403 for all other sources| EDGE\n  R --&gt; GR &amp; PR &amp; AM &amp; OSD\n  EX --&gt; PR --&gt;|rules| AM\n  OC --&gt;|OTLP/gRPC :2189| DP --&gt;|sink| OS\n  APPS --&gt;|OTLP 4317/4318| OC --&gt;|otlp/traces| TMP\n  OS --&gt;|datasource| GR\n  TMP --&gt; GR\n  PR --&gt; GR\n</code></pre>\n<h2 id=\"bkmrk-dashboards-and-datas\">Dashboards and datasources</h2>\n<ul id=\"bkmrk-datasources-%28provisi\">\n<li><strong>Datasources</strong> (provisioned at Grafana start): <code>prometheus</code>\n(default, <code>http://prometheus:9090</code>), <code>alertmanager</code>\n(<code>http://alertmanager:9093</code>), <code>tempo</code> (<code>http://tempo:3200</code>), <code>opensearch</code>\n(<code>https://opensearch.observability.svc:9200</code>, type\n<code>grafana-opensearch-datasource</code>, Basic-Auth with the DFM credential).</li>\n<li><strong>Dashboards</strong> (\"Wetopi Obs\" folder, file provider): <code>k8s-overview</code>,\n<code>prometheus-self</code>, <code>litellm</code>, <code>argocd</code>, <code>opensearch-health</code> (F6.3) +\n<code>edge-traefik</code>, <code>alertmanager-fireboard</code> (F6.4).</li>\n<li><strong>Out-of-band rendering (load-bearing):</strong> the <code>grafana-datasources</code>\nConfigMap in Git is a TEMPLATE OF RECORD carrying the Argo skip marker\n<code>+argocd:skip-file-rendering</code>; <code>scripts/apply-observability.sh</code> renders it\nout-of-band at apply time (<code>envsubst</code> of <code>${OSD_DFM_USERNAME}</code> /\n<code>${OSD_DFM_PASSWORD}</code> from Infisical) because Grafana's provisioner does\nnot expand <code>${...}</code> \u2014 a synced placeholder CM would give a 401 datasource.\nBeing outside the Argo tracked set, selfHeal can neither create the\nplaceholder nor revert the rendered CM.</li>\n</ul>\n<h2 id=\"bkmrk-alerting-%E2%80%94-ui-only-v\">Alerting \u2014 UI-only v1</h2>\n<p id=\"bkmrk-operator-decision-fo\">Operator decision for v1: <strong>no external alert channels</strong>. Alertmanager's\ndefault receiver is the no-op <code>null</code> receiver (no email, webhook or pager);\nAlertmanager only stores and displays alerts in its UI. The v1 rule set\n(<code>prometheus-rules-configmap.yaml</code>) covers node health (<code>NodeNotReady</code>,\n<code>NodeFilesystemAlmostFull</code>, <code>NodeClockSkew</code>), pod health\n(<code>PodCrashLooping</code>, <code>PodNotReady</code>, <code>ContainerOOMKilled</code>), storage\n(<code>PVCAlmostFull</code>) and self-monitoring (<code>PrometheusTargetDown</code>), each with a\n<code>severity</code> label (warning|critical). Adding a real receiver later = appending\nto <code>receivers:</code> and pointing a route sub-tree at it \u2014 no image or PVC\nchange. (Backlog v1.1: Telegram/email channels.)</p>\n<h2 id=\"bkmrk-secrets-%28infisical-p\">Secrets (Infisical paths only \u2014 never values)</h2>\n<p id=\"bkmrk-project-harness%2C-env\">Project <code>harness</code>, env <code>prod</code>. Four <strong>out-of-band</strong> k8s Secrets, created at\napply time by <code>scripts/apply-observability.sh</code> from Infisical (the single\nsource of truth for the contract is its <code>secret_contract()</code>):</p>\n<table id=\"bkmrk-k8s-secret-%28ns-obser\">\n<thead>\n<tr>\n<th>k8s Secret (ns observability)</th>\n<th>Infisical path</th>\n<th>Keys</th>\n<th>Consumed by</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>opensearch</code></td>\n<td><code>services/observability/opensearch</code></td>\n<td><code>OPENSEARCH_ADMIN_PASSWORD</code>, <code>OPENSEARCH_ADMIN_HASH</code></td>\n<td>OpenSearch STS (admin seed), bootstrap-security, ism-apply</td>\n</tr>\n<tr>\n<td><code>osd-dfm</code></td>\n<td><code>services/observability/opensearch-dashboards</code></td>\n<td><code>OSD_ADMIN_USER</code>, <code>OSD_ADMIN_PASSWORD</code></td>\n<td>OSD Deployment; rendered into the grafana-datasources CM at apply</td>\n</tr>\n<tr>\n<td><code>grafana-admin</code></td>\n<td><code>services/observability/grafana</code></td>\n<td><code>GF_ADMIN_USER</code>, <code>GF_ADMIN_PASSWORD</code></td>\n<td>Grafana Deployment (admin login)</td>\n</tr>\n<tr>\n<td><code>data-prepper-creds</code></td>\n<td><code>services/observability/data-prepper</code></td>\n<td><code>DATA_PREPPER_OPENSEARCH_PASSWORD</code>, <code>OPENSEARCH_ENDPOINTS</code> (non-secret)</td>\n<td>Data Prepper initContainer <code>render</code></td>\n</tr>\n</tbody>\n</table>\n<p id=\"bkmrk-only-secret.example.\">Only <code>secret.example.yaml</code> (placeholders) lives in Git \u2014 root\n<code>kubernetes/observability/secret.example.yaml</code> (consolidated shape) plus the\nper-component examples. These are EXCLUDED from the Argo include glob and\nfrom any dir-wide apply (litellm-gate RC-2: an example Secret inside an\nArgo-synced tree clobbers the real one while Argo stays green \u2014 the 2026-09-04\nincident that fixed the exclude glob to a single brace glob).</p>\n<h2 id=\"bkmrk-how-is-it-deployed%3F\">How is it deployed?</h2>\n<p id=\"bkmrk-gitops-with-two-laye\">GitOps with two layers:</p>\n<ol id=\"bkmrk-argo-anchor-%E2%80%94-platfo\">\n<li><strong>Argo anchor</strong> \u2014 <code>platform-gitops/apps/observability.yaml</code>: Application\n<code>observability</code> (project <code>platform</code>) syncs\n<code>platform-observability:main</code> path <code>kubernetes/observability</code>, directory\nmode <code>recurse: true</code> with\n<code>exclude: '{*example*,*.json,README.md}'</code> (single brace glob \u2014 Argo\nexclude takes ONE glob), <code>automated: prune: true, selfHeal: true</code>,\n<code>CreateNamespace=false</code> (the namespace is created by the apply script /\n<code>namespace.yaml</code> first), <code>PruneLast=true</code> (the anchor was hardened in\nF7.4). The anchor <code>argocd/application.yaml</code> inside\n<code>platform-observability</code> is a legacy placeholder \u2014 the live anchor is the\nplatform-gitops one.</li>\n<li><strong>Apply script</strong> \u2014 <code>scripts/apply-observability.sh</code>\n(OPERATOR-GATED; the worker never runs it). It mirrors the proven\n<code>apply-litellm.sh</code> pattern and runs on the harness host with no local\nkubectl \u2014 every cluster op goes through\n<code>ssh rke-cp01 sudo /var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml</code>. Phases: preflight \u2192 Infisical fetch \u2192\ncreate the 4 Secrets \u2192 <strong>file-by-file apply in a fixed order</strong> (never\ndir-wide; never touches <code>*example*</code> or <code>*.json</code>) \u2192 OpenSearch security\nbootstrap + ISM policy (in-pod, out-of-band) \u2192 WAIT gates (all pods\nReady; certs verified) \u2192 in-cluster probes + summary. <code>--allow-existing</code>\nmakes re-runs idempotent (re-fetch from Infisical and overwrite the\nSecrets). <code>scripts/ism-apply.sh</code> (separately re-runnable) loads the\n14-day ISM policy + <code>logs-*</code> template with the OpenSearch 3.8.0 schema\nfixes live-verified at F7.2.</li>\n</ol>\n<h2 id=\"bkmrk-dependencies\">Dependencies</h2>\n<p id=\"bkmrk-depends-on%3A-ceph-csi\"><strong>Depends on:</strong> Ceph CSI / StorageClass <code>ceph-rbd</code> for all five PVCs\n(<a href=\"https://book.wetopi.net/ceph-csi.md\">SVC-CEPH-CSI</a>); the edge Gateway <code>wetopi-edge</code> + nftables\n443\u21928443 redirect (<a href=\"https://book.wetopi.net/traefik-edge.md\">SVC-TRAEFIK-EDGE</a>); cert-manager for\nthe four obs certificates (<a href=\"https://book.wetopi.net/cert-manager.md\">SVC-CERT-MANAGER</a>); DNS A\nrecords in <code>infrastructure</code> (tofu-owned, Cloudflare); Infisical\n(<a href=\"https://book.wetopi.net/vault.md\">SVC-VAULT</a>) for the four apply-time Secrets; the scrape\ntargets across the cluster (kubernetes-SD) and OTLP producers for\nlogs/traces (litellm is the first real traces producer \u2014\n<a href=\"https://book.wetopi.net/litellm.md\">SVC-LITELLM</a>).</p>\n<p id=\"bkmrk-depended-on-by%3A-ever\"><strong>Depended on by:</strong> every platform operation that needs metrics, log search\nor traces \u2014 incident response, OOM sizing decisions (litellm/vault lessons),\nE2E acceptance gates (F8.2 logs / F8.3 traces / F8.4 metrics).</p>\n<h2 id=\"bkmrk-operation\">Operation</h2>\n<p id=\"bkmrk-kubectl-is-run-on-th\">kubectl is run on the control plane (no local kubectl); every command is via\n<code>ssh rke-cp01 sudo /var/lib/rancher/rke2/bin/kubectl --kubeconfig /etc/rancher/rke2/rke2.yaml</code>.</p>\n<ul id=\"bkmrk-health%3A-kubectl--n-o\">\n<li><strong>Health:</strong> <code>kubectl -n observability get pods</code> (all <code>1/1</code>, restarts low,\nno <code>OOMKilled</code> in <code>lastState</code>); <code>kubectl -n argocd get application observability</code> (<code>Synced</code>/<code>Healthy</code>); the four public probes (DNS \u2192 2 IPs,\nTLS LE CN, <code>403</code> from a non-allowlisted source, <code>200</code>/<code>302</code> from\n<code>.89</code>/<code>.87</code>).</li>\n<li><strong>Logs:</strong> <code>kubectl -n observability logs -l app.kubernetes.io/name=&lt;component&gt;</code>;\nfor node-level filelog ingestion check the collector DaemonSet pods\n(group-0 read on <code>/var/log/pods</code> is what makes pod logs readable).</li>\n<li><strong>Adding a dashboard:</strong> drop the JSON into the right ConfigMap under\n<code>kubernetes/observability/grafana/</code> (see the repo README \"Adding a\ndashboard\" section), PR it; the <code>wetopi-obs</code> file provider picks it up\nfrom its mounted path on the next Grafana start (or the provider rescan).\nCM and mount changes are coupled (Law 4 \u2014 ship both in the same PR).</li>\n<li><strong>Adding a datasource:</strong> same pattern via\n<code>grafana-datasources-configmap.yaml</code> \u2014 but remember that file is the\nout-of-band template of record: a datasource needing credentials must\nkeep the <code>${VAR}</code> placeholder, gain a matching render step in\n<code>apply-observability.sh</code>, and a new Infisical key; plain http datasources\nare safe to edit directly (still: re-run the apply render so the live CM\nmatches).</li>\n<li><strong>Rotating credentials:</strong> rotate the value in Infisical at its path, then\nre-run <code>scripts/apply-observability.sh --allow-existing</code> (re-fetches and\noverwrites the Secrets), then roll the consuming workloads\n(<code>kubectl -n observability rollout restart deployment/&lt;name&gt;</code> /\n<code>statefulset/opensearch</code> only when the OpenSearch admin password changes).\nThe OpenSearch admin password is seeded at <strong>first boot only</strong>\n(<code>OPENSEARCH_INITIAL_ADMIN_PASSWORD</code>) \u2014 changing it later requires a\nsecurity plugin user change, not a PVC wipe.</li>\n<li><strong>OpenSearch re-bootstrap (new PVC / DR):</strong> <code>opensearch/bootstrap-security.sh</code>\n(users + roles) and <code>scripts/ism-apply.sh</code> (retention) are the two\nout-of-band steps that re-run after a fresh OpenSearch data volume; both\ncarry the live-verified OpenSearch 3.8.0 schema fixes (policy body wrapped\nas <code>{\"policy\": {...}}</code>, no <code>enabled</code> field in <code>ism_template</code>).</li>\n</ul>\n<h2 id=\"bkmrk-runbook-%E2%80%94-apply-%28f7.\">Runbook \u2014 apply (F7.2, operator-gated)</h2>\n<p id=\"bkmrk-the-full-go-live-app\">The full go-live apply is <code>scripts/apply-observability.sh</code> \u2014 see its header\nfor the phase list and the Wetopi laws baked in (explicit file list,\nnever dir-wide; Secrets out-of-band; name must match <code>secretKeyRef</code>; no\nNodePort; <code>set -e</code>). Operator steps:</p>\n<ol id=\"bkmrk-seed-infisical-%28proj\">\n<li>Seed Infisical (project <code>harness</code>, env <code>prod</code>) at the four vault paths\nabove with the listed keys.</li>\n<li>From the repo root:\n<code>VAULT_API_CLIENTID=*** VAULT_API_KEY=*** \\ scripts/apply-observability.sh</code>\n(idempotent re-runs: add <code>--allow-existing</code>).</li>\n<li>Wait for the script's WAIT gates (all pods Ready, 15m budget; OpenSearch\nbootstrap 10m budget), then verify the F7.3 public-contract probes.</li>\n</ol>\n<p id=\"bkmrk-preconditions-alread\">Preconditions already in place at go-live: the four LE certificates\nReady, the Gateway delta applied (F1.5), DNS A-records live (F1.6),\nthe Argo anchor merged (F2.3/F7.4).</p>\n<h2 id=\"bkmrk-rollback-plan-%28plan-\">Rollback plan (plan v1 \u00a77)</h2>\n<ul id=\"bkmrk-edge%3A-git-revert-the\">\n<li><strong>Edge:</strong> git-revert the Gateway certRef delta in <code>platform-edge</code> \u2014 the\nhttps listener falls back to the remaining certs; the four <code>*-tls</code>\nSecrets stay and cert-manager keeps renewing them. (Invariant R1: a\ncertRef whose Secret is missing takes down the ENTIRE https listener,\nso edge changes are always fail-safe ordered and individually reversible.)</li>\n<li><strong>Whole stack:</strong> the manifests live under the Argo app with\n<code>prune: true</code> \u2014 revert the anchor merge or <code>argocd app delete observability</code>, then <code>kubectl delete ns observability</code> manually (the\nnamespace is not Argo-created). PVCs carry the Delete policy, so\n<strong>metrics/logs/traces data is lost on rollback</strong> \u2014 accepted for v1,\neverything is re-derivable (scrapes and logs re-ingest; retention is\nby config, not by recreating PVCs \u2014 R6: never delete PVCs from a PR).</li>\n<li><strong>DNS:</strong> record removal is a tofu PR (the records carry <code>prevent_destroy</code>\ndeliberately).</li>\n<li><strong>Blast radius:</strong> nothing outside ns <code>observability</code> except the shared\nhttps listener (mitigated by R1).</li>\n</ul>\n<h2 id=\"bkmrk-estado-honesto-%28live\">Estado honesto (live)</h2>\n<p id=\"bkmrk-go-live-2026-09-05-%28\"><strong>GO-LIVE 2026-09-05 (UTC). The suite is deployed, serving, and\nend-to-end operational.</strong></p>\n<ul id=\"bkmrk-apply-gate-%28f7.2%29%3A-a\">\n<li><strong>Apply gate (F7.2):</strong> <code>apply-observability.sh</code> executed; all pods\nRunning, Argo <code>observability</code> Synced/Healthy.</li>\n<li><strong>Public-contract probes (F7.3): 21/21 green</strong> \u2014 DNS 4\u00d73 resolvers\n(<code>.193</code>/<code>.194</code>), TLS LE certs \u00d74 (correct CN per host), positive <code>200</code>\nfrom <code>92.222.245.89</code> (agents01 egress) via both edge IPs, negative\n<code>403</code> from <code>92.222.245.91</code> (ops01, outside the allowlist) on all four\nhostnames, in-cluster probes on all four backends, pods at 0 restarts.</li>\n<li><strong>Incident (resolved pre-gate):</strong> cerebro returned <code>503</code> at the\nOpenSearch Dashboards system-index step \u2014 root cause: the OSD config\nindex fell under the demo-installer's <code>system-index</code> guard. Fix: OSD\nconfig index pointed to <code>observability-osd-config</code>\n(platform-observability PR #38 + hotfix commits; <code>opensearchDashboards.index</code>\nschema-verified from the 3.8.0 image) plus the <code>dfm</code> role PUT to its\nfinal live-verified shape (commit <code>bea78ae</code>).</li>\n<li><strong>Known limitations (honest):</strong>\n<ul>\n<li><strong>Alerting is display-only</strong> \u2014 no external channels in v1 (operator\ndecision; backlogged for v1.1).</li>\n<li><strong>OpenSearch security posture is the demo installer</strong> (self-signed\ntransport/HTTP certs generated at boot, single node) \u2014 acceptable v1,\nnot a production-grade TLS story.</li>\n<li><strong>Logs e2e evidence</strong> (a pod line visible in cerebro) is the F8.2 lead\ngate \u2014 the pipeline is green (collector \u2192 prepper \u2192 OpenSearch) but\nthe public-facing screenshot evidence lands with F8.2.</li>\n<li><strong>Hubble flow-metrics and egress NP hardening</strong> are out of scope v1\n(plan \u00a78).</li>\n</ul>\n</li>\n</ul>\n<h2 id=\"bkmrk-relay-links-%28enlaces\">Relay links (enlaces de relevo)</h2>\n<ul id=\"bkmrk-manifests-%28github%29%3A-\">\n<li><strong>Manifests (GitHub):</strong>\n<a href=\"https://github.com/wetopi-platform/platform-observability/tree/main/kubernetes/observability\">platform-observability/kubernetes/observability</a>,\n<a href=\"https://github.com/wetopi-platform/platform-observability/tree/main/kubernetes/observability/edge\">edge wiring</a>,\n<a href=\"https://github.com/wetopi-platform/platform-observability/blob/main/scripts/apply-observability.sh\">apply script</a></li>\n<li><strong>Argo anchor (GitHub):</strong>\n<a href=\"https://github.com/wetopi-platform/platform-gitops/blob/main/apps/observability.yaml\">platform-gitops/apps/observability.yaml</a></li>\n<li><strong>Edge + TLS (GitHub, infrastructure):</strong>\n<a href=\"https://github.com/wetopi-platform/infrastructure/blob/main/kubernetes/cert-manager/certificate-grafana.yaml\">certificate-grafana</a>,\n<a href=\"https://github.com/wetopi-platform/infrastructure/blob/main/kubernetes/cert-manager/certificate-prometheus.yaml\">certificate-prometheus</a>,\n<a href=\"https://github.com/wetopi-platform/infrastructure/blob/main/kubernetes/cert-manager/certificate-alertmanager.yaml\">certificate-alertmanager</a>,\n<a href=\"https://github.com/wetopi-platform/infrastructure/blob/main/kubernetes/cert-manager/certificate-cerebro.yaml\">certificate-cerebro</a></li>\n<li><strong>Secrets (paths only \u2014 DocOps \u00a78, never values):</strong> Infisical project\n<code>harness</code> (env <code>prod</code>) \u2192 <code>services/observability/{opensearch, opensearch-dashboards, grafana, data-prepper}</code>.</li>\n<li><strong>Related docs:</strong>\n<a href=\"https://book.wetopi.net/decisions/ADR-0014-edge-source-ip-allowlist.md\">ADR-0014</a> (source-IP\nallowlist posture), <a href=\"https://book.wetopi.net/traefik-edge.md\">SVC-TRAEFIK-EDGE</a>,\n<a href=\"https://book.wetopi.net/cert-manager.md\">SVC-CERT-MANAGER</a>, <a href=\"https://book.wetopi.net/ceph-csi.md\">SVC-CEPH-CSI</a>,\n<a href=\"https://book.wetopi.net/vault.md\">SVC-VAULT</a>, <a href=\"https://book.wetopi.net/litellm.md\">SVC-LITELLM</a> (first traces producer),\n<a href=\"https://book.wetopi.net/index.md\">services index</a>.</li>\n</ul>\n"
 },
 {
  "id": 68,
  "title": "SVC-TRAEFIK-EDGE \u2014 edge WAN",
  "slug": "svc-traefik-edge-edge-wan",
  "book_id": 3,
  "chapter_id": 21,
  "description": null,
  "created_at": "2026-09-06T13:39:07.000000Z",
  "updated_at": "2026-09-06T21:45:55.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (services/traefik-edge.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n Traefik edge (WAN edge v1)\n\n## What is it?\n\nThe edge is how the platform is reached from the internet. Per ADR-0006\n(WAN exception for a bounded v1 window), the two edge nodes expose the\nRKE2-bundled Traefik **directly on their WAN IPs** \u2014 no L4 load-balancing\nlayer (VRRP+IPVS+DSR is deferred until an external-client service lands).\nTraefik runs in `hostNetwork` mode with the Gateway API provider enabled, so\nplain `Gateway`/`HTTPRoute` objects are the ingress programming surface.\n\n## How is it mounted?\n\n- **Chart:** `rke2-traefik` 40.1.009 (RKE2 addon), configured in place by the\n  `HelmChartConfig rke2-traefik` in `kube-system`\n  (`kubernetes/edge/helmchartconfig-rke2-traefik-edge.yaml` \u2014 the exact\n  live-verified `valuesContent` at F2 merge, apply `t_64e49873`, run 239):\n  `hostNetwork: true`, entryPoints **web=:8000 / websecure=:8443**\n  (hostPort = containerPort, chart contract), `providers.kubernetesGateway`\n  enabled, nodeSelector + toleration pinning the DaemonSet to\n  `wetopi.io/role=edge` nodes only (zero Traefik pods on CPs/platform\n  workers).\n- **Public 80/443 contract:** the `edge_lb` Ansible role nftables policy\n  REDIRECTs WAN+LAN 80\u21928000 and 443\u21928443 in the edge host netns. The\n  Gateway listeners deliberately sit on 8000/8443 \u2014 declaring 80/443 there\n  yields `PortUnavailable` and an un-Accepted Gateway (the live failure\n  that shaped F2 lead decision A).\n- **Gateway API objects:** `GatewayClass wetopi-edge`\n  (`controllerName: traefik.io/gateway-controller`) and `Gateway\n  kube-system/wetopi-edge` \u2014 listeners `web` (8000, `allowedRoutes from:\n  Same`) and `https` (8443, TLS Terminate, `from: All`).\n- **TLS:** the https listener `certificateRefs` (same-namespace Secrets):\n  `wetopi-edge-default-cert` (90-day bootstrap self-signed, F2), `argo-tls`,\n  `docs-tls`, `vault-tls` (LE PROD, issued by cert-manager \u2014 adding a ref\n  whose Secret is missing takes down the WHOLE listener; fail-safe apply\n  order documented per entry in `gateway-wetopi-edge.yaml`).\n- **HTTPRoutes on the Gateway (live set):** `docs-site` (ns docs-site),\n  `argo` + `argo-http-redirect` (kube-system co-location, argo Basic-Auth\n  Middleware), `vault` (ns vault, path-filtered deny-first), and the F3\n  acceptance `edge-test-tls` name.\n- **Nodes:** `rke-wan01` / `rke-wan02` (edge role, WAN IPs\n  79.137.114.193/.194; DNS-owned via tofu tfvars).\n\n## Diagram\n\n```mermaid\nflowchart LR\n  WAN[\"internet (WAN 80/443)\"]\n  subgraph EDGE[\"edge nodes rke-wan01/02 (hostNetwork)\"]\n    NFT[\"nftables edge_lb: 80->8000, 443->8443\"]\n    TRAEF[\"Traefik DS (rke2-traefik 40.1.009) \u2014 entryPoints 8000/8443\"]\n  end\n  GW[\"Gateway wetopi-edge (kube-system) \u2014 https TLS Terminate; certs: bootstrap + argo-tls + docs-tls + vault-tls\"]\n  subgraph ROUTES[\"HTTPRoutes\"]\n    RD[\"docs-site route (ns docs-site)\"]\n    RA[\"argo route + basic-auth (ns kube-system)\"]\n    RV[\"vault route, deny-first (ns vault)\"]\n  end\n  BACK[\"backends: docs-site / argocd-server / vault-ui Services\"]\n  WAN --> NFT --> TRAEF --> GW --> RD & RA & RV --> BACK\n```\n\n## How is it deployed?\n\nBootstrap-time, **not GitOps-managed**: the HelmChartConfig and the\nGatewayClass/Gateway objects are applied from `wetopi-ops01` with kubectl\nafter the edge nodes join (an RKE2 addon HelmChartConfig cannot be installed\nby the Cilium-style chart flow \u2014 the chart already exists; RKE2's packaged\naddon reconciler drives upgrades). Verify gates after apply: helm job\n`Complete`, DS 2/2 on edge nodes only, `:8000/:8443` LISTEN in the host\nnetns, 80/443 answered via the redirect. Gateway `certificateRefs` deltas\n(certs for new services) are reviewed declarative changes with a mandatory\n\"Certificate Ready \u2192 THEN apply Gateway\" ordering (see each Certificate\nheader).\n\n## Dependencies\n\n**Depends on:** edge nodes + the `edge_lb` nftables role (Ansible); the\nRKE2 bundled chart (version rides the RKE2 pin); cert-manager for every LE\ncertificate on the https listener ([SVC-CERT-MANAGER](cert-manager.md));\nDNS (tofu-owned A records) per hostname.\n\n**Depended on by:** every public service of the v1 window \u2014\ndocs.wetopi.net ([SVC-DOCS-SITE](docs-site.md)), argo.wetopi.net\n([SVC-ARGOCD](argocd.md)), vault.wetopi.net ([SVC-VAULT](vault.md)), and\nthe F3 acceptance name edge-test.wetopi.net. Any future external-client\nservice must land the L4 layer first (ADR-0006).\n\n## Operation\n\n- **Health:** `kubectl -n kube-system get gateway wetopi-edge` \u2014\n  `Accepted=True`, `Programmed=True`, https listener `ResolvedRefs=True`;\n  `ss -tlnp` on each edge node shows `*:8000` + `*:8443`; per-route\n  `HTTPRoute` status `Accepted=True` with the parent resolved.\n- **Adding a service:** (1) Certificate in kube-system, wait\n  `--for=condition=Ready`; (2) add the `certificateRef` entry to the Gateway\n  (reviewed change on `kubernetes/edge/gateway-wetopi-edge.yaml`); (3)\n  HTTPRoute + optional Basic-Auth Middleware (one htpasswd Secret per\n  service \u2014 never shared); (4) DNS A records (tofu apply).\n- **Rollback:** remove the `certificateRef` entry (git revert of the\n  Gateway delta) \u2014 the listener falls back to the remaining certs; the\n  Secret stays (cert-manager keeps renewing it).\n- **Runbooks:** infra repo `kubernetes/edge/README.md` (F2 shape + gates),\n  per-service READMEs under `kubernetes/<service>/`.\n\n## Estado honesto (live, 2026-08-31)\n\n**DEPLOYED.** F2 applied 2026-08-30 (task `t_64e49873`, all gates green):\nGateway Accepted+Programmed, both listeners healthy, edge_lb redirect live,\nthree HTTPRoutes attached (docs, argo, vault) with the four\n`certificateRefs` in place. v1 window is bounded by ADR-0006 \u2014 the L4 layer\nremains mandatory before any external-client service.\n\n## Relay links (enlaces de relevo)\n\n- **Manifests (GitHub):** [kubernetes/edge](https://github.com/wetopi-platform/infrastructure/tree/main/kubernetes/edge) (GatewayClass, Gateway, HelmChartConfig), [ansible/roles/edge_lb](https://github.com/wetopi-platform/infrastructure/tree/main/ansible/roles/edge_lb) (nftables redirect)\n- **Vault (https://vault.wetopi.net \u2192 project `wetopi-harness`):** the edge\n  currently holds no Vault-sourced material; per-service htpasswd Secrets\n  (`argo-basic-auth`, `docs-basic-auth`) are operator-managed cluster\n  Secrets with the Vault as the long-term home (ADR-0009).\n- **Related docs:** [ADR-0006](../decisions/ADR-0006-edge-v1-wan-exception.md) (the exception + 2-layer auth posture), [services index](index.md).",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (services/traefik-edge.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-traefik-edge-%28wan-ed\">Traefik edge (WAN edge v1)</p>\n<h2 id=\"bkmrk-what-is-it%3F\">What is it?</h2>\n<p id=\"bkmrk-the-edge-is-how-the-\">The edge is how the platform is reached from the internet. Per ADR-0006\n(WAN exception for a bounded v1 window), the two edge nodes expose the\nRKE2-bundled Traefik <strong>directly on their WAN IPs</strong> \u2014 no L4 load-balancing\nlayer (VRRP+IPVS+DSR is deferred until an external-client service lands).\nTraefik runs in <code>hostNetwork</code> mode with the Gateway API provider enabled, so\nplain <code>Gateway</code>/<code>HTTPRoute</code> objects are the ingress programming surface.</p>\n<h2 id=\"bkmrk-how-is-it-mounted%3F\">How is it mounted?</h2>\n<ul id=\"bkmrk-chart%3A-rke2-traefik-\">\n<li><strong>Chart:</strong> <code>rke2-traefik</code> 40.1.009 (RKE2 addon), configured in place by the\n<code>HelmChartConfig rke2-traefik</code> in <code>kube-system</code>\n(<code>kubernetes/edge/helmchartconfig-rke2-traefik-edge.yaml</code> \u2014 the exact\nlive-verified <code>valuesContent</code> at F2 merge, apply <code>t_64e49873</code>, run 239):\n<code>hostNetwork: true</code>, entryPoints <strong>web=:8000 / websecure=:8443</strong>\n(hostPort = containerPort, chart contract), <code>providers.kubernetesGateway</code>\nenabled, nodeSelector + toleration pinning the DaemonSet to\n<code>wetopi.io/role=edge</code> nodes only (zero Traefik pods on CPs/platform\nworkers).</li>\n<li><strong>Public 80/443 contract:</strong> the <code>edge_lb</code> Ansible role nftables policy\nREDIRECTs WAN+LAN 80\u21928000 and 443\u21928443 in the edge host netns. The\nGateway listeners deliberately sit on 8000/8443 \u2014 declaring 80/443 there\nyields <code>PortUnavailable</code> and an un-Accepted Gateway (the live failure\nthat shaped F2 lead decision A).</li>\n<li><strong>Gateway API objects:</strong> <code>GatewayClass wetopi-edge</code>\n(<code>controllerName: traefik.io/gateway-controller</code>) and <code>Gateway kube-system/wetopi-edge</code> \u2014 listeners <code>web</code> (8000, <code>allowedRoutes from: Same</code>) and <code>https</code> (8443, TLS Terminate, <code>from: All</code>).</li>\n<li><strong>TLS:</strong> the https listener <code>certificateRefs</code> (same-namespace Secrets):\n<code>wetopi-edge-default-cert</code> (90-day bootstrap self-signed, F2), <code>argo-tls</code>,\n<code>docs-tls</code>, <code>vault-tls</code> (LE PROD, issued by cert-manager \u2014 adding a ref\nwhose Secret is missing takes down the WHOLE listener; fail-safe apply\norder documented per entry in <code>gateway-wetopi-edge.yaml</code>).</li>\n<li><strong>HTTPRoutes on the Gateway (live set):</strong> <code>docs-site</code> (ns docs-site),\n<code>argo</code> + <code>argo-http-redirect</code> (kube-system co-location, argo Basic-Auth\nMiddleware), <code>vault</code> (ns vault, path-filtered deny-first), and the F3\nacceptance <code>edge-test-tls</code> name.</li>\n<li><strong>Nodes:</strong> <code>rke-wan01</code> / <code>rke-wan02</code> (edge role, WAN IPs\n79.137.114.193/.194; DNS-owned via tofu tfvars).</li>\n</ul>\n<h2 id=\"bkmrk-diagram\">Diagram</h2>\n<pre id=\"bkmrk-flowchart-lr-wan%5B%22in\"><code class=\"language-mermaid\">flowchart LR\n  WAN[\"internet (WAN 80/443)\"]\n  subgraph EDGE[\"edge nodes rke-wan01/02 (hostNetwork)\"]\n    NFT[\"nftables edge_lb: 80-&gt;8000, 443-&gt;8443\"]\n    TRAEF[\"Traefik DS (rke2-traefik 40.1.009) \u2014 entryPoints 8000/8443\"]\n  end\n  GW[\"Gateway wetopi-edge (kube-system) \u2014 https TLS Terminate; certs: bootstrap + argo-tls + docs-tls + vault-tls\"]\n  subgraph ROUTES[\"HTTPRoutes\"]\n    RD[\"docs-site route (ns docs-site)\"]\n    RA[\"argo route + basic-auth (ns kube-system)\"]\n    RV[\"vault route, deny-first (ns vault)\"]\n  end\n  BACK[\"backends: docs-site / argocd-server / vault-ui Services\"]\n  WAN --&gt; NFT --&gt; TRAEF --&gt; GW --&gt; RD &amp; RA &amp; RV --&gt; BACK\n</code></pre>\n<h2 id=\"bkmrk-how-is-it-deployed%3F\">How is it deployed?</h2>\n<p id=\"bkmrk-bootstrap-time%2C-not-\">Bootstrap-time, <strong>not GitOps-managed</strong>: the HelmChartConfig and the\nGatewayClass/Gateway objects are applied from <code>wetopi-ops01</code> with kubectl\nafter the edge nodes join (an RKE2 addon HelmChartConfig cannot be installed\nby the Cilium-style chart flow \u2014 the chart already exists; RKE2's packaged\naddon reconciler drives upgrades). Verify gates after apply: helm job\n<code>Complete</code>, DS 2/2 on edge nodes only, <code>:8000/:8443</code> LISTEN in the host\nnetns, 80/443 answered via the redirect. Gateway <code>certificateRefs</code> deltas\n(certs for new services) are reviewed declarative changes with a mandatory\n\"Certificate Ready \u2192 THEN apply Gateway\" ordering (see each Certificate\nheader).</p>\n<h2 id=\"bkmrk-dependencies\">Dependencies</h2>\n<p id=\"bkmrk-depends-on%3A-edge-nod\"><strong>Depends on:</strong> edge nodes + the <code>edge_lb</code> nftables role (Ansible); the\nRKE2 bundled chart (version rides the RKE2 pin); cert-manager for every LE\ncertificate on the https listener (<a href=\"https://book.wetopi.net/cert-manager.md\">SVC-CERT-MANAGER</a>);\nDNS (tofu-owned A records) per hostname.</p>\n<p id=\"bkmrk-depended-on-by%3A-ever\"><strong>Depended on by:</strong> every public service of the v1 window \u2014\ndocs.wetopi.net (<a href=\"https://book.wetopi.net/docs-site.md\">SVC-DOCS-SITE</a>), argo.wetopi.net\n(<a href=\"https://book.wetopi.net/argocd.md\">SVC-ARGOCD</a>), vault.wetopi.net (<a href=\"https://book.wetopi.net/vault.md\">SVC-VAULT</a>), and\nthe F3 acceptance name edge-test.wetopi.net. Any future external-client\nservice must land the L4 layer first (ADR-0006).</p>\n<h2 id=\"bkmrk-operation\">Operation</h2>\n<ul id=\"bkmrk-health%3A-kubectl--n-k\">\n<li><strong>Health:</strong> <code>kubectl -n kube-system get gateway wetopi-edge</code> \u2014\n<code>Accepted=True</code>, <code>Programmed=True</code>, https listener <code>ResolvedRefs=True</code>;\n<code>ss -tlnp</code> on each edge node shows <code>*:8000</code> + <code>*:8443</code>; per-route\n<code>HTTPRoute</code> status <code>Accepted=True</code> with the parent resolved.</li>\n<li><strong>Adding a service:</strong> (1) Certificate in kube-system, wait\n<code>--for=condition=Ready</code>; (2) add the <code>certificateRef</code> entry to the Gateway\n(reviewed change on <code>kubernetes/edge/gateway-wetopi-edge.yaml</code>); (3)\nHTTPRoute + optional Basic-Auth Middleware (one htpasswd Secret per\nservice \u2014 never shared); (4) DNS A records (tofu apply).</li>\n<li><strong>Rollback:</strong> remove the <code>certificateRef</code> entry (git revert of the\nGateway delta) \u2014 the listener falls back to the remaining certs; the\nSecret stays (cert-manager keeps renewing it).</li>\n<li><strong>Runbooks:</strong> infra repo <code>kubernetes/edge/README.md</code> (F2 shape + gates),\nper-service READMEs under <code>kubernetes/&lt;service&gt;/</code>.</li>\n</ul>\n<h2 id=\"bkmrk-estado-honesto-%28live\">Estado honesto (live, 2026-08-31)</h2>\n<p id=\"bkmrk-deployed.-f2-applied\"><strong>DEPLOYED.</strong> F2 applied 2026-08-30 (task <code>t_64e49873</code>, all gates green):\nGateway Accepted+Programmed, both listeners healthy, edge_lb redirect live,\nthree HTTPRoutes attached (docs, argo, vault) with the four\n<code>certificateRefs</code> in place. v1 window is bounded by ADR-0006 \u2014 the L4 layer\nremains mandatory before any external-client service.</p>\n<h2 id=\"bkmrk-relay-links-%28enlaces\">Relay links (enlaces de relevo)</h2>\n<ul id=\"bkmrk-manifests-%28github%29%3A-\">\n<li><strong>Manifests (GitHub):</strong> <a href=\"https://github.com/wetopi-platform/infrastructure/tree/main/kubernetes/edge\">kubernetes/edge</a> (GatewayClass, Gateway, HelmChartConfig), <a href=\"https://github.com/wetopi-platform/infrastructure/tree/main/ansible/roles/edge_lb\">ansible/roles/edge_lb</a> (nftables redirect)</li>\n<li><strong>Vault (https://vault.wetopi.net \u2192 project <code>wetopi-harness</code>):</strong> the edge\ncurrently holds no Vault-sourced material; per-service htpasswd Secrets\n(<code>argo-basic-auth</code>, <code>docs-basic-auth</code>) are operator-managed cluster\nSecrets with the Vault as the long-term home (ADR-0009).</li>\n<li><strong>Related docs:</strong> <a href=\"https://book.wetopi.net/decisions/ADR-0006-edge-v1-wan-exception.md\">ADR-0006</a> (the exception + 2-layer auth posture), <a href=\"https://book.wetopi.net/index.md\">services index</a>.</li>\n</ul>\n"
 },
 {
  "id": 69,
  "title": "SVC-VAULT \u2014 vault.wetopi.net",
  "slug": "svc-vault-vaultwetopinet",
  "book_id": 3,
  "chapter_id": 21,
  "description": null,
  "created_at": "2026-09-06T13:39:08.000000Z",
  "updated_at": "2026-09-06T21:45:55.000000Z",
  "markdown": "> Migrado desde `wetopi-platform/docs@main` (services/vault.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.\n\n Vault \u2014 Infisical self-hosted (ns vault)\n\n## What is it?\n\nInfisical is the platform's secrets manager for humans and for agent\nprofiles (machine identities via Universal Auth). ADR-0009 (accepted\n2026-08-31) fixes the shape: self-hosted **in cluster `wetopi3`, namespace\n`vault`**, with persistent PostgreSQL and Redis datastores in the same\ncluster, the Infisical \u2192 Kubernetes Operator for Secret sync, Universal Auth\nrestricted to the private network, and SOPS+age demoted to bootstrap/recovery\nonly. The operator's decision explicitly replaced the dedicated-VM variant\nwith the in-cluster deployment.\n\n## How is it mounted?\n\nAll objects Git-owned under `kubernetes/vault/` (images digest-pinned):\n\n- **Deployment `vault`** \u2014 `infisical/infisical:v0.164.1@sha256:d3ff9f82\u2026`\n  (multi-arch index digest), non-root uid 1001, replicas 1, `Recreate`\n  strategy, readiness/liveness on `:8080/api/status`. A ConfigMap\n  `vault-entrypoint` wrapper builds `DB_CONNECTION_URI` at container start\n  from Secret env (URL-encoded, never a YAML literal).\n- **Datastores:** `postgres:17.6-alpine3.22` (digest-pinned, non-root uid 70)\n  + PVC `vault-postgres-data` **5Gi**; `redis:7.4-alpine` (digest-pinned,\n  `noeviction + appendonly yes`, uid 999) + PVC `vault-redis-data` **1Gi**.\n  Both on StorageClass `ceph-rbd` \u2192 depends on Ceph CSI (below).\n- **Services:** `vault` (FULL API \u2014 in-cluster only, NO edge route) and\n  `vault-ui` (edge UI backend).\n- **Edge:** HTTPRoute `vault` (ns `vault`) \u2192 Gateway `wetopi-edge` https\n  listener; Certificate `vault-tls` (LE PROD DNS-01, kube-system).\n- **Secrets (references, never values \u2014 operator-provisioned on the\n  cluster, excluded from Argo sync):** `vault-infra-credentials`\n  (ENCRYPTION_KEY, AUTH_SECRET) and `vault-postgres-credentials`\n  (POSTGRES_USER, POSTGRES_PASSWORD); shapes: `secret.infra.example.yaml`,\n  `secret.postgres.example.yaml`.\n- **Operator (separate lifecycle):** `infisical/kubernetes-operator:v0.11.8`\n  (digest-pinned) in namespace `infisical-operator-system`\n  (`kubernetes/infisical-operator/`); anchoring its Argo Application is a\n  future reviewed change. CRDs are v1beta1 (Secrets Operator v1beta1).\n\n## Diagram\n\n```mermaid\nflowchart TD\n  AGENTS[\"agents / profiles (machine identities)\"]\n  HUMANS[\"operators (browser, login+TOTP)\"]\n  subgraph CL[\"cluster wetopi3\"]\n    subgraph NSV[\"namespace vault\"]\n      APP[\"Deployment vault \u2014 Infisical v0.164.1\"]\n      PG[\"PostgreSQL 17.6 \u2014 PVC 5Gi\"]\n      RD[\"Redis 7.4 noeviction \u2014 PVC 1Gi\"]\n      SVAULT[\"Service vault (FULL API, in-cluster only)\"]\n      SUI[\"Service vault-ui (UI edge backend)\"]\n    end\n    OP[\"Infisical Operator v0.11.8 (ns infisical-operator-system)\"]\n    GW[\"Gateway wetopi-edge (kube-system) + cert vault-tls\"]\n    SC[\"StorageClass ceph-rbd (Ceph CSI)\"]\n  end\n  CEPH[\"Ceph cluster 192.168.96.0/20 (VLAN 2)\"]\n  AGENTS -->|\"http://vault.vault.svc.cluster.local (NEVER the edge)\"| SVAULT\n  HUMANS -->|\"https://vault.wetopi.net\"| GW --> SUI\n  SVAULT --> APP\n  SUI --> APP\n  APP --> PG\n  APP --> RD\n  PG --> SC\n  RD --> SC\n  SC --> CEPH\n  OP -->|\"pulls from internal endpoint\"| SVAULT\n```\n\n## How is it deployed?\n\nGitOps: the Argo CD Application **`vault`** (anchored in the root path at\n`kubernetes/platform/vault.yaml`) syncs `kubernetes/vault/` with an\ninclude-only filter (`{namespace.yaml,postgres.yaml,redis.yaml,infisical.yaml,certificate-vault.yaml,httproute-vault.yaml}`) and `prune + selfHeal` \u2014\nthe two operator Secrets are excluded on purpose and **must exist before\nthe first sync** (runbook: `kubernetes/vault/README.md` \u00a7provisioning \u2014\nAPPLY card `t_89a254ce`). TLS: Certificate `vault-tls` must be Ready\nBEFORE the Gateway `certificateRefs` edit is applied (fail-safe order in\n`certificate-vault.yaml` header). The Infisical Operator directory has its\nown lifecycle (not yet anchored to the root app).\n\n## Dependencies\n\n**Depends on:** Ceph CSI / StorageClass `ceph-rbd` for both PVCs\n([SVC-CEPH-CSI](ceph-csi.md)) \u2014 if CSI is not live, the datastores cannot\nbind; edge Gateway `wetopi-edge` + the nftables 443\u21928443 redirect\n([SVC-TRAEFIK-EDGE](traefik-edge.md)); cert-manager for `vault-tls`\n([SVC-CERT-MANAGER](cert-manager.md)); DNS A records `vault.wetopi.net` \u2192\nedge IPs (tofu-owned, tfvars); operator Secrets `vault-infra-credentials`\n+ `vault-postgres-credentials` (APPLY S0\u2013S2, card `t_89a254ce`); RKE2\netcd encryption at rest is the at-rest story (etcd keys on ops01).\n\n**Depended on by:** every machine identity (agent profiles) \u2014 the ai-hub\n`vault.*` contract (`vault.get/put/update/delete/list/exists`) resolves\nagainst `http://vault.vault.svc.cluster.local:80`; long-term the ceph-csi\n`csi-rbd-secret` and other bootstrap secrets migrate from ops01 files into\nthe Vault (ADR-0009).\n\n## Operation\n\n- **Access (human):** `https://vault.wetopi.net` \u2014 native Infisical login\n  + password + TOTP. **No** basic-auth middleware (deliberate \u2014 ADR-0009\n  decision 2; it would break interactive flows).\n- **Access (machine):** `http://vault.vault.svc.cluster.local:80` only \u2014\n  the public route carries DENY rules (D1\u2013D6: universal-auth login,\n  identity, `/api/v1|v2|v3|v4` secret surfaces all point at the\n  nonexistent `vault-api-deny` Service \u2014 fail-closed 5xx by Gateway API\n  spec). The proof contract (public 5xx / internal 200 drill) is in\n  `kubernetes/vault/README.md` \u00a75 and runs on the ops card after apply.\n- **Health:** `kubectl -n vault get pods` (vault/postgres/redis Ready),\n  `GET http://vault.vault.svc.cluster.local/api/status` from inside the\n  cluster; edge probe: `vault-tls` Ready + https listener ResolvedRefs.\n- **Backup/recovery:** ADR-0009 AC8 \u2014 `pg_dump` + age-encrypted off-host\n  backup, restore REHEARSED before the service is considered production\n  (recovery contract compensates the accepted correlated-failure trade-off\n  of in-cluster datastores).\n\n## Estado honesto (live, 2026-08-31 20:14Z)\n\n**LIVE \u2014 vault.wetopi.net serving HTTPS 200 with LE cert.** End-to-end\nverify after the operator's tofu DNS apply (vault.wetopi.net \u2192\n79.137.114.193/.194): Web UI 200 via both edge IPs, certificate\n`vault-tls` (Let's Encrypt, valid 2026-08-31\u21922026-11-29), pods 3/3\nRunning (infisical + postgres + redis on ceph-rbd PVCs), `/api/status`\n`Ok` with Redis configured. Two lead-direct hotfixes landed in Git during\nthe apply: commit `3a87ee94` (entrypoint ConfigMap `defaultMode: 0755` \u2014\nrunAsUser 1001 exec permission, crashloop fix) and `7e0b3a0b` (infisical\nmemory limit 1Gi\u21922Gi after an OOMKill at 19:59Z, V8 heap 2.3GB).\nThe deny rules D1\u2013D3 (machine-API paths \u2192 nonexistent Service\n`vault-api-deny`) are fail-closed BY SPEC \u2014 verified: `/api/v1/secrets`\nreturns 500 from the public edge; the Argo `Degraded` on the app is the\ndocumented false positive of this mechanism (ResolvedRefs=False is the\ndeny working, not a fault). PENDING: operator admin bootstrap + TOTP QR\n(human step).\n\n## Relay links (enlaces de relevo)\n\n- **Manifests (GitHub):** [kubernetes/vault](https://github.com/wetopi-platform/infrastructure/tree/main/kubernetes/vault), [kubernetes/infisical-operator](https://github.com/wetopi-platform/infrastructure/tree/main/kubernetes/infisical-operator)\n- **Vault (https://vault.wetopi.net \u2192 project `wetopi-harness`):** this is\n  the service that hosts the rest of the platform's secrets \u2014 machine\n  identities and secret paths live under project `wetopi-harness` (e.g. the\n  drill identity `drill-probe-<date>` in the README proof). Secret values\n  are never in this repo or the docs; reference paths only (DocOps \u00a78).\n- **Related docs:** [ADR-0009](../decisions/ADR-0009-secrets-vault-harness.md) (accepted \u2014 the decision this ficha documents), [OBJ-VAULT-001](../objectives/active/OBJ-VAULT-001.md) (objective + execution ledger), [vault-system spec](../architecture/vault-system/index.md), [services index](index.md).",
  "html": "<blockquote id=\"bkmrk-migrado-desde-wetopi\">\n<p>Migrado desde <code>wetopi-platform/docs@main</code> (services/vault.md) el 2026-09-06. Fuente can\u00f3nica: Git. Mirror de lectura.</p>\n</blockquote>\n<p id=\"bkmrk-vault-%E2%80%94-infisical-se\">Vault \u2014 Infisical self-hosted (ns vault)</p>\n<h2 id=\"bkmrk-what-is-it%3F\">What is it?</h2>\n<p id=\"bkmrk-infisical-is-the-pla\">Infisical is the platform's secrets manager for humans and for agent\nprofiles (machine identities via Universal Auth). ADR-0009 (accepted\n2026-08-31) fixes the shape: self-hosted <strong>in cluster <code>wetopi3</code>, namespace\n<code>vault</code></strong>, with persistent PostgreSQL and Redis datastores in the same\ncluster, the Infisical \u2192 Kubernetes Operator for Secret sync, Universal Auth\nrestricted to the private network, and SOPS+age demoted to bootstrap/recovery\nonly. The operator's decision explicitly replaced the dedicated-VM variant\nwith the in-cluster deployment.</p>\n<h2 id=\"bkmrk-how-is-it-mounted%3F\">How is it mounted?</h2>\n<p id=\"bkmrk-all-objects-git-owne\">All objects Git-owned under <code>kubernetes/vault/</code> (images digest-pinned):</p>\n<ul id=\"bkmrk-deployment-vault-%E2%80%94-i\">\n<li><strong>Deployment <code>vault</code></strong> \u2014 <code>infisical/infisical:v0.164.1@sha256:d3ff9f82\u2026</code>\n(multi-arch index digest), non-root uid 1001, replicas 1, <code>Recreate</code>\nstrategy, readiness/liveness on <code>:8080/api/status</code>. A ConfigMap\n<code>vault-entrypoint</code> wrapper builds <code>DB_CONNECTION_URI</code> at container start\nfrom Secret env (URL-encoded, never a YAML literal).</li>\n<li><strong>Datastores:</strong> <code>postgres:17.6-alpine3.22</code> (digest-pinned, non-root uid 70)\n<ul>\n<li>PVC <code>vault-postgres-data</code> <strong>5Gi</strong>; <code>redis:7.4-alpine</code> (digest-pinned,\n<code>noeviction + appendonly yes</code>, uid 999) + PVC <code>vault-redis-data</code> <strong>1Gi</strong>.\nBoth on StorageClass <code>ceph-rbd</code> \u2192 depends on Ceph CSI (below).</li>\n</ul>\n</li>\n<li><strong>Services:</strong> <code>vault</code> (FULL API \u2014 in-cluster only, NO edge route) and\n<code>vault-ui</code> (edge UI backend).</li>\n<li><strong>Edge:</strong> HTTPRoute <code>vault</code> (ns <code>vault</code>) \u2192 Gateway <code>wetopi-edge</code> https\nlistener; Certificate <code>vault-tls</code> (LE PROD DNS-01, kube-system).</li>\n<li><strong>Secrets (references, never values \u2014 operator-provisioned on the\ncluster, excluded from Argo sync):</strong> <code>vault-infra-credentials</code>\n(ENCRYPTION_KEY, AUTH_SECRET) and <code>vault-postgres-credentials</code>\n(POSTGRES_USER, POSTGRES_PASSWORD); shapes: <code>secret.infra.example.yaml</code>,\n<code>secret.postgres.example.yaml</code>.</li>\n<li><strong>Operator (separate lifecycle):</strong> <code>infisical/kubernetes-operator:v0.11.8</code>\n(digest-pinned) in namespace <code>infisical-operator-system</code>\n(<code>kubernetes/infisical-operator/</code>); anchoring its Argo Application is a\nfuture reviewed change. CRDs are v1beta1 (Secrets Operator v1beta1).</li>\n</ul>\n<h2 id=\"bkmrk-diagram\">Diagram</h2>\n<pre id=\"bkmrk-flowchart-td-agents%5B\"><code class=\"language-mermaid\">flowchart TD\n  AGENTS[\"agents / profiles (machine identities)\"]\n  HUMANS[\"operators (browser, login+TOTP)\"]\n  subgraph CL[\"cluster wetopi3\"]\n    subgraph NSV[\"namespace vault\"]\n      APP[\"Deployment vault \u2014 Infisical v0.164.1\"]\n      PG[\"PostgreSQL 17.6 \u2014 PVC 5Gi\"]\n      RD[\"Redis 7.4 noeviction \u2014 PVC 1Gi\"]\n      SVAULT[\"Service vault (FULL API, in-cluster only)\"]\n      SUI[\"Service vault-ui (UI edge backend)\"]\n    end\n    OP[\"Infisical Operator v0.11.8 (ns infisical-operator-system)\"]\n    GW[\"Gateway wetopi-edge (kube-system) + cert vault-tls\"]\n    SC[\"StorageClass ceph-rbd (Ceph CSI)\"]\n  end\n  CEPH[\"Ceph cluster 192.168.96.0/20 (VLAN 2)\"]\n  AGENTS --&gt;|\"http://vault.vault.svc.cluster.local (NEVER the edge)\"| SVAULT\n  HUMANS --&gt;|\"https://vault.wetopi.net\"| GW --&gt; SUI\n  SVAULT --&gt; APP\n  SUI --&gt; APP\n  APP --&gt; PG\n  APP --&gt; RD\n  PG --&gt; SC\n  RD --&gt; SC\n  SC --&gt; CEPH\n  OP --&gt;|\"pulls from internal endpoint\"| SVAULT\n</code></pre>\n<h2 id=\"bkmrk-how-is-it-deployed%3F\">How is it deployed?</h2>\n<p id=\"bkmrk-gitops%3A-the-argo-cd-\">GitOps: the Argo CD Application <strong><code>vault</code></strong> (anchored in the root path at\n<code>kubernetes/platform/vault.yaml</code>) syncs <code>kubernetes/vault/</code> with an\ninclude-only filter (<code>{namespace.yaml,postgres.yaml,redis.yaml,infisical.yaml,certificate-vault.yaml,httproute-vault.yaml}</code>) and <code>prune + selfHeal</code> \u2014\nthe two operator Secrets are excluded on purpose and <strong>must exist before\nthe first sync</strong> (runbook: <code>kubernetes/vault/README.md</code> \u00a7provisioning \u2014\nAPPLY card <code>t_89a254ce</code>). TLS: Certificate <code>vault-tls</code> must be Ready\nBEFORE the Gateway <code>certificateRefs</code> edit is applied (fail-safe order in\n<code>certificate-vault.yaml</code> header). The Infisical Operator directory has its\nown lifecycle (not yet anchored to the root app).</p>\n<h2 id=\"bkmrk-dependencies\">Dependencies</h2>\n<p id=\"bkmrk-depends-on%3A-ceph-csi\"><strong>Depends on:</strong> Ceph CSI / StorageClass <code>ceph-rbd</code> for both PVCs\n(<a href=\"https://book.wetopi.net/ceph-csi.md\">SVC-CEPH-CSI</a>) \u2014 if CSI is not live, the datastores cannot\nbind; edge Gateway <code>wetopi-edge</code> + the nftables 443\u21928443 redirect\n(<a href=\"https://book.wetopi.net/traefik-edge.md\">SVC-TRAEFIK-EDGE</a>); cert-manager for <code>vault-tls</code>\n(<a href=\"https://book.wetopi.net/cert-manager.md\">SVC-CERT-MANAGER</a>); DNS A records <code>vault.wetopi.net</code> \u2192\nedge IPs (tofu-owned, tfvars); operator Secrets <code>vault-infra-credentials</code></p>\n<ul id=\"bkmrk-vault-postgres-crede\">\n<li><code>vault-postgres-credentials</code> (APPLY S0\u2013S2, card <code>t_89a254ce</code>); RKE2\netcd encryption at rest is the at-rest story (etcd keys on ops01).</li>\n</ul>\n<p id=\"bkmrk-depended-on-by%3A-ever\"><strong>Depended on by:</strong> every machine identity (agent profiles) \u2014 the ai-hub\n<code>vault.*</code> contract (<code>vault.get/put/update/delete/list/exists</code>) resolves\nagainst <code>http://vault.vault.svc.cluster.local:80</code>; long-term the ceph-csi\n<code>csi-rbd-secret</code> and other bootstrap secrets migrate from ops01 files into\nthe Vault (ADR-0009).</p>\n<h2 id=\"bkmrk-operation\">Operation</h2>\n<ul id=\"bkmrk-access-%28human%29%3A-http\">\n<li><strong>Access (human):</strong> <code>https://vault.wetopi.net</code> \u2014 native Infisical login\n<ul>\n<li>password + TOTP. <strong>No</strong> basic-auth middleware (deliberate \u2014 ADR-0009\ndecision 2; it would break interactive flows).</li>\n</ul>\n</li>\n<li><strong>Access (machine):</strong> <code>http://vault.vault.svc.cluster.local:80</code> only \u2014\nthe public route carries DENY rules (D1\u2013D6: universal-auth login,\nidentity, <code>/api/v1|v2|v3|v4</code> secret surfaces all point at the\nnonexistent <code>vault-api-deny</code> Service \u2014 fail-closed 5xx by Gateway API\nspec). The proof contract (public 5xx / internal 200 drill) is in\n<code>kubernetes/vault/README.md</code> \u00a75 and runs on the ops card after apply.</li>\n<li><strong>Health:</strong> <code>kubectl -n vault get pods</code> (vault/postgres/redis Ready),\n<code>GET http://vault.vault.svc.cluster.local/api/status</code> from inside the\ncluster; edge probe: <code>vault-tls</code> Ready + https listener ResolvedRefs.</li>\n<li><strong>Backup/recovery:</strong> ADR-0009 AC8 \u2014 <code>pg_dump</code> + age-encrypted off-host\nbackup, restore REHEARSED before the service is considered production\n(recovery contract compensates the accepted correlated-failure trade-off\nof in-cluster datastores).</li>\n</ul>\n<h2 id=\"bkmrk-estado-honesto-%28live\">Estado honesto (live, 2026-08-31 20:14Z)</h2>\n<p id=\"bkmrk-live-%E2%80%94-vault.wetopi.\"><strong>LIVE \u2014 vault.wetopi.net serving HTTPS 200 with LE cert.</strong> End-to-end\nverify after the operator's tofu DNS apply (vault.wetopi.net \u2192\n79.137.114.193/.194): Web UI 200 via both edge IPs, certificate\n<code>vault-tls</code> (Let's Encrypt, valid 2026-08-31\u21922026-11-29), pods 3/3\nRunning (infisical + postgres + redis on ceph-rbd PVCs), <code>/api/status</code>\n<code>Ok</code> with Redis configured. Two lead-direct hotfixes landed in Git during\nthe apply: commit <code>3a87ee94</code> (entrypoint ConfigMap <code>defaultMode: 0755</code> \u2014\nrunAsUser 1001 exec permission, crashloop fix) and <code>7e0b3a0b</code> (infisical\nmemory limit 1Gi\u21922Gi after an OOMKill at 19:59Z, V8 heap 2.3GB).\nThe deny rules D1\u2013D3 (machine-API paths \u2192 nonexistent Service\n<code>vault-api-deny</code>) are fail-closed BY SPEC \u2014 verified: <code>/api/v1/secrets</code>\nreturns 500 from the public edge; the Argo <code>Degraded</code> on the app is the\ndocumented false positive of this mechanism (ResolvedRefs=False is the\ndeny working, not a fault). PENDING: operator admin bootstrap + TOTP QR\n(human step).</p>\n<h2 id=\"bkmrk-relay-links-%28enlaces\">Relay links (enlaces de relevo)</h2>\n<ul id=\"bkmrk-manifests-%28github%29%3A-\">\n<li><strong>Manifests (GitHub):</strong> <a href=\"https://github.com/wetopi-platform/infrastructure/tree/main/kubernetes/vault\">kubernetes/vault</a>, <a href=\"https://github.com/wetopi-platform/infrastructure/tree/main/kubernetes/infisical-operator\">kubernetes/infisical-operator</a></li>\n<li><strong>Vault (https://vault.wetopi.net \u2192 project <code>wetopi-harness</code>):</strong> this is\nthe service that hosts the rest of the platform's secrets \u2014 machine\nidentities and secret paths live under project <code>wetopi-harness</code> (e.g. the\ndrill identity <code>drill-probe-&lt;date&gt;</code> in the README proof). Secret values\nare never in this repo or the docs; reference paths only (DocOps \u00a78).</li>\n<li><strong>Related docs:</strong> <a href=\"https://book.wetopi.net/decisions/ADR-0009-secrets-vault-harness.md\">ADR-0009</a> (accepted \u2014 the decision this ficha documents), <a href=\"https://book.wetopi.net/objectives/active/OBJ-VAULT-001.md\">OBJ-VAULT-001</a> (objective + execution ledger), <a href=\"https://book.wetopi.net/architecture/vault-system/index.md\">vault-system spec</a>, <a href=\"https://book.wetopi.net/index.md\">services index</a>.</li>\n</ul>\n"
 },
 {
  "id": 70,
  "title": "SVC-BOOKSTACK \u2014 book.wetopi.net",
  "slug": "svc-bookstack-bookwetopinet",
  "book_id": 3,
  "chapter_id": 21,
  "description": null,
  "created_at": "2026-09-06T13:39:31.000000Z",
  "updated_at": "2026-09-06T21:45:52.000000Z",
  "markdown": "---\nid: SVC-BOOKSTACK\ntitle: BookStack \u2014 wiki vivo del d\u00eda a d\u00eda (book.wetopi.net)\ntype: service\nstatus: active\nowner: wetopi-lead\nsummary: Wiki vivo (Living Knowledge) del Knowledge Plane; sirve borradores, evidencia y how-to operativo.\nlast_verified: 2026-09-06\n---\n\n# SVC-BOOKSTACK: BookStack\n\n**\u00bfQu\u00e9 es?** Wiki de conocimiento vivo (book.wetopi.net): el almac\u00e9n \"Living Knowledge\" del Knowledge Plane. Sede del Workbench de agentes (book 90), la evidencia de investigaciones y los how-to del d\u00eda a d\u00eda. NO es el SSOT de decisiones \u2014 eso es Git.\n\n**\u00bfQu\u00e9 hace?** Books por dominio (00-90), chapters, p\u00e1ginas con tags #draft/#ready/#superseded, b\u00fasqueda global, historial de revisiones, API REST program\u00e1tica para los agentes.\n\n**\u00bfC\u00f3mo est\u00e1 desplegado?**\n- ns `bookstack`: Deployment `bookstack` (lscr.io/linuxserver/bookstack@sha256:90a3df31\u2026, v26.05.4) + StatefulSet `bookstack-mariadb-0` (mariadb 11.8 @sha256:2439dcd7\u2026, ceph-rbd 10Gi)\n- PVC `bookstack-data` (ceph-rbd 10Gi), strategy Recreate (RWO single-writer)\n- Service ClusterIP :80; HTTPRoute book.wetopi.net \u2192 middleware `allowlist-middleware-bookstack` (.87/.89) \u2192 Gateway wetopi-edge\n- Certificate `book-tls` (kube-system, LE PROD DNS-01, v\u00e1lido hasta 2026-12-05)\n- Argo app `bookstack` \u2192 platform-bookstack@main (Synced+Healthy), ancla en platform-gitops (include-glob 7 manifests)\n\n**\u00bfC\u00f3mo se opera?**\n- API tokens por agente en BBDD (api_tokens) y en Infisical `services/bookstack/agents/<agent>/{BOOKSTACK_TOKEN_ID,BOOKSTACK_TOKEN_SECRET}` (E2E 200 verificado)\n- Admin: carlos@wetopi.net (vault ADMIN_*), `php artisan bookstack:create-admin --initial` para reset\n- Credenciales: Infisical `services/bookstack/bookstack` (9 keys)\n\n**\u00bfC\u00f3mo se rompe y se arregla?**\n- Pod CreateContainerConfigError tras recrear ns \u2192 reaplicar `bookstack-secrets` (Infisical) \u2014 el Secret no sobrevive al ns borrado\n- Cert tumbado \u2192 certRef del Gateway espera a Certificate Ready=True (fail-safe co-tenant)\n- 403 desde fuera = allowlist trabajando (verificar IP de origen antes de diagnosticar)\n- Los secretos del vault van al pod via apply del Secret (no hay Infisical operator para este ns \u2014 deuda anotada)",
  "html": "<hr id=\"bkmrk-\">\n<h2 id=\"bkmrk-id%3A-svc-bookstack-ti\">id: SVC-BOOKSTACK\ntitle: BookStack \u2014 wiki vivo del d\u00eda a d\u00eda (book.wetopi.net)\ntype: service\nstatus: active\nowner: wetopi-lead\nsummary: Wiki vivo (Living Knowledge) del Knowledge Plane; sirve borradores, evidencia y how-to operativo.\nlast_verified: 2026-09-06</h2>\n<h1 id=\"bkmrk-svc-bookstack%3A-books\">SVC-BOOKSTACK: BookStack</h1>\n<p id=\"bkmrk-%C2%BFqu%C3%A9-es%3F-wiki-de-con\"><strong>\u00bfQu\u00e9 es?</strong> Wiki de conocimiento vivo (book.wetopi.net): el almac\u00e9n \"Living Knowledge\" del Knowledge Plane. Sede del Workbench de agentes (book 90), la evidencia de investigaciones y los how-to del d\u00eda a d\u00eda. NO es el SSOT de decisiones \u2014 eso es Git.</p>\n<p id=\"bkmrk-%C2%BFqu%C3%A9-hace%3F-books-por\"><strong>\u00bfQu\u00e9 hace?</strong> Books por dominio (00-90), chapters, p\u00e1ginas con tags #draft/#ready/#superseded, b\u00fasqueda global, historial de revisiones, API REST program\u00e1tica para los agentes.</p>\n<p id=\"bkmrk-%C2%BFc%C3%B3mo-est%C3%A1-desplegad\"><strong>\u00bfC\u00f3mo est\u00e1 desplegado?</strong></p>\n<ul id=\"bkmrk-ns-bookstack%3A-deploy\">\n<li>ns <code>bookstack</code>: Deployment <code>bookstack</code> (lscr.io/linuxserver/bookstack@sha256:90a3df31\u2026, v26.05.4) + StatefulSet <code>bookstack-mariadb-0</code> (mariadb 11.8 @sha256:2439dcd7\u2026, ceph-rbd 10Gi)</li>\n<li>PVC <code>bookstack-data</code> (ceph-rbd 10Gi), strategy Recreate (RWO single-writer)</li>\n<li>Service ClusterIP :80; HTTPRoute book.wetopi.net \u2192 middleware <code>allowlist-middleware-bookstack</code> (.87/.89) \u2192 Gateway wetopi-edge</li>\n<li>Certificate <code>book-tls</code> (kube-system, LE PROD DNS-01, v\u00e1lido hasta 2026-12-05)</li>\n<li>Argo app <code>bookstack</code> \u2192 platform-bookstack@main (Synced+Healthy), ancla en platform-gitops (include-glob 7 manifests)</li>\n</ul>\n<p id=\"bkmrk-%C2%BFc%C3%B3mo-se-opera%3F\"><strong>\u00bfC\u00f3mo se opera?</strong></p>\n<ul id=\"bkmrk-api-tokens-por-agent\">\n<li>API tokens por agente en BBDD (api_tokens) y en Infisical <code>services/bookstack/agents/&lt;agent&gt;/{BOOKSTACK_TOKEN_ID,BOOKSTACK_TOKEN_SECRET}</code> (E2E 200 verificado)</li>\n<li>Admin: carlos@wetopi.net (vault ADMIN_*), <code>php artisan bookstack:create-admin --initial</code> para reset</li>\n<li>Credenciales: Infisical <code>services/bookstack/bookstack</code> (9 keys)</li>\n</ul>\n<p id=\"bkmrk-%C2%BFc%C3%B3mo-se-rompe-y-se-\"><strong>\u00bfC\u00f3mo se rompe y se arregla?</strong></p>\n<ul id=\"bkmrk-pod-createcontainerc\">\n<li>Pod CreateContainerConfigError tras recrear ns \u2192 reaplicar <code>bookstack-secrets</code> (Infisical) \u2014 el Secret no sobrevive al ns borrado</li>\n<li>Cert tumbado \u2192 certRef del Gateway espera a Certificate Ready=True (fail-safe co-tenant)</li>\n<li>403 desde fuera = allowlist trabajando (verificar IP de origen antes de diagnosticar)</li>\n<li>Los secretos del vault van al pod via apply del Secret (no hay Infisical operator para este ns \u2014 deuda anotada)</li>\n</ul>\n"
 },
 {
  "id": 71,
  "title": "SVC-HINDSIGHT \u2014 memory.wetopi.net",
  "slug": "svc-hindsight-memorywetopinet",
  "book_id": 3,
  "chapter_id": 21,
  "description": null,
  "created_at": "2026-09-06T13:39:32.000000Z",
  "updated_at": "2026-09-06T21:45:53.000000Z",
  "markdown": "---\nid: SVC-HINDSIGHT\ntitle: Hindsight \u2014 plano de memoria sem\u00e1ntica (memory.wetopi.net)\ntype: service\nstatus: active\nowner: wetopi-lead\nsummary: Memoria sem\u00e1ntica de la flota (banks personales + corporate v\u00eda plugin wetopi-memory).\nlast_verified: 2026-09-06\n---\n\n# SVC-HINDSIGHT: Hindsight Memory\n\n**\u00bfQu\u00e9 es?** El plano de memoria de la flota (memory.wetopi.net): Hindsight 0.9.2 (ns wetopi-memory) con banks personales user-u_{carlos,joan,josep} (provider nativo) + corporate (plugin wetopi-memory, corporate-only por decisi\u00f3n A1).\n\n**\u00bfQu\u00e9 hace?** Retain/recall/reflect de hechos sem\u00e1nticos con entity graph; punteros a evidencia (regla: la memoria apunta, no almacena). Clasificaci\u00f3n STATE/EVENT/DECISION/PROCEDURE/LEARNING.\n\n**\u00bfC\u00f3mo est\u00e1 desplegado?**\n- ns wetopi-memory: hindsight-api:8888 + hindsight-control-plane:3000 + hindsight-postgresql (pgvector @sha256:956744\u2026, ceph-rbd 20Gi)\n- Argo app hindsight (chart OCI pinned 0.9.2 + values + manifests pgvector/NetworkPolicies)\n- Edge: memory.wetopi.net con allowlist .87/.89; CP API-key en el deployment\n\n**\u00bfC\u00f3mo se opera?**\n- API key del CP: Infisical services/hindsight/hindsight\n- Verificaci\u00f3n: probes HTTPS desde .89 (200) / desde .91 (403 esperado)\n\n**\u00bfC\u00f3mo se rompe y se arregla?**\n- Sin API key en el deployment \u2192 CP logs \"No API key configured\" y el dataplane falla\n- Traefik hostNetwork + netpols con ipBlock 0/0 = hang (lecci\u00f3n BookStack: sin netpols, patr\u00f3n vault/litellm)",
  "html": "<hr id=\"bkmrk-\">\n<h2 id=\"bkmrk-id%3A-svc-hindsight-ti\">id: SVC-HINDSIGHT\ntitle: Hindsight \u2014 plano de memoria sem\u00e1ntica (memory.wetopi.net)\ntype: service\nstatus: active\nowner: wetopi-lead\nsummary: Memoria sem\u00e1ntica de la flota (banks personales + corporate v\u00eda plugin wetopi-memory).\nlast_verified: 2026-09-06</h2>\n<h1 id=\"bkmrk-svc-hindsight%3A-hinds\">SVC-HINDSIGHT: Hindsight Memory</h1>\n<p id=\"bkmrk-%C2%BFqu%C3%A9-es%3F-el-plano-de\"><strong>\u00bfQu\u00e9 es?</strong> El plano de memoria de la flota (memory.wetopi.net): Hindsight 0.9.2 (ns wetopi-memory) con banks personales user-u_{carlos,joan,josep} (provider nativo) + corporate (plugin wetopi-memory, corporate-only por decisi\u00f3n A1).</p>\n<p id=\"bkmrk-%C2%BFqu%C3%A9-hace%3F-retain%2Fre\"><strong>\u00bfQu\u00e9 hace?</strong> Retain/recall/reflect de hechos sem\u00e1nticos con entity graph; punteros a evidencia (regla: la memoria apunta, no almacena). Clasificaci\u00f3n STATE/EVENT/DECISION/PROCEDURE/LEARNING.</p>\n<p id=\"bkmrk-%C2%BFc%C3%B3mo-est%C3%A1-desplegad\"><strong>\u00bfC\u00f3mo est\u00e1 desplegado?</strong></p>\n<ul id=\"bkmrk-ns-wetopi-memory%3A-hi\">\n<li>ns wetopi-memory: hindsight-api:8888 + hindsight-control-plane:3000 + hindsight-postgresql (pgvector @sha256:956744\u2026, ceph-rbd 20Gi)</li>\n<li>Argo app hindsight (chart OCI pinned 0.9.2 + values + manifests pgvector/NetworkPolicies)</li>\n<li>Edge: memory.wetopi.net con allowlist .87/.89; CP API-key en el deployment</li>\n</ul>\n<p id=\"bkmrk-%C2%BFc%C3%B3mo-se-opera%3F\"><strong>\u00bfC\u00f3mo se opera?</strong></p>\n<ul id=\"bkmrk-api-key-del-cp%3A-infi\">\n<li>API key del CP: Infisical services/hindsight/hindsight</li>\n<li>Verificaci\u00f3n: probes HTTPS desde .89 (200) / desde .91 (403 esperado)</li>\n</ul>\n<p id=\"bkmrk-%C2%BFc%C3%B3mo-se-rompe-y-se-\"><strong>\u00bfC\u00f3mo se rompe y se arregla?</strong></p>\n<ul id=\"bkmrk-sin-api-key-en-el-de\">\n<li>Sin API key en el deployment \u2192 CP logs \"No API key configured\" y el dataplane falla</li>\n<li>Traefik hostNetwork + netpols con ipBlock 0/0 = hang (lecci\u00f3n BookStack: sin netpols, patr\u00f3n vault/litellm)</li>\n</ul>\n"
 },
 {
  "id": 72,
  "title": "SVC-REGISTRY \u2014 Zot OCI (planned)",
  "slug": "svc-registry-zot-oci-planned",
  "book_id": 3,
  "chapter_id": 21,
  "description": null,
  "created_at": "2026-09-06T13:39:32.000000Z",
  "updated_at": "2026-09-06T21:45:53.000000Z",
  "markdown": "---\nid: SVC-REGISTRY\ntitle: Zot \u2014 registry OCI interno (DESPLIEGUE PENDIENTE)\ntype: service\nstatus: planned\nowner: wetopi-lead\nsummary: Registry OCI interno (Zot v2.1.2) para CI builds y pulls LAN; NO desplegado en el cluster a\u00fan.\nlast_verified: 2026-09-06\n---\n\n# SVC-REGISTRY: Zot (platform-registry)\n\n**\u00bfQu\u00e9 es?** Registry OCI interno (Zot v2.1.2) previsto para builds de CI self-hosted y pulls LAN.\n\n**\u00bfQu\u00e9 hace?** (dise\u00f1o) Serve self-hosted CI builds (push) + cluster pulls (LAN-fast), path imperativo para despliegues fuera de ArgoCD.\n\n**\u00bfC\u00f3mo est\u00e1 desplegado?** \u26a0 **NO desplegado** \u2014 verificado live 2026-09-06: ns `platform` existe pero NO hay workloads. Los manifests viven en platform-registry (rama feat/zot-initial, sin anclar en Argo). La CI de platform-docs-runtime (R4) apunta a zot.platform.svc:5000 que NO existe a\u00fan \u2014 repin del digest docs-runtime pendiente.\n\n**\u00bfC\u00f3mo se opera?** (cuando exista) htpasswd (runner/admin, Secret zot-auth operator-side), LAN Service :5000, dedupe+GC 24h.\n\n**\u00bfC\u00f3mo se rompe y se arregla?** local-path NO existe en este cluster (solo ceph-rbd \u2014 lecci\u00f3n R2); NodePort prohibido en edge-WAN (invariante RC-5).",
  "html": "<hr id=\"bkmrk-\">\n<h2 id=\"bkmrk-id%3A-svc-registry-tit\">id: SVC-REGISTRY\ntitle: Zot \u2014 registry OCI interno (DESPLIEGUE PENDIENTE)\ntype: service\nstatus: planned\nowner: wetopi-lead\nsummary: Registry OCI interno (Zot v2.1.2) para CI builds y pulls LAN; NO desplegado en el cluster a\u00fan.\nlast_verified: 2026-09-06</h2>\n<h1 id=\"bkmrk-svc-registry%3A-zot-%28p\">SVC-REGISTRY: Zot (platform-registry)</h1>\n<p id=\"bkmrk-%C2%BFqu%C3%A9-es%3F-registry-oc\"><strong>\u00bfQu\u00e9 es?</strong> Registry OCI interno (Zot v2.1.2) previsto para builds de CI self-hosted y pulls LAN.</p>\n<p id=\"bkmrk-%C2%BFqu%C3%A9-hace%3F-%28dise%C3%B1o%29-\"><strong>\u00bfQu\u00e9 hace?</strong> (dise\u00f1o) Serve self-hosted CI builds (push) + cluster pulls (LAN-fast), path imperativo para despliegues fuera de ArgoCD.</p>\n<p id=\"bkmrk-%C2%BFc%C3%B3mo-est%C3%A1-desplegad\"><strong>\u00bfC\u00f3mo est\u00e1 desplegado?</strong> \u26a0 <strong>NO desplegado</strong> \u2014 verificado live 2026-09-06: ns <code>platform</code> existe pero NO hay workloads. Los manifests viven en platform-registry (rama feat/zot-initial, sin anclar en Argo). La CI de platform-docs-runtime (R4) apunta a zot.platform.svc:5000 que NO existe a\u00fan \u2014 repin del digest docs-runtime pendiente.</p>\n<p id=\"bkmrk-%C2%BFc%C3%B3mo-se-opera%3F-%28cua\"><strong>\u00bfC\u00f3mo se opera?</strong> (cuando exista) htpasswd (runner/admin, Secret zot-auth operator-side), LAN Service :5000, dedupe+GC 24h.</p>\n<p id=\"bkmrk-%C2%BFc%C3%B3mo-se-rompe-y-se-\"><strong>\u00bfC\u00f3mo se rompe y se arregla?</strong> local-path NO existe en este cluster (solo ceph-rbd \u2014 lecci\u00f3n R2); NodePort prohibido en edge-WAN (invariante RC-5).</p>\n"
 },
 {
  "id": 73,
  "title": "SVC-LITELLM \u2014 infer.wetopi.net",
  "slug": "svc-litellm-inferwetopinet",
  "book_id": 3,
  "chapter_id": 21,
  "description": null,
  "created_at": "2026-09-06T13:39:32.000000Z",
  "updated_at": "2026-09-06T21:45:53.000000Z",
  "markdown": "---\nid: SVC-LITELLM\ntitle: LiteLLM \u2014 proxy LLM de la flota (infer.wetopi.net)\ntype: service\nstatus: active\nowner: wetopi-lead\nsummary: OpenAI-compatible proxy (infer.wetopi.net) con Postgres full-mode, virtual keys por agente.\nlast_verified: 2026-09-06\n---\n\n# SVC-LITELLM: LiteLLM Proxy\n\n**\u00bfQu\u00e9 es?** Proxy OpenAI-compatible (infer.wetopi.net) para toda la flota Hermes, con virtual key por agente (carlos/joan/josep) y spend attribution por key_alias.\n\n**\u00bfQu\u00e9 hace?** Routing a upstreams LLM, budgets, usage tracking (SpendLogs \u2192 BBDD litellm-postgres \u2192 dashboards LLM-spend en Grafana).\n\n**\u00bfC\u00f3mo est\u00e1 desplegado?**\n- ns litellm: Deployment litellm v1.99.0 (2Gi limits \u2014 sizing OOM lecci\u00f3n L12) + postgres:16\n- Edge: infer.wetopi.net, allowlist .87/.89, LE cert\n- Argo: 2 apps (litellm + litellm-postgres) \u2192 platform-litellm@main\n\n**\u00bfC\u00f3mo se opera?**\n- Keys de agentes en vault root `LITELLM-<agent>_hermes_wetopi_key`\n- ConfigMap NO hot-reload: tras sync \u2192 `kubectl -n litellm rollout restart deployment/litellm`\n\n**\u00bfC\u00f3mo se rompe y se arregla?**\n- OOMKill al arrancar con 1Gi (full-mode + Prisma/DB) \u2192 sizing 2Gi/512Mi\n- \"No route to host\" al upstream llm.wetopi.net:443 = red cluster\u2192WAN bloqueada (NO es problema de key \u2014 /v1/models con 200 prueba la key v\u00e1lida)",
  "html": "<hr id=\"bkmrk-\">\n<h2 id=\"bkmrk-id%3A-svc-litellm-titl\">id: SVC-LITELLM\ntitle: LiteLLM \u2014 proxy LLM de la flota (infer.wetopi.net)\ntype: service\nstatus: active\nowner: wetopi-lead\nsummary: OpenAI-compatible proxy (infer.wetopi.net) con Postgres full-mode, virtual keys por agente.\nlast_verified: 2026-09-06</h2>\n<h1 id=\"bkmrk-svc-litellm%3A-litellm\">SVC-LITELLM: LiteLLM Proxy</h1>\n<p id=\"bkmrk-%C2%BFqu%C3%A9-es%3F-proxy-opena\"><strong>\u00bfQu\u00e9 es?</strong> Proxy OpenAI-compatible (infer.wetopi.net) para toda la flota Hermes, con virtual key por agente (carlos/joan/josep) y spend attribution por key_alias.</p>\n<p id=\"bkmrk-%C2%BFqu%C3%A9-hace%3F-routing-a\"><strong>\u00bfQu\u00e9 hace?</strong> Routing a upstreams LLM, budgets, usage tracking (SpendLogs \u2192 BBDD litellm-postgres \u2192 dashboards LLM-spend en Grafana).</p>\n<p id=\"bkmrk-%C2%BFc%C3%B3mo-est%C3%A1-desplegad\"><strong>\u00bfC\u00f3mo est\u00e1 desplegado?</strong></p>\n<ul id=\"bkmrk-ns-litellm%3A-deployme\">\n<li>ns litellm: Deployment litellm v1.99.0 (2Gi limits \u2014 sizing OOM lecci\u00f3n L12) + postgres:16</li>\n<li>Edge: infer.wetopi.net, allowlist .87/.89, LE cert</li>\n<li>Argo: 2 apps (litellm + litellm-postgres) \u2192 platform-litellm@main</li>\n</ul>\n<p id=\"bkmrk-%C2%BFc%C3%B3mo-se-opera%3F\"><strong>\u00bfC\u00f3mo se opera?</strong></p>\n<ul id=\"bkmrk-keys-de-agentes-en-v\">\n<li>Keys de agentes en vault root <code>LITELLM-&lt;agent&gt;_hermes_wetopi_key</code></li>\n<li>ConfigMap NO hot-reload: tras sync \u2192 <code>kubectl -n litellm rollout restart deployment/litellm</code></li>\n</ul>\n<p id=\"bkmrk-%C2%BFc%C3%B3mo-se-rompe-y-se-\"><strong>\u00bfC\u00f3mo se rompe y se arregla?</strong></p>\n<ul id=\"bkmrk-oomkill-al-arrancar-\">\n<li>OOMKill al arrancar con 1Gi (full-mode + Prisma/DB) \u2192 sizing 2Gi/512Mi</li>\n<li>\"No route to host\" al upstream llm.wetopi.net:443 = red cluster\u2192WAN bloqueada (NO es problema de key \u2014 /v1/models con 200 prueba la key v\u00e1lida)</li>\n</ul>\n"
 },
 {
  "id": 74,
  "title": "CONTRATO \u2014 reglas del Workbench (l\u00e9eme primero)",
  "slug": "contrato-reglas-del-workbench-leeme-primero",
  "book_id": 10,
  "chapter_id": 30,
  "description": null,
  "created_at": "2026-09-06T13:39:40.000000Z",
  "updated_at": "2026-09-06T13:39:40.000000Z",
  "markdown": "---\nid: PROC-WORKBENCH\ntitle: Contrato del Agents Workbench (L1) \u2014 reglas y naming\ntype: procedure\nstatus: active\nowner: wetopi-lead\nsummary: C\u00f3mo los agentes usan el Workbench para borradores, evidencia y promoci\u00f3n al Git.\nlast_verified: 2026-09-06\n---\n\n# Agents Workbench \u2014 contrato de uso\n\n## Qu\u00e9 es\n\nEl book 90 es el **L1 del circuito de conocimiento**: borradores, evidencia\nde investigaciones y docs intermedios de los agentes. No es el SSOT de nada\n\u2014 cuando algo se consolida, se promociona a Git (PR) y esta copia muere.\n\n## Estructura\n\n- Chapter por agente (`carlos`, `joan`, `josep`) \u2014 nunca escribir en el de otro.\n- Naming: `WIP-<fecha>-<tema>`, `DRAFT-ADR-<nnnn>-<slug>`, `EVIDENCE-<slug>`.\n- Tags de estado: `#draft` \u2192 `#ready` \u2192 `#superseded` (tras promocionar).\n\n## Ritual de promoci\u00f3n\n\n1. Crea la p\u00e1gina en tu chapter con `#draft`.\n2. Cuando estabiliza: marca `#ready` y abre PR al destino formal (docs / ai-hub).\n3. Tras el merge: edita la p\u00e1gina a `PROMOVED \u2192 <link al doc en Git>` y tag `#superseded`.\n\n## Qu\u00e9 va en el Workbench\n\n- Evidencia de investigaciones (INFORME.md, RECON.md, dumps \u2192 como p\u00e1gina)\n- Borradores de ADR y RFC antes de abrir el PR\n- Planes largos pre-kanban (el card kanban enlaza aqu\u00ed)\n\n## Qu\u00e9 NO va\n\n- Secretos de ning\u00fan tipo (paths de Infisical, nunca valores)\n- Copias de documentos ya institucionales (solo links)\n- Conversaciones crudas (eso es cosa de la memoria: hechos + puntero)\n\n## Recursos\n\n- API tokens: Infisical `services/bookstack/agents/<agent>/`\n- Skill del contrato: `wetopi-docops` (flota completa)\n- Fuente de las normas: `docs/objectives/active/OBJ-KDOCPANE-001.md` (Git)",
  "html": "<hr id=\"bkmrk-\">\n<h2 id=\"bkmrk-id%3A-proc-workbench-t\">id: PROC-WORKBENCH\ntitle: Contrato del Agents Workbench (L1) \u2014 reglas y naming\ntype: procedure\nstatus: active\nowner: wetopi-lead\nsummary: C\u00f3mo los agentes usan el Workbench para borradores, evidencia y promoci\u00f3n al Git.\nlast_verified: 2026-09-06</h2>\n<h1 id=\"bkmrk-agents-workbench-%E2%80%94-c\">Agents Workbench \u2014 contrato de uso</h1>\n<h2 id=\"bkmrk-qu%C3%A9-es\">Qu\u00e9 es</h2>\n<p id=\"bkmrk-el-book-90-es-el-l1-\">El book 90 es el <strong>L1 del circuito de conocimiento</strong>: borradores, evidencia\nde investigaciones y docs intermedios de los agentes. No es el SSOT de nada\n\u2014 cuando algo se consolida, se promociona a Git (PR) y esta copia muere.</p>\n<h2 id=\"bkmrk-estructura\">Estructura</h2>\n<ul id=\"bkmrk-chapter-por-agente-%28\">\n<li>Chapter por agente (<code>carlos</code>, <code>joan</code>, <code>josep</code>) \u2014 nunca escribir en el de otro.</li>\n<li>Naming: <code>WIP-&lt;fecha&gt;-&lt;tema&gt;</code>, <code>DRAFT-ADR-&lt;nnnn&gt;-&lt;slug&gt;</code>, <code>EVIDENCE-&lt;slug&gt;</code>.</li>\n<li>Tags de estado: <code>#draft</code> \u2192 <code>#ready</code> \u2192 <code>#superseded</code> (tras promocionar).</li>\n</ul>\n<h2 id=\"bkmrk-ritual-de-promoci%C3%B3n\">Ritual de promoci\u00f3n</h2>\n<ol id=\"bkmrk-crea-la-p%C3%A1gina-en-tu\">\n<li>Crea la p\u00e1gina en tu chapter con <code>#draft</code>.</li>\n<li>Cuando estabiliza: marca <code>#ready</code> y abre PR al destino formal (docs / ai-hub).</li>\n<li>Tras el merge: edita la p\u00e1gina a <code>PROMOVED \u2192 &lt;link al doc en Git&gt;</code> y tag <code>#superseded</code>.</li>\n</ol>\n<h2 id=\"bkmrk-qu%C3%A9-va-en-el-workben\">Qu\u00e9 va en el Workbench</h2>\n<ul id=\"bkmrk-evidencia-de-investi\">\n<li>Evidencia de investigaciones (INFORME.md, RECON.md, dumps \u2192 como p\u00e1gina)</li>\n<li>Borradores de ADR y RFC antes de abrir el PR</li>\n<li>Planes largos pre-kanban (el card kanban enlaza aqu\u00ed)</li>\n</ul>\n<h2 id=\"bkmrk-qu%C3%A9-no-va\">Qu\u00e9 NO va</h2>\n<ul id=\"bkmrk-secretos-de-ning%C3%BAn-t\">\n<li>Secretos de ning\u00fan tipo (paths de Infisical, nunca valores)</li>\n<li>Copias de documentos ya institucionales (solo links)</li>\n<li>Conversaciones crudas (eso es cosa de la memoria: hechos + puntero)</li>\n</ul>\n<h2 id=\"bkmrk-recursos\">Recursos</h2>\n<ul id=\"bkmrk-api-tokens%3A-infisica\">\n<li>API tokens: Infisical <code>services/bookstack/agents/&lt;agent&gt;/</code></li>\n<li>Skill del contrato: <code>wetopi-docops</code> (flota completa)</li>\n<li>Fuente de las normas: <code>docs/objectives/active/OBJ-KDOCPANE-001.md</code> (Git)</li>\n</ul>\n"
 },
 {
  "id": 75,
  "title": "SVC-DOCS-RUNTIME \u2014 docs.wetopi.net (canal runtime)",
  "slug": "svc-docs-runtime-docswetopinet-canal-runtime",
  "book_id": 3,
  "chapter_id": 21,
  "description": null,
  "created_at": "2026-09-06T21:46:09.000000Z",
  "updated_at": "2026-09-06T21:46:10.000000Z",
  "markdown": "---\nid: SVC-DOCS-RUNTIME\ntitle: docs.wetopi.net \u2014 canal runtime con auto-rebuild (G1+G2)\ntype: service\nstatus: active\nowner: wetopi-lead\nsummary: MkDocs Material servido por pod runtime con rebuild-on-webhook; commit a main = publicado <10s.\nlast_verified: 2026-09-06\n---\n\n# SVC-DOCS-RUNTIME: docs.wetopi.net (canal runtime)\n\n**\u00bfQu\u00e9 es?** El canal ACTIVO de docs.wetopi.net (desde 2026-09-06, flip G1+G2): un pod que\nrebuild-ea MkDocs Material desde `wetopi-platform/docs@main` al recibir el webhook de GitHub.\n**Commit a main = publicado en <10s**, sin rehacer imagen.\n\n**\u00bfQu\u00e9 hace?**\n- Webhook HMAC-SHA256 (push-only a main) \u2192 git pull \u2192 mkdocs --strict \u2192 swap at\u00f3mico de symlink\n- Build fallido = sigue sirviendo el \u00faltimo build bueno (nunca 500)\n- Footer con el sha7 de la build servida (identidad de versi\u00f3n)\n- Tests negativos: sin firma/firma incorrecta/evento\u2260push \u2192 403\n\n**\u00bfC\u00f3mo est\u00e1 desplegado?**\n- ns `docs-site`: Deployment `docs-runtime` (imagen can\u00f3nica GHCR `@sha256:b00efab8\u2026`)\n  + Service docs-runtime-site (:80\u21928000 site) + Service docs-runtime-webhook (:80\u21928080)\n- HTTPRoute docs-runtime (PathPrefix /) + docs-runtime-webhook (Exact /hooks/rebuild, sin allowlist \u2014 HMAC es la auth)\n- Argo app `docs-runtime` \u2192 platform-docs-runtime@main (Synced+Healthy)\n- Webhook GitHub id 675408228\n\n**\u00bfC\u00f3mo se opera?**\n- Push a docs@main \u2192 publicado <10s (E2E verificado: 80137f4 en 5s)\n- Rebuild manual: POST firmado a /hooks/rebuild (secret: Infisical services/docs-runtime/WEBHOOK_HMAC_SECRET)\n- Creds: Infisical services/docs-runtime/{WEBHOOK_HMAC_SECRET, GITHUB_PAT}\n\n**\u00bfC\u00f3mo se rompe y se arregla?**\n- 404 fail-closed del site = ExtensionRef unresolvable en el route (Ley 4) \u2192 check filters del route vs middlewares existentes\n- 401 con WWW-Authenticate \u2192 el route lleva basic-auth retirado (ADR-0014) \u2014 quitarlo\n- Secret '/dev/stdin' literal \u2192 --from-literal NO consume stdin; usar --from-file=/dev/stdin\n- selfHeal revierte patches live \u2192 el fix SIEMPRE via Git (Ley 1)\n- Imagen pineada a GHCR: repin a Zot pendiente (Zot sin desplegar)\n- Deuda G3: retirar deployment legacy docs-site tras 7 d\u00edas de estabilidad (~13-09)",
  "html": "<hr id=\"bkmrk-\">\n<h2 id=\"bkmrk-id%3A-svc-docs-runtime\">id: SVC-DOCS-RUNTIME\ntitle: docs.wetopi.net \u2014 canal runtime con auto-rebuild (G1+G2)\ntype: service\nstatus: active\nowner: wetopi-lead\nsummary: MkDocs Material servido por pod runtime con rebuild-on-webhook; commit a main = publicado &lt;10s.\nlast_verified: 2026-09-06</h2>\n<h1 id=\"bkmrk-svc-docs-runtime%3A-do\">SVC-DOCS-RUNTIME: docs.wetopi.net (canal runtime)</h1>\n<p id=\"bkmrk-%C2%BFqu%C3%A9-es%3F-el-canal-ac\"><strong>\u00bfQu\u00e9 es?</strong> El canal ACTIVO de docs.wetopi.net (desde 2026-09-06, flip G1+G2): un pod que\nrebuild-ea MkDocs Material desde <code>wetopi-platform/docs@main</code> al recibir el webhook de GitHub.\n<strong>Commit a main = publicado en &lt;10s</strong>, sin rehacer imagen.</p>\n<p id=\"bkmrk-%C2%BFqu%C3%A9-hace%3F\"><strong>\u00bfQu\u00e9 hace?</strong></p>\n<ul id=\"bkmrk-webhook-hmac-sha256-\">\n<li>Webhook HMAC-SHA256 (push-only a main) \u2192 git pull \u2192 mkdocs --strict \u2192 swap at\u00f3mico de symlink</li>\n<li>Build fallido = sigue sirviendo el \u00faltimo build bueno (nunca 500)</li>\n<li>Footer con el sha7 de la build servida (identidad de versi\u00f3n)</li>\n<li>Tests negativos: sin firma/firma incorrecta/evento\u2260push \u2192 403</li>\n</ul>\n<p id=\"bkmrk-%C2%BFc%C3%B3mo-est%C3%A1-desplegad\"><strong>\u00bfC\u00f3mo est\u00e1 desplegado?</strong></p>\n<ul id=\"bkmrk-ns-docs-site%3A-deploy\">\n<li>ns <code>docs-site</code>: Deployment <code>docs-runtime</code> (imagen can\u00f3nica GHCR <code>@sha256:b00efab8\u2026</code>)\n<ul>\n<li>Service docs-runtime-site (:80\u21928000 site) + Service docs-runtime-webhook (:80\u21928080)</li>\n</ul>\n</li>\n<li>HTTPRoute docs-runtime (PathPrefix /) + docs-runtime-webhook (Exact /hooks/rebuild, sin allowlist \u2014 HMAC es la auth)</li>\n<li>Argo app <code>docs-runtime</code> \u2192 platform-docs-runtime@main (Synced+Healthy)</li>\n<li>Webhook GitHub id 675408228</li>\n</ul>\n<p id=\"bkmrk-%C2%BFc%C3%B3mo-se-opera%3F\"><strong>\u00bfC\u00f3mo se opera?</strong></p>\n<ul id=\"bkmrk-push-a-docs%40main-%E2%86%92-p\">\n<li>Push a docs@main \u2192 publicado &lt;10s (E2E verificado: 80137f4 en 5s)</li>\n<li>Rebuild manual: POST firmado a /hooks/rebuild (secret: Infisical services/docs-runtime/WEBHOOK_HMAC_SECRET)</li>\n<li>Creds: Infisical services/docs-runtime/{WEBHOOK_HMAC_SECRET, GITHUB_PAT}</li>\n</ul>\n<p id=\"bkmrk-%C2%BFc%C3%B3mo-se-rompe-y-se-\"><strong>\u00bfC\u00f3mo se rompe y se arregla?</strong></p>\n<ul id=\"bkmrk-404-fail-closed-del-\">\n<li>404 fail-closed del site = ExtensionRef unresolvable en el route (Ley 4) \u2192 check filters del route vs middlewares existentes</li>\n<li>401 con WWW-Authenticate \u2192 el route lleva basic-auth retirado (ADR-0014) \u2014 quitarlo</li>\n<li>Secret '/dev/stdin' literal \u2192 --from-literal NO consume stdin; usar --from-file=/dev/stdin</li>\n<li>selfHeal revierte patches live \u2192 el fix SIEMPRE via Git (Ley 1)</li>\n<li>Imagen pineada a GHCR: repin a Zot pendiente (Zot sin desplegar)</li>\n<li>Deuda G3: retirar deployment legacy docs-site tras 7 d\u00edas de estabilidad (~13-09)</li>\n</ul>\n"
 },
 {
  "id": 76,
  "title": "SVC-HERMES-FLEET \u2014 flota de agentes",
  "slug": "svc-hermes-fleet-flota-de-agentes",
  "book_id": 7,
  "chapter_id": 24,
  "description": null,
  "created_at": "2026-09-06T21:46:10.000000Z",
  "updated_at": "2026-09-06T21:46:10.000000Z",
  "markdown": "---\nid: SVC-HERMES-FLEET\ntitle: Flota Hermes \u2014 3 gateways (carlos/joan/josep)\ntype: service\nstatus: active\nowner: wetopi-lead\nsummary: 3 instancias Hermes en agents01 con LiteLLM virtual keys, Hindsight y allowlist .89.\nlast_verified: 2026-09-06\n---\n\n# SVC-HERMES-FLEET: Flota Hermes\n\n**\u00bfQu\u00e9 es?** 3 instancias de Hermes Agent (carlos/joan/josep) corriendo en agents01\n(92.222.245.89, host bigardo), en `/opt/hermes-<n>/`.\n\n**\u00bfQu\u00e9 hace?** Operaci\u00f3n de plataforma, skills compartidas, memoria (nativo personal +\nplugin corporate), cron jobs, delegaci\u00f3n a subagentes.\n\n**\u00bfC\u00f3mo est\u00e1 desplegado?**\n- Docker en agents01 (no k8s): /opt/hermes-carlos, /opt/hermes-joan, /opt/hermes-josep\n- LLM v\u00eda LiteLLM (infer.wetopi.net) con virtual key por agente (vault root `LITELLM-<agent>_hermes_wetopi_key`)\n- Memoria: Hindsight personal (user-u_<agent>, memory.wetopi.net) + plugin wetopi-memory (corporate)\n- Egress IP: 92.222.245.89/32 (allowlisted en todos los edges de la plataforma)\n\n**\u00bfC\u00f3mo se opera?**\n- SSH: `ssh -i /opt/data/.ssh/bigardo root@92.222.245.89` (config lan-config del harness)\n- Skills compartidas: replicar a `/opt/hermes-<n>/data/skills/` + verificaci\u00f3n sha256\n- Dashboards: agents01 hermes-<n> dashboards (form login, no basic-auth)\n\n**\u00bfC\u00f3mo se rompe y se arregla?**\n- Profiles nuevos no heredan providers \u2192 a\u00f1adir keys en .env\n- El skill loader es per-sesi\u00f3n: las skills nuevas aparecen al reiniciar sesi\u00f3n",
  "html": "<hr id=\"bkmrk-\">\n<h2 id=\"bkmrk-id%3A-svc-hermes-fleet\">id: SVC-HERMES-FLEET\ntitle: Flota Hermes \u2014 3 gateways (carlos/joan/josep)\ntype: service\nstatus: active\nowner: wetopi-lead\nsummary: 3 instancias Hermes en agents01 con LiteLLM virtual keys, Hindsight y allowlist .89.\nlast_verified: 2026-09-06</h2>\n<h1 id=\"bkmrk-svc-hermes-fleet%3A-fl\">SVC-HERMES-FLEET: Flota Hermes</h1>\n<p id=\"bkmrk-%C2%BFqu%C3%A9-es%3F-3-instancia\"><strong>\u00bfQu\u00e9 es?</strong> 3 instancias de Hermes Agent (carlos/joan/josep) corriendo en agents01\n(92.222.245.89, host bigardo), en <code>/opt/hermes-&lt;n&gt;/</code>.</p>\n<p id=\"bkmrk-%C2%BFqu%C3%A9-hace%3F-operaci%C3%B3n\"><strong>\u00bfQu\u00e9 hace?</strong> Operaci\u00f3n de plataforma, skills compartidas, memoria (nativo personal +\nplugin corporate), cron jobs, delegaci\u00f3n a subagentes.</p>\n<p id=\"bkmrk-%C2%BFc%C3%B3mo-est%C3%A1-desplegad\"><strong>\u00bfC\u00f3mo est\u00e1 desplegado?</strong></p>\n<ul id=\"bkmrk-docker-en-agents01-%28\">\n<li>Docker en agents01 (no k8s): /opt/hermes-carlos, /opt/hermes-joan, /opt/hermes-josep</li>\n<li>LLM v\u00eda LiteLLM (infer.wetopi.net) con virtual key por agente (vault root <code>LITELLM-&lt;agent&gt;_hermes_wetopi_key</code>)</li>\n<li>Memoria: Hindsight personal (user-u_, memory.wetopi.net) + plugin wetopi-memory (corporate)</li>\n<li>Egress IP: 92.222.245.89/32 (allowlisted en todos los edges de la plataforma)</li>\n</ul>\n<p id=\"bkmrk-%C2%BFc%C3%B3mo-se-opera%3F\"><strong>\u00bfC\u00f3mo se opera?</strong></p>\n<ul id=\"bkmrk-ssh%3A-ssh--i-%2Fopt%2Fdat\">\n<li>SSH: <code>ssh -i /opt/data/.ssh/bigardo root@92.222.245.89</code> (config lan-config del harness)</li>\n<li>Skills compartidas: replicar a <code>/opt/hermes-&lt;n&gt;/data/skills/</code> + verificaci\u00f3n sha256</li>\n<li>Dashboards: agents01 hermes- dashboards (form login, no basic-auth)</li>\n</ul>\n<p id=\"bkmrk-%C2%BFc%C3%B3mo-se-rompe-y-se-\"><strong>\u00bfC\u00f3mo se rompe y se arregla?</strong></p>\n<ul id=\"bkmrk-profiles-nuevos-no-h\">\n<li>Profiles nuevos no heredan providers \u2192 a\u00f1adir keys en .env</li>\n<li>El skill loader es per-sesi\u00f3n: las skills nuevas aparecen al reiniciar sesi\u00f3n</li>\n</ul>\n"
 },
 {
  "id": 77,
  "title": "SVC-MEMORY-PLANE \u2014 memoria dual",
  "slug": "svc-memory-plane-memoria-dual",
  "book_id": 7,
  "chapter_id": 26,
  "description": null,
  "created_at": "2026-09-06T21:46:10.000000Z",
  "updated_at": "2026-09-06T21:46:10.000000Z",
  "markdown": "---\nid: SVC-MEMORY-PLANE\ntitle: Memory Plane \u2014 Hindsight nativo + plugin wetopi-memory\ntype: service\nstatus: active\nowner: wetopi-lead\nsummary: Memoria dual: nativa (personal, sin filtro) + plugin (corporate, con pipeline y ACL).\nlast_verified: 2026-09-06\n---\n\n# SVC-MEMORY-PLANE: Memoria de la flota\n\n**\u00bfQu\u00e9 es?** Sistema dual de memoria (decisi\u00f3n A1 2026-09-06):\n- **Nativo** (Hindsight provider): bank personal user-u_<agent>, retiene TODO sin filtro\n- **Plugin wetopi-memory**: banks corporate (wetopi-platform/wetopi-business), pipeline\n  (se\u00f1al + evidencia + ScopeACL + SecurityPolicy + CorporateRelevance + RetainPolicy)\n\n**\u00bfQu\u00e9 hace?** Retain/recall/reflect con fact extraction, entity graph, y clasificaci\u00f3n\nSTATE/EVENT/DECISION/PROCEDURE/LEARNING. Principio: **la memoria apunta, no almacena**\n(hecho esencial + timestamp + puntero a URL, nunca el documento).\n\n**\u00bfC\u00f3mo est\u00e1 desplegado?**\n- Hindsight 0.9.2 (ns wetopi-memory): hindsight-api:8888 + control-plane:3000 + pgvector\n- Edge: memory.wetopi.net con allowlist .87/.89\n- Plugin: hermes-runtime/plugins/wetopi-memory en ai-hub (V0.1-V0.3, 197 tests)\n\n**\u00bfC\u00f3mo se opera?**\n- ADR de memoria: ai-hub/docs/wetopi-memory/adr-memory-intent.md\n- Reglas Carlos (2026-09-06): hip\u00f3tesis NO verificadas no se memorializan; el diagn\u00f3stico\n  retratado corrige la memoria derivada; capture broadly, store compactly\n\n**\u00bfC\u00f3mo se rompe y se arregla?**\n- Tunnel vault ca\u00eddo \u2260 credenciales malas: revisar port-forward del bastion PRIMERO\n- Conflicto de pipelines en un bank: violaci\u00f3n de A1 (un due\u00f1o por bank)",
  "html": "<hr id=\"bkmrk-\">\n<h2 id=\"bkmrk-id%3A-svc-memory-plane\">id: SVC-MEMORY-PLANE\ntitle: Memory Plane \u2014 Hindsight nativo + plugin wetopi-memory\ntype: service\nstatus: active\nowner: wetopi-lead\nsummary: Memoria dual: nativa (personal, sin filtro) + plugin (corporate, con pipeline y ACL).\nlast_verified: 2026-09-06</h2>\n<h1 id=\"bkmrk-svc-memory-plane%3A-me\">SVC-MEMORY-PLANE: Memoria de la flota</h1>\n<p id=\"bkmrk-%C2%BFqu%C3%A9-es%3F-sistema-dua\"><strong>\u00bfQu\u00e9 es?</strong> Sistema dual de memoria (decisi\u00f3n A1 2026-09-06):</p>\n<ul id=\"bkmrk-nativo-%28hindsight-pr\">\n<li><strong>Nativo</strong> (Hindsight provider): bank personal user-u_, retiene TODO sin filtro</li>\n<li><strong>Plugin wetopi-memory</strong>: banks corporate (wetopi-platform/wetopi-business), pipeline\n(se\u00f1al + evidencia + ScopeACL + SecurityPolicy + CorporateRelevance + RetainPolicy)</li>\n</ul>\n<p id=\"bkmrk-%C2%BFqu%C3%A9-hace%3F-retain%2Fre\"><strong>\u00bfQu\u00e9 hace?</strong> Retain/recall/reflect con fact extraction, entity graph, y clasificaci\u00f3n\nSTATE/EVENT/DECISION/PROCEDURE/LEARNING. Principio: <strong>la memoria apunta, no almacena</strong>\n(hecho esencial + timestamp + puntero a URL, nunca el documento).</p>\n<p id=\"bkmrk-%C2%BFc%C3%B3mo-est%C3%A1-desplegad\"><strong>\u00bfC\u00f3mo est\u00e1 desplegado?</strong></p>\n<ul id=\"bkmrk-hindsight-0.9.2-%28ns-\">\n<li>Hindsight 0.9.2 (ns wetopi-memory): hindsight-api:8888 + control-plane:3000 + pgvector</li>\n<li>Edge: memory.wetopi.net con allowlist .87/.89</li>\n<li>Plugin: hermes-runtime/plugins/wetopi-memory en ai-hub (V0.1-V0.3, 197 tests)</li>\n</ul>\n<p id=\"bkmrk-%C2%BFc%C3%B3mo-se-opera%3F\"><strong>\u00bfC\u00f3mo se opera?</strong></p>\n<ul id=\"bkmrk-adr-de-memoria%3A-ai-h\">\n<li>ADR de memoria: ai-hub/docs/wetopi-memory/adr-memory-intent.md</li>\n<li>Reglas Carlos (2026-09-06): hip\u00f3tesis NO verificadas no se memorializan; el diagn\u00f3stico\nretratado corrige la memoria derivada; capture broadly, store compactly</li>\n</ul>\n<p id=\"bkmrk-%C2%BFc%C3%B3mo-se-rompe-y-se-\"><strong>\u00bfC\u00f3mo se rompe y se arregla?</strong></p>\n<ul id=\"bkmrk-tunnel-vault-ca%C3%ADdo-%E2%89%A0\">\n<li>Tunnel vault ca\u00eddo \u2260 credenciales malas: revisar port-forward del bastion PRIMERO</li>\n<li>Conflicto de pipelines en un bank: violaci\u00f3n de A1 (un due\u00f1o por bank)</li>\n</ul>\n"
 },
 {
  "id": 78,
  "title": "REC-20260906 \u2014 go-lives del d\u00eda",
  "slug": "rec-20260906-go-lives-del-dia",
  "book_id": 6,
  "chapter_id": 23,
  "description": null,
  "created_at": "2026-09-06T21:46:38.000000Z",
  "updated_at": "2026-09-06T21:46:38.000000Z",
  "markdown": "---\nid: REC-20260906-KDOCPANE\ntitle: Registro operativo 2026-09-06 \u2014 Knowledge Plane + BookStack + docs runtime\ntype: record\nstatus: active\nowner: wetopi-lead\nsummary: Registro del d\u00eda: go-live BookStack, flip docs runtime, Knowledge Plane desplegado.\nlast_verified: 2026-09-06\n---\n\n# REC-20260906: Operaciones del d\u00eda\n\n## BookStack go-live (book.wetopi.net)\n- Deploy GitOps completo: linuxserver/bookstack v26.05.4 + MariaDB 11.8 (ceph-rbd 10Gi)\n- **Incidente Cilium**: netpol con `ipBlock 0.0.0.0/0` bloqueaba SYN de pods remotos al\n  namespace nuevo (drop silencioso pre-veth, invisible al monitor). Fix: SIN netpols\n  (patr\u00f3n vault/litellm: edge allowlist + ClusterIP + app auth). Commit 8a0c796.\n- **Login**: imagen linuxserver NO hace bootstrap por ADMIN_* env \u2192 crear default y luego\n  `php artisan bookstack:create-admin --initial` (user 1 = operator / carlos@wetopi.net)\n- Login E2E: POST /login CSRF \u2192 302 \u2192 GET con cookie \u2192 200\n\n## docs.wetopi.net runtime flip (G1+G2)\n- Canal imagen \u2192 canal runtime: commit a main = publicado <10s (E2E verificado 80137f4)\n- Incidente: 404 fail-closed post-flip por ExtensionRef docs-basic-auth retirado (ADR-0014)\n  \u2192 fix en Git (PR platform-docs-runtime#2), selfHeal revirti\u00f3 el patch live (Ley 1)\n- Secret: --from-literal NO consume /dev/stdin (usar --from-file)\n- Webhook GitHub 675408228; tests negativos 403 + positivo HMAC 202+rebuild\n- G3 pendiente: retirar deployment legacy tras 7 d\u00edas estabilidad (~13-09)\n\n## Knowledge Plane (OBJ-KDOCPANE-001)\n- Ficha OBJ en PR docs#37; ADR-0010..0013 rescatados + id-registry (AC7/AC8)\n- Skill wetopi-docops replicada a los 3 agentes (sha256 MATCH) + PR ai-hub#37 (AC4)\n- BookStack footprint 00-90 con Start Here + contrato Workbench (book 90)\n- API tokens por agente en BBDD + Infisical services/bookstack/agents/\n\n## Lecciones del d\u00eda\n1. **Cilium + netpol ipBlock 0/0 en ns nuevo** = drop silencioso \u2192 patr\u00f3n sin netpols\n2. **linuxserver images** no implementan bootstrap por env ADMIN_*\n3. **--from-literal no consume stdin** en kubectl create secret\n4. **ExtensionRef unresolvable = fail-closed** (404/500, sin se\u00f1al en Argo)\n5. **El fix SIEMPRE via Git** (selfHeal deshace patches live) \u2014 Ley 1 re-verificada\n6. **Propuesta \u2260 decisi\u00f3n**: etiquetar cada bloque ([DECIDIDO]/[PROPUESTA]) antes de memorializar",
  "html": "<hr id=\"bkmrk-\">\n<h2 id=\"bkmrk-id%3A-rec-20260906-kdo\">id: REC-20260906-KDOCPANE\ntitle: Registro operativo 2026-09-06 \u2014 Knowledge Plane + BookStack + docs runtime\ntype: record\nstatus: active\nowner: wetopi-lead\nsummary: Registro del d\u00eda: go-live BookStack, flip docs runtime, Knowledge Plane desplegado.\nlast_verified: 2026-09-06</h2>\n<h1 id=\"bkmrk-rec-20260906%3A-operac\">REC-20260906: Operaciones del d\u00eda</h1>\n<h2 id=\"bkmrk-bookstack-go-live-%28b\">BookStack go-live (book.wetopi.net)</h2>\n<ul id=\"bkmrk-deploy-gitops-comple\">\n<li>Deploy GitOps completo: linuxserver/bookstack v26.05.4 + MariaDB 11.8 (ceph-rbd 10Gi)</li>\n<li><strong>Incidente Cilium</strong>: netpol con <code>ipBlock 0.0.0.0/0</code> bloqueaba SYN de pods remotos al\nnamespace nuevo (drop silencioso pre-veth, invisible al monitor). Fix: SIN netpols\n(patr\u00f3n vault/litellm: edge allowlist + ClusterIP + app auth). Commit 8a0c796.</li>\n<li><strong>Login</strong>: imagen linuxserver NO hace bootstrap por ADMIN_* env \u2192 crear default y luego\n<code>php artisan bookstack:create-admin --initial</code> (user 1 = operator / carlos@wetopi.net)</li>\n<li>Login E2E: POST /login CSRF \u2192 302 \u2192 GET con cookie \u2192 200</li>\n</ul>\n<h2 id=\"bkmrk-docs.wetopi.net-runt\">docs.wetopi.net runtime flip (G1+G2)</h2>\n<ul id=\"bkmrk-canal-imagen-%E2%86%92-canal\">\n<li>Canal imagen \u2192 canal runtime: commit a main = publicado &lt;10s (E2E verificado 80137f4)</li>\n<li>Incidente: 404 fail-closed post-flip por ExtensionRef docs-basic-auth retirado (ADR-0014)\n\u2192 fix en Git (PR platform-docs-runtime#2), selfHeal revirti\u00f3 el patch live (Ley 1)</li>\n<li>Secret: --from-literal NO consume /dev/stdin (usar --from-file)</li>\n<li>Webhook GitHub 675408228; tests negativos 403 + positivo HMAC 202+rebuild</li>\n<li>G3 pendiente: retirar deployment legacy tras 7 d\u00edas estabilidad (~13-09)</li>\n</ul>\n<h2 id=\"bkmrk-knowledge-plane-%28obj\">Knowledge Plane (OBJ-KDOCPANE-001)</h2>\n<ul id=\"bkmrk-ficha-obj-en-pr-docs\">\n<li>Ficha OBJ en PR docs#37; ADR-0010..0013 rescatados + id-registry (AC7/AC8)</li>\n<li>Skill wetopi-docops replicada a los 3 agentes (sha256 MATCH) + PR ai-hub#37 (AC4)</li>\n<li>BookStack footprint 00-90 con Start Here + contrato Workbench (book 90)</li>\n<li>API tokens por agente en BBDD + Infisical services/bookstack/agents/</li>\n</ul>\n<h2 id=\"bkmrk-lecciones-del-d%C3%ADa\">Lecciones del d\u00eda</h2>\n<ol id=\"bkmrk-cilium-%2B-netpol-ipbl\">\n<li><strong>Cilium + netpol ipBlock 0/0 en ns nuevo</strong> = drop silencioso \u2192 patr\u00f3n sin netpols</li>\n<li><strong>linuxserver images</strong> no implementan bootstrap por env ADMIN_*</li>\n<li><strong>--from-literal no consume stdin</strong> en kubectl create secret</li>\n<li><strong>ExtensionRef unresolvable = fail-closed</strong> (404/500, sin se\u00f1al en Argo)</li>\n<li><strong>El fix SIEMPRE via Git</strong> (selfHeal deshace patches live) \u2014 Ley 1 re-verificada</li>\n<li><strong>Propuesta \u2260 decisi\u00f3n</strong>: etiquetar cada bloque ([DECIDIDO]/[PROPUESTA]) antes de memorializar</li>\n</ol>\n"
 },
 {
  "id": 79,
  "title": "EVIDENCE-cluster-live-20260906",
  "slug": "evidence-cluster-live-20260906",
  "book_id": 10,
  "chapter_id": 30,
  "description": null,
  "created_at": "2026-09-06T21:46:39.000000Z",
  "updated_at": "2026-09-06T21:46:39.000000Z",
  "markdown": "---\nid: EVIDENCE-cluster-live-20260906\ntitle: Evidencia \u2014 inventario live del cluster (2026-09-06 17:2x UTC)\ntype: evidence\nstatus: active\nowner: hermes-carlos\nsummary: Snapshot read-only de workloads, routes, certs, Argo apps \u2014 fuente de las fichas SVC.\nlast_verified: 2026-09-06\n---\n\n# Evidencia: inventario live del cluster (2026-09-06)\n\n## Workloads (deploy/sts ready=1)\n- argocd: 6 pods v3.4.8 \u00b7 bookstack: bookstack + mariadb \u00b7 cert-manager: 3 pods v1.21.1\n- docs-site: 2 nginx (digest 5c67cab5 \u2014 LEGACY, retirada pendiente G3)\n- docs-runtime: 1 pod (GHCR b00efab8 \u2014 ACTIVO)\n- litellm: litellm v1.99.0 + postgres:16\n- observability: alertmanager, data-prepper, grafana 13.0.8, kube-state-metrics, opensearch 3.8.0 (+dashboards), prometheus v3.9.1, tempo 3.0.3\n- vault: infisical v0.164.1 + postgres:17.6 + redis:7.4\n- wetopi-memory: hindsight-api + control-plane + postgresql (pgvector)\n\n## HTTPRoutes (host \u2192 middleware)\nbook/bookstack \u2192 allowlist-bookstack \u00b7 docs \u2192 edge-ip-allowlist (legacy) \u00b7 argo \u2192 edge-ip-allowlist\ninfer \u2192 allowlist-litellm \u00b7 alertmanager/cerebro/grafana/otlp/prometheus \u2192 allowlist-observability\nvault \u2192 edge-ip-allowlist \u00b7 memory \u2192 allowlist-memory (2 rules: /health+/v1/ API, resto CP)\n\n## Certs LE (kube-system, todos Ready=True)\nargo, book, cerebro, docs, edge-test, grafana, infer, memory, otlp, prometheus, vault, alertmanager\n\n## Argo apps (10, todas Synced+Healthy al cierre)\nargocd, bookstack, cert-manager, docs-site, docs-runtime, gitops, hindsight, litellm, litellm-postgres, observability, vault\n\n## Zot\nns platform EXISTE pero VAC\u00cdO (sin deploy) \u2014 platform-registry en feat/zot-initial sin anclar",
  "html": "<hr id=\"bkmrk-\">\n<h2 id=\"bkmrk-id%3A-evidence-cluster\">id: EVIDENCE-cluster-live-20260906\ntitle: Evidencia \u2014 inventario live del cluster (2026-09-06 17:2x UTC)\ntype: evidence\nstatus: active\nowner: hermes-carlos\nsummary: Snapshot read-only de workloads, routes, certs, Argo apps \u2014 fuente de las fichas SVC.\nlast_verified: 2026-09-06</h2>\n<h1 id=\"bkmrk-evidencia%3A-inventari\">Evidencia: inventario live del cluster (2026-09-06)</h1>\n<h2 id=\"bkmrk-workloads-%28deploy%2Fst\">Workloads (deploy/sts ready=1)</h2>\n<ul id=\"bkmrk-argocd%3A-6-pods-v3.4.\">\n<li>argocd: 6 pods v3.4.8 \u00b7 bookstack: bookstack + mariadb \u00b7 cert-manager: 3 pods v1.21.1</li>\n<li>docs-site: 2 nginx (digest 5c67cab5 \u2014 LEGACY, retirada pendiente G3)</li>\n<li>docs-runtime: 1 pod (GHCR b00efab8 \u2014 ACTIVO)</li>\n<li>litellm: litellm v1.99.0 + postgres:16</li>\n<li>observability: alertmanager, data-prepper, grafana 13.0.8, kube-state-metrics, opensearch 3.8.0 (+dashboards), prometheus v3.9.1, tempo 3.0.3</li>\n<li>vault: infisical v0.164.1 + postgres:17.6 + redis:7.4</li>\n<li>wetopi-memory: hindsight-api + control-plane + postgresql (pgvector)</li>\n</ul>\n<h2 id=\"bkmrk-httproutes-%28host-%E2%86%92-m\">HTTPRoutes (host \u2192 middleware)</h2>\n<p id=\"bkmrk-book%2Fbookstack-%E2%86%92-all\">book/bookstack \u2192 allowlist-bookstack \u00b7 docs \u2192 edge-ip-allowlist (legacy) \u00b7 argo \u2192 edge-ip-allowlist\ninfer \u2192 allowlist-litellm \u00b7 alertmanager/cerebro/grafana/otlp/prometheus \u2192 allowlist-observability\nvault \u2192 edge-ip-allowlist \u00b7 memory \u2192 allowlist-memory (2 rules: /health+/v1/ API, resto CP)</p>\n<h2 id=\"bkmrk-certs-le-%28kube-syste\">Certs LE (kube-system, todos Ready=True)</h2>\n<p id=\"bkmrk-argo%2C-book%2C-cerebro%2C\">argo, book, cerebro, docs, edge-test, grafana, infer, memory, otlp, prometheus, vault, alertmanager</p>\n<h2 id=\"bkmrk-argo-apps-%2810%2C-todas\">Argo apps (10, todas Synced+Healthy al cierre)</h2>\n<p id=\"bkmrk-argocd%2C-bookstack%2C-c\">argocd, bookstack, cert-manager, docs-site, docs-runtime, gitops, hindsight, litellm, litellm-postgres, observability, vault</p>\n<h2 id=\"bkmrk-zot\">Zot</h2>\n<p id=\"bkmrk-ns-platform-existe-p\">ns platform EXISTE pero VAC\u00cdO (sin deploy) \u2014 platform-registry en feat/zot-initial sin anclar</p>\n"
 },
 {
  "id": 80,
  "title": "ARCH-PROXMOX \u2014 cluster wetopi7",
  "slug": "arch-proxmox-cluster-wetopi7",
  "book_id": 2,
  "chapter_id": 12,
  "description": null,
  "created_at": "2026-09-06T21:47:30.000000Z",
  "updated_at": "2026-09-06T21:47:30.000000Z",
  "markdown": "---\nid: ARCH-PROXMOX\ntitle: Proxmox cluster wetopi7\ntype: architecture\nstatus: active\nowner: wetopi-lead\nsummary: Cluster Proxmox de 8 nodos + Ceph sobre OVH vRack, VLAN 4 = node LAN.\nlast_verified: 2026-09-06\n---\n\n# Proxmox cluster wetopi7\n\n**\u00bfQu\u00e9 es?** Cluster Proxmox (8 nodos) + Ceph sobre OVH vRack. VLAN 4 / vmbr4 = node LAN 10.72.0.0/23.\n\n**Nodos:** wetopi-ops01 (bastion 92.222.245.91 WAN / 10.72.0.10), rke-cp01/02/03 (.21/.22/.23),\nrke-platform01/02/03 (.41/.42/.43), rke-wan01/02 (.51/.52 \u2014 hostNetwork edge), wetopi-natgw01 (.2, WAN .83).\n\nFuente can\u00f3nica: `docs/architecture/infrastructure/proxmox-cluster.md` (Git).",
  "html": "<hr id=\"bkmrk-\">\n<h2 id=\"bkmrk-id%3A-arch-proxmox-tit\">id: ARCH-PROXMOX\ntitle: Proxmox cluster wetopi7\ntype: architecture\nstatus: active\nowner: wetopi-lead\nsummary: Cluster Proxmox de 8 nodos + Ceph sobre OVH vRack, VLAN 4 = node LAN.\nlast_verified: 2026-09-06</h2>\n<h1 id=\"bkmrk-proxmox-cluster-weto\">Proxmox cluster wetopi7</h1>\n<p id=\"bkmrk-%C2%BFqu%C3%A9-es%3F-cluster-pro\"><strong>\u00bfQu\u00e9 es?</strong> Cluster Proxmox (8 nodos) + Ceph sobre OVH vRack. VLAN 4 / vmbr4 = node LAN 10.72.0.0/23.</p>\n<p id=\"bkmrk-nodos%3A-wetopi-ops01-\"><strong>Nodos:</strong> wetopi-ops01 (bastion 92.222.245.91 WAN / 10.72.0.10), rke-cp01/02/03 (.21/.22/.23),\nrke-platform01/02/03 (.41/.42/.43), rke-wan01/02 (.51/.52 \u2014 hostNetwork edge), wetopi-natgw01 (.2, WAN .83).</p>\n<p id=\"bkmrk-fuente-can%C3%B3nica%3A-doc\">Fuente can\u00f3nica: <code>docs/architecture/infrastructure/proxmox-cluster.md</code> (Git).</p>\n"
 },
 {
  "id": 81,
  "title": "ARCH-CEPH \u2014 storage",
  "slug": "arch-ceph-storage",
  "book_id": 2,
  "chapter_id": 13,
  "description": null,
  "created_at": "2026-09-06T21:47:31.000000Z",
  "updated_at": "2026-09-06T21:47:31.000000Z",
  "markdown": "---\nid: ARCH-CEPH\ntitle: Ceph storage\ntype: architecture\nstatus: active\nowner: wetopi-lead\nsummary: Ceph LAN 192.168.96.0/20 sin tocar; ruta por natgw v\u00eda tercera NIC (VLAN 2, route-not-NAT).\nlast_verified: 2026-09-06\n---\n\n# Ceph storage\n\n- LAN 192.168.96.0/20 (intocada, greenfield parallel)\n- storageclass del cluster: **ceph-rbd** (\u00fanica \u2014 local-path NO existe, lecci\u00f3n R2)\n- PVCs: ceph-rbd RWO (bookstack 10Gi, hindsight-pgvector 20Gi, mariadb 10Gi...)\n\nFuente can\u00f3nica: `docs/architecture/infrastructure/ceph.md` + ADR-0007 (Git).",
  "html": "<hr id=\"bkmrk-\">\n<h2 id=\"bkmrk-id%3A-arch-ceph-title%3A\">id: ARCH-CEPH\ntitle: Ceph storage\ntype: architecture\nstatus: active\nowner: wetopi-lead\nsummary: Ceph LAN 192.168.96.0/20 sin tocar; ruta por natgw v\u00eda tercera NIC (VLAN 2, route-not-NAT).\nlast_verified: 2026-09-06</h2>\n<h1 id=\"bkmrk-ceph-storage\">Ceph storage</h1>\n<ul id=\"bkmrk-lan-192.168.96.0%2F20-\">\n<li>LAN 192.168.96.0/20 (intocada, greenfield parallel)</li>\n<li>storageclass del cluster: <strong>ceph-rbd</strong> (\u00fanica \u2014 local-path NO existe, lecci\u00f3n R2)</li>\n<li>PVCs: ceph-rbd RWO (bookstack 10Gi, hindsight-pgvector 20Gi, mariadb 10Gi...)</li>\n</ul>\n<p id=\"bkmrk-fuente-can%C3%B3nica%3A-doc\">Fuente can\u00f3nica: <code>docs/architecture/infrastructure/ceph.md</code> + ADR-0007 (Git).</p>\n"
 },
 {
  "id": 82,
  "title": "ARCH-OVH-NET \u2014 red WAN",
  "slug": "arch-ovh-net-red-wan",
  "book_id": 2,
  "chapter_id": 14,
  "description": null,
  "created_at": "2026-09-06T21:47:31.000000Z",
  "updated_at": "2026-09-06T21:47:31.000000Z",
  "markdown": "---\nid: ARCH-OVH-NET\ntitle: OVH y red WAN\ntype: architecture\nstatus: active\nowner: wetopi-lead\nsummary: Edge WAN .193/.194 (nftables 80\u21928000, 443\u21928443), allowlist por hostname, NATGW egress.\nlast_verified: 2026-09-06\n---\n\n# OVH y red WAN\n\n- Edge WAN: 79.137.114.193/.194 (rke-wan01/02, hostNetwork, Traefik 8000/8443)\n- Edge LB nftables: redirect 80\u21928000 y 443\u21928443\n- Allowlists por hostname: .87 (operador VPN) + .89 (agents01/harness) \u2014 twins por ns\n- NATGW (wetopi-natgw01): pools egress gw100/101/102, crawlers nunca comparten IP con clientes\n- Wildcard *.wetopi.net \u2192 92.222.245.76 (bastion, NO el edge): hosts nuevos SIEMPRE A records expl\u00edcitos\n\nFuente: `docs/architecture/infrastructure/ovh-and-network.md` + ADR-0014 (Git).",
  "html": "<hr id=\"bkmrk-\">\n<h2 id=\"bkmrk-id%3A-arch-ovh-net-tit\">id: ARCH-OVH-NET\ntitle: OVH y red WAN\ntype: architecture\nstatus: active\nowner: wetopi-lead\nsummary: Edge WAN .193/.194 (nftables 80\u21928000, 443\u21928443), allowlist por hostname, NATGW egress.\nlast_verified: 2026-09-06</h2>\n<h1 id=\"bkmrk-ovh-y-red-wan\">OVH y red WAN</h1>\n<ul id=\"bkmrk-edge-wan%3A-79.137.114\">\n<li>Edge WAN: 79.137.114.193/.194 (rke-wan01/02, hostNetwork, Traefik 8000/8443)</li>\n<li>Edge LB nftables: redirect 80\u21928000 y 443\u21928443</li>\n<li>Allowlists por hostname: .87 (operador VPN) + .89 (agents01/harness) \u2014 twins por ns</li>\n<li>NATGW (wetopi-natgw01): pools egress gw100/101/102, crawlers nunca comparten IP con clientes</li>\n<li>Wildcard *.wetopi.net \u2192 92.222.245.76 (bastion, NO el edge): hosts nuevos SIEMPRE A records expl\u00edcitos</li>\n</ul>\n<p id=\"bkmrk-fuente%3A-docs%2Farchite\">Fuente: <code>docs/architecture/infrastructure/ovh-and-network.md</code> + ADR-0014 (Git).</p>\n"
 },
 {
  "id": 83,
  "title": "ARCH-RKE2 \u2014 cluster kubernetes",
  "slug": "arch-rke2-cluster-kubernetes",
  "book_id": 3,
  "chapter_id": 16,
  "description": null,
  "created_at": "2026-09-06T21:47:31.000000Z",
  "updated_at": "2026-09-06T21:47:31.000000Z",
  "markdown": "---\nid: ARCH-RKE2\ntitle: RKE2 cluster\ntype: architecture\nstatus: active\nowner: wetopi-lead\nsummary: RKE2 3 CP + 3 platform workers + 2 WAN; etcd embebido; API VIP 10.72.0.20.\nlast_verified: 2026-09-06\n---\n\n# RKE2 cluster\n\n- 3 CP (rke-cp01/02/03) + 3 platform workers + 2 WAN edge; etcd embebido\n- API VIP 10.72.0.20 (keepalived), server URL :9345, API :6443; tls-san = VIP + 3 CPs\n- Pod CIDR 10.64.0.0/14, Service CIDR 10.68.0.0/16; Cilium VXLAN desde d\u00eda 1 (ADR-0005)\n- kube-proxyReplacement: false; IPVS + VRRP + DSR en el edge\n\nFuente: `docs/architecture/platform/rke2-cluster.md` + ADR-0001/0004 (Git).",
  "html": "<hr id=\"bkmrk-\">\n<h2 id=\"bkmrk-id%3A-arch-rke2-title%3A\">id: ARCH-RKE2\ntitle: RKE2 cluster\ntype: architecture\nstatus: active\nowner: wetopi-lead\nsummary: RKE2 3 CP + 3 platform workers + 2 WAN; etcd embebido; API VIP 10.72.0.20.\nlast_verified: 2026-09-06</h2>\n<h1 id=\"bkmrk-rke2-cluster\">RKE2 cluster</h1>\n<ul id=\"bkmrk-3-cp-%28rke-cp01%2F02%2F03\">\n<li>3 CP (rke-cp01/02/03) + 3 platform workers + 2 WAN edge; etcd embebido</li>\n<li>API VIP 10.72.0.20 (keepalived), server URL :9345, API :6443; tls-san = VIP + 3 CPs</li>\n<li>Pod CIDR 10.64.0.0/14, Service CIDR 10.68.0.0/16; Cilium VXLAN desde d\u00eda 1 (ADR-0005)</li>\n<li>kube-proxyReplacement: false; IPVS + VRRP + DSR en el edge</li>\n</ul>\n<p id=\"bkmrk-fuente%3A-docs%2Farchite\">Fuente: <code>docs/architecture/platform/rke2-cluster.md</code> + ADR-0001/0004 (Git).</p>\n"
 },
 {
  "id": 84,
  "title": "ARCH-CILIUM \u2014 red CNI",
  "slug": "arch-cilium-red-cni",
  "book_id": 3,
  "chapter_id": 17,
  "description": null,
  "created_at": "2026-09-06T21:47:31.000000Z",
  "updated_at": "2026-09-06T21:47:31.000000Z",
  "markdown": "---\nid: ARCH-CILIUM\ntitle: Cilium network\ntype: architecture\nstatus: active\nowner: wetopi-lead\nsummary: Cilium v1.20.1 VXLAN, kubeProxyReplacement false, Hubble Relay habilitado.\nlast_verified: 2026-09-06\n---\n\n# Cilium network\n\n- v1.20.1 (chart oci://quay.io/cilium/charts), tunnel vxlan, IPAM cluster-pool 10.64.0.0/14/24\n- kubeProxyReplacement: false (kube-proxy convive); Hubble + Relay ON, UI OFF\n- UDP 8472 (VXLAN) + TCP 4244 (Hubble Relay) entre nodos\n- **Gotcha verificado**: netpol con `ipBlock 0.0.0.0/0` en ns nuevo bloquea SYN de pods\n  remotos (drop silencioso pre-veth) \u2014 patr\u00f3n casa: SIN netpols para apps edge\n- Traefik edge corre hostNetwork: su tr\u00e1fico aparece como identity 'host' (no kube-system pod)\n\nFuente: ADR-0005 + `docs/architecture/platform/cilium-network.md` (Git).",
  "html": "<hr id=\"bkmrk-\">\n<h2 id=\"bkmrk-id%3A-arch-cilium-titl\">id: ARCH-CILIUM\ntitle: Cilium network\ntype: architecture\nstatus: active\nowner: wetopi-lead\nsummary: Cilium v1.20.1 VXLAN, kubeProxyReplacement false, Hubble Relay habilitado.\nlast_verified: 2026-09-06</h2>\n<h1 id=\"bkmrk-cilium-network\">Cilium network</h1>\n<ul id=\"bkmrk-v1.20.1-%28chart-oci%3A%2F\">\n<li>v1.20.1 (chart oci://quay.io/cilium/charts), tunnel vxlan, IPAM cluster-pool 10.64.0.0/14/24</li>\n<li>kubeProxyReplacement: false (kube-proxy convive); Hubble + Relay ON, UI OFF</li>\n<li>UDP 8472 (VXLAN) + TCP 4244 (Hubble Relay) entre nodos</li>\n<li><strong>Gotcha verificado</strong>: netpol con <code>ipBlock 0.0.0.0/0</code> en ns nuevo bloquea SYN de pods\nremotos (drop silencioso pre-veth) \u2014 patr\u00f3n casa: SIN netpols para apps edge</li>\n<li>Traefik edge corre hostNetwork: su tr\u00e1fico aparece como identity 'host' (no kube-system pod)</li>\n</ul>\n<p id=\"bkmrk-fuente%3A-adr-0005-%2B-d\">Fuente: ADR-0005 + <code>docs/architecture/platform/cilium-network.md</code> (Git).</p>\n"
 },
 {
  "id": 85,
  "title": "ARCH-EDGE \u2014 Traefik Gateway API",
  "slug": "arch-edge-traefik-gateway-api",
  "book_id": 3,
  "chapter_id": 18,
  "description": null,
  "created_at": "2026-09-06T21:47:32.000000Z",
  "updated_at": "2026-09-06T21:47:32.000000Z",
  "markdown": "---\nid: ARCH-EDGE\ntitle: Edge WAN (Traefik + Gateway API)\ntype: architecture\nstatus: active\nowner: wetopi-lead\nsummary: Gateway wetopi-edge (kube-system) con 12 certRefs LE; listeners web:8000/websecure:8443.\nlast_verified: 2026-09-06\n---\n\n# Edge WAN\n\n- Gateway `wetopi-edge` (kube-system): listener web:8000 HTTP + websecure:8443 HTTPS\n  (nftables edge redirige 80\u21928000, 443\u21928443). allowedRoutes from: All en https\n- TLS Terminate; certRefs a Secrets kube-system (12: default + argo, book, cerebro, docs,\n  grafana, infer, memory, otlp, prometheus, vault, alertmanager... y edge-test self-signed)\n- **Fail-safe order**: Certificate Ready=True ANTES del certRef (un Secret ausente tumba\n  el listener completo para co-tenants)\n- Allowlist pattern: Middleware ipAllowList twin por ns (.87 + .89), ExtensionRef en la route\n- **Ley 4**: middleware twin va en el MISMO PR/directorio que la route que lo referencia\n- **ExtensionRef unresolvable = fail-closed** (404/500 para toda la ruta, Argo verde igual)\n\nFuente: platform-edge repo + ADR-0006/0014 (Git).",
  "html": "<hr id=\"bkmrk-\">\n<h2 id=\"bkmrk-id%3A-arch-edge-title%3A\">id: ARCH-EDGE\ntitle: Edge WAN (Traefik + Gateway API)\ntype: architecture\nstatus: active\nowner: wetopi-lead\nsummary: Gateway wetopi-edge (kube-system) con 12 certRefs LE; listeners web:8000/websecure:8443.\nlast_verified: 2026-09-06</h2>\n<h1 id=\"bkmrk-edge-wan\">Edge WAN</h1>\n<ul id=\"bkmrk-gateway-wetopi-edge-\">\n<li>Gateway <code>wetopi-edge</code> (kube-system): listener web:8000 HTTP + websecure:8443 HTTPS\n(nftables edge redirige 80\u21928000, 443\u21928443). allowedRoutes from: All en https</li>\n<li>TLS Terminate; certRefs a Secrets kube-system (12: default + argo, book, cerebro, docs,\ngrafana, infer, memory, otlp, prometheus, vault, alertmanager... y edge-test self-signed)</li>\n<li><strong>Fail-safe order</strong>: Certificate Ready=True ANTES del certRef (un Secret ausente tumba\nel listener completo para co-tenants)</li>\n<li>Allowlist pattern: Middleware ipAllowList twin por ns (.87 + .89), ExtensionRef en la route</li>\n<li><strong>Ley 4</strong>: middleware twin va en el MISMO PR/directorio que la route que lo referencia</li>\n<li><strong>ExtensionRef unresolvable = fail-closed</strong> (404/500 para toda la ruta, Argo verde igual)</li>\n</ul>\n<p id=\"bkmrk-fuente%3A-platform-edg\">Fuente: platform-edge repo + ADR-0006/0014 (Git).</p>\n"
 },
 {
  "id": 86,
  "title": "ARCH-GITOPS \u2014 app-of-apps Argo CD",
  "slug": "arch-gitops-app-of-apps-argo-cd",
  "book_id": 3,
  "chapter_id": 19,
  "description": null,
  "created_at": "2026-09-06T21:47:32.000000Z",
  "updated_at": "2026-09-06T21:47:32.000000Z",
  "markdown": "---\nid: ARCH-GITOPS\ntitle: GitOps (Argo CD)\ntype: architecture\nstatus: active\nowner: wetopi-lead\nsummary: App-of-apps (index platform-gitops), 11 apps, automated prune+selfHeal, include-glob contracts.\nlast_verified: 2026-09-06\n---\n\n# GitOps (Argo CD)\n\n- v3.4.8 non-HA (sin Postgres \u2014 Redis only); WebUI argo.wetopi.net\n- App-of-apps: app `gitops` \u2192 platform-gitops@main (apps/*.yaml) con recurse:true\n- 11 apps: argocd, bookstack, cert-manager, docs-site, docs-runtime, gitops, hindsight,\n  litellm, litellm-postgres, observability, vault \u2014 todas Synced+Healthy\n- include-glob CONTRACT (Ley 4): a\u00f1adir/quitar ficheros del include EN EL MISMO PR\n- repo Secrets: `repo-<name>-https` (auto-discovery por label, con url/username/password/project)\n- **Chicken-and-egg**: AppProject sourceRepos debe aplicarse LIVE antes del root que lo adopta\n- **Ley de flip**: la route/objeto que muere sale de Git EN el flip, nunca antes (incidente 09-01: 404 6h)\n\nFuente: platform-gitops README (reglas R1-R5) + memo gitops-reorg v3.1 (Git).",
  "html": "<hr id=\"bkmrk-\">\n<h2 id=\"bkmrk-id%3A-arch-gitops-titl\">id: ARCH-GITOPS\ntitle: GitOps (Argo CD)\ntype: architecture\nstatus: active\nowner: wetopi-lead\nsummary: App-of-apps (index platform-gitops), 11 apps, automated prune+selfHeal, include-glob contracts.\nlast_verified: 2026-09-06</h2>\n<h1 id=\"bkmrk-gitops-%28argo-cd%29\">GitOps (Argo CD)</h1>\n<ul id=\"bkmrk-v3.4.8-non-ha-%28sin-p\">\n<li>v3.4.8 non-HA (sin Postgres \u2014 Redis only); WebUI argo.wetopi.net</li>\n<li>App-of-apps: app <code>gitops</code> \u2192 platform-gitops@main (apps/*.yaml) con recurse:true</li>\n<li>11 apps: argocd, bookstack, cert-manager, docs-site, docs-runtime, gitops, hindsight,\nlitellm, litellm-postgres, observability, vault \u2014 todas Synced+Healthy</li>\n<li>include-glob CONTRACT (Ley 4): a\u00f1adir/quitar ficheros del include EN EL MISMO PR</li>\n<li>repo Secrets: <code>repo-&lt;name&gt;-https</code> (auto-discovery por label, con url/username/password/project)</li>\n<li><strong>Chicken-and-egg</strong>: AppProject sourceRepos debe aplicarse LIVE antes del root que lo adopta</li>\n<li><strong>Ley de flip</strong>: la route/objeto que muere sale de Git EN el flip, nunca antes (incidente 09-01: 404 6h)</li>\n</ul>\n<p id=\"bkmrk-fuente%3A-platform-git\">Fuente: platform-gitops README (reglas R1-R5) + memo gitops-reorg v3.1 (Git).</p>\n"
 },
 {
  "id": 87,
  "title": "ARCH-CERTM \u2014 LE DNS-01",
  "slug": "arch-certm-le-dns-01",
  "book_id": 3,
  "chapter_id": 20,
  "description": null,
  "created_at": "2026-09-06T21:47:32.000000Z",
  "updated_at": "2026-09-06T21:47:32.000000Z",
  "markdown": "---\nid: ARCH-CERTM\ntitle: Cert-Manager\ntype: architecture\nstatus: active\nowner: wetopi-lead\nsummary: cert-manager v1.21.1 con ClusterIssuer letsencrypt-prod (DNS-01 Cloudflare).\nlast_verified: 2026-09-06\n---\n\n# Cert-Manager\n\n- v1.21.1 (chart quay.io/jetstack/charts, pinned)\n- ClusterIssuer `letsencrypt-prod`: DNS-01 via Cloudflare (token del vault services/cloudflare)\n- Certs por host en kube-system (same-namespace del Gateway, sin ReferenceGrant)\n- 12 certs activos, todos Ready (verificaci\u00f3n: `kubectl -n kube-system get certificates`)\n- **Regla de flujo**: Certificate Ready ANTES de certRef en el Gateway (fail-safe co-tenant)\n\nFuente: `docs/services/cert-manager.md` (Git).",
  "html": "<hr id=\"bkmrk-\">\n<h2 id=\"bkmrk-id%3A-arch-certm-title\">id: ARCH-CERTM\ntitle: Cert-Manager\ntype: architecture\nstatus: active\nowner: wetopi-lead\nsummary: cert-manager v1.21.1 con ClusterIssuer letsencrypt-prod (DNS-01 Cloudflare).\nlast_verified: 2026-09-06</h2>\n<h1 id=\"bkmrk-cert-manager\">Cert-Manager</h1>\n<ul id=\"bkmrk-v1.21.1-%28chart-quay.\">\n<li>v1.21.1 (chart quay.io/jetstack/charts, pinned)</li>\n<li>ClusterIssuer <code>letsencrypt-prod</code>: DNS-01 via Cloudflare (token del vault services/cloudflare)</li>\n<li>Certs por host en kube-system (same-namespace del Gateway, sin ReferenceGrant)</li>\n<li>12 certs activos, todos Ready (verificaci\u00f3n: <code>kubectl -n kube-system get certificates</code>)</li>\n<li><strong>Regla de flujo</strong>: Certificate Ready ANTES de certRef en el Gateway (fail-safe co-tenant)</li>\n</ul>\n<p id=\"bkmrk-fuente%3A-docs%2Fservice\">Fuente: <code>docs/services/cert-manager.md</code> (Git).</p>\n"
 },
 {
  "id": 88,
  "title": "\u00cdNDICE \u2014 servicios y estado",
  "slug": "indice-servicios-y-estado",
  "book_id": 3,
  "chapter_id": 21,
  "description": null,
  "created_at": "2026-09-06T21:47:32.000000Z",
  "updated_at": "2026-09-06T21:47:32.000000Z",
  "markdown": "---\nid: REF-SERVICES-INDEX\ntitle: \u00cdndice de fichas SVC (book 20)\ntype: reference\nstatus: active\nowner: wetopi-lead\nsummary: Mapa de las fichas de servicio con su estado real (live vs planned vs legacy).\nlast_verified: 2026-09-06\n---\n\n# \u00cdndice de servicios (estado live 2026-09-06)\n\n| Ficha | Host | Estado |\n|---|---|---|\n| SVC-DOCS-RUNTIME | docs.wetopi.net | **ACTIVO** (flip G1+G2 hoy) |\n| SVC-BOOKSTACK | book.wetopi.net | ACTIVO |\n| SVC-HINDSIGHT | memory.wetopi.net | ACTIVO |\n| SVC-LITELLM | infer.wetopi.net | ACTIVO |\n| SVC-VAULT | vault.wetopi.net | ACTIVO |\n| SVC-ARGOCD | argo.wetopi.net | ACTIVO |\n| SVC-DOCS-SITE | docs.wetopi.net | LEGACY (retirada G3 ~13-09) |\n| SVC-REGISTRY | zot.platform.svc | PLANNED (ns vac\u00edo \u2014 sin desplegar) |\n| SVC-CEPH-CSI | storage driver | ACTIVO (infra) |\n| SVC-CERT-MANAGER | cert-manager ns | ACTIVO (infra) |\n| SVC-TRAEFIK-EDGE | edge WAN | ACTIVO (infra) |\n| SVC-OBSERVABILITY | grafana/prometheus/osd/alertmanager/cerebro | ACTIVO (OTel plano inerte, reactivable) |\n| SVC-HERMES-FLEET | agents01 (docker) | ACTIVO (book 60) |\n\nTodos los hosts: allowlist .87/.89 (excepto /hooks/rebuild \u2014 HMAC).",
  "html": "<hr id=\"bkmrk-\">\n<h2 id=\"bkmrk-id%3A-ref-services-ind\">id: REF-SERVICES-INDEX\ntitle: \u00cdndice de fichas SVC (book 20)\ntype: reference\nstatus: active\nowner: wetopi-lead\nsummary: Mapa de las fichas de servicio con su estado real (live vs planned vs legacy).\nlast_verified: 2026-09-06</h2>\n<h1 id=\"bkmrk-%C3%8Dndice-de-servicios-\">\u00cdndice de servicios (estado live 2026-09-06)</h1>\n<table id=\"bkmrk-ficha-host-estado-sv\">\n<thead>\n<tr>\n<th>Ficha</th>\n<th>Host</th>\n<th>Estado</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>SVC-DOCS-RUNTIME</td>\n<td>docs.wetopi.net</td>\n<td><strong>ACTIVO</strong> (flip G1+G2 hoy)</td>\n</tr>\n<tr>\n<td>SVC-BOOKSTACK</td>\n<td>book.wetopi.net</td>\n<td>ACTIVO</td>\n</tr>\n<tr>\n<td>SVC-HINDSIGHT</td>\n<td>memory.wetopi.net</td>\n<td>ACTIVO</td>\n</tr>\n<tr>\n<td>SVC-LITELLM</td>\n<td>infer.wetopi.net</td>\n<td>ACTIVO</td>\n</tr>\n<tr>\n<td>SVC-VAULT</td>\n<td>vault.wetopi.net</td>\n<td>ACTIVO</td>\n</tr>\n<tr>\n<td>SVC-ARGOCD</td>\n<td>argo.wetopi.net</td>\n<td>ACTIVO</td>\n</tr>\n<tr>\n<td>SVC-DOCS-SITE</td>\n<td>docs.wetopi.net</td>\n<td>LEGACY (retirada G3 ~13-09)</td>\n</tr>\n<tr>\n<td>SVC-REGISTRY</td>\n<td>zot.platform.svc</td>\n<td>PLANNED (ns vac\u00edo \u2014 sin desplegar)</td>\n</tr>\n<tr>\n<td>SVC-CEPH-CSI</td>\n<td>storage driver</td>\n<td>ACTIVO (infra)</td>\n</tr>\n<tr>\n<td>SVC-CERT-MANAGER</td>\n<td>cert-manager ns</td>\n<td>ACTIVO (infra)</td>\n</tr>\n<tr>\n<td>SVC-TRAEFIK-EDGE</td>\n<td>edge WAN</td>\n<td>ACTIVO (infra)</td>\n</tr>\n<tr>\n<td>SVC-OBSERVABILITY</td>\n<td>grafana/prometheus/osd/alertmanager/cerebro</td>\n<td>ACTIVO (OTel plano inerte, reactivable)</td>\n</tr>\n<tr>\n<td>SVC-HERMES-FLEET</td>\n<td>agents01 (docker)</td>\n<td>ACTIVO (book 60)</td>\n</tr>\n</tbody>\n</table>\n<p id=\"bkmrk-todos-los-hosts%3A-all\">Todos los hosts: allowlist .87/.89 (excepto /hooks/rebuild \u2014 HMAC).</p>\n"
 },
 {
  "id": 89,
  "title": "REF-SKILLS-FLOTA \u2014 skills y distribuci\u00f3n",
  "slug": "ref-skills-flota-skills-y-distribucion",
  "book_id": 7,
  "chapter_id": 25,
  "description": null,
  "created_at": "2026-09-06T21:47:43.000000Z",
  "updated_at": "2026-09-06T21:47:43.000000Z",
  "markdown": "---\nid: REF-SKILLS-FLOTA\ntitle: Skills de la flota (estado y distribuci\u00f3n)\ntype: reference\nstatus: active\nowner: wetopi-lead\nsummary: Skills activas por agente y mecanismo de replicaci\u00f3n con sha256.\nlast_verified: 2026-09-06\n---\n\n# Skills de la flota\n\n**Activas por decisi\u00f3n de Carlos (09-02):** wetopi-* (4), hermes-agent, github-* (5),\nserving-llms-vllm, huggingface-hub, evaluating-llms-harness, dev/debug (7), agentes de\ndelegaci\u00f3n (3), llm-wiki, design-md, humanizer, architecture-diagram, pretext, obsidian.\n\n**DocOps (nuevo 09-06):** `wetopi-docops` replicada a los 3 agentes (sha256 MATCH verificado).\n\u26a0 PENDIENTE CONSOLIDACI\u00d3N: la skill can\u00f3nica `docops` v1.1.0 (con plantillas) solapa con\n`wetopi-docops` v0.1.0 \u2014 plan: extender docops a v1.2.0 con el Knowledge Plane y retirar\nwetopi-docops (un solo contrato).\n\n**Replicaci\u00f3n:** local `/opt/data/skills/...` \u2192 `ssh -i /opt/data/.ssh/bigardo root@92.222.245.89`\n\u2192 `/opt/hermes-<n>/data/skills/...` \u2192 verificar sha256 en ambos.\n\n**Nota:** el skill loader es per-sesi\u00f3n \u2014 skills nuevas visibles al reiniciar la sesi\u00f3n.",
  "html": "<hr id=\"bkmrk-\">\n<h2 id=\"bkmrk-id%3A-ref-skills-flota\">id: REF-SKILLS-FLOTA\ntitle: Skills de la flota (estado y distribuci\u00f3n)\ntype: reference\nstatus: active\nowner: wetopi-lead\nsummary: Skills activas por agente y mecanismo de replicaci\u00f3n con sha256.\nlast_verified: 2026-09-06</h2>\n<h1 id=\"bkmrk-skills-de-la-flota\">Skills de la flota</h1>\n<p id=\"bkmrk-activas-por-decisi%C3%B3n\"><strong>Activas por decisi\u00f3n de Carlos (09-02):</strong> wetopi-* (4), hermes-agent, github-* (5),\nserving-llms-vllm, huggingface-hub, evaluating-llms-harness, dev/debug (7), agentes de\ndelegaci\u00f3n (3), llm-wiki, design-md, humanizer, architecture-diagram, pretext, obsidian.</p>\n<p id=\"bkmrk-docops-%28nuevo-09-06%29\"><strong>DocOps (nuevo 09-06):</strong> <code>wetopi-docops</code> replicada a los 3 agentes (sha256 MATCH verificado).\n\u26a0 PENDIENTE CONSOLIDACI\u00d3N: la skill can\u00f3nica <code>docops</code> v1.1.0 (con plantillas) solapa con\n<code>wetopi-docops</code> v0.1.0 \u2014 plan: extender docops a v1.2.0 con el Knowledge Plane y retirar\nwetopi-docops (un solo contrato).</p>\n<p id=\"bkmrk-replicaci%C3%B3n%3A-local-%2F\"><strong>Replicaci\u00f3n:</strong> local <code>/opt/data/skills/...</code> \u2192 <code>ssh -i /opt/data/.ssh/bigardo root@92.222.245.89</code>\n\u2192 <code>/opt/hermes-&lt;n&gt;/data/skills/...</code> \u2192 verificar sha256 en ambos.</p>\n<p id=\"bkmrk-nota%3A-el-skill-loade\"><strong>Nota:</strong> el skill loader es per-sesi\u00f3n \u2014 skills nuevas visibles al reiniciar la sesi\u00f3n.</p>\n"
 },
 {
  "id": 90,
  "title": "\u00cdNDICE del Workbench",
  "slug": "indice-del-workbench",
  "book_id": 10,
  "chapter_id": 30,
  "description": null,
  "created_at": "2026-09-06T21:47:43.000000Z",
  "updated_at": "2026-09-06T21:47:43.000000Z",
  "markdown": "---\nid: REF-WORKBENCH-INDEX\ntitle: \u00cdndice del Workbench (book 90)\ntype: reference\nstatus: active\nowner: wetopi-lead\nsummary: Qu\u00e9 es el Workbench, estructura por agente, naming y ciclo #draft/#ready/#superseded.\nlast_verified: 2026-09-06\n---\n\n# Agents Workbench \u2014 \u00edndice\n\nEl book 90 es el **L1 del circuito** (borradores y evidencia). NO es el SSOT de nada:\nlo que se consolida se promociona a Git (PR) y la copia aqu\u00ed muere (`PROMOVED \u2192 <link>`).\n\n- `carlos/` \u2014 EVIDENCE-cluster-live-20260906 (inventario live del cluster)\n- `joan/` \u2014 (vac\u00edo, escribir aqu\u00ed)\n- `josep/` \u2014 (vac\u00edo, escribir aqu\u00ed)\n\nNaming: `WIP-<fecha>-<tema>`, `DRAFT-ADR-<nnnn>-<slug>`, `EVIDENCE-<slug>` + fecha.\nTags: `#draft` \u2192 `#ready` \u2192 `#superseded`.\n\n**No escribir en el chapter de otro agente.** Tokens: Infisical services/bookstack/agents/.",
  "html": "<hr id=\"bkmrk-\">\n<h2 id=\"bkmrk-id%3A-ref-workbench-in\">id: REF-WORKBENCH-INDEX\ntitle: \u00cdndice del Workbench (book 90)\ntype: reference\nstatus: active\nowner: wetopi-lead\nsummary: Qu\u00e9 es el Workbench, estructura por agente, naming y ciclo #draft/#ready/#superseded.\nlast_verified: 2026-09-06</h2>\n<h1 id=\"bkmrk-agents-workbench-%E2%80%94-%C3%AD\">Agents Workbench \u2014 \u00edndice</h1>\n<p id=\"bkmrk-el-book-90-es-el-l1-\">El book 90 es el <strong>L1 del circuito</strong> (borradores y evidencia). NO es el SSOT de nada:\nlo que se consolida se promociona a Git (PR) y la copia aqu\u00ed muere (<code>PROMOVED \u2192 &lt;link&gt;</code>).</p>\n<ul id=\"bkmrk-carlos%2F-%E2%80%94-evidence-c\">\n<li><code>carlos/</code> \u2014 EVIDENCE-cluster-live-20260906 (inventario live del cluster)</li>\n<li><code>joan/</code> \u2014 (vac\u00edo, escribir aqu\u00ed)</li>\n<li><code>josep/</code> \u2014 (vac\u00edo, escribir aqu\u00ed)</li>\n</ul>\n<p id=\"bkmrk-naming%3A-wip-%3Cfecha%3E-\">Naming: <code>WIP-&lt;fecha&gt;-&lt;tema&gt;</code>, <code>DRAFT-ADR-&lt;nnnn&gt;-&lt;slug&gt;</code>, <code>EVIDENCE-&lt;slug&gt;</code> + fecha.\nTags: <code>#draft</code> \u2192 <code>#ready</code> \u2192 <code>#superseded</code>.</p>\n<p id=\"bkmrk-no-escribir-en-el-ch\"><strong>No escribir en el chapter de otro agente.</strong> Tokens: Infisical services/bookstack/agents/.</p>\n"
 },
 {
  "id": 91,
  "title": "\u00cdNDICE de mirrors (l\u00e9eme primero)",
  "slug": "indice-de-mirrors-leeme-primero",
  "book_id": 8,
  "chapter_id": null,
  "description": null,
  "created_at": "2026-09-06T21:47:43.000000Z",
  "updated_at": "2026-09-06T21:47:43.000000Z",
  "markdown": "---\nid: REF-MIRROR-INDEX\ntitle: \u00cdndice de mirrors (book 70)\ntype: reference\nstatus: active\nowner: wetopi-lead\nsummary: Este book es SOLO mirror de lectura \u2014 Git es el SSOT de ADRs y Architecture Records.\nlast_verified: 2026-09-06\n---\n\n# Mirrors (solo lectura)\n\n**Git es el SSOT.** Los ADRs viven en `wetopi-platform/docs/docs/decisions/`\n(ADR-0001..0009, 0010..0013 rescatados 2026-09-06, ADR-0014) y los design docs en\n`docs/design/`. Esta copia es para navegaci\u00f3n humana en BookStack \u2014 si diverge, manda Git.\n\nRegla de promoci\u00f3n: cualquier cambio se hace en Git; este mirror se regenera.",
  "html": "<hr id=\"bkmrk-\">\n<h2 id=\"bkmrk-id%3A-ref-mirror-index\">id: REF-MIRROR-INDEX\ntitle: \u00cdndice de mirrors (book 70)\ntype: reference\nstatus: active\nowner: wetopi-lead\nsummary: Este book es SOLO mirror de lectura \u2014 Git es el SSOT de ADRs y Architecture Records.\nlast_verified: 2026-09-06</h2>\n<h1 id=\"bkmrk-mirrors-%28solo-lectur\">Mirrors (solo lectura)</h1>\n<p id=\"bkmrk-git-es-el-ssot.-los-\"><strong>Git es el SSOT.</strong> Los ADRs viven en <code>wetopi-platform/docs/docs/decisions/</code>\n(ADR-0001..0009, 0010..0013 rescatados 2026-09-06, ADR-0014) y los design docs en\n<code>docs/design/</code>. Esta copia es para navegaci\u00f3n humana en BookStack \u2014 si diverge, manda Git.</p>\n<p id=\"bkmrk-regla-de-promoci%C3%B3n%3A-\">Regla de promoci\u00f3n: cualquier cambio se hace en Git; este mirror se regenera.</p>\n"
 }
]