Skip to main content

IAM & Service Accounts

1. Overview

This document specifies the GCP IAM configuration for the Pinpoint Payment Gateway. We follow the principle of least privilege, each service gets only the permissions it needs.


2. Service Accounts

Service AccountUsed ByPurpose
gateway-processing@{project}.iamProcessing MicroserviceTransIT API calls, DB access, Pub/Sub publish, Secret Manager
gateway-management@{project}.iamManagement MicroserviceDB access, Firebase Admin, Identity Platform, FCM terminal commands, Secret Manager
gateway-online-txn@{project}.iamOnline Txn MicroserviceDB access, Pub/Sub, App Check, Secret Manager
gateway-merchant-onboarding@{project}.iamMerchant Onboarding MicroserviceMerchant activation, XTMS API, App Check, Secret Manager
gateway-auth@{project}.iamGateway Auth ServiceOAuth token issuance, App Check, Secret Manager
gateway-status@{project}.iamStatus MicroserviceHealth checks, LB/Run viewer
gateway-github-actions@{project}.iamGitHub Actions CI/CDDeploy to Cloud Run, push images

3. IAM Role Bindings

Microservice Common Roles

All microservices (except status) get these base roles:

resource "google_project_iam_member" "spanner_database_user" {
for_each = local.db_services # processing, management, online-txn, merchant-onboarding, auth

project = var.project_id
role = "roles/spanner.databaseUser"
member = "serviceAccount:${local.service_accounts[each.key].email}"
}

resource "google_project_iam_member" "cloud_trace_agent" {
for_each = local.trace_agent_services # all services

project = var.project_id
role = "roles/cloudtrace.agent"
member = "serviceAccount:${local.service_accounts[each.key].email}"
}

Identity & App Check Roles

resource "google_project_iam_member" "identity_toolkit_viewer" {
for_each = local.identity_toolkit_viewers # management, auth, online-txn

project = var.project_id
role = "roles/identitytoolkit.viewer"
member = "serviceAccount:${local.service_accounts[each.key].email}"
}

resource "google_project_iam_member" "identity_toolkit_admin_management" {
project = var.project_id
role = "roles/identitytoolkit.admin"
member = "serviceAccount:${google_service_account.management.email}"
}

resource "google_project_iam_member" "firebase_app_check_token_verifier" {
for_each = local.app_check_token_verifiers # auth, management, online-txn, merchant-onboarding

project = var.project_id
role = "roles/firebaseappcheck.tokenVerifier"
member = "serviceAccount:${local.service_accounts[each.key].email}"
}

Firebase Cloud Messaging for Terminal Commands

The management service sends Gateway runtime commands to terminals through Firebase Cloud Messaging (FCM). Terraform provisions the Pub/Sub command-dispatch resources and the gateway-fcm-service-account-json Secret Manager secret in infra/tf/gcp/modules/device-runtime/main.tf.

The secret value is not managed by Terraform. Populate it with the Firebase Admin SDK service-account JSON before enabling live terminal-command dispatch:

gcloud secrets versions add gateway-fcm-service-account-json \
--data-file=/path/to/firebase-adminsdk.json

The management Cloud Run service account must also be allowed to send FCM messages on the Firebase project. Grant either roles/firebase.sdkAdminServiceAgent or a custom role that includes firebase.messaging.send:

gcloud projects add-iam-policy-binding <FIREBASE_PROJECT_ID> \
--member="serviceAccount:<MANAGEMENT_SA_EMAIL>" \
--role="roles/firebase.sdkAdminServiceAgent"

Keep this Firebase-project-level binding aligned with the device-runtime Terraform module note. It is separate from the GCP project Secret Manager accessor binding that Terraform manages for the FCM JSON secret.

Auth Service Token Signing

resource "google_service_account_iam_member" "auth_custom_token_signer" {
service_account_id = google_service_account.auth.name
role = "roles/iam.serviceAccountTokenCreator"
member = "serviceAccount:${google_service_account.auth.email}"
}

Status Service Roles

resource "google_project_iam_member" "status_lb_viewer" {
project = var.project_id
role = google_project_iam_custom_role.status_lb_viewer.name
member = "serviceAccount:${google_service_account.status.email}"
}

