> ## Documentation Index
> Fetch the complete documentation index at: https://docs.befailproof.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Guía de Despliegue en Kubernetes

> Documentación de la guía de despliegue de AgentEye en Kubernetes.

Esta guía despliega el stack completo de AgentEye en un clúster de Kubernetes dedicado:

* **ClickHouse 24.8** -- almacén canónico de análisis de eventos y evaluaciones (StatefulSet con volumen persistente de 100 Gi). Obligatorio: el servidor no arranca sin él.
* **PostgreSQL 16** -- almacén relacional/de metadatos para organizaciones, claves de API, usuarios, paneles, consultas guardadas y autenticación (StatefulSet con volumen persistente de 50 Gi)
* **Redis 7.2** -- caché compartida opcional y backend de límite de tasa; el servidor y el panel degradan con elegancia si no está disponible
* **AgentEye Server** -- API en Rust para ingestión de eventos, análisis y gestión de claves (2 réplicas)
* **AgentEye Dashboard** -- interfaz web en Next.js (2 réplicas)
* **AI assistant (servicio de agente)** -- asistente opcional de solo lectura en el panel en el puerto 9100; inactivo hasta que se configure un endpoint de LLM
* **Traefik (público)** -- controlador de ingreso para el tráfico del colector, protegido con mTLS
* **Traefik (panel)** -- controlador de ingreso para el panel, solo accesible desde VPN/lista de IPs permitidas
* **cert-manager** -- certificados TLS y CA de mTLS
* **Backup CronJob** -- volcado diario combinado de PostgreSQL + ClickHouse a las 03:00 UTC
* **Cert Renewal Monitor** -- alerta cuando los certificados de cliente están próximos a caducar

**Tiempo estimado:** 60--90 minutos para un primer despliegue.

Para el modelo de despliegue gestionado donde Exosphere se encarga de todo esto en tu nombre, consulta [enterprise-docs/managed-deployment.md](/es/agenteye/managed-deployment).

***

## Requisitos previos

Ejecuta cada comando de verificación antes de comenzar. Todas las comprobaciones deben pasar.

| Requisito                           | Mínimo                                       | Comando de verificación                                                    | Resultado esperado                                                          |
| ----------------------------------- | -------------------------------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Clúster de Kubernetes               | 1.27+                                        | `kubectl version`                                                          | Server Version >= v1.27                                                     |
| Kustomize (incluido con kubectl)    | Kustomize v1.14+ (incluido en kubectl 1.27+) | `kubectl kustomize --help`                                                 | Muestra texto de uso                                                        |
| Helm                                | v3                                           | `helm version`                                                             | `Version:"v3.x.x"`                                                          |
| cluster-admin RBAC                  | --                                           | `kubectl auth can-i create namespaces`                                     | `yes`                                                                       |
| StorageClass por defecto            | --                                           | `kubectl get storageclass`                                                 | Al menos una fila marcada como `(default)`                                  |
| Soporte de LoadBalancer             | --                                           | Depende del proveedor (EKS, GKE, AKS lo soportan por defecto)              | --                                                                          |
| GitHub PAT                          | --                                           | `echo $AGENTEYE_TOKEN`                                                     | No vacío (ver [enterprise-docs/github-token.md](/es/agenteye/github-token)) |
| openssl                             | --                                           | `openssl version`                                                          | OpenSSL 1.x o 3.x                                                           |
| Bucket de almacenamiento en la nube | --                                           | Para copias de seguridad de PostgreSQL + ClickHouse (S3, GCS o Azure Blob) | --                                                                          |

**Dimensionamiento del clúster:** Mínimo 3 nodos, 4 vCPU / 8 GB de RAM cada uno. Consulta [enterprise-docs/managed-deployment.md](/es/agenteye/managed-deployment) para los requisitos completos.

### Ejecutar todas las comprobaciones a la vez

```bash theme={null}
echo "--- Prerequisites Check ---"
kubectl version --client -o yaml 2>/dev/null | grep -q gitVersion && echo "PASS: kubectl" || echo "FAIL: kubectl not found"
helm version --short 2>/dev/null | grep -q v3 && echo "PASS: helm v3" || echo "FAIL: helm v3 not found"
kubectl auth can-i create namespaces 2>/dev/null | grep -q yes && echo "PASS: cluster-admin" || echo "FAIL: no cluster-admin"
kubectl get storageclass 2>/dev/null | grep -q default && echo "PASS: default StorageClass" || echo "FAIL: no default StorageClass"
[ -n "$AGENTEYE_TOKEN" ] && echo "PASS: AGENTEYE_TOKEN set" || echo "FAIL: AGENTEYE_TOKEN not set"
openssl version >/dev/null 2>&1 && echo "PASS: openssl" || echo "FAIL: openssl not found"
echo "---"
```

### Estructura del despliegue

El **endpoint de ingestión** se sirve en un nombre de host que controlas (por ejemplo, `ingest.tu-empresa.example`). cert-manager solicita un certificado TLS de confianza pública a Let's Encrypt mediante HTTP-01, de modo que los colectores verifican el certificado del servidor contra el almacén de confianza del sistema, sin necesidad de anclar una CA por cliente.

El **endpoint del panel** funciona de la misma manera: se sirve en un segundo nombre de host que controlas (por ejemplo, `agenteye.tu-empresa.example`) apuntando al LoadBalancer de Traefik del panel, y cert-manager emite su certificado Let's Encrypt a través de ese LoadBalancer. Los navegadores obtienen un certificado de confianza sin advertencias.

> **La emisión y renovación de certificados se validan mediante HTTP-01**, por lo que ambos LoadBalancers deben ser accesibles desde internet en el puerto 80. Si necesitas restringir el LoadBalancer del panel por IP, coordina primero con soporte un solver DNS-01, de lo contrario las renovaciones fallarán silenciosamente y el certificado caducará.

***

## Obtener los manifiestos

```bash theme={null}
git clone https://x:${AGENTEYE_TOKEN}@github.com/agenteye-enterprise/releases.git
cd releases/deploy
```

**Verificación:**

```bash theme={null}
ls base/kustomization.yaml
```

Resultado esperado: el archivo existe. Si no existe, la clonación ha fallado -- verifica tu `AGENTEYE_TOKEN`.

**Estructura de directorios:**

```
deploy/
  base/           Base compartida de Kustomize (todos los recursos K8s)
  overlays/       Sobreescrituras específicas del clúster (etiquetas de imagen, nombres de host, recursos)
  third-party/    Valores de Helm para Traefik, cert-manager y (opcional) monitorización de salud con Robusta
```

La **base** contiene todos los recursos necesarios para un despliegue completo, incluidos los certificados Let's Encrypt para los dos nombres de host públicos que configuras en la Fase 3.1. Un **overlay** parchea la base para un entorno específico (por ejemplo, etiquetas de imagen personalizadas, límites de recursos, variables de entorno). El directorio **third-party** contiene archivos de valores de Helm para infraestructura externa.

> **Monitorización de salud (opcional):** la sonda de disponibilidad del servidor ya refleja el estado de Postgres + ClickHouse, y `third-party/robusta/` añade alertas opcionales de fallo de pod en Kubernetes nativas a Slack. Consulta [enterprise-docs/health-monitoring.md](/es/agenteye/health-monitoring).

