> ## 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.

# Guida al Deployment su Kubernetes

> Documentazione della guida al deployment di AgentEye su Kubernetes.

Questa guida distribuisce l'intero stack AgentEye su un cluster Kubernetes dedicato:

* **ClickHouse 24.8** -- archivio canonico per l'analisi di eventi e valutazioni (StatefulSet con volume persistente da 100Gi). Obbligatorio: il server rifiuta di avviarsi senza di esso.
* **PostgreSQL 16** -- archivio relazionale/metadati per organizzazioni, chiavi API, utenti, dashboard, query salvate e autenticazione (StatefulSet con volume persistente da 50Gi)
* **Redis 7.2** -- cache condivisa opzionale e backend di rate-limit; il server e la dashboard si degradano elegantemente se non disponibile
* **AgentEye Server** -- API Rust per l'acquisizione di eventi, l'analisi e la gestione delle chiavi (2 repliche)
* **AgentEye Dashboard** -- interfaccia utente Next.js (2 repliche)
* **Assistente AI (servizio agent)** -- assistente opzionale in sola lettura all'interno della dashboard sulla porta 9100; inerte finché non viene configurato un endpoint LLM
* **Traefik (pubblico)** -- controller di ingresso per il traffico del collector, protetto con mTLS
* **Traefik (dashboard)** -- controller di ingresso per la dashboard, solo VPN/allowlist IP
* **cert-manager** -- certificati TLS e CA mTLS
* **Backup CronJob** -- dump combinato giornaliero di PostgreSQL + ClickHouse alle 03:00 UTC
* **Cert Renewal Monitor** -- avvisi quando i certificati client stanno per scadere

**Tempo stimato:** 60-90 minuti per un primo deployment.

Per il modello di deployment gestito in cui Exosphere gestisce tutto questo per conto vostro, consultate [enterprise-docs/managed-deployment.md](/it/agenteye/managed-deployment).

***

## Prerequisiti

Eseguite ogni comando di verifica prima di iniziare. Ogni controllo deve essere superato.

| Requisito                      | Minimo                                      | Comando di Verifica                                                              | Risultato atteso                                                                |
| ------------------------------ | ------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Cluster Kubernetes             | 1.27+                                       | `kubectl version`                                                                | Server Version >= v1.27                                                         |
| Kustomize (incluso in kubectl) | Kustomize v1.14+ (incluso in kubectl 1.27+) | `kubectl kustomize --help`                                                       | Stampa il testo di utilizzo                                                     |
| Helm                           | v3                                          | `helm version`                                                                   | `Version:"v3.x.x"`                                                              |
| RBAC cluster-admin             | --                                          | `kubectl auth can-i create namespaces`                                           | `yes`                                                                           |
| StorageClass predefinita       | --                                          | `kubectl get storageclass`                                                       | Almeno una riga contrassegnata come `(default)`                                 |
| Supporto LoadBalancer          | --                                          | Dipende dal cloud (EKS, GKE, AKS supportano questo per impostazione predefinita) | --                                                                              |
| GitHub PAT                     | --                                          | `echo $AGENTEYE_TOKEN`                                                           | Non vuoto (vedere [enterprise-docs/github-token.md](/it/agenteye/github-token)) |
| openssl                        | --                                          | `openssl version`                                                                | OpenSSL 1.x o 3.x                                                               |
| Bucket di archiviazione cloud  | --                                          | Per i backup di PostgreSQL + ClickHouse (S3, GCS o Azure Blob)                   | --                                                                              |

**Dimensionamento del cluster:** minimo 3 nodi, 4 vCPU / 8 GB RAM ciascuno. Consultate [enterprise-docs/managed-deployment.md](/it/agenteye/managed-deployment) per i requisiti completi.

### Eseguire tutti i controlli contemporaneamente

```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 "---"
```

### Forma del deployment

L'**endpoint di acquisizione** è servito su un hostname che controllate (ad es. `ingest.your-company.example`). cert-manager richiede un certificato TLS pubblicamente attendibile da Let's Encrypt tramite HTTP-01, pertanto i collector verificano il certificato del server rispetto all'archivio di trust del sistema, senza alcun pinning CA per cliente.

L'**endpoint della dashboard** funziona allo stesso modo: è servito su un secondo hostname che controllate (ad es. `agenteye.your-company.example`) che punta al LoadBalancer Traefik della dashboard, e cert-manager emette il suo certificato Let's Encrypt attraverso quel LoadBalancer. I browser ottengono un certificato attendibile senza avvisi.

> **L'emissione del certificato e il rinnovo si convalidano su HTTP-01**, quindi entrambi i LoadBalancer devono essere raggiungibili da Internet pubblico sulla porta 80. Se è necessario limitare l'IP del LoadBalancer della dashboard, coordinare un risolutore DNS-01 con il supporto in anticipo — altrimenti i rinnovi non riescono silenziosamente e il certificato scade.

***

## Ottenere i Manifest

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

**Testate:**

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

Risultato atteso: il file esiste. Se non esiste, il clone non è riuscito -- controllate il vostro `AGENTEYE_TOKEN`.

**Struttura directory:**

```
deploy/
  base/           Base Kustomize condivisa (tutte le risorse K8s)
  overlays/       Override specifici del cluster (tag immagini, hostname, risorse)
  third-party/    Valori Helm per Traefik, cert-manager e (opzionale) monitoraggio della salute Robusta
```

La **base** contiene ogni risorsa necessaria per un deployment completo, inclusi i certificati Let's Encrypt per i due hostname pubblici che configurate nella Fase 3.1. Un **overlay** applica patch alla base per un ambiente specifico (ad es. tag immagini personalizzati, limiti di risorse, collegamento env). La directory **third-party** contiene file di valori Helm per l'infrastruttura esterna.

> **Monitoraggio della salute (opzionale):** il probe di readiness del server già riflette la salute di Postgres + ClickHouse, e `third-party/robusta/` aggiunge avvisi facoltativi nativi di Kubernetes per i guasti dei pod a Slack. Consultate [enterprise-docs/health-monitoring.md](/it/agenteye/health-monitoring).

***

