
.NET: Open Policy Agent (OPA) with Styra DAS
Open Policy Agent (OPA), pronounced “oh-pha”, is an incredible technology for decoupling authorization from applications. Styra the company that creates and maintains OPA has a tool called Styra DAS that provides a UI for visualizing decisions, authorizations, troubleshooting, testing, and monitoring policy. This tutorial will demonstrate how to set up Styra DAS on a custom namespace, apply mutations, and deploy your first .NET API microservice that uses OPA.
This tutorial will demonstrate how to integrate Styra DAS/OPA into a .NET Microservice running on a Kubernetes cluster with Istio as the gateway.
Requirements
- Kubernetes / Istio
- Open Policy Agent (OPA)
- Styra DAS (FREE)
- Duende Identity Server 6
- Helm
- .NET 8 / Microservices
GitHub
GitHub: mrjamiebowman’s OPA Styra Das
Home Lab
Note: My current home lab is an HPE Gen 10 Microserver running VMware ESXi 8 and I am running a Virtual Machine with Ubuntu and Rancher RKE single node cluster. I’m using Azure DevOps to build and push the image to the Container Registry and ArgoCD to continuously deploy helm, Kubernetes, and application changes to the cluster.
Open Policy Agent (OPA)
Using OPA has some profound benefits that can make your environment more secure, safe, and testable, but, get this, the developers no longer have to write authorization code.
Decoupled Architecture
Decoupling authorization code from your services has a lot of hidden benefits. OPA policies have the capability of inheriting other policies and thus sharing rules with other policies through Rego code. This means there is less policy code and duplication which follows the Don’t Repeat Yourself (DRY) principle. Simply reducing code reduces mistakes. These policies can be checked into source control and maintained separately from the applications.
Rego Code
Rego code is straightforward to learn and get started with. It’s very capable of doing complex things like JWT Validation and API calls to validate access. Rego policies are shipped and stored together in what are known as Bundles.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package istio.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 { parsed_path[0] == "hc" http_request.method == "GET" } allow { parsed_path[0] == "up" http_request.method == "GET" } |
Security
There are many different architectures for setting up OPA, but in this tutorial, OPA sits as a sidecar on a Kubernetes pod admitting access to the underlying service based on policy decisions. OPA can be more secure in the sense that policy decisions can easily be logged, validated, replayed, and tested. The life cycle of OPA policies in general is going to be far more secure than having authorization code distributed throughout the code of multiple applications. Having it better organized will lead to better Cloud Governance, especially at scale. There have been some concerns about Kubernetes cluster security, however, if a hacker has compromised the cluster then you’re going to have much bigger problems. If a hacker compromises an individual service and is able to execute commands against other internal APIs, those services will still be protected through OPA. The policies will still apply and the hacker will be limited.
Testing
Rego policies can easily be tested through Rego test code and instructions. This is wonderful because if a policy has changed the tests can be written, ran, and reviewed for thoroughness. This can significantly reduce mistakes through a system of checks and balances.
CI/CD Pipeline Compatible
Rego Bundles can be produced, packaged, and tested in a Continous Integration (CI) / Continous Deployment (CD) scenario. This means with a click of a button your application’s authorization code can be updated without re-deploying the apps. Not to mention, this bundle will not be deployed if all of the policy tests don’t pass.
Styra DAS
Hold on to your mouse! Styra DAS has to be the hottest-coolest Identity tool that I’ve seen come to market. I have absolutely been blown away by what Styra DAS is capable of.
Flexibility
Styra is capable of being installed on many different types of systems.
Common Systems
- Envoy
- Istio
- Kong Gateway / Mesh
- Kubernetes
- Terraform
Architectural Patterns
In addition, there are several different common architectural patterns that Styra DAS supports.
Capabilities
Styra DAS is much more than just an interface to Rego policy and decisions. It is very capable of doing much more than that.
Kubernetes Mutations
This has to be one of Styra’s most impressive features. Using Rego code, mutations can be dynamically applied to the Kubernetes manifest based on policy. In the tutorial below we will mutate pods to inject the OPA sidecar and bundle based on the namespace and other validating requirements (is it a pod? is it a create? ..or update? etc…).
GitHub: Kurt Roekle’s Mutations
Search Capabilities
The search capabilities of Styra DAS are built on Lucene syntax.
Policy Replay
Being able to replay and analyze a policy decision is absolutely crucial for being able to determine why and how a rule or decision was applied. This allows for much quicker development, support, and management of policies.
In this next image, we can see that the user does not have the correct role to access /api/security/grid
.

