
Cosa fare quando le Google Application Default Credentials smettono di funzionare
L'autenticazione su Google Cloud… funziona e basta, no? Avvii una nuova VM, ci installi gcloud e, come per magia, accedi ai tuoi bucket di Google Cloud Storage.
Una delle grandi promesse di Google Cloud è proprio quella di semplificare l'autenticazione. Basta ottenere le cosiddette "Application Default Credentials" (ADC) e tutto funziona ovunque: utilizzano le credenziali utente sulla macchina di sviluppo e quelle del service account quando il codice gira su Google Cloud. E in questo, devo ammetterlo, funzionano davvero bene.
Finché si resta dentro Google Cloud e non si tenta di accedere ad altri servizi Google, le ADC mantengono la promessa. Ma due parole bastano a rompere l'incantesimo: SCOPE e SUBJECT.
Cosa sono gli Scope?
Sono certo che hai già sentito questo termine. Si tratta di URL come quelli qui sotto, dietro ai quali non c'è alcun sito web reale :)
https://www.googleapis.com/auth/cloud-platform
https://www.googleapis.com/auth/compute.readonlySono scope oAuth 2.0 che servono a configurare un controllo degli accessi a maglie larghe per GCP e per altre API di Google (Drive, directory di G Suite, ecc.). Qui trovi l'elenco completo.
Agli inizi di Google Cloud non esisteva il sistema IAM flessibile di oggi, capace di gestire permessi a grana fine. Google Cloud aveva solo i ruoli definiti "primitivi" — Viewer, Editor e Owner — e si affidava agli scope per limitare ciò che le VM potevano fare.
Ad esempio, per avviare una VM che dovesse scrivere dati su Cloud Storage ed elencare (cioè "scoprire") altre VM, il piano d'azione sarebbe stato:
- Avviare la VM con un service account dotato del ruolo Editor sul progetto (Viewer non basterebbe, dato che serve scrivere su Cloud Storage)
- Specificare in modo esplicito gli scope all'avvio della VM, ovvero:
compute.readonlyedevstroage.read_write(qualcuno sa perché si chiama *dev*storage?)
Google si è resa conto rapidamente che il controllo degli accessi basato sugli scope era troppo grossolano e così sono entrati in gioco i permessi IAM, rendendo gli scope obsoleti nella maggior parte dei casi. Infatti, oggi quando avvii una VM con un service account personalizzato, gli scope predefiniti si limitano a cloud-platform — leggi "accesso completo a tutto GCP" — ma di fatto non significa nulla, perché l'accesso effettivo è governato dai permessi IAM concessi al service account della VM.
Quando contano davvero gli scope?
Uscendo dall'ecosistema GCP, è probabile che tu ti sia imbattuto negli scope la prima volta che hai provato ad accedere a un servizio Google esterno a GCP. Cioè quando si usano API non coperte dallo scope cloud-platform, ad esempio per leggere un foglio di calcolo dal tuo Google Drive.
Per accedere a Google Drive, le credenziali della tua app devono includere lo scope ``https://www.googleapis.com/auth/drive. Anche in questo caso ti viene concessa solo la possibilità di accedere a Google Drive in linea generale. Perché tutto funzioni davvero, dovrai condividere il foglio di calcolo con l'email del service account della VM.
In sintesi: se usi un'API Google esterna a GCP, devi tenere conto degli scope quando provisioni le credenziali.
Specificare gli scope
Ricapitolando, se sei un Pythonista, di solito scrivi:
import google.authcreds, project = google.auth.default()e funziona e basta. Ma ora ti servono scope aggiuntivi, ed è qui che la faccenda si fa spinosa.
GCE e GKE
Con Compute Engine e Kubernetes Engine sei relativamente fortunato: puoi specificare gli scope durante la creazione della VM o del nodepool, anche se solo tramite gcloud (cerca l'opzione —-scopes). La console web, invece, ti toglie completamente la possibilità di impostare gli scope se usi un tuo service account, oppure ti permette di regolare solo quelli relativi a GCP.
Non è possibile impostare gli scope dalla Web Console
Ecco una breve demo via CLI:
gcloud compute instances create instance-1 ... \ --scopes=cloud-platform,https://www.googleapis.com/auth/drive
gcloud compute ssh instance-1 -- python3>>> import google.auth>>> creds, project = google.auth.default()>>> from google.auth.transport import requests>>> creds.refresh(requests.Request())>>> creds.scopes['\```\\[https://www.googleapis.com/auth/cloud-platform'](https://www.googleapis.com/auth/cloud-platform%27)\\```\, '\```\\[https://www.googleapis.com/auth/drive'](https://www.googleapis.com/auth/drive%27)\\```\]````
Dico "relativamente fortunato" con GCE e GKE perché gli scope si possono definire solo in fase di creazione. Con una singola VM è un problema marginale, ma con GKE può diventare un bel grattacapo: rischi di dover ridistribuire l'intero node pool solo per aggiungere uno scope.
Il risultato è che il tuo codice non cambia, ma devi prevedere in anticipo qualche pratica DevOps.
In ogni caso, esiste un modo per ottenere credenziali con scope _non_ pre-autorizzati alla VM, e lo descrivo verso la fine.
#### Google App Engine
App Engine è uno dei servizi GCP più datati ed è un autentico framework serverless che, sotto molti aspetti, era avanti rispetto al suo tempo. Tuttavia, una grande eredità si porta dietro anche parecchie restrizioni legacy. Una di queste è che tutte le app App Engine hanno gli scope fissati a `cloud-platform` e non c'è una via d'uscita semplice.
Una soluzione alternativa consiste nell'impersonare il service account predefinito di App Engine verso un altro service account, o anche verso sé stesso, richiedendo nuovi scope nel processo. [**Qui**](https://cloud.google.com/iam/docs/understanding-service-accounts#acting_as_a_service_account) trovi la documentazione ufficiale di Google Cloud sull'argomento. Vediamo come si presenta in Python:import google.auth from google.auth import impersonated_credentials as ic
````SCOPES = ["\```\\[https://www.googleapis.com/auth/drive](https://www.googleapis.com/auth/spreadsheets.readonly)\\```\"]svcacc = "test````
[@my-project-id.iam.gserviceaccount.com](mailto:[email protected])"
adc, _ = google.auth.default() creds = ic.Credentials(source_credentials=adc, target_principal=svcacc, target_scopes=SCOPES)
service = build("sheets", "v4", credentials=creds, cache_discovery=False)
Attenzione all'argomento `cache_discovery=False`: è necessario a causa di un [bug](https://github.com/googleapis/google-api-python-client/issues/299). Perché il codice funzioni, devi anche assegnare al service account di destinazione il ruolo Token Creator sul service account predefinito di AppEngine del tuo progetto:gcloud iam service-accounts add-iam-policy-binding
test@{$PRJ_ID}.iam.gserviceaccount.com
--member="serviceAccount:{$PRJ_ID}@
[appspot.gserviceaccount.com](https://console.cloud.google.com/iam-admin/serviceaccounts/details/108759703281763060579?authuser=1&project=zaar-playground)"
--role=roles/iam.serviceAccountTokenCreator
**Attenzione** **a un'importante implicazione di sicurezza di questo approccio** — tutti i servizi App Engine all'interno di un progetto vengono eseguiti con _lo stesso service account_. Di conseguenza _tutti_ potranno effettuare l'impersonificazione, e questo potrebbe rappresentare un rischio.
Purtroppo non c'è una soluzione semplice, ma più avanti in questo post ne propongo una.
**Aggiornamento novembre 2021:** App Engine ora [consente di specificare](https://cloud.google.com/appengine/docs/standard/python3/user-managed-service-accounts) un service account personalizzato per ciascun servizio.
#### Cloud Run / Functions
Quando si distribuisce un servizio Cloud Run o una Cloud Function, degli scope non si parla nemmeno: finalmente! Perché complicarsi la vita quando puoi semplicemente richiedere al volo gli scope che ti servono, in questo modo:
````import google.authSCOPES = ["\```\\[https://www.googleapis.com/auth/drive](https://www.googleapis.com/auth/spreadsheets.readonly)\\```\"]creds, _ = google.auth.default(scopes=SCOPES)````
Poi prosegui come al solito con l'API che preferisci. [Qui](https://medium.com/@guillaume.blaquiere/yes-my-experience-isnt-the-same-63ac3c783864) trovi l'esempio completo.
A mio avviso è così che avrebbero dovuto funzionare anche GKE/GCE/GAE.
Nel caso te lo stessi chiedendo: no, fare lo stesso su GCE/GKE non produce l'effetto sperato. Su GCE/GKE, gli scope richiesti dall'app vengono ignorati e riportati a quelli configurati per la VM o il node pool.
#### **Sviluppo locale**
Quando ho iniziato a lavorare con la Google Directory API sono rimasto sorpreso scoprendo che, per invocare l'API, si **deve** usare un service account GCP e quindi disporre di un progetto GCP corretto con l'API desiderata abilitata.
Poiché le credenziali ADC ottenute nell'ambiente di sviluppo locale (ovvero sul tuo laptop) di norma fanno capo al tuo utente, sono praticamente inutili per lavorare con molte API non-GCP (ad es. Directory API).
L'unica strada è, ancora una volta, l'impersonificazione: il codice della sezione GAE qui sopra funziona qui "così com'è".
## Cosa sono i Subject?
I subject sono spesso un piccolo dettaglio che complica ulteriormente il codice di bootstrap dell'autenticazione. Il punto è che, quando si accede alla _Directory API_ (es. per elencare gli utenti del tuo dominio G Suite), si agisce per conto di un determinato utente della directory, che va specificato come _subject_ della credenziale.
Se ci pensi, in fase di sviluppo locale può diventare un'impersonificazione closed-loop: un `[email protected]` autenticato localmente impersona un service account per dialogare con la Directory API del proprio dominio; e quel service account, a sua volta, agisce per conto di `[email protected]` nella directory.
In ogni caso, dobbiamo specificare questa email "act as" come subject delle credenziali, e il codice di setup dell'autenticazione esplode così:import google.auth import google.auth.iam from google.auth.transport.requests import Request from google.oauth2 import service_account as sa
TOKEN_URI = "
[https://accounts.google.com/o/oauth2/token](https://accounts.google.com/o/oauth2/token)
````"SCOPES = ["\```\\[https://www.googleapis.com/auth/drive](https://www.googleapis.com/auth/spreadsheets.readonly)\\```\"]svcacc = "test````
[@my-project-id.iam.gserviceaccount.com](mailto:[email protected])" subject = "[email protected]"
adc, _ = google.auth.default() signer = google.auth.iam.Signer(Request(), adc, svcacc) creds = sa.Credentials(signer, svcacc, TOKEN_URI, scopes=SCOPES, subject=subject) service = build("sheets", "v4", credentials=creds, cache_discovery=False)
Funziona anche per tutti i casi descritti sopra: basta passare `subject=None` se non utilizzi la Directory API e assicurarsi di assegnare correttamente i ruoli Token Creator.
**Quindi una soluzione ce l'abbiamo, evviva!** Il problema è che, quando porti questo approccio in produzione e aggiungi all'equazione i token di _identità_, ti ritrovi con circa 200 righe di [codice](https://gist.github.com/haizaar/fcf8ee4b98b2452c618582bca632a338) solo per ottenere le credenziali di autenticazione, di cui metà sono commenti: in genere un chiaro segnale di complessità.
## Una strada migliore?
Abbiamo una soluzione che funziona ovunque (a parte il potenziale problema di sicurezza con App Engine), ma adesso sembra contortissima rispetto alla semplice chiamata `google.auth.default()` di prima. C'è un'alternativa?
Ho una proposta che ad alcuni potrà sembrare più lineare. Se non altro, mette una toppa al problema di sicurezza di App Engine legato all'impersonificazione. Eccola:
- Crea un file di chiave per il service account che vuoi impersonare. Sì, il classico, vecchio e "brutto" file di chiave.- Cifra il file con [Mozilla sops](https://github.com/mozilla/sops) + GCP KMS e archivialo insieme al tuo codice.- Concedi al service account operativo il ruolo di decryptor sulla chiave GCP KMS usata per cifrare il file di chiave del service account.
Ora il tuo codice di bootstrap avrà questo aspetto:import json import os import subprocess from google.oauth2 import service_account as sa
path = os.getenv("SERVICE_ACCOUNT_KEY_PATH") json_data: str = subprocess.run(["sops", "-d", path], check=True, capture_output=True, timeout=10).stdout sa_info = json.loads(json_data) creds = sa.Credentials.from_service_account_info(sa_info)
L'oggetto `creds` risultante mette a disposizione due metodi pratici, `with_scopes()` e `with_subject()`, che restituiscono un nuovo oggetto credenziali con scope/subject aggiornati.
Sebbene richieda più configurazione — incluso integrare il binario `sops` da 30MiB nelle Cloud Functions e nelle immagini dei container — il flusso è molto più facile da capire (al contrario dell'approccio precedente).
Questa soluzione risolve anche il problema di App Engine: resta vero che il service account predefinito di App Engine può potenzialmente decifrare tutti i file di chiave, ma servizi GAE distribuiti da repository git differenti non avranno accesso ai rispettivi file di chiave cifrati. Otteniamo così un vero isolamento dei segreti!
#### Tracciabilità dell'autenticazione
Questa è una sezione bonus: grazie per essere arrivato fin qui.
Un bel vantaggio dell'impersonificazione rispetto agli scomodi file di chiave del service account è che puoi risalire al principal originario che ha effettuato l'impersonificazione verso un service account.
Ecco il cheat-sheet di audit:
- [Abilita](https://cloud.google.com/iam/docs/audit-logging#enabling_audit_logging) i log di audit Data Access per il servizio IAM- Esegui un'impersonificazione, ad es. `gcloud --impersonate-service-account=<email> projects list`- Apri [Logs Explorer](https://console.cloud.google.com/logs) e seleziona "Service Account" dal menu a tendina "Resource" e "data\_access" dal menu "Log name".
Oppure, più semplicemente, esegui questa query:resource.type="service_account"
logName="projects/
A seconda di come avviene l'impersonificazione, ti interessano i payload con `methodName` uguale a `SignBlob` o `GenerateAccessToken`:
Audit Log dell'azione di impersonificazione
Ora, con l'approccio chiavi del service account + SOPS, siamo tornati al punto di partenza, o forse no? Alla fine usiamo semplicemente la chiave del service account, quindi non c'è un'indicazione diretta di chi sia l'entità reale dietro l'operazione, _ma_ dobbiamo prima decifrare quel file di chiave JSON con Cloud KMS (cosa che SOPS fa dietro le quinte): ed è qui che lasciamo le briciole di pane.
- Abilita i log di audit Data Access per il servizio Cloud KMS
- Usa SOPS per decifrare il file di chiave JSON
- Ora interrogando con `resource.type="cloudkms_cryptokey" resource.labels.location="global"`, abbiamo i nostri indizi:

In altre parole, possiamo vedere chi ha utilizzato le nostre chiavi per decifrare; e benché da sola non sia una prova schiacciante, mantenere buone convenzioni di denominazione insieme a un approccio "una chiave KMS per ciascun service account" può fornire una correlazione solida, utile per risalire ai sospetti.
## Epilogo
Spero che questo post abbia chiarito perché la tua autenticazione potrebbe iniziare a fare i capricci nel momento in cui esci dal mondo solo-GCP per addentrarti nell'ecosistema, ben più ampio, delle API di Google.
Ricapitolando: gli scope oAuth, pur non rappresentando un vero meccanismo di sicurezza per molti servizi che oggi offrono un IAM completo, restano comunque obbligatori per far funzionare le cose; e a seconda della piattaforma — App Engine, GCE/GKE, ecc. — arrivarci può richiedere un impegno tutt'altro che trascurabile.
**_Zaar Hai è Staff Cloud Architect presso_** [**_DoiT International_**](https://www.doit.com/) **_. Dai un'occhiata alla nostra_** [**_pagina careers_**](http://careers.doit.com/) **_se vuoi lavorare con Zaar e con gli altri Senior Cloud Architects di DoiT International!_**