Google Cloud Tasks cannot authenticate to Cloud Run - service-accounts

I am trying to invoke a Cloud Run service using Cloud Tasks as described in the docs here.
I have a running Cloud Run service. If I make the service publicly accessible, it behaves as expected.
I have created a cloud queue and I schedule the cloud task with a local script. This one is using my own account. The script looks like this
from google.cloud import tasks_v2
client = tasks_v2.CloudTasksClient()
project = 'my-project'
queue = 'my-queue'
location = 'europe-west1'
url = 'https://url_to_my_service'
parent = client.queue_path(project, location, queue)
task = {
'http_request': {
'http_method': 'GET',
'url': url,
'oidc_token': {
'service_account_email': 'my-service-account#my-project.iam.gserviceaccount.com'
}
}
}
response = client.create_task(parent, task)
print('Created task {}'.format(response.name))
I see the task appear in the queue, but it fails and retries immediately. The reason for this (by checking the logs) is that the Cloud Run service returns a 401 response.
My own user has the roles "Service Account Token Creator" and "Service Account User". It doesn't have the "Cloud Tasks Enqueuer" explicitly, but since I am able to create the task in the queue, I guess I have inherited the required permissions.
The service account "my-service-account#my-project.iam.gserviceaccount.com" (which I use in the task to get the OIDC token) has - amongst others - the following roles:
Cloud Tasks Enqueuer (Although I don't think it needs this one as I'm creating the task with my own account)
Cloud Tasks Task Runner
Cloud Tasks Viewer
Service Account Token Creator (I'm not sure whether this should be added to my own account - the one who schedules the task - or to the service account that should perform the call to Cloud Run)
Service Account User (same here)
Cloud Run Invoker
So I did a dirty trick: I created a key file for the service account, downloaded it locally and impersonated locally by adding an account to my gcloud config with the key file. Next, I run
curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" https://url_to_my_service
That works! (By the way, it also works when I switch back to my own account)
Final tests: if I remove the oidc_token from the task when creating the task, I get a 403 response from Cloud Run! Not a 401...
If I remove the "Cloud Run Invoker" role from the service account and try again locally with curl, I also get a 403 instead of a 401.
If I finally make the Cloud Run service publicly accessible, everything works.
So, it seems that the Cloud Task fails to generate a token for the service account to authenticate properly at the Cloud Run service.
What am I missing?

I had the same issue here was my fix:
Diagnosis: Generating OIDC tokens currently does not support custom domains in the audience parameter. I was using a custom domain for my cloud run service (https://my-service.my-domain.com) instead of the cloud run generated url (found in the cloud run service dashboard) that looks like this: https://XXXXXX.run.app
Masking behavior: In the task being enqueued to Cloud Tasks, If the audience field for the oidc_token is not explicitly set then the target url from the task is used to set the audience in the request for the OIDC token.
In my case this meant that enqueueing a task to be sent to the target https://my-service.my-domain.com/resource the audience for the generating the OIDC token was set to my custom domain https://my-service.my-domain.com/resource. Since custom domains are not supported when generating OIDC tokens, I was receiving 401 not authorized responses from the target service.
My fix: Explicitly populate the audience with the Cloud Run generated URL, so that a valid token is issued. In my client I was able to globally set the audience for all tasks targeting a given service with the base url: 'audience' : 'https://XXXXXX.run.app'. This generated a valid token. I did not need to change the url of the target resource itself. The resource stayed the same: 'url' : 'https://my-service.my-domain.com/resource'
More Reading:
I've run into this problem before when setting up service-to-service authentication: Google Cloud Run Authentication Service-to-Service

1.I created a private cloud run service using this code:
import os
from flask import Flask
from flask import request
app = Flask(__name__)
#app.route('/index', methods=['GET', 'POST'])
def hello_world():
target = os.environ.get('TARGET', 'World')
print(target)
return str(request.data)
if __name__ == "__main__":
app.run(debug=True,host='0.0.0.0',port=int(os.environ.get('PORT', 8080)))
2.I created a service account with --role=roles/run.invoker that I will associate with the cloud task
gcloud iam service-accounts create SERVICE-ACCOUNT_NAME \
--display-name "DISPLAYED-SERVICE-ACCOUNT_NAME"
gcloud iam service-accounts list
gcloud run services add-iam-policy-binding SERVICE \
--member=serviceAccount:SERVICE-ACCOUNT_NAME#PROJECT-ID.iam.gserviceaccount.com \
--role=roles/run.invoker
3.I created a queue
gcloud tasks queues create my-queue
4.I create a test.py
from google.cloud import tasks_v2
from google.protobuf import timestamp_pb2
import datetime
# Create a client.
client = tasks_v2.CloudTasksClient()
# TODO(developer): Uncomment these lines and replace with your values.
project = 'your-project'
queue = 'your-queue'
location = 'europe-west2' # app engine locations
url = 'https://helloworld/index'
payload = 'Hello from the Cloud Task'
# Construct the fully qualified queue name.
parent = client.queue_path(project, location, queue)
# Construct the request body.
task = {
'http_request': { # Specify the type of request.
'http_method': 'POST',
'url': url, # The full url path that the task will be sent to.
'oidc_token': {
'service_account_email': "your-service-account"
},
'headers' : {
'Content-Type': 'application/json',
}
}
}
# Convert "seconds from now" into an rfc3339 datetime string.
d = datetime.datetime.utcnow() + datetime.timedelta(seconds=60)
# Create Timestamp protobuf.
timestamp = timestamp_pb2.Timestamp()
timestamp.FromDatetime(d)
# Add the timestamp to the tasks.
task['schedule_time'] = timestamp
task['name'] = 'projects/your-project/locations/app-engine-loacation/queues/your-queue/tasks/your-task'
converted_payload = payload.encode()
# Add the payload to the request.
task['http_request']['body'] = converted_payload
# Use the client to build and send the task.
response = client.create_task(parent, task)
print('Created task {}'.format(response.name))
#return response
5.I run the code in Google Cloud Shell with my user account which has Owner role.
6.The response received has the form:
Created task projects/your-project/locations/app-engine-loacation/queues/your-queue/tasks/your-task
7.Check the logs, success

The next day I am no longer able to reproduce this issue. I can reproduce the 403 responses by removing the Cloud Run Invoker role, but I no longer get 401 responses with exactly the same code as yesterday.
I guess this was a temporary issue on Google's side?
Also, I noticed that it takes some time before updated policies are actually in place (1 to 2 minutes).

For those like me, struggling through documentation and stackoverflow when having continuous UNAUTHORIZED responses on Cloud Tasks HTTP requests:
As was written in thread, you better provide audience for oidcToken you send to CloudTasks. Ensure your requested url exactly equals to your resource.
For instance, if you have Cloud Function named my-awesome-cloud-function and your task request url is https://REGION-PROJECT-ID.cloudfunctions.net/my-awesome-cloud-function/api/v1/hello, you need to ensure, that you set function url itself.
{
serviceAccountEmail: SERVICE-ACCOUNT_NAME#PROJECT-ID.iam.gserviceaccount.com,
audience: https://REGION-PROJECT-ID.cloudfunctions.net/my-awesome-cloud-function
}
Otherwise seems full url is used and leads to an error.

Related

What service account permissions should I set for secure flexible app engine -> cloud function communication

I am having problems identifying which service account I need to give certain roles to.
I have a NodeJS app running on my flexible app engine environment.
I have a single hello-world python3.7 HTTP cloud function.
I want to do a GET request from my app engine to my cloud function.
When the allUser member is given the Cloud Function Invoker role on the hello-world cloud function everything works fine.
But now I want to secure my cloud function endpoint so that only my flexible app engine can reach it.
I remove the allUser member and as expected I get a 403 when the app engine tries to call.
Now I add the #appspot.gserviceaccount.com and #gae-api-prod.google.com.iam.gserviceaccount.com members to the hello-world cloud function and give them Cloud Function Invoker roles.
I would expect the flexible app engine to now be able to call the hello-world cloud function seeing as I gave it the Cloud Function Invoker role.
But I keep getting a 403 error.
What service account is app engine flexible using to do these calls to the cloud function API?
The are some settings to made in order to connect cloud functions wit a service account:
Enable required APIs
Enable Service account
Act as User Service Account
The default service account creates a cloud function and sometimes doesn't have all the privileges.
You can find more info Here:
https://cloud.google.com/functions/docs/securing/
John Hanley was correct,
When using GCP libraries to perform actions (like google-cloud-firestore for example) the executing function will use the underlying service account permissions to do those actions.
When doing manual HTTP requests to cloud function URLs, you will have to fetch a token from the metadata server to properly authenticate your request.
def generate_token() -> str:
"""Generate a Google-signed OAuth ID token"""
token_request_url: str = f'http://metadata/computeMetadata/v1/instance/service-
accounts/default/identity?audience={TARGET_URL}'
token_request_headers: dict = {'Metadata-Flavor': 'Google'}
token_response = requests.get(token_request_url, headers=token_request_headers)
return token_response.content.decode("utf-8")
def do_request():
token: str = generate_token()
headers: dict = {
'Content-type': 'application/json',
'Accept': 'application/json',
'Authorization': f'Bearer {token}'
}
requests.post(url=TARGET_URL, json=data, headers=headers)

Allowing access to the Google Admin SDK Directory API in Python

I'm trying to setup a google group for marketing purposes, in which when certain users sign up to my application, I send their email to this google group with the following code
# google_admin_apis.py
def add_member(member):
if not member.email:
return False
try:
service = build('admin', 'directory_v1')
except DefaultCredentialsError: # For developers
return False
group_key = 'mygroup#mydomain.com'
body = {
"email": member.email
}
members = service.members()
request = members.insert(groupKey=group_key, body=body)
response = request.execute()
return True
My application is hosted on Google App Engine, so by default ADC will use the default service account when run on the server. I have tried to run this code locally by using gcloud auth application-default-account login and logging in with my G Suite admin account, and also my personal account (both are owners of the GCP project). After this failed, I did some research and realised that to enable OAuth2 to access my G Suite User data (I'm not really accessing anything by inserting a user?!?) I had to 'enable domain wide delegation' on the default service account, so I did this, I then downloaded the service account JSON and attempted to manually authorise with $GOOGLE_APPLICATION_CREDENTIALS, but was still getting a 403. I then went one step further and followed these instructions. Giving my Client ID access to https://www.googleapis.com/auth/admin.directory.group and group.member.
After all this, I still get a 403 error.
With the application-default-credentials I get:
<HttpError 403 when requesting
https://www.googleapis.com/admin/directory/v1/groups/groupKey/members?alt=json
returned "Insufficient Permission">
When using the app engine default service account through .json with either activate-service-account or through the GOOGLE_APPLICATION_CREDENTIALS, I get:
<HttpError 403 when requesting
https://www.googleapis.com/admin/directory/v1/groups/groupKey/members?alt=json
returned "Not Authorized to access this resource/api">
(groupKey intentially censored)
In short, I have an app-engine default service account with domain wide delegation and have given it's client ID access to both roles required for the Directory API's member.insert() function, yet I am still not allowed to call the API as above.
Any help would be greatly appreciated.
I followed this tutorial https://developers.google.com/admin-sdk/directory/v1/quickstart/python to run a similar function locally using Google's google_auth_oauthlib to set up OAuth2 credentials
service = build('admin', 'directory_v1', credentials=creds)

Authorize requests to app engine app with a service account

I am using the app.yaml's login: admin in handlers to restrict access to my app only to selected Google accounts (which I can edit in IAM). I'm using the python27 standard environment on GAE.
I would like to use the JSON API my app exposes from another server app (not hosted on GAE). Using a service account looks like a straightforward solution, but I am unable to get the scopes or the request itself right, so the endpoint would see an authenticated Google user.
The service-user currently has Project/Viewer role in the IAM. I tried a few more like AppEngine/Viewer, AppEngine/Admin. I also tried some more scopes.
My test code:
"""Try do do an API request to a deployed app
with the current service account.
https://google-auth.readthedocs.io/en/latest/user-guide.html
"""
import sys
from google.auth.transport.requests import AuthorizedSession
from google.oauth2 import service_account
def main():
if len(sys.argv) < 2:
sys.exit("use: %s url" % sys.argv[0])
credentials = service_account.Credentials.from_service_account_file(
'service-user.json')
scoped_credentials = credentials.with_scopes(
['https://www.googleapis.com/auth/cloud-platform.read-only'])
authed_http = AuthorizedSession(scoped_credentials)
response = authed_http.request('GET', sys.argv[1])
print response.status_code, response.reason
print response.text.encode('utf-8')
if __name__ == '__main__':
main()
There is no error, the request behaves like unauthenticated. I checked the headers on the server, and while requesting from the browser there are several session cookies, the AuthorizedSession request contains single Authorization: Bearer .. header.
Normally the roles you would need is App Engine Admin; it's designed for this purpose. It should also work with the viewer/editor/owner primitive roles. That being said, to make sure it's not a "role" issue, simply give it the project owner role and also the explicit App Engine Admin role and try again. This will eliminate any role-based issue.
Let me know if that works for you.

Google Pub/Sub access rights

I created a topic in my project Project 1 and I have an app on Google app engine which posts every minute a message to this topic.
I have a google cloud compute machine in a second project (Project 2) which subscribed to this topic and receives the messages.
I did not give any access right to the machine on my Project 2, but even without access rights, It managed to receive the messages. More precisely, I did not write specific permissions associated to the topic I created.
My questions are:
1- is this normal? Shouldn't the machine on Project 2 get a "forbidden access error"?
2- how can I restrain access on a certain topic?
Here is the code of my subscription part:
import httplib2
import base64
import pandas
import json
from apiclient import discovery
from oauth2client import client as oauth2client
from oauth2client.client import SignedJwtAssertionCredentials
from oauth2client.client import GoogleCredentials
def create_pubsub_client(http=None):
credentials = GoogleCredentials.get_application_default()
if not http:
http = httplib2.Http()
credentials.authorize(http)
return discovery.build('pubsub', 'v1', http=http)
client = create_pubsub_client()
# You can fetch multiple messages with a single API call.
batch_size = 1
subscription_str = 'projects/<myproject1>/subscriptions/testo'
# Create a POST body for the Pub/Sub request
body = {
# Setting ReturnImmediately to false instructs the API to wait
# to collect the message up to the size of MaxEvents, or until
# the timeout.
'returnImmediately': False,
'maxMessages': batch_size,
}
while True:
resp = client.projects().subscriptions().pull(
subscription=subscription_str, body=body).execute()
received_messages = resp.get('receivedMessages')
if received_messages is not None:
ack_ids = []
for received_message in received_messages:
pubsub_message = received_message.get('message')
if pubsub_message:
# Process messages
msg = base64.b64decode(str(pubsub_message.get('data')))
treatment(msg)
# Get the message's ack ID
ack_ids.append(received_message.get('ackId'))
# Create a POST body for the acknowledge request
ack_body = {'ackIds': ack_ids}
# Acknowledge the message.
client.projects().subscriptions().acknowledge(
subscription=subscription_str, body=ack_body).execute()
The ability of the machine in Project 2 to access the topic/subscription in Project 1 depends entirely on how machine is authenticated. If it is authenticated with something that has permissions on both projects, e.g., your developer account, then you would be able to access the subscription on the topic in Project 1. That is normal.
If you want to restrict the access, create a service account in Project 1 and set the permissions on your topic and/or subscription to allow only that service account. You would do so in the Pub/Sub section of the Google Developers Console. Then, only machines authenticated via that service account will be able to access them.

The resource 'projects/<my project>' was not found" error when trying to get list of running instances

My goal is to test out google's orchestrator and the compute engine api by first retrieving a list of active instances. The orchestrator project including the servlet file is stored in a jar.
I'm trying to test out the java google compute engine client api. I have a cron job which calls on the orchestrator servlet. The target for the cron is a backend. From which I try to get the list of instances:
...
AppIdentityCredential credential = getCredential(computeScope);
String appName = ConfigProperties.getInstance().getGceConfigProperties().get("projectId");
try {
httpTransport = GoogleNetHttpTransport.newTrustedTransport();
final Compute compute = new Compute.Builder(
httpTransport, JSON_FACTORY, credential).setApplicationName(appName)
.build();
logger.info("================== Listing Compute Engine Instances ==================");
Compute.Instances.List instances = compute.instances().list(projectId, zone);
InstanceList list = instances.execute();
if (list.getItems() == null) {
logger.info("No instances found. Sign in to the Google APIs Console and create "
+ "an instance at: code.google.com/apis/console");
} else {
for (Instance instance : list.getItems()) {
logger.info(instance.toPrettyString());
}
}
...
There error response I get is(I omitted my project name from the response, I confirmed that I'm using the correct project id in my code):
com.google.cloud.solutions.sampleapps.orchestration.orchestrator.server.GceClientApiUtils
getInstances: com.google.api.client.googleapis.json.GoogleJsonResponseException: 404 OK
{
"code" : 404,
"errors" : [ {
"domain" : "global",
"message" : "The resource 'projects/<project-name-here>' was not found",
"reason" : "notFound"
} ],
"message" : "The resource 'projects/<project-name_here>' was not found"
}
I've also attempted this by retrieving an access token and making a RESTful call to get the list of instances and i received the exact same response. I confirmed the Url constructed was correct by comparing it against a successful query of the instances using the api explorer.
EDIT: I determined the solution to the issue with help of another post:
I was finally able to find the solution in the post Compute Engine API call fails with http 404
I needed to add my app engine service account as a team member with edit capabilities, which it does not have by default. Once I did this, the code worked as expected. I had to do this through cloud.google.com/console, as if done through appengine.google.com, a pending status will be given to the service account and will not have access.
For me i had to make sure i had authorization. Try this in the terminal gcloud auth login
Make sure you are in the right project, you can run this command on your vm to see if you are in the right project:
gcloud config list
Take a look at this post in Google Groups
Do you have access to the developers console https://console.developers.google.com?
It seems that the user account #appspot.gserviceaccount.com has not access to compute engine. In my case I see #developer.gserviceaccount.com.
If you don't have one, visit https://developers.google.com/console/help/new/#generatingoauth2 to create a new Client ID

Resources