Saltar a contenido

Fase 4 — GitOps

Objetivo

Desplegar la plataforma completa vía Argo CD desde gitops/: storage, MetalLB, Gateway API, Dex + GitHub OAuth, y waves de operaciones.

Qué aprendes

GitOps: el clúster converge al estado declarado en Git; un solo kubectl apply (ApplicationSet homelab-root) y Argo CD sincroniza el resto. Separación clara: Ansible bootstrap vs GitOps continuo.

Aprende esta fase

En simple: Git es la receta aprobada y Argo CD es el chef que compara la cocina con la receta y corrige cualquier diferencia. Si alguien cambia algo a mano en el clúster, Argo CD lo deja como dice Git. El equipo no "ejecuta comandos": cambia la receta.

Conceptos clave

  • GitOps: el repositorio es la fuente de verdad; el clúster converge a lo que dice.
  • Application y sync: Argo CD vigila una carpeta de Git y la aplica; "Synced" significa que coinciden.
  • Drift y self-heal: drift es la diferencia entre Git y el clúster; el self-heal la corrige solo.
  • Olas (sync-waves): el orden de despliegue; la base (red, certificados) va antes que las aplicaciones.
  • Secretos fuera de Git: las claves viven en 1Password y se entregan al clúster sin subirlas al repositorio.
  • Gateway y TLS (Transport Layer Security): la puerta de entrada HTTPS y el certificado que la protege.

Reto práctico (solo lectura): compara el estado del clúster con lo que declara Git.

kubectl -n argocd get applications
kubectl get gateway homelab -n gateway

Abre gateway/kong/gateway.yaml en el repositorio gitops y busca los mismos puertos y el nombre homelab que ves en el clúster.

¿Qué significa que una Application esté Synced y Healthy?

Synced: lo que hay en el clúster coincide con Git. Healthy: los recursos funcionan.

¿Por qué los secretos no se guardan en Git?

Porque cualquiera con acceso al repositorio los vería, y el historial de Git conserva todo lo que se subió.

¿Qué hace el self-heal si alguien cambia algo a mano?

Detecta la diferencia con Git y vuelve a aplicar lo declarado.

¿Dudas? Usa el botón Aprende con IA junto a cada título, o la página Aprende.

Stack de esta fase

flowchart LR
  subgraph wave0 [Wave_0]
    GA[Gateway_API_CRDs]
    CM[cert_manager]
    SS[Sealed_Secrets]
    OB[OpenEBS]
    SC[StorageClasses]
    ML[MetalLB]
  end
  subgraph wave1 [Wave_1]
    MLC[MetalLB_config]
    CMC[CA_del_HomeLab]
    GW[Gateway_Kong]
    LH[Longhorn]
  end
  subgraph wave2 [Wave_2]
    AR[ArgoCD_HTTPRoute]
    ACFG[ArgoCD_Dex]
  end
  subgraph wave3 [Wave_3]
    UPG[K3s_upgrade]
  end
  subgraph manual [Manual]
    VC[vCluster]
    CAPN[CAPN_demo]
  end
  root[root_app] --> wave0 --> wave1 --> wave2 --> wave3
  wave2 -.-> manual

Por qué Gateway API y no Ingress

El proyecto Ingress NGINX se archivó en marzo de 2026 y Ingress está congelado en Kubernetes. El HomeLab publica todo con Gateway y HTTPRoute. Se activa un solo controlador a la vez (Kong, Traefik o NGINX Gateway Fabric): ver Cambiar el controlador de Gateway.

Profundización: Catálogo GitOps · Storage · Secretos

Antes de empezar

  • Fase 3 — K3s completada (argocd-server Running).
  • kubectl configurado en tu estación.
  • Fase 0 — GitHub y 1Password completado (Apps, repos e items de 1Password).
  • En el router, el rango 192.168.23.200–192.168.23.220 fuera del DHCP (reservado): es el pool de MetalLB para los servicios LoadBalancer, como el Gateway (esquema de IPs).

4.1 — Acceso kubectl

Copia a tu estación el kubeconfig del control plane (deborah), como ~/.kube/homelab-k3s.yaml: es el archivo que usan los playbooks y los procedimientos de esta guía. Si en la Fase 3 corriste playbook-k3s.yml completo, ya lo tienes (k3s_install.fetch_kubeconfig.enabled: true).

cd ansible
ansible-playbook -i inventory.ini playbook-k3s.yml --tags kubeconfig
cd ..