## Fase 1 -- Infrastruttura di terze parti (\~30 min)

### 1.1 Installare cert-manager

cert-manager gestisce i certificati TLS per HTTPS e la CA privata utilizzata per i certificati client 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
```

**Testate:**

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

Risultato atteso: 3 pod tutti `Running` -- `cert-manager`, `cert-manager-cainjector`, `cert-manager-webhook`.

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

Risultato atteso: almeno `certificates.cert-manager.io`, `clusterissuers.cert-manager.io`, `issuers.cert-manager.io`.

**Se non riesce:** i pod in `CrashLoopBackOff` di solito significano che i CRD non sono stati installati. Eseguite di nuovo con `--set crds.install=true`. Se i pod webhook non superano il controllo di readiness, aspettate 30 secondi e controllate di nuovo -- possono impiegare un momento per avviarsi.

***

### 1.2 Installare Traefik -- Controller di Acquisizione Pubblico

Questa istanza Traefik gestisce il traffico del collector su un LoadBalancer **esterno**. Termina TLS e applica mTLS (verifica del certificato client) sull'endpoint di acquisizione.

```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
```

**Testate:**

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

Risultato atteso: 1 pod `Running`.

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

Risultato atteso: la IngressClass esiste (non è la classe predefinita).

**Se non riesce:** controllate `kubectl describe pod -n traefik-public <pod-name>` per errori di pull dell'immagine o vincoli di risorse.

***

### 1.3 Installare Traefik -- Controller della Dashboard

Questa istanza Traefik serve la dashboard su un LoadBalancer dedicato, limitato da allowlist IP.

> **Due meccanismi di allowlist vengono forniti per questa istanza.** Questa guida utilizza `values-dashboard.yaml`, che limita l'accesso con il campo portatile `service.loadBalancerSourceRanges`. Un `values-internal.yaml` parallelo è fornito anche per gli ambienti AWS che preferiscono l'annotazione `service.beta.kubernetes.io/aws-load-balancer-source-ranges`. Scegliete uno e usatelo coerentemente; i passaggi seguenti presuppongono `values-dashboard.yaml`.

**Prima dell'installazione**, modificate `third-party/traefik/values-dashboard.yaml` per impostare gli IP sorgente consentiti. Il campo `loadBalancerSourceRanges` controlla quali IP possono raggiungere la dashboard. Per impostazione predefinita è impostato su `0.0.0.0/0` (tutti gli IP); limitatelo a VPN, ufficio o IP di uscita noti.

#### Allowlist di un singolo IP

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

#### Allowlist di più IP

Aggiungete una voce per IP o blocco CIDR. Un suffisso `/32` corrisponde a un singolo indirizzo IPv4; un blocco CIDR (ad es. `/24`) corrisponde a un intervallo. Potete mescolare liberamente IP individuali e intervalli:

```yaml theme={null}
service:
  loadBalancerSourceRanges:
    - "203.0.113.10/32"       # gateway ufficio
    - "203.0.113.11/32"       # gateway ufficio di backup
    - "198.51.100.0/24"       # pool VPN
    - "192.0.2.50/32"         # IP home dell'ingegnere on-call
```

Suggerimenti per mantenere la lista:

* Mantenete una voce per riga e aggiungete un commento breve `#` identificando il proprietario o lo scopo di ogni IP; questo è ciò che gli operatori futuri utilizzano per decidere se una voce è ancora necessaria.
* Usate sempre la notazione CIDR. Un IP bare come `203.0.113.10` è rifiutato dal provider cloud; usate `203.0.113.10/32`.
* Per gli intervalli IPv6, usate il CIDR equivalente `/128` (singolo indirizzo) o più grande, ad es. `2001:db8::1/128`. Non tutti i provider cloud supportano gli intervalli di origine IPv6; controllate la documentazione del vostro provider su LoadBalancer.
* La lista è un **OR**: il traffico è consentito se la sorgente corrisponde a qualsiasi voce.

Dopo aver modificato il file, procedete a `helm install` di seguito. Se il controller è già installato, eseguite `helm upgrade` con gli stessi flag, o applicate una patch al Service in fase di esecuzione (sezione successiva).

#### Aggiornare l'allowlist in fase di esecuzione

Potete modificare gli IP consentiti senza un upgrade Helm applicando una patch al Service direttamente. **La patch sostituisce l'intera lista**; includete sempre ogni IP che volete mantenere, non solo quello nuovo.

Per sostituire la lista con una nuova serie di IP:

```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"]}}'
```

Per **aggiungere in modo sicuro** un IP senza perdere le voci esistenti, leggete prima la lista attuale, quindi applicate una patch con il set combinato:

```bash theme={null}
# 1. Mostra l'allowlist corrente
kubectl get svc traefik-dashboard -n traefik-dashboard \
  -o jsonpath='{.spec.loadBalancerSourceRanges}'

# 2. Applica una patch con la lista completa incluso il nuovo IP
kubectl patch svc traefik-dashboard -n traefik-dashboard \
  -p '{"spec":{"loadBalancerSourceRanges":["<EXISTING_IP_1>/32","<EXISTING_IP_2>/32","<NEW_IP>/32"]}}'
```

> Le patch in fase di esecuzione non vengono mantenute in `values-dashboard.yaml`. Per mantenere il cambiamento nei futuri upgrade Helm, aggiornate anche il file di valori e committatelo.

Quindi installate:

```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
```

**Testate:**

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

Risultato atteso: 1 pod `Running`.

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

Risultato atteso: la IngressClass esiste.

***

### 1.4 Attendere i LoadBalancer

Entrambe le istanze Traefik hanno bisogno di IP esterni prima di procedere.

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

**Testate:** entrambi i servizi mostrano un `EXTERNAL-IP` (non `<pending>`).

Se ancora in sospeso, aspettate l'assegnazione:

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

Premete `Ctrl+C` una volta che l'IP appare. L'assegnazione dell'IP di solito richiede 2-5 minuti.

**Se non riesce:** `<pending>` dopo 10 minuti di solito significa che il provider cloud non può provisioning un LoadBalancer. Controllate: tag della subnet (EKS richiede `kubernetes.io/role/elb`), configurazione VPC, quote di servizio e che l'annotazione LB interna corretta sia impostata per l'istanza interna.

