Config-Driven ABAC Plugin For Kong Gateway
Introduction to Config-Driven Attribute-Based Access Control (ABAC)
Hey folks! Let's dive into the world of secure APIs and how we can control access to them. We're talking about a config-driven ABAC plugin for Kong Gateway. What does that even mean? Well, it's about building a system that decides whether to let someone access an API based on various attributes associated with the request and the user. Think of it like a smart bouncer at a club, but instead of checking IDs, it checks a bunch of things like your role, what you're trying to access, and even the time of day. The best part? You can configure all of this without touching any code. Super cool, right?
This plugin will be your digital gatekeeper, providing fine-grained authorization at the API gateway level. The core idea is simple: You define a set of policies that describe who can do what, and the plugin enforces those policies. These policies are written in a configuration file (like YAML or JSON) and loaded into the plugin. When a request comes in, the plugin extracts various attributes (like user roles, the API path, the request method, etc.) from the request, compares them against the configured policies, and decides whether to allow or deny the request. By default, it's a deny-by-default model, meaning that if no policy matches, access is denied. This is the safest approach, ensuring nothing gets through unless explicitly permitted. We will walk through how to build the Kong ABAC plugin, how to set it up, and how to configure policies to meet security requirements.
The Core Concept of Config-Driven ABAC
At its heart, ABAC is about making access control decisions based on attributes. These attributes can be anything relevant to the request or the user. We will look at how this plugin is made for Kong Gateway. Think of it like this:
- Subjects: Who is trying to access the API? (e.g., user roles, user type).
- Resources: What are they trying to access? (e.g., API paths, HTTP methods).
- Actions: What are they trying to do? (e.g., GET, POST, PUT, DELETE).
- Environment: What are the circumstances? (e.g., time of day, IP address).
The beauty of config-driven ABAC is that you can change the access rules without rewriting code. You simply modify the configuration file and reload the plugin. This makes it far easier to adapt to evolving security needs and business requirements. This article gives you a step-by-step guide to achieving the end results. This is what we will do:
- Define the ABAC Model and Policy Schema.
- Implement the Attribute Extraction Design.
- Establish the Plugin Architecture and Flow.
- Develop a Policy Evaluation Engine.
- Build the Configuration Model and Examples.
- Handle Error Handling and Logging.
- Address Security Considerations.
- Consider Performance and Caching.
- Implement Testing and Validation.
- Provide Documentation and Examples.
Defining the ABAC Model and Policy Schema
First things first, we need to define the rules of the game. We need to figure out what attributes we'll be using to make decisions. Think of these as the ingredients for our access control recipe. Here's a breakdown of the key attributes we should support in our v1 plugin:
- Subject Attributes: These describe the user or the entity making the request. Examples include: user roles (e.g.,
ADMIN,USER,BILLING_READ), groups, user type (e.g.,CITIZEN,EMPLOYEE), the user's jurisdiction, and the user's department. These attributes will likely come from the user's JWT or other authentication mechanisms. - Resource Attributes: These describe the API being accessed. This includes the service/module name, the specific API path (e.g.,
/billing/transactions), the HTTP method (GET, POST, etc.), and the tenant ID (if applicable). - Context Attributes: These describe the environment in which the request is made. This might include the time of day, the environment (e.g.,
production,staging), and the request origin (IP address, network).
Policy Schema in YAML/JSON
Once we have our attributes, we need a way to express our access control policies. We'll use a policy schema, which will be written in a human-readable format, such as YAML or JSON. Here's an example to get you started:
policies:
- id: "employee-read-billing"
description: "Allow employees with BILLING_READ role to access billing APIs"
effect: "allow" # "allow" or "deny"
subjects:
roles: ["BILLING_READ"]
userType: ["EMPLOYEE"]
resources:
paths: ["/billing/*"]
methods: ["GET"]
tenants: ["pb", "uk"]
conditions:
- type: "timeWindow"
after: "08:00"
before: "20:00"
- id: "block-cross-tenant-access"
effect: "deny"
resources:
tenants: ["*"]
conditions:
- type: "mismatch"
attribute: "userTenant"
resourceAttribute: "tenantFromPath"
Let's break down this example:
policies: This is a list of individual policies.id: A unique identifier for the policy (e.g.,employee-read-billing).description: A human-readable description of what the policy does.effect: Whether toallowordenyaccess if the policy matches. Adenypolicy usually takes precedence.subjects: Who the policy applies to. In this example, it applies to users with theBILLING_READrole and theEMPLOYEEuser type.resources: What the policy applies to. In this example, it applies to paths starting with/billing/, theGETmethod, and thepbanduktenants.conditions: Additional criteria that must be met. Here, access is only allowed between 8 AM and 8 PM.
Policy Evaluation and Defaults
Now, how does the plugin actually decide whether to allow or deny a request? Here's how policy evaluation generally works:
- The plugin extracts attributes from the request.
- It iterates through the policies in order.
- For each policy, it checks if the attributes match the
subjects,resources, andconditionsdefined in the policy. - If a policy matches, the
effect(allow or deny) is applied. If multiple policies match, a defined conflict resolution strategy is used (e.g., deny overrides allow). - If no policy matches, the default behavior is to deny access. This is the deny-by-default principle in action.
Attribute Extraction: Unveiling the Request Data
Next up, we need to design how the plugin will actually get the data it needs to make its decisions. This is the attribute extraction phase, where we pull relevant information from the incoming request. Here's where the magic happens:
Where Attributes Come From
The plugin will need to pull attributes from a variety of sources:
- JWT Access Tokens: JWTs (JSON Web Tokens) are commonly used to authenticate users. The plugin will extract claims from the JWT, such as:
sub(subject, usually the user ID),tenantId,roles, and any custom claims related to your application. Make sure to choose the correct JWT plugin and set it up correctly. - HTTP Headers: Headers can carry important information. For instance,
x-tenant-idmight indicate the tenant the user belongs to, andx-user-typecould indicate the user's role. - Request Metadata: The plugin can also access request metadata, like the API path, the HTTP method, query parameters, and the hostname. These are often used to identify the resource being accessed.
Mapping and Error Handling
To make this all work, we'll need a way to map the data from these different sources to our attributes. For example:
userTenantcan be extracted from thex-tenant-idheader or from atenantIdclaim in the JWT.resourceTenantmight be extracted from a segment of the URL, such as/tenant/{tenantId}/....
What happens if a required attribute is missing? The plugin needs to handle this gracefully. The options include:
- Rejecting the request: This is the most secure option. If a required attribute is missing, the request is denied.
- Falling back to a default safe value: In some cases, you might have a default value you can use. For example, if the tenant ID is missing, you might default to a