El rol k3s_fetch_kubeconfig lee /etc/rancher/k3s/k3s.yaml en deborah, cambia la dirección del API (Application Programming Interface) por la IP real del control plane (o la VIP si usas kube-vip) y lo guarda en ~/.kube/homelab-k3s.yaml con permisos 0600.

El archivo en el nodo es de root con permisos 600, así que se lee con sudo; después se cambia 127.0.0.1 por la IP del control plane. <usuario-admin> es tu usuario administrador del nodo (Fase 0, Día 0): aquí todavía no existe el acceso por SSO (4.12), que necesita a Dex.

mkdir -p ~/.kube
ssh <usuario-admin>@192.168.20.5 sudo cat /etc/rancher/k3s/k3s.yaml \
  | sed 's#https://127.0.0.1:6443#https://192.168.20.5:6443#' \
  > ~/.kube/homelab-k3s.yaml
chmod 600 ~/.kube/homelab-k3s.yaml

Úsalo y comprueba el acceso:

export KUBECONFIG=~/.kube/homelab-k3s.yaml
kubectl get nodes

Alternativa web (Fase 5)

Tras desplegar vCluster Platform, puedes descargar el kubeconfig del management K3s desde https://vcluster.homelab.local (connected cluster), con SSO GitHub vía Dex. El kubeconfig local de arriba sigue siendo el camino para el bootstrap, antes de la Fase 5.

Acceso kubectl
Kubeconfig local (Ansible o manual)
Funciona antes del Gateway y vCluster
Archivo en disco; rotación manual
vCluster Platform (Fase 5)
Kubeconfig vía SSO; connected cluster
Requiere Fase 4+5 y permisos asignados

4.2 — Bootstrap GitOps (ApplicationSet)

Un único apply manual; a partir de aquí Argo CD gestiona el resto (auto).

Primero, los secretos de plataforma. symintel/gitops es privado: sin las credenciales de la App symintel-argocd, ArgoCD no puede leer el repo. El playbook los lee de 1Password y los deja en el clúster. Igual que en la Fase 0, la app de 1Password tiene que estar desbloqueada y ONEPASSWORD_ACCOUNT_NAME con el nombre de tu cuenta (el de la barra lateral de la app).

export ONEPASSWORD_ACCOUNT_NAME="Mi Cuenta"  # tu cuenta
cd ansible
ansible-playbook -i inventory.ini playbook-platform-secrets.yml
cd ..
Secret Namespace Para qué
repo-gitops argocd App symintel-argocd para https://github.com/symintel/gitops.git
arc-runners-github-app arc-runners Registro del runner de ARC
symintel-terraform-app, tofu-vars arc-runners Credenciales de OpenTofu (TF_VAR_*) para el pipeline

También crea el namespace terraform, donde OpenTofu guarda su state. Usa siempre ~/.kube/homelab-k3s.yaml, no el contexto actual de kubectl. Es idempotente: re-córrelo cuando rotes algo en 1Password. Detalle: Plataforma symintel — GitHub.

Después, el ApplicationSet de bootstrap:

kubectl apply -f gitops/bootstrap/root-appset.yaml

bootstrap/root-appset.yaml genera una Application raíz, homelab-root, con las apps que están descomentadas en su lista apps. Al principio solo está argocd (ArgoCD administrándose a sí mismo) (Dex, RBAC y el health check que necesitan las olas), para ir probando cada componente de a uno:

  1. Descomenta la siguiente app de la lista (van en orden de ola) y haz push al repo gitops.
  2. Vuelve a aplicar el ApplicationSet: ArgoCD no lo gestiona (lo creaste tú con kubectl), así que el push solo no cambia la lista:

    kubectl apply -f gitops/bootstrap/root-appset.yaml
    
  3. ArgoCD la despliega; espera a que quede Healthy (kubectl get applications -n argocd).

  4. Sigue con la próxima.

Olas: como todas las apps se sincronizan juntas en homelab-root, ArgoCD respeta su anotación argocd.argoproj.io/sync-wave y despliega por olas (-1 → 0 → 1 → 2 → 3), esperando a que cada una esté Healthy antes de la siguiente. Las secciones 4.3 a 4.9 siguen ese orden.

Para desactivar una app, vuelve a comentarla: homelab-root borra su Application (los recursos que creó quedan en el clúster).

Verificar:

kubectl get applications -n argocd

