Control: Pub/Sub topic IAM policies should only grant access to trusted principals
Description
This control checks whether Pub/Sub topic IAM policies grant access to untrusted users, groups, domains, or service accounts.
Usage
Run the control in your terminal:
powerpipe control run gcp_perimeter.control.pubsub_topic_policy_shared_access
Snapshot and share results via Turbot Pipes:
powerpipe loginpowerpipe control run gcp_perimeter.control.pubsub_topic_policy_shared_access --share
Steampipe Tables
SQL
with policy_analysis as ( select name as resource_id, -- Count all members count(*) as total_members, -- Count project-level members count(*) filter (where member like 'project%') as project_members, -- Count trusted members (excluding project-level) count(*) filter (where member not like 'project%' and ( (member like 'user:%' and split_part(member, 'user:', 2) = any(($1)::text[])) or (member like 'group:%' and split_part(member, 'group:', 2) = any(($2)::text[])) or (member like 'domain:%' and split_part(member, 'domain:', 2) = any(($3)::text[])) or (member like 'serviceAccount:%' and split_part(member, 'serviceAccount:', 2) = any(($4)::text[])) ) ) as trusted_members, -- Collect untrusted members for alarm messages array_agg(distinct member) filter (where member not like 'project%' and not ( (member like 'user:%' and split_part(member, 'user:', 2) = any(($1)::text[])) or (member like 'group:%' and split_part(member, 'group:', 2) = any(($2)::text[])) or (member like 'domain:%' and split_part(member, 'domain:', 2) = any(($3)::text[])) or (member like 'serviceAccount:%' and split_part(member, 'serviceAccount:', 2) = any(($4)::text[])) ) ) as untrusted_members from gcp_pubsub_topic, jsonb_array_elements(iam_policy -> 'bindings') as binding, jsonb_array_elements_text(binding -> 'members') as member group by name ) select r.name as resource, case -- SKIP: When no members exist when (r.iam_policy -> 'bindings') is null or jsonb_array_length(r.iam_policy -> 'bindings') = 0 then 'skip' -- INFO: When only project-level roles are assigned when p.total_members = p.project_members then 'info' -- OK: When all non-project members are trusted when p.untrusted_members is null and (p.trusted_members > 0 or p.project_members > 0) then 'ok' -- ALARM: When there are untrusted members else 'alarm' end as status, case when (r.iam_policy -> 'bindings') is null or jsonb_array_length(r.iam_policy -> 'bindings') = 0 then title || ' has no IAM policy members.' when p.total_members = p.project_members then title || ' only has project-level role assignments (' || p.project_members || ' members).' when p.untrusted_members is null and (p.trusted_members > 0 or p.project_members > 0) then title || ' policy only grants access to trusted principals (' || coalesce(p.trusted_members, 0) || ' trusted + ' || coalesce(p.project_members, 0) || ' project-level).' else title || ' policy contains ' || coalesce(array_length(p.untrusted_members, 1), 0) || ' untrusted member(s): ' || array_to_string(p.untrusted_members, ', ') end as reason, r.project, r.location from gcp_pubsub_topic as r left join policy_analysis as p on p.resource_id = r.name where -- Only check resources where we have access to IAM policy r.iam_policy is not null;
Params
Args | Name | Default | Description | Variable |
---|---|---|---|---|
$1 | trusted_users |
| A list of trusted Google Account emails. | |
$2 | trusted_groups |
| A list of trusted Google Groups. | |
$3 | trusted_domains |
| A list of trusted Google Workspace domains. | |
$4 | trusted_service_accounts |
| A list of trusted service accounts. |