X

Break New Ground

Understanding Effective vs Actual IAM Wildcard Policy Statements Using Python

Oracle Cloud Infrastructure (OCI) Identity and Access Management (IAM) is a powerful service that's really central to all other OCI services. This is the main nerve center of where (and how) privileges are managed on OCI. IAM is powerful, allowing for very simple to very complex security policies to be configured. Simple IAM statements are those that use hard-coded, static values, such as the following:

allow group storage-admins to manage buckets in compartment dev

I can read and understand this pretty easily, even if I'm not familiar with how IAM policy statements are structured. Let's take this a step further and see how this can get really complicated, really quickly... but don't get discouraged. While it might look bleak and complex for a minute, we'll pivot to look at a solution for how this can be deciphered!  Without further ado, let's look at a fictitious scenario that will lay the foundation for the rest of our time together in this article.

Fictitious Scenario

ABC Foods is using OCI as its cloud provider. One of their OCI tenancies hosts various development environments, with each environment being in its own compartment:

Root (tenancy)
|- dev
|- dev-2
|- dev-3
|- dev-4
|- dev-5
|- dev-6
|- dev-7
|- dev-8
\- dev-9

First off, the compartment names chosen aren't that great - there's very little description (not to mention being inconsistent, as dev should really be dev-1 to keep to the naming standard).

The following IAM policies are configured in the root (tenancy) compartment:


allow group bucket-admins to manage buckets in compartment dev
allow group object-admins to manage objects in tenancy where target.compartment.name = /dev*/

It turns out that the dev compartment (environment) is special and different from the others.  It has a special group of admins and is tightly guarded.  ABC Foods want to allow the object-admins group to manage objects in compartments with dev-<number>, which is what the second IAM policy is intended to capture.  Is this really the case?

Evaluating IAM Wildcard Statements

Let's audit the IAM statements, ensuring that the desired privileges are in-place.  This should be pretty easy, with only two IAM statements, right?!  No, not really... let's see why.

Effective vs Actual IAM Statements

The first IAM statement is pretty simple, straight forward.  It can't be made any more cut-and-dry!  Let's move forward and look at the next statement... let's pick it apart piece-by-piece:

Policy Component Description
Allow group object-admins Easy - this policy is granting access to the object-admins group.
to manage objects Ok, the object-admins group is permitted to manage (versus inspect, read or use) objects in OCI Object Storage.  So far, so good.
in tenancy Hmm... this is a little suspicious.  We don't want them to have access to the dev compartment.  If the statement stopped here, this could be a problem.  Thankfully for us, it doesn't... let's continue to look at the where clause(s).
where target.compartment.name = Phew!  I'm glad that we're filtering by the compartment where the object resides.
/dev*/ This is a wildcard match.  Looks like the name must start with dev and end in any value.  What will this match?  dev, dev-2, dev-3, dev-4, dev-5, dev-6, dev-7, dev-8 and dev-9.  This is a bit too permissive, also encapsulating the dev compartment, which we don't want.  The correct value should be /dev-*/, which would match the dev-2 to dev-9 compartments.

That last statement matched quite a few compartments, due to the use of a wildcard in the where clause.  This is where there are eleven (11) effective policy statements, but only two (2) actual policy statements.  Effective... actual... what?!  I thought a policy statement is just a policy statement?!  Let's look at what this means:

Actual Statements Effective Statements
allow group bucket-admins to manage buckets in compartment dev
allow group object-admins to manage objects in tenancy where target.compartment.name = /dev*/
allow group bucket-admins to manage buckets in compartment dev
allow group object-admins to manage objects in tenancy where target.compartment.name = dev
allow group object-admins to manage objects in tenancy where target.compartment.name = dev-2
allow group object-admins to manage objects in tenancy where target.compartment.name = dev-3
allow group object-admins to manage objects in tenancy where target.compartment.name = dev-4
allow group object-admins to manage objects in tenancy where target.compartment.name = dev-5
allow group object-admins to manage objects in tenancy where target.compartment.name = dev-6
allow group object-admins to manage objects in tenancy where target.compartment.name = dev-7
allow group object-admins to manage objects in tenancy where target.compartment.name = dev-8
allow group object-admins to manage objects in tenancy where target.compartment.name = dev-9

While we actually only have two policy statements, because there are 10 compartments matching the wildcard, it effectively balloons the effective number of statements to 11.

What would happen if we added a new compartment to this tenancy named dev-99?  We'd still only have two actual statements, but would have 12 effective policy statements (now it would not only match dev, dev-2 to dev-9 but also dev-99).

