Authorizing access to Cloud Run for Anthos services using Istio

This tutorial demonstrates using Istio to authorize access to the services that you deploy on Cloud Run for Anthos.

Cloud Run provides a developer-focused experience for deploying and serving apps and functions. Cloud Run runs services either in a fully managed environment, or in a Google Kubernetes Engine (GKE) cluster, called Cloud Run for Anthos.

The developer experience is the same in both environments, but the capabilities of the underlying platforms differ.

Cloud Run for Anthos doesn't use Identity and Access Management (IAM) to grant permissions to invoke services. Instead, you use Istio to implement authentication and authorization. In this tutorial, you use Istio authentication and authorization policies to help you to secure a Cloud Run for Anthos sample service.

The authentication and authorization policies specify which identities (IAM service accounts and users) can invoke the sample service.

In this tutorial, you implement authentication and authorization for clients running outside the GKE cluster.

Objectives

  • Create a GKE cluster with Cloud Run enabled.
  • Deploy a sample service to Cloud Run for Anthos.
  • Create an IAM service account.
  • Create an Istio authentication policy.
  • Create an Istio authorization policy.
  • Test the solution.

Costs

This tutorial uses the following billable components of Google Cloud:

To generate a cost estimate based on your projected usage, use the pricing calculator. New Google Cloud users might be eligible for a free trial.

When you finish this tutorial, you can avoid continued billing by deleting the resources you created. For more information, see Cleaning up.

Before you begin

  1. Sign in to your Google Account.

    If you don't already have one, sign up for a new account.

  2. In the Google Cloud Console, on the project selector page, select or create a Google Cloud project.

    Go to the project selector page

  3. Make sure that billing is enabled for your Cloud project. Learn how to confirm that billing is enabled for your project.

  4. Enable the Google Kubernetes Engine API, the Cloud Run API, and the Cloud APIs.
    Enable the APIs
  5. In the Cloud Console, go to Cloud Shell.
    Go to Cloud Shell
    At the bottom of the Cloud Console, a Cloud Shell session opens and displays a command-line prompt. Cloud Shell is a shell environment with the Cloud SDK already installed, including the gcloud command-line tool, and with values already set for your current project. It can take a few seconds for the session to initialize. You use Cloud Shell to run all the commands in this tutorial.

Setting up the environment

  1. Define an environment variables and gcloud tool default for the Compute Engine zone that you want to use for this tutorial:

    ZONE=us-central1-f
    gcloud config set compute/zone $ZONE
    

    You can change the zone.

  2. Create a GKE cluster with the Cloud Run add-on:

    CLUSTER=cloud-run-gke-invoker-tutorial
    
    gcloud beta container clusters create $CLUSTER \
        --addons HorizontalPodAutoscaling,HttpLoadBalancing,CloudRun \
        --enable-ip-alias \
        --enable-stackdriver-kubernetes \
        --machine-type e2-standard-2 \
        --release-channel regular
    

    This tutorial requires GKE version 1.15.11-gke.9 and later, 1.16.8-gke.7 and later, or 1.17.4-gke.5 and later. New GKE clusters that use the regular release channel meet the version constraints.