***

## Fase 1 -- Infraestructura de terceros (\~30 min)

### 1.1 Instalar cert-manager

cert-manager gestiona los certificados TLS para HTTPS y la CA privada utilizada para los certificados de cliente mTLS.

```bash theme={null}
helm repo add jetstack https://charts.jetstack.io
helm repo update

helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager --create-namespace \
  --set crds.install=true
```

**Verificación:**

```bash theme={null}
kubectl get pods -n cert-manager
```

Resultado esperado: 3 pods en estado `Running` -- `cert-manager`, `cert-manager-cainjector`, `cert-manager-webhook`.

```bash theme={null}
kubectl get crds | grep cert-manager
```

Resultado esperado: al menos `certificates.cert-manager.io`, `clusterissuers.cert-manager.io`, `issuers.cert-manager.io`.

**Si falla:** Los pods en `CrashLoopBackOff` normalmente indican que los CRDs no se instalaron. Vuelve a ejecutar con `--set crds.install=true`. Si los pods del webhook fallan en la verificación de disponibilidad, espera 30 segundos y vuelve a comprobar -- pueden tardar un momento en arrancar.

***

### 1.2 Instalar Traefik -- Controlador de ingestión público

Esta instancia de Traefik gestiona el tráfico del colector en un LoadBalancer **externo**. Termina TLS y aplica mTLS (verificación de certificado de cliente) en el endpoint de ingestión.

```bash theme={null}
helm repo add traefik https://traefik.github.io/charts
helm repo update

helm install traefik-public traefik/traefik \
  --namespace traefik-public --create-namespace \
  --version 39.0.8 \
  -f third-party/traefik/values-public.yaml
```

**Verificación:**

```bash theme={null}
kubectl get pods -n traefik-public
```

Resultado esperado: 1 pod en estado `Running`.

```bash theme={null}
kubectl get ingressclass traefik-public
```

Resultado esperado: la IngressClass existe (no es la clase por defecto).

**Si falla:** Comprueba `kubectl describe pod -n traefik-public <nombre-pod>` para errores de descarga de imagen o restricciones de recursos.

***

### 1.3 Instalar Traefik -- Controlador del panel

Esta instancia de Traefik sirve el panel en un LoadBalancer dedicado, restringido por lista de IPs permitidas.

> **Esta instancia incluye dos mecanismos de lista de permitidos.** Esta guía utiliza `values-dashboard.yaml`, que restringe el acceso con el campo portable `service.loadBalancerSourceRanges`. También se proporciona un `values-internal.yaml` paralelo para entornos AWS que prefieren la anotación `service.beta.kubernetes.io/aws-load-balancer-source-ranges`. Elige uno y úsalo de forma consistente; los pasos siguientes asumen `values-dashboard.yaml`.

**Antes de instalar**, edita `third-party/traefik/values-dashboard.yaml` para establecer las IPs de origen permitidas. El campo `loadBalancerSourceRanges` controla qué IPs pueden acceder al panel. Por defecto está configurado como `0.0.0.0/0` (todas las IPs); restrínguelo a tu VPN, oficina o IPs de salida conocidas.

#### Permitir una sola IP

```yaml theme={null}
service:
  loadBalancerSourceRanges:
    - "<TU_IP_VPN>/32"
```

#### Permitir múltiples IPs

Añade una entrada por IP o bloque CIDR. Un sufijo `/32` coincide con una sola dirección IPv4; un bloque CIDR (por ejemplo, `/24`) coincide con un rango. Puedes mezclar IPs individuales y rangos libremente:

```yaml theme={null}
service:
  loadBalancerSourceRanges:
    - "203.0.113.10/32"       # puerta de enlace de la oficina
    - "203.0.113.11/32"       # puerta de enlace de oficina de respaldo
    - "198.51.100.0/24"       # pool de VPN
    - "192.0.2.50/32"         # IP doméstica del ingeniero de guardia
```

Consejos para mantener la lista:

* Mantén una entrada por línea y añade un comentario `#` breve que identifique el propietario o propósito de cada IP; esto es lo que los futuros operadores usarán para decidir si una entrada sigue siendo necesaria.
* Usa siempre notación CIDR. Una IP sin sufijo como `203.0.113.10` es rechazada por el proveedor de nube; usa `203.0.113.10/32`.
* Para rangos IPv6, usa el equivalente `/128` (dirección única) o un CIDR mayor, por ejemplo `2001:db8::1/128`. No todos los proveedores de nube admiten rangos de origen IPv6; consulta la documentación de LoadBalancer de tu proveedor.
* La lista es un **OR**: el tráfico se permite si el origen coincide con cualquier entrada.

Después de editar el archivo, procede con `helm install` a continuación. Si el controlador ya está instalado, ejecuta `helm upgrade` con las mismas opciones, o parchea el Service en tiempo de ejecución (sección siguiente).

#### Actualizar la lista de permitidos en tiempo de ejecución

Puedes cambiar las IPs permitidas sin actualizar Helm parcheando el Service directamente. **El parche reemplaza la lista completa**; incluye siempre todas las IPs que quieras conservar, no solo la nueva.

Para reemplazar la lista con un nuevo conjunto de IPs:

```bash theme={null}
kubectl patch svc traefik-dashboard -n traefik-dashboard \
  -p '{"spec":{"loadBalancerSourceRanges":["203.0.113.10/32","198.51.100.0/24","192.0.2.50/32"]}}'
```

Para **añadir** una IP de forma segura sin perder las entradas existentes, lee primero la lista actual y luego aplica el parche con el conjunto combinado:

```bash theme={null}
# 1. Muestra la lista de permitidos actual
kubectl get svc traefik-dashboard -n traefik-dashboard \
  -o jsonpath='{.spec.loadBalancerSourceRanges}'

# 2. Aplica el parche con la lista completa incluyendo la nueva IP
kubectl patch svc traefik-dashboard -n traefik-dashboard \
  -p '{"spec":{"loadBalancerSourceRanges":["<IP_EXISTENTE_1>/32","<IP_EXISTENTE_2>/32","<NUEVA_IP>/32"]}}'
```

> Los parches en tiempo de ejecución no se persisten en `values-dashboard.yaml`. Para mantener el cambio en futuras actualizaciones de Helm, actualiza también el archivo de valores y confírmalo en el repositorio.

Luego instala:

```bash theme={null}
helm install traefik-dashboard traefik/traefik \
  --namespace traefik-dashboard --create-namespace \
  --version 39.0.8 \
  -f third-party/traefik/values-dashboard.yaml
```

**Verificación:**

```bash theme={null}
kubectl get pods -n traefik-dashboard
```

Resultado esperado: 1 pod en estado `Running`.

```bash theme={null}
kubectl get ingressclass traefik-dashboard
```

Resultado esperado: la IngressClass existe.

***

### 1.4 Esperar a los LoadBalancers

Ambas instancias de Traefik necesitan IPs externas antes de continuar.

```bash theme={null}
kubectl get svc -n traefik-public
kubectl get svc -n traefik-dashboard
```

**Verificación:** Ambos servicios muestran un `EXTERNAL-IP` (no `<pending>`).

Si aún están pendientes, espera a que se asigne:

```bash theme={null}
kubectl get svc -n traefik-public -w
```

