
O qué hacer cuando las Application Default Credentials de Google te fallan
La autenticación en Google Cloud… simplemente funciona, ¿no? Levantas una VM nueva, le instalas gcloud y, ¡listo!, ya puedes acceder a tus buckets de Google Cloud Storage.
Una de las grandes promesas de Google Cloud es justamente esa: simplificar la autenticación. Basta con obtener las llamadas "Application Default Credentials" (ADC) y funcionarán en todas partes; es decir, usarán tus credenciales de usuario en tu máquina de desarrollo y las credenciales de la cuenta de servicio cuando corras en Google Cloud. Y la verdad es que funcionan muy bien.
Mientras te quedes dentro de Google Cloud y no intentes acceder a otros servicios de Google, las ADC cumplen su promesa. Pero hay dos palabras que rompen este cuento de hadas: SCOPE y SUBJECT.
¿Qué son los Scopes?
Seguro ya escuchaste este término antes. Son URLs como las de abajo, sin un sitio web real detrás :)
https://www.googleapis.com/auth/cloud-platform
https://www.googleapis.com/auth/compute.readonlySon scopes de oAuth 2.0 que sirven para configurar un control de acceso de grano grueso a GCP y a otras APIs de Google (Drive, directorio de G Suite, etc.). Aquí está la lista completa.
En los primeros días de Google Cloud no existía el sistema flexible de IAM que tenemos hoy para el control granular de permisos. Google Cloud solo contaba con los llamados roles "primitivos" (Viewer, Editor y Owner) y se apoyaba en los scopes para limitar lo que tus VMs podían hacer.
Por ejemplo, si querías levantar una VM que necesitara escribir datos en Cloud Storage y listar ("descubrir") otras VMs, el plan de acción era el siguiente:
- Levantar la VM bajo una cuenta de servicio con rol Editor en tu proyecto (como quieres escribir en Cloud Storage, Viewer no alcanza).
- Especificar scopes explícitos al levantar la VM, en este caso:
compute.readonlyydevstroage.read_write(¿alguien sabe por qué se llama *dev*storage?).
Google se dio cuenta rápidamente de que el control de acceso basado en scopes era demasiado tosco, así que los permisos de IAM tomaron protagonismo y dejaron a los scopes obsoletos en la mayoría de los casos. De hecho, hoy cuando levantas una VM con una cuenta de servicio personalizada, los scopes por defecto son simplemente cloud-platform —léase "acceso completo a todo GCP"—, pero eso por sí solo no significa nada, porque el acceso real lo controlan los permisos de IAM otorgados a la cuenta de servicio de la VM.
¿Cuándo te deberían importar los scopes?
Saliendo del ecosistema de GCP, lo más probable es que te hayas topado con los scopes la primera vez que intentaste acceder a un servicio de Google que no forma parte de GCP. Es decir, al usar APIs no cubiertas por el scope cloud-platform, como cuando quieres leer una hoja de cálculo de tu Google Drive.
Para acceder a Google Drive, las credenciales de tu app necesitan tener el scope ``https://www.googleapis.com/auth/drive. Una vez más, eso solo te dará la capacidad de acceder a Google Drive en general. Para que la cosa funcione de verdad, tendrás que compartir tu hoja de cálculo con el correo de la cuenta de servicio de la VM.
En resumen: si usas una API de Google que no es parte de GCP, hay que tener en cuenta los scopes al aprovisionar las credenciales.
Cómo especificar scopes
Para recapitular: si eres pythonista, lo normal es usar:
import google.authcreds, project = google.auth.default()y simplemente funciona. Pero ahora necesitas scopes adicionales, y aquí es donde la cosa se pone peluda.
GCE y GKE
Con Compute Engine y Kubernetes Engine tienes relativa suerte: puedes especificar scopes durante la creación de la VM o el nodepool, aunque solo usando gcloud (busca la opción —-scopes). La consola web te privará por completo de los scopes si usas tu propia cuenta de servicio, o solo te permitirá ajustar los scopes relacionados con GCP en otros casos.
No es posible configurar scopes desde la consola web
Aquí va una breve demo desde la 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)\\```\]````
Digo "relativa suerte" con GCE y GKE porque los scopes solo pueden especificarse al momento de la creación. Es un problema menor con una sola VM, pero puede volverse un dolor de cabeza con GKE, donde podrías terminar redesplegando todo el node pool solo para agregar un scope.
El resultado final es que tu código no cambia, pero tienes que aplicar algunas prácticas de DevOps por adelantado.
De todos modos, existe una forma de obtener credenciales con scopes que _no están_ autorizados de antemano para la VM, y la describo más adelante.
#### Google App Engine
App Engine es uno de los servicios más antiguos de GCP y un verdadero framework serverless que en muchos aspectos se adelantó a su época. Sin embargo, tanta historia trae consigo bastantes restricciones heredadas. Una de ellas es que todas las apps de App Engine tienen sus scopes fijados solo en `cloud-platform`, y no hay forma fácil de cambiarlo.
Una solución alterna sería que la cuenta de servicio por defecto de App Engine se haga pasar (impersonate) por otra cuenta de servicio, o incluso por sí misma, solicitando nuevos scopes en el camino. [**Aquí**](https://cloud.google.com/iam/docs/understanding-service-accounts#acting_as_a_service_account) está la documentación oficial de Google Cloud sobre el tema. Veamos cómo se ve en 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)
Pon atención al argumento `cache_discovery=False`: es necesario por un [bug](https://github.com/googleapis/google-api-python-client/issues/299). Para que lo anterior funcione también debes otorgarle a tu cuenta de servicio destino el rol Token Creator sobre la cuenta de servicio por defecto de AppEngine de tu proyecto: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
**Ten en cuenta** **que este enfoque tiene una consideración de seguridad importante**: todos los servicios de App Engine en un proyecto dado se ejecutan bajo _la misma cuenta de servicio_. Por lo tanto, _todos ellos_ podrán hacer impersonation, lo que puede convertirse en un problema de seguridad.
Lamentablemente, no hay forma fácil de evitarlo, pero más adelante en el post propongo una solución.
**Actualización de noviembre de 2021:** App Engine ahora [permite especificar](https://cloud.google.com/appengine/docs/standard/python3/user-managed-service-accounts) una cuenta de servicio personalizada por servicio.
#### Cloud Run / Functions
Al desplegar un servicio de Cloud Run o una Cloud Function no se mencionan los scopes —¡por fin!—. ¿Para qué complicarse si puedes solicitar los scopes que necesitas al vuelo, así:
````import google.authSCOPES = ["\```\\[https://www.googleapis.com/auth/drive](https://www.googleapis.com/auth/spreadsheets.readonly)\\```\"]creds, _ = google.auth.default(scopes=SCOPES)````
Después continúa como siempre con la API que prefieras. Mira [aquí](https://medium.com/@guillaume.blaquiere/yes-my-experience-isnt-the-same-63ac3c783864) el ejemplo completo.
Para mí, así es como debería haber funcionado también en GKE/GCE/GAE.
Por si te lo preguntas: no, hacer lo mismo en GCE/GKE no produce el efecto deseado. En GCE/GKE, los scopes que solicitas en tu app se ignoran y se restablecen a los configurados para la VM o el node pool.
#### **Desarrollo local**
Cuando empecé a trabajar con la API de Google Directory me sorprendió que para invocarla uno **tiene** que usar una cuenta de servicio de GCP y, en consecuencia, contar con un proyecto GCP en regla con la API correspondiente habilitada.
Como las credenciales ADC obtenidas en el entorno de desarrollo local (en tu laptop, vamos) suelen pertenecer a tu usuario, son prácticamente inútiles para trabajar con muchas APIs que no son de GCP (por ejemplo, la Directory API).
La única vía es, otra vez, hacer impersonation, y el código de la sección de GAE de arriba funciona aquí "tal cual".
## ¿Qué son los Subjects?
Los subjects suelen ser un pequeño detalle que complica todavía más el código de bootstrap de autenticación. Lo que pasa es que, al acceder a la _Directory API_ (por ejemplo, para listar los usuarios de tu dominio de G Suite), actúas en nombre de un usuario concreto del directorio, que debe especificarse como el _subject_ de la credencial.
Visto así, en realidad puede convertirse en un closed-loop de impersonation cuando se ejecuta desde un dev local: un `[email protected]` autenticado localmente se hace pasar por una cuenta de servicio para hablar con la Directory API de su propio dominio; y luego esa cuenta de servicio actúa en nombre de `[email protected]` dentro del directorio.
En cualquier caso, hay que especificar este correo "act as" como subject de las credenciales, lo que hace que nuestro código de configuración de auth se infle así: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)
Funciona también para todos los casos descritos arriba: solo pasa `subject=None` si no usas la Directory API y asegúrate de asignar bien los roles de Token Creator.
**¡Tenemos una solución, genial!** Lo único es que cuando llevas este enfoque a producción y le sumas tokens de _identidad_, terminan siendo unas ~200 líneas de [código](https://gist.github.com/haizaar/fcf8ee4b98b2452c618582bca632a338) solo para obtener credenciales de autenticación, la mitad de las cuales son comentarios, lo que suele ser señal de complejidad.
## ¿Hay una mejor manera?
Tenemos una solución que funciona en todas partes (salvo por el posible problema de seguridad con App Engine), pero ahora se siente bastante enrevesada en comparación con la simple llamada `google.auth.default()` que usábamos antes. ¿Existe otro camino?
Tengo una propuesta que a algunos les puede sonar más simple. Al menos resuelve el problema de seguridad de App Engine con el impersonation. Aquí va:
- Crea un archivo de clave para la cuenta de servicio que quieres impersonar. Sí, el archivo de clave de toda la vida, simple y "feo".- Cifra el archivo con [Mozilla sops](https://github.com/mozilla/sops) + GCP KMS y guárdalo como parte de tu código.- Otorga a tu cuenta de servicio operativa el rol de descifrado (decryptor) sobre la clave de GCP KMS que usaste para cifrar el archivo de clave de la cuenta de servicio.
Ahora tu código de bootstrap se verá así: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)
El objeto `creds` resultante tiene dos métodos muy prácticos, `with_scopes()` y `with_subject()`, que devuelven un nuevo objeto de credenciales con los scopes y el subject actualizados.
Si bien esto requiere más configuración —incluyendo incorporar el binario `sops` de 30 MiB en tus Cloud Functions e imágenes de contenedor—, es muy fácil entender el flujo (a diferencia del enfoque anterior).
Esta solución también resuelve el tema con App Engine: sigue siendo cierto que la cuenta de servicio por defecto de App Engine podría descifrar todos los archivos de clave, pero distintos servicios de GAE desplegados desde distintos repos de git no tendrán acceso a los archivos de clave cifrados de los demás. ¡Así que aquí sí hay aislamiento de secretos!
#### Trazabilidad de la autenticación
Esta es una sección extra: gracias por llegar hasta aquí.
Una ventaja del impersonation frente a los feos archivos de clave de cuenta de servicio es que puedes rastrear hasta una cuenta de servicio al principal original que hizo el impersonation.
Aquí va el cheat-sheet de auditoría:
- [Habilita](https://cloud.google.com/iam/docs/audit-logging#enabling_audit_logging) los logs de auditoría de Data Access para el servicio IAM.- Haz impersonation, por ejemplo: `gcloud --impersonate-service-account=<email> projects list`.- Abre [Logs Explorer](https://console.cloud.google.com/logs) y selecciona "Service Account" en el desplegable "Resource" y "data\_access" en el desplegable "Log name".
O simplemente ingresa la siguiente consulta:resource.type="service_account"
logName="projects/
Según cómo realices el impersonation, los payloads con `methodName` igual a `SignBlob` y `GenerateAccessToken` son los que estás buscando:
Log de auditoría del acto de impersonation
Ahora bien, con el enfoque de claves de cuenta de servicio + SOPS, ¿volvemos al punto de partida? No tan rápido. Al final solo usamos la clave de la cuenta de servicio, así que no hay un indicador directo de quién es la entidad real detrás de la operación, _pero_ primero hay que descifrar ese archivo JSON de clave usando Cloud KMS (lo que SOPS hace tras bambalinas), y ahí es donde dejamos las migajas:
- Habilita los logs de auditoría de Data Access para el servicio Cloud KMS.
- Usa SOPS para descifrar tu archivo JSON de clave.
- Ahora consulta por `resource.type="cloudkms_cryptokey" resource.labels.location="global"` y tendrás tus pistas:

Es decir, podemos ver quién usó nuestras claves para descifrar cosas; y aunque por sí solo no sea evidencia lo bastante sólida, mantener buenas convenciones de nombres junto con un enfoque de "una clave KMS por cada cuenta de servicio" puede aportar una correlación fuerte para rastrear sospechosos.
## Epílogo
Espero que este post haya servido para arrojar luz sobre por qué tu autenticación puede empezar a fallar en el momento en que sales de un mundo solo-GCP hacia el ecosistema mucho más amplio de las APIs de Google.
Para recapitular: los scopes de oAuth, aunque ya no son un mecanismo de seguridad real para muchos servicios que hoy ofrecen un IAM en condiciones, siguen siendo obligatorios para que las cosas funcionen; y según la plataforma —App Engine, GCE/GKE, etc.—, llegar hasta ahí puede requerir un esfuerzo considerable.
**_Zaar Hai es Staff Cloud Architect en_** [**_DoIT International_**](https://www.doit.com/) **_. ¡Visita nuestra_** [**_página de carreras_**](http://careers.doit.com/) **_si te gustaría trabajar con Zaar y otros Senior Cloud Architects en DoiT International!_**