Driving is one of those freedom's that we sometimes take for granted. Life would be quite different without the luxuries that a vehicle affords us. Unfortunately, driving is not without its dangers. Distracted driving can lead to collisions, cars can break down unexpectedly, and accidents are inevitable. We're only human.
While it would be nice to have an expert driver for every person who needs to get to their destination, it's not a scalable solution. Each person has their own specific timeline and destination.
Our solution as a society, then, is to give each person the ability to handle their own transportation. You take a driver's test to prove that you have a fundamental understanding of the rules of the road and have the ability to actually drive the vehicle. If you pass, you are granted the privileged to join the open road with millions of other human beings. While it sounds like chaos, and honestly it sometimes is, how do we seemingly manage to pull it off day in and day out?
The answer is policies.
To keep drivers from getting distracted while on the road, we've enacted distracted driver policies. These policies include rules that prohibit drivers from using a cellphone while on the road, and bar the use of earbuds when the car is in motion.
We have policies around safety that state that cars must abide by all traffic signs which tell us when we should stop, and how fast we can go.
The key of this approach to enforcement is that it is generic and focuses on what the policy ultimately sets out to do. It doesn't matter if you're driving a Ford, a Chevy, or a Dodge, you're beholden to the same policies as everyone else.
So what's the point of all of this? The same philosophy can be applied within an organization! When policies are enforced within an organization, employees are more empowered to complete their tasks on their own, but are still kept in check by the policies that have been put in place.
You may be thinking, how does a policy differ from permissions?
Permissions for the most part, are relatively easy to wrap our heads around. You either have a permission, or you do not. If you do not have permission perform an action, well, you can't perform that action. This approach is unwavering and not flexible as there are many other aspects that should be considered when someone can or cannot do something.
If you add in roles, we can now reason about what sorts of permissions someone should have given their role in the company. Truthfully, for small systems, this could be completely acceptable. Where roles and permissions fall down, is when you consider larger systems, especially with multi-tenancy.
Let's consider the driving scenario again in the context of the USA.
The United States would be an organization, and the states would be tenants. In this context, it wouldn't be far fetched to have a "allowed to drive" permission per state. Immediately we've introduced 50 permissions, per user, into our system. Not only is that a lot of permissions, we'd be going about securing the roads in the wrong way.
As an organization, we don't necessarily care whether or not you can drive in a specific state. We have driving laws that are applicable everywhere, and really, as long as you abide by them that's all we should care about. This approach gives us an immense amount of flexibility. It would be crazy to have to pass a drivers test for every state you wanted to drive in! So why do we do it in software systems?
The scenario described above, associating a role (or permission) with every specific use case is commonly referred to as role explosion. So what's the alternative?
Attribute-based Access Control
The key difference between ABAC and a role-based permission model is that ABAC considers the context when an authorization request is made. Remember roles and permissions are binary, you can perform the action, or you cannot. Our rules and regulations around driving is a pretty good example of attribute-based access control.
For example, lets say when you pass your driver's test you are added to the Driver role. Then when you're on the road you're constantly being validated by policy checks. Are you going the speed limit? Are you on the correct side of the road? As you drive, we have that context, and the policy engine (police officer) can determine if you are violating the policies that the United States have set for all of its tenants.
If not already obvious, it is worth pointing out that in this scenario we did still use a role. Attribute-based access control can actually use roles as part of its decision making process!
Leveraging policies instead of fine grained permissions gives us a lot of flexibility and lets us focus on what we actually care about. Whereas permission models are often far too restrictive and ignore the context in which the action is being performed in.
Now, we're all well versed in checking specific permissions and roles. One little if() statement can give you a yes or no answer to whether or not the action can be performed. But what does validating a policy actually look like?
Policy Validation with Rego and OPA
I won't dive too deep into Rego as that could be a post in itself, but it is worth introducing Rego as a solution to policy validation for familiarity.
Rego is a policy language drives Open Policy Agent policies. Keeping with the driving theme, an example distracted driving policy would be:
In order to reduce the risks associated with distracted driving, certain conduct is prohibited while driving a company-owned motor vehicle or while driving a personal vehicle while on company business, including:
• Using cell phones (including hands-free)
• Operating laptops, tablets, portable media devices, and GPS devices
• Reading maps or any type of document, printed or electronic
Taken from https://emcins.com
This policy, represented in Rego, might look something like the following:
package distracted
disallowed_conduct = [
"USING_PHONE",
"OPERATING_MEDIA_DEVICE",
"READING_DOCUMENT"
]
deny[msg] {
input.vehicle.status = "IN_MOTION"
input.driver.conduct = disallowed_conduct[_]
msg = sprintf("invalid conduct: %v", input.driver.conduct)
}
In this Rego policy, all of the conditions must be met in order for the policy agent to return a deny
result for the authorization request.
If the vehicle has a status of IN_MOTION and the operator's conduct is in the disallowed list, we set the msg stating that they are in violation of the policy.
We'd also probably have another policy somewhere, outside of the distracted driving policy, that checks to make sure the operator has the Driver
role:
package global
deny[msg] {
not input.operator.role = "Driver"
msg = sprintf("Operator must be in the %v role", input.operator.role)
}
Rego input is the JSON we all know and love, so an authorization request in this context could be:
{
"operator": {
"role": "Driver",
"conduct": "USING_PHONE"
},
"vehicle": {
"status": "IN_MOTION"
}
}
The power of this approach is that because it is so flexible, you can validate policy against almost any data set as long as it has some structure to it.
- You can validate that a
Dockerfile
only usesFROM
on blessed images. - You can validate that a
.tf
file from Terraform only uses specific providers. - You can validate that a Kubernetes manifest contains specific label(s).
.. just to name a few of the possibilities.
Because when you really think about it, these are the things we really care about. We don't necessarily care about a permission, we grant permission because we trust someone to perform an action, because we trust they will enforce the policy.
Here we can just define the policy outright and let anyone* make changes, because we know the policy will always be enforced.
Like everything else in software and in life, it's not a silver bullet. You're still going to use roles and permissions to some degree, and you can't have policies around everything. It simply opens doors to additional approaches to automation via automatic approvals, and explicitly defines what our policies are in code.
If you're interested in seeing some more examples of what types of files you can validate, and what those policies might look like, you can checkout the examples folder for conftest which can be found here: https://github.com/instrumenta/conftest/tree/master/examples
I do help maintain the project so if you do decide to explore it, I'd love to hear your feedback to help make it better.