gcp

Using Google Cloud IAM Deny

2022-03-07

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


Setup

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 Resource

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

Create Deny Policy

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)

Configure group membership

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

Attempt Resource Access

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

images/deny_log.png

Also critically note that the list permissions succeeded but the get operations did not

images/deny_methods.png

If you want to remove alice from the group and retest,

gcloud identity groups memberships delete \
  --group-email="deniedgcs@$DOMAIN" --member-email=alice@$DOMAIN

Add Deny Exemption

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

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.


Client Library

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.