The term "effective policy statements" doesn't exist outside of this article (or my head)... it's not an official OCI term, but effectively (no pun intended!) communicates the scope and impact of wildcards in an environment.

Identifying Effective IAM Statements

The challenge with wildcards in IAM statements is that there are only a couple of ways for us to evaluate them:

  • By hand (manually)
  • Programmatically

Looking things over by hand is fine when we're dealing with ten compartments and two actual statements, but it's not efficient or scalable (dare I say reliable?) as evaluating them programmatically.  Let's take a look at how this can be done using the OCI CLI and a bit of Python code.

Capturing the Actual IAM Statements

There are lots of ways to do this... you could query the OCI API programmatically, use the OCI CLI, etc.  For this exercise, we'll use the OCI CLI to grab a current snapshot of the policies within our compartment (use your own tenancy/compartment OCID in the following command):

oci iam compartment list --compartment-id <TENANCY OR COMPARTMENT OCID> --output json > comp.json

NOTE: If you're using a non-standard OCI config file name, you'll need to add the --config-file <PATH TO YOUR OCI CONFIG FILE> parameter to the above command.

This is an easy way to get your compartments dumped to a JSON file, which we can then be easily parsed using Python.  Before we proceed, we should also cache a dump of the IAM policies we need to evaluate.  We'll again use OCI CLI to export this to JSON:

$ oci iam policy list --compartment-id <TENANCY OR COMPARTMENT OCID> --output json > policies.json

NOTE: Again, don't forget to specify your config file if using a non-standard OCI config file name!

After "prettying" up the JSON, we have something like the following (this is for our fictitious scenario) for our compartments:


{
  "data": [
    {
      "compartmentId": "ocid1.tenancy.oc1..abcdef123456",
      "definedTags": {},
      "description": "Main dev compartment",
      "freeformTags": {},
      "id": "ocid1.compartment.oc1..abcdef1",
      "inactiveStatus": null,
      "isAccessible": true,
      "lifecycleState": "ACTIVE",
      "name": "dev",
      "timeCreated": "2016-08-25T21:10:29.600Z"
    },
    {
      "compartmentId": "ocid1.tenancy.oc1..abcdef123456",
      "definedTags": {},
      "description": "dev env #2",
      "freeformTags": {},
      "id": "ocid1.compartment.oc1..abcdef2",
      "inactiveStatus": null,
      "isAccessible": true,
      "lifecycleState": "ACTIVE",
      "name": "dev-2",
      "timeCreated": "2016-08-25T21:10:29.600Z"
    },
    {
      "compartmentId": "ocid1.tenancy.oc1..abcdef123456",
      "definedTags": {},
      "description": "dev env #3",
      "freeformTags": {},
      "id": "ocid1.compartment.oc1..abcdef3",
      "inactiveStatus": null,
      "isAccessible": true,
      "lifecycleState": "ACTIVE",
      "name": "dev-3",
      "timeCreated": "2016-08-25T21:10:29.600Z"
    },
    {
      "compartmentId": "ocid1.tenancy.oc1..abcdef123456",
      "definedTags": {},
      "description": "dev env #4",
      "freeformTags": {},
      "id": "ocid1.compartment.oc1..abcdef4",
      "inactiveStatus": null,
      "isAccessible": true,
      "lifecycleState": "ACTIVE",
      "name": "dev-4",
      "timeCreated": "2016-08-25T21:10:29.600Z"
    },
    {
      "compartmentId": "ocid1.tenancy.oc1..abcdef123456",
      "definedTags": {},
      "description": "dev env #5",
      "freeformTags": {},
      "id": "ocid1.compartment.oc1..abcdef5",
      "inactiveStatus": null,
      "isAccessible": true,
      "lifecycleState": "ACTIVE",
      "name": "dev-5",
      "timeCreated": "2016-08-25T21:10:29.600Z"
    },
    {
      "compartmentId": "ocid1.tenancy.oc1..abcdef123456",
      "definedTags": {},
      "description": "dev env #6",
      "freeformTags": {},
      "id": "ocid1.compartment.oc1..abcdef6",
      "inactiveStatus": null,
      "isAccessible": true,
      "lifecycleState": "ACTIVE",
      "name": "dev-6",
      "timeCreated": "2016-08-25T21:10:29.600Z"
    },
    {
      "compartmentId": "ocid1.tenancy.oc1..abcdef123456",
      "definedTags": {},
      "description": "dev env #7",
      "freeformTags": {},
      "id": "ocid1.compartment.oc1..abcdef7",
      "inactiveStatus": null,
      "isAccessible": true,
      "lifecycleState": "ACTIVE",
      "name": "dev-7",
      "timeCreated": "2016-08-25T21:10:29.600Z"
    },
    {
      "compartmentId": "ocid1.tenancy.oc1..abcdef123456",
      "definedTags": {},
      "description": "dev env #8",
      "freeformTags": {},
      "id": "ocid1.compartment.oc1..abcdef8",
      "inactiveStatus": null,
      "isAccessible": true,
      "lifecycleState": "ACTIVE",
      "name": "dev-8",
      "timeCreated": "2016-08-25T21:10:29.600Z"
    },
    {
      "compartmentId": "ocid1.tenancy.oc1..abcdef123456",
      "definedTags": {},
      "description": "dev env #9",
      "freeformTags": {},
      "id": "ocid1.compartment.oc1..abcdef9",
      "inactiveStatus": null,
      "isAccessible": true,
      "lifecycleState": "ACTIVE",
      "name": "dev-9",
      "timeCreated": "2016-08-25T21:10:29.600Z"
    }
  ]
}

