- OPAL
- Best Practices
You're Doing Shift-Left Wrong
Learn from a real case study how to Shift-Left in a way that will impact the product's security. Minimize friction between security and development teams.
Daniel Bass
Introduction
Shift left is everywhere. While it could be interpreted in many different ways, originating in the security space, implementing security features right from the start of your software development process is at the “Shift-Left” core.
There are security tools available for the early planning stages as well as the development stage, both helping you to make sure our application is safe from the very beginning.
The Problem with Shift Left
But there's an inherent problem with "Shift-Left" works today - Developers don't like having their work measured all the time, and a lot of the tools promoting “shift left” just create more and more work for them. Instead of these tools assisting in the development process, they just become one more task on their to-do list.
Therefore, we must ask: Is it good to push security measurements into engineering? Or does it just create more unnecessary work for the development team?
The answer to this question presents as an alternative - focus on measurable intrinsic impact rather than broad-based measurement.
A comprehensive list of DevSecOps tools, most of them just measure security with no impact on the application.
Instead of endlessly measuring and analyzing our application’s security, we should strive to impact it in a meaningful way, by guiding and helping the developer to create an intrinsically more secure application; Turning proactive measurement into proactive design. To see how this can be achieved, let’s look at the example of application-level access control.
The case study: Application-level access control
In the traditional way "Shift-Left" of handling application-level authorization, a tool would be applied to check the access control layer to make sure that there are no mistakes in it and probe the API trying to get unauthorized access. This often creates extra work for the developer - constantly chasing around alerts, instead of aiding them with the implementation process. Before we dive into the alternative, let’s better understand what application-level control is.
Authentication vs. Authorization
Let’s start by making an important distinction: Access control consists of two main components: Authentication and Authorization. Many companies offer authentication services, allowing developers to easily verify user identities. Determining what actions a user can take after they log in (authorization) poses a bigger challenge.
Most authentication tools provide developers with straightforward code to integrate into their applications. They often supply an SDK, which gives developers a user object, based on which they can make the decision of allowed/denied. These products usually include a wide variety of pre-built features that developers can easily integrate into their code, such as MFA, User management, Login/Signup flows, etc. Not having to build these features from scratch saves a ton of valuable time and allows developers to incorporate secure, tested features into their applications with very minimal effort.
The existence of pre-built authentication tools creates a de-facto standard for building and implementing authentication. Instead of developers building it themselves, and security engineers providing them with ways to measure their own code, this standard directly impacts the way in which developers implement authentication, creating robust, industry-tested solutions.
Moving to authorization, the same kind of allow/deny decision is not only much harder to build, but the lack of unified industry standards only leaves us with measurement-based tools. In this reality, developers tend to build their own homebrew authorization solutions - leading to problematic results, with only measurement-based tools available to amend them.
The Issue with Homebrew Authorization
Let’s look at a typical way of implementing application-level authorization:
// Middleware
if (req.user.roles.indexOf(role) === -1) {
return res.send(403);
}
// Endpoint
@authz('admin')
const Document = () => {
...
}
It typically begins with decorators. By employing a decorator, a developer can set security requirements, such as ensuring that only admin users can execute certain operations on specific resources. To achieve this, the developer constructs a middleware—a function that operates right before the endpoint is called—to verify if a user possesses the "admin" role.
Application security and usability requirements are, however, usually more nuanced than this. Developers usually end up writing more complex code in the middleware, allowing decorators to assign multiple permission models to endpoints.
As the authorization code evolves, it becomes difficult to ensure in a "Shift-Left" approach that authorization is adequately secure due to its decentralized nature, and we're often left in the dark about how to thoroughly analyze and review the decisions made during the authorization process.
Think of this example - We've opted to let developers craft their own authorization layer. Then, the unthinkable happens: someone gains unauthorized access to our system.
Without a dedicated authorization system in place, a security engineer would examine the code, see the middleware, and recognize the framework that was promised to be secure, but the code offers no clarity on the breach's origin.
Vulnerabilities like this often occur when, for example, a well-intentioned developer deviates from the framework, deciding to introduce a simplistic 'if' statement to decide user access. Unfortunately, such ad-hoc solutions can unintentionally grant unauthorized users access to protected resources.
In today's era of shift-left and DevSecOps, the tools at our disposal primarily help us assess and measure access control. However, our primary objective now should be to consider how we can positively impact and guide our developers toward crafting a more robust and efficient authorization system. This can be achieved by understanding the autonomous authorization lifecycle.
The autonomous authorization lifecycle
To create an autonomous authorization system that helps positively impact apps and guide our developers instead of just offering measurements, it needs to adhere to the following standards:
Policy Authorship: The initial step is giving our security engineers, or any other relevant stakeholders in the organization a straightforward process through which they can create authorization policies.
Auditability: Once policies are in place, we must be able to review them consistently. A crucial aspect of any authorization system is the ability to perform audits on decisions made, ensuring that policies are being followed correctly and no unauthorized actions are taking place.
Autonomy: A key feature of the system is its self-sufficiency. We should be able to manage the entire authorization lifecycle without constantly altering the application's code. Whether it's auditing or authoring policies, these actions should not be coupled with the application's intricacies.
In essence, as we plan our authorization system, our foremost goal should be to establish a robust, self-reliant authorization lifecycle, decoupled from the application itself. This ensures agility, security, and efficiency in managing access rights and privileges. Understanding the autonomous authorization life cycle can help us guide developers toward implementing high security standards from the very beginning of the systems development life cycle without any extra measurement and effort.
How can we build such an authorization system?
The first step in creating an authorization system such as the one we described earlier is to establish two Contracts:
A contract between the Security team and the Development team
A contract between the Authorization system and the Application
Both of these contracts require us to establish a clear agreement on how we configure authorization policies and how we execute these configurations.
Both of these contracts can be achieved by employing a policy engine. A policy engine allows us to adhere to several important authorization best practices:
Decoupling Policy from Code: Utilizing a Policy Engine helps us keep the policy separate from the application code. Embedding authorization logic directly in code can lead to oversights and complications, especially when changes are made. By decoupling, we also ensure that developers don't inadvertently create untraceable policies.
Achieving Centralized Policy Management with Decentralized Architecture: Utilizing a Policy Engine allows us to keep policy management centralized for better control, uniformity, and ease of updates, and the architecture itself decentralized. This ensures the system is robust, scalable, and can work across various deployment environments, especially in a microservices-based architecture.
Model Agnosticism: Policy engines allow support for the authorization system to work with different permission models, whether it's RBAC, ABAC, ReBAC, or any other model. This ensures longevity and adaptability, allowing for changes in security requirements without necessitating a system overhaul. Note that different policy engines are better suited to handle different authorization models.
Developer Experience (DevEx) First: Prioritizing developer experience is crucial. A system that's difficult or cumbersome to use will be resisted or improperly implemented. The system should seamlessly integrate into a developer's workflow, not disrupt it.
There are several options when it comes to picking a policy engine. Each offers unique features and benefits, catering to different use cases and organizational needs. In this article, we want to focus on how you can achieve an autonomous, modern, and robust authorization system with AWS Cedar.
AWS Cedar
Cedar is a language for writing authorization policies, together with an engine for evaluating those policies to make authorization decisions, offering a unique approach to Policy-as-Code. While other policy languages (like Rego) tend to offer a multi-propose language that could fit into application-level authorization, Cedar is built with application-level authorization in mind.
Aside from impressive performance, one of the most significant advantages of Cedar is its readability. The language is designed to be extremely readable, empowering even non-technical stakeholders to read it (if not write it) for auditing purposes. Cedar policies are written in a declarative language, which means they can be easily understood and audited. It is also possible to ensure that policies are enforced correctly with Cedar's policy testing and simulation features.
Cedar autonomous lifecycle
Considering the authorization cycle with Cedar at its center:
- The Cedar File: This is the policy blueprint, essentially the foundation where rules and permissions are defined. You can read more about how a Cedar policy is designed here.
- Cedar Agent: an open-source project that allows for easy deployment of Cedar policies within your application. Acting as an HTTP server, Cedar Agent efficiently manages a policy store and a data store, allowing you to easily control and monitor access to your application's resources.
- Cedar Agent Logs: Post-decision, the agent maintains a log, capturing the specifics of every verdict. This log serves as a pivotal audit trail.
- At the heart of this setup is the application, which can seamlessly interact with the Cedar agent using an agnostic HTTP API. Whether it's to access audit logs, fetch the Cedar file, or liaise with the agent, the application's connection ensures an optimized authorization lifecycle that remains independent of the application’s intrinsic operations.
Why use Cedar?
By using Cedar, we can leverage all the previously mentioned best practices in one solution:
The Cedar language gives us a coherent way to define policies and decouple them from the application code.
Cedar is a model-agnostic language. This allows engineers to use it to define RBAC, ABAC, and even simple ReBAC, using simple structuring guidelines.
The Cedar agent, together with GitOps best practices, allows us to keep the configuration in a central environment. At the same time, the Cedar agent can run in a decentralized fashion across our deployments, maintaining the same config everywhere.
Cedar agent supports rich audit logs that give us an easy overview of the authorization layer when trying to analyze and understand the reasoning behind a decision.
At its heart, Cedar is a policy language created for application-level authorization, unlike other general-purpose languages. Using Cedar and its ecosystem gives the developer building access control exactly what they need, instead of creating more friction with useless features and steep learning curves.
Bottom line: Don’t just measure - Impact!
Looking at the OWASP top 10 Security risks in the past years, we can see the highest-ranked access control vulnerabilities changed from authentication to authorization. Today, authorization alone holds three of the top 10 places on the OWASP top 10 API Security risks for 2023 list.
Authentication has changed, focusing on impacting developers instead of “Shifting-left” by measuring them. It’s time authorization goes through the same process. Using authorization tools like AWS Cedar or Permit.io allows developers to create better authorization systems that make good use of the “Shift-Left” trend, focusing on impacting development from day one.
Want to learn more about Authorization and access control? Join our Slack Community, where world-class authorization specialists and hundreds of devs are discussing, building, and implementing authorization together.