Cloud Intelligence™Cloud Intelligence™

Cloud Intelligence™

Autenticazione tra microservizi: è davvero così difficile?

By Joshua FoxJun 28, 202210 min read

Questa pagina è disponibile anche in English, Deutsch, Español, Français, 日本語 e Português.

Vediamo diversi metodi per gestire l'autenticazione tra microservizi, dal più semplice ma meno sicuro e manutenibile fino alle architetture consigliate.

authentication-microservices

Un'architettura a microservizi ruota tutta attorno a servizi che si invocano a vicenda. Ma come si garantisce la sicurezza, facendo in modo che solo i propri servizi possano chiamare gli altri servizi interni? Come si gestisce l'autenticazione?

Nelle applicazioni browser il meccanismo è noto: l'utente inserisce le credenziali, il browser le invia al servizio che restituisce un token di sessione, valido per autenticare l'utente per un certo periodo. Con un microservizio, però, non c'è nessun essere umano nel ciclo che possa digitare una password o una chiave di autenticazione a più fattori.

In questo articolo descriverò come affrontare il problema, concentrandomi sui server in esecuzione su servizi Google Cloud Platform come Cloud Run o Google Kubernetes Engine (GKE). I servizi client possono trovarsi su GCP, on-premises o su AWS.

Ciò che mi ha spinto a scrivere questo articolo è il fatto che API Gateway e Cloud Endpoints sono tecnologie in rapida evoluzione, con solide capacità di autenticazione, ma anche con limiti se confrontate tra loro e con altri servizi concorrenti. Esistono molti modi per gestire l'autenticazione tra microservizi: partirò dal più semplice ma meno sicuro e manutenibile, per arrivare alle architetture consigliate.

Per chiarezza, mi concentrerò sui microservizi in cui si controllano entrambi i lati, ma gli stessi principi valgono anche quando il servizio client è esterno alla propria organizzazione.

Le basi: header, chiavi e proxy

Anche se esistono modalità più semplici e altre più complesse per realizzarla, l'autenticazione tra servizi richiede una progettazione molto attenta. Lo schema di base è il seguente.

  • Il client usa una chiave segreta per firmare un token
  • Il formato standard per la trasmissione è il JSON Web Token
  • Il token viene inserito in un header HTTP Authorization come segue
Authorization: Bearer <JWT>

dove è il token codificato in base64.

  • Il server convalida quel token interrogando un servizio. In alternativa, un reverse proxy può ricevere la richiesta e, prima di inoltrarla al server effettivo, convalidare il token interrogando un servizio.
  • Su GCP, questo servizio è fornito direttamente dalla piattaforma.

In questo articolo vedremo diversi modi per realizzarla, partendo da soluzioni semplici e poco sicure per arrivare a soluzioni più complete.

Troppo semplice: "API Key" gestite in proprio

Una soluzione di base, spesso adottata da chi non conosce l'intera gamma di tecnologie disponibili, è simile a ciò che fa un utente quando accede con username e password: si memorizza una stringa segreta nel servizio client, una "API Key", che funge da credenziale, e la si convalida lato server.

APIKEY=Microservice1:78eb9a45897f

Limiti

Questo approccio non è sicuro.

Fuga di chiavi

Le chiavi possono trapelare in più modi di quanti se ne possano immaginare.

Per evitarlo, non bisognerebbe memorizzare i segreti in Git o in altri sistemi di controllo del codice sorgente — è fin troppo facile renderli pubblici per errore; meglio affidarsi a un servizio di gestione dei segreti come Google Cloud Secret Manager o Hashicorp Vault. Resta però lo stesso problema: il servizio client dovrà comunque memorizzare le credenziali per accedere al secret manager.

Gestione delle chiavi

Sarà necessario sviluppare un database lato server per memorizzare queste chiavi e un livello che convalidi la correttezza di una chiave ricevuta. Anche da questo livello le chiavi non devono trapelare, quindi il client non dovrebbe trasmettere la API key in chiaro, ma solo un hash, che il server confronta con un hash della chiave già memorizzato. Mantenere tutto questo è costoso. Ed è anche poco sicuro, perché difficilmente si dispone delle competenze e del tempo necessari per chiudere ogni possibile falla. Per quanto possibile, i sistemi di sicurezza vanno lasciati agli esperti.

Rotazione

Poiché le fughe sono inevitabili, la best practice è ruotare la chiave con frequenza: crearne una nuova e invalidare la precedente trascorso un certo periodo. Questo richiede di automatizzare lato client un meccanismo che richieda una nuova chiave (autenticando questa stessa richiesta con la chiave vecchia!). Lato server, invece, serve un meccanismo per generare nuove chiavi su richiesta e per invalidare la vecchia a una data successiva specifica.

E questo genere di cose è sempre più complicato di quanto si pensi all'inizio: ad esempio, è opportuno imporre un numero massimo di versioni valide di una chiave in un dato momento, perché avere due versioni valide è una parte necessaria della rotazione, ma cento versioni equivalgono a una fuga di dati annunciata.