***

## Fase 2 -- Creare Secret (\~10 min)

Tutti i secret vengono creati manualmente prima di distribuire l'applicazione. Ciò garantisce che i valori sensibili non compaiano mai nei file manifest.

### 2.1 Creare lo namespace

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

**Testate:**

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

Risultato atteso: stato `Active`.

***

### 2.2 Secret di pull dell'immagine

Questo secret autentica con `ghcr.io` per fare il pull delle immagini del container AgentEye. Consultate [enterprise-docs/github-token.md](/it/agenteye/github-token) per come generare il vostro 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}"
```

**Testate:**

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

Risultato atteso: `kubernetes.io/dockerconfigjson`.

**Testate (approfondimento)** -- verificate che il token possa effettivamente fare il pull delle immagini:

Usate il tag dell'immagine `server` fissato nel `kustomization.yaml` del vostro overlay (attualmente `v0.0.1-beta.48` sia nell'overlay `acme` incluso che nel deployment base). Sostituite il tag di seguito con quello che state distribuendo in modo che questo controllo non diverga tra le versioni:

```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

# Aspettate qualche secondo per il pull, quindi:
kubectl logs test-pull -n agenteye
kubectl delete pod test-pull -n agenteye
```

Risultato atteso: `ok` stampato nei log.

**Se non riesce:** `ErrImagePull` o `401 Unauthorized` significa che il PAT è non valido o manca l'ambito `read:packages`. Ri-controllate [enterprise-docs/github-token.md](/it/agenteye/github-token).

***

### 2.3 Credenziali 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:** utilizziamo `-hex` (non `-base64`) per generare la password. L'output in base64 può contenere `+`, `/` e `=` che interrompono la stringa di connessione `DATABASE_URL`. Consultate [enterprise-docs/troubleshooting.md](/it/agenteye/troubleshooting) per i dettagli.

> **Memorizzate `POSTGRES_PASSWORD` nel vostro gestore di segreti immediatamente.** Ve ne avrete bisogno se dovete mai ripristinare da un backup o connettervi al database direttamente.

**Testate:**

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

Risultato atteso: il secret esiste.

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

Risultato atteso: `48` (24 byte hex = 48 caratteri).

***

### 2.4 Chiave API Admin

```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 chiave admin è la credenziale di bootstrap. Il server la upsert a ogni avvio con tutti i permessi. Usatela per creare chiavi collector limitate nella Fase 7. Consultate [enterprise-docs/api-keys.md](/it/agenteye/api-keys) per il modello di permessi completo.

> **Memorizzate `ADMIN_KEY` nel vostro gestore di segreti immediatamente.**

**Testate:**

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

Risultato atteso: il secret esiste.

***

### 2.5 Configurazione dell'autenticazione (accesso alla dashboard)

La dashboard utilizza email + OTP per l'accesso dell'utente. Senza questo secret il server si avvia comunque e il percorso della chiave API `ADMIN_KEY` continua a funzionare, ma **nessun utente può accedere tramite l'interfaccia utente**.

Tutte le chiavi sono referenziate come `optional: true` nel manifest base, quindi i secret parziali (o nessun secret affatto) vanno bene; il server ritorna ai valori predefiniti documentati. Raggruppare tutto in un unico secret `agenteye-auth` mantiene la superficie di autenticazione ruotabile in un unico luogo.

```bash theme={null}
kubectl create secret generic agenteye-auth \
  --namespace agenteye \
  --from-literal=ADMIN_EMAIL="admin@yourcompany.com" \
  --from-literal=ALLOWED_EMAILS="*@yourcompany.com" \
  --from-literal=SMTP_HOST="smtp.yourprovider.com" \
  --from-literal=SMTP_PORT="587" \
  --from-literal=SMTP_USERNAME="your-smtp-user" \
  --from-literal=SMTP_PASSWORD="your-smtp-password" \
  --from-literal=SMTP_FROM="noreply@yourcompany.com" \
  --from-literal=SMTP_TLS="starttls" \
  --from-literal=DEFAULT_ORG_NAME="Acme Corp" \
  --from-literal=DEFAULT_ORG_SLUG="acme"
```

| Chiave                                                                  | Scopo                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ADMIN_EMAIL`                                                           | Utente admin di bootstrap. Upserted a ogni avvio con tutti i permessi e protetto da eliminazione/modifiche di permessi tramite la dashboard. Senza di esso, nessun admin è seminato e il primo accesso è impossibile.                                                                                                                                                                                                                                                                  |
| `ALLOWED_EMAILS`                                                        | Allowlist separata da virgola. Supporta indirizzi esatti (`user@example.com`) e wildcard di dominio (`*@example.com`). Senza di esso, **nessun utente può accedere o essere creato**.                                                                                                                                                                                                                                                                                                  |
| `SMTP_HOST`, `SMTP_PORT`, `SMTP_USERNAME`, `SMTP_PASSWORD`, `SMTP_FROM` | Relè SMTP per inviare codici OTP. Se `SMTP_HOST` non è impostato, i codici OTP vengono registrati nello stdout del server anziché inviati tramite email (utile per i test di smoke del primo avvio). Fornite tutte le chiavi SMTP insieme per la consegna di email reale.                                                                                                                                                                                                              |
| `SMTP_TLS`                                                              | Uno di `starttls` (predefinito), `tls` o `none`.                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `DEFAULT_ORG_NAME`, `DEFAULT_ORG_SLUG`                                  | Opzionale. Date all'organizzazione incorporata `default` un nome di visualizzazione amichevole e uno slug URL in modo che risieda ad es. in `/acme` anziché in `/default`. Applicato **solo al primo avvio**; una volta che rinominate l'organizzazione con `agenteye-orgctl org rename` (consultate §7.6) questi vengono ignorati. Lo slug deve essere 1-40 alfanumerici minuscoli con singoli trattini interni. Lasciate entrambi non impostati per mantenere il `default` generico. |

> **Memorizzate le credenziali SMTP nel vostro gestore di segreti.**

**Testate:**

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

Risultato atteso: le chiavi che avete compilato appaiono nell'output.

***

### 2.6 Chiave di isolamento organizzazione multi-tenant (opzionale)

Saltate questo per un deployment a tenant singolo; il server funziona su un default incorporato dev e serve bene l'unica organizzazione `default`. **Prima di creare una seconda organizzazione**, impostate un forte e stabile `ORG_CH_SECRET`: la password ClickHouse di ogni organizzazione è derivata come `HMAC(ORG_CH_SECRET, org_id)`, quindi il default dev pubblicamente noto produrrebbe credenziali per organizzazione pubblicamente derivabili. Il comando `agenteye-orgctl org create` (consultate [§7.6 Provisioning di organizzazioni](#76-provisioning-di-organizzazioni-multi-tenant)) rifiuta di eseguire mentre il server è ancora sul default dev incorporato.

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

# Riavviate il server in modo che raccolga il nuovo valore.
kubectl -n agenteye rollout restart deployment/server
```

