Google Cloud Doing service account things without a service account key

Before we deploy an application to Google Cloud, we typically want to test it locally. If the application uses Google Cloud APIs, then we somehow need to ensure that the application can authenticate. We could use a service account key for that, but the best practices for using and managing service accounts (guess who wrote these) advise against that approach:

During your daily work, you might use tools such as the Google Cloud CLI, gsutil, or terraform. Don't use a service account to run these tools. Instead, let them use your credentials by running `gcloud auth login` (for the gcloud CLI and gsutil) or `gcloud auth application-default login` (for terraform and other third-party tools) first.
You can use a similar approach for developing and debugging applications that you plan to deploy on Google Cloud. Once deployed, the application might require a service account—but if you run it on your local workstation, you can let it use your personal credentials.
To help ensure that your application supports both personal credentials and service account credentials, use the Cloud Client Libraries to find credentials automatically.

Running gcloud auth application-default login once, and letting applications use application default credentials works great 95% of the time…but sometimes it doesn’t.

Most Google Cloud APIs don’t care whether we use a service account or a user account to authenticate – as long as we have a valid access token, we’re good. But there are a few things that only service accounts can do – for example, requesting ID tokens with a custom audience.

Things only service accounts can do

Let’s say we have an application that uses ID tokens to authenticate service-to-service calls. When deployed on Cloud Run or a Compute Engine instance with an attached service account, the application can request ID tokens by calling the metadata server:

http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=AUDIENCE

The metadata server allows us to specify any audience we want, and returns an ID token that asserts the identity of the service account and uses the requested audience as aud claim:

{
  "alg": "RS256",
  "kid": "cec13debf4b96479683736205082466c14797bd0",
  "typ": "JWT" 
}.{
  "aud": "bob",                     ← requested audience
  "azp": "101860428376911733634",
  "exp": 1649406237,
  "iat": 1649402637,
  "iss": "https://accounts.google.com",
  "sub": "101860428376911733634"    ← service account
}.[Signature]

But calling the metadata server obviously doesn’t work if we run the application locally. Worse yet, the capability to request ID tokens with arbitrary audiences isn’t available to normal users. As a user, the only way to obtain an ID token is to sign in using OIDC, and even then we can’t control the audience of the ID token – it’ll always use the app’s client ID as aud.

But there’s a way to do things only service accounts can do – we can impersonate a service account.

Doing service account things as a user

The idea of impersonating a service account is that we obtain an access token or ID token, but not for ourselves, but for a service account. Technically, the most common way to do that is to call one of two APIs:

Calling these methods requires the iam.serviceAccounts.getAccessToken and iam.serviceAccounts.getOpenIdToken permissions. One way to grant these permissions is to assign the Service Account Token Creator role. But that role is actually more powerful than necessary, so using the (more narrow) Workload Identity User role might be more appropriate.

Most client libraries support impersonation, so we don’t actually have to worry about calling these methods directly.

Turning back to the original problem of local development, here’s what we can do:

  1. Create a service account that we can use for developing. Ideally, each developer uses their own, personal service account.

    But we don’t create a service account key.

  2. Grant our own (Cloud Identity/Workspace/Gmail) user the Workload Identity User role on that service account. That enables impersonation.

  3. Add an ImpersonateServiceAccount configuration option to our application that (optionally) instructs the application to impersonate a service account.

In the startup logic of our app, we then check whether the ImpersonateServiceAccount configuration option is set. If so, we impersonate that service account – otherwise, we simply skip this step.

In the Startup class of an ASP.NET Core application, this could look like:

public void ConfigureServices(IServiceCollection services)
{
  //
  // Lookup credentials.
  //
  ICredential appCredential;
  if (this.Configuration["ImpersonateServiceAccount"] is var serviceAccount &&
    !string.IsNullOrEmpty(serviceAccount))
  {
    //
    // Use an impersonated service account - required to support
    // local development scenarios where the application default
    // credential is a user credential.
    //
    appCredential = GoogleCredential.GetApplicationDefault()
      .Impersonate(new ImpersonatedCredential.Initializer(serviceAccount)
      {
        Scopes = new[] { "https://www.googleapis.com/auth/cloud-platform" }
      });
  }
  else
  {
    //
    // Use application default credentials.
    //
    appCredential = GoogleCredential.GetApplicationDefault();
  }
  //
  // Make credential available for dependency injection.
  //
  services.AddSingleton<ICredential>(appCredential);
  
  ...
}

By following this approach, we can let the application do things only service accounts can do – without the risk and burden of managing service account keys.

Any opinions expressed on this blog are Johannes' own. Refer to the respective vendor’s product documentation for authoritative information.
« Back to home