Initial Kubernetes Setup
1 2 3 4 5 |
# create myapp namespace kubectl create ns myapp # enable istio injection kubectl label namespace myapp istio-injection=enabled --overwrite |
Deploying Styra DAS to Kubernetes
So this is where it gets tricky and Styra DAS really starts to shine with its extensive features. I found this process a bit confusing since there are many different ways to configure Styra DAS systems and different styles of architecture. Styra was very willing to help me understand and configure this. Their Technical Architect, Kurt Roekle was very helpful and provided a sample mutation that I modified to work with this particular scenario. In order to do this, I need to install 2 systems, one for Kubernetes and one for Istio which will provide application-level authorization and policy decisions through an injected sidecar. The Kubernetes system would track cluster-level authorization and apply mutations.
System ID
Each Styra DAS system will have a unique System ID that is often used in configuration and is important to know for troubleshooting.
Installing Styra DAS to a Kubernetes System
It is best to install the Kubernetes system first before installing the Istio system. It’s going to be much easier to install and it’s required for the pod mutations. If this can’t be installed and working none of the processes below will work either.
// TODO: PICS / INSTRUCTIONS
Installing Styra DAS to an Istio System
Note: Installing the Istio System is a bit tricky when it comes to deploying to a custom namespace but I will show you how to do this. This part may be updated and removed entirely because I hear they are planning on adding the ability to set the namespace on the deployment. This section is subject to change.
Create the Istio System
Creating the Istio System in Styra DAS is very easy. Just name it and click “Add system”. This will open a window that defaults to the “Settings” tab with Install instructions.

Download the Manifest Files
Instead of running the commands in the settings, we will download the manifest files so we can modify them before deploying this to our Kubernetes cluster. That way we can add our custom namespace to their manifest files.
Modifying the Manifest Files
The manifest files as they are will deploy to the default namespace since they don’t have a specified namespace. We need to modify this so it goes to the “myapp” namespace by adding the namespace property through a variable.
With this architectural pattern with Styra DAS, we will create a new system for each set of microservices. Therefore, we need to modify the manifest files to include the namespace that it belongs to.
Now, there are 3 files downloaded and I will include these in the Helm chart, however, the EnvoyFilter.yaml
doesn’t need to be modified. I include that in the Helm charts just because it’s easier to keep it all together.
Modifying values.yaml
The Helm values.yaml
file should be empty and contain this value.
1 |
namespace: "myapp" |
Modifying OpaConfig.yaml
This one is easy all you have to do here is add the namespace to the metadata path. Also, take note that the System ID is used here.
1 2 3 |
metadata: namespace: {{ .Values.namespace }} |

