Cloud Intelligence™Cloud Intelligence™

Cloud Intelligence™

Google Auth — Der Magie auf den Grund gehen

By Zaar HaiNov 15, 202010 min read

Diese Seite ist auch in English, Español, Français, Italiano, 日本語 und Português verfügbar.

1 eufdq5voejcitteb 8cv4w

Oder was tun, wenn die Google Application Default Credentials streiken

Authentifizierung in Google Cloud … läuft einfach, oder? Sie ziehen eine neue VM hoch, installieren gcloud – und zack: Zugriff auf Ihre Google Cloud Storage Buckets.

Eines der großen Versprechen von Google Cloud ist tatsächlich, Authentifizierung simpel zu halten. Sie holen sich die sogenannten "Application Default Credentials" (ADC), und schon klappt es überall – auf Ihrer Entwicklermaschine mit Ihren Nutzer-Credentials, in Google Cloud mit den Service-Account-Credentials. Und das funktioniert wirklich gut.

Solange Sie sich innerhalb von Google Cloud bewegen und nicht auf andere Google-Dienste zugreifen wollen, hält ADC sein Versprechen. Doch zwei Wörter zerstören dieses Märchen: SCOPE und SUBJECT.

Was sind Scopes?

Den Begriff haben Sie sicher schon gehört. Es sind URLs wie diese – ohne tatsächliche Website dahinter :)

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

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

Das sind oAuth-2.0-Scopes, mit denen sich grobe Zugriffskontrollen für GCP und andere Google-APIs (Drive, G Suite Directory etc.) konfigurieren lassen. Die vollständige Liste finden Sie hier.

In den Anfangstagen von Google Cloud gab es das flexible IAM-System, das wir heute für die feingranulare Berechtigungsverwaltung kennen, noch nicht. Google Cloud hatte nur die sogenannten "primitiven" Rollen Viewer, Editor und Owner und nutzte Scopes, um zu begrenzen, was Ihre VMs tun durften.

Wollten Sie etwa eine VM starten, die Daten nach Cloud Storage schreiben und andere VMs auflisten (also "entdecken") sollte, sah Ihr Vorgehen so aus:

  • VM unter einem Service Account mit Editor-Rolle im Projekt starten (denn um nach Cloud Storage zu schreiben, reicht Viewer nicht aus)
  • Beim Start explizite Scopes angeben: compute.readonly und devstroage.read_write (Weiß eigentlich jemand, warum es *dev*storage heißt?)

Google merkte schnell, dass scope-basierte Zugriffskontrolle zu grobschlächtig ist – und so übernahmen IAM-Berechtigungen das Ruder und machten Scopes in den meisten Fällen überflüssig. Wenn Sie heute eine VM mit einem eigenen Service Account starten, ist der Standard-Scope schlicht cloud-platform – sprich "voller Zugriff auf alles in GCP". Das hat zunächst aber keine Auswirkung, denn der tatsächliche Zugriff wird über die IAM-Berechtigungen des VM-Service-Accounts gesteuert.

Wann sind Scopes für Sie relevant?

Außerhalb des GCP-Ökosystems begegnen Ihnen Scopes meist dann, wenn Sie zum ersten Mal auf einen Google-Dienst zugreifen wollen, der nicht zu GCP gehört – also APIs, die der cloud-platform-Scope nicht abdeckt, etwa wenn Sie ein Spreadsheet aus Ihrem Google Drive auslesen möchten.

Für den Zugriff auf Google Drive brauchen Ihre App-Credentials den Scope ``https://www.googleapis.com/auth/drive. Auch das verschafft Ihnen lediglich die generelle Möglichkeit, auf Google Drive zuzugreifen. Damit es tatsächlich klappt, müssen Sie Ihr Spreadsheet zusätzlich mit der E-Mail-Adresse des VM-Service-Accounts teilen.

Kurz gesagt: Wenn Sie eine Google-API nutzen, die nicht zu GCP gehört, müssen Sie Scopes beim Provisionieren der Credentials mitdenken.

Scopes spezifizieren

