7

In my GCP project, I have a python API running in a docker container (using connexion). I want to expose the API (with an API key) using API Gateway.

When I deploy the docker container with --ingress internal, I get Access is forbidden. on API calls over the Gateway. So the API gateway cannot access the Google Run container. When I use --ingress all, all works as expected, but then my internal API is accessible from the web, which is not what I want.

I created a service account for this:

gcloud iam service-accounts create $SERVICE_ACCOUNT_ID \
#  --description="the api gateway user" \
#  --display-name="api gateway user"

... gave the account run.invoker permissions:

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --role=roles/run.invoker --member \
  serviceAccount:$SERVICE_ACCOUNT_EMAIL

... and used the service account to create the API Config:

gcloud api-gateway api-configs create $CONFIG_ID \
  --api=$API_ID --openapi-spec=$API_DEFINITION \
  --project=$PROJECT_ID --backend-auth-service-account=$SERVICE_ACCOUNT_EMAIL

But I can't access the docker API from API Gateway. What am I missing here? How can I secure my API, so API Gateway can connect internally.

Update1: Also applied the role to my run service:

gcloud run services add-iam-policy-binding $SERVICE_ID \
  --region $REGION --member="serviceAccount:$SERVICE_ACCOUNT_EMAIL" \
  --role="roles/run.invoker"

Update2: Some extra info as requested by John Hanley:

My gateway yml looks like this:

swagger: '2.0'

info:
  title: "title"
  description: "description"
  version: "0.1"

schemes:
- https

x-google-backend:
  address: <CLOUD_RUN_SERVICE_URL>

paths:
  /api:
    post:
      operationId: api
      consumes:
        - application/json
      produces:
        - application/json
      security:
        - api_key: []
      parameters:
        - in: body
          name: request
          description: request
          required: true
          schema:
            $ref: '#/definitions/Request'
      responses:
        200:
          description: "success"
        400:
          description: "bad data"
        503:
          description: "internal error"

definitions:
  Request:
    properties:
      parameter1:
        type: string
      parameter1:
        type: string
    required:  
      - parameter1

securityDefinitions:
  api_key:
    type: "apiKey"
    name: "key"
    in: "query"
gcloud api-gateway api-configs describe api-config --api api-api
createTime: '2021-06-12T15:02:27.382098034Z'
displayName: api-config
gatewayServiceAccount: projects/-/serviceAccounts/apigatewayuser@projectid.iam.gserviceaccount.com
name: projects/722514052893/locations/global/apis/api-api/configs/api-config
serviceConfigId: api-config-3hytlxf4gfvzj
state: ACTIVE
updateTime: '2021-06-12T15:05:09.778404414Z'
gcloud api-gateway gateways describe api-gateway --location europe-west1
apiConfig: projects/722514052893/locations/global/apis/api-api/configs/api-config
createTime: '2021-06-12T15:06:03.383002459Z'
defaultHostname: api-gateway-97x27n6l.ew.gateway.dev
displayName: api-gateway
name: projects/projectid/locations/europe-west1/gateways/api-gateway
state: ACTIVE
updateTime: '2021-06-12T15:07:37.590520122Z'
gcloud run services describe api --region europe-west1
✔ Service api in region europe-west1
 
URL:     https://api-o3rf5h4boa-ew.a.run.app
Ingress: internal
Traffic:
  100% LATEST (currently api-00010-lig)
 
Last updated on 2021-06-12T17:42:49.913232Z by myemail@gmail.com:
  Revision api-00010-lig
  Image:         gcr.io/projectid/api
  Port:          8080
  Memory:        512Mi
  CPU:           1000m
  Concurrency:   80
  Max Instances: 100
  Timeout:       300s

Tried debugging directly on Cloud Run:

gcloud iam service-accounts keys create $KEY_FILE --iam-account=$SERVICE_ACCOUNT_EMAIL
gcloud auth activate-service-account $SERVICE_ACCOUNT_EMAIL --key-file $KEY_FILE
BEARER=$(gcloud auth print-identity-token $SERVICE_ACCOUNT_EMAIL)


curl --header "Content-Type: application/json" \
  --header "Authorization: bearer $BEARER" \
  --request POST \
  --data '{"parameter1":"somedata"}' \
  $SERVICE_URL/api