Service account key del provider cloud

Perché implementare da soli i meccanismi di hashing, validazione, rotazione e scadenza? Una soluzione migliore è creare un service account e poi scaricare un file di chiave da Google Cloud Identity and Access Management (IAM). Si può creare una chiave dalla pagina Service Account.

authentication-between-microservices

authentication-and-authorization-in-microservices

Si scarica il JSON, ricordandosi di impostare la data di scadenza — una nuova funzionalità di Google Cloud. Il JSON ha questo aspetto. (Tranquilli, ho oscurato accuratamente il testo 😁 e ho già disabilitato la chiave!)

{
"type": "service_account",
"project_id": "myproject",
"private_key_id": "ded9d97108b…..5cfd179e95e0e1",
"private_key": " — — -BEGIN PRIVATE KEY — — -\nMIIEvKIBADABNBg….QDA6woGjE4Q — — -END PRIVATE KEY — — -\n",
"client_email": "[email protected]",
"client_id": "106482...4210366919",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/kubeflowpipeline%40joshua-playground.iam.gserviceaccount.com"
}

Limite

Sembra una buona soluzione, vero? Ma — come avrete intuito — non è sicura e pratica quanto vorremmo.

Proprio come le API key fatte in casa, anche il file della Service Account key può trapelare, e quindi va ruotato. Google aiuta a far sì che le chiavi scadano e mette a disposizione API per ottenerne di nuove, ma queste funzionalità vanno comunque utilizzate.

Più avanti vedremo come fare a meno del file di chiave, integrando il Service Account direttamente nelle applicazioni client. Prima, però, vediamo come usare il Service Account, sia tramite il file di chiave sia nella variante integrata.

Autenticazione nel codice applicativo

Per autenticarsi, il client-service utilizza il proprio Service Account.

Il meccanismo si basa su OpenID Connect (OIDC), che trasmette JSON Web Token (JWT) firmati. Questi token restano validi solo per un breve periodo — ore, non settimane — riducendo al minimo il rischio in caso di fuga.

Si può gestire tutto a livello di codice, con librerie software lato client e lato server.

  • Per prima cosa, il client crea e utilizza una libreria software, insieme alla chiave del Service Account, per firmare un JWT di richiesta di accesso.
  • Usa poi quel token per richiederne un altro, un access token, al server di autenticazione di Google. Quest'ultimo verifica che il Service Account abbia effettivamente firmato il JWT di richiesta di accesso e restituisce l'access token a conferma.
  • Il client utilizza questo access token per chiamare il microservizio.
  • Il microservizio utilizza una libreria software per verificare che l'access token sia effettivamente convalidato e firmato dal servizio Google.

Il flusso è simile a questo, con la differenza che il servizio invocato è il vostro microservizio anziché un'API di Google.

authorization-in-microservices

Limite

Accoppiamento con il server

Questa soluzione richiede codice all'interno del server. E poiché spesso si hanno più microservizi con le stesse esigenze, significa mantenere e proteggere questo livello di autenticazione in più codebase.

Più avanti vedremo come evitare di inserire questo codice nell'applicazione. Prima, però, vediamo come smettere del tutto di usare i file di chiave.

Service Account integrati: GCP

Se si distribuisce il servizio client in GCP, è meglio non usare una Service Account key. Conviene invece avviare il servizio con il Service Account già integrato.

Ad esempio, con un'istanza Google Compute Engine (GCE) si può usare

gcloud compute instances create [INSTANCE_NAME] --service-account [SERVICE_ACCOUNT_EMAIL] ...

per specificare il Service Account; lo stesso vale per Cloud Run e per gli altri servizi GCP da cui il client-microservizio potrebbe invocare altri microservizi.