Il server legge questo tramite un `secretKeyRef` **opzionale**, quindi un cluster a tenant singolo che non lo crea mai si avvia comunque normalmente. Mantenete il valore **stabile e identico su tutte le repliche**; ruotarlo invalida la password ClickHouse derivata di ogni organizzazione finché il riconcile di avvio non ri-provisioning gli utenti (un riavvio rotante con il valore coerente ovunque lo guarisce). Consultate `deploy/base/server/secret.example.yaml`.

> **Memorizzate `ORG_CH_SECRET` nel vostro gestore di segreti e non rotelatelo casualmente.**

***

### 2.7 Verificare tutti i secret

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

Output atteso (tra qualsiasi secret predefinito):

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

I quattro secret principali (`agenteye-admin-key`, `agenteye-auth`, `agenteye-image-pull`, `agenteye-postgres`) devono essere presenti prima di continuare. `agenteye-org-ch-secret` è richiesto solo per deployment multi-tenant (consultate §2.6).

***

## Fase 3 -- Distribuire l'Applicazione (\~5 min)

### 3.1 Configurare gli hostname pubblici

cert-manager ha bisogno degli hostname di acquisizione e della dashboard prima di poter richiedere i loro certificati Let's Encrypt. Copiate il modello e impostate entrambi:

```bash theme={null}
cp base/certificates/domain.env.example base/certificates/domain.env
# Modificate base/certificates/domain.env e impostate:
#   INGEST_DOMAIN=ingest.your-company.example      (risolve al LB Traefik pubblico)
#   DASHBOARD_DOMAIN=agenteye.your-company.example (risolve al LB Traefik della dashboard)
```

`domain.env` è gitignored; rimane locale a ogni deployment. Il build kustomize non riesce in modo evidente se una delle due chiavi è mancante.

> **Il DNS deve risolvere prima.** Non dovete puntare il DNS ai LB ancora (non esistono fino al completamento della Fase 1.2), ma l'emissione ACME nel passaggio 3.2 ripeterà il tentativo finché ogni hostname non si risolva al suo LoadBalancer. Potete impostare il DNS ora (usando i nomi host LB catturati nella Fase 1.4) o procedere e aggiungere i record nella Fase 4.

***

### 3.2 Applicare i manifest

Applicate la base direttamente per un'installazione pulita, o un overlay se ne avete ritagliato uno per questo ambiente (gli overlay fissano solo tag immagini, variabili env e limiti di risorse; ereditano i certificati e il routing della base):

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

L'overlay include la base automaticamente; applicate uno, non entrambi.

***

### 3.3 Aspettare i pod

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

L'attesa è limitata ai pod del piano dati principale. I pod opzionali `agent` (assistente AI) e `redis` vengono avviati insieme a loro; l'assistente rimane inerte fino a quando non fornite il suo endpoint LLM (consultate [enterprise-docs/assistant.md](/it/agenteye/assistant)), e Redis è una cache best-effort, quindi nessuno dei due ha bisogno di essere Ready affinché la piattaforma serva il traffico.

**Testate:**

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

