Authorization with OPA and Envoy
In my previous post I described how we can use Envoy for Authentication. The purpose of authentication is to verify that someone or something is who they claim to be. This post is about Authorization which determines what they can do once they have access. (Who vs What).
In dotnet, authorization is usually done by verifying the user has the right scopes and/or application roles. To protect an asp.net or dotnetcore web API, you must add the [Authorize]
attribute on the controller or an action as below.
[Authorize(Roles = "client.read")]
MyController : ApiController
{
// ...
}
[Authorize(Roles = "client.write")]
public async Task<IActionResult> AddTodoItem(....)
Instead of doing this in application code, what we are looking at here is how we can do it using Envoy and Open Policy Agent(OPA). This will help to decouple authorization from application code and move it to Envoy which runs alongside the application in "Out of Process" manner.
Envoy supports an External Authorization filter which calls an authorization service to check if the incoming request is authorized or not. In this particular scenario, we use OPA as the authorization service which makes an informed decision about the fate of the incoming request received by Envoy.
What is OPA...
The Open Policy Agent (OPA, pronounced “oh-pa”) is CNCF graduated open source project that lets you specify policy as code and allow you to offload policy decision-making from your software. You can define those policies using high-level declarative language called Rego which is easy to read and write. Then you can enforce those policies in microservices, Kubernetes, CI/CD pipelines, API gateways, etc..
When your software needs to make policy decisions it queries OPA and supplies structured data as input. Then OPA evaluates those query input against policies/data and provide you the policy decision.
There are multiple ways you can integrate OPA. Most common way is deploying it as a sidecar. In this post we'll look at how we can do this using OPA-Envoy which is an extended version of OPA with a gRPC server that implements the Envoy External Authorization API.
Now that we got some basic understanding of OPA, Let's look at the code. (complete sample is here in github).
In the program.cs file we have 3 endpoints as below to simulate get and post requests. Other than that there is no any added code for authorization.
app.MapGet("/health", () => "alive");
app.MapPost("/weatherfeed", () => "im here");
app.MapGet("/weatherforecast", () =>
{
....
});
In the docker-compose.yml file, we have images for Envoy and OPA and In the docker.compose.override.yml
file, you will see that there are configs for both envoy and OPA as below. ( note that both envoy.yaml and policy.rego files added as volumes)
services:
envoy:
volumes:
- ./Envoy/config/envoy.yaml:/etc/envoy/envoy.yaml
ports:
- "5200:8000"
- "15200:8001"
opa:
volumes:
- ./opa-policy/policy.rego:/etc/policy.rego
command:
- run
- --server
- --log-level=debug
- --log-format=json-pretty
- --set=plugins.envoy_ext_authz_grpc.addr=:9191
- --set=decision_logs.console=true
- --set=plugins.envoy_ext_authz_grpc.path=envoy/authz/allow
- /etc/policy.rego
Now let's take a look at envoy.yaml. As describe above It has envoy.filters.http.ext_authz
Http filter which responsible for calling OPA to verify if the incoming request is authorized or not.
http_filters:
- name: envoy.ext_authz
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
transport_api_version: V3
with_request_body:
max_request_bytes: 8192
allow_partial_message: true
failure_mode_allow: false
grpc_service:
google_grpc:
target_uri: opa:9191
stat_prefix: ext_authz
timeout: 0.5s
Finally, let's examine policy.rego file. It will decode the incoming jwt_token
and extract roles. Then the configured OPA policy restricts/allows access to the endpoints exposed by our sample dotnet api:
/health
has no restrictions and its always allowed (nojwt_token
required).- Users with
client.read
role can perform aGET
request to/weatherforecast
. - Users with
client.readwrite
role can perform aPOST
request to/weatherfeed
.
package envoy.authz
import input.attributes.request.http as http_request
import input.parsed_path
default allow = false
allow {
parsed_path[0] == "health"
http_request.method == "GET"
}
allow {
print("Found Claims",claims.roles)
required_roles[r]
}
required_roles[r] {
perm := role_perms[claims.roles[r]][_]
perm.method = http_request.method
perm.path = http_request.path
}
claims := payload {
[_, payload, _] := io.jwt.decode(bearer_token)
}
bearer_token := t {
v := http_request.headers.authorization
startswith(v, "Bearer ")
t := substring(v, count("Bearer "), -1)
}
role_perms = {
"client.read": [
{"method": "GET", "path": "/weatherforecast"},
],
"client.readwrite": [
{"method": "POST", "path": "/weatherfeed"},
],
}
Now lets test this...
1) Clone the git repo.
2) Assuming you are in same directory as docker-compose.yml
file, run docker compose up
.
3) Now if you curl the below two endpoints you will see the result like below. Notice the /weatherforecast
endpoint fails with "403 Forbidden".
$ curl http://localhost:5200/health
alive
$ curl http://localhost:5200/weatherforecast -w "responsecode: %{response_code}\n"
responsecode: 403
4) Finally, follow my post on Azure AD app registration till the end to generate an access_token
with client.read
app role. Now curl the /weatherforecast
with the token as below.
TOKEN=<replace this with your access_token>
$ curl -H "Authorization: Bearer ${TOKEN}" http://localhost:5200/weatherforecast
[{"date":"2022-10-01T20:02:54.3453397+00:00","temperatureC":9,"summary":"Mild","temperatureF":48}]
5) Now if you try to use the same token which only has client.read
role to access /weatherfeed
you will end up with "403 Forbidden". (You need a access_token
with client.readwrite
role to perform the POST request to /weatherfeed
).
$ curl -H "Authorization: Bearer ${TOKEN}" http://localhost:5200/weatherfeed -w "responsecode: %{response_code}\n"
responsecode: 403
Also take a look at the OPA container logs to see the input received by OPA and detailed decision logs to better understand or debug...
That's All!