Implementing OPA authorization
Introduction
The Stackable Platform offers an OpenPolicyAgent (OPA) operator for policy-based access control. This document shows how to configure a Stackable operator and its managed product to query OPA to enforce policy-based access control.
What is OPA?
OPA is an open source, general purpose policy engine. It supports a high-level declarative language called Rego
. Rego enables you to specify complex policies as code and transfer the decision-making processes from your software to OPA. We refer to policies written in Rego as Rego rules.
The provided OPA REST API allows you to enforce policies within microservices, Kubernetes, CI/CD pipelines and more.
OPA accepts arbitrary structured input data (e.g. JSON) when running queries against the API (and your Rego rules), to decouple policy decision-making from policy enforcement.
Examples for OPA policies
The combination of arbitrary input data and the Rego rules enable you to specify and enforce almost any kind of policies. You can define powerful policies for e.g. user access for database tables, schemas, columns etc. You can enforce local network traffic, access time periods and many more.
See the OPA documentation for further examples.
Stackable Operator for OPA
The Stackable Operator for OPA deploys OPA as a DaemonSet in Kubernetes. This ensures that every registered Node runs exactly one OPA instance. In order to reduce traffic and latency, deployed products querying OPA must use the local OPA provided on their respective Node.
Service Discovery
Furthermore, the Stackable Operator for OPA deploys a service discovery ConfigMap (see Implementing service discovery) to expose its service URL to other Stackable operators. These operators then are configured to use the service discovery ConfigMap to extract the required URL and configure their products and authorizers. Authorizers are plugins for products that allow authorization and access control.
The ConfigMap has one data entry that points to the OPA ClusterIP service:
apiVersion: v1
kind: ConfigMap
metadata:
name: {clustername}
data:
OPA: http://{clustername}.{rolegroup}.svc.cluster.local:8081/
Provide Rego rules
In order for OPA to make policy decisions, Rego rules must be bundled and supplied. The Stackable Operator for OPA has its own controller to bundle policies and provide them to OPA. Polices can be provided via ConfigMaps:
apiVersion: v1
kind: ConfigMap
metadata:
name: simple-product-rego
labels:
opa.stackable.tech/bundle: "product"
data:
product.rego: |
package product
allow {
true
}
The OPA bundle controller creates a bundle.tar.gz
file locally, bundling the content of every ConfigMap labeled with opa.stackable.tech/bundle
(the label value can be arbitrary). This bundle is read and activated by OPA.
You can query the allow
rule provided in the example above via:
http://{clustername}.{rolegroup}.svc.cluster.local:8081/data/v1/product/allow
Consume the discovery ConfigMap
In order to configure another operator and its product to query OPA, the service discovery ConfigMap
must be consumed. There are two ways that qualify as best practice, depending on the product you are configuring:
Configure OPA access in the operator
The operator-rs
framework has a module called opa.rs
that offers a predefined struct OpaConfig and several helper methods to extract the OPA URL from the service discovery ConfigMap.
#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OpaConfig {
pub config_map_name: String,
pub package: Option<String>,
}
You can use that struct in the CRD of your operator to configure the OPA service discovery ConfigMap as well as the desired package name. The method full_document_url_from_config_map
reads the service discovery ConfigMap provided by the Stackable operator for OPA and constructs the URL to configure the product or its authorizers (the plugins that talk to OPA).
Usually OPA authorizers are configured with the URL to the OPA REST API, authentication / security and other properties like caching. This differs from authorizer to authorizer.