A questo punto non c'è più da preoccuparsi della fuga dei file di chiave, perché semplicemente non esistono. Al loro posto, il metadata server genera un token di istanza firmato che attesta l'identità del service account. (E la richiesta al metadata server non lascia mai l'istanza fisica su cui gira la VM.)

Su Kubernetes

Kubernetes ha un proprio sistema di service account che fanno parte di un meccanismo di autenticazione specifico di Kubernetes, separato dal livello GCP IAM. Se quindi il servizio client gira su Google Kubernetes Engine, conviene utilizzare Workload Identity per assegnare un GCP IAM Service Account al livello Kubernetes. Workload Identity intercetta in modo trasparente tutte le chiamate da GKE alle API GCP, facendo da proxy e arricchendole con l'access token.

Se il client si trova su Elastic Kubernetes Service di AWS, è possibile assegnargli un IAM role per partecipare ai flussi GCP, come illustrato nella sezione successiva.

Ruoli integrati: AWS e Workload Identity Federation

Se il servizio client è in AWS, non lo si può avviare con un GCP Service Account, ma si può avviarlo con la variante AWS, il Role. La Lambda si avvia con un execution role, mentre l'istanza EC2 con un role (incapsulato in un "Instance Profile".)

GCP non può fidarsi direttamente di quel role, quindi si usa Workload Identity Federation (WIF) per fare da ponte tra AWS e GCP ( articolo).

Il flusso è il seguente:

  • Per prima cosa, il servizio client su AWS usa il proprio role per firmare un token (token 1).
  • Usa il token 1 per richiederne un altro (token 2) firmato da AWS IAM.
  • Usa il token 2 per chiedere a GCP WIF di firmare un access token (token 3). Google WIF è preconfigurato per fidarsi del role AWS specificato e, una volta che AWS ha certificato che la richiesta proviene da quel role, WIF firma e restituisce l'access token (token 3).
  • Il servizio client utilizza il token 3 esattamente come farebbe un servizio client basato su GCP con un access token; da qui in poi il flusso prosegue allo stesso modo.

microservices-authentication-and-authorization

Sembra complicato, ma evita di far girare per internet quelle stringhe segrete e facilmente esponibili — in questo caso, da inviare a un altro cloud.

Autenticazione da Google verso un workload AWS

Questo articolo è dedicato soprattutto ai servizi in esecuzione su Google, ma gtoken merita una breve menzione. Fa l'opposto di Workload Identity Federation: autentica un workload GKE per interrogare le API AWS, fornendo all'invocazione un'identità AWS temporanea.

Proxy di autenticazione: API Gateway

Tuttavia, come accennato in precedenza, c'è un limite: è il proprio codice applicativo a eseguire il passaggio finale, ovvero la convalida che la firma provenga davvero dal principal autorizzato da Google. Quando possibile, è preferibile affidarsi a sistemi collaudati e prodotti in modo industriale, realizzati da esperti di sicurezza.

Vale quindi la pena dare un'occhiata ad API Gateway per ottenere un livello di autenticazione service-to-service robusto e configurabile, senza doverlo gestire in proprio.

Funziona come un proxy. Espone un indirizzo pubblico, frapponendosi tra il client e il servizio serverless su Cloud Run, Cloud Functions e App Engine. Si occupa di ricevere il token e di invocare i servizi Google per autenticare la richiesta, prima di inoltrarla al backend serverless. Per proteggere il collegamento tra API Gateway e il backend, Google inserisce uno speciale header, sotto il proprio controllo, che nessun attaccante può aggiungere.

Limite

API Gateway, però, non funziona con GKE, perché è strettamente integrato con le interfacce esposte dai servizi serverless gestiti da Google.

Proxy di autenticazione: Cloud Endpoint

Come ci si autentica allora con GKE, mantenendo comunque sicuro il collegamento dal livello di autenticazione al servizio backend? In questo caso si usa Extensible Service Proxy con Google Cloud Endpoints: un servizio un po' più datato, su cui si basa API Gateway e che quest'ultimo estende.

ESP (oggi alla v2) è un container che espone un indirizzo pubblico e autentica le richieste. Per usarlo con GKE, lo si distribuisce come pod nel cluster. (Tra l'altro, anche se la documentazione indica che sono supportati solo i cluster più recenti VPC-native/IP alias, funziona anche con i precedenti cluster basati su routes.)

Anche il collegamento tra ESPv2 e i servizi Kubernetes all'interno del cluster va protetto. Lo si può fare a livello di networking del cluster, non esponendo alcun indirizzo pubblico tranne ESP, oppure con soluzioni più sofisticate come mutual TLS o Istio Security.

Per una sicurezza ancora maggiore, si può distribuire ESPv2 come sidecar, in modo che il proxy e l'applicazione (Kubernetes Deployment) condividano lo spazio sicuro "localhost" di un pod. (Anche se non è la modalità di deployment principale per ESPv2, è supportata in questa ricetta YAML condivisa sull'account GitHub ufficiale di Google Cloud.)

In conclusione: mettete in sicurezza i vostri microservizi!

Non si può lasciare che chiunque invochi le proprie API. In passato il problema veniva risolto con i confini di rete o, nel cloud, con le VPC. Ma le architetture moderne supportano l'integrazione tra account cloud, tra cloud provider diversi e con sistemi non cloud. E anche all'interno della VPC è opportuno avere un ulteriore livello di sicurezza mirato proprio sul singolo collegamento client-server: ogni endpoint deve fidarsi dell'altro.

La sfida, quindi, è duplice:

  • Autenticarsi senza lasciare in giro file sensibili che possano trapelare.
  • Delegare l'autenticazione a servizi affidabili, senza accoppiarla al codice applicativo.

In questo articolo ho descritto alcuni modi per ottenere questo risultato, aggiungendo gradualmente sicurezza e manutenibilità ma anche richiedendo la conoscenza di un numero crescente di tecnologie. Imparare a usarle è un investimento che ripaga ampiamente — molto meno costoso che cadere vittima di un attacco!