Deploying a sample service

  1. In Cloud Shell, create a namespace called tutorial in the GKE cluster:

    kubectl create namespace tutorial
    

    You can change the name of the namespace.

  2. Deploy a service called sample to Cloud Run for Anthos in the tutorial namespace:

    gcloud run deploy sample \
        --cluster $CLUSTER \
        --cluster-location $ZONE \
        --namespace tutorial \
        --image gcr.io/knative-samples/simple-api \
        --platform gke
    

    This command creates a Knative Serving service object.

  3. Cloud Run for Anthos exposes services on the external IP address of the Istio ingress gateway. Retrieve the external IP address and store it in an environment variable called EXTERNAL_IP and a file called external-ip.txt:

    export EXTERNAL_IP=$(./get-external-ip.sh | tee external-ip.txt)
    
    echo $EXTERNAL_IP
    
    get_external_ip () {
        external_ip=$(kubectl -n gke-system get svc istio-ingress \
            -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
    }
    get_external_ip
    while [ -z "$external_ip" ]; do
        sleep 2
        get_external_ip
    done
    echo "$external_ip"
  4. Verify that the sample service responds with HTTP/1.1 200 OK:

    curl -siH "Host: sample.tutorial.example.com" $EXTERNAL_IP | head -n1
    

Creating a service account

  1. Create an IAM service account that you grant access to the sample service in a later step. Store the service account email address in an environment variable:

    export SERVICE_ACCOUNT_EMAIL=$(gcloud iam service-accounts create \
        cloudrun-gke-invoker-tutorial \
        --display-name "Cloud Run for Anthos authorization tutorial service account" \
        --format "value(email)")
    

    This tutorial uses the service account name cloudrun-gke-invoker-tutorial. You can change the name.

Configuring Istio authentication and authorization

  1. Create an Istio authentication policy:

    kubectl apply -f authenticationpolicy.yaml
    
    apiVersion: authentication.istio.io/v1alpha1
    kind: Policy
    metadata:
      name: istio-ingress-jwt
      namespace: gke-system
    spec:
      targets:
      - name: istio-ingress
        ports:
        - name: http2
        - name: https
      origins:
      - jwt:
          issuer: https://accounts.google.com
          jwksUri: https://www.googleapis.com/oauth2/v3/certs
      originIsOptional: true # use authorization policy to allow or deny request
      principalBinding: USE_ORIGIN

    This authentication policy validates JSON Web Tokens (JWTs) in the Authorization header of incoming requests to the http2 and https ports of the Istio ingress gateway. To satisfy the policy, the JWTs must be Google-issued and signed OpenID Connect (OIDC) ID tokens. For more information about ID token validation, see the Google Identity Platform documentation.

    The attribute originIsOptional: true means that the authentication policy accepts requests even if they do not contain a JWT that satisfies the specified constraints. The purpose of this authentication policy is to make attributes of the JWT available to the authorization policy that you create in the next step. The authorization policy decides whether to accept or reject requests.

  2. Create an Istio authorization policy called invoke-tutorial:

    envsubst < authorizationpolicy-invoker.tmpl.yaml | kubectl apply -f -
    

    This authorization policy provides access to all workloads with hostnames that match *.tutorial.example.com, using any HTTP method and any URL path, to all requests that include a JWT issued and signed by Google (https://accounts.google.com) to the service account identified by $SERVICE_ACCOUNT_EMAIL with an audience (aud) claim of http://example.com.

    If you change the Cloud Run for Anthos default domain, in your own environment, adjust the value of the hosts field to match your domain.

    apiVersion: security.istio.io/v1beta1
    kind: AuthorizationPolicy
    metadata:
      name: invoke-tutorial
      namespace: gke-system
    spec:
      action: ALLOW
      rules:
      - to:
        - operation:
            hosts:
            - '*.tutorial.example.com'
        when:
        - key: request.auth.claims[aud]
          values:
          - http://example.com
        - key: request.auth.claims[email]
          values:
          - $SERVICE_ACCOUNT_EMAIL
        - key: request.auth.claims[iss]
          values:
          - https://accounts.google.com
      selector:
       matchLabels:
         istio: ingress-gke-system
  3. It can take a moment for the authentication and authorization policies to take effect. Run the following command and wait until you see HTTP/1.1 403 Forbidden in the output:

    while sleep 2; do
        curl -siH "Host: sample.tutorial.example.com" $EXTERNAL_IP | head -n1
    done
    

    At first, you might see the output alternate between HTTP/1.1 200 OK and HTTP/1.1 403 Forbidden because policy changes in Envoy and Istio are eventually consistent.

    When you see HTTP/1.1 403 Forbidden repeatedly, press Ctrl+C to stop waiting.

Accessing the service

The authentication and authorization policies that you created require Google-issued and signed ID tokens. You can obtain tokens several different ways. The following lists four options in order of recommendation:

  1. If you're accessing the protected service from a Compute Engine instance, a GKE cluster, or another environment where you have access to the metadata server, use the Compute Engine metadata server.
  2. If it's available for your programming language, use a Google authentication client library.
  3. If you can't use a Google authentication client library, use the IAM Service Account Credentials API directly.
  4. If you want to test your service interactively, such as during implementation, use the gcloud command-line tool.

Use the Compute Engine metadata server

  1. In Cloud Shell, create a Compute Engine instance (VM) and attach the service account you previously created:

    VM=cloudrun-gke-invoker-tutorial-vm
    
    gcloud compute instances create $VM \
        --scopes cloud-platform \
        --service-account $SERVICE_ACCOUNT_EMAIL
    

    This tutorial uses the instance name cloudrun-gke-invoker-tutorial-vm. You can change the name.

  2. Copy the file containing the public IP address of the Istio ingress gateway to the VM:

    gcloud compute scp external-ip.txt $VM:~
    
  3. Connect to the VM by using SSH:

    gcloud compute ssh $VM
    
  4. While in the SSH session, obtain an ID token from the Compute Engine metadata server:

    ID_TOKEN=$(curl -s -H Metadata-Flavor:Google \
        --data-urlencode format=full \
        --data-urlencode audience=http://example.com \
        http://metadata/computeMetadata/v1/instance/service-accounts/default/identity)
    
  5. Send a request with the ID token to the sample Cloud Run for Anthos service:

    curl -s -w"\n" -H "Host: sample.tutorial.example.com" \
        -H "Authorization: Bearer $ID_TOKEN" $(cat external-ip.txt)
    

    The output is:

    OK
    
  6. Leave the SSH session:

    exit
    

Use a client library

The Google authentication client libraries for Python, Java, Go, and Node.js let you obtain ID tokens using convenient APIs. To use the Python library and the IDTokenCredentials class, follow these steps:

  1. In Cloud Shell, create and download service account credentials:

    gcloud iam service-accounts keys create service-account.json \
        --iam-account $SERVICE_ACCOUNT_EMAIL
    
  2. Create a Python virtual environment:

    virtualenv -p python3 .venv
    
  3. Activate the Python virtual environment for this terminal session:

    source .venv/bin/activate
    
  4. Install the dependencies:

    pip install -r requirements.txt
    
  5. Obtain an ID token and send a request to the sample Cloud Run for Anthos service:

    python id_token_request.py http://$EXTERNAL_IP http://example.com \
        Host:sample.tutorial.example.com
    

    The output is:

    OK
    
    import os
    import sys
    from google.auth.transport.requests import AuthorizedSession
    from google.oauth2 import service_account
    
    
    def request(method, url, target_audience=None, service_account_file=None,
                data=None, headers=None, **kwargs):
        """Obtains a Google-issued ID token and uses it to make a HTTP request.
    
        Args:
          method (str): The HTTP request method to use
                ('GET', 'OPTIONS', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE')
          url: The URL where the HTTP request will be sent.
          target_audience (str): Optional, the value to use in the audience
                ('aud') claim of the ID token. Defaults to the value of 'url'
                if not provided.
          service_account_file (str): Optional, the full path to the service
                account credentials JSON file. Defaults to
                '<working directory>/service-account.json'.
          data: Optional dictionary, list of tuples, bytes, or file-like object
                to send in the body of the request.
          headers (dict): Optional dictionary of HTTP headers to send with the
                request.
          **kwargs: Any of the parameters defined for the request function:
                https://github.com/requests/requests/blob/master/requests/api.py
                If no timeout is provided, it is set to 90 seconds.
    
        Returns:
          The page body, or raises an exception if the HTTP request failed.
        """
        # Set target_audience, if missing
        if not target_audience:
            target_audience = url
    
        # Set service_account_file, if missing
        if not service_account_file:
            service_account_file = os.path.join(os.getcwd(),
                                                'service-account.json')
    
        # Set the default timeout, if missing
        if 'timeout' not in kwargs:
            kwargs['timeout'] = 90  # seconds
    
        # Obtain ID token credentials for the specified audience
        creds = service_account.IDTokenCredentials.from_service_account_file(
            service_account_file, target_audience=target_audience)
    
        # Create a session for sending requests with the ID token credentials
        session = AuthorizedSession(creds)
    
        # Send a HTTP request to the provided URL using the Google-issued ID token
        resp = session.request(method, url, data=data, headers=headers, **kwargs)
        if resp.status_code == 403:
            raise Exception('Service account {} does not have permission to '
                            'access the application.'.format(
                                creds.service_account_email))
        elif resp.status_code != 200:
            raise Exception(
                'Bad response from application: {!r} / {!r} / {!r}'.format(
                    resp.status_code, resp.headers, resp.text))
        else:
            return resp.text

The Identity-Aware Proxy documentation has sample code that demonstrates how to obtain Google-issued ID tokens using other programming languages.

Using the IAM Service Account Credentials API

If there is no Google authentication client library available for your programming language with the ability to create ID tokens, you can use the IAM Service Account Credentials API directly:

  1. Create a custom IAM role with permission to create ID tokens for service accounts:

    gcloud iam roles create serviceAccountIdTokenCreator \
        --project $GOOGLE_CLOUD_PROJECT \
        --description "Impersonate service accounts to create OpenID Connect ID tokens" \
        --permissions "iam.serviceAccounts.getOpenIdToken" \
        --stage GA \
        --title "Service Account ID Token Creator"
    

    You can use the predefined Service Account Token Creator role (iam.serviceAccountTokenCreator) instead of creating a custom role, but this role provides extra permissions that aren't needed to create ID tokens with the Service Account Credentials API.

  2. Create a policy binding to grant yourself the custom role on the service account you previously created:

    gcloud iam service-accounts add-iam-policy-binding $SERVICE_ACCOUNT_EMAIL \
        --member user:$(gcloud config get-value account) \
        --role projects/$GOOGLE_CLOUD_PROJECT/roles/serviceAccountIdTokenCreator
    

    It can take a moment for the policy binding to take effect. The following command checks if the policy binding is in effect every five seconds. It will stop checking and return you to the terminal prompt when the policy binding is in effect:

    status_code=""
    while [ "$status_code" != "200" ] ; do
        sleep 5
        echo "Checking permissions"
        status_code=$(curl -s -w "%{http_code}" -o /dev/null -X POST \
            -H "Authorization: Bearer $(gcloud auth print-access-token)" \
            --data-urlencode audience=http://example.com \
            --data-urlencode includeEmail=true \
            "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${SERVICE_ACCOUNT_EMAIL}:generateIdToken")
    done
    
  3. Use the generateIdToken method of the IAM Service Account Credentials API to generate an ID token for the service account:

    ID_TOKEN=$(curl -s -X POST \
        -H "Authorization: Bearer $(gcloud auth print-access-token)" \
        --data-urlencode audience=http://example.com \
        --data-urlencode includeEmail=true \
        "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${SERVICE_ACCOUNT_EMAIL}:generateIdToken" | jq -r '.token')
    
  4. Send a request with the ID token to the sample Cloud Run for Anthos service:

    curl -s -w"\n" -H "Host: sample.tutorial.example.com" \
        -H "Authorization: Bearer $ID_TOKEN" $EXTERNAL_IP
    

    The output is:

    OK
    

Use the gcloud command-line tool

To obtain Google-issued ID tokens, you can use the gcloud command-line tool. These tokens identify yourself or a service account, which can be useful for testing services interactively.

  1. Grant yourself the predefined Service Account Token Creator role on the service account you previously created:

    gcloud iam service-accounts add-iam-policy-binding $SERVICE_ACCOUNT_EMAIL \
        --member user:$(gcloud config get-value account) \
        --role roles/iam.serviceAccountTokenCreator
    

    It can take a moment for the policy binding to take effect. The following command checks if the policy binding is in effect every five seconds. It will stop checking and return you to the terminal prompt when the policy binding is in effect:

    status_code=""
    while [ "$status_code" != "200" ] ; do
        sleep 3
        echo "Checking permissions"
        status_code=$(curl -s -w "%{http_code}" -o /dev/null -X POST \
            -H "Authorization: Bearer $(gcloud auth print-access-token)" \
            -H "Content-Type: application/json" \
            -d '{"payload": "{}"}' \
            "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${SERVICE_ACCOUNT_EMAIL}:signJwt")
    done
    
  2. Impersonate the service account and obtain an ID token:

    ID_TOKEN=$(gcloud auth print-identity-token \
        --audiences http://example.com \
        --impersonate-service-account $SERVICE_ACCOUNT_EMAIL \
        --include-email)
    
  3. Send a request with the ID token to the sample Cloud Run for Anthos service:

    curl -s -w"\n" -H "Host: sample.tutorial.example.com" \
        -H "Authorization: Bearer $ID_TOKEN" $EXTERNAL_IP
    

    The output is:

    OK
    

Cleaning up

To avoid incurring charges to your Google Cloud account for the resources used in this tutorial, either delete the project that contains the resources, or keep the project and delete the individual resources.

Delete the project

  1. In the Cloud Console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

Delete the resources

If you want to keep the Google Cloud project you used in this tutorial, delete the individual resources:

  1. Delete the GKE cluster:

    gcloud container clusters delete $CLUSTER --async --quiet
    
  2. Delete the service account:

    gcloud iam service-accounts delete $SERVICE_ACCOUNT_EMAIL --quiet
    
  3. Delete the service account credentials file:

    rm -f service-account.json
    
  4. Delete the Compute Engine instance:

    gcloud compute instances delete $VM --quiet
    

What's next