The result is still a Forbidden:

<!DOCTYPE html>
<html lang=en>
  <meta charset=utf-8>
  <meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
  <title>Error 403 (Forbidden)!!1</title>
  <style>
    *{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
  </style>
  <a href=//www.google.com/><span id=logo aria-label=Google></span></a>
  <p><b>403.</b> <ins>That’s an error.</ins>
  <p>Access is forbidden.  <ins>That’s all we know.</ins>

So the problem lies in the Cloud Run application not being accessible by the service account. I'm not sure why this does not work, since the run.invoker role was added to the Run service.

dvrslype
  • 103
  • 1
  • 7
  • 1
    Add the service account identity to your Cloud Run service: `gcloud run services add-iam-policy-binding` https://cloud.google.com/sdk/gcloud/reference/run/services/add-iam-policy-binding – John Hanley Jun 12 '21 at 17:22
  • Did this: `gcloud run services add-iam-policy-binding $SERVICE_ID --region $REGION --member="serviceAccount:$SERVICE_ACCOUNT_EMAIL" --role="roles/run.invoker"`, but still get `Access is forbidden`. – dvrslype Jun 12 '21 at 17:46
  • 1
    Add the following to your question: a) the API Gateway YAML file to your question; b) `gcloud api-gateway api-configs describe`; c) `gcloud api-gateway gateways describe`; d) `gcloud run services describe` – John Hanley Jun 12 '21 at 18:07
  • 1
    Use curl or postman and make an authenticated call to your Cloud Run service to verify that the service account is accepted. `gcloud auth print-identity-token` will give you an Identity Token for the "Authorization: bearer TOKEN` header. That test will divide your problem in two. If that works, then you have an API Gateway problem, otherwise Cloud Run. – John Hanley Jun 12 '21 at 18:09
  • updated my question with the requested info – dvrslype Jun 13 '21 at 09:46
  • 1
    I think @guillaume posted the correct answer. Please confirm for us. – John Hanley Jun 13 '21 at 23:19

1 Answers1

14

Ingress internal means "Accept only the requests coming from the project's VPC or VPC SC perimeter".

When you use API Gateway, you aren't in your VPC, it's serverless, it's in Google Cloud managed VPC. Therefore, your query are forbidden.

And because API Gateway can't be plugged to a VPC Connector (for now) and thus can't route the request to your VPC, you can't use this ingress=internal mode.


Thus, the solution is to set an ingress to all, which is not a concern is you authorize only the legit accounts to access it.

For that, check in Cloud Run service is there is allUsers granted with the roles/run.invoker in your project.

  • If yes, remove it

Then, create a service account and grant it the roles/run.invoker on the Cloud Run service.

Follow this documentation

  • Step 4: update the x-google-backend in your OpenAPI spec file to add the correct authentication audience when you call your Cloud Run (it's the base service URL)
  • Step 5: create a gateway with a backend service account; set the service account that you created previously

At the end, only the account authenticated and authorized will be able to reach your Cloud Run service

All the unauthorized access are filtered by Google Front End and discarded before reaching your service. Therefore, your service isn't invoked for nothing and therefore your pay nothing!

Only API Gateway (and the potential other accounts that you let on the Cloud Run service) can invoke to the Cloud Run service.

So, OK, your URL is public, reachable from the wild internet, but protected with Google Front End and IAM.

guillaume blaquiere
  • 66,369
  • 2
  • 47
  • 76
  • 1
    Thanks for your explanation. My confusion was in opening the ingress of the Cloud Run service to All, which meant exposing it to the web. But indeed, it cannot be called without IAM authentication, so I didn't have to worry about that. – dvrslype Jun 14 '21 at 13:22
  • @guillaume , I am trying to do this as well, but am having trouble blocking traffic on the API Gateway side. It seems that it is allowing anyone to call it. Is there a specific way to set up the config.yaml that will restrict calls only to a specific set of callers? – swygerts Jun 02 '23 at 12:39
  • @swygerts, I don't see any role "invoker" so I guess what you expect is not possible. – guillaume blaquiere Jun 02 '23 at 13:49