This issue has been assessed as high severity. Review affected configurations immediately.
Google Cloud Secret Manager stores API keys, database credentials, TLS certificates, and other sensitive values used by GCP workloads. When it works correctly, it eliminates the need to embed credentials in code, environment variables, or configuration files. When IAM is misconfigured, it becomes the single storage location for every credential in an organisation’s GCP environment — a high-value target that, if compromised, can provide lateral movement paths to every downstream service the secrets protect.
This guide covers how Secret Manager attack paths work in practice, what misconfigured IAM looks like in GCP environments, and the controls that prevent credential exfiltration.
How Secret Manager Works (and Where IAM Controls Live)
Secret Manager stores secrets as versioned resources. A secret is a resource within a GCP project; versions are the actual credential values. IAM permissions control access at two granularities:
- Project-level: A principal with
secretmanager.secretAccessoron the project can access all secrets in that project - Resource-level: IAM bindings on individual secrets allow scoping access to specific secrets
The three relevant permissions:
| Role | Permissions | Attack Risk |
|---|---|---|
roles/secretmanager.secretAccessor | Access secret versions (read credential values) | Critical — the direct exfiltration permission |
roles/secretmanager.secretVersionManager | Create, destroy, disable secret versions | High — enables credential rotation attacks |
roles/secretmanager.admin | Full control including IAM policy modification | Critical — can grant secretAccessor to any principal |
Common Attack Paths
Path 1: Over-Privileged Workload Identity
The most common Secret Manager misconfiguration is assigning secretmanager.secretAccessor at the project level to a Workload Identity used by a Cloud Run service, GKE pod, or Compute Engine instance. This means any code running in that workload — including code injected through a vulnerability — can exfiltrate every secret in the project.
What over-privileged workload identity looks like in Terraform:
# VULNERABLE: grants access to ALL secrets in the project
resource "google_project_iam_member" "bad_binding" {
project = var.project_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.app.email}"
}
# CORRECT: grant access only to the specific secret this workload needs
resource "google_secret_manager_secret_iam_member" "scoped_binding" {
project = var.project_id
secret_id = google_secret_manager_secret.db_password.secret_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.app.email}"
}
Attack scenario: An SSRF vulnerability in a Cloud Run service allows an attacker to reach the GCP metadata server and retrieve the workload identity access token. With that token and project-level secretAccessor, the attacker can enumerate and exfiltrate every secret in the project:
# Attacker with stolen access token lists all secrets
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://secretmanager.googleapis.com/v1/projects/PROJECT_ID/secrets?pageSize=100"
# Access a specific secret version value
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://secretmanager.googleapis.com/v1/projects/PROJECT_ID/secrets/SECRET_ID/versions/latest:access"
# Response contains base64-encoded payload with the credential value
Path 2: Default Service Account Abuse
Compute Engine instances and App Engine applications use the default service account by default if no specific service account is assigned. The default service account frequently has the Editor role on the project — which includes secretmanager.admin. An attacker who compromises any workload running with the default service account can access and modify all secrets in the project.
Detecting default service account usage:
# Find Compute Engine instances using the default service account
gcloud compute instances list \
--format="table(name,zone,serviceAccounts[0].email)" \
--filter="serviceAccounts.email:[email protected]"
# Find Cloud Run services using the default service account
gcloud run services list \
--format="table(metadata.name,spec.template.spec.serviceAccountName)" | \
grep "[email protected]"
Path 3: allUsers or allAuthenticatedUsers on Secret Resources
Secret Manager resource-level IAM policies can be accidentally misconfigured to grant access to allAuthenticatedUsers — any authenticated Google account — or allUsers — the entire internet. This is uncommon but has been observed in environments where IAM policies are modified via the Console rather than through IaC.
# Audit Secret Manager IAM policies for overly broad bindings
for secret in $(gcloud secrets list --format="value(name)"); do
policy=$(gcloud secrets get-iam-policy $secret --format=json 2>/dev/null)
if echo "$policy" | grep -q "allUsers\|allAuthenticatedUsers"; then
echo "OVERLY BROAD: $secret"
echo "$policy"
fi
done
Path 4: Lateral Movement via Secret-Stored Service Account Keys
Many organisations store service account JSON keys in Secret Manager as a way to share credentials between services. If an attacker accesses these stored keys, they obtain independent authentication material that persists beyond the session that enabled the initial access.
# Enumerate secrets that might contain service account keys
gcloud secrets list --filter="name:sa-key OR name:service-account OR name:credentials" \
--format="table(name,createTime,replication.automatic)"
Service account keys stored in Secret Manager should be replaced with Workload Identity Federation (WIF) or direct service account impersonation. If keys must be stored, they should have the minimum required permissions and a defined rotation schedule.
Path 5: Secret Version Enumeration and Stale Credentials
Secret Manager retains all versions of a secret until explicitly destroyed. Older versions may contain credentials that are no longer in use but still valid — particularly database passwords, API keys from external services, and signing keys. An attacker with secretAccessor can request all versions of a secret, not just the latest:
# List all versions of a secret (attacker perspective with secretAccessor)
curl -H "Authorization: Bearer $TOKEN" \
"https://secretmanager.googleapis.com/v1/projects/PROJECT/secrets/SECRET/versions"
# Access an older, potentially still-valid version
curl -H "Authorization: Bearer $TOKEN" \
"https://secretmanager.googleapis.com/v1/projects/PROJECT/secrets/SECRET/versions/1:access"
Hardening: Destroy old secret versions after rotation is confirmed successful. Set a secret version retention policy using the --next-rotation-time and --rotation-period flags.
Hardening Controls
1. Least-privilege IAM for secret access
Bind secretAccessor at the individual secret level, not the project level. For workloads that need to access multiple secrets, use roles/secretmanager.secretAccessor bindings on each specific secret rather than a project-wide binding.
# Grant a service account access to a single secret only
gcloud secrets add-iam-policy-binding SECRET_NAME \
--member="serviceAccount:[email protected]" \
--role="roles/secretmanager.secretAccessor"
2. Customer-Managed Encryption Keys (CMEK)
By default, Secret Manager encrypts secrets with Google-managed keys. CMEK replaces Google-managed keys with keys in Cloud KMS that you control. If an attacker gains access to secret data at the storage layer without using the API, CMEK-encrypted secrets are not useful without the KMS key.
resource "google_secret_manager_secret" "db_password" {
secret_id = "db-password"
replication {
user_managed {
replicas {
location = "europe-west2"
customer_managed_encryption {
kms_key_name = google_kms_crypto_key.secret_key.id
}
}
}
}
}
3. VPC Service Controls perimeter
Include Secret Manager in a VPC Service Controls perimeter to prevent access from outside the trusted network context. This prevents stolen access tokens from being used to access secrets from external IP addresses.
# Add secretmanager.googleapis.com to a VPC-SC perimeter
gcloud access-context-manager perimeters update PERIMETER_NAME \
--add-resources="projects/PROJECT_NUMBER" \
--add-restricted-services="secretmanager.googleapis.com"
4. Secret rotation enforcement
Configure rotation reminders (and, where the downstream service supports it, automatic rotation) using Secret Manager’s built-in rotation notifications:
gcloud secrets update SECRET_NAME \
--next-rotation-time="2026-10-15T00:00:00Z" \
--rotation-period="2592000s" # 30 days
5. Audit logging and alerting
Data Access audit logs for Secret Manager record every AccessSecretVersion call. Enable these logs and create alerts for anomalous patterns:
# Enable Data Access audit logs for Secret Manager
gcloud projects set-iam-policy PROJECT_ID policy.json
# policy.json must include:
# "auditConfigs": [{"service": "secretmanager.googleapis.com", "auditLogConfigs": [{"logType": "DATA_READ"}, {"logType": "DATA_WRITE"}]}]
Log-based alert for bulk secret access (potential exfiltration):
# Cloud Logging query for anomalous secret access volume
resource.type="audited_resource"
protoPayload.serviceName="secretmanager.googleapis.com"
protoPayload.methodName="google.cloud.secretmanager.v1.SecretManagerService.AccessSecretVersion"
Create a log-based metric on this query, then a Cloud Monitoring alert that fires when a single service account accesses more than 10 distinct secrets within 5 minutes — a pattern consistent with automated exfiltration but not normal application behaviour.
6. Organisation policy constraints
Enforce Secret Manager policies at the organisation level:
# Prevent storing service account keys in Secret Manager (if your policy prohibits it)
# This requires a custom organisation policy or detection logic rather than a built-in constraint
# Enforce CMEK for all Secret Manager secrets
gcloud resource-manager org-policies set-policy \
--organization=ORG_ID \
policy.yaml
# policy.yaml: constraint: constraints/gcp.restrictCloudKmsKeyUsage
Detection Summary
| Attack | Detection |
|---|---|
| Project-level secretAccessor on workload identity | IAM audit: check for project-level bindings via gcloud projects get-iam-policy |
| Bulk secret enumeration/access | Data Access logs: AccessSecretVersion volume alert |
| Default service account usage | Compute/Run inventory: filter for [email protected] |
| Old secret version access | Data Access logs: filter for version != “latest” with unusual principals |
| allUsers/allAuthenticatedUsers binding | IAM policy scan across all secrets |
The single highest-impact control for most GCP environments is moving from project-level to resource-level secretAccessor bindings. This change alone significantly constrains what an attacker can reach after compromising any individual workload identity.