And we end up with something like this from our fictitious environment for the policies:


{
  "data": [
    {
      "compartmentId": "ocid1.compartment.oc1..abcdef1",
      "definedTags": {},
      "description": "Policies for managing the main dev environment",
      "freeformTags": {},
      "id": "ocid1.policy.oc1..abcdef1",
      "inactiveStatus": null,
      "lifecycleState": "ACTIVE",
      "name": "main_dev",
      "statements": [
        "allow group bucket-admins to manage buckets in compartment dev"
      ],
      "timeCreated": "2016-08-25T21:10:29.600Z",
      "versionDate": null
    },
    {
      "compartmentId": "ocid1.compartment.oc1..abcdef2",
      "definedTags": {},
      "description": "Policies for other dev environments",
      "freeformTags": {},
      "id": "ocid1.policy.oc1..abcdef2",
      "inactiveStatus": null,
      "lifecycleState": "ACTIVE",
      "name": "other_dev",
      "statements": [
        "allow group object-admins to manage objects in tenancy where target.compartment.name = /dev*/",
        "allow group dev2-admins to manage objects in tenancy where target.compartment.name = dev-2"
      ],
      "timeCreated": "2016-08-25T21:10:29.600Z",
      "versionDate": null
    }
  ]
}

So far we've not accomplished anything too exciting, except for the fact that we can now programmatically parse this data in Python (or many other languages)!

Analyzing the JSON

Now for the fun part... Here's the code for the basic_pol_eval.py file:


import json
import re

def get_compartments():
  comps = []
  json_file = "comp.json"
  json_data = json.load(open(json_file))
  return json_data['data']

def get_comp_names(compartments):
  ret = []
  for c in compartments:
    ret.append(c['name'])
  return ret

def get_policies():
  pols = []
  json_file = "policies.json"
  json_data = json.load(open(json_file))
  return json_data['data']

def convert_wildcard_to_regex(wildcard = None):
  if wildcard == None:
    return ''
  
  ret = wildcard
  ret = ret.replace('/', '')
  ret = ret.replace('*', '.*')
  
  return ret

def is_wildcard(comp_filter):
  tst = re.compile('^/\S+/$')
  if tst.match(comp_filter):
    ret = True
  else:
    ret = False
  return ret

def print_eff_policies(pols):
  for p in pols:
    print('Policy Statement: %s' % (p))
    print('  Compartments: [ %s ]' % (', '.join(pols[p])))

def main():
  comps = get_compartments()
  comp_names = get_comp_names(comps)
  policies = get_policies()
  effective_statements = {}
  num_actual = 0
  num_effective = 0
  
  for policy in policies:
    for statement in policy['statements']:
      if statement not in effective_statements:
        effective_statements[statement] = []
        num_actual += 1
      
      # look for compartments in the "where" clause(s)
      regex_comps = re.compile('^.* where .*(?Ptarget.compartment.name\s*=\s*(?P/?\S+/?)).*', re.IGNORECASE)
      comp_matches = regex_comps.findall(statement)
      if len(comp_matches) > 0:
        for match in comp_matches:
          for c in comps:
            if is_wildcard(match[1]):
              regex_test = re.compile(convert_wildcard_to_regex(match[1]), re.IGNORECASE)
              if regex_test.match(c['name']):
                effective_statements[statement].append(c['name'])
                num_effective += 1
            else:
              if c['name'] == match[1]:
                effective_statements[statement].append(c['name'])
                num_effective += 1
      
      # look for compartments in the scope...
      regex_comps = re.compile('^.* to manage \S+ in compartment (?P\S+)\s*.*', re.IGNORECASE)
      comp_match = regex_comps.match(statement)
      if comp_match is not None:
        for c in comps:
          if is_wildcard(comp_match[1]):
            regex_test = re.compile(convert_wildcard_to_regex(comp_match[1]), re.IGNORECASE)
            if regex_test.match(c['name']):
              effective_statements[statement].append(c['name'])
              num_effective += 1
          else:
            if c['name'] == comp_match[1]:
              effective_statements[statement].append(c['name'])
              num_effective += 1
      
  print("\n# Actual Statements: %s\n# Effective Statements: %s\n" % (num_actual, num_effective))
  print_eff_policies(effective_statements)
  