Risultato atteso (i pod opzionali `agent` e `redis` compaiono anche e raggiungono `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          ...
```

**Se non riesce:**

| Stato Pod          | Causa probabile                                      | Comando di Debug                                               |
| ------------------ | ---------------------------------------------------- | -------------------------------------------------------------- |
| `ImagePullBackOff` | Secret di pull dell'immagine scadente o PAT          | `kubectl describe pod <name> -n agenteye`                      |
| `CrashLoopBackOff` | Variabili d'ambiente scadenti (ad es. DATABASE\_URL) | `kubectl logs <name> -n agenteye`                              |
| `Pending`          | CPU/memoria insufficiente o nessun nodo              | `kubectl describe pod <name> -n agenteye` (controllate Events) |

***

### 3.4 Verificare l'archiviazione

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

Risultato atteso, entrambi con stato `Bound`:

| PVC                            | Capacità | Supporta                                                  |
| ------------------------------ | -------- | --------------------------------------------------------- |
| `postgres-data-postgres-0`     | `50Gi`   | Archivio relazionale/metadati PostgreSQL                  |
| `clickhouse-data-clickhouse-0` | `100Gi`  | Archivio di analisi degli eventi ClickHouse + valutazioni |

Un PVC `redis-data-redis-0` (1Gi) appare anche per la cache opzionale.

**Se non riesce:** `Pending` significa che nessuna StorageClass può provisioning il volume. Controllate `kubectl get storageclass` e assicuratevi che esista un'impostazione predefinita. Per la produzione, applicate il volume ClickHouse a una StorageClass SSD veloce (ad es. gp3 su AWS, pd-ssd su GCP); la velocità di compattazione soffre su dischi lenti.

***

### 3.5 Verificare i certificati

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

Risultato atteso: 3 certificati, tutti `Ready: True`:

| Nome            | Emittente          | Scopo                                                                                          |
| --------------- | ------------------ | ---------------------------------------------------------------------------------------------- |
| `mtls-ca`       | `selfsigned`       | CA privata per l'emissione di certificati client mTLS (validità 10 anni)                       |
| `ingest-tls`    | `letsencrypt-prod` | Certificato TLS pubblico per l'endpoint di acquisizione (90 giorni, rinnovato automaticamente) |
| `dashboard-tls` | `letsencrypt-prod` | Certificato TLS pubblico per la dashboard (90 giorni, rinnovato automaticamente)               |

**Se `ingest-tls` o `dashboard-tls` non è Ready:**

`kubectl describe certificate <name> -n agenteye` e leggete gli Events. Le cause comuni:

* **DNS non ancora puntato al LB.** Let's Encrypt risolve l'hostname e colpisce la porta 80 per convalidare — `INGEST_DOMAIN` deve risolvere al LB pubblico, `DASHBOARD_DOMAIN` al LB della dashboard. Fino a quando il CNAME/Alias non si propaga, l'ordine rimane `pending`. Una volta che il DNS è corretto, cert-manager ritenta automaticamente (non è necessario eliminare il Certificate).
* **Hostname non sostituito.** Se `dnsNames` continua a leggere `INGEST_DOMAIN_PLACEHOLDER` / `DASHBOARD_DOMAIN_PLACEHOLDER`, avete saltato il passaggio 3.1 -- create `base/certificates/domain.env` e ri-applicate.
* **Il Traefik della dashboard non può servire la sfida** (`dashboard-tls` solo). L'istanza Traefik della dashboard deve essere installata con il file di valori incluso (Fase 1.2), che abilita il provider Ingress scoped che serve il risolutore HTTP-01 di cert-manager. Un'istanza installata senza di esso lascia la sfida non instradabile e l'ordine `pending` per sempre.

**Se `mtls-ca` non è Ready:** cert-manager stesso non è salubre. Ri-controllate i pod cert-manager dal passaggio 1.1.

***

### 3.6 Verificare i CronJob

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

Risultato atteso:

| Nome                 | Pianificazione | Scopo                                                      |
| -------------------- | -------------- | ---------------------------------------------------------- |
| `agenteye-backup`    | `0 3 * * *`    | Backup giornaliero di Postgres + ClickHouse alle 03:00 UTC |
| `cert-renewal-check` | `0 3,15 * * *` | Avvisi di scadenza certificato alle 03:00 e 15:00 UTC      |

***

### 3.7 Verificare che il server sia stato avviato correttamente

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

**Testate:** cercate una linea di avvio che indichi che il server è in ascolto sulla porta 8080. Non dovrebbero esserci errori di connessione al database (il server richiede che sia PostgreSQL che ClickHouse siano raggiungibili prima che riporti Ready).

**Se non riesce:** la causa più comune è un `POSTGRES_PASSWORD` contenente caratteri non sicuri per URL che interrompono `DATABASE_URL`. Consultate [enterprise-docs/troubleshooting.md](/it/agenteye/troubleshooting).

***

### 3.8 Verificare che la dashboard sia connessa al server

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

**Testate:** cercate `Ready` nell'output senza errori `ECONNREFUSED` o simili.

**Se non riesce:** verificate che il Service `server` esista (`kubectl get svc server -n agenteye`) e che `AGENTEYE_SERVER_URL` sia impostato su `http://server:8080` nel deployment della dashboard.

***

## Fase 4 -- Accesso di rete (\~5 min)

### 4.1 Recuperare gli indirizzi LoadBalancer

```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}')
```

> Su AWS EKS, i LoadBalancer restituiscono un hostname invece di un IP. Sostituite `.ip` con `.hostname` nei comandi di cui sopra.

**Testate:**

```bash theme={null}
echo "Public  (ingest):    $PUBLIC_IP"
echo "Internal (dashboard): $INTERNAL_IP"
```

Entrambi devono essere non vuoti.

***

### 4.2 Puntare DNS ai LoadBalancer

Create record DNS in modo che gli hostname da `base/certificates/domain.env` si risolvano ai loro LoadBalancer — `INGEST_DOMAIN` al LB Traefik **pubblico**, `DASHBOARD_DOMAIN` al LB Traefik della **dashboard**:

* **AWS Route 53:** record `A` con `Alias = Yes`, target = hostname LB. Non usate A semplice → IP; gli IP ELB ruotano.
* **Qualsiasi altro provider:** `CNAME` dall'hostname all'hostname LB.

Verificate:

```bash theme={null}
dig +short ingest.your-company.example
dig +short agenteye.your-company.example
```

Dovrebbe restituire gli stessi indirizzi di `$PUBLIC_IP` e `$INTERNAL_IP` rispettivamente (o, su EKS, risolvere agli stessi hostname `*.elb.amazonaws.com`).

Una volta che il DNS si risolve, cert-manager termina gli ordini ACME in sospeso dalla Fase 3.5 entro un minuto. Ri-eseguite `kubectl get certificates -n agenteye` finché sia `ingest-tls` che `dashboard-tls` non mostrano `Ready: True`.

***

### 4.3 Raggiungere l'endpoint di acquisizione

L'endpoint di acquisizione pubblico applica TLS reciproco, quindi ogni richiesta (incluso `/health`) deve presentare un certificato client. Emettete il vostro primo certificato client nella Fase 5; se ne avete uno già, verificate la raggiungibilità ora:

```bash theme={null}
curl -s --cert issued/<cluster-name>/client.crt \
        --key issued/<cluster-name>/client.key \
        https://ingest.your-company.example/health
```

Risultato atteso: `{"status":"ok"}`. Non è necessario `-k` -- il certificato del server è vincolato a una CA pubblica per `INGEST_DOMAIN`, quindi si convalida rispetto all'archivio di trust del sistema. Raggiungete l'endpoint di acquisizione dal suo hostname `INGEST_DOMAIN` (che corrisponde al certificato emesso), non dall'IP/hostname del LoadBalancer raw.

L'endpoint della dashboard è servito su `DASHBOARD_DOMAIN` con un certificato pubblicamente attendibile e non è dietro mTLS, quindi non `-k` e nessun certificato client è necessario:

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

Raggiungete la dashboard dal suo hostname, non dall'indirizzo LB raw — il certificato è vincolato a `DASHBOARD_DOMAIN`, quindi l'indirizzo raw mostra una mancata corrispondenza del nome del certificato.

**Se non riesce:** se `curl` blocca, controllate che il LB sia raggiungibile dalla vostra macchina (VPN, security group, regole firewall). Un errore di handshake `certificate required` sull'hostname di acquisizione significa che nessun certificato client è stato presentato; completate prima la Fase 5. Un errore di convalida TLS sull'hostname di acquisizione significa che il certificato del server non ha finito di essere emesso; tornate a Fase 3.5 e risolvete il problema lì.

***

## Fase 5 -- Emettere Certificati Client mTLS (\~10 min per cluster)

I collector si autenticano con **due fattori**: un certificato client (livello di trasporto, prove che la richiesta proviene da un cluster autorizzato) e una chiave API (livello applicazione, prova che la richiesta proviene da un collector con permesso `events:add`). Una chiave persa è inutile senza il certificato; un certificato rubato è inutile senza una chiave valida.

### 5.1 Emettere un certificato

Ogni cluster che esegue collector ha bisogno del suo certificato client. Dalla directory dei manifest:

```bash theme={null}
cd base/certificates/client-certs
./issue-client-cert.sh <cluster-name>
```

Sostituite `<cluster-name>` con un identificatore significativo (ad es. `us-east-1-prod`, `staging`).

**Testate:** lo script stampa `==> Done!` ed elenca i file di output.

```bash theme={null}
kubectl get certificate mtls-client-<cluster-name> -n agenteye
```

Risultato atteso: `Ready: True`.

File di output in `issued/<cluster-name>/`:

| File                         | Scopo                                                              |
| ---------------------------- | ------------------------------------------------------------------ |
| `client.crt`                 | Certificato client (validità 90 giorni)                            |
| `client.key`                 | Chiave privata client                                              |
| `ca.crt`                     | Certificato CA per la verifica del server                          |
| `collector-mtls-secret.yaml` | Secret Kubernetes pronto da applicare per il cluster del collector |

***

### 5.1b Consegna alternativa: AWS Secrets Manager

Se il consumer del certificato è un Pod Kubernetes che ha bisogno di `client.crt` e `client.key` su disco -- il caso tipico quando eseguite il agenteye-collector come sidecar nel vostro pod applicazione -- spingete il bundle del certificato in AWS Secrets Manager. Il pod dell'applicazione lo monta quindi tramite il [Secrets Store CSI Driver](https://secrets-store-csi-driver.sigs.k8s.io/) con IRSA, e la rotazione del certificato è completamente senza mani.

```bash theme={null}
cd base/certificates/client-certs
export AWS_REGION=us-east-1     # regione dove il vostro carico di lavoro viene eseguito
./issue-client-cert.sh <cluster-name> --save-to aws-secrets-manager
```

Sulla ri-esecuzione (rinnovo), lo script chiama `PutSecretValue` sullo stesso secret, quindi l'ARN e il nome rimangono stabili. Il CSI Driver raccoglie la nuova versione al suo prossimo poll di rotazione e riscrive i file all'interno del pod.

**Prerequisiti:**

* `aws` CLI v2 autenticato al vostro account AWS.
* `jq` installato.
* Variabile d'ambiente `AWS_REGION` impostata.
* Permessi IAM sulla vostra identità di caller (ambito `Resource` a `arn:aws:secretsmanager:<region>:<account>:secret:agenteye/mtls-client/*`):
  * `secretsmanager:CreateSecret`
  * `secretsmanager:DescribeSecret`
  * `secretsmanager:PutSecretValue`
  * `secretsmanager:TagResource`

**Cosa fa lo script in questa modalità:**

| Passaggio | Azione                                                                                                                                                                                                                                                            |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1         | Emette / ri-estrae il certificato tramite cert-manager (uguale alla modalità predefinita).                                                                                                                                                                        |
| 2         | Chiama `DescribeSecret` su `agenteye/mtls-client/<cluster-name>` per decidere creare-vs-aggiornare.                                                                                                                                                               |
| 3         | Al primo avvio: `CreateSecret` con un payload JSON a tre chiavi (`client.crt`, `client.key`, `ca.crt`), taggato `AgentEyeCluster=<cluster-name>`. Sui successivi avvii: `PutSecretValue` per pubblicare una nuova versione; tag aggiornato tramite `TagResource`. |
| 4         | Cancella `issued/<cluster-name>/` solo dopo un caricamento riuscito. In caso di errore, la directory viene conservata in modo che possiate ritentare.                                                                                                             |

**Se il secret è pianificato per l'eliminazione**, lo script non riesce con un messaggio di errore chiaro che vi dice di eseguire `aws secretsmanager restore-secret --secret-id agenteye/mtls-client/<cluster-name>` prima di ritentare.

Per il cablaggio completo del pod (SecretProviderClass, setup IRSA, comportamento di rotazione, troubleshooting) consultate [enterprise-docs/single-pod-deployment.md](/it/agenteye/single-pod-deployment).

***

### 5.2 Verificare che il certificato funzioni

Testate il certificato emesso rispetto all'ingresso mTLS:

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

Risultato atteso: `{"status":"ok"}`

**Se non riesce:**

| Errore                 | Causa                                     | Correzione                                                                                                                 |
| ---------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `certificate required` | Certificato non presentato                | Controllate i percorsi file nel comando `curl`                                                                             |
| `bad certificate`      | Mancata corrispondenza CA                 | Verificate che `mtls-ca-issuer` abbia emesso il certificato: `kubectl describe certificate mtls-client-<name> -n agenteye` |
| `connection refused`   | Hostname sbagliato o LB non raggiungibile | Controllate `/etc/hosts` o DNS                                                                                             |

***

### 5.3 Consegnare al cluster del collector

Inviate `collector-mtls-secret.yaml` al team che gestisce il cluster del collector. Loro lo applicano:

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

Quindi configurate il collector per montare il secret e utilizzare i percorsi certificato:

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

Consultate [enterprise-docs/collector-installation.md](/it/agenteye/collector-installation) per la configurazione completa del collector inclusi i montaggi del volume Kubernetes.

**Testate (nel cluster del collector):**

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

Risultato atteso: il secret esiste con 3 chiavi di dati (`client.crt`, `client.key`, `ca.crt`).

***

### 5.4 Ciclo di vita del certificato

| Proprietà                   | Valore                                                 |
| --------------------------- | ------------------------------------------------------ |
| Validità certificato client | 90 giorni                                              |
| Auto-rinnovo                | cert-manager rinnova 15 giorni prima della scadenza    |
| Validità CA                 | 10 anni                                                |
| Avvisi di scadenza          | CronJob avvisa 30 giorni prima della scadenza (Fase 6) |

cert-manager rinnova automaticamente il certificato sul **cluster AgentEye**, ma il certificato rinnovato deve essere ri-consegnato al cluster del collector. Ri-eseguite `issue-client-cert.sh` e ri-applicate `collector-mtls-secret.yaml` prima che il vecchio certificato scada.

Se state usando `--save-to aws-secrets-manager` (consultate § 5.1b), ri-eseguite lo stesso comando. Lo script chiama `PutSecretValue` sullo stesso secret; i pod che montano il secret tramite il Secrets Store CSI Driver raccolgono la nuova versione al loro prossimo poll di rotazione (predefinito: ogni ora), senza riavvio del pod richiesto.

***

### 5.5 Revocare un certificato

Per bloccare immediatamente l'accesso del collector di un cluster:

```bash theme={null}
kubectl delete certificate mtls-client-<cluster-name> -n agenteye
```

**Testate:** il comando `curl` dal passaggio 5.2 ora non riesce con un errore di handshake TLS.

***

## Fase 6 -- Monitoraggio del Rinnovo dei Certificati (\~2 min)

Un CronJob incorporato viene eseguito ogni 12 ore (03:00 e 15:00 UTC) e controlla tutti i certificati client etichettati come `agenteye.io/cert-type=mtls-client`. Avvisa quando un certificato è entro 30 giorni dalla scadenza.

### 6.1 Abilita notifiche Slack (opzionale)

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

Senza questo secret, il CronJob continua a funzionare e registra lo stato del certificato nello stdout.

**Testate:**

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

Risultato atteso: il secret esiste.

***

### 6.2 Testare il 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
```

Risultato atteso: un elenco di certificati con il loro stato di scadenza. Se il webhook Slack è configurato, controllate il canale Slack per il messaggio di avviso.

**Se non riesce:** controllate RBAC -- il ServiceAccount del CronJob ha bisogno di permessi `get, list` su risorse Certificate di cert-manager. Verificate con: `kubectl describe role cert-renewal-check -n agenteye`.

Pulite il job di test:

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

***

## Fase 7 -- Verificare End-to-End

Questa fase conferma che l'intero pipeline funziona: controllo di salute, creazione della chiave, acquisizione di eventi e visualizzazione della dashboard.

> **Nota:** gli esempi di seguito raggiungono l'endpoint di acquisizione dal suo indirizzo LoadBalancer raw (`${PUBLIC_IP}`) per comodità, motivo per cui passano `-k`; il certificato del server è vincolato a `INGEST_DOMAIN`, non all'IP LB, quindi il controllo del nome host è saltato. L'endpoint di acquisizione applica TLS reciproco su **ogni** percorso, quindi ogni chiamata deve presentare un certificato client (`--cert`/`--key`). Per convalidare il certificato pubblico pure, puntate a `https://ingest.your-company.example/...` anziché `${PUBLIC_IP}` e lasciate cadere `-k`.

### 7.1 Controllo di salute

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

Risultato atteso: `{"status":"ok"}` con HTTP 200.

***

### 7.2 Creare chiavi collector limitate

La chiave admin è per il bootstrap e la gestione. Create chiavi dedicate `events:add` per i collector:

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

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

**Testate:** la risposta include `"id"`, `"name": "prod-collector"`, `"permissions": ["events:add"]`, `"created_at"`.

**Testate:** verificate che la chiave appaia nell'elenco di chiavi:

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

Risultato atteso: `prod-collector` appare nella risposta.

Consultate [enterprise-docs/api-keys.md](/it/agenteye/api-keys) per il riferimento di gestione completo delle chiavi.

***

### 7.3 Acquisire un evento di test

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

Risultato atteso: `{"accepted":1,"skipped":0}` con HTTP 200.

**Se non riesce:**

| Codice di stato HTTP    | Causa                                                                         |
| ----------------------- | ----------------------------------------------------------------------------- |
| 401                     | Chiave API non valida o mancante                                              |
| 403                     | La chiave manca del permesso `events:add`                                     |
| Errore di handshake TLS | Problema del certificato client -- consultate il troubleshooting della Fase 5 |

***

### 7.4 Verificare che l'evento appaia nella dashboard

Aprite `https://agenteye.your-company.example` (il vostro `DASHBOARD_DOMAIN`) in un browser. Il certificato è pubblicamente attendibile, quindi non c'è avviso.

> Se il LoadBalancer della dashboard è limitato dall'allowlist IP e non potete connettervi, verificate che il vostro IP sia consentito:
>
> ```bash theme={null}
> kubectl get svc traefik-dashboard -n traefik-dashboard -o jsonpath='{.spec.loadBalancerSourceRanges}'
> ```
>
> Ricordate che Let's Encrypt rinnova il certificato della dashboard su HTTP-01 sulla porta 80, e gli intervalli di origine si applicano all'intero LoadBalancer — prima di limitarlo agli intervalli aziendali, coordinare un risolutore DNS-01 con il supporto o i rinnovi non riescono silenziosamente.

**Testate:** l'evento smoke-test dovrebbe apparire nell'elenco di eventi con sessione `test` e agent `smoke-test`.

**Se non riesce:** controllate i log della dashboard (`kubectl logs -n agenteye -l app=dashboard --tail=50`). Verificate che `AGENTEYE_SERVER_URL` e `AGENTEYE_API_KEY` siano impostati correttamente.

***

### 7.5 Testare il CronJob di backup

```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
```

Risultato atteso: `Backup created: agenteye-YYYYMMDD-HHMMSS.tar.gz (NNN)` nei log; l'archivio raggruppa il dump di Postgres e le tabelle ClickHouse.

> Il passaggio di caricamento S3 è cablato nella CronJob e viene eseguito ogni volta che `BACKUP_BUCKET` è impostato (la base spedisce un valore di bucket predefinito). Viene saltato solo quando `BACKUP_BUCKET` è vuoto o letteralmente `PLACEHOLDER`. Puntatealo al vostro bucket proprio e concedete al ServiceAccount `agenteye-backup` l'accesso in scrittura prima di fare affidamento su di esso (consultate la sezione Backup di seguito).

Pulite:

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

***

### 7.6 Provisioning di organizzazioni (multi-tenant)

Saltate questo per un deployment a tenant singolo; tutti i dati risiedono nell'organizzazione `default` incorporata e nulla qui è richiesto.

Se state eseguendo più tenant isolati, le organizzazioni e le loro appartenenze vengono create con la **CLI `agenteye-orgctl`**. Viene fornita **all'interno dell'immagine del server** (insieme a `agenteye-server`) e lo eseguite **all'interno del Deployment `server` esistente con `kubectl exec`**; non c'è un pod, Job o Deployment separato, e nessuna API HTTP o pulsante della dashboard per il ciclo di vita del tenant.\*\* L'esecuzione nel pod del server significa che riusa `DATABASE_URL`, `CLICKHOUSE_URL` e il `ORG_CH_SECRET` dal pod §2.6.

> **Prerequisito:** completate prima §2.6. `org create` rifiuta di eseguire mentre il server è ancora sul `ORG_CH_SECRET` dev incorporato, e l'utente ClickHouse per organizzazione che provisioning dipende da quel secret essendo forte e stabile.

**Creare un'organizzazione e aggiungere il suo primo admin:**

```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
```

Il nuovo membro riceve un OTP al primo accesso della dashboard e quindi funziona interamente nell'interfaccia utente sotto il prefisso URL dell'organizzazione (ad es. `/acme/...`).

**Altri comandi** (eseguiti nello stesso modo `kubectl -n agenteye exec deploy/server -- agenteye-orgctl …`):

| Comando                                                                             | Cosa fa                                                                                                    |
| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `org list`                                                                          | Elenca le organizzazioni e il loro stato.                                                                  |
| `org rename --slug <slug> --name <new name>`                                        | Rinomina un'organizzazione (slug invariato).                                                               |
| `org delete --slug <slug>`                                                          | Soft-delete + rilascia l'utente ClickHouse dell'organizzazione; **dati conservati**.                       |
| `org purge --slug <slug>`                                                           | Wipe dei dati irreversibile; l'organizzazione deve essere prima `delete`d; mai l'organizzazione `default`. |
| `member list --org <slug>`                                                          | Elenca i membri e i loro permessi.                                                                         |
| `member update --org <slug> --email <email> [--set ...] [--add ...] [--remove ...]` | Modificare i permessi di un membro.                                                                        |
| `member remove --org <slug> --email <email>`                                        | Rimuovere un membro dall'organizzazione.                                                                   |

I set di permessi incorporati sono `admin`, `standard` e `read-only`. **Le chiavi API per organizzazione sono comunque coniate nella dashboard/API dai membri dell'organizzazione (il §7.2 mostra l'API delle chiavi); solo il ciclo di vita dell'organizzazione + membro è solo per l'operatore.** Riferimento completo e un esempio funzionante: [enterprise-docs/tenant-management.md](/it/agenteye/tenant-management).