Kurzer Rückblick: Wer in Python unterwegs ist, nutzt typischerweise:

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

Und es funktioniert einfach. Doch jetzt brauchen Sie zusätzliche Scopes – und genau hier wird es haarig.

GCE und GKE

Mit Compute Engine und Kubernetes Engine haben Sie es vergleichsweise leicht – Sie können Scopes beim Anlegen einer VM oder eines Nodepools angeben, allerdings ausschließlich über gcloud (siehe Option —-scopes). Die Web-Konsole nimmt Ihnen die Scope-Konfiguration entweder ganz aus der Hand, wenn Sie einen eigenen Service Account verwenden, oder erlaubt nur Anpassungen an GCP-bezogenen Scopes.

1 wzoav2bqnmwljx5vzyhswqScopes lassen sich in der Web-Konsole nicht setzen

Hier eine kurze CLI-Demo:

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)\
\
```\
]
````
Ich sage "vergleichsweise leicht", weil Scopes bei GCE und GKE nur beim Anlegen festgelegt werden können. Bei einer einzelnen VM ist das verschmerzbar, bei GKE jedoch lästig – möglicherweise müssen Sie den gesamten Node Pool neu deployen, nur um einen Scope hinzuzufügen.
Unterm Strich ändert sich Ihr Code nicht, aber Sie müssen vorab etwas DevOps-Vorarbeit leisten.
Es gibt übrigens einen Weg, Credentials mit Scopes zu erhalten, die der VM _nicht_ vorab freigegeben wurden – darauf gehe ich weiter unten ein.
#### Google App Engine
App Engine zählt zu den ältesten GCP-Diensten und ist ein echtes Serverless-Framework, das in vielerlei Hinsicht seiner Zeit voraus war. Doch mit langer Tradition kommen auch Altlasten. Eine davon: Alle App-Engine-Apps sind auf den Scope `cloud-platform` festgelegt – und es gibt keinen einfachen Ausweg.
Ein Workaround besteht darin, den Standard-Service-Account von App Engine einen anderen Service Account impersonieren zu lassen – oder sogar sich selbst, um dabei neue Scopes anzufordern. Die offizielle Google-Cloud-Doku dazu finden Sie [**hier**](https://cloud.google.com/iam/docs/understanding-service-accounts#acting_as_a_service_account). Schauen wir uns das in Python an:

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)

Achten Sie auf das Argument `cache_discovery=False` – es ist wegen eines [Bugs](https://github.com/googleapis/google-api-python-client/issues/299) nötig. Damit das Ganze funktioniert, müssen Sie außerdem dem Standard-AppEngine-Service-Account in Ihrem Projekt die Token-Creator-Rolle für Ihren Ziel-Service-Account erteilen:

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

**Wichtig:** **Bei diesem Ansatz gibt es einen relevanten Sicherheitsaspekt** – alle App-Engine-Services in einem Projekt laufen unter _demselben Service Account_. Damit können _alle_ impersonieren, was ein Sicherheitsrisiko sein kann.
Einen einfachen Ausweg gibt es leider nicht – aber weiter unten im Beitrag stelle ich eine Lösung vor.
**Update November 2021:** App Engine [erlaubt mittlerweile](https://cloud.google.com/appengine/docs/standard/python3/user-managed-service-accounts), pro Service einen eigenen Service Account zu vergeben.
#### Cloud Run / Functions
Beim Deployment eines Cloud-Run-Service oder einer Cloud Function ist von Scopes keine Rede mehr – endlich! Warum sich auch belasten, wenn man die nötigen Scopes einfach zur Laufzeit anfordern kann:
````
import google.auth
SCOPES = ["\
```\
\
[https://www.googleapis.com/auth/drive](https://www.googleapis.com/auth/spreadsheets.readonly)\
\
```\
"]
creds, _ = google.auth.default(scopes=SCOPES)
````
Anschließend wie gewohnt mit der API Ihrer Wahl weiterarbeiten. Das vollständige Beispiel finden Sie [hier](https://medium.com/@guillaume.blaquiere/yes-my-experience-isnt-the-same-63ac3c783864).
Meiner Meinung nach hätte es in GKE/GCE/GAE eigentlich genauso funktionieren sollen.
Falls Sie sich fragen: Nein, dasselbe in GCE/GKE bringt nicht das gewünschte Ergebnis. Auf GCE/GKE werden die in Ihrer App angeforderten Scopes ignoriert und auf die für VM/Node Pool konfigurierten zurückgesetzt.
#### **Lokale Entwicklung**
Als ich anfing, mit der Google Directory API zu arbeiten, war ich überrascht: Um die API aufzurufen, **muss** man einen GCP-Service-Account verwenden – und folglich ein passendes GCP-Projekt mit aktivierter API.
Da ADC-Credentials in der lokalen Entwicklungsumgebung (also auf Ihrem Laptop) üblicherweise zu Ihrem Nutzer gehören, sind sie für viele Nicht-GCP-APIs (etwa die Directory API) praktisch nutzlos.
Auch hier hilft nur Impersonation – und der Code aus dem GAE-Abschnitt oben funktioniert dort "so wie er ist".
## Was sind Subjects?
Subjects sind oft genau jenes kleine Detail, das den Authentifizierungs-Bootstrap-Code zusätzlich verkompliziert. Der Punkt ist: Beim Zugriff auf die _Directory API_ (etwa um die Nutzer Ihrer G-Suite-Domain aufzulisten) handeln Sie im Namen eines bestimmten Nutzers im Verzeichnis, der als _Subject_ der Credentials angegeben werden muss.
Das kann in der lokalen Entwicklung sogar zu einer closed-loop-Impersonation führen: Ein lokal authentifizierter `[email protected]` impersoniert einen Service Account, um mit der Directory API der eigenen Domain zu sprechen – und dieser Service Account handelt dann seinerseits im Namen von `[email protected]` im Verzeichnis.
Wie auch immer: Wir müssen diese "Act-as"-E-Mail als Subject der Credentials angeben, was unseren Auth-Setup-Code aufbläht zu:

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)

Das funktioniert auch für alle oben beschriebenen Fälle – einfach `subject=None` übergeben, wenn Sie die Directory API nicht nutzen, und sicherstellen, dass die Token-Creator-Rollen sauber vergeben sind.
**Damit haben wir eine Lösung – juhu!** Nur dass dieser Ansatz, sobald man ihn produktionsreif macht und _Identity_-Tokens ins Spiel kommen, in rund 200 Zeilen [Code](https://gist.github.com/haizaar/fcf8ee4b98b2452c618582bca632a338) mündet – allein für den Erhalt der Authentifizierungs-Credentials. Die Hälfte davon sind Kommentare, was meist ein Indikator für Komplexität ist.
## Geht es auch besser?
Wir haben eine Lösung, die überall funktioniert (abgesehen vom potenziellen Sicherheitsproblem bei App Engine), aber sie wirkt verglichen mit dem schlichten `google.auth.default()`-Aufruf von vorhin reichlich verworren. Geht es auch anders?
Ich habe einen Vorschlag, der für manche einfacher klingen dürfte. Zumindest entschärft er das App-Engine-Sicherheitsproblem mit Impersonation. Hier ist er:
- Erstellen Sie eine Key-Datei für den Service Account, den Sie impersonieren möchten. Ja, die schlichte, alte und "hässliche" Key-Datei.
- Verschlüsseln Sie die Datei mit [Mozilla sops](https://github.com/mozilla/sops) + GCP KMS und legen Sie sie als Teil Ihres Codes ab.
- Geben Sie Ihrem operativen Service Account die Decryptor-Rolle für den GCP-KMS-Key, mit dem Sie die Service-Account-Key-Datei verschlüsselt haben.
Ihr Bootstrap-Code sieht dann so aus:

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)

Das resultierende `creds`-Objekt hat zwei praktische Methoden – `with_scopes()` und `with_subject()` –, die ein neues Credentials-Objekt mit aktualisierten Scopes/Subject zurückgeben.
Das erfordert zwar mehr Setup-Aufwand, einschließlich der Einbindung der 30 MiB großen `sops`-Binary in Ihre Cloud Functions und Container-Images, aber der Ablauf ist sehr leicht zu durchschauen (anders als beim vorherigen Ansatz).
Diese Lösung räumt auch das App-Engine-Problem aus dem Weg: Zwar kann der Standard-Service-Account von App Engine theoretisch alle Key-Dateien entschlüsseln, doch unterschiedliche GAE-Services, die aus verschiedenen Git-Repositories deployt werden, haben keinen Zugriff auf die jeweils anderen verschlüsselten Service-Account-Key-Dateien – wir haben also Secret-Isolation!
#### Auth-Nachvollziehbarkeit
Das ist ein Bonusteil – danke, dass Sie bis hierhin durchgehalten haben.
Ein netter Vorteil von Impersonation gegenüber den hässlichen Service-Account-Key-Dateien: Sie können den ursprünglichen Principal nachverfolgen, der die Impersonation eines Service Accounts vorgenommen hat.
Hier der Audit-Spickzettel:
- Data-Access-Audit-Logs für den IAM-Service [aktivieren](https://cloud.google.com/iam/docs/audit-logging#enabling_audit_logging)
- Impersonieren, z. B. `gcloud --impersonate-service-account=<email> projects list`
- [Logs Explorer](https://console.cloud.google.com/logs) öffnen, im Dropdown "Resource" "Service Account" auswählen und im Dropdown "Log name" "data\_access".
Oder schlicht folgende Query eingeben:

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


Je nachdem, wie Sie die Impersonation umsetzen, sind Payloads mit `methodName` gleich `SignBlob` oder `GenerateAccessToken` das, wonach Sie suchen:

![1 bv51wsjouk7dpv9q8ytydw](https://media.doit.com/imports/wordpress/2020/11/ec7e59b4de45-1_bv51wsjouk7dpv9q8ytydw.png)Audit-Log eines Impersonation-Vorgangs

Mit dem Service-Account-Keys-+-SOPS-Ansatz sind wir nun wieder am Anfang – oder doch nicht? Letztlich verwenden wir nur den Service-Account-Key, also gibt es keinen direkten Hinweis auf die tatsächliche Entität hinter der Operation – _aber_ wir müssen die JSON-Key-Datei zuerst mit Cloud KMS entschlüsseln (was SOPS hinter den Kulissen erledigt), und genau dort hinterlassen wir Brotkrumen:

-   Data-Access-Audit-Logs für den Cloud-KMS-Service aktivieren
-   SOPS verwenden, um die JSON-Key-Datei zu entschlüsseln
-   Mit der Query `resource.type="cloudkms_cryptokey" resource.labels.location="global"` haben wir unsere Spuren:

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

Damit sehen wir, wer unsere Keys zum Entschlüsseln verwendet hat. Allein ist das zwar kein starkes Indiz, doch saubere Namenskonventionen kombiniert mit dem Prinzip "ein KMS-Key pro Service Account" liefern eine starke Korrelation für die Spurensuche.

## Epilog

Ich hoffe, dieser Beitrag konnte etwas Licht darauf werfen, warum Ihre Authentifizierung möglicherweise streikt, sobald Sie die reine GCP-Welt verlassen und in das deutlich breitere Google-APIs-Ökosystem eintauchen.

Zusammengefasst: oAuth-Scopes sind zwar für viele Dienste, die heute ein vollwertiges IAM bieten, kein echter Sicherheitsmechanismus mehr – aber sie sind nach wie vor zwingend erforderlich, damit alles läuft. Und je nach Plattform – App Engine, GCE/GKE etc. – kann der Aufwand erheblich sein, dorthin zu kommen.

**_Zaar Hai ist Staff Cloud Architect bei_** [**_DoiT International_**](https://www.doit.com/)**_. Schauen Sie auf unserer_** [**_Karriereseite_**](http://careers.doit.com/) **_vorbei, wenn Sie mit Zaar und anderen Senior Cloud Architects bei DoiT International arbeiten möchten!_**