UI (interfaz de usuario) provisional (sin Gateway aún; argocd-server habla HTTP, abre http://localhost:8080):

kubectl port-forward svc/argocd-server -n argocd 8080:80
kubectl get secret argocd-initial-admin-secret -n argocd \
  -o jsonpath='{.data.password}' | base64 -d; echo

4.3 — Wave 0: secretos + storage + MetalLB controller

Espera Healthy en: gateway-api, cert-manager, sealed-secrets, openebs, homelab-storage, metallb.

Application Producto Para qué sirve
gateway-api Gateway API CRDs (Custom Resource Definition) Gateway, HTTPRoute, etc., canal standard
cert-manager cert-manager Emite certificados; crea el de cada Gateway con la anotación cert-manager.io/cluster-issuer
sealed-secrets Sealed Secrets Descifra SealedSecret en el clúster; usa la llave de 1Password (item sealed-secrets) si el playbook de secretos ya la creó
openebs OpenEBS LocalPV default
homelab-storage — StorageClasses openebs-hostpath y longhorn-mixto (cuál usar y sus trade-offs)
metallb MetalLB Controller LoadBalancer

Verificar:

kubectl get storageclass
kubectl get pods -n metallb-system
kubectl get pods -n openebs

4.4 — Wave 1: pool MetalLB + Gateway + Longhorn

Espera Healthy en: metallb-config, cert-manager-config, kong y longhorn.

Application Producto Para qué sirve
metallb-config MetalLB Pool L2 192.168.23.200–.220
cert-manager-config cert-manager CA propia del HomeLab (ClusterIssuer homelab-ca)
kong Kong Ingress Controller Controlador de Gateway API y Gateway homelab (HTTP 80 y HTTPS 443, *.homelab.local)
longhorn Longhorn Storage replicado HA
kubectl get gateway homelab -n gateway
# ADDRESS en rango 192.168.23.200–.220 y PROGRAMMED=True

Verificar:

kubectl get ipaddresspool -n metallb-system
kubectl get pods -n longhorn-system

4.5 — Wave 2: ArgoCD en LAN + Dex

Espera Healthy en argocd-route y argocd.

Recurso Producto Para qué sirve
argocd-route Gateway API HTTPRoute hacia https://argocd.homelab.local (el Gateway termina el TLS; argocd-server queda en HTTP con server.insecure)
argocd ArgoCD y Dex ArgoCD se actualiza solo (tag stable); suma el connector GitHub, los staticClients de vCluster e Incus UI, el RBAC y el health check de Application (Actualizar ArgoCD)

4.6 — kube-vip vs MetalLB

kube-vip MetalLB
Quién lo instala Ansible (Fase 3) Argo CD (Fase 4)
Qué VIPea Solo API :6443 Services LoadBalancer
Config clave svc_enable=false Pool 192.168.23.200–.220

No uses kube-vip para Services de aplicación.

Análisis de trade-offs — kube-vip vs MetalLB

Alternativa Ventaja Coste / riesgo
kube-vip (Fase 3) VIP fija solo para API :6443 No expone Gateways ni Services LB
MetalLB (Fase 4) LoadBalancer para el Gateway y apps Pool L2 192.168.23.200–.220 debe estar libre en LAN
Ambos (default HomeLab) Responsabilidades separadas Dos mecanismos VIP distintos — no mezclar roles

4.7 — Secretos OAuth (GitHub + vCluster + Incus UI)

Los valores sensibles no van en git. Guárdalos en 1Password e inyecta argocd-secret.

OAuth App ya creada en la Fase 0

Si seguiste la Fase 0, la OAuth App Symintel Dex y sus campos GITHUB_* ya existen: solo falta correr el playbook de más abajo.

GitHub OAuth App en la org symintel (symintel → Settings → Developer settings → OAuth Apps → New OAuth App). Creada en la org, Dex puede leer sus teams sin aprobarla como app de terceros:

Campo GitHub Valor
Application name Symintel Dex (o similar)
Homepage URL https://argocd.homelab.local
Redirect URIs https://argocd.homelab.local/api/dex/callback (solo esta)

Homepage URL es informativa; el campo crítico es Redirect URIs (en versiones anteriores de GitHub se llamaba Authorization callback URL): tiene que ser exactamente la URL de arriba.

Ítems 1Password (cuenta personal, bóveda HomeLab por defecto): ver argocd/secrets/1password.md.

Probar lectura (mismo módulo que el apply):

export ONEPASSWORD_ACCOUNT_NAME="Mi Cuenta"   # tu cuenta personal
export ONEPASSWORD_VAULT="HomeLab"             # o HomeLab
python3 ansible/scripts/test-1password.py GITHUB_CLIENT_ID

Inyectar en el clúster (recomendado — Ansible):

cd ansible
ansible-playbook -i inventory.ini playbook-dex-oauth-secrets.yml

El campo VCLUSTER_CLIENT_SECRET se genera en 1Password automáticamente si no existe. Crea antes en 1Password (manual) GITHUB_CLIENT_ID y GITHUB_CLIENT_SECRET desde la OAuth App de GitHub.

Alternativa script Python:

pip install onepassword-sdk
python3 gitops/argocd/secrets/apply-from-1password.py.example
kubectl rollout restart deployment argocd-dex-server -n argocd

Connector GitHub (solo el team devops de la org symintel) y staticClients vCluster e Incus UI en dex.config.

VCLUSTER_CLIENT_SECRET no viene de GitHub. Ansible lo genera en 1Password si falta y lo aplica según destino. Incus no usa secreto: es un cliente OIDC público (PKCE) en Dex.

Campo Destinos (tags Ansible)
VCLUSTER_CLIENT_SECRET argocd-secret (argocd) + Helm Platform (vcluster)

Probar el login: URLs y qué esperar

Primero comprueba que las cuatro claves llegaron a argocd-secret y que Dex se reinició:

kubectl -n argocd get secret argocd-secret -o jsonpath='{.data}' | python3 -c "import sys,json;print(list(json.load(sys.stdin)))"
kubectl -n argocd get pods -l app.kubernetes.io/name=argocd-dex-server

Tienen que aparecer dex.github.clientId, dex.github.clientSecret y dex.vcluster.platform.clientSecret. Las URLs necesitan el DNS de *.homelab.local (o /etc/hosts); el certificado lo firma la CA del HomeLab, así que el navegador avisa hasta que la importes (y curl necesita -k).

Qué probar URL Qué debes ver
ArgoCD (UI) https://argocd.homelab.local Pantalla de login con el botón Log in via GitHub. Al pulsarlo pasa por GitHub y vuelve a ArgoCD con las Applications
ArgoCD (HTTP) http://argocd.homelab.local Redirige (301) a la versión HTTPS
ArgoCD (API) https://argocd.homelab.local/api/version {"Version":"v3.x.x"}: el servidor responde a través del Gateway
Dex (descubrimiento) https://argocd.homelab.local/api/dex/.well-known/openid-configuration JSON con "issuer": "https://argocd.homelab.local/api/dex"
Dex (callback) https://argocd.homelab.local/api/dex/callback No se abre a mano. Es la URL que tiene que estar en Redirect URIs de la OAuth App de GitHub
vCluster Platform https://vcluster.homelab.local Login SSO con GitHub (Fase 5; antes no existe)
Incus UI https://incus.homelab.local:8443 Login with SSO → GitHub (ver más abajo)

Desde la terminal:

curl -sk https://argocd.homelab.local/api/version
curl -sk https://argocd.homelab.local/api/dex/.well-known/openid-configuration
curl -s -o /dev/null -w "%{http_code} -> %{redirect_url}\n" http://argocd.homelab.local/
Si ves… Causa Qué hacer
GitHub responde 404 y la URL trae client_id=.github.clientId Faltan las claves dex.* en argocd-secret: Dex no pudo sustituir $dex.github.clientId Corre el playbook de arriba (--tags argocd) y vuelve a probar
GitHub dice redirect_uri mismatch Redirect URIs de la OAuth App distinto Debe ser exactamente https://argocd.homelab.local/api/dex/callback
El login termina bien pero ArgoCD no muestra Applications Tu cuenta no está en el team devops de symintel Agrégala al team; el claim groups es symintel:devops
invalid_client o bad credentials GITHUB_CLIENT_SECRET incorrecto o rotado Actualiza el campo en 1Password y vuelve a correr el playbook

Incus UI OIDC

Requisitos: UI instalada en Fase 2, argocd Healthy, secretos inyectados y Dex reiniciado.

Incus (7.x) solo conoce cinco claves OIDC: oidc.issuer, oidc.client.id, oidc.audience, oidc.claim y oidc.scopes. No tiene oidc.client.secret, ni oidc.groups.claim, ni el comando incus auth (esas son de LXD). Por eso en Dex el cliente incus-ui es público (public: true, PKCE, sin secreto): está en dex.config.

Ansible (recomendado) — group_vars/dex_oauth_secrets.yml ya trae apply_incus: true. Con las claves dex.* ya cargadas (Probar el login), corre:

export KUBECONFIG=~/.kube/homelab-k3s.yaml
export ONEPASSWORD_ACCOUNT_NAME="Mi Cuenta"
cd ansible
ansible-playbook -i inventory.ini playbook-dex-oauth-secrets.yml --tags ensure,argocd,incus

Eso aplica las claves en argocd-secret, reinicia Dex, instala la CA del HomeLab en invincible y configura oidc.issuer y oidc.client.id en Incus.

Incus tiene que confiar en la CA del HomeLab

Incus valida el certificado HTTPS de Dex (https://argocd.homelab.local/api/dex), que firma la CA propia del HomeLab. Si invincible no la conoce, el login falla (curl desde el nodo da ssl_verify=20). El playbook, con el tag incus, la lee del clúster (kubectl -n cert-manager get secret homelab-ca), la instala en /usr/local/share/ca-certificates/ y reinicia el servicio incus (las instancias siguen corriendo; la API queda unos segundos sin responder). Solo ese paso: --tags incus_ca.

Comprueba en el nodo que ya confía y que OIDC quedó configurado:

curl -s -o /dev/null -w "%{http_code} ssl_verify=%{ssl_verify_result}\n" https://argocd.homelab.local/api/dex/.well-known/openid-configuration
incus config get oidc.issuer
incus config get oidc.client.id

Deben dar 200 ssl_verify=0, https://argocd.homelab.local/api/dex e incus-ui.

Alternativa manual en el nodo bootstrap (invincible), después de instalar la CA (manual):

incus config set oidc.issuer=https://argocd.homelab.local/api/dex
incus config set oidc.client.id=incus-ui

Quién puede entrar y qué puede hacer

  • Quién inicia sesión: el conector GitHub de Dex solo deja pasar a miembros del team devops de la org symintel (orgs/teams en dex.config). Quien no está en el team no llega a Incus.
  • Qué puede hacer: con la configuración por defecto de Incus, cualquier usuario que se autentica con el proveedor OIDC tiene acceso completo al servidor. Para permisos granulares (solo un proyecto, solo lectura) Incus usa OpenFGA como motor de autorización: no está configurado en el HomeLab. Mientras tanto, el control es la pertenencia al team devops.

Verificar:

  • https://incus.homelab.local:8443 → Login with SSO → GitHub → entras a la UI de Incus
  • Una cuenta que no está en el team devops → GitHub/Dex rechaza el login antes de llegar a Incus

Flujo SSO

sequenceDiagram
  participant U as Usuario
  participant Argo as argocd_homelab_local
  participant Dex as Dex
  participant GH as GitHub
  U->>Argo: Login_SSO
  Argo->>Dex: OIDC
  Dex->>GH: OAuth
  GH->>Dex: token
  Dex->>U: sesion
Decisión Opción A (default HomeLab) Opción B
Secretos OAuth SDK local; cuenta Personal, bóveda HomeLab ONEPASSWORD_ACCOUNT_NAME / ONEPASSWORD_VAULT o kubectl patch

Análisis de trade-offs — secretos OAuth

Alternativa Ventaja Coste / riesgo
Ansible + 1Password SDK Genera INCUS_* / VCLUSTER_*; idempotente App 1Password desbloqueada en la estación
Script Python manual Mismo SDK sin playbook Pasos sueltos; fácil olvidar restart Dex
kubectl patch directo Sin 1Password Secretos en historial; no reproducible

4.8 — DNS de *.homelab.local

Los nombres los resuelve el DNS de la LAN (BIND en deborah, playbook-bind-dns.yml). La zona homelab.local trae:

Registro Apunta a
incus.homelab.local invincible (192.168.20.6)
invincible.homelab.local, oliver.homelab.local, deborah.homelab.local cada servidor, con su IP fija (192.168.20.6, .7 y .5)
argocd.homelab.local, vcluster.homelab.local, api.homelab.local el Gateway: gateway_ip = 192.168.23.200 (primera IP del pool de MetalLB)

No hay comodín: cada nombre es un registro explícito en db.homelab.local.j2. Para publicar una app con un HTTPRoute nuevo, añade su registro a esa plantilla y aplica el playbook; hasta entonces su nombre no resuelve.

La IP del Gateway es fija: los Service de kong, traefik y nginx-gateway la piden con una anotación de MetalLB, y la misma variable gateway_ip (ansible/group_vars/all.yml) alimenta el DNS. Si la cambias, cámbiala en los dos lados. Aplica el DNS (desde ansible/ en homelab):

ansible-playbook -i inventory.ini playbook-bind-dns.yml
dig @192.168.20.5 argocd.homelab.local +short   # 192.168.23.200
dig @192.168.20.5 oliver.homelab.local +short   # 192.168.20.7

Tu estación tiene que usar 192.168.20.5 como DNS. Si no puedes, el respaldo es /etc/hosts:

192.168.23.200  argocd.homelab.local vcluster.homelab.local api.homelab.local
192.168.20.6    incus.homelab.local invincible.homelab.local
192.168.20.7    oliver.homelab.local
192.168.20.5    deborah.homelab.local

Comprueba que el Gateway tiene esa IP:

kubectl get gateway homelab -n gateway -o jsonpath='{.status.addresses[0].value}'; echo

El certificado lo firma la CA del HomeLab: para que el navegador confíe, importa su certificado raíz (kubectl -n cert-manager get secret homelab-ca -o jsonpath='{.data.ca\.crt}' | base64 -d > homelab-ca.crt) o usa curl -k mientras tanto.

curl -kI https://argocd.homelab.local
curl -kI https://incus.homelab.local:8443

4.9 — Wave 3: actualizaciones K3s (opcional)

Espera Healthy en k3s-upgrade (system-upgrade-controller).


4.10 — Checklist Fase 4

Comprobación Criterio
Nodos Ready kubectl get nodes → 3 Ready
Apps auto Healthy kubectl get applications -n argocd
Storage default PVC openebs-hostpath provisiona
Gateway responde curl -k https://argocd.homelab.local
SSO Dex Login GitHub en ArgoCD
Incus UI OIDC Login SSO en https://incus.homelab.local:8443
Runner ARC arc-runners Healthy; runner listado en symintel → Settings → Actions → Runners (4.11)
Pipeline OpenTofu Primer tofu apply de infra en verde; existe terraform/tfstate-default-github (4.11)
SSH con Dex (opcional) opkssh login y ssh devops@oliver.homelab.local entran (4.12)

Checklist global (URLs, /etc/hosts, nodos): Resumen del HomeLab.


4.11 — ARC y pipeline de OpenTofu

Con arc-helm-repo, arc-controller, terraform-rbac y arc-runners descomentadas en homelab-root, ArgoCD despliega el runner de GitHub Actions:

Application Qué despliega
arc-controller Controller de ARC (arc-systems)
terraform-rbac ServiceAccount tofu-runner, con acceso solo a Secrets/Leases del namespace terraform
arc-runners El único scale set (hasta 3 runners), para CI, deploys y OpenTofu
arc-runners (caché) Volumen compartido arc-cache montado en /cache de los runners (Caché de los runners)
kubectl get applications -n argocd | grep -E 'arc|terraform'
kubectl get pods -n arc-systems        # controller + listener de arc-runners

El runner aparece en symintel → Settings → Actions → Runners.

Primer pipeline. En symintel/infra, lanza Actions → tofu → Run workflow sobre main, o haz un push a main. El workflow reusable tofu.yml corre en arc-runners: hace plan en los PRs (pull requests) y apply en main. La primera ejecución crea el repo api con sus environments y secrets FTP (File Transfer Protocol).

kubectl -n terraform get secret tfstate-default-github   # state tras el primer apply

A partir de acá, cada repo nuevo se agrega con el procedimiento de rutina — Agregar un repo: credenciales FTP en 1Password (si despliega), entrada en el mapa de infra por PR y el workflow de CI/CD.

4.12 — SSH a los hosts con Dex (opcional)

Los miembros del team devops de la org symintel en GitHub entran por SSH (Secure Shell, conexión cifrada por terminal) a invincible, oliver y deborah con la identidad de Dex, sin repartir llaves. Las instancias de Incus no entran aquí: se manejan con incus exec y el login SSO de la UI. Cómo entra una persona: Entrar por SSH a los servidores.

Requisitos.

  • Dex funcionando (4.5 y 4.7).
  • El DNS de homelab.local aplicado (4.8) y cada nodo usando el BIND de deborah en su /etc/resolv.conf (se fija en la Fase 1 con playbook-set-static-ip.yml). Sin eso un nodo no puede validar ningún login.
  • kubectl con el kubeconfig del HomeLab: el rol lee la CA de cert-manager para que los nodos confíen en Dex.
  • Tu llave normal en authorized_keys de cada nodo (hardening). Este acceso se suma a las llaves, no las reemplaza: es tu vía de emergencia si Dex o K3s caen.

Cómo funciona

SSH no habla OIDC (OpenID Connect, protocolo de inicio de sesión). opkssh, basado en OpenPubkey, hace de puente:

sequenceDiagram
    participant U as Estación del usuario
    participant D as Dex + GitHub
    participant H as Host (sshd + opkssh)
    U->>D: opkssh login (navegador, PKCE)
    D-->>U: token firmado (solo miembros del team devops)
    U->>H: ssh devops@host (llave efímera + token)
    H->>H: opkssh verify (proveedor, firma, grupo)
    H-->>U: sesión como devops
  1. opkssh login autentica contra Dex (GitHub) y deja una llave SSH que caduca a las 24 horas.
  2. Al conectar, sshd ejecuta opkssh verify (vía AuthorizedKeysCommand), que valida la firma del token y que el emisor es Dex.
  3. Un policy plugin de opkssh exige el grupo symintel:devops y el usuario Linux devops.

Pasos

  1. Cliente en Dex. El cliente opkssh (público, PKCE, con los tres callbacks en localhost) está en dex.config. Cuando ArgoCD sincronice, reinicia Dex para que lo cargue:

    kubectl -n argocd rollout restart deploy/argocd-dex-server
    
  2. Comprobar que los nodos resuelven Dex (desde ansible/ en homelab):

    ansible incus_cluster -i inventory.ini -b -m command -a "getent hosts argocd.homelab.local"
    

    Los tres deben dar 192.168.23.200. Si alguno no, arregla primero su DNS (ver Si falla abajo).

  3. Un nodo de prueba primero (oliver), con --check antes de aplicar:

    export KUBECONFIG=~/.kube/homelab-k3s.yaml
    cd ansible && source ../.venv/bin/activate
    ansible-playbook -i inventory.ini playbook-opkssh.yml --limit oliver --check --diff
    ansible-playbook -i inventory.ini playbook-opkssh.yml --limit oliver
    

    El rol se detiene antes de instalar nada si el nodo no resuelve a Dex o no confía en su certificado.

  4. Probar el login siguiendo Entrar por SSH a los servidores y, con la sesión funcionando, extender al resto: --limit invincible,deborah.

Qué instala el rol opkssh en cada nodo: el binario con versión fijada y su checksum, /etc/opk/ (proveedor Dex y el plugin), la cuenta Linux devops, la CA del HomeLab y un archivo en /etc/ssh/sshd_config.d/.

Quién entra

El acceso depende de pertenecer al team devops en GitHub. No hay listas de usuarios ni de emails.

  • Dex solo emite tokens a miembros del team (conector GitHub) y pone el team en el claim groups como symintel:devops.
  • Cada nodo exige ese grupo con un policy plugin (/etc/opk/devops-team.sh). El grupo se cambia con opkssh_allowed_group en el rol.

Dar de alta o de baja es cambiar el team en GitHub. Una baja impide obtener tokens nuevos, pero la llave que ya tenga la persona vale hasta que caduque (24 horas).

Por qué un plugin y no /etc/opk/auth_id

La regla normal oidc:groups:symintel:devops no funciona con grupos de GitHub: opkssh separa la regla por : y solo compara el último trozo (devops) con el grupo completo (symintel:devops), así que nunca coinciden, y poner el valor entre comillas tampoco sirve. Por eso auth_id queda sin reglas y decide el plugin. Se comprobó leyendo policy/enforcer.go de opkssh.

Cuenta compartida con sudo

Todos entran como devops, con sudo sin contraseña (opkssh_login_user_sudo_nopasswd): quien está en el team es administrador de los tres nodos y el registro de sesiones no distingue personas por el usuario Linux. Para exigir contraseña de sudo, pon esa variable en false y asigna una contraseña a la cuenta.

Análisis de trade-offs — cómo entrar por SSH

Alternativa Ventaja Coste / riesgo
opkssh con Dex (esta guía) Reutiliza Dex y el team; llaves de 24 h; sin listas que mantener Proyecto joven; hace falta un plugin por los grupos con :; Dex corre en K3s sobre estos mismos nodos
CA SSH con certificados efímeros (step-ca) Muy madura Otro componente con su propia llave raíz que operar. Plan B
Llaves públicas de GitHub filtradas por team Simple No usa Dex; las llaves no caducan
authorized_keys por Ansible Sin piezas nuevas Cada alta o baja es un cambio manual
Teleport / Boundary Auditoría y grabación de sesiones Pesado para tres nodos; Teleport Community no ofrece OIDC genérico

Deshacer

playbook-uninstall-opkssh.yml quita lo que instala el rol (configuración de sshd, binario, /etc/opk y la cuenta devops). Es seguro por defecto: sin -e opkssh_uninstall_confirm=true solo muestra qué quitaría.

ansible-playbook -i inventory.ini playbook-uninstall-opkssh.yml --limit oliver
ansible-playbook -i inventory.ini playbook-uninstall-opkssh.yml --limit oliver -e opkssh_uninstall_confirm=true

No toca authorized_keys ni el hardening. La CA del HomeLab se conserva; para quitarla también: -e opkssh_uninstall_remove_ca=true.

Si falla (en el servidor)

Síntoma Causa probable Qué revisar
El rol se detiene: "no resuelve argocd.homelab.local" El nodo no usa el BIND del HomeLab. Pasó en deborah: NetworkManager, sin dns=none, reescribió /etc/resolv.conf con el DNS del router cat /etc/resolv.conf y getent hosts argocd.homelab.local en el nodo. Corrige con playbook-set-static-ip.yml --limit <nodo> (Fase 1)
Permission denied; en /var/log/opkssh.log: no policy to allow … El token no trae el grupo symintel:devops, o se pidió otro usuario Linux opkssh inspect ~/.ssh/id_ecdsa-cert.pub (debe listar groups); /etc/opk/policy.d/devops-team.yml
Permission denied; en /var/log/opkssh.log: lookup … no such host Igual que la primera fila DNS del nodo
x509: certificate signed by unknown authority en el nodo El nodo no confía en la CA del HomeLab curl -s -o /dev/null -w "%{http_code} ssl_verify=%{ssl_verify_result}\n" https://argocd.homelab.local/api/dex/.well-known/openid-configuration debe dar 200 ssl_verify=0
Nadie entra con SSO y Dex no responde Dex vive en K3s sobre estos mismos nodos Entra con la llave de emergencia de authorized_keys

Opciones post-Fase 4

Componente Sync Siguiente paso
vCluster Platform manual Fase 5
CAPN manual Fase 6
CNI externo (Canal / Calico / Cilium) manual Fase 3 — CNI; normalmente vía Ansible k3s_cni, no Fase 4

Análisis de trade-offs — post-Fase 4

Alternativa Ventaja Coste / riesgo
Terminar en Fase 4 Management GitOps completo; menos superficie Sin vClusters ni workload clusters CAPN
Fase 5 vCluster Kubeconfig web; clústeres virtuales ligeros Más RAM en K3s; OAuth + permisos Platform
Fase 6 CAPN Clusters kubeadm reales sobre Incus Alto consumo RAM; sync manual obligatorio
CNI vía GitOps Reinstalar Canal/Calico/Cilium sin Ansible Solo con K3s en flannel-backend=none; no mezclar CNIs
Sync manual (5/6) No borra clusters efímeros por prune Paso extra en Argo CD UI

Si falla

Síntoma Revisar
Application Degraded kubectl describe application -n argocd <nombre>
Gateway sin dirección metallb-config, pool libre
SSO falla Secretos 1Password + restart argocd-dex-server
homelab-root ComparisonError / repo no accesible Falta argocd/repo-gitops, o la App symintel-argocd no está instalada en symintel/gitops: re-correr playbook-platform-secrets.yml
arc-runners OutOfSync y el pod del listener se reinicia cada pocos minutos ArgoCD poda en bucle los recursos que crea el controlador de ARC (AutoscalingListener, Role, RoleBinding: copian la etiqueta app.kubernetes.io/instance). Se corrige con application.resourceTrackingMethod: annotation en argocd-cm (ya está en gitops/argocd/config). Comprueba: kubectl -n argocd get cm argocd-cm -o jsonpath='{.data.application\.resourceTrackingMethod}' → annotation; si no, sincroniza argocd y reinicia el controlador: kubectl -n argocd rollout restart statefulset argocd-application-controller
Apps en Error: ComparisonError … terminatingReplicas: field not declared in schema ArgoCD es anterior a la 3.5 y el clúster tiene Kubernetes 1.34 o más nuevo: Actualizar ArgoCD
Login con GitHub da 404 (client_id=.github.clientId) Faltan dex.* en argocd-secret: corre playbook-dex-oauth-secrets.yml --tags argocd (4.7)
Runner no aparece en GitHub kubectl logs -n arc-systems del listener; Secret arc-runners/arc-runners-github-app (App instalada en symintel)
Pipeline tofu sin runner o sin credenciales Runner arc-runners Online; Secrets symintel-terraform-app/tofu-vars en arc-runners
Longhorn pending Etiquetas Longhorn (playbook-options)

Catálogo y troubleshooting extendido: GitOps — profundización.

Siguiente

Opcional:

O terminar aquí si solo necesitas el management cluster GitOps.