
Ou o que fazer quando o Application Default Credentials do Google te deixa na mão
Autenticação no Google Cloud… simplesmente funciona, né? Você sobe uma VM nova, instala o gcloud nela e, mágica! — já consegue acessar seus buckets do Google Cloud Storage.
Uma das grandes promessas do Google Cloud é justamente tornar a autenticação simples. Basta obter o que eles chamam de "Application Default Credentials" (ADC) e funciona em todo lugar — ou seja, usa as credenciais do seu usuário na máquina de desenvolvimento e as credenciais da service account quando rodando no Google Cloud. E funciona muito bem nesse cenário.
Enquanto você ficar dentro do Google Cloud e não tentar acessar outros serviços do Google, o ADC cumpre o que promete. Mas duas palavrinhas quebram esse conto de fadas: SCOPE e SUBJECT.
O que são Scopes?
Tenho certeza de que você já ouviu esse termo. São URLs como as abaixo, sem nenhum site real por trás :)
https://www.googleapis.com/auth/cloud-platform
https://www.googleapis.com/auth/compute.readonlySão scopes do oAuth 2.0 que servem para configurar um controle de acesso mais amplo ao GCP e a outras APIs do Google (Drive, diretório do G Suite, etc.). Aqui está a lista completa.
Nos primórdios do Google Cloud, não tínhamos o sistema flexível de IAM que existe hoje para controle granular de permissões. O Google Cloud só tinha o que chamavam de roles "primitivas" — Viewer, Editor e Owner — e dependia dos scopes para limitar o que suas VMs podiam fazer.
Por exemplo, se você quisesse subir uma VM que precisava gravar dados no Cloud Storage e listar (no sentido de "descobrir") outras VMs, seu plano de ação seria:
- Subir a VM com uma service account que tivesse a role de Editor no projeto (já que você quer gravar no Cloud Storage, Viewer não dá conta)
- Especificar scopes explícitos ao subir a VM, no caso:
compute.readonlyedevstroage.read_write(Alguém sabe por que isso se chama *dev*storage?)
O Google rapidamente percebeu que o controle de acesso baseado em scopes era amplo demais e, por isso, as permissões de IAM passaram a reinar, tornando os scopes obsoletos na maioria dos casos. Na verdade, quando você sobe uma VM hoje com uma service account customizada, os scopes vêm por padrão apenas como cloud-platform — leia-se "acesso completo a todo o GCP" —, mas isso por si só não significa nada, porque o acesso real é controlado pelas permissões de IAM concedidas à service account da VM.
Quando os scopes vão te interessar?
Saindo do ecossistema do GCP, é provável que você tenha esbarrado em scopes na primeira vez que tentou acessar um serviço do Google que não faz parte do GCP. Ou seja, ao usar APIs que não são cobertas pelo scope cloud-platform, como quando você quer ler uma planilha do seu Google Drive.
Para acessar o Google Drive, as credenciais do seu app precisam ter o scope ``https://www.googleapis.com/auth/drive. De novo, isso te dá apenas a capacidade de acessar o Google Drive de forma geral. Para a coisa funcionar de fato, você vai precisar compartilhar a planilha com o e-mail da service account da VM.
Recapitulando — se você usa uma API do Google que não faz parte do GCP, precisa considerar os scopes ao provisionar credenciais.
Especificando scopes
Recapitulando, se você é Pythonista, normalmente usa:
import google.authcreds, project = google.auth.default()e simplesmente funciona. Mas agora você precisa de scopes adicionais e é aí que a coisa complica.
GCE e GKE
Com o Compute Engine e o Kubernetes Engine, você tem certa sorte — dá para especificar scopes durante a criação da VM/nodepool, mas somente via gcloud (procure pela opção —-scopes). O console web ou vai te tirar totalmente os scopes se você usar sua própria service account, ou só vai permitir ajustar os scopes relacionados ao GCP nos demais casos.
Não dá para definir scopes no Console Web
Aqui vai uma demo rápida pela 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 que você tem "certa sorte" com GCE e GKE porque os scopes só podem ser definidos na criação. Em uma única VM, isso é menos problemático, mas pode virar uma dor de cabeça com o GKE, em que você pode acabar tendo que recriar todo o node pool só para adicionar um scope.
No fim das contas, seu código não muda, mas você precisa aplicar algumas práticas de DevOps com antecedência.
De qualquer forma, existe um jeito de obter credenciais com scopes que _não foram_ previamente autorizados na VM, que descrevo no final.
#### Google App Engine
O App Engine é um dos serviços mais antigos do GCP e é um framework verdadeiramente serverless que, em muitos aspectos, estava à frente do seu tempo. Mas, com tanta bagagem, vêm também várias restrições legadas. Uma delas é que todos os apps do App Engine têm seus scopes fixados apenas em `cloud-platform` e não há saída fácil.
Uma alternativa é fazer a service account padrão do App Engine impersonar outra service account, ou até ela mesma, solicitando novos scopes no caminho. [**Aqui**](https://cloud.google.com/iam/docs/understanding-service-accounts#acting_as_a_service_account) está a documentação oficial do Google Cloud sobre o assunto. Vamos ver como fica em 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)
Atenção ao argumento `cache_discovery=False` — ele é necessário por causa de um [bug](https://github.com/googleapis/google-api-python-client/issues/299). Para o trecho acima funcionar, você também precisa conceder a role de Token Creator à service account padrão do AppEngine no seu projeto, apontando para a service account de destino: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
**Atenção: existe uma consideração importante de segurança nessa abordagem** — todos os serviços do App Engine de um mesmo projeto rodam sob _a mesma service account_. Logo, _todos eles_ poderão fazer impersonation, o que pode ser uma preocupação de segurança.
Não há um jeito fácil de contornar isso, infelizmente, mas apresento uma solução mais adiante neste post.
**Atualização de novembro de 2021:** o App Engine agora [permite especificar](https://cloud.google.com/appengine/docs/standard/python3/user-managed-service-accounts) uma service account customizada por serviço.
#### Cloud Run / Functions
Ao implantar um serviço do Cloud Run ou uma Cloud Function, scopes nem entram na conversa — finalmente! Por que se complicar quando você pode simplesmente solicitar os scopes que precisa em tempo de execução, assim:
````import google.authSCOPES = ["\```\\[https://www.googleapis.com/auth/drive](https://www.googleapis.com/auth/spreadsheets.readonly)\\```\"]creds, _ = google.auth.default(scopes=SCOPES)````
Depois é só seguir normalmente com a API de sua escolha. Veja [aqui](https://medium.com/@guillaume.blaquiere/yes-my-experience-isnt-the-same-63ac3c783864) o exemplo completo.
Acredito que era assim que isso deveria ter funcionado também no GKE/GCE/GAE.
Caso você esteja se perguntando — não, fazer o mesmo no GCE/GKE não vai produzir o efeito desejado. No GCE/GKE, os scopes que você solicita no app são ignorados e redefinidos para os configurados na VM/node pool.
#### **Dev Local**
Quando comecei a trabalhar com a Google Directory API, fiquei surpreso ao descobrir que, para invocar a API, é **obrigatório** usar uma service account do GCP e, consequentemente, ter um projeto GCP devidamente configurado com a API desejada habilitada.
Como as credenciais de ADC obtidas no ambiente de desenvolvimento local (no seu laptop, ou seja) normalmente pertencem ao seu usuário, elas são praticamente inúteis para trabalhar com várias APIs que não são do GCP (como a Directory API).
O único caminho é, novamente, fazer impersonation, e o código da seção do GAE acima funciona aqui "como está".
## O que são Subjects?
Subjects costumam ser um detalhezinho que complica ainda mais o código de bootstrap de autenticação. A questão é que, ao acessar a _Directory API_ (por exemplo, listar usuários do seu domínio do G Suite), você atua em nome de um usuário específico do diretório, que precisa ser informado como o _subject_ da credencial.
Considerando isso, pode acontecer uma impersonation em closed-loop ao rodar localmente em dev — um `[email protected]` autenticado localmente impersona uma service account para conversar com a Directory API do próprio domínio; e, então, essa service account atua em nome de `[email protected]` no diretório.
Enfim, precisamos especificar esse e-mail "act as" como subject das credenciais, o que infla nosso código de configuração de auth para isto: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 em todos os casos descritos acima também — basta passar `subject=None` se você não usa a Directory API e garantir que as roles de Token Creator estejam atribuídas corretamente.
**Então temos uma solução, eba!** O único detalhe é que, quando você leva essa abordagem para produção e joga tokens de _identity_ na equação, isso resulta em ~200 linhas de [código](https://gist.github.com/haizaar/fcf8ee4b98b2452c618582bca632a338) só para obter credenciais de autenticação, sendo metade comentários, o que normalmente é sinal de complexidade.
## Existe um jeito melhor?
Temos uma solução que funciona em todo lugar (tirando a possível preocupação de segurança com o App Engine), mas ela acaba parecendo bem rebuscada perto da simples chamada `google.auth.default()` que usávamos antes. Existe outro caminho?
Tenho uma sugestão que pode soar mais simples para algumas pessoas. Ao menos resolve a preocupação de segurança do App Engine com impersonation. Aqui vai:
- Crie um arquivo de chave para a service account que você quer impersonar. Sim, o velho, simples e "feio" arquivo de chave.- Criptografe o arquivo com [Mozilla sops](https://github.com/mozilla/sops) + GCP KMS e armazene-o como parte do seu código.- Conceda à sua service account operacional uma role de descriptografia para a chave do GCP KMS usada para criptografar o arquivo de chave da service account.
Agora seu código de bootstrap fica assim: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)
O objeto `creds` resultante tem dois métodos práticos, `with_scopes()` e `with_subject()`, que retornam um novo objeto de credenciais com os scopes/subject atualizados.
Embora isso exija mais configuração, incluindo incorporar o binário `sops` de 30MiB nas suas Cloud Functions e imagens de container, é muito fácil entender o fluxo (ao contrário da abordagem anterior).
Essa solução também resolve o problema com o App Engine — continua sendo verdade que a service account padrão do App Engine pode, em tese, descriptografar todos os arquivos de chave, mas serviços diferentes do GAE implantados a partir de repositórios git diferentes não terão acesso aos arquivos de chave criptografados das service accounts uns dos outros — ou seja, temos isolamento de segredos aqui!
#### Rastreabilidade da Auth
Esta é uma parte bônus — obrigado por ter chegado até aqui.
Um ponto bacana de usar impersonation em vez dos feios arquivos de chave de service account é que dá para rastrear o principal original que fez a impersonation para uma service account.
Aqui vai o cheat-sheet de auditoria:
- [Habilite](https://cloud.google.com/iam/docs/audit-logging#enabling_audit_logging) os audit logs de Data Access para o serviço de IAM- Faça impersonation, por exemplo: `gcloud --impersonate-service-account=<email> projects list`- Abra o [Logs Explorer](https://console.cloud.google.com/logs) e selecione "Service Account" no dropdown "Resource" e "data\_access" no dropdown "Log name".
Ou simplesmente cole a consulta abaixo:resource.type="service_account"
logName="projects/
Dependendo de como você executa a impersonation, payloads com `methodName` igual a `SignBlob` e `GenerateAccessToken` são o que você procura:
Audit Log do Ato de Impersonation
Agora, com a abordagem de chaves de service account + SOPS, voltamos à estaca zero — ou será que sim? No fim das contas, simplesmente usamos a chave da service account, então não há indicação direta de quem é a entidade real por trás da operação, _mas_ precisamos descriptografar aquele arquivo JSON de chave primeiro usando o Cloud KMS (o que é feito pelo SOPS por trás dos panos) e é aí que deixamos as migalhas:
- Habilite os audit logs de Data Access para o serviço Cloud KMS
- Use o SOPS para descriptografar seu arquivo JSON de chave
- Agora, consultando por `resource.type="cloudkms_cryptokey" resource.labels.location="global"`, temos nossas pistas:

Ou seja, dá para ver quem usou nossas chaves para descriptografar coisas; e, embora não seja uma evidência forte por si só, manter boas convenções de nomenclatura junto com uma abordagem de "uma chave KMS por service account" pode dar uma correlação forte para rastrear suspeitos.
## Epílogo
Espero que este post tenha ajudado a esclarecer por que sua autenticação pode estar quebrando no momento em que você sai de um mundo só de GCP para o ecossistema bem mais amplo das APIs do Google.
Recapitulando: scopes do oAuth, embora não sejam um mecanismo de segurança real para muitos serviços que oferecem um IAM de verdade hoje em dia, ainda são obrigatórios para fazer as coisas funcionarem; e, dependendo da sua plataforma — App Engine, GCE/GKE, etc. —, pode dar um trabalho considerável para chegar lá.
**_Zaar Hai é Staff Cloud Architect na_** [**_DoiT International_**](https://www.doit.com/) **_. Confira nossa_** [**_página de carreiras_**](http://careers.doit.com/) **_se você quiser trabalhar com o Zaar e outros Senior Cloud Architects da DoiT International!_**