resource "google_project_iam_member" "status_run_viewer" {
project = var.project_id
role = "roles/run.viewer"
member = "serviceAccount:${google_service_account.status.email}"
}

GitHub Actions Deploy Service Account

resource "google_project_iam_member" "github_actions_roles" {
for_each = toset([
"roles/artifactregistry.writer",
"roles/run.developer",
])

project = var.project_id
role = each.value
member = "serviceAccount:${google_service_account.github_actions.email}"
}

resource "google_service_account_iam_member" "github_actions_sa_user_microservice" {
for_each = local.service_accounts

service_account_id = each.value.name
role = "roles/iam.serviceAccountUser"
member = "serviceAccount:${google_service_account.github_actions.email}"
}

4. Secret Manager Access

Secrets Inventory

Secret NameAccessible ByDescription
gateway-database-urlAll except statusSpanner JDBC connection URL
gateway-internal-api-shared-secretmanagement, online-txn, processingInter-service authentication secret
gateway-fcm-service-account-jsonmanagementFirebase Admin SDK service-account JSON for terminal-command FCM dispatch
transit-developer-idprocessingTransIT API developer identifier
transit-developer-keyprocessingTransIT API authentication key
oauth-signing-keyauthKey for signing OAuth JWTs

Per-Secret IAM (Fine-Grained)

resource "google_secret_manager_secret_iam_member" "database_url" {
for_each = local.db_services

secret_id = var.database_url_secret_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${local.service_accounts[each.key].email}"
}

resource "google_secret_manager_secret_iam_member" "internal_api_shared_secret" {
for_each = local.internal_api_secret_callers

secret_id = var.internal_api_shared_secret_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${local.service_accounts[each.key].email}"
}

resource "google_secret_manager_secret_iam_member" "transit_developer_id_processing" {
secret_id = var.transit_developer_id_secret_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.processing.email}"
}

resource "google_secret_manager_secret_iam_member" "oauth_signing_key_auth" {
secret_id = var.oauth_signing_key_secret_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.auth.email}"
}

5. Service-to-Service Authentication

Cloud Run services authenticate to each other using GCP IAM identity:

# Allow management service to invoke processing service
resource "google_cloud_run_v2_service_iam_member" "management_to_processing" {
project = var.project_id
location = var.region
name = "gateway-processing"
role = "roles/run.invoker"
member = "serviceAccount:${google_service_account.management.email}"
}

# Allow management service to invoke auth service
resource "google_cloud_run_v2_service_iam_member" "management_to_auth" {
project = var.project_id
location = var.region
name = "gateway-auth"
role = "roles/run.invoker"
member = "serviceAccount:${google_service_account.management.email}"
}

No public access to the Processing Microservice, it is only callable by other internal services.


6. Human Access

RoleGCP IAM RoleScope
Platform Engineerroles/editorFull project access
Developerroles/viewer + roles/run.developerView + deploy
On-callroles/viewer + roles/logging.viewerView + logs
Security Auditorroles/iam.securityReviewer + roles/logging.viewerAudit

Workload Identity Federation (for GitHub Actions)

resource "google_iam_workload_identity_pool" "github" {
project = var.project_id
workload_identity_pool_id = "gateway-github-actions"
display_name = "Gateway GitHub Actions"
}

resource "google_iam_workload_identity_pool_provider" "github" {
project = var.project_id
workload_identity_pool_id = google_iam_workload_identity_pool.github.workload_identity_pool_id
workload_identity_pool_provider_id = "gateway-github-provider"
display_name = "Gateway GitHub OIDC"

attribute_mapping = {
"google.subject" = "assertion.sub"
"attribute.actor" = "assertion.actor"
"attribute.repository" = "assertion.repository"
"attribute.ref" = "assertion.ref"
}

oidc {
issuer_uri = "https://token.actions.githubusercontent.com"
}

attribute_condition = "assertion.repository == '${var.github_repository}'"
}

resource "google_service_account_iam_member" "github_wif_impersonation" {
service_account_id = google_service_account.github_actions.name
role = "roles/iam.workloadIdentityUser"
member = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github.name}/attribute.repository/${var.github_repository}"

condition {
title = "main-branch-only"
description = "Allow WIF impersonation from main branch"
expression = "attribute.repository == '${var.github_repository}' && attribute.ref == 'refs/heads/main'"
}
}