Pulsa `Ctrl+C` cuando aparezca la IP. La asignación de IP suele tardar entre 2 y 5 minutos.

**Si falla:** `<pending>` después de 10 minutos normalmente significa que el proveedor de nube no puede aprovisionar un LoadBalancer. Comprueba: etiquetas de subred (EKS requiere `kubernetes.io/role/elb`), configuración de VPC, cuotas de servicio y que la anotación correcta del LB interno esté configurada para la instancia interna.

***

## Fase 2 -- Crear secretos (\~10 min)

Todos los secretos se crean manualmente antes de desplegar la aplicación. Esto garantiza que los valores sensibles nunca aparezcan en los archivos de manifiesto.

### 2.1 Crear el namespace

```bash theme={null}
kubectl create namespace agenteye
```

**Verificación:**

```bash theme={null}
kubectl get namespace agenteye
```

Resultado esperado: estado `Active`.

***

### 2.2 Secreto de extracción de imagen

Este secreto autentica con `ghcr.io` para extraer las imágenes de contenedor de AgentEye. Consulta [enterprise-docs/github-token.md](/es/agenteye/github-token) para saber cómo generar tu PAT.

```bash theme={null}
kubectl create secret docker-registry agenteye-image-pull \
  --namespace agenteye \
  --docker-server=ghcr.io \
  --docker-username=agenteye-enterprise \
  --docker-password="${AGENTEYE_TOKEN}"
```

**Verificación:**

```bash theme={null}
kubectl get secret agenteye-image-pull -n agenteye -o jsonpath='{.type}'
```

Resultado esperado: `kubernetes.io/dockerconfigjson`.

**Verificación profunda** -- comprueba que el token puede extraer imágenes realmente:

Usa la etiqueta de imagen `server` fijada en el `kustomization.yaml` de tu overlay (actualmente `v0.0.1-beta.48` tanto en el overlay `acme` incluido como en el despliegue base). Sustituye la etiqueta en el comando siguiente por la que estés desplegando para que esta comprobación no quede desactualizada entre versiones:

```bash theme={null}
kubectl run test-pull \
  --image=ghcr.io/agenteye-enterprise/server:v0.0.1-beta.48 \
  --overrides='{"spec":{"imagePullSecrets":[{"name":"agenteye-image-pull"}]}}' \
  --restart=Never -n agenteye --command -- echo ok

# Espera unos segundos para la descarga y luego:
kubectl logs test-pull -n agenteye
kubectl delete pod test-pull -n agenteye
```

Resultado esperado: `ok` impreso en los logs.

**Si falla:** `ErrImagePull` o `401 Unauthorized` significa que el PAT no es válido o no tiene el permiso `read:packages`. Revisa [enterprise-docs/github-token.md](/es/agenteye/github-token).

***

### 2.3 Credenciales de PostgreSQL

```bash theme={null}
POSTGRES_PASSWORD=$(openssl rand -hex 24)

kubectl create secret generic agenteye-postgres \
  --namespace agenteye \
  --from-literal=POSTGRES_USER=postgres \
  --from-literal=POSTGRES_PASSWORD="${POSTGRES_PASSWORD}" \
  --from-literal=POSTGRES_DB=agenteye
```

> **Importante:** Usamos `-hex` (no `-base64`) para generar la contraseña. La salida en base64 puede contener `+`, `/` y `=`, que rompen la cadena de conexión `DATABASE_URL`. Consulta [enterprise-docs/troubleshooting.md](/es/agenteye/troubleshooting) para más detalles.

> **Guarda `POSTGRES_PASSWORD` en tu gestor de secretos inmediatamente.** La necesitarás si alguna vez restauras desde una copia de seguridad o te conectas directamente a la base de datos.

**Verificación:**

```bash theme={null}
kubectl get secret agenteye-postgres -n agenteye
```

Resultado esperado: el secreto existe.

```bash theme={null}
kubectl get secret agenteye-postgres -n agenteye \
  -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d | wc -c
```

Resultado esperado: `48` (24 bytes hexadecimales = 48 caracteres).

***

### 2.4 Clave de API de administrador

```bash theme={null}
ADMIN_KEY=$(openssl rand -hex 32)

kubectl create secret generic agenteye-admin-key \
  --namespace agenteye \
  --from-literal=ADMIN_KEY="${ADMIN_KEY}"
```

La clave de administrador es la credencial de arranque inicial. El servidor la inserta o actualiza en cada inicio con todos los permisos. Úsala para crear claves de colector con permisos restringidos en la Fase 7. Consulta [enterprise-docs/api-keys.md](/es/agenteye/api-keys) para el modelo completo de permisos.

> **Guarda `ADMIN_KEY` en tu gestor de secretos inmediatamente.**

**Verificación:**

```bash theme={null}
kubectl get secret agenteye-admin-key -n agenteye
```

Resultado esperado: el secreto existe.

***

### 2.5 Configuración de autenticación (inicio de sesión en el panel)

El panel utiliza email + OTP para el inicio de sesión de usuarios. Sin este secreto, el servidor sigue arrancando y la ruta de API de `ADMIN_KEY` sigue funcionando, pero **ningún usuario puede iniciar sesión a través de la interfaz de usuario**.

Todas las claves están referenciadas como `optional: true` en el manifiesto base, por lo que los secretos parciales (o ningún secreto) son válidos; el servidor recurre a los valores predeterminados documentados. Agrupar todo en un único secreto `agenteye-auth` permite rotar toda la superficie de autenticación en un solo lugar.

```bash theme={null}
kubectl create secret generic agenteye-auth \
  --namespace agenteye \
  --from-literal=ADMIN_EMAIL="admin@tuempresa.com" \
  --from-literal=ALLOWED_EMAILS="*@tuempresa.com" \
  --from-literal=SMTP_HOST="smtp.tuproveedor.com" \
  --from-literal=SMTP_PORT="587" \
  --from-literal=SMTP_USERNAME="tu-usuario-smtp" \
  --from-literal=SMTP_PASSWORD="tu-contraseña-smtp" \
  --from-literal=SMTP_FROM="noreply@tuempresa.com" \
  --from-literal=SMTP_TLS="starttls" \
  --from-literal=DEFAULT_ORG_NAME="Acme Corp" \
  --from-literal=DEFAULT_ORG_SLUG="acme"
```

