Service Account Scoping & Cross-Project Access
Service Account Fundamentals
Service accounts là machine identity để authenticate GCP APIs:
User → User identity (email@company.com)
Machine → Service account (@project.iam.gserviceaccount.com)Two authentication methods:
- Keys (long-lived, traditional)
- Tokens (short-lived, recommended)
Keys vs Tokens
Keys (Old pattern):
- JSON file with private key
- Long-lived (manual rotation needed)
- Stored on VMs/laptops (theft risk)
- Still used for:
- Local development
- External tools
- Legacy systems
Tokens (New pattern):
- Automatically generated
- Short-lived (1 hour default)
- Automatically rotated
- Generated by:
- gcloud CLI
- Workload Identity (GKE/Cloud Run)
- Service account impersonationScoping Service Accounts
Service account should have minimum required permissions:
python
# Example: Batch processor service account
# What it needs:
# 1. Read from Cloud Storage (input data)
# 2. Write to BigQuery (results)
# 3. Write logs
# ❌ BAD: Owner role (too permissive)
grant_role(service_account, "roles/owner")
# ✓ GOOD: Custom role with minimum permissions
custom_role_permissions = [
"storage.buckets.get",
"storage.objects.get",
"bigquery.datasets.get",
"bigquery.tables.get",
"bigquery.tables.update",
"bigquery.tables.updateData",
"logging.logEntries.create"
]
grant_custom_role(service_account, custom_role_permissions)Cross-Project Service Account Access
Pattern 1: Direct Project Access
Service Project A → Grant SA role ← Host Project
(where SA is created) (uses resource)bash
# Create service account ở project A
gcloud iam service-accounts create batch-processor \
--project=PROJECT_A
# Grant role ở project B (to use resources)
gcloud projects add-iam-policy-binding PROJECT_B \
--member=serviceAccount:batch-processor@PROJECT_A.iam.gserviceaccount.com \
--role=roles/bigquery.dataEditorPattern 2: Impersonation Chain
Service account A impersonates B → C (nested):
bash
# Service account hierarchy
# VM runs as: vm-sa@project-a
# Needs to access: resources ở project-b
# Step 1: vm-sa@project-a has permission to impersonate batch-sa@project-b
gcloud iam service-accounts add-iam-policy-binding \
batch-sa@project-b.iam.gserviceaccount.com \
--role=roles/iam.serviceAccountTokenCreator \
--member=serviceAccount:vm-sa@project-a.iam.gserviceaccount.com
# Step 2: VM code
from google.auth import impersonated_credentials
credentials = impersonated_credentials.Credentials(
source_credentials=compute.Credentials(),
target_principal="batch-sa@project-b.iam.gserviceaccount.com",
target_scopes=["https://www.googleapis.com/auth/cloud-platform"]
)
# Step 3: Use credentials to access project B resources
bigquery_client = bigquery.Client(
project="project-b",
credentials=credentials
)Workload Identity (Recommended)
Modern approach: Remove service account keys entirely:
yaml
# GKE Pod with Workload Identity
# 1. Pod service account (Kubernetes level)
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: default
# 2. Bind to GCP service account
# Pod runs as: app-sa (k8s) → app-gcp@project.iam.gserviceaccount.com (GCP)
# In code:
from google.cloud import bigquery
from google.auth import default
# Automatically uses Workload Identity token
credentials, project = default()
client = bigquery.Client(
project=project,
credentials=credentials
)Advantages of Workload Identity:
- No keys to steal
- Automatic token rotation
- Pod-level access control
- Works with GKE, Cloud Run, Cloud Functions
Cross-Organization Access (Advanced)
Rare scenario: Service account ở org A needs access to org B:
bash
# This requires explicit cross-org trust
# Org A (source):
# Service account: batch@proj-a.iam.gserviceaccount.com
# Org B (target):
# 1. Create external identity pool (allows Org A identities)
# 2. Map org-a service account to org-b service account
# 3. Grant permissions
# Very complex, usually not needed. Better to:
# - Keep resources in same organization
# - Use APIs/webhooks for inter-org communicationService Account Keys Security
If keys are necessary (local dev, legacy):
bash
# Create and immediately save
gcloud iam service-accounts keys create key.json \
--iam-account=dev-sa@project-id.iam.gserviceaccount.com
# Immediately restrict file permissions
chmod 400 key.json
# Store in secure location (not Git!)
# Use environment variables
export GOOGLE_APPLICATION_CREDENTIALS=/secure/key.json
# Rotate keys regularly
gcloud iam service-accounts keys list \
--iam-account=dev-sa@project-id.iam.gserviceaccount.com
# Delete old keys
gcloud iam service-accounts keys delete KEY_ID \
--iam-account=dev-sa@project-id.iam.gserviceaccount.comAudit Trail for Impersonation
python
def audit_service_account_usage():
"""Log who is using service account"""
from google.cloud import logging as cloud_logging
client = cloud_logging.Client()
# Query audit logs for service account usage
query = """
protoPayload.authenticationInfo.principalEmail ~ ".*@iam.gserviceaccount.com"
AND protoPayload.methodName ~ ".*iam.serviceaccounts.implicitDelegationCheck"
"""
entries = client.list_entries(filter_=query)
for entry in entries:
principal = entry.payload.get('authenticationInfo', {}).get('principalEmail')
method = entry.payload.get('methodName')
print(f"{principal} performed {method}")Terraform Service Account Management
hcl
# Create service account
resource "google_service_account" "batch" {
project = var.project_id
account_id = "batch-processor"
display_name = "Batch Processing Service Account"
}
# Grant permissions ở different project
resource "google_project_iam_member" "batch_bigquery_access" {
project = var.data_project_id
role = "roles/bigquery.dataEditor"
member = "serviceAccount:${google_service_account.batch.email}"
}
# Create key (only if necessary)
resource "google_service_account_key" "batch_key" {
service_account_id = google_service_account.batch.name
public_key_type = "TYPE_X509_PEM_FILE"
}
# Store key securely (never in version control!)
output "service_account_key" {
value = google_service_account_key.batch_key.private_key
sensitive = true
}Cost Tracking by Service Account
sql
-- BigQuery: Track API calls per service account
SELECT
protoPayload.authenticationInfo.principalEmail as service_account,
protoPayload.methodName as operation,
COUNT(*) as count,
DATE(timestamp) as date
FROM `project.dataset.cloudaudit_googleapis_com_activity`
WHERE DATE(timestamp) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
GROUP BY service_account, operation, date
ORDER BY count DESC;Anti-Patterns
| Anti-pattern | Problem | Solution |
|---|---|---|
| Service account keys on laptops | Key theft | Use gcloud CLI / Workload Identity |
| Owner role for service accounts | Can do anything | Least privilege custom roles |
| Same SA for all services | Cannot audit per-service | Create per-service SAs |
| Long-lived keys | Compromise window | Use short-lived tokens |
| No key rotation | Old compromised keys still valid | Automated key rotation |