***

## Checklist Post-Deployment

Usate questa checklist per confermare che tutto funziona. Ogni elemento dovrebbe essere controllato prima di consegnare ai collector.

* [ ] Tutti i pod `Running` nello spazio dei nomi `agenteye`
* [ ] PVC PostgreSQL bound (50Gi) e PVC ClickHouse bound (100Gi)
* [ ] Tutti e 3 i certificati `Ready: True`
* [ ] Entrambi gli IP LoadBalancer assegnati
* [ ] DNS o `/etc/hosts` configurato e che si risolve
* [ ] `/health` restituisce HTTP 200
* [ ] Test del certificato mTLS superato (`curl` con certificato client a `/health`)
* [ ] Chiave collector scoped creata e testata
* [ ] Evento di test acquisito (`accepted: 1`)
* [ ] Evento visibile nella dashboard
* [ ] Certificati client emessi per ogni cluster del collector
* [ ] CronJob di backup testato manualmente
* [ ] CronJob di rinnovo certificato testato manualmente
* [ ] Webhook Slack per avvisi di certificato configurato (opzionale)
* [ ] Bucket di backup configurato nell'overlay (consultate di seguito)
* [ ] Chiave admin e password Postgres memorizzate nel gestore di segreti

***

## Backup

Un singolo `agenteye-backup` CronJob viene eseguito ogni giorno alle 03:00 UTC. Esegue il dump di **entrambi** gli archivi: PostgreSQL (stato relazionale) e ClickHouse (le tabelle di analisi `events` + `evaluations`), in un archivio compresso nel pod, quindi lo carica nello spazio di archiviazione che controllate nell'overlay.

Ogni esecuzione produce un oggetto, `agenteye-<timestamp>.tar.gz`, che si decomprime in:

```
postgres.sql        # pg_dump del database relazionale
events.sql          # DDL della tabella degli eventi ClickHouse
events.native       # Dati degli eventi ClickHouse (formato Native)
evaluations.sql     # DDL della tabella delle valutazioni ClickHouse
evaluations.native  # Dati delle valutazioni ClickHouse
```

ClickHouse viene letto sulla sua API HTTP (lo stesso endpoint che il server utilizza), quindi il job non ha bisogno di nessun client ClickHouse. Solo le due tabelle fisiche vengono scritte; il server ricrea tutte le viste (`agent_sessions`, gli alias `analytics.*`) e le politiche di riga all'avvio, quindi quelle tabelle sono l'immagine completa.

### Configurare il caricamento cloud

Il CronJob di backup viene fornito con il passaggio di caricamento S3 (`aws s3 cp`)
