This is just a sample tutorial about GCP’s IAM Deny which will apply a deny rule on a single resource.
IAM deny allows administrators to specify an explicit rule that prevents resource access even if the owner of a resource allows it.
It reminds me of Mandatory Access Control vs Discretionary Access Control. In the latter, its your discretion to allow access (eg, change permissions/ownership on a file). In the former an administrator sets bounds for you.
In this example, the resource is just a GCS bucket that Alice and Bob and Carol have access to.
Their domain administrator creates a deny policy on their project that stipulates “deny any user that is members of a group denygcs@$DOMAIN
access to this resource”
The flow will be like this:
Alice
, Bob
and Carol
all have IAM Policies that allow access to read objects in gs://iam-deny-bucket
An administrator creates a Deny Policy that stipulates that nobody in group deniedgcs@domain.com
should ever be allowed access to that bucket
Administrator adds Alice
, Bob
to that group
Alice
and Bob
cannot access the bucket
Administrator makes an exemption for Bob
to the deny rule
Bob
can now access the bucket but Alice
cannot
To use this tutorial, you need to be a domain admin
First enable the administrator of the domain to manage Deny
export PROJECT_ID=`gcloud config get-value core/project`
export PROJECT_NUMBER=`gcloud projects describe $PROJECT_ID --format="value(projectNumber)"`
export DOMAIN=yourdomain.com
# as domain administrator (admin@$DOMAIN)
gcloud config set account admin@$DOMAIN
# set the value for the environment variable
gcloud organizations list
# enter your own ORG ID here
export ORGANIZATION_ID=your_ord_id_number
# enable IAM binding
gcloud organizations add-iam-policy-binding $ORGANIZATION_ID \
--member=user:admin@$DOMAIN --role=roles/iam.denyAdmin
# create a group called deniedgcs@
# print the group members we have in the target group we will use (it should be empty!)
gcloud identity groups describe deniedgcs@$DOMAIN
gcloud identity groups memberships list \
--group-email="deniedgcs@$DOMAIN"
You will also need to create three users
Alice
: alice@$DOMAIN
Bob
: bob@$DOMAIN
Carol
: carol@$DOMAIN
Create the GCS Bucket and add IAM permissions for all three users
## create a bucket
gsutil mb gs://$PROJECT_ID-iam-deny-bucket
echo -n bar > /tmp/foo.txt
gsutil cp /tmp/foo.txt gs://$PROJECT_ID-iam-deny-bucket/
# set permissions
gsutil iam ch user:alice@$DOMAIN:admin gs://$PROJECT_ID-iam-deny-bucket
gsutil iam ch user:bob@$DOMAIN:admin gs://$PROJECT_ID-iam-deny-bucket
gsutil iam ch user:carol@$DOMAIN:admin gs://$PROJECT_ID-iam-deny-bucket
Now as each user, see if you can access the object
gcloud config set account alice@$DOMAIN
gsutil defstorageclass get gs://$PROJECT_ID-iam-deny-bucket
gcloud config set account bob@$DOMAIN
gsutil defstorageclass get gs://$PROJECT_ID-iam-deny-bucket
gcloud config set account carol@$DOMAIN
gsutil defstorageclass get gs://$PROJECT_ID-iam-deny-bucket
So now lets create the deny policy at the project level. You can also apply a deny rule at the folder or organization level.
Note, only certain permissions are supported in IAM Deny
policy.json.tmpl
{
"displayName": "deny bucket",
"rules": [
{
"denyRule": {
"deniedPrincipals": [
"principalSet://goog/group/deniedgcs@$DOMAIN"
],
"deniedPermissions": [
"storage.googleapis.com/buckets.get"
]
}
}
]
}
What that says is “do not allow anyone in the deniedgcs@
group the permission storage.googleapis.com/objects.get
First configure the settings file
export RESOURCE_ID="cloudresourcemanager.googleapis.com/projects/$PROJECT_ID"
export POLICY_ID=my-deny-policy
envsubst < "policy.json.tmpl" > "policy.json"
Then create the policy
gcloud beta iam policies create $POLICY_ID \
--attachment-point=$RESOURCE_ID \
--kind=denypolicies \
--policy-file=policy.json
gcloud beta iam policies list \
--attachment-point=$RESOURCE_ID \
--kind=denypolicies \
--format=json
gcloud beta iam policies get $POLICY_ID \
--attachment-point=$RESOURCE_ID \
--kind=denypolicies \
--format=json
Applying the deny policy would have no effect on Alice,Bob or Carol (since they’re not members of the group)
Add Alice and Bob to the group
Check that
gcloud config set account admin@$DOMAIN.com
gcloud identity groups memberships list \
--group-email="deniedgcs@$DOMAIN"
gcloud identity groups memberships add \
--group-email="deniedgcs@$DOMAIN" --member-email=alice@$DOMAIN
gcloud identity groups memberships add \
--group-email="deniedgcs@$DOMAIN" --member-email=bob@$DOMAIN
the final outcome should be
gcloud identity groups memberships list \
--group-email="deniedgcs@$DOMAIN"
---
name: groups/023ckvvd35mlnri/memberships/109197082494565754684
preferredMemberKey:
id: alice@$DOMAIN
roles:
- name: MEMBER
---
name: groups/023ckvvd35mlnri/memberships/109861909738747328246
preferredMemberKey:
id: bob@$DOMAIN
roles:
- name: MEMBER
Now try to access the resource as Alice, Bob and Carol
# denied
gcloud config set account alice@$DOMAIN
gsutil defstorageclass get gs://$PROJECT_ID-iam-deny-bucket
AccessDeniedException: 403 AccessDenied
<?xml version='1.0' encoding='UTF-8'?>
<Error>
<Code>AccessDenied</Code>
<Message>Access denied.</Message>
<Details>alice@esodemoapp2.com does not have storage.buckets.get access to the Google Cloud Storage bucket.</Details>
</Error>
# denied
gcloud config set account bob@$DOMAIN
gsutil defstorageclass get gs://$PROJECT_ID-iam-deny-bucket
AccessDeniedException: 403 AccessDenied
<?xml version='1.0' encoding='UTF-8'?>
<Error>
<Code>AccessDenied</Code>
<Message>Access denied.</Message>
<Details>bob@DOMAIN.com does not have storage.buckets.get access to the Google Cloud Storage bucket.</Details>
</Error>
# allowed
gcloud config set account carol@$DOMAIN
gsutil defstorageclass get gs://$PROJECT_ID-iam-deny-bucket
gs://cicp-oidc-iam-deny-bucket: STANDARD
If you look at the logs for GCS, it gives no indication that a deny policy was preventing access
Also critically note that the list
permissions succeeded but the get
operations did not
If you want to remove alice
from the group and retest,
gcloud identity groups memberships delete \
--group-email="deniedgcs@$DOMAIN" --member-email=alice@$DOMAIN
What we will do now is create an exemption for Bob
on the deny policy.
What this configuration here does says “ignore this deny policy for Bob even if he is in the deniedgcs@
group and subject to this policy.
policy_exempt.json.tmpl
{
"displayName": "deny bucket",
"rules": [
{
"denyRule": {
"deniedPrincipals": [
"principalSet://goog/group/deniedgcs@$DOMAIN"
],
"exceptionPrincipals": [
"principal://goog/subject/bob@$DOMAIN"
],
"deniedPermissions": [
"storage.googleapis.com/buckets.get"
]
}
}
]
}
Apply the policy
envsubst < "policy_expempt.json.tmpl" > "policy_exempt.json"
gcloud beta iam policies update $POLICY_ID \
--attachment-point=$RESOURCE_ID \
--kind=denypolicies \
--policy-file=policy_exempt.json
gcloud beta iam policies get $POLICY_ID \
--attachment-point=$RESOURCE_ID \
--kind=denypolicies \
--format=json
Test with exemptions
# denied
gcloud config set account alice@$DOMAIN
gsutil defstorageclass get gs://$PROJECT_ID-iam-deny-bucket
AccessDeniedException: 403 AccessDenied
<?xml version='1.0' encoding='UTF-8'?> <Error>
<Code>AccessDenied</Code>
<Message>Access denied.</Message>
<Details>alice@DOMAIN.com does not have storage.buckets.get access to the Google Cloud Storage bucket.</Details>
</Error>
# allowed
gcloud config set account bob@$DOMAIN
gsutil defstorageclass get gs://$PROJECT_ID-iam-deny-bucket
gs://cicp-oidc-iam-deny-bucket: STANDARD
# allowed
gcloud config set account carol@$DOMAIN
gsutil defstorageclass get gs://$PROJECT_ID-iam-deny-bucket
gs://cicp-oidc-iam-deny-bucket: STANDARD
Finally, if you want to delete the deny policy
gcloud beta iam policies delete $POLICY_ID \
--attachment-point=$RESOURCE_ID \
--kind=denypolicies
Troubleshooting is very difficult….
While the there is a Deny Policy Troubleshooting Guide, all that it really describes is how to ‘display’ any deny policy in the hierarcy.
Its ultimately upto the admin to eyeball any deny policy in the chain and see if it applies.
yes, its difficult and awkward…
In my case the troubleshooting command barfs a lot of json back but the core bit to notice is that in the hierarchy this bit exists:
gcloud beta projects get-ancestors-iam-policy $PROJECT_ID --include-deny --format=json
{
"id": "cicp-oidc",
"policy": {
"createTime": "2022-03-09T14:05:34.481679Z",
"displayName": "deny bucket",
"etag": "MTc3MjE4ODA5MzI3Mzc4NTEzOTI=",
"kind": "DenyPolicy",
"name": "policies/cloudresourcemanager.googleapis.com%2Fprojects%2F343794733782/denypolicies/my-deny-policy",
"rules": [
{
"denyRule": {
"deniedPermissions": [
"storage.googleapis.com/buckets.get"
],
"deniedPrincipals": [
"principalSet://goog/group/deniedgcs@DOMAIN.com"
],
"exceptionPrincipals": [
"principal://goog/subject/bob@DOMAIN.com"
]
}
}
],
"uid": "b6da22f3-e12c-17ee-4eb4-3df38466936a",
"updateTime": "2022-03-09T14:37:57.021615Z"
},
"type": "project"
},
Which basically states the final policy is in the chain at the project level (type: project
).
Hopefully, the iam policy troubleshooter will support introspection someday.
As of 3/9/22
, IAM deny does not have any samples in an SDK library.
However, as is the case with any google API, you can sort of awkwardly use the discovery
endpoint
import httplib2
import json
from apiclient.discovery import build
from oauth2client.client import GoogleCredentials
from google.api_core import operations_v1
from google.api_core import operation
from google.longrunning.operations_pb2 import Operation
credentials = GoogleCredentials.get_application_default()
if credentials.create_scoped_required():
credentials = credentials.create_scoped("https://www.googleapis.com/auth/cloud-platform")
http = httplib2.Http()
credentials.authorize(http)
PROJECT_NUMBER='12345'
FOLDER_NUMBER='67890'
ORGANIZATION_NUMBER='45678'
# https://iam.googleapis.com/$discovery/rest?version=v2beta
service = build(serviceName='iam', version='v2beta', http=http, static_discovery=False)
# deny policy at project level
parent = 'policies/cloudresourcemanager.googleapis.com%2Fprojects%2F{}/denypolicies'.format(PROJECT_NUMBER)
# list existing polciies
resp = service.policies().listPolicies(parent=parent).execute()
print(json.dumps(resp, indent=4, sort_keys=True))
## crate a policy
rules = {
"displayName": "deny bucket",
"rules": [
{
"denyRule": {
"deniedPrincipals": [
"principalSet://goog/group/deniedgcs@yourdomain.com"
],
"exceptionPrincipals": [
"principal://goog/subject/bob@yourdomain.com"
],
"deniedPermissions": [
"storage.googleapis.com/buckets.get"
]
}
}
]
}
policyId='my-deny-policy'
lropb = service.policies().createPolicy(parent=parent,policyId=policyId,body=rules).execute()
print(lropb)
op = operation.from_http_json(operation=lropb)
result = op.result()
print(result)
# get policy
policyId='my-deny-policy'
name = 'policies/cloudresourcemanager.googleapis.com%2Fprojects%2F{}/denypolicies/{}'.format(PROJECT_NUMBER,policyId)
resp = service.policies().get(name=name).execute()
print(json.dumps(resp, indent=4, sort_keys=True))
# list folder policies
parent = 'policies/cloudresourcemanager.googleapis.com%2Ffolders%2F{}/denypolicies'.format(FOLDER_NUMBER)
resp = service.policies().listPolicies(parent=parent).execute()
print(json.dumps(resp, indent=4, sort_keys=True))
# list org policies
parent = 'policies/cloudresourcemanager.googleapis.com%2Forganizations%2F{}/denypolicies'.format(ORGANIZATION_NUMBER)
resp = service.policies().listPolicies(parent=parent).execute()
print(json.dumps(resp, indent=4, sort_keys=True))
This site supports webmentions. Send me a mention via this form.