Modifying Slp.yaml
The slp.yaml
file has a lot of services necessary to install and set up the Styra Local Control Plane (SLP) and will require careful attention.
All of the individual manifests (Secret, Service, StatefulSet) will need this added to them.
1 2 |
metadata: namespace: {{ .Values.namespace }} |
The StatefulSet has a property for creating a Volume Claim, add it there.
1 2 3 4 |
spec: volumeClaimTemplates: - metadata: namespace: {{ .Values.namespace }} |
Deploying Styra DAS via Helm Charts
Run this command after modifying the files with the namespace.
1 2 |
# deploy styra das helm install -n myapp charts/styradas |
Verifying the Styra DAS Installation
To verify that the service has deployed correctly you will want to check several things to know if Styra DAS was deployed correctly.
Logs
The logs should look like JSON without any errors.
OPA Sidecar
We want to make sure that the mutation does not apply an OPA sidecar to this service. At this point, we haven’t set up the mutation but sometimes we re-create systems and clusters so it’s a good habit to learn to check this. When I run the command below I can see 1/1 containers ready. This tells me that the OPA sidecar was not injected. This is good because if it had been injected there would be 1/2 in the READY state.
1 2 3 4 |
kubectl get pods -n myapp NAME READY STATUS RESTARTS AGE <strong>slp-istio-app-0 1/1 Running 0 114s</strong> |
ConfigMap
If you are deploying multiple times it may be a good idea to check the opa-istio-config
to see if the System ID matches.
1 2 3 4 5 6 |
kubectl get cm -n myapp NAME DATA AGE istio-ca-root-cert 1 6m59s kube-root-ca.crt 1 6m59s <strong>opa-istio-config 1 4m39s</strong> |
Secrets
This secret contains the token bearer access token that is needed to communicate with Styra DAS.
1 2 3 4 5 |
kubectl get secrets -n myapp NAME TYPE DATA AGE sh.helm.release.v1.styradas.v1 helm.sh/release.v1 1 5m5s <strong>slp-istio Opaque 1 5m5s</strong> |
Setting up Pod Mutations in Styra DAS
I was completely unaware that this is a strategy for injecting OPA sidecars into pods but I can assure you, this is the best way I’ve seen this done. This technique gives a lot of flexibility with which pods get OPA injected whereas a namespace label would apply to all pods in a namespace. Manually applying this through manifest would still mean the application has to be redeployed. Using Styra DAS allows me to dynamically apply this policy in several ways. If I needed to disable it, I could modify the mutation by setting it to “Monitor” or “Ignore”, then restart the deployment, and the OPA sidecar would be removed. I can modify the mutation and restart the application deployment to see if the sidecar has been injected. It’s actually very easy once you learn it.
I also recommend the OPA and Styra Slack Channels.
GitHub: Kurt Roekle’s Mutations
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
package policy["com.styra.kubernetes.mutating"].rules.rules # collection of rules injectablePod { input.request.kind.kind = "Pod" data.kubernetes.resources.namespaces[input.request.namespace].metadata.labels["istio-injection"] = "enabled" not input.request.object.metadata.labels["app"] = "slp" # slp does not need an opa sidecar not opaContainerExists isValidNamespace isCreateOrUpdate # calls both methods below (really cool..) } # policy enforce[decision] { # title: Inject OPA sidecar injectablePod decision := { "allowed": true, "message": "Adding OPA Injection", "patch": patches } } patches = [ opaPath, volumePatch ] # create envoy opa sidecar and mount configuration into opa opaPath = patch { patch := { "op": "add", "path": "/spec/containers/-", "value": { "image": "openpolicyagent/opa:latest-envoy", "name": "opa", "args": [ "run", "--server", "--config-file=/config/conf.yaml" ], "startupProbe": { "httpGet": { "path": "/health?bundles", "port": 8181 } }, "volumeMounts": [ { "mountPath": "/config", "name": "opa-config-vol" }] } } } # create a volume for opa's configuration volumePatch = patch { patch := { "op": "add", "path": "/spec/volumes/-", "value": { "name": "opa-config-vol", "configMap": { "name": "opa-istio-config" } } } } # limit to 1 opa sidecar opaContainerExists { input.request.object.spec.containers[_].name == "opa" } # create? isCreateOrUpdate { input.request.operation == "CREATE" } # update? isCreateOrUpdate { input.request.operation == "UPDATE" } # must be in myapp namespace isValidNamespace { input.request.namespace == "myapp" } |
Application Setup
This will demonstrate using a simple API that authorizes through the Identity Server using a username and password credentials. The application will be minimal and run in a Docker container deployed on Kubernetes through Helm. The Helm charts will be included in the GitHub repository.
.NET 8 Minimal API
With the latest .NET, building minimal APIs is very easy and can be done on a single .csharp
file. This service will have 3 API Methods that will be protected through OPA.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
using MrJB.OPA.StyraDas.API; var builder = WebApplication.CreateSlimBuilder(args); var app = builder.Build(); var securityApi = app.MapGroup("api/security/grid"); securityApi.MapGet("/", () => Results.Ok("Access Granted Security Grid!")); var systemApi = app.MapGroup("api/system/test"); systemApi.MapGet("/", () => Results.Ok("Access Granted to System Test!")); app.Run(); |
Note: .NET 8 is currently in preview and will be released in November of 2023 and be tagged as a Long Term Support (LTS) release.
Deploy Application Helm Charts
1 2 |
# deploy microservice api helm install -n myapp charts/myapp |
Verifying the Pod Mutations
If the app is deploying it should create 3 containers (Istio, OPA, App) and 1 init container (Istio startup). We need to verify a few things to make sure it’s starting up correctly. First, we’ll want to check that the OPA container is there and that it is not throwing any errors. Then we can view the schema of the Pod just to look over the modified YAML.
OPA Policy
/api/up – always allow anonymous
/api/security/grid – only allow Dennis
/api/system/test – allow Ray and Dennis
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
package policy.ingress import future.keywords import input.attributes.request.http as http_request import input.parsed_path default allow = false # allow opa health check allow { parsed_path[0] == "health" http_request.method == "GET" } # allow /api/hc allow { parsed_path[0] == "api" parsed_path[1] == "hc" http_request.method == "GET" } # allow /api/up allow { parsed_path[0] == "api" parsed_path[1] == "up" http_request.method == "GET" } allow if { some r in roles_for_user r in required_roles } roles_for_user contains r if { some r in user_roles[user_name] } required_roles contains r if { some perm in role_perms[r] perm.method == http_request.method perm.path == http_request.path } user_name := parsed if { [_, encoded] := split(http_request.headers.authorization, " ") [parsed, _] := split(base64url.decode(encoded), ":") } user_roles := { "ray": ["guest"], "dennis": ["admin"], } role_perms := { "guest": [{"method": "GET", "path": "/api/system/test"}], "admin": [ {"method": "GET", "path": "/api/security/grid"}, {"method": "GET", "path": "/api/system/test"} ], } |
Testing Authorization
We will demonstrate this using basic auth which will not check the password and only verify that the Username matches the correct path and security role.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# get ip of the service export SERVICE_HOST=$(kubectl -n myapp get service myapp -o jsonpath='{.status.loadBalancer.ingress[0].ip}') # allow all curl -i http://$SERVICE_HOST/api/up # dennis curl --user dennis:password -i http://$SERVICE_HOST/api/security/grid curl --user dennis:password -i http://$SERVICE_HOST/api/system/test # ray curl --user ray:password -i http://$SERVICE_HOST/api/security/grid curl --user ray:password -i http://$SERVICE_HOST/api/system/test |
Further Reading
Styra DAS: Istio Documentation
Envoy External Authorization Filter