gcp

Override default Service Accounts for Google AppEngine Standard

2022-01-25

This template describes how you can override the default AppEngine Service Account.

Until very recently, appengine could only use its built-in service account (YOUR_PROJECT_ID@appspot.gserviceaccount.com) for authentication to different GCP services. This limitation has very recently been addressed by allowing users to define a service_account: setting in app.yaml.

While it is currently in alpha, this tutorial shows how to set and use that. In this mode, the default service account appenengine uses will be a user-defined one in the configuration file.

This also demonstrates how to use impersonated_credentials. In this mode, the default service account appengine is runs with is _temporarily overridden` at runtime nd exchanged for a different service account.

If you simply want a global override, use the setting in app.yaml. If you want to temporarily override, use impersonation.

For additional information, see


Anyway, to continue, this tutorial will

  • create two service account: one that represents an override for the default service account one that is impersonated by the overridden default
  • create and deploy an appengine application
  • print the userid and id_token for each service account

Setup


## setup
export PROJECT_ID=`gcloud config get-value core/project`
export PROJECT_NUMBER=`gcloud projects describe $PROJECT_ID --format="value(projectNumber)"`

## enable services
gcloud services enable appengine.googleapis.com containerregistry.googleapis.com iamcredentials.googleapis.com

## create gae app
gcloud app create --region us-central


## create non-default SA 
gcloud iam service-accounts create nondefault

## create SA for impersonation
gcloud iam service-accounts create impersonated
Created service account [impersonated].

$ gcloud iam service-accounts list
DISPLAY NAME                        EMAIL                                         DISABLED
App Engine default service account  gae-svc@appspot.gserviceaccount.com           False
                                    nondefault@gae-svc.iam.gserviceaccount.com    False
                                    impersonated@gae-svc.iam.gserviceaccount.com  False


# allow impersonation for nondefault@
gcloud iam service-accounts add-iam-policy-binding \
  impersonated@$PROJECT_ID.iam.gserviceaccount.com  \
  --member serviceAccount:$PROJECT_ID@appspot.gserviceaccount.com \
  --role roles/iam.serviceAccountTokenCreator

# Allow impersonation for gae-svc@  (we are not demonstrating this but left it here as an example)
# gcloud iam service-accounts add-iam-policy-binding \
#   impersonated@gae-svc.iam.gserviceaccount.com  \
#   --member serviceAccount:gae-svc@appspot.gserviceaccount.com \
#   --role roles/iam.serviceAccountTokenCreator


# allow impersonated credential to write to logs
gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member=serviceAccount:nondefault@$PROJECT_ID.iam.gserviceaccount.com \
  --role roles/logging.logWriter

The iam.serviceAccountTokenCreator role includes concentric set of permission (see Concentric IAMCredentials Permissions: The secret life of signBlob. If you just need to generate an access_token or an id_token, consider generating a custom role with that specific capability.

Deploy Application

Edit app.yaml

set the value for the override service account (remember to replace $PROJECT_ID, eg)

runtime: python37
service: default
service_account: nondefault@$PROJECT_ID.iam.gserviceaccount.com

(again remember to replace $PROJECT_ID) in main.py

    target_credentials = impersonated_credentials.Credentials(
        source_credentials=source_credentials,
        target_principal='impersonated@$PROJECT_ID.iam.gserviceaccount.com',
        target_scopes = ['https://www.googleapis.com/auth/cloud-platform','https://www.googleapis.com/auth/userinfo.email'],
        lifetime=500)

Then deploy

virtualenv  -p /usr/bin/python3 /tmp/venv
source /tmp/venv/bin/activate

mkdir -p lib
pip3 install -r requirements.txt -t lib

gcloud app deploy --version=1 --no-promote .
Access service
curl -s https://1-dot-$PROJECT_ID.uc.r.appspot.com/  | jq '.'

What you should see is the JSON response from httpbin.org/get that echos back the id_token that was sent.

In the logs, you shold see the override default SA, its id_token, then the impersonated service account and then finally its id_token (which is also returned back to the client)

images/logs.png

thats it. the reason i wrote this up is i often see this issue come up and to write about that brand-new flag.


Source

  • main.py
from flask import Flask
import google.auth
from google.auth import compute_engine
from google.auth import impersonated_credentials
from google.auth.transport.requests import AuthorizedSession, Request

import logging

from logging.config import dictConfig

dictConfig({
    'version': 1,
    'formatters': {'default': {
        'format': '%(message)s',
    }},
    'handlers': {'wsgi': {
        'class': 'logging.StreamHandler',
        'stream': 'ext://sys.stdout',
        'formatter': 'default'
    }},
    'root': {
        'level': 'INFO',
        'handlers': ['wsgi']
    }
})

app = Flask(__name__)

@app.route('/')
def hello():
    logger = logging.getLogger()
    # source_credential is for gae-svc@appspot.gserviceaccount.com by default
    # source_credential is nondefault@gae-svc.iam.gserviceaccount.com if service_account is set in app.yaml
    source_credentials, project_id = google.auth.default()
    authed_session = AuthorizedSession(source_credentials)
    response = authed_session.request('GET', 'https://www.googleapis.com/userinfo/v2/me')
    app.logger.info(response.json())

    ## to get id_token from the base credential in GAE, you need to check and cast to compute_engine Credential
    ## the following, i'm just casting directly and assuming i'm running in GAE
    ## https://googleapis.dev/python/google-auth/1.14.1/user-guide.html#identity-tokens
    ## https://github.com/salrashid123/google_id_token/blob/master/python/main.py#L42
    request = Request()
    cid = compute_engine.IDTokenCredentials(request=request, target_audience='https://foo.bar', use_metadata_identity_endpoint=True)
    cid.refresh(request)
    app.logger.info(cid.token)

    ############  now use impersonation

    #  target_credential is impersonated@gae-svc.iam.gserviceaccount.com
    target_credentials = impersonated_credentials.Credentials(
        source_credentials=source_credentials,
        target_principal='impersonated@{}.iam.gserviceaccount.com'.format(project_id),
        target_scopes = ['https://www.googleapis.com/auth/cloud-platform','https://www.googleapis.com/auth/userinfo.email'],
        lifetime=500)

    authed_session= AuthorizedSession(target_credentials)
    response = authed_session.request('GET', 'https://www.googleapis.com/userinfo/v2/me')
    app.logger.info(response.json())        


    idt = impersonated_credentials.IDTokenCredentials(
        target_credentials=target_credentials,
        target_audience='https://foo.bar',
        include_email=True
    )

    url = 'https://httpbin.org/get'      

    authed_session = AuthorizedSession(idt)
    response = authed_session.request('GET', url)
    app.logger.info(response.json())

    return response.json()

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=8080, debug=True)
  • app.yaml
runtime: python37
service: default
service_account: nondefault@$PROJECT_ID.iam.gserviceaccount.com
  • appengine_config.py
import os
import sys
import logging

from google.appengine.ext import vendor
vendor.add('lib')
sys.path.append(os.path.join(os.path.dirname(__file__), 'lib'))

logging.basicConfig(level=logging.INFO)
  • requirements.txt
flask
google-auth
requests

This site supports webmentions. Send me a mention via this form.