Cloud Run services can call each other securely using identity tokens, which are short-lived JWTs signed by Google.

Here’s a simple Go service that makes authenticated calls to another Cloud Run service.

package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"os"

	"cloud.google.com/go/compute/metadata"
)

func main() {
	http.HandleFunc("/", handler)
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}
	fmt.Printf("Listening on port %s\n", port)
	if err := http.ListenAndServe(":"+port, nil); err != nil {
		panic(err)
	}
}

func handler(w http.ResponseWriter, r *http.Request) {
	targetServiceURL := "https://target-service-url.a.run.app" // Replace with your target service URL
	targetAudience := "https://target-service-url.a.run.app"   // Replace with your target service URL

	token, err := metadata.NewClient(nil).OnBehalfOfGoogle(targetAudience)
	if err != nil {
		http.Error(w, fmt.Sprintf("Failed to get identity token: %v", err), http.StatusInternalServerError)
		return
	}

	req, err := http.NewRequest("GET", targetServiceURL, nil)
	if err != nil {
		http.Error(w, fmt.Sprintf("Failed to create request: %v", err), http.StatusInternalServerError)
		return
	}
	req.Header.Set("Authorization", "Bearer "+token)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		http.Error(w, fmt.Sprintf("Failed to call target service: %v", err), http.StatusInternalServerError)
		return
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		http.Error(w, fmt.Sprintf("Failed to read response body: %v", err), http.StatusInternalServerError)
		return
	}

	fmt.Fprintf(w, "Target service responded: %s\n", string(body))
}

This setup allows one Cloud Run service, acting as a client, to securely invoke another Cloud Run service, acting as the server. The client service obtains an identity token from the Compute Metadata server, which is available within the Cloud Run environment. This token is then passed in the Authorization: Bearer <token> header of the HTTP request to the target service.

The target service must be configured to require authentication. When it receives a request with an identity token, it verifies the token’s signature and checks if the aud (audience) claim matches its own service URL. If both are valid, the request is allowed to proceed. This mechanism ensures that only authenticated and authorized services can access your APIs.

To configure your target Cloud Run service to require authentication:

  1. Deploy your target service:

    gcloud run deploy TARGET_SERVICE_NAME \
      --image gcr.io/PROJECT_ID/TARGET_IMAGE \
      --platform managed \
      --region REGION \
      --no-allow-unauthenticated
    

    The --no-allow-unauthenticated flag is crucial. It forces all incoming requests to be authenticated.

  2. Grant the client service’s identity the run.invoker role: On the target Cloud Run service, you need to grant the client service’s runtime identity the permission to invoke it. The client service’s identity is its service account.

    First, find the client service account. If you didn’t specify one during deployment, Cloud Run automatically creates a default service account for your service. This default service account has the format: PROJECT_NUMBER-compute@developer.gserviceaccount.com for the project’s default Compute Engine service account, or SERVICE_NAME@PROJECT_ID.iam.gserviceaccount.com if you specified a custom service account.

    Let’s assume your client service is named my-client-service and your project ID is my-gcp-project. The service account would likely be my-client-service@my-gcp-project.iam.gserviceaccount.com.

    Then, grant the run.invoker role to this service account on the target service:

    gcloud run services add-iam-policy-binding TARGET_SERVICE_NAME \
      --member "serviceAccount:my-client-service@my-gcp-project.iam.gserviceaccount.com" \
      --role "roles/run.invoker" \
      --region REGION \
      --platform managed
    

The metadata.NewClient(nil).OnBehalfOfGoogle(targetAudience) call is the core of the client-side operation. It communicates with the Compute Metadata server running on the same VM instance that your Cloud Run service is running on. This server acts as a secure intermediary, generating a short-lived, signed JWT for the specific targetAudience you provide. The targetAudience must precisely match the URL of the service you are trying to invoke. The metadata server uses the runtime service account of your Cloud Run service to sign this token, proving that the request originates from a legitimate instance associated with that service account.

The targetAudience is not just the URL; it’s a specific identifier that the token is issued for. When the target Cloud Run service receives the token, it decodes it and checks the aud claim. If this claim matches the service’s own URL (which is what you configure when deploying the target service), and the token is successfully verified as signed by Google, the request is considered authentic. This prevents a token issued for service A from being used to authenticate a call to service B.

A common pitfall is using a generic audience like https://www.googleapis.com/ or omitting the targetAudience entirely. The OnBehalfOfGoogle method requires a specific audience that aligns with the endpoint being called. If the audience in the token doesn’t match the expected audience for the target service, the validation will fail, and the target service will reject the request, typically with a 401 Unauthorized or 403 Forbidden error. You must ensure the targetAudience passed to OnBehalfOfGoogle is identical to the URL of the target Cloud Run service.

If your target service is not configured with --no-allow-unauthenticated, and instead allows unauthenticated access, the identity token check will not be performed by Cloud Run itself. In this scenario, the target service would need to implement its own logic to validate the identity token. However, the recommended and most secure approach is to enforce authentication at the Cloud Run level using --no-allow-unauthenticated.

The next hurdle is handling token expiration. Identity tokens are short-lived, typically for one hour. Your client service should be prepared to refresh the token if it expires during a long-running operation or if the target service returns a 401 error indicating an invalid or expired token. The metadata.NewClient(nil).OnBehalfOfGoogle() call will automatically fetch a fresh token each time it’s invoked, so you don’t need to manage expiration manually if you simply call it before each authenticated request.

When you encounter a 403 Forbidden error on the target service, double-check the IAM policy binding. The most frequent cause is that the service account of the calling service has not been granted the run.invoker role on the target service. Ensure the member in the gcloud run services add-iam-policy-binding command precisely matches the service account your client Cloud Run service is running as, and that the role is roles/run.invoker.

Want structured learning?

Take the full Cloud-run course →