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
id_token
for each service account
## 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.
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 .
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)
thats it. the reason i wrote this up is i often see this issue come up and to write about that brand-new flag.
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.