Cloud Intelligence™Cloud Intelligence™

Cloud Intelligence™

Google Auth — Démystifier la magie

By Zaar HaiNov 15, 202010 min read

Cette page est également disponible en English, Deutsch, Español, Italiano, 日本語 et Português.

1 eufdq5voejcitteb 8cv4w

Ou que faire lorsque les Google Application Default Credentials vous lâchent

L'authentification sur Google Cloud… ça marche tout seul, non ? Vous lancez une nouvelle VM, vous y installez gcloud, et hop, comme par magie : vous accédez à vos buckets Google Cloud Storage.

L'une des grandes promesses de Google Cloud est en effet de simplifier l'authentification. Il suffit d'obtenir ce qu'ils appellent les Application Default Credentials (ADC) pour que tout fonctionne partout : vos identifiants utilisateur sont utilisés sur votre machine de dev, et ceux du compte de service lorsque le code tourne sur Google Cloud. Et ça fonctionne, plutôt bien d'ailleurs.

Tant que vous restez dans Google Cloud sans chercher à accéder à d'autres services Google, les ADC tiennent leurs promesses. Mais deux mots viennent briser ce conte de fées : SCOPE et SUBJECT.

Que sont les scopes ?

Vous connaissez sûrement déjà ce terme. Il s'agit d'URL comme celles ci-dessous, sans véritable site web derrière :)

https://www.googleapis.com/auth/cloud-platform

https://www.googleapis.com/auth/compute.readonly

Ce sont des scopes oAuth 2.0 qui servent à configurer un contrôle d'accès grossier à GCP et aux autres APIs Google (Drive, annuaire G Suite, etc.). Voici la liste complète.

Aux débuts de Google Cloud, nous ne disposions pas du système IAM flexible que nous avons aujourd'hui pour le contrôle fin des permissions. Google Cloud n'offrait que ce qu'il appelle des rôles primitifs — Viewer, Editor et Owner — et s'appuyait sur les scopes pour limiter ce que vos VM pouvaient faire.

Par exemple, pour lancer une VM devant écrire des données dans Cloud Storage et lister (au sens " découvrir ") d'autres VM, votre plan d'action serait :

  • Lancer la VM sous un compte de service ayant le rôle Editor sur votre projet (puisque vous voulez écrire dans Cloud Storage, Viewer ne suffira pas).
  • Spécifier des scopes explicites au lancement de la VM, à savoir compute.readonly et devstroage.read_write (quelqu'un sait pourquoi cela s'appelle *dev*storage ?).

Google a vite compris que le contrôle d'accès basé sur les scopes était trop grossier, et les permissions IAM ont donc pris le dessus, rendant les scopes obsolètes dans la plupart des cas. De fait, lorsque vous lancez aujourd'hui une VM avec un compte de service personnalisé, les scopes se limitent par défaut à cloud-platform — autrement dit " accès complet à tout GCP " — mais cela ne signifie plus grand-chose, puisque l'accès réel est régi par les permissions IAM accordées au compte de service de votre VM.

Quand devez-vous vous soucier des scopes ?

En sortant de l'écosystème GCP, il y a fort à parier que vous avez croisé les scopes la première fois en essayant d'accéder à un service Google qui ne fait pas partie de GCP. C'est-à-dire en utilisant des APIs non couvertes par le scope cloud-platform, par exemple pour lire un tableur depuis votre Google Drive.

Pour accéder à Google Drive, les identifiants de votre application doivent disposer du scope ``https://www.googleapis.com/auth/drive. Là encore, cela ne vous donne qu'une capacité à accéder à Google Drive en général. Pour que ça fonctionne réellement, vous devrez partager votre tableur avec l'adresse e-mail du compte de service de la VM.

Pour résumer : si vous utilisez une API Google qui ne fait pas partie de GCP, vous devez tenir compte des scopes lors du provisionnement des identifiants.

Spécifier des scopes

Pour rappel, si vous êtes Pythonista, vous utilisez généralement :

import google.auth
creds, project = google.auth.default()

et ça marche tout seul. Mais dès qu'il vous faut des scopes supplémentaires, les choses se corsent.

GCE et GKE