main()

Let's take a quick look at what's going on here, then we'll run it.

get_compartments() and get_policies()

These are both pretty straightforward... they just load the data from the JSON files we've saved from OCI CLI.  In a more robust implementation, we'd not hard-code the filenames, rather parameterize them to allow for flexibility.

get_comp_names()

This returns a list of the names of each compartment... nothing too complex.

convert_wildcard_to_regex()

This is a quick-and-dirty way of converting the wildcard format used with IAM to a Regular Expression (RegEx).  Python (and lots of other languages) has rich support for RegEx, so having this conversion script allows us to effectively convert this from something only IAM recognizes to something Python can work with (match against).

is_wildcard()

This compliments the conversion, in that it detects whether or not a wildcard is present.  This uses a RegEx to match for the IAM wildcard format... if it's a wildcard, it'll return True, otherwise it returns False.

print_eff_policies()

This gives a user-friendly display of each statement and the compartment(s) it applies to.

main()

This is where the main processing logic happens.  The first part is straightforward, with the latter half being split between looking for one of two scenarios:

  1. Compartment name(s) mentioned in "where" clauses
    1. This is looking for "where" clauses that contain target.compartment.name = BLAH (or a wildcard, such as /BLAH1*/).  Multiple compartments may be specified in the "where" clause section of the statement, making it necessary to findall matches, as well as iterate through those (with the possibility there's more than one target compartment mentioned).
  2. Compartment name in the scope
    1. This is the more simplistic of the two, where a statement says allow group ABC to manage XYZ in compartment BLAH.  There can only be one compartment specified in this type of statement... and there's no wildcards to be found with this, so it's pretty straightforward.

After processing it, we simply display the results and go on our way.

Running It

Here's what we get when running this:

$ python3 basic_pol_eval.py
 
# Actual Statements: 3
# Effective Statements: 11
 
Policy Statement: allow group bucket-admins to manage buckets in compartment dev
  Compartments: [ dev ]
Policy Statement: allow group object-admins to manage objects in tenancy where target.compartment.name = /dev*/
  Compartments: [ dev, dev-2, dev-3, dev-4, dev-5, dev-6, dev-7, dev-8, dev-9 ]
Policy Statement: allow group dev2-admins to manage objects in tenancy where target.compartment.name = dev-2
  Compartments: [ dev-2 ]

While not super pretty, we're able to see what compartments these one-liner policy statements apply to. We could easily change the format (export to JSON, CSV, rewrite the statement to show the matching compartment, etc.) to convey the effective policy statements (from the actual). In this our scenario here, we have three actual statements yielding 11 effective statements. We can see from this really small (and simple) example how these things can quickly balloon out!

Food For Thought

While this is a great starting point, there are several facets that are lacking in this solution:

  • We don't bother to look at tenancy-wide statements (though this could easily be added).
  • We don't look at compartment OCIDs (also supported, instead of using names).
  • No intelligence or awareness to compartment inheritance is provided.
  • The awareness of compartment hierarchy is lacking - meaning that if a policy statement is applied only on dev-9, only dev-9 and/or its descendants should be matched.  The current logic isn't aware of parent/child relationships (instead, seeing everything as being flat).

This is enough to lay the foundation for what an effective policy statement is (and its relationship to actual statements), as well as get you thinking about how you might go about managing these potentially complex relationships.

Wrapping It Up

As we build secure OCI environments, it's really important to have a strong security posture enforced by IAM.  As our IAM policies grow in size and complexity, managing them can be a bit difficult.  Hopefully, this short read gives you a little bit of perspective on how you might consider evaluating (auditing?) your IAM policy statements, to really understand their scope and impact.

Image by Thomas Breher from Pixabay

Be the first to comment

Comments ( 0 )
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.