Istio Service Mesh & Kiali
Riferimento operativo per OpenShift Service Mesh (OSSM 2.x / Istio): architettura, mTLS, AuthorizationPolicy, audit da CLI e navigazione Kiali. Pensato per cluster enterprise con pattern zero-trust a namespace.
Architettura in 60 secondi
| Componente | Ruolo |
|---|---|
| Istiod (control plane) | Distribuisce config e certificati agli Envoy proxy |
| Envoy sidecar (data plane) | Proxy iniettato in ogni pod: intercetta tutto il traffico, applica mTLS e policy |
| Ingress Gateway | Envoy standalone in istio-system: punto d’ingresso del traffico esterno |
| Kiali | Console web: topologia, traffico, validazione config Istio |
| Prometheus | Sorgente delle metriche: il grafo Kiali è costruito da qui |
| Jaeger | Distributed tracing (trace e span delle richieste) |
Concetti chiave:
- Il mesh opera a livello rete: le feature (mTLS, authz, routing, resilienza) si ottengono senza modificare il codice applicativo.
- In OSSM l’appartenenza al mesh è governata dal ServiceMeshMemberRoll (
smmr): un namespace non membro è invisibile a Kiali e non riceve sidecar. - PeerAuthentication = chi sei (identità/mTLS). AuthorizationPolicy = cosa puoi fare (accesso). Vanno lette insieme: i
principalsfunzionano solo con mTLS attivo.
Inventario mesh
oc get smcp -n istio-systemoc get smmr -n istio-system default -o jsonpath='{.status.members}' | tr ',' '\n'oc get pods -n istio-systemoc get route -n istio-system kiali -o jsonpath='https://{.spec.host}{"\n"}'oc get route -n istio-system jaeger -o jsonpath='https://{.spec.host}{"\n"}'Verifica sidecar iniettato in un namespace (2/2 = app + istio-proxy):
oc get pods -n <ns> -o custom-columns='POD:.metadata.name,CONTAINERS:.spec.containers[*].name'mTLS & PeerAuthentication
Cos’è. PeerAuthentication (PA) governa come i sidecar Envoy si autenticano tra loro: decide se il traffico service-to-service dentro il mesh deve viaggiare in mTLS (mutual TLS) o può restare in chiaro. È la risposta alla domanda “chi sei?” a livello di trasporto — distinta e complementare rispetto ad AuthorizationPolicy, che risponde a “cosa puoi fare?”.
Perché conta. Con mTLS attivo ogni pod ottiene da Istiod un certificato con un’identità SPIFFE derivata dalla sua ServiceAccount (spiffe://cluster.local/ns/<ns>/sa/<sa>). Due effetti: il traffico interno è cifrato end-to-end tra sidecar, e le AuthorizationPolicy possono basarsi sui principals (l’identità verificata), non su IP falsificabili. Ecco perché PA e AuthorizationPolicy si leggono sempre insieme: un from.source.principals funziona solo se l’mTLS è attivo, altrimenti non c’è identità da verificare.
Come si applica. Una PA senza selector vale per tutto il namespace; nel root namespace del mesh (istio-system) vale per tutto il mesh. Con selector si restringe a un workload. La precedenza è workload > namespace > mesh.
Modalità
| Mode | Comportamento |
|---|---|
PERMISSIVE | Accetta sia plaintext che mTLS. È il default e serve in migrazione: i client non ancora nel mesh continuano a funzionare mentre quelli con sidecar passano a mTLS |
STRICT | Solo mTLS: il plaintext viene rifiutato. È l’obiettivo zero-trust, ma va introdotto con cautela (un client senza sidecar viene tagliato fuori) |
DISABLE | mTLS disattivato. Usato per eccezioni mirate: scrape Prometheus, collector esterni, health check di componenti non-mesh |
Estrazione
oc get peerauthentication -Aoc get peerauthentication,requestauthentication -A -o yaml > all-istio-auth.yamlEsempi
Mesh-wide STRICT (nel root namespace del mesh):
apiVersion: security.istio.io/v1kind: PeerAuthenticationmetadata: name: default namespace: istio-systemspec: mtls: mode: STRICTOverride a livello porta (es. escludere una porta dal wrapping mTLS):
apiVersion: security.istio.io/v1kind: PeerAuthenticationmetadata: name: app-mtls namespace: my-nsspec: selector: matchLabels: app: my-app mtls: mode: STRICT portLevelMtls: "9090": mode: DISABLEGotcha classici
- STRICT lato server ≠ mTLS garantito lato client. La PA STRICT dice al server di pretendere mTLS, ma è la DestinationRule che dice al client di originare connessioni mTLS. Se manca una DR mesh-wide, Kiali segnala KIA0401 (“Mesh-wide DestinationRule enabling mTLS is missing”). Nella pratica l’auto-mTLS di Istio spesso compensa — i client con sidecar negoziano mTLS da soli e il traffico viaggia cifrato lo stesso — ma la config è incompleta e fragile. Verifica sempre nel Graph di Kiali che gli archi in-mesh mostrino il lucchetto e, cliccando l’arco,
mTLS Enabledcon i principals SPIFFE. - DestinationRule che disabilita mTLS. Al contrario, una DR con
trafficPolicy.tls.mode: DISABLErompe l’mTLS anche con PA STRICT: il client smette di cifrare, il server STRICT rifiuta → connessioni fallite. Kiali flagga il conflitto in Istio Config. - Precedenza PA: workload > namespace > mesh. Una PA con selector su un workload vince su quella di namespace, che vince su quella mesh-wide in
istio-system. - Traffico TCP non-HTTP (es. Hazelcast, database): Envoy lo wrappa comunque in mTLS come connessione opaca, salvo
portLevelMtls. Nel Graph appare come arco blu (salute non valutabile), ma il lucchetto c’è.
AuthorizationPolicy
Modello mentale
Tre campi decidono tutto:
apiVersion: security.istio.io/v1kind: AuthorizationPolicymetadata: name: example namespace: my-nsspec: selector: # A CHI si applica (pod destinazione); assente = tutto il ns matchLabels: app: my-app action: ALLOW # ALLOW | DENY | AUDIT rules: # CHI / COSA è permesso (from / to / when) - from: - source: principals: ["cluster.local/ns/other-ns/sa/my-sa"] to: - operation: methods: ["GET", "POST"] paths: ["/api/*"]Regole di precedenza (9 troubleshooting su 10 partono da qui):
- DENY vince sempre su ALLOW (i DENY si valutano prima).
- Se esiste almeno una ALLOW su un workload → tutto ciò che non matcha è negato implicitamente.
- Nessuna policy sul workload → tutto permesso (default allow di Istio).
selectorassente → policy su tutto il namespace; nel root namespace (istio-system) → tutto il mesh.rules: []con action ALLOW → deny-all (non matcha nulla). Attenzione al duale:rules: [{}](regola vuota) → allow-all.source.principalsrichiede mTLS attivo;source.namespacesfida chiunque nel namespace sorgente.
Pattern zero-trust a namespace (i 4 template)
Baseline tipica ripetuta su ogni namespace applicativo:
| Policy | Funzione |
|---|---|
allow-nothing | rules: [] → default deny: da sola nega tutto |
allow-from-ns | Consente il traffico intra-namespace (source.namespaces = stesso ns) |
allow-gateway-sa | Consente la ServiceAccount del micro-gateway (ns -mgw gemello) |
allow-istio-ingressgateway | Consente l’ingress gateway di istio-system |
apiVersion: security.istio.io/v1kind: AuthorizationPolicymetadata: name: allow-nothing namespace: my-nsspec: {} # nessuna rule = deny-allapiVersion: security.istio.io/v1kind: AuthorizationPolicymetadata: name: allow-from-ns namespace: my-nsspec: action: ALLOW rules: - from: - source: namespaces: ["my-ns"]apiVersion: security.istio.io/v1kind: AuthorizationPolicymetadata: name: allow-istio-ingressgateway namespace: my-nsspec: action: ALLOW rules: - from: - source: principals: ["cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account"]Il diagramma dei flussi risultanti — utente e sistemi esterni entrano da due gateway distinti (frontend per gli utenti, backend per il M2M), i micro-gateway fanno da dorsale interna:
Nota: se
source.namespacescontiene*o*-mgw, Istio lo tratta come nome letterale e la regola non matcha nulla (Kiali segnala KIA0101). Il wildcard cross-app va quindi verificato, non dato per funzionante.
Audit da CLI (i comandi core)
Estrazione raw
oc get authorizationpolicy -Aoc get authorizationpolicy -A -o yaml > all-istio-authz.yamloc get networkpolicy -A -o yaml > all-netpol.yamlMatrice “chi parla con chi” (CSV) ⭐
Inventario rapido (quali/dove):
oc get authorizationpolicy -AExport YAML integrale (backup/analisi offline):
oc get authorizationpolicy -A -o yaml > istio-authz.yamlEstrae namespace, policy, action e sorgenti ammesse per tutte le AuthorizationPolicy:
oc get authorizationpolicy -A -o json | jq -r '.items[] | [.metadata.namespace, .metadata.name, (.spec.action // "ALLOW"), ((.spec.rules // []) | if length == 0 then "DENY-ALL" else map((.from // []) | map(.source | ((.principals // []) + (.namespaces // []) | join(","))) | join(";")) | join(" | ") end)] | @csv' > authz-matrix.csvVersione arricchita — aggiunge il selector (pod target) e le operation (metodi/porte/path permessi). È la vista human-readable completa sorgente → destinazione → operazioni:
oc get authorizationpolicy -A -o json | jq -r '.items[] | [.metadata.namespace, .metadata.name, (.spec.action // "ALLOW"), ((.spec.selector.matchLabels // {}) | if . == {} then "ALL-PODS-IN-NS" else tostring end), ((.spec.rules // []) | if length == 0 then "DENY-ALL" else map(((.from // []) | map(.source | ((.principals // []) + (.namespaces // []) + (.ipBlocks // []) | join(","))) | join(";")) + (if .to then " -> " + ((.to // []) | map(.operation | ((.methods // []) + (.ports // []) + (.paths // []) | join(","))) | join(";")) else "" end)) | join(" | ") end)] | @csv' > authz-matrix-full.csvVersione a video (tabellare):
oc get authorizationpolicy -A -o json | jq -r '.items[] | [.metadata.namespace, .metadata.name, (.spec.action // "ALLOW"), ((.spec.rules // []) | if length == 0 then "DENY-ALL (no rules)" else map((.from // []) | map(.source | ((.principals // []) + (.namespaces // []) | join(","))) | join(";")) | join(" | ") end)] | @tsv' | column -t -s $'\t'Deduplica template
Con centinaia di policy, verifica quante varianti distinte esistono davvero (nome + spec identica):
oc get authorizationpolicy -A -o json | jq -r '.items[] | "\(.metadata.name)\t\(.spec | tostring)"' | sort | uniq -c | sort -rnSe escono 4-6 righe → è un template stampato ovunque: leggi 4 YAML e conosci l’intero cluster. Le varianti in minoranza sono le eccezioni da investigare.
Inventario NetworkPolicy (per confronto L3/L4)
oc get netpol -A -o custom-columns='NS:.metadata.namespace,NAME:.metadata.name,POD-SELECTOR:.spec.podSelector.matchLabels,TYPES:.spec.policyTypes'Matrice NetworkPolicy (CSV) — l’equivalente L3/L4
Namespace, policy, pod target, policyTypes e sorgenti ingress ammesse (podSelector / namespaceSelector / ipBlock):
oc get networkpolicy -A -o json | jq -r '.items[] | [.metadata.namespace, .metadata.name, ((.spec.podSelector.matchLabels // {}) | if . == {} then "ALL-PODS-IN-NS" else tostring end), ((.spec.policyTypes // []) | join("+")), ((.spec.ingress // []) | if length == 0 then (if ((.spec.policyTypes // []) | index("Ingress")) then "DENY-ALL-INGRESS" else "-" end) else map(((.from // []) | if length == 0 then "ANY" else map(if .podSelector then "pod:"+((.podSelector.matchLabels // {})|tostring) elif .namespaceSelector then "ns:"+((.namespaceSelector.matchLabels // {})|tostring) elif .ipBlock then "cidr:"+.ipBlock.cidr else "?" end) | join(",") end)) | join(" | ") end)] | @csv' > netpol-matrix.csvRegola d’oro dell’incrocio
Un flusso A → B funziona solo se passa entrambi i livelli. Diagnosi rapida: timeout / connection refused → guarda le NetworkPolicy (L3/L4); HTTP 403 RBAC: access denied → guarda le AuthorizationPolicy (L7). Se un flusso è permesso in una matrice ma assente nell’altra, hai trovato l’incoerenza.
Certificazione coverage (audit)
Un namespace è scoperto se è membro del mesh (ha la NetworkPolicy istio-mesh-basic generata da Maistra per ogni membro del ServiceMeshMemberRoll) ma non contiene alcuna AuthorizationPolicy → Istio applica il default-allow, raggiungibile a L7 da qualsiasi workload autenticato del mesh.
Lista certificabile (fonte primaria: API server, non la console):
comm -23 <(oc get netpol -A -o jsonpath='{range .items[?(@.metadata.name=="istio-mesh-basic")]}{.metadata.namespace}{"\n"}{end}' | sort -u) <(oc get authorizationpolicy -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"\n"}{end}' | sort -u)Confezionamento come evidenza datata + hash per l’integrità:
{ echo "=== AUDIT AuthZ coverage — $(date -u +%FT%TZ) ==="; echo "Cluster: $(oc whoami --show-server)"; comm -23 <(oc get netpol -A -o jsonpath='{range .items[?(@.metadata.name=="istio-mesh-basic")]}{.metadata.namespace}{"\n"}{end}' | sort -u) <(oc get authorizationpolicy -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"\n"}{end}' | sort -u); } | tee authz-coverage-$(date +%Y%m%d).txtsha256sum authz-coverage-$(date +%Y%m%d).txt > authz-coverage-$(date +%Y%m%d).sha256Codici di validazione Kiali (KIA)
Kiali fa validazione semantica: dice cosa Istio fa davvero, non cosa sembra dire lo YAML. I codici incontrati sul campo:
| Codice | Significato | Insidia |
|---|---|---|
| KIA0101 | Namespace not found for this rule | source.namespaces tratta il valore come nome letterale, non glob: - '*' o - '*-mgw' non matchano nulla. Una regola pensata come apertura è in realtà inefficace. |
| KIA0401 | Mesh-wide DestinationRule enabling mTLS is missing | PeerAuthentication STRICT lato server senza la DR che fa originare mTLS ai client. Spesso l’auto-mTLS compensa (traffico cifrato lo stesso), ma la config è incompleta — verifica nel Graph che gli archi abbiano il lucchetto. |
| KIA1106 | More than one Virtual Service for same host | Più VirtualService sullo stesso host → routing non deterministico. |
Per verificare l’effetto reale di una policy sospetta, la controprova è sempre istioctl x authz check (sotto) o il flag mTLS sull’arco nel Graph.
istioctl / Envoy debugging
Quali policy Envoy applica effettivamente a un pod (tabella per policy con action e match):
istioctl x authz check <pod-name> -n <ns>Validazione config del mesh (stesso motore dei warning Kiali):
istioctl analyze -n <ns>Stato di sincronizzazione dei proxy col control plane:
istioctl proxy-statusConfig RBAC grezza nell’Envoy del pod (fallback se istioctl non è disponibile):
istioctl proxy-config listener <pod-name> -n <ns> -o json | grep -A5 rbacLog Envoy del pod, cercando i deny:
oc logs <pod-name> -n <ns> -c istio-proxy | grep RBACLa firma di un blocco authz è HTTP 403 con flag
RBAC: access denied.
Osservabilità (OSSM 3.x)
L’osservabilità del mesh si appoggia allo stack di monitoring di OpenShift: Kiali non ha un proprio database, correla metriche Prometheus con la config Istio. Se Prometheus non scrapa, gran parte di Kiali (grafo, health, tassi) resta vuota.
Componenti
| Componente | Ruolo |
|---|---|
| Prometheus | Scrapa le metriche dai sidecar Envoy e dai container applicativi. È la sorgente del Traffic Graph |
| Thanos | Aggrega, gestisce la retention e l’API di query sopra Prometheus |
| Alertmanager | Gestisce e instrada gli alert generati da Prometheus |
| Grafana Tempo | Backend di distributed tracing; sostituisce Jaeger nelle versioni recenti di OSSM |
| OpenTelemetry | API vendor-neutral per instrumentare il codice e inviare trace/span ai backend |
| Kiali / OSSMC | Visualizza topologia, traffico, health; l’OSSMC integra le viste Kiali nella console OpenShift |
Pipeline dati di Kiali
Il flusso in 4 passi: (1) Prometheus scrapa le metriche da Envoy e app, (2) Kiali interroga le API OpenShift/Istio per la config, (3) il Kiali server correla metriche e config, (4) OSSMC presenta i dati nella console. Il tracing è un layer a parte: i proxy Envoy generano gli span iniziali anche per servizi non instrumentati, li inoltrano al collector, e Kiali mostra i trace con link alla UI di tracing.
Onboarding di un namespace nell’osservabilità
Due azioni, da cluster-admin. Primo, abilitare il user workload monitoring (crea uno stack Prometheus/Thanos/Alertmanager dedicato in openshift-user-workload-monitoring):
apiVersion: v1kind: ConfigMapmetadata: name: cluster-monitoring-config namespace: openshift-monitoringdata: config.yaml: | enableUserWorkload: trueSecondo, etichettare il namespace per l’iniezione del sidecar (senza questo, niente Envoy e niente metriche):
oc label namespace <namespace> istio-injection=enabledLa Kiali Custom Resource
Il Kiali operator installa una CR kiali.io/v1alpha1.Kiali nello stesso namespace della CR Istio. Con più mesh sullo stesso cluster puoi avere una console Kiali per mesh; con un mesh multi-cluster, una sola vista consolidata.
apiVersion: kiali.io/v1alpha1kind: Kialimetadata: name: kialispec: istio_namespace: istio-system auth: strategy: openshift # integra l'auth con OpenShift OAuth external_services: prometheus: {} # sorgente metriche (essenziale) grafana: {} tracing: {} # Tempo/JaegerURL della console:
echo "https://$(oc get routes -n istio-system kiali -o jsonpath='{.spec.host}')"OSSMC vs console Kiali esterna
L’OpenShift Service Mesh Console plug-in (OSSMC) porta tutte le viste Kiali dentro la console OpenShift (menu Service Mesh nella admin view, più un tab Service Mesh su Deployment/Service). Unico limite rispetto alla console Kiali standalone: l’OSSMC visualizza un solo mesh alla volta.
Distributed tracing
Il tracing segue una singola richiesta dall’ingresso attraverso tutti i servizi che attraversa. Ogni servizio aggiunge uno span (info contestuale); la collezione di span di una richiesta forma il trace completo. Serve instrumentare le app (via OpenTelemetry), ma il data plane genera gli span iniziali anche senza instrumentazione completa. I trace compaiono nel side panel del nodo/edge in Kiali, se il tracing è configurato.
Kiali
Navigazione
| Vista | Cosa ci trovi |
|---|---|
| Overview | Card per namespace: salute, warning config, membri del mesh |
| Traffic Graph | Topologia col traffico reale (da Prometheus) |
| Istio Config | Inventario oggetti Istio con validazione inline |
| Workloads | Dettaglio Deployment: policy associate, tab Logs (incluso istio-proxy) |
| Services / Applications | Aggregazioni per Service e per label app |
Leggere il Graph
Geografia a tre fasce: grigio sinistra = sorgenti esterne al ns selezionato → bianco centro = il tuo namespace → grigio destra = destinazioni fuori dal mesh (egress, ServiceEntry, PassthroughCluster).
| Elemento | Significato |
|---|---|
| Triangolo | Service Kubernetes |
| Cerchio | Workload (Deployment/pod) |
| Quadrato | App (aggregazione per label app) |
| Icona chiave | Destinazione esterna via egress |
PassthroughCluster | Traffico verso destinazioni non registrate nel mesh (no ServiceEntry) |
| Arco verde | HTTP sano |
| Arco arancio | HTTP degradato (errori sopra soglia warning) |
| Arco rosso | HTTP con errori sopra soglia critica |
| Arco blu | TCP puro: Kiali non ne giudica la salute |
| Lucchetto | mTLS attivo sull’arco (Display → Security) |
Tipi di grafo: App graph per la visione d’insieme (meno nodi), Workload graph per il debug puntuale. Con Display → Service Nodes disattivato si dimezzano i nodi.
I quattro rendering disponibili:
| Tipo | Cosa mostra |
|---|---|
| App graph | Aggrega i workload per label applicativa — vista logica |
| Service graph | Alto livello: traffico aggregato per Service Kubernetes |
| Versioned app graph | Come App graph ma separa le versioni (traffico version-specific) |
| Workload graph | Dettaglio massimo: comunicazione workload-to-workload |
Interazioni sul grafo
- Single-click su un nodo → lo seleziona e apre il side panel; double-click → drill-down nel dettaglio di quel nodo, con il traffico visto dalla prospettiva del suo proxy.
- Il side panel (nodo/edge/box) riporta: grafici di request rate / error rate / response time, health e stato, response code e host breakdown, link alle risorse del mesh, e i trace correlati se il tracing è configurato.
- Le icone punto esclamativo sul grafo sono cliccabili e danno l’help contestuale sul campo vicino.
Menu Display (overlay senza toccare il traffico)
- Edge labels: request rate, error rate, response time
- Security: lucchetto mTLS, missing sidecar, health badge
- Istio config overlays: VirtualService, DestinationRule, Gateway, ServiceEntry
- Resilience: circuit breaker e fault injection
- Node visibility: service nodes, application nodes, operation nodes, idle nodes/edges
- Protocol badge: HTTP, gRPC, TCP
Traffic animation: cerchi animati = richieste HTTP riuscite, rombi rossi = errori; densità ∝ volume, velocità ∝ response time. Il traffico TCP appare come cerchi sfalsati la cui velocità riflette il throughput.
Replay: rigioca il traffico di una finestra temporale passata. Lo stato del grafo (replay incluso) è bookmarkable — puoi condividere una vista precisa con un collega via URL.
Find / Hide (domare il caos)
name *= activegatename *= activegate or name = PassthroughClusterhttpout > 0Occhio alla finestra temporale (in alto a destra): il grafo mostra traffico reale, non configurazione — un workload senza traffico recente sparisce. Grafo vuoto ≠ mesh rotto: quasi sempre è finestra troppo corta, zero traffico, o Prometheus del mesh con problemi.
Workflow di audit sicurezza
- Graph → Display → Security: tutti gli archi attivi in-mesh devono avere il lucchetto (con PA STRICT mesh-wide). Archi senza lucchetto = da investigare.
- Istio Config → filtro
PeerAuthentication: verifica la policy mesh-wide inistio-systeme che gli override (namespace/workload) siano voluti. - Istio Config → filtro
AuthorizationPolicy: per ogni policy controlla selector, sorgenti (principals/namespaces) e operation. I warning tipici: selector che non matcha nessun workload (policy morta), conflitti PA/DestinationRule, riferimenti a SA/namespace inesistenti. - Servizi scoperti: incrocia i servizi del namespace con le policy — senza policy (e senza deny-all a livello ns) un servizio è raggiungibile da tutto il mesh.
- Zero warning: l’audit è completo quando tutte le risorse passano la validazione Kiali.
Troubleshooting 403 / RBAC
Flusso rapido quando qualcosa viene bloccato:
- Graph: arco rosso → click → pannello a destra → response code
403. - Workloads → pod destinazione → Logs →
istio-proxy: cercaRBAC: access denied. istioctl x authz check <pod> -n <ns>: elenca le policy effettivamente applicate.- Verifica in ordine: esiste una ALLOW che matcha la sorgente? C’è una DENY che vince? La sorgente arriva in mTLS (i
principalsnon matchano il plaintext)? La ServiceAccount nel principal è quella giusta (cluster.local/ns/<ns>/sa/<sa>)?
Gotchas enterprise
- Kiali mostra solo il mesh: le NetworkPolicy native (OVN-K) non le vede. Authz Istio = L7/identità; NetworkPolicy = L3/L4. Sono complementari, servono entrambe.
- Il muro di archi rossi verso collector esterni (es. agent APM/ActiveGate) spesso non è un incendio: TLS su porte non dichiarate o classificazioni Envoy. Click sull’arco e leggi i codici prima di allarmarti.
- Label
app/versionnon curate → App graph illeggibile (nodi aggregati con nomi generici). - Policy vecchie di anni con warning “no matching workloads” = debito tecnico da ripulire, non da ignorare.
- Aggiungere la prima ALLOW a un workload finora senza policy nega implicitamente tutto il resto: pianifica le sorgenti prima di applicare.
- Dopo ogni upgrade del mesh, rifai il giro mTLS nel Graph per verificare che nulla si sia rotto.