Avec Compute Engine et Kubernetes Engine, vous avez relativement de chance : vous pouvez spécifier des scopes lors de la création de la VM ou du nodepool, mais uniquement via gcloud (cherchez l'option —-scopes). La console web vous prive complètement des scopes si vous utilisez votre propre compte de service, ou ne vous laisse ajuster que les scopes liés à GCP dans le cas contraire.

1 wzoav2bqnmwljx5vzyhswqImpossible de définir les scopes dans la console web

Voici une courte démo en ligne de commande :

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)\
\
```\
]
````
Je dis " relativement de chance " avec GCE et GKE car les scopes ne peuvent être spécifiés qu'à la création. C'est un moindre mal pour une VM unique, mais cela peut devenir pénible avec GKE, où vous risquez de devoir redéployer tout le node pool juste pour ajouter un scope.
Au final, votre code ne change pas, mais vous devez anticiper côté DevOps.
Cela dit, il existe un moyen d'obtenir des identifiants avec des scopes qui _n'ont pas été_ autorisés à l'avance pour la VM, que je décris vers la fin.
#### Google App Engine
App Engine est l'un des plus anciens services GCP et un véritable framework serverless qui, à bien des égards, était en avance sur son temps. Mais qui dit grand héritage dit aussi nombreuses contraintes héritées du passé. L'une d'elles : toutes les applications App Engine ont leurs scopes figés à `cloud-platform`, sans moyen simple de s'en affranchir.
Une solution de contournement consiste à faire usurper l'identité du compte de service par défaut d'App Engine vers un autre compte de service, voire vers lui-même, en demandant de nouveaux scopes au passage. [**Voici**](https://cloud.google.com/iam/docs/understanding-service-accounts#acting_as_a_service_account) la documentation officielle de Google Cloud sur le sujet. Voyons ce que cela donne 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)

Attention à l'argument `cache_discovery=False` : il est nécessaire à cause d'un [bug](https://github.com/googleapis/google-api-python-client/issues/299). Pour que ce qui précède fonctionne, vous devez aussi accorder le rôle Token Creator au compte de service par défaut d'AppEngine sur votre compte de service cible :

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

**À noter** **: cette approche soulève une question de sécurité importante** — tous les services App Engine d'un projet donné s'exécutent sous _le même compte de service_. Par conséquent, _tous_ pourront usurper l'identité, ce qui peut poser problème.
Il n'y a malheureusement pas de parade simple, mais je présente une solution plus loin dans cet article.
**Mise à jour de novembre 2021 :** App Engine permet désormais de [spécifier](https://cloud.google.com/appengine/docs/standard/python3/user-managed-service-accounts) un compte de service personnalisé par service.
#### Cloud Run / Functions
Lors du déploiement d'un service Cloud Run ou d'une Cloud Function, plus de question de scopes : enfin ! Pourquoi se compliquer la vie quand on peut tout simplement demander les scopes voulus à la volée, comme ceci :
````
import google.auth
SCOPES = ["\
```\
\
[https://www.googleapis.com/auth/drive](https://www.googleapis.com/auth/spreadsheets.readonly)\
\
```\
"]
creds, _ = google.auth.default(scopes=SCOPES)
````
Procédez ensuite comme d'habitude avec l'API de votre choix. Voir l'exemple complet [ici](https://medium.com/@guillaume.blaquiere/yes-my-experience-isnt-the-same-63ac3c783864).
À mon sens, c'est ainsi que cela aurait dû fonctionner aussi dans GKE/GCE/GAE.
Au cas où vous vous poseriez la question : non, faire la même chose dans GCE/GKE ne donnera pas l'effet escompté. Sur GCE/GKE, les scopes demandés dans votre application sont ignorés et réinitialisés à ceux configurés pour la VM ou le node pool.
#### **Développement local**
Quand j'ai commencé à travailler avec l'API Google Directory, j'ai été surpris de constater que pour invoquer cette API, il **fallait** obligatoirement passer par un compte de service GCP, et donc disposer d'un projet GCP correctement configuré avec l'API souhaitée activée.
Comme les identifiants ADC obtenus dans l'environnement de dev local (sur votre laptop, donc) appartiennent généralement à votre utilisateur, ils sont en pratique inutilisables avec de nombreuses APIs non-GCP (l'API Directory, par exemple).
La seule voie reste, là encore, l'usurpation d'identité ; le code de la section GAE ci-dessus fonctionne ici tel quel.
## Que sont les subjects ?
Les subjects sont souvent un petit détail qui complique encore le code d'amorçage de l'authentification. Le fait est que pour accéder à _Directory API_ (par exemple pour lister les utilisateurs de votre domaine G Suite), vous agissez au nom d'un certain utilisateur de l'annuaire qui doit être spécifié comme _subject_ de l'identifiant.
Du coup, on peut se retrouver dans une usurpation en boucle lors de l'exécution depuis un environnement de dev local : un `[email protected]` authentifié localement usurpe l'identité d'un compte de service afin de dialoguer avec l'API Directory de son propre domaine ; puis ce compte de service agit au nom d'`[email protected]` dans l'annuaire.
Quoi qu'il en soit, il faut spécifier cet e-mail " act as " comme subject des identifiants, ce qui fait exploser notre code de configuration de l'authentification :

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)

Cela fonctionne aussi pour tous les cas décrits plus haut : il suffit de passer `subject=None` si vous n'utilisez pas l'API Directory, et de bien attribuer les rôles Token Creator.
**On a donc une solution, youpi !** Seul bémol : en passant cette approche en production et en y injectant les tokens d'_identité_, on se retrouve avec près de 200 lignes de [code](https://gist.github.com/haizaar/fcf8ee4b98b2452c618582bca632a338) juste pour obtenir des identifiants d'authentification — dont la moitié sont des commentaires, signe habituel de complexité.
## Une meilleure approche ?
Nous avons une solution qui fonctionne partout (mis à part le risque de sécurité potentiel avec App Engine), mais elle paraît désormais bien tarabiscotée comparée au simple appel `google.auth.default()` que nous utilisions auparavant. Existe-t-il une autre voie ?
J'ai une suggestion qui paraîtra peut-être plus simple à certains. Au moins, elle répond au problème de sécurité d'App Engine lié à l'usurpation d'identité. La voici :
- Créer un fichier de clé pour le compte de service dont vous voulez usurper l'identité. Oui, le bon vieux fichier de clé " disgracieux ".
- Chiffrer ce fichier avec [Mozilla sops](https://github.com/mozilla/sops) + GCP KMS et le stocker avec votre code.
- Accorder à votre compte de service opérationnel un rôle de déchiffrement sur la clé GCP KMS utilisée pour chiffrer le fichier de clé du compte de service.
Votre code d'amorçage ressemblera désormais à ceci :

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'objet `creds` obtenu dispose de deux méthodes pratiques, `with_scopes()` et `with_subject()`, qui retournent un nouvel objet d'identifiants avec les scopes/subject mis à jour.
Bien que cela demande davantage de configuration, notamment l'intégration du binaire `sops` de 30 Mio dans vos Cloud Functions et images de conteneurs, le flux est très facile à comprendre (contrairement à l'approche précédente).
Cette solution résout aussi le problème d'App Engine : il reste vrai que le compte de service par défaut d'App Engine peut potentiellement déchiffrer tous les fichiers de clé, mais des services GAE déployés depuis des dépôts git différents n'auront pas accès aux fichiers de clé chiffrés des autres — d'où une vraie isolation des secrets !
#### Traçabilité de l'authentification
Voici une partie bonus — merci d'être arrivé jusqu'ici.
L'un des avantages de l'usurpation d'identité par rapport aux disgracieux fichiers de clé de compte de service, c'est que l'on peut retracer le principal d'origine ayant effectué l'usurpation jusqu'au compte de service.
Voici l'aide-mémoire d'audit :
- [Activez](https://cloud.google.com/iam/docs/audit-logging#enabling_audit_logging) les audit logs Data Access pour le service IAM.
- Effectuez l'usurpation, par exemple `gcloud --impersonate-service-account=<email> projects list`.
- Ouvrez l'[Explorateur de logs](https://console.cloud.google.com/logs) et sélectionnez " Service Account " dans le menu déroulant " Resource " et " data\_access " dans le menu déroulant " Log name ".
Ou saisissez tout simplement la requête ci-dessous :

resource.type="service_account" logName="projects//logs/cloudaudit.googleapis.com%2Fdata_access" protoPayload.methodName=("SignBlob" OR "GenerateAccessToken")


Selon la manière dont vous effectuez l'usurpation, ce sont les payloads dont le `methodName` vaut `SignBlob` ou `GenerateAccessToken` qui vous intéressent :

![1 bv51wsjouk7dpv9q8ytydw](https://media.doit.com/imports/wordpress/2020/11/ec7e59b4de45-1_bv51wsjouk7dpv9q8ytydw.png)Audit Log d'une opération d'usurpation d'identité

Avec l'approche clés de compte de service + SOPS, on revient à la case départ — vraiment ? Au final, on utilise simplement la clé du compte de service, donc aucune indication directe sur l'entité réelle derrière l'opération, _mais_ il faut d'abord déchiffrer ce fichier JSON via Cloud KMS (ce que SOPS fait en coulisses), et c'est là que l'on sème des miettes :

-   Activez les audit logs Data Access pour le service Cloud KMS.
-   Utilisez SOPS pour déchiffrer votre fichier de clé JSON.
-   En interrogeant désormais `resource.type="cloudkms_cryptokey" resource.labels.location="global"`, vous avez vos indices :

![1 uzvfa5k4g0mf1hlwh0ox1w](https://media.doit.com/imports/wordpress/2020/11/cb76aea62858-1_uzvfa5k4g0mf1hlwh0ox1w.png)

Autrement dit, on voit qui a utilisé nos clés pour déchiffrer du contenu ; et même si cette preuve n'est pas suffisamment solide à elle seule, de bonnes conventions de nommage couplées à une approche " une clé KMS par compte de service " offrent une corrélation forte pour identifier des suspects.

## Épilogue

J'espère que cet article aura permis d'éclairer les raisons pour lesquelles votre authentification peut soudain dérailler dès que vous sortez d'un univers strictement GCP pour entrer dans l'écosystème bien plus vaste des APIs Google.

En résumé : les scopes oAuth, bien qu'ils ne constituent plus un véritable mécanisme de sécurité pour les nombreux services désormais dotés d'un vrai IAM, restent indispensables pour faire fonctionner les choses ; et selon votre plateforme — App Engine, GCE/GKE, etc. — y parvenir peut demander un effort conséquent.

**_Zaar Hai est Staff Cloud Architect chez_** [**_DoiT International_**](https://www.doit.com/) **_. Consultez notre_** [**_page carrières_**](http://careers.doit.com/) **_si vous souhaitez travailler avec Zaar et d'autres Senior Cloud Architects chez DoiT International !_**