| Clave                                                                   | Propósito                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ADMIN_EMAIL`                                                           | Usuario administrador inicial. Se inserta o actualiza en cada inicio con todos los permisos y está protegido contra eliminación/edición de permisos desde el panel. Sin él, no se crea ningún administrador y el primer inicio de sesión es imposible.                                                                                                                                                                                                                                  |
| `ALLOWED_EMAILS`                                                        | Lista de permitidos separada por comas. Admite direcciones exactas (`user@example.com`) y comodines de dominio (`*@example.com`). Sin él, **ningún usuario puede iniciar sesión ni ser creado**.                                                                                                                                                                                                                                                                                        |
| `SMTP_HOST`, `SMTP_PORT`, `SMTP_USERNAME`, `SMTP_PASSWORD`, `SMTP_FROM` | Relay SMTP para enviar códigos OTP. Si `SMTP_HOST` no está configurado, los códigos OTP se registran en stdout del servidor en lugar de enviarse por email (útil para pruebas de humo en el primer arranque). Proporciona todas las claves SMTP juntas para el envío real de emails.                                                                                                                                                                                                    |
| `SMTP_TLS`                                                              | Uno de `starttls` (por defecto), `tls` o `none`.                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `DEFAULT_ORG_NAME`, `DEFAULT_ORG_SLUG`                                  | Opcional. Asigna un nombre de visualización amigable y un slug de URL a la organización `default` integrada, de modo que viva en, por ejemplo, `/acme` en lugar de `/default`. Se aplica **solo en el primer arranque**; una vez que renombres la organización con `agenteye-orgctl org rename` (ver §7.6) se ignoran. El slug debe tener entre 1 y 40 caracteres alfanuméricos en minúsculas con guiones internos simples. Déjalos sin configurar para mantener el `default` genérico. |

> **Guarda las credenciales SMTP en tu gestor de secretos.**

**Verificación:**

```bash theme={null}
kubectl get secret agenteye-auth -n agenteye \
  -o jsonpath='{.data}' | grep -o '"[A-Z_]*"' | sort -u
```

Resultado esperado: las claves que has configurado aparecen en la salida.

***

### 2.6 Clave de aislamiento de organizaciones multi-tenant (opcional)

Omite esto para un despliegue de un solo tenant; el servidor funciona con un valor de desarrollo integrado y sirve correctamente a la organización `default`. **Antes de crear una segunda organización**, configura un `ORG_CH_SECRET` fuerte y estable: la contraseña de ClickHouse de cada organización se deriva como `HMAC(ORG_CH_SECRET, org_id)`, de modo que el valor de desarrollo por defecto, que es público, generaría credenciales por organización derivables públicamente. El comando `agenteye-orgctl org create` (ver [§7.6 Aprovisionar organizaciones](#76-provision-organizations-multi-tenant)) se niega a ejecutarse mientras el servidor siga usando el valor de desarrollo integrado.

```bash theme={null}
kubectl create secret generic agenteye-org-ch-secret \
  --namespace agenteye \
  --from-literal=ORG_CH_SECRET="$(openssl rand -base64 48)"

# Reinicia el servidor para que recoja el nuevo valor.
kubectl -n agenteye rollout restart deployment/server
```

El servidor lo lee a través de una referencia `secretKeyRef` **opcional**, por lo que un clúster de un solo tenant que nunca lo crea sigue arrancando normalmente. Mantén el valor **estable e idéntico en todas las réplicas**; rotarlo invalida la contraseña de ClickHouse derivada de cada organización hasta que la reconciliación al arrancar vuelva a aprovisionar los usuarios (un reinicio gradual con el valor consistente en todas partes lo soluciona). Consulta `deploy/base/server/secret.example.yaml`.

> **Guarda `ORG_CH_SECRET` en tu gestor de secretos y no lo rotes sin motivo.**

***

### 2.7 Verificar todos los secretos

```bash theme={null}
kubectl get secrets -n agenteye -o custom-columns='NAME:.metadata.name,TYPE:.type'
```

Salida esperada (entre los secretos predeterminados):

```
NAME                    TYPE
agenteye-admin-key      Opaque
agenteye-auth           Opaque
agenteye-image-pull     kubernetes.io/dockerconfigjson
agenteye-postgres       Opaque
agenteye-org-ch-secret  Opaque   # solo si completaste §2.6 (multi-tenant)
```

Los cuatro secretos principales (`agenteye-admin-key`, `agenteye-auth`, `agenteye-image-pull`, `agenteye-postgres`) deben estar presentes antes de continuar. `agenteye-org-ch-secret` solo es necesario para despliegues multi-tenant (ver §2.6).

***

## Fase 3 -- Desplegar la aplicación (\~5 min)

### 3.1 Configurar los nombres de host públicos

cert-manager necesita los nombres de host de ingestión y del panel antes de poder solicitar sus certificados Let's Encrypt. Copia la plantilla y configura ambos:

```bash theme={null}
cp base/certificates/domain.env.example base/certificates/domain.env
# Edita base/certificates/domain.env y configura:
#   INGEST_DOMAIN=ingest.tu-empresa.example      (resuelve al LB público de Traefik)
#   DASHBOARD_DOMAIN=agenteye.tu-empresa.example (resuelve al LB de Traefik del panel)
```

`domain.env` está en el gitignore; permanece local a cada despliegue. La compilación de kustomize falla con un error claro si falta cualquiera de las claves.

> **El DNS debe resolver primero.** No es necesario apuntar el DNS a los LBs todavía (no existen hasta que se complete la Fase 1.2), pero la emisión de ACME en el paso 3.2 reintentará hasta que cada nombre de host resuelva a su LoadBalancer. Puedes configurar el DNS ahora (usando los nombres de host de LB capturados en la Fase 1.4) o continuar y añadir los registros en la Fase 4.

***

### 3.2 Aplicar los manifiestos

Aplica la base directamente para una instalación nueva, o un overlay si has creado uno para este entorno (los overlays solo fijan etiquetas de imagen, variables de entorno y límites de recursos; heredan los certificados y el enrutamiento de la base):

```bash theme={null}
kubectl apply -k base/
# o
kubectl apply -k overlays/<tu-entorno>/
```

El overlay incluye la base automáticamente; aplica uno, no ambos.

***

### 3.3 Esperar a los pods

```bash theme={null}
kubectl wait --for=condition=Ready pod -l 'app in (server,dashboard,postgres,clickhouse)' \
  -n agenteye --timeout=180s
