Using Google Cloud Service Account impersonation with Terraform

feature-image.png
CONTENTS

    Google Cloud service accounts are a special kind of account typically used by applications and other resources in your Google Cloud project to access APIs and services. Best practice suggests that you should limit your personal account permissions and run your Terraform code with dedicated service accounts which have just the right permissions to perform the configuration required. In this post I will demonstrate how to configure a Terraform project to use service account impersonation and avoid managing service account keys.

    Service account keys vs impersonation

    There are two main ways to use service accounts in Google Cloud, service account keys and service account impersonation.

    Using service account keys involves creating and downloading a JSON keyfile and setting the GOOGLE_APPLICATION_CREDENTIALS environment variable to the file location on your filesystem. Applications such as the gcloud CLI and Terraform are aware of this variable and will use the private key found in the keyfile to authenticate as the service account when performing actions.

    This method is a security nightmare. As you can imagine, service accounts running Terraform configurations are often very powerful. So, having multiple plain text keyfiles stored all over the place, increasing the risk of private keys getting into the wrong hands, probably isn’t the best idea.

    This brings me onto service account impersonation. Service account impersonation is where a principal, such as a user or another service account, creates short-lived credentials to authenticate as a service account. These credentials do not require service account keys making it a much more secure solution.

    Configuring service account impersonation IAM bindings

    To allow a principal to impersonate a service account they must be granted the iam.serviceAccountTokenCreator role on the service account. This can be assigned at the project level to allow a principal to impersonate all service accounts in a project. However, it is best practice to assign the role at the service account level itself, to adhere to the principal of least privilege.

    For example, to allow the user joe.bloggs@example.com to impersonate the service account terraform-admin@my-project.iam.gserviceaccount.com you can assign the iam.serviceAccountTokenCreator role as follows.

    gcloud iam service-accounts add-iam-policy-binding \
        terraform-admin@my-project.iam.gserviceaccount.com \
        --member='user:joe.bloggs@example.com' \
        --role='roles/iam.serviceAccountTokenCreator'
    

    The principal can then impersonate the terraform-admin@my-project.iam.gserviceaccount.com service account be setting the GOOGLE_IMPERSONATE_SERVICE_ACCOUNT environment variable, for example:

    export GOOGLE_IMPERSONATE_SERVICE_ACCOUNT=terraform-admin@my-project.iam.gserviceaccount.com
    

    Applications such as the gcloud CLI and Terraform are aware of this environment variable and will attempt to impersonate the service account when performing any actions. However, this is not always the best option when impersonating service accounts with Terraform, as the following section will explain.

    Impersonating service accounts in Terraform providers

    As mentioned above, exporting the GOOGLE_IMPERSONATE_SERVICE_ACCOUNT environment variable is not always the best way to impersonate service accounts. For one thing, it can be cumbersome to have to remember to export the GOOGLE_IMPERSONATE_SERVICE_ACCOUNT environment variable for every new shell session. But also, it can often be useful in complex Terraform configurations to impersonate multiple service accounts. For example, maybe you have a specific service account which has permissions to create monitoring resources in a central monitoring project, but which doesn’t have permission to create resources in the main project. This is where configuring the impersonation directly in the Terraform providers can be useful.

    First, create a google provider as follows.

    provider "google" {
      alias = "impersonation"
      scopes = [
        "https://www.googleapis.com/auth/cloud-platform",
        "https://www.googleapis.com/auth/userinfo.email",
      ]
    }
    

    This provider will run in the context of your personal user account (as it doesn’t have any other credentials or access tokens configured) and has the impersonation alias.

    Next, add a data block to retrieve the short-lived access token which will be used to authenticate as the target service account. Notice that you’re explicitly telling the data source to use the google.impersonation aliased provider, thus requesting the access token via your personal user account.

    data "google_service_account_access_token" "default" {
      target_service_account 	= "terraform-admin@my-project.iam.gserviceaccount.com"
      scopes                 	= ["userinfo-email", "cloud-platform"]
    
      provider               	= google.impersonation
    }
    

    And finally, create a second google provider that will use the access token of your service account. Notice that this provider doesn’t have an alias, meaning it’ll be the default provider used for any Google resources in your Terraform code.

    provider "google" {
       access_token = data.google_service_account_access_token.default.access_token
    }
    

    Using aliases to configure multiple providers with different service accounts

    I mentioned in the previous section that it can be useful in complex Terraform configurations to impersonate multiple service accounts. Take the example of creating monitoring resources in a central monitoring project using a different service account to the one which can create resources in the main project. You could expand on the previous example as follows.

    provider "google" {
      alias = "impersonation"
      scopes = [
        "https://www.googleapis.com/auth/cloud-platform",
        "https://www.googleapis.com/auth/userinfo.email",
      ]
    }
    
    data "google_service_account_access_token" "default" {
      provider               	= google.impersonation
      target_service_account 	= "terraform-admin@my-project.iam.gserviceaccount.com"
      scopes                 	= ["userinfo-email", "cloud-platform"]
    }
    
    data "google_service_account_access_token" "monitoring" {
      provider               	= google.impersonation
      target_service_account 	= "terraform-monitoring@my-project.iam.gserviceaccount.com"
      scopes                 	= ["userinfo-email", "cloud-platform"]
    }
    
    provider "google" {
      access_token = data.google_service_account_access_token.default.access_token
    }
    
    provider "google" {
      alias = "monitoring"
      access_token = data.google_service_account_access_token.monitoring.access_token
    }
    

    By creating another google provider, this time with the alias monitoring, and setting the access_token to that of a second impersonated service account (terraform-monitoring@my-project.iam.gserviceaccount.com in the example above), you’re now able to specify that certain resources be created using the terraform-monitoring service account via the provider meta-argument on the resource.

    For example, the following logging metric would be creating using the default provider, which is impersonating the terraform-admin@my-project.iam.gserviceaccount.com service account.

    resource "google_logging_metric" "main" {
      name   = "my-custom/metric"
      # More arguments...
    }
    

    And the following alert policy uses the google.monitoring aliased provider, meaning that it will be created using the terraform-monitoring@my-project.iam.gserviceaccount.com service account. It specifies the google.monitoring provider using the provider meta-argument of the google_monitoring_alert_policy resource.

    resource "google_monitoring_alert_policy" "main" {
      display_name = "My Alert Policy"
      combiner     = "OR"
      conditions {
        display_name = "test condition"
        # More arguments...
      }
    
      provider = google.monitoring
    }
    

    Hopefully this demonstrates how flexible these configurations can be in allowing you to configure service accounts with just the right level of permissions to perform their required tasks.

    Impersonating service accounts when accessing remote state files

    There is one gotcha though. Remote state.

    When using the gcs backend, you’ll need to explicitly tell it to impersonate a service account using the impersonate_service_account argument. This is because Terraform needs to access the state bucket before it even thinks about loading any of the provider configurations that you configured above. However, this is pretty trivial to configure as you can see in the example below.

    terraform {
      backend "gcs" {
        bucket                      = "my-bucket-name"
        impersonate_service_account = "terraform-state@my-project.iam.gserviceaccount.com"
      }
    }
    

    It’s worth noting that the argument values in the backend "gcs" block cannot be variables, so you’ll need to hard code the service account email. This can be a pain if you’re using workspaces with different service accounts per-workspace. In this specific situation I’ve often opted to create a dedicated terraform-state service account which only has permissions to manage the state files for all workspaces of a project, nothing else.

    Conclusion

    I hope this post has been helpful in explaining the pros of using Google’s service account impersonation in Terraform, and in demonstrating how easily it can be configured. Thanks for reading!

    ×