feat(gcp): add secretmanager_secret_not_publicly_accessible check (#11025)

Co-authored-by: Lydia Vilchez <lydiavilchezlopez@gmail.com>
This commit is contained in:
s1ns3nz0
2026-06-22 19:55:42 +09:00
committed by GitHub
parent 8a1d7bcd6b
commit 5d5f0676e0
11 changed files with 506 additions and 0 deletions
+1
View File
@@ -13,6 +13,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `cloudsql_instance_high_availability_enabled` check for GCP provider, verifying Cloud SQL primary instances use `REGIONAL` availability for automatic zone failover [(#11024)](https://github.com/prowler-cloud/prowler/pull/11024)
- `cloudfunction_function_inside_vpc` check for GCP provider, verifying Cloud Functions have a Serverless VPC Access connector for private egress [(#11021)](https://github.com/prowler-cloud/prowler/pull/11021)
- `cloudfunction_function_not_publicly_accessible` check for GCP provider, detecting Cloud Functions with `allUsers` or `allAuthenticatedUsers` IAM invocation bindings [(#11022)](https://github.com/prowler-cloud/prowler/pull/11022)
- `secretmanager_secret_not_publicly_accessible` check for GCP provider, detecting Secret Manager secrets with public IAM bindings [(#11025)](https://github.com/prowler-cloud/prowler/pull/11025)
- `identity_storage_service_level_admins_scoped` check for OCI provider CIS 3.1 control 1.15, ensuring storage service-level administrators exclude delete permissions [(#11523)](https://github.com/prowler-cloud/prowler/pull/11523)
- `cosmosdb_account_automatic_failover_enabled` check for Azure provider [(#11031)](https://github.com/prowler-cloud/prowler/pull/11031)
- `cosmosdb_account_backup_policy_continuous` check for Azure provider [(#11032)](https://github.com/prowler-cloud/prowler/pull/11032)
@@ -0,0 +1,6 @@
from prowler.providers.common.provider import Provider
from prowler.providers.gcp.services.secretmanager.secretmanager_service import (
SecretManager,
)
secretmanager_client = SecretManager(Provider.get_global_provider())
@@ -0,0 +1,40 @@
{
"Provider": "gcp",
"CheckID": "secretmanager_secret_not_publicly_accessible",
"CheckTitle": "Secret Manager secret is not publicly accessible",
"CheckType": [],
"ServiceName": "secretmanager",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "critical",
"ResourceType": "secretmanager.googleapis.com/Secret",
"Description": "Secret Manager secrets deny access to `allUsers` and `allAuthenticatedUsers`, so only **explicitly authorized identities** can read or use them.\n\nThe evaluation reviews each secret's IAM policy bindings to confirm no public principals are granted access.",
"Risk": "Granting public IAM access to a secret exposes credentials, API keys, certificates, or other sensitive values to any caller on the internet or any authenticated Google account. This compromises **confidentiality**, enables **lateral movement** with leaked credentials, and can trigger regulatory breaches under PCI-DSS, ISO 27001, and GDPR.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://cloud.google.com/secret-manager/docs/access-control",
"https://cloud.google.com/iam/docs/overview"
],
"Remediation": {
"Code": {
"CLI": "gcloud secrets remove-iam-policy-binding <SECRET_NAME> --member=<allUsers|allAuthenticatedUsers> --role=roles/secretmanager.secretAccessor",
"NativeIaC": "",
"Other": "1. In Google Cloud Console, go to Security > Secret Manager\n2. Select the secret and open the Permissions tab\n3. Remove any binding with allUsers or allAuthenticatedUsers\n4. Grant access only to required service accounts or groups",
"Terraform": "```hcl\nresource \"google_secret_manager_secret_iam_binding\" \"<example_resource_name>\" {\n secret_id = \"<example_resource_id>\"\n role = \"roles/secretmanager.secretAccessor\"\n members = [\"serviceAccount:<example_resource_id>\"] # Critical: never include allUsers or allAuthenticatedUsers\n}\n```"
},
"Recommendation": {
"Text": "Apply **least privilege** to Secret Manager: grant `roles/secretmanager.secretAccessor` only to the specific service accounts or groups that need to read each secret.\n\nUse **workload identity** for GKE workloads and rotate credentials regularly to limit blast radius when a binding is misconfigured.",
"Url": "https://hub.prowler.com/check/secretmanager_secret_not_publicly_accessible"
}
},
"Categories": [
"internet-exposed"
],
"DependsOn": [],
"RelatedTo": [
"secretmanager_secret_rotation_enabled",
"kms_key_not_publicly_accessible",
"cloudstorage_bucket_public_access"
],
"Notes": "This check evaluates the secret-level IAM policy. Project-level IAM policies that may indirectly grant public access are not evaluated by this check."
}
@@ -0,0 +1,41 @@
from prowler.lib.check.models import Check, Check_Report_GCP
from prowler.providers.gcp.services.secretmanager.secretmanager_client import (
secretmanager_client,
)
class secretmanager_secret_not_publicly_accessible(Check):
"""Check that Secret Manager secrets do not grant access to all users.
Verifies that no Secret Manager secret has an IAM binding granting access
to `allUsers` or `allAuthenticatedUsers`.
"""
def execute(self) -> list[Check_Report_GCP]:
"""Execute the public-access check across all Secret Manager secrets.
Returns:
A list of `Check_Report_GCP` findings, one per secret. Status is
`FAIL` when the secret is accessible to `allUsers` or
`allAuthenticatedUsers` and `PASS` otherwise.
"""
findings = []
for secret in secretmanager_client.secrets:
report = Check_Report_GCP(
metadata=self.metadata(),
resource=secret,
resource_id=secret.name,
)
if secret.publicly_accessible:
report.status = "FAIL"
report.status_extended = (
f"Secret {secret.name} is publicly accessible "
f"(allUsers or allAuthenticatedUsers IAM binding detected)."
)
else:
report.status = "PASS"
report.status_extended = (
f"Secret {secret.name} is not publicly accessible."
)
findings.append(report)
return findings
@@ -0,0 +1,87 @@
from pydantic.v1 import BaseModel
from prowler.lib.logger import logger
from prowler.providers.gcp.config import DEFAULT_RETRY_ATTEMPTS
from prowler.providers.gcp.gcp_provider import GcpProvider
from prowler.providers.gcp.lib.service.service import GCPService
class SecretManager(GCPService):
"""Secret Manager service client.
Enumerates Secret Manager secrets across every accessible project using
the `secretmanager.googleapis.com` v1 API and exposes them through the
`secrets` attribute.
"""
def __init__(self, provider: GcpProvider) -> None:
"""Initialize the service and preload secrets with their IAM policies."""
super().__init__("secretmanager", provider)
self.secrets = []
self._get_secrets()
self._get_secrets_iam_policy()
def _get_secrets(self) -> None:
"""Fetch Secret Manager secrets for every project."""
for project_id in self.project_ids:
try:
request = (
self.client.projects()
.secrets()
.list(parent=f"projects/{project_id}")
)
while request is not None:
response = request.execute(num_retries=DEFAULT_RETRY_ATTEMPTS)
for secret in response.get("secrets", []):
self.secrets.append(
Secret(
id=secret["name"],
name=secret["name"].split("/")[-1],
project_id=project_id,
)
)
request = (
self.client.projects()
.secrets()
.list_next(previous_request=request, previous_response=response)
)
except Exception as error:
logger.error(
f"{project_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
def _get_secrets_iam_policy(self) -> None:
"""Fetch IAM policy for every Secret Manager secret in parallel."""
self.__threading_call__(self._get_secret_iam_policy, self.secrets)
def _get_secret_iam_policy(self, secret: "Secret") -> None:
"""Mark a secret as publicly accessible when bound to `allUsers` or `allAuthenticatedUsers`."""
try:
response = (
self.client.projects()
.secrets()
.getIamPolicy(resource=secret.id)
.execute(
http=self.__get_AuthorizedHttp_client__(),
num_retries=DEFAULT_RETRY_ATTEMPTS,
)
)
for binding in response.get("bindings", []):
members = binding.get("members", [])
if "allUsers" in members or "allAuthenticatedUsers" in members:
secret.publicly_accessible = True
break
except Exception as error:
logger.error(
f"{secret.project_id} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
class Secret(BaseModel):
"""Secret Manager secret resource consumed by GCP checks."""
id: str
name: str
project_id: str
location: str = "global"
publicly_accessible: bool = False
@@ -0,0 +1,169 @@
from unittest import mock
from tests.providers.gcp.gcp_fixtures import (
GCP_PROJECT_ID,
set_mocked_gcp_provider,
)
_CHECK_PATH = (
"prowler.providers.gcp.services.secretmanager."
"secretmanager_secret_not_publicly_accessible."
"secretmanager_secret_not_publicly_accessible"
)
_CLIENT_PATH = f"{_CHECK_PATH}.secretmanager_client"
def _secret_id(name: str) -> str:
return f"projects/{GCP_PROJECT_ID}/secrets/{name}"
class Test_secretmanager_secret_not_publicly_accessible:
def test_no_secrets(self):
secretmanager_client = mock.MagicMock()
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_gcp_provider(),
),
mock.patch(
_CLIENT_PATH,
new=secretmanager_client,
),
):
from prowler.providers.gcp.services.secretmanager.secretmanager_secret_not_publicly_accessible.secretmanager_secret_not_publicly_accessible import (
secretmanager_secret_not_publicly_accessible,
)
secretmanager_client.secrets = []
check = secretmanager_secret_not_publicly_accessible()
result = check.execute()
assert len(result) == 0
def test_secret_private(self):
secretmanager_client = mock.MagicMock()
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_gcp_provider(),
),
mock.patch(
_CLIENT_PATH,
new=secretmanager_client,
),
):
from prowler.providers.gcp.services.secretmanager.secretmanager_secret_not_publicly_accessible.secretmanager_secret_not_publicly_accessible import (
secretmanager_secret_not_publicly_accessible,
)
from prowler.providers.gcp.services.secretmanager.secretmanager_service import (
Secret,
)
secretmanager_client.secrets = [
Secret(
id=_secret_id("secret-private"),
name="secret-private",
project_id=GCP_PROJECT_ID,
publicly_accessible=False,
)
]
check = secretmanager_secret_not_publicly_accessible()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== "Secret secret-private is not publicly accessible."
)
assert result[0].resource_id == "secret-private"
assert result[0].resource_name == "secret-private"
assert result[0].location == "global"
assert result[0].project_id == GCP_PROJECT_ID
def test_secret_publicly_accessible(self):
secretmanager_client = mock.MagicMock()
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_gcp_provider(),
),
mock.patch(
_CLIENT_PATH,
new=secretmanager_client,
),
):
from prowler.providers.gcp.services.secretmanager.secretmanager_secret_not_publicly_accessible.secretmanager_secret_not_publicly_accessible import (
secretmanager_secret_not_publicly_accessible,
)
from prowler.providers.gcp.services.secretmanager.secretmanager_service import (
Secret,
)
secretmanager_client.secrets = [
Secret(
id=_secret_id("secret-public"),
name="secret-public",
project_id=GCP_PROJECT_ID,
publicly_accessible=True,
)
]
check = secretmanager_secret_not_publicly_accessible()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].status_extended == (
"Secret secret-public is publicly accessible "
"(allUsers or allAuthenticatedUsers IAM binding detected)."
)
assert result[0].resource_id == "secret-public"
assert result[0].resource_name == "secret-public"
assert result[0].location == "global"
assert result[0].project_id == GCP_PROJECT_ID
def test_multiple_secrets_mixed(self):
secretmanager_client = mock.MagicMock()
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_gcp_provider(),
),
mock.patch(
_CLIENT_PATH,
new=secretmanager_client,
),
):
from prowler.providers.gcp.services.secretmanager.secretmanager_secret_not_publicly_accessible.secretmanager_secret_not_publicly_accessible import (
secretmanager_secret_not_publicly_accessible,
)
from prowler.providers.gcp.services.secretmanager.secretmanager_service import (
Secret,
)
secretmanager_client.secrets = [
Secret(
id=_secret_id("secret-private"),
name="secret-private",
project_id=GCP_PROJECT_ID,
publicly_accessible=False,
),
Secret(
id=_secret_id("secret-public"),
name="secret-public",
project_id=GCP_PROJECT_ID,
publicly_accessible=True,
),
]
check = secretmanager_secret_not_publicly_accessible()
result = check.execute()
assert len(result) == 2
by_id = {r.resource_id: r for r in result}
assert by_id["secret-private"].status == "PASS"
assert by_id["secret-public"].status == "FAIL"
@@ -0,0 +1,162 @@
from unittest.mock import MagicMock, patch
from prowler.providers.gcp.services.secretmanager.secretmanager_service import (
SecretManager,
)
from tests.providers.gcp.gcp_fixtures import (
GCP_PROJECT_ID,
mock_is_api_active,
set_mocked_gcp_provider,
)
def _make_secretmanager_client(secrets_list, iam_bindings=None):
"""Return a mock GCP API client for the Secret Manager service."""
client = MagicMock()
client.projects().secrets().list().execute.return_value = {"secrets": secrets_list}
client.projects().secrets().list_next.return_value = None
iam_response = {"bindings": iam_bindings or []}
def mock_get_iam_policy(resource):
rv = MagicMock()
rv.execute.return_value = iam_response
return rv
client.projects().secrets().getIamPolicy = mock_get_iam_policy
return client
class TestSecretManagerService:
def test_get_secrets(self):
def mock_api_client(*args, **kwargs):
return _make_secretmanager_client(
secrets_list=[
{
"name": f"projects/{GCP_PROJECT_ID}/secrets/my-secret",
}
]
)
with (
patch(
"prowler.providers.gcp.lib.service.service.GCPService.__is_api_active__",
new=mock_is_api_active,
),
patch(
"prowler.providers.gcp.lib.service.service.GCPService.__generate_client__",
new=mock_api_client,
),
):
sm_client = SecretManager(
set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID])
)
assert len(sm_client.secrets) == 1
secret = sm_client.secrets[0]
assert secret.name == "my-secret"
assert secret.id == f"projects/{GCP_PROJECT_ID}/secrets/my-secret"
assert secret.project_id == GCP_PROJECT_ID
assert secret.location == "global"
assert secret.publicly_accessible is False
def test_get_secrets_iam_policy_all_users(self):
def mock_api_client(*args, **kwargs):
return _make_secretmanager_client(
secrets_list=[
{
"name": f"projects/{GCP_PROJECT_ID}/secrets/public-secret",
}
],
iam_bindings=[
{
"role": "roles/secretmanager.secretAccessor",
"members": ["allUsers"],
}
],
)
with (
patch(
"prowler.providers.gcp.lib.service.service.GCPService.__is_api_active__",
new=mock_is_api_active,
),
patch(
"prowler.providers.gcp.lib.service.service.GCPService.__generate_client__",
new=mock_api_client,
),
):
sm_client = SecretManager(
set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID])
)
assert len(sm_client.secrets) == 1
assert sm_client.secrets[0].publicly_accessible is True
def test_get_secrets_iam_policy_all_authenticated_users(self):
def mock_api_client(*args, **kwargs):
return _make_secretmanager_client(
secrets_list=[
{
"name": f"projects/{GCP_PROJECT_ID}/secrets/auth-users-secret",
}
],
iam_bindings=[
{
"role": "roles/secretmanager.secretAccessor",
"members": ["allAuthenticatedUsers"],
}
],
)
with (
patch(
"prowler.providers.gcp.lib.service.service.GCPService.__is_api_active__",
new=mock_is_api_active,
),
patch(
"prowler.providers.gcp.lib.service.service.GCPService.__generate_client__",
new=mock_api_client,
),
):
sm_client = SecretManager(
set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID])
)
assert len(sm_client.secrets) == 1
assert sm_client.secrets[0].publicly_accessible is True
def test_get_secrets_iam_policy_not_public(self):
def mock_api_client(*args, **kwargs):
return _make_secretmanager_client(
secrets_list=[
{
"name": f"projects/{GCP_PROJECT_ID}/secrets/private-secret",
}
],
iam_bindings=[
{
"role": "roles/secretmanager.secretAccessor",
"members": ["user:alice@example.com"],
}
],
)
with (
patch(
"prowler.providers.gcp.lib.service.service.GCPService.__is_api_active__",
new=mock_is_api_active,
),
patch(
"prowler.providers.gcp.lib.service.service.GCPService.__generate_client__",
new=mock_api_client,
),
):
sm_client = SecretManager(
set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID])
)
assert len(sm_client.secrets) == 1
assert sm_client.secrets[0].publicly_accessible is False