```

La espera está limitada a los pods del plano de datos principales. Los pods opcionales `agent` (asistente de IA) y `redis` arrancan junto a ellos; el asistente permanece inactivo hasta que le proporciones su endpoint de LLM (ver [enterprise-docs/assistant.md](/es/agenteye/assistant)), y Redis es una caché de mejor esfuerzo, por lo que ninguno de los dos necesita estar listo para que la plataforma sirva tráfico.

**Verificación:**

```bash theme={null}
kubectl get pods -n agenteye
```

Resultado esperado (los pods opcionales `agent` y `redis` también aparecen y alcanzan el estado `Running`):

```
NAME                         READY   STATUS    RESTARTS   AGE
agent-xxxxxxxxxx-xxxxx       1/1     Running   0          ...
clickhouse-0                 1/1     Running   0          ...
dashboard-xxxxxxxxxx-xxxxx   1/1     Running   0          ...
dashboard-xxxxxxxxxx-xxxxx   1/1     Running   0          ...
postgres-0                   1/1     Running   0          ...
redis-0                      1/1     Running   0          ...
server-xxxxxxxxxx-xxxxx      1/1     Running   0          ...
server-xxxxxxxxxx-xxxxx      1/1     Running   0          ...
```

**Si falla:**

| Estado del pod     | Causa probable                                                | Comando de diagnóstico                                      |
| ------------------ | ------------------------------------------------------------- | ----------------------------------------------------------- |
| `ImagePullBackOff` | Secreto de extracción de imagen o PAT incorrecto              | `kubectl describe pod <nombre> -n agenteye`                 |
| `CrashLoopBackOff` | Variables de entorno incorrectas (por ejemplo, DATABASE\_URL) | `kubectl logs <nombre> -n agenteye`                         |
| `Pending`          | CPU/memoria insuficiente o sin nodos                          | `kubectl describe pod <nombre> -n agenteye` (revisa Events) |

***

### 3.4 Verificar el almacenamiento

```bash theme={null}
kubectl get pvc -n agenteye
```

Resultado esperado, ambos con estado `Bound`:

| PVC                            | Capacidad | Respaldo                                                    |
| ------------------------------ | --------- | ----------------------------------------------------------- |
| `postgres-data-postgres-0`     | `50Gi`    | Almacén relacional/de metadatos de PostgreSQL               |
| `clickhouse-data-clickhouse-0` | `100Gi`   | Almacén de análisis de eventos + evaluaciones de ClickHouse |

También aparece un PVC `redis-data-redis-0` (1 Gi) para la caché opcional.

**Si falla:** `Pending` significa que ninguna StorageClass puede aprovisionar el volumen. Comprueba `kubectl get storageclass` y asegúrate de que existe una por defecto. Para producción, sobrescribe el volumen de ClickHouse en tu overlay con una StorageClass de SSD rápida (por ejemplo, gp3 en AWS, pd-ssd en GCP); el rendimiento de compactación se degrada en discos lentos.

***

### 3.5 Verificar los certificados

```bash theme={null}
kubectl get certificates -n agenteye
```

Resultado esperado: 3 certificados, todos con `Ready: True`:

| Nombre          | Emisor             | Propósito                                                                              |
| --------------- | ------------------ | -------------------------------------------------------------------------------------- |
| `mtls-ca`       | `selfsigned`       | CA privada para emitir certificados de cliente mTLS (validez de 10 años)               |
| `ingest-tls`    | `letsencrypt-prod` | Certificado TLS público para el endpoint de ingestión (90 días, renovación automática) |
| `dashboard-tls` | `letsencrypt-prod` | Certificado TLS público para el panel (90 días, renovación automática)                 |

**Si `ingest-tls` o `dashboard-tls` no están listos:**

Ejecuta `kubectl describe certificate <nombre> -n agenteye` y lee los Events. Las causas más comunes son:

* **DNS aún no apunta al LB.** Let's Encrypt resuelve el nombre de host y accede al puerto 80 para validar -- `INGEST_DOMAIN` debe resolver al LB público y `DASHBOARD_DOMAIN` al LB del panel. Hasta que se propague el CNAME/Alias, la orden permanece en `pending`. Una vez que el DNS sea correcto, cert-manager reintentará automáticamente (no es necesario eliminar el Certificate).
* **Nombre de host no sustituido.** Si `dnsNames` sigue mostrando `INGEST_DOMAIN_PLACEHOLDER` / `DASHBOARD_DOMAIN_PLACEHOLDER`, omitiste el paso 3.1 -- crea `base/certificates/domain.env` y vuelve a aplicar.
* **Traefik del panel no puede servir el desafío** (solo `dashboard-tls`). La instancia de Traefik del panel debe instalarse con el archivo de valores incluido (Fase 1.2), que habilita el proveedor de Ingress con alcance limitado que sirve el solver HTTP-01 de cert-manager. Una instancia instalada sin él deja el desafío sin ruta y la orden en `pending` indefinidamente.

**Si `mtls-ca` no está listo:** cert-manager en sí está en mal estado. Revisa los pods de cert-manager del paso 1.1.

***

### 3.6 Verificar los CronJobs

```bash theme={null}
kubectl get cronjobs -n agenteye
```

Resultado esperado:

| Nombre               | Programación   | Propósito                                                          |
| -------------------- | -------------- | ------------------------------------------------------------------ |
| `agenteye-backup`    | `0 3 * * *`    | Copia de seguridad diaria de Postgres + ClickHouse a las 03:00 UTC |
| `cert-renewal-check` | `0 3,15 * * *` | Alertas de caducidad de certificados a las 03:00 y 15:00 UTC       |

***

### 3.7 Verificar que el servidor arrancó correctamente

```bash theme={null}
kubectl logs -n agenteye -l app=server --tail=20
```

**Verificación:** Busca una línea de inicio que indique que el servidor está escuchando en el puerto 8080. No debe haber errores de conexión a la base de datos (el servidor requiere que tanto PostgreSQL como ClickHouse sean accesibles antes de reportar estado Ready).

**Si falla:** La causa más común es un `POSTGRES_PASSWORD` que contiene caracteres no seguros para URLs que rompen la `DATABASE_URL`. Consulta [enterprise-docs/troubleshooting.md](/es/agenteye/troubleshooting).

***

### 3.8 Verificar que el panel se conectó al servidor

```bash theme={null}
kubectl logs -n agenteye -l app=dashboard --tail=20
```

**Verificación:** Busca `Ready` en la salida sin errores `ECONNREFUSED` ni similares.

**Si falla:** Comprueba que el Service `server` existe (`kubectl get svc server -n agenteye`) y que `AGENTEYE_SERVER_URL` está configurado como `http://server:8080` en el despliegue del panel.

***

## Fase 4 -- Acceso de red (\~5 min)

### 4.1 Obtener las direcciones de los LoadBalancers

```bash theme={null}
PUBLIC_IP=$(kubectl get svc -n traefik-public \
  -o jsonpath='{.items[0].status.loadBalancer.ingress[0].ip}')

INTERNAL_IP=$(kubectl get svc -n traefik-dashboard \
  -o jsonpath='{.items[0].status.loadBalancer.ingress[0].ip}')
```

> En AWS EKS, los LoadBalancers devuelven un nombre de host en lugar de una IP. Reemplaza `.ip` por `.hostname` en los comandos anteriores.

**Verificación:**

```bash theme={null}
echo "Público  (ingestión):    $PUBLIC_IP"
echo "Interno  (panel):        $INTERNAL_IP"
```

Ambos deben ser no vacíos.

***

### 4.2 Apuntar el DNS a los LoadBalancers

Crea registros DNS para que los nombres de host de `base/certificates/domain.env` resuelvan a sus LoadBalancers -- `INGEST_DOMAIN` al LB de Traefik **público**, `DASHBOARD_DOMAIN` al LB de Traefik del **panel**:

* **AWS Route 53:** registro `A` con `Alias = Yes`, destino = el nombre de host del LB. No uses A → IP simple; las IPs de ELB rotan.
* **Cualquier otro proveedor:** `CNAME` desde el nombre de host al nombre de host del LB.

Verifica:

```bash theme={null}
dig +short ingest.tu-empresa.example
dig +short agenteye.tu-empresa.example
```

Deben devolver las mismas direcciones que `$PUBLIC_IP` y `$INTERNAL_IP` respectivamente (o, en EKS, resolver a los mismos nombres de host `*.elb.amazonaws.com`).

Una vez que el DNS resuelva, cert-manager completará las órdenes ACME pendientes de la Fase 3.5 en un minuto. Vuelve a ejecutar `kubectl get certificates -n agenteye` hasta que tanto `ingest-tls` como `dashboard-tls` muestren `Ready: True`.

***

### 4.3 Acceder al endpoint de ingestión

El endpoint de ingestión público aplica mutual TLS, por lo que cada solicitud (incluyendo `/health`) debe presentar un certificado de cliente. Emites tu primer certificado de cliente en la Fase 5; si ya tienes uno, verifica la accesibilidad ahora:

