Package: cloudsecpy
AboutcloudsecCloudSec (cloudsecpyon PyPI) is a library and toolkit for formally analyzing security policies in cloud and API systems using Python. It has a modular and extensible architecture allowing it to support multiple backends;
currently,z3andcvc5are supported, but more backends will be supported in the future.
The CloudSec project takes influence from a number of related projects, such as theZ3 Firewall Checker.Table of ContentsAboutBackgroundIntroductionInstallationBuilding the Images From SourceTrying the Examples with DockerBeyond the Introduction: Component Types, Policy Specifications, Variables and Policy Templates7a.Defining Custom Policy Types7b.Policy Specifications, Variables and Policy TemplatesDeveloping CloudSecBackgroundCloud and API systems are commonly secured through the use of securitypolicies-- rules governing the set of  actions that agents (users, services, etc) are authorized to take within the system. For example, AWS uses IAM Policies; Kubernetes provides pod security policies, network policies, RBAC, etc.Within this context, a common question arises: given two sets of security policies,PandQ, are the rules generated byPandQequivalent, or is one set strictly more permissive than the other? A particular common case is to letPbe some existing policy set and letQbe a policy set that includes a security vulnerability. In this case, determining thatPis equivalent toQ(or even just thatP=>Q) establishes that the existing policy set contains a vulnerability. These are the types of questionscloudsectries to help answer.IntroductionTo get a quick sense of what CloudSec is about, let's assume we have a web API with an authorization
model that grants users access to different endpoints in the API. For simplicity, an "endpoint" will be
a URL path (e.g.,/apps,/jobs, etc) and an HTTP verb (e.g.,GET,POST,DELETE, etc.). We can use
CloudSec to define a new policy type for our web API with just a few lines of code:fromcloudsec.coreimportStringComponent,StringEnumComponent,Policy,PolicyType,OneWildcardMatchingusername=StringComponent(name="username",char_set=ALPHANUM_SET,max_len=25,matching_type=OneWildcardMatching())path=StringComponent(name="path",char_set=PATH_CHAR_SET,max_len=250,matching_type=OneWildcardMatching())verb=StringEnumComponent(name="verb",values=["GET","POST","PUT","DELETE"],matching_type=OneWildcardMatching())WebAPIPolicyType=PolicyType(components=[username,path,verb])In the code above, we create a new security policy type (WebAPIPolicyType) defined by three parts:
the username, representing the identity to be authorized, and the path and verb, together representing an
endpoint in our Web API. Note that this small amount of code is all that is needed to define a new policy
type -- it would be similarly easy to define a policy type for other kinds of systems.Once we have a policy type defined, we can create and analyze policies. Creating policies amounts to
providing values for each of the components (username,pathandverb, in the case above) as well as
a specicaldecisionfield which all policy types inherit. Thedecisionfield takes one of two values:allowordeny, indicating whether the policy authorizes or does not authorize the action.Here we create two policies:p=Policy(policy_type=WebAPIPolicyType,username="jstubbs",path="/apps",verb="GET",decision="allow")q=Policy(policy_type=WebAPIPolicyType,username="jstubbs",path="/apps",verb="*",decision="allow")In the first policy,p, we authorize the user "jstubbs" for the GET /apps endpoint. In the second,q,
we authorize the same user forallverbs on the /apps path. The*is a wildcard which indicates any
value. We can use wildcard characters when defining any of the values in our policies.We can now analyze these two policies using a cloudSecPolicyEquivalenceChecker.checker=PolicyEquivalenceChecker(policy_type=WebAPIPolicyType,policy_set_p=[p],policy_set_q=[q])Thecheckerobject has methods that allow us to analyze the equivalence of the two policy sets,[p]and[q]:# p implies q here, because p is less permissiveresult=checker.p_implies_q()result.proved-->True# q does not imply p; q is strictly more permissiveresult=checker.q_implies_p()result.proved-->Falseresult.found_counter_ex-->Trueresult.model-->[verb="PUT",path="/apps",username="jstubbs"]First we check ifp => q, and cloudSec finds the result is proved. That's because every action authorized
by ourppolicy is indeed authorized byq. Next, we check ifq => p. Here, cloudSec shows the result is
not proved and in fact it found a counter example. Indeed,pis strictly less permissive thanq, and theresult.modelprovides an example of an action that is authorized byqbut notp. Note that in the case
above, cloudSec is leveraging the z3 theorem prover, but we could have used cvc5 instead -- all we would need
to do is specifybackend="c5c5"when constructing thePolicyEquivalenceCheckerinstance.InstallationThe CloudSec library is available on pypi ascloudsecpy. Install using poetry, pip, etc. For example:poetryaddcloudsecpyNote that CloudSec depends on SMT solvers, such as Z3 and cvc5. The python package will install the
necessary dependent packages, e.g.,z3-solver.The project also provides Docker images withcloudsecpyand its dependencies preinstalled. THe following images are maintained:ghcr.io/applyfmsec/cloudsec-- Base image with CloudSec software and dependencies.ghcr.io/applyfmsec/cloudsec-exs-- Extends the base image with examples.ghcr.io/applyfmsec/cloudsec-tests-perf-- Extends the examples image with a complete performance test suite.You can download any of the images above using the Docker CLI, for example:dockerpullghcr.io/applyfmsec/cloudsecBuilding the Images From SourceYou can also build any and all of the Docker images from source. First, clone this git repository:gitclonehttps://github.com/applyfmsec/cloudsec.gitcdcloudsecThen, use theMakefileto generate the images. For example, to build all of the images with one command:makebuildSee the Makefile for commands to build specific images.Trying the Examples with DockerWith the images installed locally, we can try out the examples in a Docker container.
Start a container with the CloudsSec software and examples usingdocker(change the image name if you built
from source):$dockerrun-it--rm--entrypoint=bash--name=secghcr.io/applyfmsec/cloudsec-exsFrom within the container, start a Python shell and import the examples:
(Have a look at theexamples_z3.pyfile, contained within theexamplesdirectory, for all the definitions
of the objects used below.)# from within the container started above,>>>fromexamples_z3import*>>>result=checker.p_implies_q()# p => q is True, meaning that the p policy set is less permissive than the q policy set# i.e., any activity allowed by p is also allowed by q.>>>result.provedTrue>>>result=checker.q_implies_p()# q => p is False, however, because q is NOT less permissive than p; that is, q allows some activities# that p does not allow.>>>result.provedFalse# In fact, cloudsec was able to find a counter example to the statement q => p; i.e., it was able to find# an activity allowed by q but not by p.>>>result.found_counter_exTrue# the result.model contains a counter example to the q => p statement; i.e., it contains an example of# an activity that is allowed by the q policy set but not by the q policy set.>>>result.model[action="PUT",resource_path="s2/home/jstubbs",resource_service="files",resource_tenant="a2cps",principal_username="jstubbs",principal_tenant="a2cps"]Beyond the Introduction: Component Types, Policy Specifications, Variables and Policy TemplatesDefining Custom Policy TypesThe CloudSec library is designed to provide reusable components you can use to build your own security policy types representing the policies of your application. There are two types of building blocks provided by CloudSec that can be used for defining your own policy types: types where the underlying data are strings, and types where the underlying data are bit vectors. We discuss the string types first.String TypesCloudSec currently provides 3 different base types for working with string data in security policies:StringComponent,StringEnumComponent, andTupleComponent. All three descend from the base class,BaseComponent, and allow for policies with wildcard (*) characters by utilizing regular expression constraints over the theory of strings.TheStringEnumComponentTypeThecore.StringEnumComponenttype can be used for strings that can take a fixed, finite set of values. Thecloud.actiontype, representing an HTTP verb, is a good example of aStringEnumComponentbecause instances of the type can only take on one of the following values: GET, POST, PUT, DELETE or *. In general,StringEnumComponentvalues cannot contain a wildcard with other characters; e.g.,P*is not a validactionvalue.To create a new type based onStringEnumComponent, specify thenameof the component, the allowablevaluesfor the new type and amatching_type:example_enum=StringEnumComponent(name="example_enum",values=['value_1','value_2','value_3'],matching_type=OneWildcardMatching()Thematching_type=OneWildcardMatching()specifies that we allow matching with one wildcard character. Currently, this is one of two supported matching types in CloudSec, the other beingExactMatching()which precludes the use of wildcards. Support for additional matching types is planned for future releases.TheStringComponentTypeThecore.StringComponenttype can be used for strings that can take arbitrary values from a specified character set. A username in a cloud system is an example of something we might model with aStringComponenttype, if we do not consider the list of usernames in our system to be a fixed, finite list (otherwise,StringEnumComponentwould be a better choice). UnlikeStringEnumComponent, aStringComponentvalue can contain wildcard characters with other characters.To create a type based onStringComponent, one needs to specify the name, character set, maximum length, and the matching type. Here's an
example representing a "path" (from thecloudsec.cloudmodule):path=StringComponent(name="path",char_set=PATH_CHAR_SET,max_len=250,matching_type=one_wildcard_matching)Thecloudmodule makes use ofStringComponentfor usernames as well.TheTupleComponentTypeThecore.TupleComponenttype is useful for types that are really the composition of multipleStringComponentand/orStringEnumComponenttypes that should be thought of individually for the purposes of wildcard matching, but should be thought of as a single value in the overall policy.We illustrate the concept using the example of a principal (a user identity) in a multi-tenant cloud system. In such
a system, every user belongs to some tenant, and it is typical to represent an end-user identity as a username together with a tenant id. For example, for thejsmithuser in thefootenant, we might write the principal asjsmith.foo(orjsmith@foo, etc). For security policies in such a system that authorize principal(s) for one or more resources/actions, it is important to match the entire principal (user and tenant). For example, a security policy that authorizesjsmith.bar(jsmithin thebartenant) for some resources has no bearing on thejsmith.foouser.
However, with wildcard characters, we want the wildcard to apply to only one component of the principal. For example,*.foowould be all users in the foo tenant, whilejsmith*.foowould be all users in the foo tenant whose username starts withjsmith.We can create a new tuple type from existing types by using theTupleComponentclass and specifying afieldsattribute. Thefieldsattribute is a list of previously defined components (StringComponentand/orStringEnumComponent).To complete the example, here is how we might define ourprincipaltype as a tuple oftenantandusernametypes, described above:tenant=StringEnumComponent(name="tenant",values=set(["tenant1","tenant2","tenant3"]),matching_type=ExactMatching())username=StringComponent(name="username",char_set=ALPHANUM_SET,max_len=25,matching_type=OneWildcardMatching())principal=TupleComponent(name="principal",fields=[tenant,username])Policy Specifications, Variables and Policy TemplatesA primary motivating use case for CloudSec is to formally prove or disprove that a set of policies in the real world conforms to a set of rules that dictates what kinds of policies are "allowable". CloudSec suggests the following approach: letPbe the set of policies that exist in our real world system, and define the complete set of all "allowable" policies asQ. Then, use CloudSec to show either: 1)P => Q, in which case the policies in our real world system are all "safe"; or 2)P NOT=> Q, in which case CloudSec can return a counter example which will constitute a policy that is not safe.We refer to the set of all allowable policies for a system (the setQin the example above) as a policy specification.
In general, defining the policy specification for a system is very difficult. One of the challenges is that policies evolve over time:
new users are added, resources are created, modified and deleted, etc., and the security policies must be updated to reflect the changes.To support capturing the evolving nature of the security policies in a concise way, CloudSec provides support for defining policies with variables. We refer to policies that include variables aspolicy templatesbecause typically, they represent a family of policies.As a motivating example, consider a shared Unix file system supporting multiple users. Each user has a home directory where they have exclusive read/write access. We might model the security policies for such as a system in CloudSec as follows:username=StringComponent(name="username",char_set=ALPHANUM_SET,max_len=25,matching_type=OneWildcardMatching())path=StringComponent(name="path",char_set=PATH_CHAR_SET,max_len=250,matching_type=OneWildcardMatching())permission_level=StringEnumComponent(name="permission_level",values=["read","write","execute"],matching_type=OneWildcardMatching())FileSystemPolicyType=PolicyType(components=[username,path,permission_level])With this model, we can represent the permission individual users have
on their home directory as follows:joe_home_dir_policy=Policy(policy_type=FileSystemPolicyType,username="jstubbs",path="/home/jstubbs/",permission_level="*",decision="allow")smruti_home_dir_policy=Policy(policy_type=FileSystemPolicyType,username="spadhy",path="/home/spadhy/",permission_level="*",decision="allow")Here, we have declared that userjstubbsand userspadhyshould have
access to their respective home directories. But what about other users?
Using variables, we can specify that every user should have access to their
home directory in one, succint policy template, as follows:home_dir_policy_template=Policy(policy_type=FileSystemPolicyType,username="{{ username }}",path="/home/{{ username }}/*",permission_level="*",decision="allow")We enclose CloudSec variable names in{{ }}; here, we are using theusernamevariable. Note that CloudSec defines free variables for each field in a policy; for fields within a tuples, the variable name is<tuple_name>_<field_name>.We can now use CloudSec to check that our first two policies imply (i.e., are no more permissive) than the policy template:checker=PolicyEquivalenceChecker(policy_type=FileSystemPolicyType,policy_set_p=[joe_home_dir_policy,smruti_home_dir_policy],policy_set_q=[home_dir_policy_template])result=checker.p_implies_q()result.proved()-->TrueWe can think ofhome_dir_policy_templateas the policy specification for this
case. With this approach, if we add another user to the system and a corresponding third home directory policy to our set, the implication will still hold.Developing CloudSecThecloudseclibrary includes a test suite based onpytest. The Makefile can be used to build
the tests container image and run the tests:# Build the tests image$makebuild-tests# Run the tests$maketest