```bash theme={null}
curl -s --cert issued/<nombre-clúster>/client.crt \
        --key issued/<nombre-clúster>/client.key \
        https://ingest.tu-empresa.example/health
```

Resultado esperado: `{"status":"ok"}`. No se necesita `-k` -- el certificado del servidor encadena a una CA pública para `INGEST_DOMAIN`, por lo que se valida contra el almacén de confianza del sistema. Accede al endpoint de ingestión por su nombre de host `INGEST_DOMAIN` (que coincide con el certificado emitido), no por la IP/nombre de host bruto del LoadBalancer.

El endpoint del panel se sirve en `DASHBOARD_DOMAIN` con un certificado de confianza pública y no está detrás de mTLS, por lo que no se necesita `-k` ni certificado de cliente:

```bash theme={null}
curl -s https://agenteye.tu-empresa.example/ -o /dev/null -w '%{http_code}\n'
```

Accede al panel por su nombre de host, no por la dirección bruta del LB -- el certificado está vinculado a `DASHBOARD_DOMAIN`, por lo que la dirección bruta mostrará un error de nombre en el certificado.

**Si falla:** Si `curl` se cuelga, comprueba que el LB es accesible desde tu máquina (VPN, grupos de seguridad, reglas de firewall). Un error de handshake `certificate required` en el nombre de host de ingestión significa que no se presentó ningún certificado de cliente; completa primero la Fase 5. Un error de validación TLS en el nombre de host de ingestión significa que el certificado del servidor aún no ha terminado de emitirse; vuelve a la Fase 3.5 y resuelve el problema allí.

***

## Fase 5 -- Emitir certificados de cliente mTLS (\~10 min por clúster)

Los colectores se autentican con **dos factores**: un certificado de cliente (capa de transporte, demuestra que la solicitud proviene de un clúster autorizado) y una clave de API (capa de aplicación, demuestra que la solicitud es de un colector con permiso `events:add`). Una clave filtrada es inútil sin el certificado; un certificado robado es inútil sin una clave válida.

### 5.1 Emitir un certificado

Cada clúster que ejecuta colectores necesita su propio certificado de cliente. Desde el directorio de manifiestos:

```bash theme={null}
cd base/certificates/client-certs
./issue-client-cert.sh <nombre-clúster>
```

Reemplaza `<nombre-clúster>` con un identificador significativo (por ejemplo, `us-east-1-prod`, `staging`).

**Verificación:** El script imprime `==> Done!` y lista los archivos de salida.

```bash theme={null}
kubectl get certificate mtls-client-<nombre-clúster> -n agenteye
```

Resultado esperado: `Ready: True`.

Archivos de salida en `issued/<nombre-clúster>/`:

| Archivo                      | Propósito                                                          |
| ---------------------------- | ------------------------------------------------------------------ |
| `client.crt`                 | Certificado de cliente (validez de 90 días)                        |
| `client.key`                 | Clave privada del cliente                                          |
| `ca.crt`                     | Certificado de CA para verificación del servidor                   |
| `collector-mtls-secret.yaml` | Secret de Kubernetes listo para aplicar en el clúster del colector |

***

### 5.1b Entrega alternativa: AWS Secrets Manager

Si el consumidor del certificado es un Pod de Kubernetes que necesita `client.crt` y `client.key` en disco -- el caso típico cuando ejecutas el agenteye-collector como sidecar en tu pod de aplicación -- envía el bundle del certificado a AWS Secrets Manager. El pod de la aplicación lo monta entonces mediante el [Secrets Store CSI Driver](https://secrets-store-csi-driver.sigs.k8s.io/) con IRSA, y la rotación del certificado es completamente automática.

```bash theme={null}
cd base/certificates/client-certs
export AWS_REGION=us-east-1     # región donde se ejecuta tu carga de trabajo
./issue-client-cert.sh <nombre-clúster> --save-to aws-secrets-manager
```

En cada reejecución (renovación), el script llama a `PutSecretValue` en el mismo secreto, de modo que el ARN y el nombre permanecen estables. El CSI Driver recoge la nueva versión en su próximo ciclo de rotación y reescribe los archivos dentro del pod.

**Requisitos previos:**

* CLI de `aws` v2 autenticado en tu cuenta de AWS.
* `jq` instalado.
* Variable de entorno `AWS_REGION` configurada.
* Permisos IAM en tu identidad (limita `Resource` a `arn:aws:secretsmanager:<región>:<cuenta>:secret:agenteye/mtls-client/*`):
  * `secretsmanager:CreateSecret`
  * `secretsmanager:DescribeSecret`
  * `secretsmanager:PutSecretValue`
  * `secretsmanager:TagResource`

**Lo que hace el script en este modo:**

| Paso | Acción                                                                                                                                                                                                                                                                                           |
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 1    | Emite o reextrae el certificado mediante cert-manager (igual que el modo por defecto).                                                                                                                                                                                                           |
| 2    | Llama a `DescribeSecret` en `agenteye/mtls-client/<nombre-clúster>` para decidir si crear o actualizar.                                                                                                                                                                                          |
| 3    | En la primera ejecución: `CreateSecret` con un payload JSON de tres claves (`client.crt`, `client.key`, `ca.crt`), etiquetado con `AgentEyeCluster=<nombre-clúster>`. En ejecuciones posteriores: `PutSecretValue` para publicar una nueva versión; etiqueta actualizada mediante `TagResource`. |
| 4    | Elimina `issued/<nombre-clúster>/` solo después de una carga exitosa. En caso de error, el directorio se conserva para poder reintentar.                                                                                                                                                         |

**Si el secreto está programado para eliminación**, el script falla con un mensaje claro indicándote que ejecutes `aws secretsmanager restore-secret --secret-id agenteye/mtls-client/<nombre-clúster>` antes de reintentar.

Para la configuración completa del pod (SecretProviderClass, configuración de IRSA, comportamiento de rotación y solución de problemas), consulta [enterprise-docs/single-pod-deployment.md](/es/agenteye/single-pod-deployment).

***

### 5.2 Verificar que el certificado funciona

Prueba el certificado emitido contra el ingreso mTLS:

```bash theme={null}
curl -sk --cert issued/<nombre-clúster>/client.crt \
     --key issued/<nombre-clúster>/client.key \
     https://${PUBLIC_IP}/health
```

Resultado esperado: `{"status":"ok"}`

**Si falla:**

| Error                  | Causa                                       | Solución                                                                                                             |
| ---------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `certificate required` | El certificado no se está presentando       | Verifica las rutas de archivo en el comando `curl`                                                                   |
| `bad certificate`      | Discrepancia de CA                          | Verifica que `mtls-ca-issuer` emitió el certificado: `kubectl describe certificate mtls-client-<nombre> -n agenteye` |
| `connection refused`   | Nombre de host incorrecto o LB no accesible | Comprueba `/etc/hosts` o DNS                                                                                         |

***

### 5.3 Entregar al clúster del colector

Envía `collector-mtls-secret.yaml` al equipo que opera el clúster del colector. Ellos lo aplican:

```bash theme={null}
kubectl apply -f collector-mtls-secret.yaml -n <namespace-colector>
```

Luego configura el colector para montar el secreto y usar las rutas del certificado:

```json theme={null}
{
  "tls_cert": "/etc/agenteye/tls/client.crt",
  "tls_key": "/etc/agenteye/tls/client.key"
}
```

Consulta [enterprise-docs/collector-installation.md](/es/agenteye/collector-installation) para la configuración completa del colector, incluyendo los montajes de volumen de Kubernetes.

**Verificación (en el clúster del colector):**

```bash theme={null}
kubectl get secret agenteye-collector-mtls -n <namespace-colector>
```

Resultado esperado: el secreto existe con 3 claves de datos (`client.crt`, `client.key`, `ca.crt`).

***

### 5.4 Ciclo de vida del certificado

| Propiedad                          | Valor                                                 |
| ---------------------------------- | ----------------------------------------------------- |
| Validez del certificado de cliente | 90 días                                               |
| Renovación automática              | cert-manager renueva 15 días antes de la caducidad    |
| Validez de la CA                   | 10 años                                               |
| Alertas de caducidad               | CronJob alerta 30 días antes de la caducidad (Fase 6) |

cert-manager renueva automáticamente el certificado en el **clúster de AgentEye**, pero el certificado renovado debe volver a entregarse al clúster del colector. Vuelve a ejecutar `issue-client-cert.sh` y vuelve a aplicar `collector-mtls-secret.yaml` antes de que caduque el certificado antiguo.

Si estás usando `--save-to aws-secrets-manager` (ver §5.1b), vuelve a ejecutar el mismo comando. El script llama a `PutSecretValue` en el mismo secreto; los pods que montan el secreto mediante el Secrets Store CSI Driver recogen la nueva versión en su próximo ciclo de rotación (por defecto: cada hora), sin necesidad de reiniciar el pod.

***

### 5.5 Revocar un certificado

Para bloquear inmediatamente el acceso del colector de un clúster:

```bash theme={null}
kubectl delete certificate mtls-client-<nombre-clúster> -n agenteye
```

**Verificación:** El comando `curl` del paso 5.2 ahora falla con un error de handshake TLS.

***

## Fase 6 -- Monitorización de renovación de certificados (\~2 min)

Un CronJob integrado se ejecuta cada 12 horas (03:00 y 15:00 UTC) y comprueba todos los certificados de cliente etiquetados con `agenteye.io/cert-type=mtls-client`. Alerta cuando algún certificado está a menos de 30 días de caducar.

### 6.1 Habilitar notificaciones de Slack (opcional)

```bash theme={null}
kubectl create secret generic cert-renewal-notify-config \
  --namespace agenteye \
  --from-literal=SLACK_WEBHOOK_URL="https://hooks.slack.com/services/TU/WEBHOOK/URL"
```

Sin este secreto, el CronJob sigue ejecutándose y registra el estado de los certificados en stdout.

**Verificación:**

```bash theme={null}
kubectl get secret cert-renewal-notify-config -n agenteye
```

Resultado esperado: el secreto existe.

***

### 6.2 Probar el CronJob

```bash theme={null}
kubectl create job --from=cronjob/cert-renewal-check test-cert-check -n agenteye

kubectl wait --for=condition=Complete job/test-cert-check -n agenteye --timeout=60s

kubectl logs -n agenteye -l job-name=test-cert-check
```

Resultado esperado: una lista de certificados con su estado de caducidad. Si el webhook de Slack está configurado, comprueba el canal de Slack para ver el mensaje de alerta.

**Si falla:** Comprueba el RBAC -- la ServiceAccount del CronJob necesita permisos `get, list` en los recursos Certificate de cert-manager. Verifica con: `kubectl describe role cert-renewal-check -n agenteye`.

Limpia el job de prueba:

```bash theme={null}
kubectl delete job test-cert-check -n agenteye
```

***

## Fase 7 -- Verificar el funcionamiento end-to-end

Esta fase confirma que toda la cadena funciona: comprobación de salud, creación de clave, ingestión de eventos y visualización en el panel.

> **Nota:** Los ejemplos siguientes acceden al endpoint de ingestión por su dirección bruta del LoadBalancer (`${PUBLIC_IP}`) por comodidad, por lo que se pasa `-k`; el certificado del servidor está vinculado a `INGEST_DOMAIN`, no a la IP del LB, por lo que se omite la verificación del nombre de host. El endpoint de ingestión aplica mutual TLS en **todas** las rutas, por lo que cada llamada también debe presentar un certificado de cliente (`--cert`/`--key`). Para validar también el certificado público, apunta a `https://ingest.tu-empresa.example/...` en lugar de `${PUBLIC_IP}` y elimina el `-k`.

### 7.1 Comprobación de salud

```bash theme={null}
curl -sk --cert issued/<nombre-clúster>/client.crt \
        --key issued/<nombre-clúster>/client.key \
        https://${PUBLIC_IP}/health
```

Resultado esperado: `{"status":"ok"}` con HTTP 200.

***

### 7.2 Crear claves de colector con permisos restringidos

La clave de administrador es para el arranque inicial y la gestión. Crea claves dedicadas con permiso `events:add` para los colectores:

```bash theme={null}
COLLECTOR_KEY=$(openssl rand -hex 32)

curl -sk -X POST https://${PUBLIC_IP}/keys \
  --cert issued/<nombre-clúster>/client.crt \
  --key issued/<nombre-clúster>/client.key \
  -H "Authorization: Bearer ${ADMIN_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "prod-collector",
    "key": "'"${COLLECTOR_KEY}"'",
    "permissions": ["events:add"]
  }'
```

**Verificación:** La respuesta incluye `"id"`, `"name": "prod-collector"`, `"permissions": ["events:add"]`, `"created_at"`.

**Verificación:** Comprueba que la clave aparece en la lista de claves:

```bash theme={null}
curl -sk https://${PUBLIC_IP}/keys \
  --cert issued/<nombre-clúster>/client.crt \
  --key issued/<nombre-clúster>/client.key \
  -H "Authorization: Bearer ${ADMIN_KEY}"
```

Resultado esperado: `prod-collector` aparece en la respuesta.

Consulta [enterprise-docs/api-keys.md](/es/agenteye/api-keys) para la referencia completa de gestión de claves.

***

### 7.3 Ingestar un evento de prueba

```bash theme={null}
echo '{"session_id":"test","agent_id":"smoke-test","type":"test","timestamp":"2026-04-20T00:00:00Z"}' \
  | curl -sk --cert issued/<nombre-clúster>/client.crt \
         --key issued/<nombre-clúster>/client.key \
         -H "Authorization: Bearer ${COLLECTOR_KEY}" \
         -H "Content-Type: application/x-ndjson" \
         --data-binary @- \
         https://${PUBLIC_IP}/events
```

Resultado esperado: `{"accepted":1,"skipped":0}` con HTTP 200.

**Si falla:**

| Estado HTTP            | Causa                                                                            |
| ---------------------- | -------------------------------------------------------------------------------- |
| 401                    | Clave de API inválida o ausente                                                  |
| 403                    | La clave no tiene el permiso `events:add`                                        |
| Error de handshake TLS | Problema con el certificado de cliente -- ver solución de problemas de la Fase 5 |

***

### 7.4 Verificar que el evento aparece en el panel

Abre `https://agenteye.tu-empresa.example` (tu `DASHBOARD_DOMAIN`) en un navegador. El certificado tiene confianza pública, por lo que no hay advertencias.

> Si el LoadBalancer del panel está restringido por lista de IPs y no puedes conectarte, verifica que tu IP está permitida:
>
> ```bash theme={null}
> kubectl get svc traefik-dashboard -n traefik-dashboard -o jsonpath='{.spec.loadBalancerSourceRanges}'
> ```
>
> Ten en cuenta que Let's Encrypt renueva el certificado del panel mediante HTTP-01 en el puerto 80, y los rangos de origen se aplican a todo el LoadBalancer -- antes de restringirlo a rangos corporativos, coordina un solver DNS-01 con soporte o las renovaciones fallarán silenciosamente.

**Verificación:** El evento de prueba debe aparecer en la lista de eventos con la sesión `test` y el agente `smoke-test`.

**Si falla:** Comprueba los logs del panel (`kubectl logs -n agenteye -l app=dashboard --tail=50`). Verifica que `AGENTEYE_SERVER_URL` y `AGENTEYE_API_KEY` están configurados correctamente.

***

### 7.5 Probar el CronJob de copia de seguridad

```bash theme={null}
kubectl create job --from=cronjob/agenteye-backup test-backup -n agenteye

kubectl wait --for=condition=Complete job/test-backup -n agenteye --timeout=300s

kubectl logs -n agenteye -l job-name=test-backup
```

Resultado esperado: `Backup created: agenteye-YYYYMMDD-HHMMSS.tar.gz (NNN)` en los logs; el archivo agrupa el volcado de Postgres y las tablas de ClickHouse.

> El paso de carga a S3 viene integrado en el CronJob y se ejecuta siempre que `BACKUP_BUCKET` esté configurado (la base incluye un valor de bucket por defecto). Solo se omite cuando `BACKUP_BUCKET` está vacío o literalmente es `PLACEHOLDER`. Apúntalo a tu propio bucket y concede al ServiceAccount `agenteye-backup` acceso de escritura antes de confiar en él (ver la sección de Copias de seguridad más abajo).

Limpieza:

```bash theme={null}
kubectl delete job test-backup -n agenteye
```

***

### 7.6 Aprovisionar organizaciones (multi-tenant)

Omite esto para un despliegue de un solo tenant; todos los datos viven en la organización `default` integrada y nada aquí es necesario.

Si ejecutas múltiples tenants aislados, las organizaciones y sus miembros se crean con la CLI **`agenteye-orgctl`**. Esta herramienta se incluye **dentro de la imagen del servidor** (junto a `agenteye-server`) y se ejecuta **dentro del Deployment `server` existente con `kubectl exec`; no hay pod, Job ni Deployment separado, y no hay API HTTP ni botón en el panel para el ciclo de vida del tenant.** Ejecutarla en el pod del servidor significa que reutiliza la `DATABASE_URL`, `CLICKHOUSE_URL` y el `ORG_CH_SECRET` del §2.6 del pod.

> **Requisito previo:** completa primero el §2.6. `org create` se niega a ejecutarse mientras el servidor siga usando el `ORG_CH_SECRET` de desarrollo integrado, y el usuario de ClickHouse por organización que aprovisiona depende de que ese secreto sea fuerte y estable.

**Crear una organización y añadir su primer administrador:**

```bash theme={null}
kubectl -n agenteye exec deploy/server -- \
  agenteye-orgctl org create --slug acme --name "Acme Corp"

kubectl -n agenteye exec deploy/server -- \
  agenteye-orgctl member add --org acme --email alice@acme.example --set admin
```

El nuevo miembro recibe un OTP en su primer inicio de sesión en el panel y luego trabaja completamente en la interfaz de usuario bajo el prefijo de URL de la organización (por ejemplo, `/acme/...`).

**Otros comandos** (se ejecutan de la misma manera con `kubectl -n agenteye exec deploy/server -- agenteye-orgctl …`):

| Comando                                                                             | Qué hace                                                                                                           |
| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `org list`                                                                          | Lista las organizaciones y su estado.                                                                              |
| `org rename --slug <slug> --name <nuevo nombre>`                                    | Renombra una organización (el slug no cambia).                                                                     |
| `org delete --slug <slug>`                                                          | Eliminación suave + eliminación del usuario de ClickHouse de la organización; **los datos se conservan**.          |
| `org purge --slug <slug>`                                                           | Borrado irreversible de datos; la organización debe estar eliminada primero; nunca para la organización `default`. |
| `member list --org <slug>`                                                          | Lista los miembros y sus permisos.                                                                                 |
| `member update --org <slug> --email <email> [--set ...] [--add ...] [--remove ...]` | Cambia los permisos de un miembro.                                                                                 |
| `member remove --org <slug> --email <email>`                                        | Elimina un miembro de la organización.                                                                             |

Los conjuntos de permisos integrados son `admin`, `standard` y `read-only`. **Las claves de API por organización se siguen creando en el panel/API por los miembros de la organización (el §7.2 muestra la API de claves); solo el ciclo de vida de la organización y los miembros es exclusivo para operadores.** Referencia completa y un ejemplo detallado: [enterprise-docs/tenant-management.md](/es/agenteye/tenant-management).

***

## Lista de verificación post-despliegue

Usa esta lista para confirmar que todo funciona. Cada elemento debe estar comprobado antes de entregar a los colectores.

* [ ] Todos los pods en estado `Running` en el namespace `agenteye`
* [ ] PVC de PostgreSQL vinculado (50 Gi) y PVC de ClickHouse vinculado (100 Gi)
* [ ] Los 3 certificados con `Ready: True`
* [ ] Ambas IPs de LoadBalancer asignadas
* [ ] DNS o `/etc/hosts` configurado y resolviendo
* [ ] `/health` devuelve HTTP 200
* [ ] Prueba de certificado mTLS superada (`curl` con certificado de cliente a `/health`)
* [ ] Clave de colector con permisos restringidos creada y probada
* [ ] Evento de prueba ingestado (`accepted: 1`)
* [ ] Evento visible en el panel
* [ ] Certificados de cliente emitidos para cada clúster de colector
* [ ] CronJob de copia de seguridad probado manualmente
* [ ] CronJob de renovación de certificados probado manualmente
* [ ] Webhook de Slack para alertas de certificados configurado (opcional)
* [ ] Bucket de copia de seguridad configurado en el overlay (ver más abajo)
* [ ] Clave de administrador y contraseña de Postgres guardadas en el gestor de secretos

***

## Copias de seguridad

Un único CronJob `agenteye-backup` se ejecuta diariamente a las 03:00 UTC. Vuelca **ambos** almacenes: PostgreSQL (estado relacional) y ClickHouse (las tablas de análisis `events` + `evaluations`), en un archivo comprimido en el pod, y luego lo carga en el almacenamiento de objetos que configures en tu overlay.

Cada ejecución produce un objeto, `agenteye-<timestamp>.tar.gz`, que se descomprime en:

```
postgres.sql        # pg_dump de la base de datos relacional
events.sql          # DDL de la tabla events de ClickHouse
events.native       # Datos de la tabla events de ClickHouse (formato Native)
evaluations.sql     # DDL de la tabla evaluations de ClickHouse
evaluations.native  # Datos de la tabla evaluations de ClickHouse
```

ClickHouse se lee a través de su API HTTP (el mismo endpoint que usa el servidor), por lo que el job no necesita
