Cerbos authorization management platform ==================== Cerbos is an authorization management platform for applications, APIs, AI agents, MCP servers, services, and workloads. It works alongside your application to produce fine-grained, contextual access decisions based on policies you define. The platform is composed of four components. Each can be adopted independently. ## [](#%5Fplatform%5Fcomponents)Platform components | **Cerbos PDP** | The open-source policy decision point. Evaluates authorization requests against policy definitions and returns decisions. Stateless, AuthZEN-compliant, and designed to run anywhere including container orchestrators like Kubernetes, serverless runtimes, and edge environments. [Cerbos PDP documentation →](what-is-cerbos.html) | | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Cerbos Hub** | The control plane for Cerbos PDPs with centralised policy authoring, versioning, testing, secure distribution, and audit log aggregation. Create multiple deployments for your different environments or systems and any policy changes are automatically tested and pushed to the relevant PDPs without restarts or redeployments. All requests and decisions made by the PDPs are aggregated and stored to help with security investigations and comply with SOC 2, ISO 27001, HIPAA, PCI DSS, and GDPR audit requirements. [Cerbos Hub documentation →](../../cerbos-hub/index.html) | | **Cerbos Synapse** | A customisable context enrichment layer for the PDP. Intercept and enrich requests and responses to/from the PDP with identity, resource, and relationship data from identity providers, databases, graph stores, and internal APIs. Use Cerbos policies with systems that support external authorization such as Envoy, Kafka, and Trino. [Cerbos Synapse documentation →](../../synapse/latest/index.html) | | **Cerbos PEP Integrations** | Client libraries and integrations for calling the PDP from application code. Available for JavaScript, Go, Python, Java, .NET, Rust, PHP, and Ruby. [PEP Integrations →](https://www.cerbos.dev/ecosystem) | ## [](#%5Fhow%5Fcerbos%5Fworks)How Cerbos works ![How Cerbos works](_images/how_cerbos_works.png) Quickstart ==================== Create a directory to store the policies. ```sh mkdir -p cerbos-quickstart/policies ``` Now start the Cerbos PDP server. We are using the container image in this guide but you can follow along using the binary as well. See [installation instructions](installation/binary.html) for more information. ```shell docker run --rm --name cerbos -d -v $(pwd)/cerbos-quickstart/policies:/policies -p 3592:3592 -p 3593:3593 ghcr.io/cerbos/cerbos:0.55.0 ``` Time to try out a simple request. | | If you prefer to use [Postman](https://www.postman.com), [Insomnia](https://insomnia.rest) or any other software that supports OpenAPI, you can follow this guide along on those tools by downloading the OpenAPI definitions from . You can also use the built-in API browser by pointing your browser to . | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | * cURL * .NET * Go * Java * JS * PHP * Python * Ruby * Rust ```shell cat < {(effect == Effect.Allow ? "EFFECT_ALLOW" : "EFFECT_DENY")}\n"); } } } } ``` ```go package main import ( "context" "log" "github.com/cerbos/cerbos-sdk-go/cerbos" ) func main() { c, err := cerbos.New("localhost:3593", cerbos.WithPlaintext()) if err != nil { log.Fatalf("Failed to create client: %v", err) } principal := cerbos.NewPrincipal("bugs_bunny", "user") principal.WithAttr("beta_tester", true) kind := "album:object" actions := []string{"view:public", "comment"} r1 := cerbos.NewResource(kind, "BUGS001") r1.WithAttributes(map[string]any{ "owner": "bugs_bunny", "public": false, "flagged": false, }) r2 := cerbos.NewResource(kind, "DAFFY002") r2.WithAttributes(map[string]any{ "owner": "daffy_duck", "public": true, "flagged": false, }) batch := cerbos.NewResourceBatch() batch.Add(r1, actions...) batch.Add(r2, actions...) resp, err := c.CheckResources(context.Background(), principal, batch) if err != nil { log.Fatalf("Failed to check resources: %v", err) } log.Printf("%v", resp) } ``` ```java package demo; import static dev.cerbos.sdk.builders.AttributeValue.boolValue; import static dev.cerbos.sdk.builders.AttributeValue.stringValue; import java.util.Map; import dev.cerbos.sdk.CerbosBlockingClient; import dev.cerbos.sdk.CerbosClientBuilder; import dev.cerbos.sdk.CheckResult; import dev.cerbos.sdk.builders.Principal; import dev.cerbos.sdk.builders.ResourceAction; public class App { public static void main(String[] args) throws CerbosClientBuilder.InvalidClientConfigurationException { CerbosBlockingClient client=new CerbosClientBuilder("localhost:3593").withPlaintext().buildBlockingClient(); for (String n : new String[]{"BUGS001", "DAFFY002"}) { CheckResult cr = client.batch( Principal.newInstance("bugs_bunny", "user") .withAttribute("beta_tester", boolValue(true)) ) .addResources( ResourceAction.newInstance("album:object","BUGS001") .withAttributes( Map.of( "owner", stringValue("bugs_bunny"), "public", boolValue(false), "flagged", boolValue(false) ) ) .withActions("view:public", "comment"), ResourceAction.newInstance("album:object","DAFFY002") .withAttributes( Map.of( "owner", stringValue("daffy_duck"), "public", boolValue(true), "flagged", boolValue(false) ) ) .withActions("view:public", "comment") ) .check().find(n).orElse(null); if (cr != null) { System.out.printf("\nResource: %s\n", n); cr.getAll().forEach((action, allowed) -> { System.out.printf("\t%s -> %s\n", action, allowed ? "EFFECT_ALLOW" : "EFFECT_DENY"); }); } } } } ``` ```javascript const { GRPC: Cerbos } = require("@cerbos/grpc"); const cerbos = new Cerbos("localhost:3593", { tls: false }); (async() => { const kind = "album:object"; const actions = ["view:public", "comment"]; const cerbosPayload = { principal: { id: "bugs_bunny", roles: ["user"], attributes: { beta_tester: true, }, }, resources: [ { resource: { kind: kind, id: "BUGS001", attributes: { owner: "bugs_bunny", public: false, flagged: false, }, }, actions: actions, }, { resource: { kind: kind, id: "DAFFY002", attributes: { owner: "daffy_duck", public: true, flagged: false, }, }, actions: actions, }, ], }; const decision = await cerbos.checkResources(cerbosPayload); console.log(decision.results) })(); ``` ```php withPlaintext(true) ->build(); $request = CheckResourcesRequest::newInstance() ->withRequestId(RequestId::generate()) ->withPrincipal( Principal::newInstance("bugs_bunny") ->withRole("user") ->withAttribute("beta_tester", AttributeValue::boolValue(true)) ) ->withResourceEntries( [ ResourceEntry::newInstance("album:object", "BUGS001") ->withAttribute("owner", AttributeValue::stringValue("bugs_bunny")) ->withAttribute("public", AttributeValue::boolValue(false)) ->withAttribute("flagged", AttributeValue::boolValue(false)) ->withActions(["comment", "view:public"]), ResourceEntry::newInstance("album:object", "DAFFY002") ->withAttribute("owner", AttributeValue::stringValue("daffy_duck")) ->withAttribute("public", AttributeValue::boolValue(true)) ->withAttribute("flagged", AttributeValue::boolValue(false)) ->withActions(["comment", "view:public"]) ] ); $checkResourcesResponse = $client->checkResources($request); foreach (["BUGS001", "DAFFY002"] as $resourceId) { $resultEntry = $checkResourcesResponse->find($resourceId); $actions = $resultEntry->getActions(); foreach ($actions as $k => $v) { printf("%s -> %s", $k, Effect::name($v)); } } ?> ``` ```python import json from cerbos.sdk.client import CerbosClient from cerbos.sdk.model import Principal, Resource, ResourceAction, ResourceList from fastapi import HTTPException, status principal = Principal( "bugs_bunny", roles=["user"], attr={ "beta_tester": True, }, ) actions = ["view:public", "comment"] resource_list = ResourceList( resources=[ ResourceAction( Resource( "BUGS001", "album:object", attr={ "owner": "bugs_bunny", "public": False, "flagged": False, }, ), actions=actions, ), ResourceAction( Resource( "DAFFY002", "album:object", attr={ "owner": "daffy_duck", "public": True, "flagged": False, }, ), actions=actions, ), ], ) with CerbosClient(host="http://localhost:3592") as c: try: resp = c.check_resources(principal=principal, resources=resource_list) resp.raise_if_failed() except Exception: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Unauthorized" ) print(json.dumps(resp.to_dict(), sort_keys=False, indent=4)) ``` ```ruby require 'cerbos' require 'json' client = Cerbos::Client.new("localhost:3593", tls: false) kind = "album:object" actions = ["view:public", "comment"] r1 = { :kind => kind, :id => "BUGS001", :attributes => { :owner => "bugs_bunny", :public => false, :flagged => false, } } r2 = { :kind => kind, :id => "DAFFY002", :attributes => { :owner => "daffy_duck", :public => true, :flagged => false, } } decision = client.check_resources( principal: { id: "bugs_bunny", roles: ["user"], attributes: { beta_tester: true, }, }, resources: [ { resource: r1, actions: actions }, { resource: r2, actions: actions }, ], ) res = { :results => [ { :resource => r1, :actions => { :comment => decision.allow?(resource: r1, action: "comment"), :"view:public" => decision.allow?(resource: r1, action: "view:public"), }, }, { :resource => r2, :actions => { :comment => decision.allow?(resource: r2, action: "comment"), :"view:public" => decision.allow?(resource: r2, action: "view:public"), }, }, ], } puts JSON.pretty_generate(res) ``` ```rust use cerbos::sdk::attr::attr; use cerbos::sdk::model::{Principal, Resource, ResourceAction, ResourceList}; use cerbos::sdk::{CerbosAsyncClient, CerbosClientOptions, CerbosEndpoint, Result}; #[tokio::main] async fn main() -> Result<()> { let opt = CerbosClientOptions::new(CerbosEndpoint::HostPort("localhost", 3593)).with_plaintext(); let mut client = CerbosAsyncClient::new(opt).await?; let principal = Principal::new("bugs_bunny", ["user"]).with_attributes([attr("beta_tester", true)]); let actions: [&str; 2] = ["view:public", "comment"]; let resp = client .check_resources( principal, ResourceList::new_from([ ResourceAction( Resource::new("BUGS001", "album:object").with_attributes([ attr("owner", "bugs_bunny"), attr("public", false), attr("flagged", false), ]), actions, ), ResourceAction( Resource::new("DAFFY002", "album:object").with_attributes([ attr("owner", "daffy_duck"), attr("public", true), attr("flagged", false), ]), actions, ), ]), None, ) .await?; println!("{:?}", resp.response); Ok(()) } ``` In this example, the `bugs_bunny` principal is trying to perform two actions (`view:public` and `comment`) on two `album:object` resources. The resource instance with the ID `BUGS001` belongs to `bugs_bunny` and is private (`public` attribute is `false`). The other resource instance with the ID `DAFFY002` belongs to `daffy_duck` and is public. This is the response from the server: Response ```json { "requestId": "quickstart", "results": [ { "resource": { "id": "BUGS001", "kind": "album:object" }, "actions": { "comment": "EFFECT_DENY", "view:public": "EFFECT_DENY" } }, { "resource": { "id": "DAFFY002", "kind": "album:object" }, "actions": { "comment": "EFFECT_DENY", "view:public": "EFFECT_DENY" } } ] } ``` Bugs Bunny is not allowed to view or comment on any of the album resources — even the ones that belong to him. This is because currently there are no policies defined for the `album:object` resource. Now create a [derived roles](policies/derived%5Froles.html) definition that assigns the `owner` dynamic role to a user if the `owner` attribute of the resource they’re trying to access is equal to their ID. ```sh cat > cerbos-quickstart/policies/derived_roles_common.yaml < cerbos-quickstart/policies/resource_album.yaml < cerbos-quickstart/policies/resource_album.yaml <..v/`). For example, a resource policy for `leave_request` with version `default` and scope `acme.hr` would have the ID `resource.leave_request.vdefault/acme.hr`. curl -k -u cerbos:cerbosAdmin -X POST \ 'https://localhost:3592/admin/policy/delete?id=principal.donald_duck.vdefault&id=derived_roles.my_derived_roles' Response ```json { "deletedPolicies": 2 (1) } ``` | **1** | Number of policies deleted | | ----- | -------------------------- | ### [](#disable-policies)Disable Policies POST /admin/policy/disable?id=policy_id PUT /admin/policy/disable?id=policy_id DELETE /admin/policy?id=policy_id [DEPRECATED] | | This endpoint requires a mutable storage driver such as [sqlite3](../configuration/storage.html#sqlite3) to be configured. | | ----------------------------------------------------------------------------------------------------------------------------- | Issue a POST request to the endpoint with the list of IDs (the `id` query parameter can be repeated multiple times) to disable. IDs follow the same format as the output of `ListPolicies` (`..v/`). For example, a resource policy for `leave_request` with version `default` and scope `acme.hr` would have the ID `resource.leave_request.vdefault/acme.hr`. curl -k -u cerbos:cerbosAdmin -X POST \ 'https://localhost:3592/admin/policy/disable?id=principal.donald_duck.vdefault&id=derived_roles.my_derived_roles' Response ```json { "disabledPolicies": 2 (1) } ``` | **1** | Number of policies disabled | | ----- | --------------------------- | ### [](#enable-policies)Enable Policies POST /admin/policy/enable?id=policy_id PUT /admin/policy/enable?id=policy_id | | This endpoint requires a mutable storage driver such as [sqlite3](../configuration/storage.html#sqlite3) to be configured. | | ----------------------------------------------------------------------------------------------------------------------------- | Issue a POST request to the endpoint with the list of IDs (the `id` query parameter can be repeated multiple times) to enable. IDs follow the same format as the output of `ListPolicies` (`..v/`). For example, a resource policy for `leave_request` with version `default` and scope `acme.hr` would have the ID `resource.leave_request.vdefault/acme.hr`. curl -k -u cerbos:cerbosAdmin -X POST \ 'https://localhost:3592/admin/policy/enable?id=principal.donald_duck.vdefault&id=derived_roles.my_derived_roles' Response ```json { "enabledPolicies": 2 (1) } ``` | **1** | Number of policies enabled | | ----- | -------------------------- | ## [](#%5Fschema%5Fmanagement)Schema Management ### [](#%5Faddupdate%5Fschemas)Add/update schemas POST /admin/schema PUT /admin/schema | | This endpoint requires a mutable storage driver such as [sqlite3](../configuration/storage.html#sqlite3) to be configured. | | ----------------------------------------------------------------------------------------------------------------------------- | Request ```json { "schemas": [ (1) { "id": "principal.json", "definition": "ewogICIkc2NoZW1hIjogImh0dHBzOi8vanNvbi1zY2hlbWEub3JnL2RyYWZ0LzIwMjAtMTIvc2NoZW1hIiwKICAidHlwZSI6ICJvYmplY3QiLAogICJwcm9wZXJ0aWVzIjogewogICAgImRlcGFydG1lbnQiOiB7CiAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICJlbnVtIjogWwogICAgICAgICJtYXJrZXRpbmciLAogICAgICAgICJlbmdpbmVlcmluZyIKICAgICAgXQogICAgfSwKICAgICJnZW9ncmFwaHkiOiB7CiAgICAgICJ0eXBlIjogInN0cmluZyIKICAgIH0sCiAgICAidGVhbSI6IHsKICAgICAgInR5cGUiOiAic3RyaW5nIgogICAgfSwKICAgICJtYW5hZ2VkX2dlb2dyYXBoaWVzIjogewogICAgICAidHlwZSI6ICJzdHJpbmciCiAgICB9LAogICAgIm9yZ0lkIjogewogICAgICAidHlwZSI6ICJzdHJpbmciCiAgICB9LAogICAgImpvYlJvbGVzIjogewogICAgICAidHlwZSI6ICJhcnJheSIsCiAgICAgICJpdGVtcyI6IHsKICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgfQogICAgfSwKICAgICJ0YWdzIjogewogICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAicHJvcGVydGllcyI6IHsKICAgICAgICAiYnJhbmRzIjogewogICAgICAgICAgInR5cGUiOiAiYXJyYXkiLAogICAgICAgICAgIml0ZW1zIjogewogICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgICAgIH0KICAgICAgICB9LAogICAgICAgICJjbGFzc2VzIjogewogICAgICAgICAgInR5cGUiOiAiYXJyYXkiLAogICAgICAgICAgIml0ZW1zIjogewogICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgICAgIH0KICAgICAgICB9LAogICAgICAgICJyZWdpb25zIjogewogICAgICAgICAgInR5cGUiOiAiYXJyYXkiLAogICAgICAgICAgIml0ZW1zIjogewogICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgICAgIH0KICAgICAgICB9CiAgICAgIH0KICAgIH0KICB9LAogICJyZXF1aXJlZCI6IFsKICAgICJkZXBhcnRtZW50IiwKICAgICJnZW9ncmFwaHkiLAogICAgInRlYW0iCiAgXQp9Cg==" (2) } ] } ``` | **1** | List of schema definitions | | ----- | --------------------------------------------------------------- | | **2** | base64 encoded [JSON schema](http://json-schema.org) definition | Response ```json {} ``` ### [](#%5Flist%5Fschemas)List schemas GET /admin/schemas Issue a GET request to the endpoint to list the schemas available in the store. | | Only the schema IDs will be returned from this request. Use the GetSchema endpoint to retrieve the full definition of a schema. | | ---------------------------------------------------------------------------------------------------------------------------------- | ```shell curl -k -u cerbos:cerbosAdmin \ 'https://localhost:3592/admin/schemas' ``` Response ```json { "schemaIds": [ (1) "principal.json", "leave_request.json" ] } ``` | **1** | List of schema ids | | ----- | ------------------ | ### [](#%5Fget%5Fschemas)Get schema(s) GET /admin/schema Issue a GET request to the endpoint to get the schema(s) stated in the query parameters. ```shell curl -k -u cerbos:cerbosAdmin \ 'https://localhost:3592/admin/schema?id=principal.json&id=leave_request.json' ``` Response ```json { "schemas": [ (1) { "id": "principal.json", "definition": "ewogICIkc2NoZW1hIjogImh0dHBzOi8vanNvbi1zY2hlbWEub3JnL2RyYWZ0LzIwMjAtMTIvc2NoZW1hIiwKICAidHlwZSI6ICJvYmplY3QiLAogICJwcm9wZXJ0aWVzIjogewogICAgImRlcGFydG1lbnQiOiB7CiAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICJlbnVtIjogWwogICAgICAgICJtYXJrZXRpbmciLAogICAgICAgICJlbmdpbmVlcmluZyIKICAgICAgXQogICAgfSwKICAgICJnZW9ncmFwaHkiOiB7CiAgICAgICJ0eXBlIjogInN0cmluZyIKICAgIH0sCiAgICAidGVhbSI6IHsKICAgICAgInR5cGUiOiAic3RyaW5nIgogICAgfSwKICAgICJtYW5hZ2VkX2dlb2dyYXBoaWVzIjogewogICAgICAidHlwZSI6ICJzdHJpbmciCiAgICB9LAogICAgIm9yZ0lkIjogewogICAgICAidHlwZSI6ICJzdHJpbmciCiAgICB9LAogICAgImpvYlJvbGVzIjogewogICAgICAidHlwZSI6ICJhcnJheSIsCiAgICAgICJpdGVtcyI6IHsKICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgfQogICAgfSwKICAgICJ0YWdzIjogewogICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAicHJvcGVydGllcyI6IHsKICAgICAgICAiYnJhbmRzIjogewogICAgICAgICAgInR5cGUiOiAiYXJyYXkiLAogICAgICAgICAgIml0ZW1zIjogewogICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgICAgIH0KICAgICAgICB9LAogICAgICAgICJjbGFzc2VzIjogewogICAgICAgICAgInR5cGUiOiAiYXJyYXkiLAogICAgICAgICAgIml0ZW1zIjogewogICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgICAgIH0KICAgICAgICB9LAogICAgICAgICJyZWdpb25zIjogewogICAgICAgICAgInR5cGUiOiAiYXJyYXkiLAogICAgICAgICAgIml0ZW1zIjogewogICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgICAgIH0KICAgICAgICB9CiAgICAgIH0KICAgIH0KICB9LAogICJyZXF1aXJlZCI6IFsKICAgICJkZXBhcnRtZW50IiwKICAgICJnZW9ncmFwaHkiLAogICAgInRlYW0iCiAgXQp9Cg==" }, { "id": "leave_request.json", "definition": "ewogICIkc2NoZW1hIjogImh0dHBzOi8vanNvbi1zY2hlbWEub3JnL2RyYWZ0LzIwMjAtMTIvc2NoZW1hIiwKICAidHlwZSI6ICJvYmplY3QiLAogICJwcm9wZXJ0aWVzIjogewogICAgImRlcGFydG1lbnQiOiB7CiAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICJlbnVtIjogWwogICAgICAgICJtYXJrZXRpbmciLAogICAgICAgICJlbmdpbmVlcmluZyIKICAgICAgXQogICAgfSwKICAgICJnZW9ncmFwaHkiOiB7CiAgICAgICJ0eXBlIjogInN0cmluZyIKICAgIH0sCiAgICAidGVhbSI6IHsKICAgICAgInR5cGUiOiAic3RyaW5nIgogICAgfSwKICAgICJpZCI6IHsKICAgICAgInR5cGUiOiAic3RyaW5nIgogICAgfSwKICAgICJvd25lciI6IHsKICAgICAgInR5cGUiOiAic3RyaW5nIgogICAgfSwKICAgICJzdGF0dXMiOiB7CiAgICAgICJ0eXBlIjogInN0cmluZyIKICAgIH0sCiAgICAiZGV2X3JlY29yZCI6IHsKICAgICAgInR5cGUiOiAiYm9vbGVhbiIKICAgIH0KICB9LAogICJyZXF1aXJlZCI6IFsKICAgICJkZXBhcnRtZW50IiwKICAgICJnZW9ncmFwaHkiLAogICAgInRlYW0iLAogICAgImlkIgogIF0KfQo=" } ] } ``` | **1** | List of schemas | | ----- | --------------- | ### [](#%5Fdelete%5Fschemas)Delete schema(s) DELETE /admin/schema Issue a DELETE request to the endpoint to delete the schema(s) stated in the query parameters. ```shell curl -k -u cerbos:cerbosAdmin -X DELETE \ 'https://localhost:3592/admin/schema?id=principal.json&id=leave_request.json' ``` Response ```json { "deletedSchemas": 2 (1) } ``` | **1** | Number of schemas deleted | | ----- | ------------------------- | ## [](#store-management)Store Management ### [](#%5Fpurge%5Fstore%5Frevisions)Purge store revisions DELETE /admin/store/revisions Issue a DELETE request to the endpoint to delete rows from the `policy_revision` table. Use `keepLast=N` query parameter to keep the last N revisions for each policy. If not specified, the entire table will be truncated. Purge the store revisions ```shell curl -k -u cerbos:cerbosAdmin -X DELETE \ 'https://localhost:3592/admin/store/revisions' ``` Response ```json {} ``` | | This endpoint requires a mutable storage driver such as [sqlite3](../configuration/storage.html#sqlite3) to be configured. | | ----------------------------------------------------------------------------------------------------------------------------- | ### [](#%5Freload%5Fstore)Reload store GET /admin/store/reload Issue a GET request to the endpoint to force a reload of the store. Reload the store ```shell curl -k -u cerbos:cerbosAdmin -X GET \ 'https://localhost:3592/admin/store/reload' ``` Reload the store and block until it finishes ```shell curl -k -u cerbos:cerbosAdmin -X GET \ 'https://localhost:3592/admin/store/reload?wait=true' ``` Response ```json {} ``` | | This endpoint requires a reloadable storage driver such as [blob](../configuration/storage.html#blob), [disk](../configuration/storage.html#disk) and [git](../configuration/storage.html#git) to be configured. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | The Cerbos API ==================== The main API endpoint for making policy decisions is the [/api/check/resources REST endpoint](#check-resources) (`cerbos.svc.v1.CerbosService/CheckResources` RPC in the gRPC API). You can browse a [static version of the Cerbos OpenAPI specification on this site](%5Fattachments/cerbos-api.html). To interactively explore the API, launch a Cerbos PDP instance and access the root directory of the HTTP endpoint using a browser. ```sh docker run --rm --name cerbos -p 3592:3592 -p 3593:3593 ghcr.io/cerbos/cerbos:0.55.0 ``` Navigate to using your browser to explore the Cerbos API documentation. Alternatively, you can explore the API using the following methods as well: * Using an OpenAPI-compatible software like [Postman](https://www.postman.com) or [Insomnia](https://insomnia.rest) to explore the Cerbos OpenAPI spec available at . * Using [grpcurl](https://github.com/fullstorydev/grpcurl) or any other tool that supports [gRPC server reflection](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md) API to explore the gRPC API exposed on port 3593. ## [](#%5Fclient%5Fsdks)Client SDKs * [![Go](_images/go.svg)](https://pkg.go.dev/github.com/cerbos/cerbos-sdk-go/cerbos)[ Go](https://pkg.go.dev/github.com/cerbos/cerbos-sdk-go/cerbos) * [![Java](_images/java.svg)](https://github.com/cerbos/cerbos-sdk-java)[ Java](https://github.com/cerbos/cerbos-sdk-java) * [![JavaScript](_images/javascript.svg)](https://github.com/cerbos/cerbos-sdk-javascript)[ JavaScript](https://github.com/cerbos/cerbos-sdk-javascript) * [![.NET](_images/dot-net.svg)](https://github.com/cerbos/cerbos-sdk-net)[ .NET](https://github.com/cerbos/cerbos-sdk-net) * [![Laravel](_images/laravel.svg)](https://github.com/cerbos/cerbos-sdk-laravel)[ Laravel](https://github.com/cerbos/cerbos-sdk-laravel) * [![PHP](_images/php.svg)](https://github.com/cerbos/cerbos-sdk-php)[ PHP](https://github.com/cerbos/cerbos-sdk-php) * [![Python](_images/python.svg)](https://github.com/cerbos/cerbos-sdk-python)[ Python](https://github.com/cerbos/cerbos-sdk-python) * [![Ruby](_images/ruby.svg)](https://github.com/cerbos/cerbos-sdk-ruby)[ Ruby](https://github.com/cerbos/cerbos-sdk-ruby) * [![Rust](_images/rust.svg)](https://github.com/cerbos/cerbos-sdk-rust)[ Rust](https://github.com/cerbos/cerbos-sdk-rust) Other languages coming soon ## [](#%5Fdemos)Demos | | Demos are constantly being added or updated by the Cerbos team. Visit for the latest list. | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | * [Application (Python)](https://github.com/cerbos/demo-python) * [GraphQL Service (NodeJS)](https://github.com/cerbos/demo-graphql) * [REST Service (Go)](https://github.com/cerbos/demo-rest) Get help * [Join the Cerbos community on Slack](http://go.cerbos.io/slack) * [Email us at ](mailto:help@cerbos.dev)[help@cerbos.dev](mailto:help@cerbos.dev) ## [](#%5Frequest%5Fand%5Fresponse%5Fformats)Request and response formats ### [](#check-resources)`CheckResources` (`/api/check/resources`) This is the main API entrypoint for checking permissions for a set of resources. Request ```json { "requestId": "test", (1) "principal": { "id": "alice", (2) "policyVersion": "20210210", (3) "scope": "acme.corp", (4) "roles": [ (5) "employee" ], "attr": { (6) "department": "accounting", "geography": "GB", "team": "design" } }, "resources": [ (7) { "resource": { "id": "XX125", (8) "kind": "leave_request", (9) "policyVersion": "20210210", (10) "scope": "acme.corp", (11) "attr": { (12) "department": "accounting", "geography": "GB", "id": "XX125", "owner": "john", "team": "design" } }, "actions": [ (13) "view:public", "approve", "create" ] } ], "auxData": { (14) "jwt": { (15) "token": "xxx.yyy.zzz", (16) "keySetId": "ks1" (17) }, "jwts": { (18) "app_token": { (19) "token": "xxx.yyy.zzz", (20) "keySetId": "ks1" (21) }, "gateway_token": { "token": "aaa.bbb.ccc", "keySetId": "ks2" } } }, "includeMeta": true (22) } ``` | **1** | Request ID is an optional, application-provided identifier useful for correlating logs. | | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **2** | ID of the principal whose permissions are being checked. This usually comes from the identity provider (IdP). | | **3** | Principal policy version. Optional. The server falls back to the [configured default version](../configuration/engine.html) if this is not specified. | | **4** | Principal policy scope. Optional. See [Scoped policies](../policies/scoped%5Fpolicies.html). | | **5** | The roles attached to this principal by the identity provider. | | **6** | Free-form context data about this principal. Policy rule conditions are evaluated based on these values. | | **7** | List of resources the principal is attempting to access. Up to 50 resources may be provided in a single request by default. This [limit can be configured](../configuration/server.html#request-limits). | | **8** | ID of the resource. | | **9** | Resource kind. This is used to determine the resource policy that applies to this resource. | | **10** | Resource policy version. Optional. The server falls back to the [configured default version](../configuration/engine.html) if this is not specified. | | **11** | Resource policy scope. Optional. See [Scoped policies](../policies/scoped%5Fpolicies.html). | | **12** | Free-form context data about this resource. Policy rule conditions are evaluated based on these values. | | **13** | List of actions being performed on the resource. Up to 50 actions per resource may be provided by default. This [limit can be configured](../configuration/server.html#request-limits). | | **14** | Optional section for providing auxiliary data. | | **15** | Single JWT to use as an auxiliary data source. Mutually exclusive with jwts. | | **16** | Encoded token string. | | **17** | ID of the keyset to use to verify the JWT. Optional if only a single [keyset is configured](../configuration/auxdata.html). | | **18** | Multiple JWTs to use as auxiliary data. Since Cerbos PDP 0.55.0 only. Mutually exclusive with jwt. | | **19** | Named JWT. | | **20** | Encoded token string. | | **21** | ID of the keyset to use to verify the JWT. Optional if only a single [keyset is configured](../configuration/auxdata.html). | | **22** | Optional flag to receive metadata about request evaluation. | Response ```json { "requestId": "test", (1) "results": [ (2) { "resource": { (3) "id": "XX125", "kind": "leave_request", "policyVersion": "20210210", "scope": "acme.corp" }, "actions": { (4) "view:public": "EFFECT_ALLOW", "approve": "EFFECT_DENY" }, "outputs": [ (5) { "src": "resource.leave_request.v20210210/acme#rule-001", (6) "val": "create_allowed:john" (7) }, { "src": "resource.leave_request.v20210210#public-view", "val": { "id": "john", "keys": ["foo", "bar", "baz"] } } ], "validationErrors": [ (8) { "path": "/department", "message": "value must be one of \"marketing\", \"engineering\"", "source": "SOURCE_PRINCIPAL" }, { "path": "/department", "message": "value must be one of \"marketing\", \"engineering\"", "source": "SOURCE_RESOURCE" } ], "meta": { (9) "actions": { "view:public": { "matchedPolicy": "resource.leave_request.v20210210/acme.corp", (10) "matchedScope": "acme" (11) }, "approve": { "matchedPolicy": "resource.leave_request.v20210210/acme.corp" } }, "effectiveDerivedRoles": [ (12) "employee_that_owns_the_record", "any_employee" ] } } ], "cerbosCallId": "01HHENANTHFD5DV3HZGDKB87PJ" (13) } ``` | **1** | Request ID that was sent with the request. | | ------ | ----------------------------------------------------------------------------------------------------------------------------- | | **2** | List of results. Items are in the same order as they were sent in the request. | | **3** | Resource identifiers. | | **4** | Access decisions for each of the actions. | | **5** | List of outputs from policy evaluation, if there are any. See [Outputs](../policies/outputs.html). | | **6** | Name of the rule that produced the output. | | **7** | Output value produced by the rule. | | **8** | Validation errors, if [schema enforcement](../policies/schemas.html) is enabled and the request didn’t conform to the schema. | | **9** | Metadata (if includeMeta was true in the request) | | **10** | Name of the policy that produced the decision for this action. | | **11** | Name of the scope that was active when the decision was made for the action. | | **12** | List of derived roles that were activated. | | **13** | The call ID generated by Cerbos and stored in the audit log for this request. | ### [](#resources-query-plan)`PlanResources` (`/api/plan/resources`) Produces a query plan that can be used to obtain a list of resources that a principal is allowed to perform a particular action on. Request ```json { "requestId": "test01", (1) "action": "approve", (2) "actions": ["approve", "view"], (3) "resource": { "policyVersion": "dev", (4) "kind": "leave_request", (5) "scope": "acme.corp", (6) "attr": { (7) "owner": "alicia" } }, "principal": { "id": "alicia", (8) "policyVersion": "dev", (9) "scope": "acme.corp", (10) "roles": ["user"], (11) "attr": { (12) "geography": "GB" } }, "includeMeta": true, (13) "auxData": { (14) "jwt": { (15) "token": "xxx.yyy.zzz", (16) "keySetId": "ks1" (17) }, "jwts": { (18) "app_token": { (19) "token": "xxx.yyy.zzz", (20) "keySetId": "ks1" (21) }, "gateway_token": { "token": "aaa.bbb.ccc", "keySetId": "ks2" } } } } ``` | **1** | Request ID can be anything that uniquely identifies a request. | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **2** | Action being performed on the resource instances. Either <2> or <3> is required. | | **3** | Actions being performed on the resource instances. The query plan is the logical AND of individual query plans for each action. Either <2> or <3> is required. | | **4** | Resource policy version. Optional. The server falls back to the [configured default version](../configuration/engine.html) if this is not specified. | | **5** | Resource kind. Required. This value is used to determine the resource policy to evaluate. | | **6** | Resource scope. Optional. See [Scoped policies](../policies/scoped%5Fpolicies.html). | | **7** | Free-form context data about the resources under consideration. The object holds all attributes known about the resource at the time the request. Optional. Policy rule conditions will be (partially) evaluated based on these values. If an effective policy rule condition(s) requires a resource attribute not present in this object, then the response will contain the condition(s) abstract syntax tree. | | **8** | ID of the principal performing the actions. Required. | | **9** | Principal policy version. Optional. The server falls back to the [configured default version](../configuration/engine.html) if this is not specified. | | **10** | Principal scope. Optional. See [Scoped policies](../policies/scoped%5Fpolicies.html). | | **11** | Static roles that are assigned to this principal by your identity management system. Required. | | **12** | Free-form context data about this principal. Policy rule conditions are evaluated based on these values. | | **13** | An optional flag to signal that the response should include metadata about evaluation. Useful for debugging. | | **14** | Optional section for providing auxiliary data. | | **15** | Single JWT to use as an auxiliary data source. Mutually exclusive with jwts. | | **16** | Encoded token string. | | **17** | ID of the keyset to use to verify the JWT. Optional if only a single [keyset is configured](../configuration/auxdata.html). | | **18** | Multiple named JWTs. Since Cerbos 0.55.0 only. Mutually exclusive with jwt. | | **19** | Named JWT. | | **20** | Encoded token string. | | **21** | ID of the keyset to use to verify the JWT. Optional if only a single [keyset is configured](../configuration/auxdata.html). | Response ```json { "requestId": "test01", "action": "approve", "resourceKind": "leave_request", "policyVersion": "dev", "filter": { "kind": "KIND_CONDITIONAL", (1) "condition": { (2) "expression": { "operator": "eq", "operands": [ { "variable": "request.resource.attr.status" }, { "value": "PENDING_APPROVAL" } ] } } }, "meta": { "filterDebug": "(request.resource.attr.status == \"PENDING_APPROVAL\")" (3) }, "cerbosCallId": "01HHENANTHFD5DV3HZGDKB87PJ" (4) } ``` | **1** | Filter kind can be KIND\_ALWAYS\_ALLOWED, KIND\_ALWAYS\_DENIED or KIND\_CONDITIONAL. See below for description of what these values mean. | | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | **2** | Populated only if kind is KIND\_CONDITIONAL. Contains the abstract syntax tree (AST) of the condition that must be satisfied to allow the action. | | **3** | Condition AST represented as a human readable string. Useful for debugging. | | **4** | The call ID generated by Cerbos and stored in the audit log for this request. | #### [](#%5Fstructure%5Fof%5Fthe%5Ffilter%5Fblock)Structure of the `filter` block The `kind` field defines the filter kind. `KIND_ALWAYS_ALLOWED` The principal is unconditionally allowed to perform the action `KIND_ALWAYS_DENIED` The principal is unconditionally not permitted to perfrom the action `KIND_CONDITIONAL` The principal is allowed to perform the action if the condition is satisfied The `condition` field holds the AST of the condition that must be satisfied. It is rooted in an expression that has an `operator` (e.g. equals, greater than) and `operands` (e.g. a constant value, a variable or another expression). __Common Operators__ | Operator | Description | | -------- | -------------------------- | | add | Addition (+) | | and | Logical AND (&&) | | div | Division (/) | | eq | Equality (==) | | ge | Greater than or equal (>=) | | gt | Greater than (>) | | in | List membership (in) | | index | Array or map index | | lambda | Anonymous function | | le | Less than or equal (⇐) | | list | List constructor | | lt | Less than (<) | | mod | Modulo (%) | | mult | Multiplication (\*) | | ne | Not equal (!=) | | not | Logical NOT | | or | Logical OR | | sub | Subtract (-) | Example: `request.resource.attr.status == "PENDING_APPROVAL"` ```json { "expression": { "operator": "eq", "operands": [ { "variable": "request.resource.attr.status" }, { "value": "PENDING_APPROVAL" } ] } } ``` Example: `(request.resource.attr.department == "marketing") && (request.resource.attr.team != "design")` ```json { "expression": { "operator": "and", "operands": [ { "expression": { "operator": "eq", "operands": [ { "variable": "request.resource.attr.department" }, { "value": "marketing" } ] } }, { "expression": { "operator": "ne", "operands": [ { "variable": "request.resource.attr.team" }, { "value": "design" } ] } } ] } } ``` Example: `request.resource.attr.values.filter(t, t > 0)` ```json { "expression": { "operator": "filter", "operands": [ { "variable": "request.resource.attr.values" }, { "expression": { "operator": "lambda", "operands": [ { "variable": "t" }, { "expression": { "operator": "gt", "operands": [ { "variable": "t" }, { "value": 0 } ] } } ] } } ] } } ``` #### [](#%5Fquery%5Fplan%5Fadapters)Query plan adapters Rather than interpreting the condition AST directly, Cerbos provides reference implementations of query plan adapters that convert the `PlanResources` response into a native filter for your data layer. These adapters serve as a starting point — use them as-is or adapt them to your application’s requirements. Reference implementations are available for Prisma, Drizzle, Mongoose, Convex, LangChain/ChromaDB, and SQLAlchemy. See [Query plan adapters](../recipes/query-plan-adapters/index.html) for installation, usage, and operator support details. ### [](#server-info)`ServerInfo` (`/api/server_info`) Returns Cerbos PDP server version. Response ```json { "version": "0.25.0", "commit": "6b5a051a160398a3c04370f742e6090fab2ed0b8", "buildDate": "2023-02-13T09:31:48Z" } ``` ## [](#authzen)Request and response formats (AuthZEN Authorization API) Cerbos PDP partially implements the [OpenID AuthZEN Authorization API specification](https://openid.github.io/authzen/), providing a standardized way to make authorization decisions. The AuthZEN API uses a simplified entity model that maps to Cerbos PDP’s native authorization model. ### [](#authzen-entity-model)AuthZEN entity model mapping The AuthZEN specification uses a simplified entity model with three core entities: * **Subject**: The principal requesting access (maps to Cerbos `principal`) * **Resource**: The target of the access request (maps to Cerbos `resource`) * **Action**: The operation being performed (maps to Cerbos `actions`) #### [](#%5Fmapping%5Fauthzen%5Fto%5Fcerbos)Mapping AuthZEN to Cerbos AuthZEN entities are mapped to Cerbos PDP CheckResources requests as follows: **Subject → Principal** | AuthZEN Field | Cerbos Field | Notes | | --------------------------------------- | ----------------------- | ----------------------------------------------------- | | subject.type | \- | Informational field describing the type of subject | | subject.id | principal.id | Unique identifier for the principal | | subject.properties.cerbos.roles | principal.roles | Array of role names assigned by the identity provider | | subject.properties.cerbos.policyVersion | principal.policyVersion | Policy version to use for the principal | | subject.properties.cerbos.scope | principal.scope | Policy scope for the principal | | subject.properties.\* | principal.attr.\* | All other properties become principal attributes | **Resource → Resource** | AuthZEN Field | Cerbos Field | Notes | | ---------------------------------------- | ---------------------- | -------------------------------------------------- | | resource.type | resource.kind | Type of resource (determines which policy applies) | | resource.id | resource.id | Unique identifier for the resource | | resource.properties.cerbos.policyVersion | resource.policyVersion | Policy version to use for the resource | | resource.properties.cerbos.scope | resource.scope | Policy scope for the resource | | resource.properties.\* | resource.attr.\* | All other properties become resource attributes | **Action → Actions** | AuthZEN Field | Cerbos Field | Notes | | ----------------- | ------------ | -------------------------------------------------------- | | action.name | actions\[0\] | Action name added as the first item in the actions array | | action.properties | \- | Reserved for future use | **Context** | AuthZEN Field | Cerbos Field | Notes | | -------------------------- | ------------ | ------------------------------------------------------------ | | context.cerbos.requestId | requestId | Application-provided correlation identifier | | context.cerbos.auxData | auxData | Auxiliary data such as JWT tokens | | context.cerbos.includeMeta | \- | Controls whether full Cerbos response is included in context | ### [](#authzen-metadata)`Metadata` (`/.well-known/authzen-configuration`) Returns metadata about the Cerbos PDP, including endpoint URLs for accessing the AuthZEN APIs. Request This endpoint accepts GET requests with no body. Response ```json { "policy_decision_point": "https://localhost:3592", (1) "access_evaluation_endpoint": "https://localhost:3592/access/v1/evaluation", (2) "access_evaluations_endpoint": "https://localhost:3592/access/v1/evaluations" (3) } ``` | **1** | Base URL of the Policy Decision Point | | ----- | ------------------------------------------------ | | **2** | Full URL for single access evaluation request | | **3** | Full URL for multiple access evaluation requests | ### [](#authzen-access-evaluation)`Access Evaluation` (`/access/v1/evaluation`) Evaluates whether a subject can perform a single action on a single resource according to the AuthZEN specification. Request ```json { "subject": { (1) "type": "user", (2) "id": "donald_duck", (3) "properties": { (4) "cerbos.policyVersion": "20210210", "cerbos.roles": ["employee"], "department": "marketing", "geography": "GB", "team": "design" } }, "resource": { (5) "type": "leave_request", (6) "id": "XX125", (7) "properties": { (8) "cerbos.policyVersion": "20210210", "department": "marketing", "geography": "GB", "owner": "john", "team": "design" } }, "action": { (9) "name": "view:public", (10) "properties": {} (11) }, "context": { (12) "cerbos.requestId": "test", "cerbos.auxData": { (13) "jwt": { "token": "xxx.yyy.zzz" } }, "cerbos.includeMeta": true (14) } } ``` | **1** | Subject requesting access (maps to Cerbos principal) | | ------ | -------------------------------------------------------------------------------------------------------------------------- | | **2** | Type of the subject (informational field) | | **3** | Unique identifier for the subject | | **4** | Properties about the subject. Special cerbos.\* properties are mapped to Cerbos-specific fields, others become attributes | | **5** | Resource being accessed (maps to Cerbos resource) | | **6** | Type of the resource (maps to Cerbos resource.kind) | | **7** | Unique identifier for the resource | | **8** | Properties about the resource. Special cerbos.\* properties are mapped to Cerbos-specific fields, others become attributes | | **9** | Action being performed | | **10** | Name of the action to check | | **11** | Optional properties about the action (currently unused) | | **12** | Environmental/contextual data | | **13** | Auxiliary data such as JWT tokens. See [CheckResources (/api/check/resources)](#check-resources) for details | | **14** | Include metadata about evaluation in the response (Cerbos extension) | Response ```json { "decision": true, (1) "context": { (2) "cerbos.response": { (3) "requestId": "test", "results": [ { "resource": { "id": "XX125", "kind": "leave_request", "policyVersion": "20210210" }, "actions": { "view:public": "EFFECT_ALLOW" }, "meta": { "actions": { "view:public": { "matchedPolicy": "resource.leave_request.v20210210" } }, "effectiveDerivedRoles": [ "employee_that_owns_the_record" ] } } ], "cerbosCallId": "01HHENANTHFD5DV3HZGDKB87PJ" } } } ``` | **1** | Boolean decision: true for allow, false for deny | | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **2** | Additional context about the evaluation | | **3** | Full Cerbos CheckResources response (included when cerbos.includeMeta is true). See [CheckResources (/api/check/resources)](#check-resources) for response structure details | ### [](#authzen-access-evaluation-batch)`Access Evaluations` (`/access/v1/evaluations`) Evaluates multiple access requests in a single call. This endpoint supports default values that can be overridden for individual evaluations, and evaluation semantics that control execution behavior. Request ```json { "subject": { (1) "type": "user", "id": "donald_duck", "properties": { "cerbos.policyVersion": "20210210", "cerbos.roles": ["employee"], "department": "marketing", "geography": "GB", "team": "design" } }, "resource": { (2) "type": "leave_request", "id": "XX125", "properties": { "cerbos.policyVersion": "20210210", "department": "marketing", "geography": "GB", "owner": "john", "team": "design" } }, "context": { (3) "cerbos.requestId": "test", "cerbos.includeMeta": true }, "evaluations": [ (4) { "action": { (5) "name": "view:public", "properties": {} } }, { "action": { "name": "approve", "properties": {} } }, { "resource": { (6) "type": "leave_request", "id": "XX150", "properties": { "cerbos.policyVersion": "20210210", "department": "marketing", "geography": "GB", "owner": "mary", "team": "design" } }, "action": { "name": "create", "properties": {} } } ], "options": { (7) "evaluations_semantic": "execute_all" (8) } } ``` | **1** | Default subject for all evaluations (can be overridden in individual evaluations) | | ----- | ---------------------------------------------------------------------------------------------------- | | **2** | Default resource for all evaluations (can be overridden in individual evaluations) | | **3** | Default context for all evaluations (can be overridden in individual evaluations) | | **4** | Array of evaluations to perform. Each can override the default subject, resource, action, or context | | **5** | Evaluation using default subject and resource, specifying only the action | | **6** | Evaluation overriding the default resource while using the default subject | | **7** | Optional execution controls | | **8** | Evaluation semantic: execute\_all (default), deny\_on\_first\_deny, or permit\_on\_first\_permit | **Evaluation Semantics:** `execute_all` (default) Process all evaluations regardless of individual results `deny_on_first_deny` Stop processing after the first denial `permit_on_first_permit` Stop processing after the first permit Response ```json { "evaluations": [ (1) { "decision": true, (2) "context": { "cerbos.response": { (3) "requestId": "test", "results": [ { "resource": { "id": "XX125", "kind": "leave_request", "policyVersion": "20210210" }, "actions": { "view:public": "EFFECT_ALLOW" }, "meta": { "actions": { "view:public": { "matchedPolicy": "resource.leave_request.v20210210" } } } } ], "cerbosCallId": "01HHENANTHFD5DV3HZGDKB87PJ" } } }, { "decision": false, "context": { "cerbos.response": { "requestId": "test", "results": [ { "resource": { "id": "XX125", "kind": "leave_request", "policyVersion": "20210210" }, "actions": { "approve": "EFFECT_DENY" } } ], "cerbosCallId": "01HHENANTHFD5DV3HZGDKB87PJ" } } }, { "decision": false, "context": { "cerbos.response": { "requestId": "test", "results": [ { "resource": { "id": "XX150", "kind": "leave_request", "policyVersion": "20210210" }, "actions": { "create": "EFFECT_DENY" } } ], "cerbosCallId": "01HHENANTHFD5DV3HZGDKB87PJ" } } } ] } ``` | **1** | Array of evaluation responses, in the same order as the request | | ----- | ------------------------------------------------------------------------------------------------------ | | **2** | Boolean decision for this evaluation | | **3** | Full Cerbos PDP CheckResources response for this evaluation (included when cerbos.includeMeta is true) | ## [](#%5Faccessing%5Fthe%5Fapi)Accessing the API ### [](#%5Fusing%5Fcurl%5Fto%5Faccess%5Fthe%5Frest%5Fapi)Using curl to access the REST API #### [](#%5Fcerbos%5Fapi%5Fexamples)Cerbos API Examples ```sh cat <. Cerbos gRPC API definitions are published to the [Buf schema registry (BSR)](https://buf.build/cerbos/cerbos-api) and can be easily added to your project if you use the [Buf build system for protobufs](https://docs.buf.build). ### [](#%5Frest)REST There are many tools available to generate clients from an OpenAPI specification. is a good resource for finding a tool suitable for your preferred language. #### [](#%5Fexample%5Fgenerating%5Fa%5Fjava%5Fclient%5Fusing%5Fopenapi%5Fgenerator)Example: Generating a Java client using OpenAPI Generator | | [OpenAPI Generator](https://openapi-generator.tech) has [support for many popular programming languages and frameworks](https://openapi-generator.tech/docs/generators#client-generators). Please consult the documentation to find the client generation instructions for your favourite language. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | This is an example of using the popular [OpenAPI Generator](https://openapi-generator.tech) service to generate a Java client API. * Download the Cerbos OpenAPI specification ```sh curl -Lo swagger.json http://localhost:3592/schema/swagger.json ``` * Run the OpenAPI Generator ```sh docker run --rm -v $(pwd):/oas openapitools/openapi-generator-cli generate -i /oas/swagger.json -g java -o /oas/java ``` ### [](#%5Fgrpc)gRPC **Any language** You can access the Cerbos protobuf definitions from the [Cerbos source tree](https://github.com/cerbos/cerbos/tree/main/api). However, the easiest way to generate client code for your preferred language is to use the [Buf build tool](https://docs.buf.build) to obtain the published API definitions from the [Buf schema registry (BSR)](https://buf.build/cerbos/cerbos-api). * Run `buf export buf.build/cerbos/cerbos-api -o proto` to download the API definitions with dependencies to the `proto` directory. * You can now use [buf generate](https://docs.buf.build/generate-usage) or `protoc` to generate code using the protobufs available in the `proto` directory. | | [BSR generated SDKs](https://buf.build/cerbos/cerbos-api/sdks) feature can be used to download pre-packaged, generated code for supported languages. | | ------------------------------------------------------------------------------------------------------------------------------------------------------- | **Go** The [Cerbos Go SDK](https://pkg.go.dev/github.com/cerbos/cerbos/client) uses the gRPC API to communicate with Cerbos. The generated gRPC and protobuf code is available under the `github.com/cerbos/cerbos/api/genpb` package. ```sh go get github.com/cerbos/cerbos/api/genpb ``` You can also make use [Buf generated SDKs](https://buf.build/cerbos/cerbos-api) to pull down the Cerbos gRPC API as a Go module: ```sh go get buf.build/gen/go/cerbos/cerbos-api/grpc/go@latest ``` API reference ==================== [API reference](%5Fattachments/cerbos-api.html) cerbos ==================== See [Install from binary](../installation/binary.html) or [Run from container](../installation/container.html) for instructions on how to install the `cerbos` binary. This binary provides the following sub commands: `compile` Validate, compile and run tests on a policy repo `compile-store` Validate and compile policies on any Cerbos store `healthcheck` Perform a healthcheck on a Cerbos PDP `repl` An interactive REPL (read-evaluate-print-loop) for CEL conditions `run` Start a PDP and run a command within its context `server` Start the PDP server Example: Running `compile` using the binary ```sh ./cerbos compile --help ``` Example: Running `compile` using the container ```sh docker run -i -t ghcr.io/cerbos/cerbos:0.55.0 compile --help ``` ## [](#compile)`compile` Command Runs the Cerbos compiler to validate policy definitions and run any test suites. See [Policy compilation](../policies/compile.html) for more information. This command exits with different exit codes depending on the kind of failure. | 0 | No compile or test failures | | - | ---------------------------- | | 1 | Unknown failure | | 2 | Invalid arguments to command | | 3 | Compilation failed | | 4 | Tests failed | ```none Usage: cerbos compile [flags] Compile and test policies Examples: # Compile and run tests found in /path/to/policy/repo cerbos compile /path/to/policy/repo # Compile but skip tests cerbos compile --skip-tests /path/to/policy/repo # Compile and run tests matching a filter (globs for suite, test, principal, resource, action) cerbos compile --test-filter='suite=MySuite;test=album*;principal=alice;resource=my_album;action=view' /path/to/policy/repo # Multiple filters can be combined (all filter dimensions are merged) cerbos compile --test-filter='principal=alice,bob' --test-filter='action=view,edit' /path/to/policy/repo Arguments: Policy directory Flags: -h, --help Show context-sensitive help. --version Show cerbos version --ignore-schemas Ignore schemas during compilation --test-filter=TEST-FILTER Filter tests by dimensions (suite, test, principal, resource, action). Format: 'dimension=glob1,glob2;...'. Can be specified multiple times. --skip-tests Skip tests --skip-batching Skip batching tests --strict-evaluation Run tests with strict evaluation enabled. --test-output=TEST-OUTPUT Test output format. If unspecified matches the value of the output flag. (tree,list,json,junit) -o, --output="tree" Output format (tree,list,json) --color=COLOR Output color level (auto,never,always,256,16m). Defaults to auto. --no-color Disable colored output --verbose Verbose output on test failure ``` ## [](#compile-store)`compile-store` Command Runs the Cerbos compiler to validate policy definitions on any Cerbos store. See [Policy compilation](../policies/compile.html) and [Storage](../configuration/storage.html) for more information. This command exits with different exit codes depending on the kind of failure. | 0 | No compile or test failures | | - | ---------------------------- | | 1 | Unknown failure | | 2 | Invalid arguments to command | | 3 | Compilation failed | | 4 | Tests failed | ```none Usage: cerbos compile-store [flags] Compile store Examples: # Compile store cerbos compile-store /path/to/cerbos/config.yaml cerbos compile-store --output=json /path/to/cerbos/config.yaml # Compile database store and disable invalid policies cerbos compile-store --disable-invalid /path/to/cerbos/config.yaml cerbos compile-store --disable-invalid --output=json /path/to/cerbos/config.yaml Arguments: Path to config file ($CERBOS_CONFIG) Flags: -h, --help Show context-sensitive help. --version Show cerbos version --set=server.adminAPI.enabled=true,... Config overrides --disable-invalid Disable invalid policies if database store --assume-yes Answer yes to all confirmation questions -o, --output="tree" Output format (tree,list,json) --color=COLOR Output color level (auto,never,always,256,16m). Defaults to auto. --no-color Disable colored output ``` ## [](#healthcheck)`healthcheck` Command Utility to perform healthchecks on a Cerbos PDP. Can be used as a [Docker HEALTHCHECK](https://docs.docker.com/engine/reference/builder/#healthcheck) command. You can share the configuration between Cerbos PDP and the healthcheck by using the `CERBOS_CONFIG` environment variable to define the path to the config file. Example: Docker healthcheck based on mounted config file ```sh docker run -i -t -p 3592:3592 -p 3593:3593 \ -v /path/to/conf/dir:/config \ -e CERBOS_CONFIG=/config/.cerbos.yaml \ ghcr.io/cerbos/cerbos:0.55.0 ``` ```none Usage: cerbos healthcheck (hc) Healthcheck utility Performs a healthcheck on a Cerbos PDP. This can be used as a Docker HEALTHCHECK command. When the path to the Cerbos config file is provided via the '--config' flag or the CERBOS_CONFIG environment variable, the healthcheck will be automatically configured based on the settings from the file. By default, the gRPC endpoint will be checked using the gRPC healthcheck protocol. This is usually sufficient for most cases as the Cerbos REST API is built on top of the gRPC API as well. Examples: # Check gRPC endpoint cerbos healthcheck --config=/path/to/.cerbos.yaml # Check HTTP endpoint and ignore server certificate verification cerbos healthcheck --config=/path/to/.cerbos.yaml --kind=http --insecure # Check the HTTP endpoint of a specific host with no TLS. cerbos healthcheck --kind=http --host-port=10.0.1.5:3592 --no-tls Flags: -h, --help Show context-sensitive help. --version Show cerbos version --kind="grpc" Healthcheck kind (grpc,http) ($CERBOS_HC_KIND) --insecure Do not verify server certificate ($CERBOS_HC_INSECURE) --timeout=10s Healthcheck timeout ($CERBOS_HC_TIMEOUT) config --config=STRING Cerbos config file ($CERBOS_CONFIG) manual --host-port=STRING Host and port to connect to ($CERBOS_HC_HOSTPORT) --ca-cert=STRING Path to CA cert for validating server cert ($CERBOS_HC_CACERT) --no-tls Don't use TLS ($CERBOS_HC_NOTLS) ``` ## [](#repl)`repl` Command The REPL is an interactive utility to experiment with [CEL conditions](../policies/conditions.html) used in Cerbos policy rules. All Cerbos library functions and special variables (`request`, `R`, `P` and so on) are available in this environment. Example: Running the REPL using the binary ```sh ./cerbos repl ``` Example: Running the REPL using the container ```sh docker run -i -t ghcr.io/cerbos/cerbos:0.55.0 repl ``` You can type in valid CEL expressions at the prompt to instantly evaluate them. -> 5 + 1 _ = 6 -> "test".charAt(1) _ = "e" The special variable `_` holds the result of the last expression evaluated. -> 5 + 5 _ = 10 -> _ * 10 _ = 100 You can define variables using the `:let` directive. -> :let x = hierarchy("a.b.c") x = [ "a", "b", "c" ] -> :let y = hierarchy("a.b") y = [ "a", "b" ] -> x.immediateChildOf(y) _ = true You can also set special variables used in Cerbos policies (`request`, `variables`, `R`, `P`, `V`) and try out CEL expressions using them. -> :let request = { > "principal":{"id":"john","roles":["employee"],"attr":{"scope":"foo.bar.baz.qux"}}, > "resource":{"id":"x1","kind":"leave_request","attr":{"scope":"foo.bar"}} > } -> hierarchy(R.attr.scope).ancestorOf(hierarchy(P.attr.scope)) _ = true Type `:vars` to display the values of all the variables currently defined in the environment. You can load a Cerbos policy into the REPL by typing `:load path/to/policy_file.yaml`. This will read the policy and load any rules that have conditions attached. These can be viewed by typing `:rules`. Execute any rule by providing its number to the `:exec` directive (for example, `:exec #2`). Rules are executed using the variables defined in the current REPL session. You can set or update them using the `:let` directive and re-execute the rules to see the effects. -> :load store/leave_request.yaml Loaded resource.leave_request.v20210210 Policy variables: { "pending_approval": "(\"PENDING_APPROVAL\")", "principal_location": "(P.attr.ip_address.inIPAddrRange(\"10.20.0.0/16\") ? \"GB\" : \"\")" } Conditional rules in 'resource.leave_request.v20210210' [#0] actions: - approve condition: match: expr: request.resource.attr.status == V.pending_approval derivedRoles: - direct_manager effect: EFFECT_ALLOW -> :exec #0 └──request.resource.attr.status == V.pending_approval [false] Type `:help` or `:h` to display help. Type `:quit`, `:q` or `:exit` to exit the REPL. | | Use the up/down arrow keys (or Ctrl+P/Ctrl+N) to navigate command history. Most of the standard line-editing commands such as Ctrl+a, Ctrl+h, Ctrl+r are supported as well. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ## [](#run)`run` Command This provides a quick way to try out Cerbos. It launches a Cerbos PDP instance and then invokes a command of your choice that can then use the PDP to make access decisions. A good use case for this command is as an integration test runner. If you have written some tests that make use of Cerbos, you can run them within the context of an actual PDP instance as follows: ```sh cerbos run -- python -m unittest ``` By default, the policies are loaded from the `policies` directory in the current working directory and HTTP and gRPC endpoints will be exposed on `127.0.0.1:3592` and `127.0.0.1:3593` respectively. Your application can obtain the actual endpoint addresses by inspecting the `CERBOS_HTTP` or `CERBOS_GRPC` environment variables. If a file named `.cerbos.yaml` exists in the current working directory, that file will be used as the Cerbos [configuration file](../configuration/index.html). You can use a different config file or override specific config values using the same flags as the `server` command above. ```none Usage: cerbos run ... Run a command in the context of a Cerbos PDP Launches a command within the context of a Cerbos PDP. The policies are loaded by default from a directory named "policies" in the current working directory. The launched application can access Cerbos endpoints using the values from CERBOS_HTTP or CERBOS_GRPC environment variables. If a file named ".cerbos.yaml" exists in the current working directory, it will be used as the configuration file for the PDP. You can override the config file and/or other configuration options using the flags described below. Examples: # Launch Go tests within a Cerbos context cerbos run -- go test ./... # Start Cerbos with a custom configuration file and run Python tests within the context cerbos run --config=myconf.yaml -- python -m unittest # Silence Cerbos log output cerbos run --log-level=error -- curl -I http://127.0.0.1:3592/_cerbos/health Arguments: ... Command to run Flags: -h, --help Show context-sensitive help. --version Show cerbos version --log-level="info" Log level (debug,info,warn,error) --config=.cerbos.yaml Path to config file --set=server.adminAPI.enabled=true,... Config overrides --timeout=30s Cerbos startup timeout ``` ## [](#server)`server` Command Starts the Cerbos PDP. ```none Usage: cerbos server --config=.cerbos.yaml Start Cerbos server (PDP) Examples: # Start the server cerbos server --config=/path/to/.cerbos.yaml # Start the server with the Admin API enabled and the 'sqlite' storage driver cerbos server --config=/path/to/.cerbos.yaml --set=server.adminAPI.enabled=true --set=storage.driver=sqlite3 --set=storage.sqlite3.dsn=':memory:' Flags: -h, --help Show context-sensitive help. --version Show cerbos version --debug-listen-addr=:6666 Address to start the gops listener --log-level="info" Log level (debug,info,warn,error) --config=.cerbos.yaml Path to config file --set=server.adminAPI.enabled=true,... Config overrides ``` cerbosctl ==================== This utility can be downloaded as a separate container, tar archive, or [npm package](https://www.npmjs.com/package/cerbosctl). It is automatically installed when installing Cerbos through [Linux packages or the Homebrew tap](../installation/binary.html#linux-packages). Run from the container ```sh docker run -it ghcr.io/cerbos/cerbosctl:0.55.0 \ --server=192.168.1.10:3593 \ --username=user \ --password=password \ get rp ``` __Download and run the appropriate binary from __ | OS | Arch | Bundle | | ----- | --------- | ----------------------------------------- | | Linux | x86-64 | cerbosctl\_0.55.0\_Linux\_x86\_64.tar.gz | | Linux | arm64 | cerbosctl\_0.55.0\_Linux\_arm64.tar.gz | | MacOS | universal | cerbosctl\_0.55.0\_Darwin\_all.tar.gz | | MacOS | x86-64 | cerbosctl\_0.55.0\_Darwin\_x86\_64.tar.gz | | MacOS | arm64 | cerbosctl\_0.55.0\_Darwin\_arm64.tar.gz | Cerbosctl requires the [Admin API to be enabled](../configuration/server.html#admin-api) on the Cerbos server. The server address to connect to and the credentials to authenticate can be provided through environment variables or as arguments to the command. ```none Usage: cerbosctl A CLI for managing Cerbos The Cerbos Admin API must be enabled in order for these commands to work. The Admin API requires credentials. They can be provided using a netrc file, environment variables or command-line arguments. Environment variables - CERBOS_SERVER: gRPC address of the Cerbos server - CERBOS_USERNAME: Admin username - CERBOS_PASSWORD: Admin password When more than one method is used to provide credentials, the precedence from lowest to highest is: netrc < environment < command line. Examples # Connect to a TLS enabled server while skipping certificate verification and launch the decisions viewer cerbosctl --server=localhost:3593 --username=user --password=password --insecure decisions # Connect to a non-TLS server and launch the decisions viewer cerbosctl --server=localhost:3593 --username=user --password=password --plaintext decisions Flags: -h, --help Show context-sensitive help. --server="localhost:3593" Address of the Cerbos server ($CERBOS_SERVER) --username=STRING Admin username ($CERBOS_USERNAME) --password=STRING Admin password ($CERBOS_PASSWORD) --ca-cert=STRING Path to the CA certificate for verifying server identity --client-cert=STRING Path to the TLS client certificate --client-key=STRING Path to the TLS client key --insecure Skip validating server certificate --plaintext Use plaintext protocol without TLS Commands: get derived_roles (derived_role,dr) [ ...] get export_constants (ec) [ ...] get export_variables (ev) [ ...] get principal_policies (principal_policy,pp) [ ...] get resource_policies (resource_policy,rp) [ ...] get role_policies (role_policy,rlp) [ ...] get schemas (schema,s) [ ...] inspect policies (p) [ ...] Inspect policies in the store store export (e) store reload (r) delete policy (policies,p) ... delete schema (schemas,s) ... disable policy (policies,p) ... enable policy (policies,p) ... put policy (policies,p) ... put schema (schemas,s) ... decisions Interactive decision log viewer audit View audit logs version Show cerbosctl and PDP version Run "cerbosctl --help" for more information on a command. ``` ## [](#audit)`audit` This command allows you to view the audit logs captured by the Cerbos server. [Audit logging](../configuration/audit.html) must be enabled on the server to obtain the data through this command. Filters tail Get the last N records (e.g. `--tail=10`) between Get records between two ISO-8601 timestamps. If the last timestamp is left out, get records from the first timestamp up to now. * `--between=2021-07-01T00:00:00Z,2021-07-02T00:00:00Z`: From midnight of 2021-07-01 to midnight of 2021-07-02. * `--between=2021-07-01T00:00:00Z`: From midnight of 2021-07-01 to now. since Get records from N hours/minutes/second ago to now. (e.g. `--since=3h`) lookup Get a specific record by ID. (e.g. `--lookup=01F9Y5MFYTX7Y87A30CTJ2FB0S`) View the last 10 access logs ```sh cerbosctl audit --kind=access --tail=10 ``` View the decision logs from midnight 2021-07-01 to midnight 2021-07-02 ```sh cerbosctl audit --kind=decision --between=2021-07-01T00:00:00Z,2021-07-02T00:00:00Z ``` View the decision logs from midnight 2021-07-01 to now ```sh cerbosctl audit --kind=decision --between=2021-07-01T00:00:00Z ``` View the access logs from 3 hours ago to now as newline-delimited JSON ```sh cerbosctl audit --kind=access --since=3h --raw ``` View a specific access log entry by call ID ```sh cerbosctl audit --kind=access --lookup=01F9Y5MFYTX7Y87A30CTJ2FB0S ``` ## [](#decisions)`decisions` This command starts an interactive text user interface to view and analyze the decision records captured by the Cerbos server. It accepts the same [filter flags](#audit-filters) as the `audit` command. ![Decisions](_images/decisions-tui.png) * tab Switch focus to different panes in the UI * esc Close window (or exit if you are in the main screen) * q Exit Use the arrow keys (or Vim keys h, j, k, l) to scroll horizontally or vertically. Press enter to select/open an item. Start analyzing the last 20 decision records ```sh cerbosctl decisions --tail=20 ``` ## [](#delete)`delete` This command deletes the polices and the schemas with the specified ids. Delete policies cerbosctl delete policies derived_roles.my_derived_roles cerbosctl delete policy derived_roles.my_derived_roles cerbosctl delete p derived_roles.my_derived_roles Delete multiple policies cerbosctl delete policies derived_roles.my_derived_roles resource.leave_request.default cerbosctl delete policy derived_roles.my_derived_roles resource.leave_request.default cerbosctl delete p derived_roles.my_derived_roles resource.leave_request.default Delete schemas cerbosctl delete schemas principal.json cerbosctl delete schema principal.json cerbosctl delete s principal.json Delete multiple schemas cerbosctl delete schemas principal.json leave_request.json cerbosctl delete schema principal.json leave_request.json cerbosctl delete s principal.json leave_request.json ## [](#disable)`disable` This command disables the policies with the specified ids. Disable policies cerbosctl disable policies derived_roles.my_derived_roles cerbosctl disable policy derived_roles.my_derived_roles cerbosctl disable p derived_roles.my_derived_roles Disable multiple policies cerbosctl disable policies derived_roles.my_derived_roles resource.leave_request.default cerbosctl disable policy derived_roles.my_derived_roles resource.leave_request.default cerbosctl disable p derived_roles.my_derived_roles resource.leave_request.default | | Scoped policies must have unbroken scope chains. If you’re disabling a scoped policy, make sure that its descendant policies are disabled as well. | | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#enable)`enable` This command enables the policies with the specified ids. Enable policies cerbosctl enable policies derived_roles.my_derived_roles cerbosctl enable policy derived_roles.my_derived_roles cerbosctl enable p derived_roles.my_derived_roles Enable multiple policies cerbosctl enable policies derived_roles.my_derived_roles resource.leave_request.default cerbosctl enable policy derived_roles.my_derived_roles resource.leave_request.default cerbosctl enable p derived_roles.my_derived_roles resource.leave_request.default ## [](#get)`get` This command lists the policies available in the configured policy repository. You can also retrieve individual policies or schemas by their identifiers and view their definitions as YAML or JSON. You can filter the output using the `name` and `version` flags. Each flag accepts multiple comma-separated values which are OR’ed together. For example, `--name=a.yaml,b.yaml` matches policies that are either named `a.yaml` or `b.yaml`. Separately, you can filter the output using the `name-regexp`, `version-regexp` and `scope-regexp` flags. Each flag accepts a regular expression string. These are separate from the `name` and `version` flags above, and cannot be used with their respective counterparts. You can include disabled policies in the results by adding `--include-disabled` flag. You can optionally restrict the list to a specific set of policies by supplying their IDs as trailing arguments. List derived roles cerbosctl get derived_roles cerbosctl get derived_role cerbosctl get dr List principal policies cerbosctl get principal_policies cerbosctl get principal_policy cerbosctl get pp List resource policies cerbosctl get resource_policies cerbosctl get resource_policy cerbosctl get rp List derived\_roles where `name` is `my_policy` or `a_policy` cerbosctl get derived_roles --name my_policy,a_policy cerbosctl get dr --name my_policy,a_policy List derived\_roles where `name` is `my_policy` or `a_policy`, using regular expression cerbosctl get derived_roles --name-regexp "^(my|a)_policy\$" cerbosctl get dr --name-regexp "^(my|a)_policy\$" Get derived roles policies having the ID `common_roles.yaml` and `other_roles.yaml` cerbosctl get derived_roles common_roles.yaml other_roles.yaml cerbosctl get dr common_roles.yaml other_roles.yaml List principal\_policies where `version` is `default` or `v1` cerbosctl get principal_policies --version default,v1 cerbosctl get pp --version default,v1 List principal\_policies where `version` is `default` or `v1`, using regular expression cerbosctl get principal_policies --version-regexp "(default|v1)" cerbosctl get pp --version-regexp "(default|v1)" Get principal\_policies having the ID `alex.yaml` and `john.yaml` cerbosctl get principal_policies alex.yaml john.yaml cerbosctl get pp alex.yaml john.yaml List resource\_policies where `scope` includes the substring `foo`, using regular expression cerbosctl get resource_policies --scope-regexp foo cerbosctl get rp --scope-regexp foo Get resource\_policies having the ID `leave_request.yaml` and `purchase_order.yaml` cerbosctl get resource_policies leave_request.yaml purchase_order.yaml cerbosctl get rp leave_request.yaml purchase_order.yaml List derived\_roles and sort by column `policyId` or `name` cerbosctl get derived_roles --sort-by policyId cerbosctl get dr --sort-by policyId cerbosctl get derived_roles --sort-by name cerbosctl get dr --sort-by name List principal\_policies and sort by column `policyId`, `name` or `version` cerbosctl get principal_policies --sort-by policyId cerbosctl get pp --sort-by policyId cerbosctl get principal_policies --sort-by name cerbosctl get pp --sort-by name cerbosctl get principal_policies --sort-by version cerbosctl get pp --sort-by version List resource\_policies and sort by column `policyId`, `name` or `version` cerbosctl get resource_policies --sort-by policyId cerbosctl get rp --sort-by policyId cerbosctl get resource_policies --sort-by name cerbosctl get rp --sort-by name cerbosctl get resource_policies --sort-by version cerbosctl get rp --sort-by version Get JSON cerbosctl get derived_roles my_derived_roles --output=json Get YAML cerbosctl get derived_roles my_derived_roles --output=yaml ## [](#hub)`hub` Operations related to Cerbos Hub. ## [](#inspect-policies)`inspect policies` This command is to inspect policies in the store. Currently, it lists actions and variables defined in the policies. Inspect policies cerbosctl inspect policies ## [](#put)`put` This command puts the given policies or schemas to the configured policy repository. Put policies cerbosctl put policies ./path/to/policy.yaml cerbosctl put policy ./path/to/policy.yaml cerbosctl put p ./path/to/policy.yaml Put multiple policies cerbosctl put policy ./path/to/policy.yaml ./path/to/other/policy.yaml Put policies under a directory cerbosctl put policy ./dir/to/policies ./other/dir/to/policies Put policies under a directory recursively cerbosctl put policy --recursive ./dir/to/policies cerbosctl put policy -R ./dir/to/policies Put policies from a zip file cerbosctl put policy ./dir/to/policies.zip Put schemas cerbosctl put schemas ./path/to/schema.json cerbosctl put schema ./path/to/schema.json cerbosctl put s ./path/to/schema.json Put multiple schemas cerbosctl put schema ./path/to/schema.json ./path/to/other/schema.json Put schemas under a directory cerbosctl put schema ./dir/to/schemas ./other/dir/to/schemas Put schemas under a directory recursively cerbosctl put schema --recursive ./dir/to/schemas cerbosctl put schema -R ./dir/to/schemas Put schemas from a zip file cerbosctl put schema ./dir/to/schemas.zip ## [](#store)`store` Trigger operations on the policy store of the PDP ### [](#export)`export` Exports the policies and schemas from the store into a directory. Export policies and schemas from the store into a directory cerbosctl store export path/to/dir Export policies and schemas from the store into a zip archive cerbosctl store export path/to/archive.zip Export policies and schemas from the store into a gzip archive cerbosctl store export path/to/archive.gzip cerbosctl store export path/to/archive.tar.gz ### [](#reload)`reload` Reloads the store. Reload the store cerbosctl store reload Reload the store and wait until it finishes cerbosctl store reload --wait` ### [](#revisions)`revisions` Trigger operations on the policy store revisions #### [](#purge)`purge` Deletes revisions from the `policy_revision` table Purge all revisions cerbosctl store revisions purge Purge all revisions but keep last 2 revisions of each policy. cerbosctl store revisions purge --keep-last=2 Cerbos CLI ==================== Every [Cerbos release](https://github.com/cerbos/cerbos/releases/tag/v0.55.0) ships with two binaries: [cerbos](cerbos.html) The Cerbos PDP server and the compiler/test runner [cerbosctl](cerbosctl.html) Command line utility to interact with Cerbos PDP instances that have the [Admin API enabled](../configuration/server.html#admin-api) Audit block ==================== The `audit` block configures the audit logging settings for the Cerbos instance. Audit logs capture access records and decisions made by the engine along with the associated context data. Cerbos API responses include a `cerbosCallId` field that contains the unique identifier under which the request was logged to the audit log (if enabled) and the Cerbos activity log. It is recommended that applications log this ID as part of their activity logs too so that those log entries can be joined together with Cerbos logs during log analysis to build a complete picture of the authorization decisions. | | Audit logging has some overhead in terms of resource usage (disk IO, CPU and memory). This overhead is usually negligible unless Cerbos is running in a resource-constrained environment. If resources are scarce or if you are expecting heavy traffic, disabling audit logging might have a positive impact on performance. | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ```yaml audit: accessLogsEnabled: false # AccessLogsEnabled defines whether access logging is enabled. backend: local # Backend states which backend to use for Audits. decisionLogFilters: # DecisionLogFilters define the filters to apply while producing decision logs. checkResources: # CheckResources defines the filters that apply to CheckResources calls. ignoreAllowAll: false # IgnoreAllowAll ignores responses that don't contain an EFFECT_DENY. planResources: # PlanResources defines the filters that apply to PlanResources calls. ignoreAll: false # IgnoreAll prevents any plan responses from being logged. Takes precedence over other filters. ignoreAlwaysAllow: false # IgnoreAlwaysAllow ignores ALWAYS_ALLOWED plans. decisionLogsEnabled: false # DecisionLogsEnabled defines whether logging of policy decisions is enabled. enabled: false # Enabled defines whether audit logging is enabled. excludeMetadataKeys: ['authorization'] # ExcludeMetadataKeys defines which gRPC request metadata keys should be excluded from the audit logs. Takes precedence over includeMetadataKeys. includeMetadataKeys: ['content-type'] # IncludeMetadataKeys defines which gRPC request metadata keys should be included in the audit logs. file: additionalPaths: [stdout] # AdditionalPaths to mirror the log output. Has performance implications. Use with caution. logRotation: # LogRotation settings (optional). maxFileAgeDays: 10 # MaxFileAgeDays sets the maximum age in days of old log files before they are deleted. maxFileCount: 10 # MaxFileCount sets the maximum number of files to retain. maxFileSizeMB: 100 # MaxFileSizeMB sets the maximum size of individual log files in megabytes. path: /path/to/file.log # Required. Path to the log file to use as output. The special values stdout and stderr can be used to write to stdout or stderr respectively. hub: advanced: bufferSize: 256 flushInterval: 1s gcInterval: 60s maxBatchSize: 32 mask: # Mask defines a list of attributes to exclude from the audit logs, specified as lists of JSONPaths checkResources: - inputs[*].principal.attr.foo - inputs[*].auxData - outputs metadata: ['authorization'] peer: - address - forwarded_for planResources: ['input.principal.attr.nestedMap.foo'] retentionPeriod: 168h # How long to keep records for storagePath: /path/to/dir # Path to store the data pipeOutput: # Secondary backend to send logs to. backend: file enabled: false kafka: ack: all # Ack mode for producing messages. Valid values are "none", "leader" or "all" (default). Idempotency is disabled when mode is not "all". authentication: # Authentication tls: caPath: /path/to/ca.crt # Required. CAPath is the path to the CA certificate. certPath: /path/to/tls.cert # CertPath is the path to the client certificate. insecureSkipVerify: true # InsecureSkipVerify controls whether the server's certificate chain and host name are verified. Default is false. keyPath: /path/to/tls.key # KeyPath is the path to the client key. reloadInterval: 5m # ReloadInterval is the interval at which the TLS certificates are reloaded. The default is 0 (no reload). brokers: ['localhost:9092'] # Required. Brokers list to seed the Kafka client. clientID: cerbos # ClientID reported in Kafka connections. closeTimeout: 30s # CloseTimeout sets how long when closing the client to wait for any remaining messages to be flushed. compression: ['snappy'] # Compression sets the compression algorithm to use in order of priority. Valid values are "none", "gzip", "snappy","lz4", "zstd". Default is ["snappy", "none"]. encoding: json # Encoding format. Valid values are "json" (default) or "protobuf". maxBufferedRecords: 1000 # MaxBufferedRecords sets the maximum number of records the client should buffer in memory in async mode. produceSync: false # ProduceSync forces the client to produce messages to Kafka synchronously. This can have a significant impact on performance. topic: cerbos.audit.log # Required. Topic to write audit entries to. local: advanced: bufferSize: 256 flushInterval: 1s gcInterval: 60s maxBatchSize: 32 retentionPeriod: 168h # How long to keep records for storagePath: /path/to/dir # Path to store the data ``` Including or excluding request metadata in log entries To tune how request metadata (headers) is logged to access and decision log entries, configure `includeMetadataKeys` and `excludeMetadataKeys` as follows: * Both `includeMetadataKeys` and `excludeMetadataKeys` are empty: no metadata will be logged * Only `includeMetadataKeys` is defined: only the metadata keys in the list will be logged * Only `excludeMetadataKeys` is defined: everything except the keys defined in the list will be logged * Both `includeMetadataKeys` and `excludeMetadataKeys` are defined: Only the keys in the include list will be logged if, and only if, they are not in the exclude list | | If requests contain sensitive data such as authorization tokens, they will be captured by the audit logs and visible to anyone with access to the log files. Cerbos automatically excludes the authorization header. However, if you use other header keys to store sensitive data, always exclude them using the excludeMetadataKeys configuration setting. | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#file)File backend The `file` backend writes audit records as newline-delimited JSON to a file or stdout/stderr. With this backend you can use your existing log aggregation system (Datadog agent, Elastic agent, Fluentd, Graylog — to name a few) to collect, process and archive the audit data from all Cerbos instances. | | This backend cannot be queried using the Admin API, cerbosctl audit or cerbosctl decisions. | | ---------------------------------------------------------------------------------------------- | Minimal configuration with file output and no log rotation ```yaml audit: enabled: true accessLogsEnabled: true decisionLogsEnabled: true backend: file file: path: /path/to/audit.log ``` Configuration with log rotation and output to both stdout and a file ```yaml audit: enabled: true accessLogsEnabled: true decisionLogsEnabled: true backend: file file: path: /path/to/file.log additionalPaths: - stdout logRotation: maxFileAgeDays: 10 # Maximum age in days of old log files before they are deleted. maxFileCount: 10 # Maximum number of old log files to retain. maxFileSizeMB: 100 # Maximum size of individual log files in megabytes. ``` The `path` field can be set to special names `stdout` or `stderr` to log to stdout or stderr. Note that this would result in audit logs being mixed up with normal Cerbos operational logs. It is recommended to use an actual file for audit log output if your container orchestrator has support for collecting logs from files in addition to stdout/stderr. Audit log entries can be selected by setting a filter on `log.logger == "cerbos.audit"`. Access log entries have `log.kind == "access"` and decision log entries have `log.kind == "decision"`. If log rotation is enabled, `maxFileSizeMB` is the only required setting. If `maxFileCount` and `maxFileAgeDays` settings are not defined, files are never deleted by the Cerbos process. ## [](#hub)Hub backend | | Requires a [Cerbos Hub](https://www.cerbos.dev/product-cerbos-hub) account. [![Try Cerbos Hub](../_images/try_cerbos_hub.png)](https://hub.cerbos.cloud) | | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | Securely sends audit logs to Cerbos Hub for aggregation and analysis. This vastly simplifies the work that would otherwise be required to configure and deploy a log aggregation solution to securely collect, store and query audit logs from across your fleet. If you are new to Cerbos Hub, follow the [getting started guide](../../../cerbos-hub/getting-started.html). For more information about configuring the PDP to send audit logs to Cerbos Hub, refer to the [audit log collection documentation](../../../cerbos-hub/audit-log-collection.html). ## [](#kafka)Kafka backend The `kafka` backend writes audit records to a Kafka topic. By default, the messages are published asynchronously to the specified topic in JSON format. The message header named `cerbos.audit.kind` would have the value `access` for access log entries and `decision` for decision log entries. You can configure the audit logger to produce data in the Protocol Buffers binary encoding format as well. The schema for messages is available at . Minimal configuration ```yaml audit: enabled: true accessLogsEnabled: true decisionLogsEnabled: true backend: kafka kafka: brokers: ['broker1.kafka:9092', 'broker2.kafka:9092'] topic: cerbos.audit.log ``` Full configuration ```yaml audit: enabled: true accessLogsEnabled: true decisionLogsEnabled: true backend: kafka kafka: ack: all # Ack mode for producing messages. Valid values are "none", "leader" or "all" (default). Idempotency is disabled when mode is not "all". authentication: # Authentication tls: caPath: /path/to/ca.crt # Required. CAPath is the path to the CA certificate. certPath: /path/to/tls.cert # CertPath is the path to the client certificate. insecureSkipVerify: true # InsecureSkipVerify controls whether the server's certificate chain and host name are verified. Default is false. keyPath: /path/to/tls.key # KeyPath is the path to the client key. reloadInterval: 5m # ReloadInterval is the interval at which the TLS certificates are reloaded. The default is 0 (no reload). brokers: ['localhost:9092'] # Required. Brokers list to seed the Kafka client. clientID: cerbos # ClientID reported in Kafka connections. closeTimeout: 30s # CloseTimeout sets how long when closing the client to wait for any remaining messages to be flushed. encoding: json # Encoding format. Valid values are "json" (default) or "protobuf". maxBufferedRecords: 1000 # MaxBufferedRecords sets the maximum number of records the client should buffer in memory in async mode. produceSync: false # ProduceSync forces the client to produce messages to Kafka synchronously. This can have a significant impact on performance. topic: cerbos.audit.log # Required. Topic to write audit entries to. compression: ['snappy'] # Compression sets the compression algorithm to use in order of priority. Valid values are "none", "gzip", "snappy","lz4", "zstd". Default is ["snappy", "none"]. ``` ## [](#local)Local backend The `local` backend uses an embedded key-value store to save audit records. Records are preserved for seven days by default and can be queried using the [Admin API](../api/admin%5Fapi.html), the [cerbosctl audit](../cli/cerbosctl.html#audit) command or the [cerbosctl decisions](../cli/cerbosctl.html#decisions) text interface (TUI). The only required setting for the `local` backend is the `storagePath` field which specifies the path on disk where the logs should be stored. ```yaml audit: enabled: true accessLogsEnabled: true decisionLogsEnabled: true backend: local local: storagePath: /path/to/dir retentionPeriod: 168h advanced: bufferSize: 16 # Size of the memory buffer. Increasing this will use more memory and the chances of losing data during a crash. maxBatchSize: 16 # Write batch size. If your records are small, increasing this will reduce disk IO. flushInterval: 30s # Time to keep records in memory before committing. gcInterval: 15m # How often the garbage collector runs to remove old entries from the log. ``` | | [Cerbos Hub](https://docs.cerbos.dev/cerbos-hub/) aggregates decision logs from connected PDPs into a single searchable store, with sensitive fields filtered locally before transmission. See [Cerbos Hub audit log collection documentation for more information](https://docs.cerbos.dev/cerbos-hub/audit-log-collection). | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | AuxData block ==================== The `auxData` block configures the auxiliary data sources that can be referenced in policy conditions. ## [](#%5Fjwt)JWT Cerbos supports reading claims from a JWT issued by an authentication system. This helps reduce the boilerplate on the client side to extract the claims from a JWT and add them as attributes to the Cerbos API request. (See [The Cerbos API](../api/index.html) and [Auxiliary Data](../policies/conditions.html#auxdata) for more information on how to craft the API request and access the JWT claims in policies.) In order to verify the JWT, the Cerbos instance must have access to the appropriate keysets. They can be fetched from a URL or read from the local file system. Verification involves checking that the signature is valid and that the token has not expired. | | Although Cerbos verifies JWTs, it’s intended as a convenience rather than an authoritative check. We recommend that the tokens are independently verified at the gateway or application level before being sent to the PDP. The verification at the PDP is purely a precaution against tampering in-flight, which should not be possible in a production deployment where TLS is enforced between the application and the PDP. | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Using multiple keysets ```yaml auxData: jwt: keySets: - id: ks1 # Unique ID that can be used in API requests to indicate the keyset to use to verify a particular token. remote: # Fetch from a JWKS URL. url: https://domain.tld/.well-known/keys.jwks - id: ks2 remote: url: https://other-domain.tld/.well-known/keys.jwks refreshInterval: 1h # Explicitly set the refresh interval. - id: ks3 local: # Load from a local file file: /path/to/keys.jwks - id: ks4 local: # Load from a base64-encoded key data defined inline. data: BASE64-ENCODED-KEY-DATA - id: ks5 local: file: /path/to/keys.pem pem: true # Treat the file (or data) as PEM. ``` | | When multiple keysets are defined in the configuration file, all API requests _must_ include the keyset ID along with the JWT. When only a single keyset is defined in the configuration, then the keyset ID can be dropped from the API requests. | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | When keysets are fetched from a `remote` source, if the `refreshInterval` is not defined in the configuration, Cerbos will respect the `Cache-Control` and `Expiry` headers returned from the remote source when determining the refresh interval. If none of these data points are available, then the default refresh interval is one hour. You can disable JWT verification by setting `disableVerification` to `true`. When verification is disabled, Cerbos will not perform cryptographic verification of the JWT but the `exp` and `nbf` claims are still checked to ensure that the token is valid. You can configure the acceptable time skew for those claims by setting `acceptableTimeSkew` to a positive time duration. | | Don’t disable JWT verification unless you are certain that the token cannot be tampered with before it reaches the PDP. Similarly, it’s not recommended to set acceptableTimeSkew to more than a few seconds to prevent replay attacks. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ```yaml auxData: jwt: disableVerification: true acceptableTimeSkew: 2s ``` Some legacy authentication systems have key sets that do not contain `alg` or `kid` fields. Not having these fields defined is a security risk and the default behaviour of Cerbos is to fail the parsing of JWT. If you are aware of the risks and still want to enable those tokens to be parsed, set the `optionalAlg` and `optionalKid` options. ```yaml auxData: jwt: keySets: - id: default remote: url: https://domain.tld/.well-known/keys.jwks insecure: optionalAlg: true # Set to true only if the keyset doesn't have an alg field optionalKid: true # Set to true only if the keyset doesn't have a kid field ``` Engine block ==================== ## [](#cel%5Ferror%5Flog%5Flevel)CEL error log level When a policy condition or variable raises a CEL runtime error (for example, comparing incompatible types or referencing a missing attribute), the expression is treated as not satisfied. Cerbos logs each such error and includes it in the audit log entry for the request. The `celErrorLogLevel` setting controls the level these messages are logged at: valid values are `none`, `debug`, `info`, `warn` (the default) and `error`. Set it to `none` to disable the log messages entirely. The errors are included in audit log entries regardless of this setting. ```yaml engine: celErrorLogLevel: warn ``` ## [](#default%5Fpolicy%5Fversion)Default policy version [Cerbos policies](../policies/index.html) have a `version` field to support use cases such as having different policies for different environments (production, staging etc.) or for gradual rollout of a new version of an application. By default, when a request does not explicitly specify the policy version, the Cerbos engine attempts to find a matching policy that has its version set to `default`. You can change this fallback value by setting the `defaultPolicyVersion`. For example, if you have a Cerbos deployment for your staging environment, you may want to set `defaultPolicyVersion: staging` to ensure that the default policies in effect are the ones versioned as `staging`. ```yaml engine: defaultPolicyVersion: "default" ``` ## [](#default%5Fscope)Default scope [Cerbos policies](../policies/index.html) have a `scope` field to support a way to model hierarchical relationships that regularly occur in many situations. By default, when a request does not explicitly specify the scope, the Cerbos engine attempts to find a matching policy that has its scope set to `""`. You can change this fallback value by setting the `defaultScope`. For example, you may set the default scope to `acme` with `defaultScope: acme` which will take effect when the request does not explicitly specify `scope` field. ```yaml engine: defaultScope: "acme" ``` ## [](#globals)Globals Global variables are a way to pass environment-specific information to [policy conditions](../policies/conditions.html). For example, you might want to grant additional permissions to a role in your staging environment, without creating separate policy versions for different environments. ```yaml engine: globals: environment: "staging" ``` Values set in `globals` can then be referenced in policy conditions: ```yaml rules: - actions: - view effect: EFFECT_ALLOW roles: - developer condition: match: expr: globals.environment != "production" ``` As with other configuration settings, environment variables can be used to set global values. ```yaml engine: globals: environment: ${CERBOS_ENVIRONMENT:development} ``` ## [](#lenient%5Fscopes)Lenient scope search When working with [scopes](../policies/scoped%5Fpolicies.html), the default behaviour of the Cerbos engine is to expect that a policy file exists for the requested scope. For example, if the API request defines `a.b.c` as the `scope`, a policy file _must exist_ in the policy repository with the `a.b.c` scope. This behaviour can be overridden by setting `lenientScopeSearch` configuration to `true`. When lenient scope search is enabled, if a policy with scope `a.b.c` does not exist in the store, Cerbos will attempt to find scopes `a.b`, `a` and \`\` in that order. | | This setting only affects how Cerbos treats missing leaf scopes when searching for policies. The policies stored in your policy store _must_ have unbroken scope chains (for example, if you have a scoped policy a.b.c in the store, the policy files for scopes a.b, a and \`\` must also exist). | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ```yaml engine: lenientScopeSearch: true ``` ## [](#strict%5Fevaluation)Strict evaluation By default, when an error is raised during the evaluation of a policy condition (for example, comparing incompatible types or referencing a missing attribute), the affected expression is treated as not satisfied and the evaluation carries on. The error is logged (see [CEL error log level](#cel%5Ferror%5Flog%5Flevel)) and included in the audit log entry for the request. Because a rule whose condition raises an error never matches, an `EFFECT_DENY` rule could be silently skipped. Setting `strictEvaluation` to `true` makes such errors deny the affected action. This applies to both the check and the query planner APIs. The deny is scoped as this: * An error in a rule condition denies the actions the rule applies to. * An error in a variable denies only the actions whose conditions reference that variable. * An error in the condition of an imported derived role denies the actions of the rules that reference the derived role in their `derivedRoles`, and the actions whose conditions access `runtime.effectiveDerivedRoles`. Actions that reference neither are not affected. [Policy tests](../policies/compile.html#testing) can be run in this mode as well by passing the `--strict-evaluation` flag to the `cerbos compile` command. ```yaml engine: strictEvaluation: false ``` Configuration ==================== The Cerbos server is configured with a YAML file, conventionally named `.cerbos.yaml`. Start the server by passing the configuration file using the `--config` flag. The values defined in the file can be overridden from the command-line by using the `--set` flag. The `--set` flag can be used multiple times. For example, to override `server.httpListenAddr` and `engine.defaultPolicyVersion`, the `--set` flag can be used as follows: ```sh ./cerbos server --config=/path/to/.cerbos.yaml --set=server.httpListenAddr=:3592 --set=engine.defaultPolicyVersion=staging ``` | | Config values can reference environment variables by enclosing them between ${}, for example ${HOME}. Defaults can be set using ${VAR:default}. | | -------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#minimal-configuration)Minimal Configuration At a minimum, Cerbos requires a storage driver to be configured. If no explicit configuration is provided using the `--config` flag, Cerbos defaults to a `disk` driver configured to look for policies in a directory named `policies` in the current working directory. Default configuration ```yaml --- server: httpListenAddr: ":3592" grpcListenAddr: ":3593" engine: defaultPolicyVersion: "default" auxData: jwt: disableVerification: true storage: driver: "disk" disk: directory: "${PWD}/policies" watchForChanges: true ``` ## [](#full-configuration)Full Configuration Cerbos has many configuration options that are either optional or has reasonable defaults built-in. The following section describes all user-configurable options and their defaults. Cerbos configuration file ```yaml --- audit: accessLogsEnabled: false # AccessLogsEnabled defines whether access logging is enabled. backend: local # Backend states which backend to use for Audits. decisionLogFilters: # DecisionLogFilters define the filters to apply while producing decision logs. checkResources: # CheckResources defines the filters that apply to CheckResources calls. ignoreAllowAll: false # IgnoreAllowAll ignores responses that don't contain an EFFECT_DENY. planResources: # PlanResources defines the filters that apply to PlanResources calls. ignoreAll: false # IgnoreAll prevents any plan responses from being logged. Takes precedence over other filters. ignoreAlwaysAllow: false # IgnoreAlwaysAllow ignores ALWAYS_ALLOWED plans. decisionLogsEnabled: false # DecisionLogsEnabled defines whether logging of policy decisions is enabled. enabled: false # Enabled defines whether audit logging is enabled. excludeMetadataKeys: ['authorization'] # ExcludeMetadataKeys defines which gRPC request metadata keys should be excluded from the audit logs. Takes precedence over includeMetadataKeys. includeMetadataKeys: ['content-type'] # IncludeMetadataKeys defines which gRPC request metadata keys should be included in the audit logs. file: additionalPaths: [stdout] # AdditionalPaths to mirror the log output. Has performance implications. Use with caution. logRotation: # LogRotation settings (optional). maxFileAgeDays: 10 # MaxFileAgeDays sets the maximum age in days of old log files before they are deleted. maxFileCount: 10 # MaxFileCount sets the maximum number of files to retain. maxFileSizeMB: 100 # MaxFileSizeMB sets the maximum size of individual log files in megabytes. path: /path/to/file.log # Required. Path to the log file to use as output. The special values stdout and stderr can be used to write to stdout or stderr respectively. hub: advanced: bufferSize: 256 flushInterval: 1s gcInterval: 60s maxBatchSize: 32 mask: # Mask defines a list of attributes to exclude from the audit logs, specified as lists of JSONPaths checkResources: - inputs[*].principal.attr.foo - inputs[*].auxData - outputs metadata: ['authorization'] peer: - address - forwarded_for planResources: ['input.principal.attr.nestedMap.foo'] pipeOutput: backend: file enabled: false retentionPeriod: 168h # How long to keep records for storagePath: /path/to/dir # Path to store the data kafka: ack: all # Ack mode for producing messages. Valid values are "none", "leader" or "all" (default). Idempotency is disabled when mode is not "all". authentication: # Authentication tls: caPath: /path/to/ca.crt # Required. CAPath is the path to the CA certificate. certPath: /path/to/tls.cert # CertPath is the path to the client certificate. insecureSkipVerify: true # InsecureSkipVerify controls whether the server's certificate chain and host name are verified. Default is false. keyPath: /path/to/tls.key # KeyPath is the path to the client key. reloadInterval: 5m # ReloadInterval is the interval at which the TLS certificates are reloaded. The default is 0 (no reload). brokers: ['localhost:9092'] # Required. Brokers list to seed the Kafka client. clientID: cerbos # ClientID reported in Kafka connections. closeTimeout: 30s # CloseTimeout sets how long when closing the client to wait for any remaining messages to be flushed. compression: ['snappy', 'none'] # Compression sets the compression algorithm to use in order of priority. Valid values are "none", "gzip", "snappy","lz4", "zstd". Default is ["snappy", "none"]. encoding: json # Encoding format. Valid values are "json" (default) or "protobuf". maxBufferedRecords: 1000 # MaxBufferedRecords sets the maximum number of records the client should buffer in memory in async mode. produceSync: false # ProduceSync forces the client to produce messages to Kafka synchronously. This can have a significant impact on performance. topic: cerbos.audit.log # Required. Topic to write audit entries to. local: advanced: bufferSize: 256 flushInterval: 1s gcInterval: 60s maxBatchSize: 32 retentionPeriod: 168h # How long to keep records for storagePath: /path/to/dir # Path to store the data auxData: jwt: # JWT holds the configuration for JWTs used as an auxiliary data source for the engine. acceptableTimeSkew: 2s # AcceptableTimeSkew sets the acceptable skew when checking exp and nbf claims. disableVerification: false # DisableVerification disables JWT verification. keySets: # KeySets is the list of keysets to be used to verify tokens. - id: ks1 # Required. ID is the unique reference to this keyset. insecure: # Insecure options for relaxing security. Not recommended for production use. Use with caution. optionalAlg: false # OptionalAlg configures Cerbos to not require the alg field to be set in the key set. optionalKid: false # OptionalKid configures Cerbos to not require the kid field to be set in the key set. local: # Local defines a local keyset. Mutually exclusive with Remote. data: base64encodedJWK # Data is the encoded JWK data for this keyset. Mutually exclusive with File. file: /path/to/keys.jwk # File is the path to file containing JWK data. Mutually exclusive with Data. pem: true # PEM indicates that the data is PEM encoded. remote: # Remote defines a remote keyset. Mutually exclusive with Local. refreshInterval: 1h # RefreshInterval is the refresh interval for the keyset. url: https://domain.tld/.well-known/keys.jwks # Required. URL is the JWKS URL to fetch the keyset from. engine: celErrorLogLevel: warn # CELErrorLogLevel is the level at which CEL runtime errors raised during policy evaluation are logged. Valid values are none, debug, info, warn (default) and error. defaultPolicyVersion: "default" # DefaultPolicyVersion defines what version to assume if the request does not specify one. defaultScope: "" # DefaultScope defines what scope to assume if the request does not specify one. globals: {"environment": "staging"} # Globals are environment-specific variables to be made available to policy conditions. lenientScopeSearch: false # LenientScopeSearch configures the engine to ignore missing scopes and search upwards through the scope tree until it finds a usable policy. policyLoaderTimeout: 2s # PolicyLoaderTimeout is the timeout for loading policies from the policy store. strictEvaluation: false # StrictEvaluation denies the affected action when an error is raised during policy evaluation, instead of ignoring the erroring rule. hub: credentials: # Credentials holds Cerbos Hub client credentials. clientID: 92B0K05B6HOF # ClientID of the Cerbos Hub credential. Defaults to the value of the CERBOS_HUB_CLIENT_ID environment variable. clientSecret: ${CERBOS_HUB_CLIENT_SECRET} # ClientSecret of the Cerbos Hub credential. Defaults to the value of the CERBOS_HUB_CLIENT_SECRET environment variable. pdpID: crb-004 # PDPID is the unique identifier for this Cerbos instance. Defaults to the value of the CERBOS_HUB_PDP_ID environment variable. schema: cacheSize: 1024 # CacheSize defines the number of schemas to cache in memory. enforcement: reject # Enforcement defines level of the validations. Possible values are none, warn, reject. server: apiExplorerEnabled: true # APIExplorerEnabled defines whether the API explorer UI is enabled. adminAPI: # AdminAPI defines the admin API configuration. adminCredentials: # AdminCredentials defines the admin user credentials. passwordHash: JDJ5JDEwJEdEOVFzZDE2VVhoVkR0N2VkUFBVM09nalc0QnNZaC9xc2E4bS9mcUJJcEZXenp5OUpjMi91Cgo= # PasswordHash is the base64-encoded bcrypt hash of the password to use for authentication. username: cerbos # Username is the hardcoded username to use for authentication. enabled: true # Enabled defines whether the admin API is enabled. advanced: # Advanced server settings. grpc: # GRPC server settings. connectionTimeout: 60s # ConnectionTimeout sets the timeout for establishing a new connection. maxConcurrentStreams: 1024 # MaxConcurrentStreams sets the maximum concurrent streams per connection. Defaults to 1024. Set to 0 to allow the maximum possible number of streams. maxConnectionAge: 600s # MaxConnectionAge sets the maximum age of a connection. maxRecvMsgSizeBytes: 4194304 # MaxRecvMsgSizeBytes sets the maximum size of a single request message. Defaults to 4MiB. Affects performance and resource utilisation. http: # HTTP server settings. idleTimeout: 120s # IdleTimeout sets the keepalive timeout. readHeaderTimeout: 15s # ReadHeaderTimeout sets the timeout for reading request headers. readTimeout: 30s # ReadTimeout sets the timeout for reading a request. writeTimeout: 30s # WriteTimeout sets the timeout for writing a response. cors: # CORS defines the CORS configuration for the server. allowedHeaders: ['content-type'] # AllowedHeaders is the contents of the allowed-headers header. allowedOrigins: ['*'] # AllowedOrigins is the contents of the allowed-origins header. disabled: false # Disabled sets whether CORS is disabled. maxAge: 10s # MaxAge is the max age of the CORS preflight check. grpcListenAddr: ":3593" # Required. GRPCListenAddr is the dedicated GRPC address. httpListenAddr: ":3592" # Required. HTTPListenAddr is the dedicated HTTP address. logRequestPayloads: false # LogRequestPayloads defines whether the request payloads should be logged. metricsEnabled: true # MetricsEnabled defines whether the metrics endpoint is enabled. requestLimits: # RequestLimits defines the limits for requests. maxActionsPerResource: 50 # MaxActionsPerResource sets the maximum number of actions that could be checked for a resource in a single request. maxResourcesPerRequest: 50 # MaxResourcesPerBatch sets the maximum number of resources that could be sent in a single request. tls: # TLS defines the TLS configuration for the server. caCert: /path/to/CA_certificate # CACert is the path to the optional CA certificate for verifying client requests. cert: /path/to/certificate # Cert is the path to the TLS certificate file. key: /path/to/private_key # Key is the path to the TLS private key file. udsFileMode: 0o766 # UDSFileMode sets the file mode of the unix domain sockets created by the server. storage: # This section is required. The field driver must be set to indicate which driver to use. driver: "disk" # Required. Driver defines which storage driver to use. blob: # This section is required only if storage.driver is blob. bucket: "s3://my-bucket-name?region=us-east-2" # Required. Bucket URL (Examples: s3://my-bucket?region=us-west-1 gs://my-bucket). downloadTimeout: 30s # DownloadTimeout specifies the timeout for downloading from cloud storage. prefix: policies # Prefix specifies a subdirectory to download. requestTimeout: 10s # RequestTimeout specifies the timeout for an HTTP request. updatePollInterval: 15s # UpdatePollInterval specifies the interval to poll the cloud storage. Set to 0 to disable. workDir: ${HOME}/tmp/cerbos/work # WorkDir is the local path to check out policies to. disk: # This section is required only if storage.driver is disk. directory: pkg/test/testdata/store # Required. Directory is the path on disk where policies are stored. watchForChanges: false # Required. WatchForChanges enables watching the directory for changes. git: # This section is required only if storage.driver is git. branch: policies # Branch is the branch to checkout. checkoutDir: ${HOME}/tmp/cerbos/work # CheckoutDir is the local path to checkout the Git repo to. https: # HTTPS holds auth details for the HTTPS protocol. password: ${GITHUB_TOKEN} # The password (or token) to use for authentication. username: cerbos # The username to use for authentication. operationTimeout: 60s # OperationTimeout specifies the timeout for git operations. protocol: file # Required. Protocol is the Git protocol to use. Valid values are https, ssh, and file. ssh: # SSH holds auth details for the SSH protocol. password: pw # The password to the SSH private key. privateKeyFile: ${HOME}/.ssh/id_rsa # The path to the SSH private key file. user: git # The git user. Defaults to git. subDir: policies # SubDir is the path under the checked-out Git repo where the policies are stored. url: file://${HOME}/tmp/cerbos/policies # Required. URL is the URL to the Git repo. updatePollInterval: 60s # UpdatePollInterval specifies the interval to poll the Git repository for changes. Set to 0 to disable. hub: # This section is required only if storage.driver is hub. cacheSize: 1024 # CacheSize defines the number of policies to cache in memory. local: # Local holds configuration for local bundle source. bundlePath: /path/to/bundle.crbp # Required. BundlePath is the full path to the local bundle file. encryptionKey: 9b941a7f43fcade02d1e07bdaca008aedd0310e8804bc465bc44814a2010ecd3 # EncryptionKey is encryption key to decode the bundle. It must be hex encoded. tempDir: ${TEMP} # TempDir is the directory to use for temporary files. remote: # Remote holds configuration for remote bundle source. Takes precedence over local if both are defined. cacheDir: ${XDG_CACHE_DIR} # CacheDir is the directory to use for caching downloaded bundles. deploymentID: TVWD7S5W4V5O # DeploymentID to fetch from the server. Mutually exclusive with PlaygroundID disableAutoUpdate: false # DisableAutoUpdate sets whether new bundles should be automatically downloaded and applied. playgroundID: HDUDDWLR6ZVM # PlaygroundID to fetch from the server. Mutually exclusive with DeploymentID. tempDir: ${TEMP} # TempDir is the directory to use for temporary files. mysql: # This section is required only if storage.driver is mysql. connPool: maxLifeTime: 60m maxIdleTime: 45s maxOpen: 4 maxIdle: 1 connRetry: maxAttempts: 3 initialInterval: 0.5s maxInterval: 60s dsn: "user:password@tcp(localhost:3306)/db?interpolateParams=true" # Required. DSN is the data source connection string. serverPubKey: mykey: testdata/server_public_key.pem skipSchemaCheck: false # SkipSchemaCheck skips checking for required database tables on startup. tls: mytls: cert: /path/to/certificate key: /path/to/private_key caCert: /path/to/CA_certificate overlay: # This section is required only if storage.driver is overlay. baseDriver: blob # Required. BaseDriver is the default storage driver fallbackDriver: disk # Required. FallbackDriver is the secondary or fallback storage driver fallbackErrorThreshold: 5 # FallbackErrorThreshold is the max number of errors we allow within the fallbackErrorWindow period fallbackErrorWindow: 5m # FallbackErrorWindow is the cyclic period within which we aggregate failures postgres: # This section is required only if storage.driver is postgres. connPool: maxLifeTime: 60m maxIdleTime: 45s maxOpen: 4 maxIdle: 1 connRetry: maxAttempts: 3 initialInterval: 0.5s maxInterval: 60s skipSchemaCheck: false # SkipSchemaCheck skips checking for required database tables on startup. url: "postgres://user:password@localhost:port/db" # Required. URL is the Postgres connection URL. See https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING sqlite3: # This section is required only if storage.driver is sqlite3. dsn: ":memory:?_fk=true" # Required. Data source name telemetry: disabled: false # Disabled sets whether telemetry collection is disabled or not. reportInterval: 1h # ReportInterval is the interval between telemetry pings. stateDir: ${HOME}/.config/cerbos # StateDir is used to persist state to avoid repeatedly sending the data over and over again. ``` Observability ==================== Cerbos is designed from the ground up to be cloud native and has first-class support for observability via OpenTelemetry metrics and distributed traces. ## [](#metrics)Metrics By default, Cerbos exposes a metrics endpoint at `/_cerbos/metrics` that can be scraped by Prometheus or other metrics scrapers that support the Prometheus metrics format. This endpoint can be disabled by setting `server.metricsEnabled` configuration value to `false` (see [Server block](server.html)). Cerbos also has support for OpenTelemetry protocol (OTLP) push metrics. It can be configured using [OpenTelemetry environment variables](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/). The following environment variables are supported. | Environment variable | Description | | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | OTEL\_EXPORTER\_OTLP\_METRICS\_ENDPOINT or OTEL\_EXPORTER\_OTLP\_ENDPOINT | Address of the OTLP metrics receiver (for example: ). If not defined, OTLP metrics are disabled. | | OTEL\_EXPORTER\_OTLP\_METRICS\_INSECURE or OTEL\_EXPORTER\_OTLP\_INSECURE | Skip validating the TLS certificate of the endpoint | | OTEL\_EXPORTER\_OTLP\_METRICS\_CERTIFICATE or OTEL\_EXPORTER\_OTLP\_CERTIFICATE | Path to the certificate to use for validating the server’s TLS credentials. | | OTEL\_EXPORTER\_OTLP\_METRICS\_CLIENT\_CERTIFICATE or OTEL\_EXPORTER\_OTLP\_CLIENT\_CERTIFICATE | Path to the client certificate to use for mTLS | | OTEL\_EXPORTER\_OTLP\_METRICS\_CLIENT\_KEY or OTEL\_EXPORTER\_OTLP\_CLIENT\_KEY | Path to the client key to use for mTLS | | OTEL\_EXPORTER\_OTLP\_METRICS\_PROTOCOL or OTEL\_EXPORTER\_OTLP\_PROTOCOL | OTLP protocol. Supported values are grpc and http/protobuf. Defaults to grpc. | | OTEL\_METRIC\_EXPORT\_INTERVAL | The export interval in milliseconds. Defaults to 60000. | | OTEL\_METRIC\_EXPORT\_TIMEOUT | Timeout for exporting the data in milliseconds. Defaults to 30000. | | OTEL\_METRICS\_EXPORTER | Set to otlp to enable the OTLP exporter. Defaults to prometheus. | Refer to for more information about exporter configuration through environment variables. Note that the OpenTelemetry Go SDK used by Cerbos might not have full support for some of the environment variables listed on the OpenTelemetry specification. | | OTEL\_METRICS\_EXPORTER and OTEL\_EXPORTER\_OTLP\_METRICS\_ENDPOINT are the only required environment variables to enable OTLP metrics. | | ------------------------------------------------------------------------------------------------------------------------------------------ | ## [](#traces)Traces Cerbos supports distributed tracing to provide insights into application performance and request lifecycle. Traces from Cerbos can be exported to any compatible collector that supports the OpenTelemetry protocol (OTLP). Trace configuration should be done using [OpenTelemetry environment variables](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/). The following environment variables are supported. | | If you are upgrading from a Cerbos version older than 0.33.0, refer to [migration instructions](tracing.html#migration) for information about mapping file-based configuration to environment variables. | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Environment variable | Description | | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | OTEL\_SERVICE\_NAME | Service name reported in the traces. Defaults to cerbos. | | OTEL\_TRACES\_SAMPLER | [Trace sampler](https://opentelemetry.io/docs/specs/otel/trace/sdk/#sampling). Defaults to parentbased\_always\_off. Supported values: always\_on Record every trace. always\_off Don’t record any traces. traceidratio Record a fraction of traces based on ID. Set OTEL\_TRACES\_SAMPLER\_ARG to a value between 0 and 1 to define the fraction. parentbased\_always\_on Record all traces except those where the parent span is not sampled. parentbased\_always\_off Don’t record any traces unless the parent span is sampled. parentbased\_traceidratio Record a fraction of traces where the parent span is sampled. Set OTEL\_TRACES\_SAMPLER\_ARG to a value between 0 and 1 to define the fraction. | | OTEL\_TRACES\_SAMPLER\_ARG | Set the sampling ratio when OTEL\_TRACES\_SAMPLER is a ratio-based sampler. Defaults to 0.1. | | OTEL\_EXPORTER\_OTLP\_TRACES\_ENDPOINT or OTEL\_EXPORTER\_OTLP\_ENDPOINT | Address of the OTLP collector (for example: ). If not defined, traces are disabled. | | OTEL\_EXPORTER\_OTLP\_TRACES\_INSECURE or OTEL\_EXPORTER\_OTLP\_INSECURE | Skip validating the TLS certificate of the endpoint | | OTEL\_EXPORTER\_OTLP\_TRACES\_CERTIFICATE or OTEL\_EXPORTER\_OTLP\_CERTIFICATE | Path to the certificate to use for validating the server’s TLS credentials. | | OTEL\_EXPORTER\_OTLP\_TRACES\_CLIENT\_CERTIFICATE or OTEL\_EXPORTER\_OTLP\_CLIENT\_CERTIFICATE | Path to the client certificate to use for mTLS | | OTEL\_EXPORTER\_OTLP\_TRACES\_CLIENT\_KEY or OTEL\_EXPORTER\_OTLP\_CLIENT\_KEY | Path to the client key to use for mTLS | | OTEL\_EXPORTER\_OTLP\_TRACES\_PROTOCOL or OTEL\_EXPORTER\_OTLP\_PROTOCOL | OTLP protocol. Supported values are grpc and http/protobuf. Defaults to grpc. | Refer to for more information about exporter configuration through environment variables. Note that the OpenTelemetry Go SDK used by Cerbos might not have full support for some of the environment variables listed on the OpenTelemetry specification. | | OTEL\_EXPORTER\_OTLP\_TRACES\_ENDPOINT is the only required environment variable to enable OTLP trace exports. | | ----------------------------------------------------------------------------------------------------------------- | Schema block ==================== See [Schemas](../policies/schemas.html) for more information about schemas. ## [](#%5Fenforcement)Enforcement `enforcement` can be set to one of the following three values: * `none` (default): Do not validate requests using schemas * `warn`: Validate requests and log warnings when there are validation failures * `reject`: Deny the request if it fails validation ```yaml schema: enforcement: reject ``` Server block ==================== ## [](#%5Flisten%5Faddresses)Listen addresses By default the server will start an HTTP server on port `3592` and a gRPC server on `3593` that will listen on all available interfaces. Listen on all available interfaces (default) ```yaml server: httpListenAddr: ":3592" grpcListenAddr: ":3593" ``` Listen on a specific interface ```yaml server: httpListenAddr: "192.168.0.17:3592" grpcListenAddr: "192.168.0.17:3593" ``` Listen on a Unix domain socket ```yaml server: httpListenAddr: "unix:/var/sock/cerbos.http" grpcListenAddr: "unix:/var/sock/cerbos.grpc" ``` Listen on a Unix domain socket with specific file mode ```yaml server: httpListenAddr: "unix:/var/sock/cerbos.http" grpcListenAddr: "unix:/var/sock/cerbos.grpc" udsFileMode: 0o776 ``` ## [](#%5Fmetrics)Metrics By default, Prometheus metrics are available to scrape from the `/_cerbos/metrics` HTTP endpoint. If you want to disable metrics reporting, set `metricsEnabled` to `false`. ```yaml server: metricsEnabled: false ``` ## [](#%5Fpayload%5Flogging)Payload logging For debugging or auditing purposes, you can enable request and response payload logging for each request. | | Enabling this setting affects server performance and could expose potentially sensitive data contained in the requests to anyone with access to the server logs. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ```yaml server: logRequestPayloads: true ``` ## [](#%5Ftransport%5Flayer%5Fsecurity%5Ftls)Transport layer security (TLS) You can enable transport layer security (TLS) by defining the paths to the certificate and key file in the `TLS` section. ```yaml server: tls: cert: /path/to/certificate key: /path/to/private_key ``` | | For production use cases that require automatic certificate reloading, workload identities and other advanced features, we recommend running a proxy server such as [Envoy](https://www.envoyproxy.io), [Ghostunnel](https://github.com/ghostunnel/ghostunnel) or [Traefik](https://traefik.io) in front of the Cerbos server. | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#%5Fcors)CORS By default, [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) is enabled on the HTTP service with all origins allowed. The default allowed headers are `accept`, `content-type`, `user-agent` and `x-requested-with`. You can disable CORS by setting `server.cors.disabled` to `true`. You can also restrict the list of allowed origins and headers by setting `server.cors.allowedOrigins` and `server.cors.allowedHeaders` respectively. ```yaml server: cors: allowedOrigins: - example.com - example.org allowedHeaders: - accept - content-type - user-agent - x-custom-header - x-requested-with ``` ## [](#request-limits)Request limits By default, each Cerbos API request can include a batch of 50 resources with up to 50 actions to be checked for each resource. This limit is in place to prevent the server from being overloaded by very large requests — which affects throughput and CPU,memory,I/O usage. | | Changing these settings could have a large impact on the performance and resource utilisation of Cerbos instances. | | --------------------------------------------------------------------------------------------------------------------- | ```yaml server: requestLimits: maxActionsPerResource: 50 maxResourcesPerRequest: 50 ``` ## [](#admin-api)Enable Admin API The [Cerbos Admin API](../api/admin%5Fapi.html) provides administration functions such as adding or updating policies (if the underlying storage engine supports it) to the running Cerbos instance. It is disabled by default. Authentication is mandatory for the Admin API. See [Cerbos Admin API documentation](../api/admin%5Fapi.html) for more details. | | TLS should be enabled to ensure that credentials are transmitted securely over the network. We also highly recommend changing the default username and password when deploying Cerbos. | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ```yaml server: adminAPI: enabled: true adminCredentials: username: cerbos passwordHash: JDJ5JDEwJE5HYnk4cTY3VTE1bFV1NlR2bmp3ME9QOXdXQXFROGtBb2lWREdEY2xXbzR6WnoxYWtSNWNDCgo= ``` ### [](#password-hash)Generating a password hash Cerbos expects the password to be hashed with bcrypt and encoded with base64\. This can be achieved using the `htpasswd` and `base64` utilities available on most operating systems. ```sh echo "cerbosAdmin" | htpasswd -niBC 10 cerbos | cut -d ':' -f 2 | base64 -w0 ``` | | On MacOS, the base64 utility does not require the \-w0 argument. echo "cerbosAdmin" \| htpasswd -niBC 10 cerbos | cut -d ':' -f 2 | base64 | | ------------------------------------------------------------------------------------------------------------------ | --------------- | ------ | | | The output of the above command for a given password value is not deterministic. It will vary between invocations or between different machines. This is because the bcrypt algorithm uses a salt (random noise) to make password cracking harder. | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Storage block ==================== Cerbos supports multiple backends for storing policies. Which storage driver to use is defined by the `driver` setting. ## [](#blob-driver)Blob driver Cerbos policies can be stored in AWS S3, Google Cloud Storage, or any other S3-compatible storage systems such as [Minio](https://www.minio.io). Configuration keys * `bucket`: Required. A URL specifying the service (e.g. S3, GCS), the storage bucket and any other configuration parameters required by the provider. * AWS S3: `s3://my-bucket?region=us-west-1`. Must specify region in the URL. * Google Cloud Storage: `gs://my-bucket` * S3-compatible (e.g. Minio): `s3://my-bucket?endpoint=http://my.minio.local:8080&hostname_immutable=true®ion=local`. Must specify region in the URL. * `prefix`: Optional. Look for policies only under this key prefix. * `workDir`: Optional. Path to the local directory to download the policies to. Defaults to the system cache directory if not specified. * `updatePollInterval`: Optional. How frequently the blob store should be checked to discover new or updated policies. Defaults to 0 — which disables polling. * `requestTimeout`: Optional. HTTP request timeout. It takes an HTTP request to download a policy file. Defaults to 5s. * `downloadTimeout`: Optional. Timeout to download all policies from the the storage provider. Must be greater than the `requestTimeout`. Defaults to 60s. | | Setting the updatePollInterval to a low value could increase resource consumption in both the client and the server systems. Some managed service providers may even impose rate limits or temporary suspensions on your account if the number of requests is too high. | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Credentials for accessing the storage buckets are retrieved from the environment. The method of specifying credentials in the environment vary by cloud provider and security configuration. Usually, it involves defining environment variables such as `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` for S3 and `GOOGLE_APPLICATION_CREDENTIALS` for GCS. Refer to the relevant cloud provider documentation for more details. * AWS: * Google: AWS S3 ```yaml storage: driver: "blob" blob: bucket: "s3://my-bucket-name?region=us-east-2" prefix: policies workDir: ${HOME}/tmp/cerbos/work updatePollInterval: 15s downloadTimeout: 30s requestTimeout: 10s ``` Google Cloud Storage ```yaml storage: driver: "blob" blob: bucket: "gs://my-bucket-name" workDir: ${HOME}/tmp/cerbos/work updatePollInterval: 10s ``` Minio local container ```yaml storage: driver: "blob" blob: bucket: "s3://my-bucket-name?endpoint=http://localhost:9000&hostname_immutable=true®ion=local" workDir: ${HOME}/tmp/cerbos/work updatePollInterval: 10s ``` ## [](#disk-driver)Disk driver The disk driver is a way to serve the policies from a directory on the filesystem. Any `.yaml`, `.yml` or `.json` files in the directory tree rooted at the given path will be read and parsed as policies. Static fileset with no change detection ```yaml storage: driver: disk disk: directory: /etc/cerbos/policies ``` Dynamic fileset with change detection ```yaml storage: driver: disk disk: directory: /etc/cerbos/policies watchForChanges: true ``` | | On some platforms the automatic change detection feature can be inefficient and resource-intensive if the watched directory contains many files or gets updated frequently. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ### [](#disk-driver-archives)Archive files Alternatively, you can opt to archive and/or compress your policies directory into a Zip (`.zip`), Tar (`.tar`) or Gzip file (`.tgz` or `.tar.gz`). The archive is assumed to be laid out like a standard policy directory. It must contain no non-policy YAML files. You specify the file in your config like so: Archived fileset using a Zip file ```yaml storage: driver: disk disk: directory: /etc/cerbos/policies.zip ``` | | Change detection will be disabled when using archive files. | | -------------------------------------------------------------- | ## [](#git-driver)Git driver Git is the preferred method of storing Cerbos policies. The server is smart enough to detect when new commits are made to the git repository and refresh its state based on the changes. | | Azure DevOps repositories use a newer protocol that is currently not supported by the Git library used by Cerbos. We are working to address this issue. In the mean time, please consider using the Cerbos disk storage in conjunction with an external Git sync implementation such as or using a CI pipeline to publish your policies to another storage implementation supported by Cerbos. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | * Git repositories can be local (`file` protocol) or remote (`ssh` or `https`). Please note that the local `file` protocol requires `git` to be available and cannot be used with the Cerbos container. * If no `branch` is specified, the default branch would be the `master` branch. * If no `subDir` is specified, the entire repository would be scanned for policies (`.yaml`, `.yml` or `.json`). * The `checkoutDir` is the working directory of the server and must be writable by the server process. * If `updatePollInterval` is set to 0, the source repository will not be polled to pick up any new commits. * If `operationTimeout` is not specified, the default timeout for git operations is 60 seconds. | | If the git repository is remote, setting the updatePollInterval to a low value could increase resource consumption in both the client and the server systems. Some managed service providers may even impose rate limits or temporary suspensions on your account if the number of requests is too high. | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Local git repository ```yaml storage: driver: "git" git: protocol: file url: file://${HOME}/tmp/cerbos/policies checkoutDir: ${HOME}/tmp/cerbos/work updatePollInterval: 10s ``` Remote git repository accessed over HTTPS ```yaml storage: driver: "git" git: protocol: https url: https://github.com/cerbos/policy-test.git branch: main subDir: policies checkoutDir: ${HOME}/tmp/work/policies updatePollInterval: 60s operationTimeout: 30s https: username: cerbos password: ${GITHUB_TOKEN} ``` Remote git repository accessed over SSH ```yaml storage: driver: "git" git: protocol: ssh url: github.com:cerbos/policy-test.git branch: main subDir: policies checkoutDir: ${HOME}/tmp/cerbos/work updatePollInterval: 60s ssh: user: git privateKeyFile: ${HOME}/.ssh/id_rsa ``` ## [](#hub)Hub driver | | Requires a [Cerbos Hub](https://www.cerbos.dev/product-cerbos-hub) account. [![Try Cerbos Hub](../_images/try_cerbos_hub.png)](https://hub.cerbos.cloud) | | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | Connects the PDP to a Cerbos Hub [Deployment](../../../cerbos-hub/deployments.html). A Cerbos Hub Deployment tracks a connected Git repository or a [managed Cerbos Hub store](../../../cerbos-hub/policy-stores.html) and automatically compiles, tests and pushes an optimized policy bundle to the PDP whenever a policy change is committed. If you are new to Cerbos Hub, follow the [getting started guide](../../../cerbos-hub/getting-started.html). For more information about configuring a PDP to connect to Cerbos Hub, refer to the [Deployments documentation](../../../cerbos-hub/deployments.html). ## [](#mysql)MySQL driver The MySQL storage backend is one of the dynamic stores that supports adding or updating policies at runtime through the [Admin API](server.html#admin-api). | | The [cerbosctl utility](../cli/cerbosctl.html) is a handy way to interact with the Admin API and supports loading policies through the [built-in put command](../cli/cerbosctl.html#put). | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | | Cerbos has an in-memory cache for holding compiled policy definitions to speed up the evaluation process. When a policy is removed or updated using the [Admin API](../api/admin%5Fapi.html#policy-management) this cache is updated by the instance that handles the request. However, if you share the database with multiple Cerbos instances, the other instances won’t be aware of the change and might still have the old policy definition cached in memory. There are two ways to handle this situation. By default, the cache entries are stored indefinitely until there’s memory pressure. You can set a maximum cache duration for entries by setting the compile.cacheDuration configuration value. This could help make all the Cerbos instances to become eventually consistent within a time frame that’s acceptable to you. Setting the compile.cacheDuration to a low value helps to reach in an eventually consistent state quicker. Invoke the [/admin/store/reload API endpoint](../api/admin%5Fapi.html#store-management) on all the Cerbos instances whenever you make a change to your policies. | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | | Unlike the SQLite3 driver, the tables and other database objects are not created automatically by the Cerbos MySQL driver. This is to minimize the privileges the Cerbos instance has on the MySQL installation. You must create the required tables using the provided script before configuring Cerbos to connect to the database. | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | The driver configuration expects the connection details to be provided as a DSN in the following form: [username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN] See for the list of supported parameters. You can use environment variable references in the URL to avoid storing credentials as part of the Cerbos configuration file. Using MySQL as a storage backend for Cerbos ```yaml storage: driver: "mysql" mysql: dsn: "${MYSQL_USER}:${MYSQL_PASSWORD}@tcp(localhost:3306)/cerbos" ``` ### [](#%5Fsecure%5Fconnections)Secure connections If your MySQL server requires TLS or if you want to use RSA key pair-based password exchange, you can configure those settings as follows: TLS certificates ```yaml storage: driver: "mysql" mysql: dsn: "${MYSQL_USER}:${MYSQL_PASSWORD}@tcp(localhost:3306)/cerbos?tls=mysecuretls" tls: mysecuretls: caCert: /path/to/ca_certificate.crt cert: /path/to/certificate.crt key: /path/to/private.key ``` Server public key ```yaml storage: driver: "mysql" mysql: dsn: "${MYSQL_USER}:${MYSQL_PASSWORD}@tcp(localhost:3306)/cerbos?serverPubKey=mypubkey" serverPubKey: mypubkey: /path/to/server_public_key.pem ``` ### [](#%5Fconnection%5Fpool)Connection pool Cerbos uses a connection pool when connecting to a database. You can configure the connection pool settings by adding a `connPool` section to the driver configuration. Available options are: `maxLifeTime` The maximum length of time a connection can be reused for. This is useful when your database enforces a maximum lifetime on connections or if you have a load balancer in front of your database to spread the load. `maxIdleTime` How long a connection should be idle for before it is closed. Useful if you want to cleanup idle connections quickly. `maxOpen` Maximum number of connections that can be open at any given time (including idle connections). `maxIdle` Maximum number of idle connections that can be open at any given time. | | Connection pool settings can have a significant impact on the performance of Cerbos and your database server. Make sure you fully understand the implications of updating these settings before making any changes. | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ```yaml storage: driver: "mysql" mysql: dsn: "${MYSQL_USER}:${MYSQL_PASSWORD}@tcp(localhost:3306)/cerbos" connPool: maxLifeTime: 5m maxIdleTime: 3m maxOpen: 10 maxIdle: 5 ``` ### [](#%5Fconnection%5Fretries)Connection retries Cerbos attempts to connect to the database on startup and exits if connection cannot be established after three attempts. You can configure the connection retry settings using the `connRetry` options. `maxAttempts` Maximum number of connection attempts before giving up `initialInterval` The time to wait before the second connection attempt. Subsequent attempts have increasing wait times (exponential backoff) derived from a combination of this value and the retry attempt number `maxInterval` Maximum amount of time to wait between retries. This affects the maximum value produced by the exponential backoff algorithm. | | Changing the retry settings affect the availability of Cerbos and the time it takes to detect and recover from a failure. For example, if the database connection details are incorrect or have changed, it will take longer for a Cerbos PDP to fail on startup because of retries. | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ### [](#mysql-schema)Database object definitions You can customise the script below to suit your environment. Make sure to specify a strong password for the `cerbos_user` user. ```sql CREATE DATABASE IF NOT EXISTS cerbos CHARACTER SET utf8mb4; USE cerbos; CREATE TABLE IF NOT EXISTS policy ( id BIGINT PRIMARY KEY, kind VARCHAR(128) NOT NULL, name VARCHAR(1024) NOT NULL, version VARCHAR(128) NOT NULL, scope VARCHAR(512), description TEXT, disabled BOOLEAN default false, definition BLOB); CREATE TABLE IF NOT EXISTS policy_dependency ( policy_id BIGINT NOT NULL, dependency_id BIGINT NOT NULL, PRIMARY KEY (policy_id, dependency_id), FOREIGN KEY (policy_id) REFERENCES policy(id) ON DELETE CASCADE); CREATE TABLE IF NOT EXISTS policy_ancestor ( policy_id BIGINT NOT NULL, ancestor_id BIGINT NOT NULL, PRIMARY KEY (policy_id, ancestor_id), FOREIGN KEY (policy_id) REFERENCES policy(id) ON DELETE CASCADE); CREATE TABLE IF NOT EXISTS policy_revision ( revision_id INTEGER AUTO_INCREMENT PRIMARY KEY, action ENUM('INSERT', 'UPDATE', 'DELETE'), id BIGINT NOT NULL, kind VARCHAR(128), name VARCHAR(1024), version VARCHAR(128), scope VARCHAR(512), description TEXT, disabled BOOLEAN, definition BLOB, update_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP); CREATE TABLE IF NOT EXISTS attr_schema_defs ( id VARCHAR(255) PRIMARY KEY, definition JSON); DROP TRIGGER IF EXISTS policy_on_insert; CREATE TRIGGER policy_on_insert AFTER INSERT ON policy FOR EACH ROW INSERT INTO policy_revision(action, id, kind, name, version, scope, description, disabled, definition) VALUES('INSERT', NEW.id, NEW.kind, NEW.name, NEW.version, NEW.scope, NEW.description, NEW.disabled, NEW.definition); DROP TRIGGER IF EXISTS policy_on_update; CREATE TRIGGER policy_on_update AFTER UPDATE ON policy FOR EACH ROW INSERT INTO policy_revision(action, id, kind, name, version, scope, description, disabled, definition) VALUES('UPDATE', NEW.id, NEW.kind, NEW.name, NEW.version, NEW.scope, NEW.description, NEW.disabled, NEW.definition); DROP TRIGGER IF EXISTS policy_on_delete; CREATE TRIGGER policy_on_delete AFTER DELETE ON policy FOR EACH ROW INSERT INTO policy_revision(action, id, kind, name, version, scope, description, disabled, definition) VALUES('DELETE', OLD.id, OLD.kind, OLD.name, OLD.version, OLD.scope, OLD.description, OLD.disabled, OLD.definition); CREATE USER IF NOT EXISTS cerbos_user IDENTIFIED BY 'changeme'; GRANT SELECT,INSERT,UPDATE,DELETE ON cerbos.policy TO cerbos_user; GRANT SELECT,INSERT,UPDATE,DELETE ON cerbos.attr_schema_defs TO cerbos_user; GRANT SELECT,INSERT,UPDATE,DELETE ON cerbos.policy_dependency TO cerbos_user; GRANT SELECT,INSERT,UPDATE,DELETE ON cerbos.policy_ancestor TO cerbos_user; GRANT SELECT,INSERT,DELETE ON cerbos.policy_revision TO cerbos_user; ``` ## [](#overlay)Overlay driver You can provide redundancy by configuring an `overlay` driver, which wraps a `base` and a `fallback` driver. Under normal operation, the base driver will be targeted as usual. However, if the driver consistently errors, the PDP will start targeting the fallback driver instead. The fallback is determined by a configurable [circuit breaker pattern](https://learn.microsoft.com/en-us/previous-versions/msp-n-p/dn589784%28v=pandp.10%29). You can configure the fallback error threshold and the fallback error window to determine how many errors can occur within a rolling window before the circuit breaker is tripped. ```yaml storage: driver: "overlay" overlay: baseDriver: postgres fallbackDriver: disk fallbackErrorThreshold: 5 # number of errors that occur within the fallbackErrorWindow to trigger failover fallbackErrorWindow: 5s # the rolling window in which errors are aggregated disk: directory: policies watchForChanges: true postgres: url: "postgres://${PG_USER}:${PG_PASSWORD}@localhost:5432/postgres?sslmode=disable&search_path=cerbos" ``` | | The overlay driver assumes the same interface as the base driver. Any operations that are available on the base driver but not the fallback driver will error if the circuit breaker is open and the fallback driver is being targeted. Likewise, even if the fallback driver supports additional operations compared to the base driver, these will still not be available should failover occur. | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#postgres)Postgres driver The Postgres storage backend is one of the dynamic stores that supports adding or updating policies at runtime through the [Admin API](server.html#admin-api). | | The [cerbosctl utility](../cli/cerbosctl.html) is a handy way to interact with the Admin API and supports loading policies through the [built-in put command](../cli/cerbosctl.html#put). | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | | Cerbos has an in-memory cache for holding compiled policy definitions to speed up the evaluation process. When a policy is removed or updated using the [Admin API](../api/admin%5Fapi.html#policy-management) this cache is updated by the instance that handles the request. However, if you share the database with multiple Cerbos instances, the other instances won’t be aware of the change and might still have the old policy definition cached in memory. There are two ways to handle this situation. By default, the cache entries are stored indefinitely until there’s memory pressure. You can set a maximum cache duration for entries by setting the compile.cacheDuration configuration value. This could help make all the Cerbos instances to become eventually consistent within a time frame that’s acceptable to you. Setting the compile.cacheDuration to a low value helps to reach in an eventually consistent state quicker. Invoke the [/admin/store/reload API endpoint](../api/admin%5Fapi.html#store-management) on all the Cerbos instances whenever you make a change to your policies. | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | | Unlike the SQLite3 driver, the tables and other database objects are not created automatically by the Cerbos Postgres driver. This is to minimize the privileges the Cerbos instance has on the Postgres installation. You must create the required tables using the provided script before configuring Cerbos to connect to the database. | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | The driver configuration expects the connection details to be provided as connection URL. See [Postgres connstring documentation](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING) for more information. Use the `search_path` parameter to point to the schema containing the Cerbos tables. You can use environment variable references in the URL to avoid storing credentials as part of the Cerbos configuration file. Using Postgres as a storage backend for Cerbos ```yaml storage: driver: "postgres" postgres: url: "postgres://${PG_USER}:${PG_PASSWORD}@localhost:5432/postgres?sslmode=disable&search_path=cerbos" ``` ### [](#%5Fconnection%5Fpool%5F2)Connection pool Cerbos uses a connection pool when connecting to a database. You can configure the connection pool settings by adding a `connPool` section to the driver configuration. Available options are: `maxLifeTime` The maximum length of time a connection can be reused for. This is useful when your database enforces a maximum lifetime on connections or if you have a load balancer in front of your database to spread the load. `maxIdleTime` How long a connection should be idle for before it is closed. Useful if you want to cleanup idle connections quickly. `maxOpen` Maximum number of connections that can be open at any given time (including idle connections). `maxIdle` Maximum number of idle connections that can be open at any given time. | | Connection pool settings can have a significant impact on the performance of Cerbos and your database server. Make sure you fully understand the implications of updating these settings before making any changes. | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ```yaml storage: driver: "postgres" postgres: url: "postgres://${PG_USER}:${PG_PASSWORD}@localhost:5432/postgres?sslmode=disable&search_path=cerbos" connPool: maxLifeTime: 5m maxIdleTime: 3m maxOpen: 10 maxIdle: 5 ``` ### [](#%5Fconnection%5Fretries%5F2)Connection retries Cerbos attempts to connect to the database on startup and exits if connection cannot be established after three attempts. You can configure the connection retry settings using the `connRetry` options. `maxAttempts` Maximum number of connection attempts before giving up `initialInterval` The time to wait before the second connection attempt. Subsequent attempts have increasing wait times (exponential backoff) derived from a combination of this value and the retry attempt number `maxInterval` Maximum amount of time to wait between retries. This affects the maximum value produced by the exponential backoff algorithm. | | Changing the retry settings affect the availability of Cerbos and the time it takes to detect and recover from a failure. For example, if the database connection details are incorrect or have changed, it will take longer for a Cerbos PDP to fail on startup because of retries. | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ### [](#postgres-schema)Database object definitions You can customise the script below to suit your environment. Make sure to specify a strong password for the `cerbos_user` user. ```sql CREATE SCHEMA IF NOT EXISTS cerbos; SET search_path TO cerbos; CREATE TABLE IF NOT EXISTS policy ( id bigint NOT NULL PRIMARY KEY, kind VARCHAR(128) NOT NULL, name VARCHAR(1024) NOT NULL, version VARCHAR(128) NOT NULL, scope VARCHAR(512), description TEXT, disabled BOOLEAN default false, definition BYTEA ); CREATE TABLE IF NOT EXISTS policy_dependency ( policy_id BIGINT, dependency_id BIGINT, PRIMARY KEY (policy_id, dependency_id), FOREIGN KEY (policy_id) REFERENCES cerbos.policy(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS policy_ancestor ( policy_id BIGINT, ancestor_id BIGINT, PRIMARY KEY (policy_id, ancestor_id), FOREIGN KEY (policy_id) REFERENCES cerbos.policy(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS policy_revision ( revision_id SERIAL PRIMARY KEY, action VARCHAR(64), id BIGINT, kind VARCHAR(128), name VARCHAR(1024), version VARCHAR(128), scope VARCHAR(512), description TEXT, disabled BOOLEAN, definition BYTEA, update_timestamp TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS attr_schema_defs ( id VARCHAR(255) PRIMARY KEY, definition JSON ); CREATE OR REPLACE FUNCTION process_policy_audit() RETURNS TRIGGER AS $policy_audit$ BEGIN IF (TG_OP = 'DELETE') THEN INSERT INTO policy_revision(action, id, kind, name, version, scope, description, disabled, definition) VALUES('DELETE', OLD.id, OLD.kind, OLD.name, OLD.version, OLD.scope, OLD.description, OLD.disabled, OLD.definition); ELSIF (TG_OP = 'UPDATE') THEN INSERT INTO policy_revision(action, id, kind, name, version, scope, description, disabled, definition) VALUES('UPDATE', NEW.id, NEW.kind, NEW.name, NEW.version, NEW.scope, NEW.description, NEW.disabled, NEW.definition); ELSIF (TG_OP = 'INSERT') THEN INSERT INTO policy_revision(action, id, kind, name, version, scope, description, disabled, definition) VALUES('INSERT', NEW.id, NEW.kind, NEW.name, NEW.version, NEW.scope, NEW.description, NEW.disabled, NEW.definition); END IF; RETURN NULL; END; $policy_audit$ LANGUAGE plpgsql; CREATE TRIGGER policy_audit AFTER INSERT OR UPDATE OR DELETE ON policy FOR EACH ROW EXECUTE PROCEDURE process_policy_audit(); CREATE USER cerbos_user WITH PASSWORD 'changeme'; GRANT CONNECT ON DATABASE postgres TO cerbos_user; GRANT USAGE ON SCHEMA cerbos TO cerbos_user; GRANT SELECT,INSERT,UPDATE,DELETE ON cerbos.policy, cerbos.policy_dependency, cerbos.policy_ancestor, cerbos.attr_schema_defs TO cerbos_user; GRANT SELECT,INSERT,DELETE ON cerbos.policy_revision TO cerbos_user; GRANT USAGE,SELECT ON cerbos.policy_revision_revision_id_seq TO cerbos_user; ``` ## [](#sqlite3)SQLite3 driver The SQLite3 storage backend is one of the dynamic stores that supports adding or updating policies at runtime through the [Admin API](server.html#admin-api). | | The [cerbosctl utility](../cli/cerbosctl.html) is a handy way to interact with the Admin API and supports loading policies through the [built-in put command](../cli/cerbosctl.html#put). | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | In-memory ephemeral database ```yaml storage: driver: "sqlite3" sqlite3: dsn: "file::memory:?cache=shared" ``` | | Cerbos uses a database connection pool which would result in unexpected behaviour when using the SQLite:memory: database. Use file::memory:?cache=shared instead. See for details. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | On-disk persistent database ```yaml storage: driver: "sqlite3" sqlite3: dsn: "file:/tmp/cerbos.sqlite?mode=rwc&cache=shared&_fk=true" ``` | | [Cerbos Hub](https://docs.cerbos.dev/cerbos-hub/) automates policy change detection, testing and secure distribution to connected PDPs. | | ------------------------------------------------------------------------------------------------------------------------------------------ | Telemetry ==================== Cerbos developers rely on anonymous usage data to help prioritise new features and improve the product. The information collected is completely anonymous, never shared with external entities, and you can opt out at any time. ## [](#%5Fwhat%5Fkind%5Fof%5Fdata%5Fis%5Fcollected)What kind of data is collected? * Cerbos build information like version, commit and build date * Operating system type and architecture * Enabled Cerbos features (storage backend type and schema enforcement level are some examples of this information) * Aggregated statistics about the policies like the total number of policies and average number of rules in a policy * Aggregated statistics about Cerbos API calls and the gRPC user agents. You can view the full schema of the telemetry data on [GitHub](https://github.com/cerbos/cerbos/tree/main/api/public/cerbos/telemetry/v1/telemetry.proto) or on the [Buf schema registry](https://buf.build/cerbos/cerbos-api/docs/main/cerbos.telemetry.v1). Only a small number of Zenauth (the company leading the development of Cerbos) employees have access to the data. ## [](#%5Fhow%5Fto%5Fdisable%5Ftelemetry%5Fcollection)How to disable telemetry collection There are multiple ways in which you can disable telemetry collection. ### [](#%5Fuse%5Fthe%5Fconfiguration%5Ffile)Use the configuration file Set `telemetry.disabled: true` in the [Cerbos configuration file](index.html). ```yaml telemetry: disabled: true ``` ### [](#%5Fset%5Fan%5Fenvironment%5Fvariable)Set an environment variable Set `CERBOS_NO_TELEMETRY=1` or `CERBOS_NO_TELEMETRY=true` in your environment. We also honour the `DO_NOT_TRACK` environment variable if it exists. With the binary ```sh CERBOS_NO_TELEMETRY=1 ./cerbos server --config=/path/to/.cerbos.yaml ``` With the container ```sh docker run -i -t -p 3592:3592 \ -e CERBOS_NO_TELEMETRY=true \ ghcr.io/cerbos/cerbos:0.55.0 ``` ### [](#%5Fthrough%5Fthe%5Fcommand%5Fline)Through the command line Start Cerbos with `--set=telemetry.disabled=true` command line flag. With the binary ```sh ./cerbos server --config=/path/to/.cerbos.yaml --set=telemetry.disabled=true ``` With the container ```sh docker run -i -t -p 3592:3592 \ ghcr.io/cerbos/cerbos:0.55.0 \ server --set=telemetry.disabled=true ``` Tracing block ==================== | | The tracing block was deprecated in Cerbos 0.32.0 and removed in Cerbos 0.33.0\. Refer to [observability configuration](observability.html#traces) for information about configuring traces. | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#migration)Migrating tracing configuration from previous Cerbos versions From Cerbos 0.32.0, the preferred method of trace configuration is through the OpenTelemetry environment variables described in [observability configuration](observability.html#traces). The `tracing` section is no longer supported by Cerbos versions starting from 0.33.0\. Native Jaeger protocol is superseded by OTLP as well and no longer supported. Follow the instructions below to migrate your existing configuration. | Configuration setting | New configuration | | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | tracing.serviceName | Set OTEL\_SERVICE\_NAME environment variable | | tracing.sampleProbability | Set OTEL\_TRACES\_SAMPLER to parentbased\_traceidratio and OTEL\_TRACES\_SAMPLER\_ARG to the probability value | | tracing.jaeger.agentEndpoint or tracing.jaeger.collectorEndpoint | Jaeger now has [stable support for OTLP](https://www.jaegertracing.io/docs/1.51/apis/#opentelemetry-protocol-stable) and is the recommended way to send traces. Set OTEL\_EXPORTER\_OTLP\_TRACES\_ENDPOINT to the address of your Jaeger instance (for example: ) and, optionally, set OTEL\_EXPORTER\_OTLP\_TRACES\_INSECURE=true if Jaeger is using a self-signed certificate. If you want to use the HTTP API or customize other aspects, refer to the documentation above for other supported environment variables. | | tracing.otlp.collectorEndpoint | Set OTEL\_EXPORTER\_OTLP\_TRACES\_ENDPOINT to the value of the collector endpoint and OTEL\_EXPORTER\_OTLP\_INSECURE=true to emulate the behaviour of Cerbos OTLP exporter before version 0.32.0. | Deploy Cerbos to Cloud platforms ==================== ## [](#%5Faws%5Fmarketplace)AWS Marketplace Cerbos is avaliable via the [AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-6kkahbtwv3gtq) and can be deployed in either [Elastic Kubernetes Service (EKS)](https://aws.amazon.com/eks/) or [Elastic Container Service (ECS)](https://aws.amazon.com/ecs/). When deploying Cerbos via the Marketplace, your Cerbos Hub account is included with the purchase via AWS and no additional paid account is required. ### [](#%5Felastic%5Fkubernetes%5Fservice%5Feks)Elastic Kubernetes Service (EKS) #### [](#%5Fstep%5F1%5Fcreate%5Fan%5Fiam%5Fpolicy)Step 1: Create an IAM policy To deploy Cerbos from AWS Marketplace, you need to assign an IAM policy with appropriate IAM permission to a Kubernetes service account before starting the deployment. You can either use AWS managed policy `arn:aws:iam::aws:policy/AWSMarketplaceMeteringRegisterUsage` or create your own IAM policy. Here’s an example IAM policy: ```json { "Version": "2012-10-17", "Statement": [ { "Action": [ "aws-marketplace:RegisterUsage" ], "Effect": "Allow", "Resource": "*" } ] } ``` #### [](#%5Fstep%5F2%5Fcreate%5Fan%5Fiam%5Frole%5Ffor%5Fthe%5Fkubernetes%5Fservice%5Faccount%5Firsa)Step 2: Create an IAM role for the Kubernetes service account (IRSA) Once the IAM role has been created, a Kubernetes service account needs to be created and assicated with the role. We recommend doing this via [eksctl](https://docs.aws.amazon.com/eks/latest/userguide/eksctl.html). The command below automates the process to: 1. Create an IAM role with AWS-managed IAM policy (or you can provide your own ARN). 2. Create a Kubernetes service account name `cerbos-serviceaccount` in the cluster. 3. Set up a trust relationship between the IAM role and the service account. 4. Modify `cerbos-serviceaccount` annotation to associate it with the created IAM role Remember to replace `CLUSTER_NAME` with your actual Amazon EKS cluster name and optionally set the namespace. ```sh eksctl create iamserviceaccount \ --name cerbos-serviceaccount \ --attach-policy-arn arn:aws:iam::aws:policy/AWSMarketplaceMeteringRegisterUsage \ --namespace default \ --cluster CLUSTER_NAME \ --approve \ --override-existing-serviceaccounts ``` #### [](#%5Fstep%5F4%5Fdeploy%5Fcerbos%5Fwith%5Fthe%5Fservice%5Faccount)Step 4: Deploy Cerbos with the service account | | Requires a [Cerbos Hub](https://www.cerbos.dev/product-cerbos-hub) account. [![Try Cerbos Hub](../_images/try_cerbos_hub.png)](https://hub.cerbos.cloud) | | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | For the following steps, you need a Cerbos Hub account with a workspace connected to your policy repository and a set of client credentials. See the [Cerbos Hub getting started guide](../../../cerbos-hub/getting-started.html) for details. * Create a new Kubernetes secret to hold the Cerbos Hub credentials. See the [Cerbos Hub guide](../../../cerbos-hub/getting-started.html) for details. ```sh kubectl create secret generic cerbos-hub-credentials \ --from-literal=CERBOS_HUB_CLIENT_ID=YOUR_CLIENT_ID \ (1) --from-literal=CERBOS_HUB_CLIENT_SECRET=YOUR_CLIENT_SECRET \ (2) --from-literal=CERBOS_HUB_DEPLOYMENT_ID=YOUR_DEPLOYMENT_ID (3) ``` | **1** | Client ID from the Cerbos Hub credential | | ----- | ---------------------------------------------------------------- | | **2** | Client secret from the Cerbos Hub credential | | **3** | Cerbos Hub [Deployment ID](../../../cerbos-hub/deployments.html) | * Create a new values file named `hub-values.yaml` with the following contents: ```yaml # Assign the service account serviceAccount: name: cerbos-serviceaccount # Set Cerbos configuration cerbos: config: # Configure the Hub audit backend audit: enabled: true <1> backend: "hub" hub: storagePath: /audit_logs # Create environment variables from the secret. envFrom: - secretRef: name: cerbos-hub-credentials # Mount volume for locally buffering the audit logs. A persistent volume is recommended for production use cases. volumes: - name: cerbos-audit-logs emptyDir: {} volumeMounts: - name: cerbos-audit-logs mountPath: /audit_logs ``` | **1** | Enables audit log collection. See [Hub audit log collection documentation](../../../cerbos-hub/audit-log-collection.html) for information about masking sensitive fields and other advanced settings. | | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | * Deploy Cerbos using the AWS Helm chart ```sh aws ecr get-login-password \ --region us-west-1 | helm registry login \ --username AWS \ --password-stdin 709825985650.dkr.ecr.us-west-1.amazonaws.com helm install cerbos oci://709825985650.dkr.ecr.us-east-1.amazonaws.com/cerbos/cerbos-aws-helm --values=hub-values.yaml ``` ### [](#%5Felastic%5Fcontainer%5Fservice%5Fecs)Elastic Container Service (ECS) #### [](#%5Fstep%5F1%5Fcreate%5Fecs%5Ftask%5Frole%5Fpolicy)Step 1: Create ECS Task Role policy To deploy Cerbos from AWS Marketplace, you need to create an ECS Task AIM Role with appropriate IAM permission before starting the deployment. You can either use AWS managed policy `arn:aws:iam::aws:policy/AWSMarketplaceMeteringRegisterUsage` or create your own IAM policy. Here’s an example IAM policy required - you will need the ARN for this role when defining the task. ```json { "Version": "2012-10-17", "Statement": [ { "Action": [ "aws-marketplace:RegisterUsage" ], "Effect": "Allow", "Resource": "*" } ] } ``` #### [](#%5Fstep%5F2%5Fcreate%5Fthe%5Ftask%5Fdefinition)Step 2: Create the task definition In the AWS console or the CLI, create the task using the following JSON definition, subsituting the values noted: ```json { "family": "cerbos", "containerDefinitions": [ { "name": "cerbos", "image": "709825985650.dkr.ecr.us-east-1.amazonaws.com/cerbos/cerbos:0.55.0", "cpu": 0, "portMappings": [ { "name": "cerbos-3592-tcp", "containerPort": 3592, "hostPort": 3592, "protocol": "tcp", "appProtocol": "http" }, { "name": "cerbos-3593-tcp", "containerPort": 3593, "hostPort": 3593, "protocol": "tcp" } ], "essential": true, "environment": [ { "name": "CERBOS_HUB_CLIENT_ID", "value": "YOUR_CLIENT_ID" <1> }, { "name": "CERBOS_HUB_CLIENT_SECRET", "value": "YOUR_CLIENT_SECRET" <2> }, { "name": "CERBOS_HUB_DEPLOYMENT_ID", "value": "YOUR_DEPLOYMENT_ID" <3> } ], "command": [ "server", "--set=audit.enabled=true", <4> "--set=audit.backend=hub", "--set=audit.hub.storagePath=/tmp" ], "environmentFiles": [], "mountPoints": [], "volumesFrom": [], "ulimits": [], "healthCheck": { "command": [ "CMD", "/cerbos", "healthcheck" ], "interval": 30, "timeout": 5, "retries": 3, "startPeriod": 5 }, "systemControls": [] } ], "taskRoleArn": "TASK_ROLE_ARN", <5> "executionRoleArn": "TASK_EXECUTION_ROLE_ARN", <6> "networkMode": "awsvpc", "requiresCompatibilities": [ "FARGATE" ], "cpu": "1024", "memory": "3072", "runtimePlatform": { "cpuArchitecture": "X86_64", "operatingSystemFamily": "LINUX" } } ``` | **1** | Client ID from the Cerbos Hub credential | | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **2** | Client secret from the Cerbos Hub credential | | **3** | Cerbos Hub [Deployment ID](../../../cerbos-hub/deployments.html) | | **4** | Enables audit log collection. See [Hub audit log collection documentation](../../../cerbos-hub/audit-log-collection.html) for information about masking sensitive fields and other advanced settings. | | **5** | The ARN for the custom ECS Task Role defined in Step 1. | | **6** | The ARN for the ECS Task Execution. The default is arn:aws:iam:::role/ecsTaskExecutionRole | #### [](#%5Fstep%5F4%5Flaunch%5Fa%5Fservice)Step 4: Launch a service Using the above task defintion, launch a service in your ECS Cluster. Take note to ensure the service is running attached to the security groups which your applications will be calling Cerbos from. ## [](#%5Ffly%5Fio)Fly.io You can deploy Cerbos on Fly.io as a [Fly Launch](https://fly.io/docs/apps) app. The following `fly.toml` file shows how to deploy Cerbos with healthchecks and metrics: ```toml app = '' (1) primary_region = '' (2) [build] image = 'ghcr.io/cerbos/cerbos:0.55.0' [[mounts]] source = 'policies' destination = '/policies' initial_size = '1GB' [[services]] protocol = '' internal_port = 3592 [[services.ports]] port = 3592 handlers = ['tls', 'http'] [[services.http_checks]] interval = '5s' timeout = '2s' grace_period = '5s' method = 'get' path = '/_cerbos/health' protocol = 'http' [[services]] protocol = '' internal_port = 3593 [[services.ports]] port = 3593 handlers = ['tls'] [services.ports.tls_options] alpn = ['h2'] [[vm]] memory = '1gb' cpu_kind = 'shared' cpus = 1 [metrics] port = 3592 path = "/_cerbos/metrics" ``` | **1** | The name of the [Fly App](https://fly.io/docs/apps) | | ----- | ----------------------------------------------------------------------------- | | **2** | Pick a Fly.io [region](https://fly.io/docs/reference/regions/#fly-io-regions) | The example above launches a Cerbos instance with the [minimal configuration](../configuration/index.html#minimal-configuration) using an empty [Fly volume](https://fly.io/docs/reference/volumes/) mounted as the policy directory. For production use cases, consider using one of the following methods for policy storage. | | Your host or service for an application should be listening on the right address within the VM: Fly Proxy reaches services through a private IPv4 address on each VM, so the process should listen on 0.0.0.0: ([but see A note on IPv4 and IPv6 wildcards](https://fly.io/docs/networking/app-services/#a-note-on-ipv4-and-ipv6-wildcards)). | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | * Cerbos [git driver](../configuration/storage.html#git-driver) with a Git provider such as GitHub or GitLab * Cerbos [blob driver](../configuration/storage.html#blob-driver) with [Tigris](https://fly.io/docs/reference/tigris/#create-and-manage-a-tigris-storage-bucket) * Cerbos [sqlite3 driver](../configuration/storage.html#sqlite3) with a standalone SQLite database or [LiteFS](https://fly.io/docs/litefs/#litefs-cloud) * Cerbos [postgres driver](../configuration/storage.html#postgres) with [Fly Postgres](https://fly.io/docs/postgres/) * [Cerbos Hub](https://www.cerbos.dev/product-cerbos-hub) | | Cerbos can be [configured entirely from the command line](../configuration/index.html) using \--set flags. On the Fly.io platform, they can be set by overriding the cmd setting in the [experimental section](https://fly.io/docs/reference/configuration/#the-experimental-section) of the fly.toml file. | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ### [](#%5Fusing%5Ftigris%5Fas%5Fa%5Fpolicy%5Frepository)Using Tigris as a policy repository Cerbos `blob` driver can be used with any S3-compatible blob storage backend such as [Tigris](https://fly.io/docs/reference/tigris). Create a storage bucket on Tigris. Refer to for more information about creating storage buckets. ```bash flyctl storage create ``` Note down the credentials for accessing the bucket and save them as application secrets. ```bash flyctl apps create (1) flyctl secrets set --app= AWS_ACCESS_KEY_ID=tid_XXXXXX (2) flyctl secrets set --app= AWS_SECRET_ACCESS_KEY=tsec_XXXXXX (3) ``` | **1** | Your application name on Fly.io | | ----- | ------------------------------- | | **2** | Tigris key ID | | **3** | Tigris secret access key | Create a `fly.toml` file. ```toml app = '' (1) primary_region = '' (2) [build] image = 'ghcr.io/cerbos/cerbos:0.55.0' [experimental] cmd = [ 'server', '--set', 'storage.driver=blob', '--set', 'storage.blob.bucket=s3://?endpoint=fly.storage.tigris.dev®ion=auto', (3) '--set', 'storage.blob.downloadTimeout=30s', '--set', 'storage.blob.prefix=policies', '--set', 'storage.blob.updatePollInterval=15s', '--set', 'storage.blob.workDir=/policies' ] [[mounts]] source = 'policies' destination = '/policies' initial_size = '1GB' [[services]] protocol = '' internal_port = 3592 auto_stop_machines = true [[services.ports]] port = 3592 handlers = ['tls', 'http'] [[services.http_checks]] interval = '5s' timeout = '2s' grace_period = '5s' method = 'get' path = '/_cerbos/health' protocol = 'http' [[services]] protocol = '' internal_port = 3593 auto_stop_machines = true [[services.ports]] port = 3593 handlers = ['tls'] [services.ports.tls_options] alpn = ['h2'] [[vm]] memory = '1gb' cpu_kind = 'shared' cpus = 1 [metrics] port = 3592 path = "/_cerbos/metrics" ``` | **1** | The name of the [Fly App](https://fly.io/docs/apps) | | ----- | ----------------------------------------------------------------------------- | | **2** | Pick a Fly.io [region](https://fly.io/docs/reference/regions/#fly-io-regions) | | **3** | Storage bucket name | Deploy the app. ```bash flyctl deploy ``` ### [](#%5Fusing%5Flitefs%5Fas%5Fa%5Fpolicy%5Frepository)Using LiteFS as a policy repository Fly.io’s distributed SQLite storage layer [LiteFS](https://fly.io/docs/litefs) can be used for policy storage using Cerbos' `sqlite3` driver. Start by creating an app on Fly.io. ```bash flyctl apps create ``` Create a LiteFS configuration file named `litefs.yml`. ```yaml data: dir: "/var/lib/litefs" exec: - cmd: "/cerbos server --set=storage.driver=sqlite3 --set=storage.sqlite3.dsn=file:/litefs/db --set=server.adminAPI.enabled=true --set=server.adminAPI.adminCredentials.username=$CERBOS_ADMIN_USER --set=server.adminAPI.adminCredentials.passwordHash=$CERBOS_ADMIN_PASSWORD_HASH" exit-on-error: false fuse: dir: "/litefs" lease: advertise-url: "http://${FLY_ALLOC_ID}.vm.${FLY_APP_NAME}.internal:20202" candidate: ${FLY_REGION == PRIMARY_REGION} consul: url: "${FLY_CONSUL_URL}" key: "${FLY_APP_NAME}/primary" promote: true type: "consul" ``` | | Refer to [Configuring LiteFS](https://fly.io/docs/litefs/getting-started-docker/#configuring-litefs) documentation for other available configuration parameters. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Create a Dockerfile. ```Dockerfile FROM flyio/litefs:0.5 AS litefs FROM ghcr.io/cerbos/cerbos:0.55.0 AS cerbos FROM alpine:3.16 AS base RUN apk add fuse3 sqlite ADD litefs.yml /etc/litefs.yml COPY --from=cerbos /cerbos /cerbos COPY --from=litefs /usr/local/bin/litefs /usr/local/bin/litefs ENTRYPOINT ["litefs"] CMD ["mount"] ``` Create a `fly.toml` file to launch Cerbos. ```toml app = '' (1) primary_region = '' (2) [build] dockerfile = "Dockerfile" [mounts] source = "litefs" destination = "/var/lib/litefs" (3) [[services]] protocol = '' internal_port = 3592 [[services.ports]] port = 3592 handlers = ['tls', 'http'] [[services.http_checks]] interval = '5s' timeout = '2s' grace_period = '5s' method = 'get' path = '/_cerbos/health' protocol = 'http' [[services]] protocol = '' internal_port = 3593 [[services.ports]] port = 3593 handlers = ['tls'] [services.ports.tls_options] alpn = ['h2'] [[vm]] memory = '1gb' cpu_kind = 'shared' cpus = 1 [metrics] port = 3592 path = "/_cerbos/metrics" ``` | **1** | The name of the [Fly App](https://fly.io/docs/apps) | | ----- | ---------------------------------------------------------------------- | | **2** | Pick a [region](https://fly.io/docs/reference/regions/#fly-io-regions) | | **3** | Destination must be equal to the one specified in the litefs.yaml | Create secrets to hold Cerbos Admin API credentials. Refer to [password hash generation instructions](../configuration/server.html#password-hash) to learn how to generate the password hash. ```bash flyctl secrets set CERBOS_ADMIN_USER= flyctl secrets set CERBOS_ADMIN_PASSWORD_HASH= ``` Attach to Consul to manage LiteFS leases. ```bash flyctl consul attach ``` | | See [lease configuration](https://fly.io/docs/litefs/getting-started-fly/#lease-configuration) for more information about Consul leases on Fly.io. | | ----------------------------------------------------------------------------------------------------------------------------------------------------- | Finally, deploy Cerbos. ```bash flyctl deploy ``` You can interact with the Cerbos [Admin API](../api/admin%5Fapi.html) using one of the Cerbos SDKs or the [cerbosctl](../cli/cerbosctl.html) utility to manage the policies stored on LiteFS. List policies with cerbosctl ```bash cerbosctl \ --server=.fly.dev:3593 \ --username= \ --password= \ get rp ``` Put a policy or a directory consisting of multiple policies with cerbosctl ```bash cerbosctl \ --server=.fly.dev:3593 \ --username= \ --password= \ put policies -R \ policy_dir ``` | | [Cerbos Hub](https://docs.cerbos.dev/cerbos-hub/) securely distributes policy bundles to connected PDPs, eliminating per-instance polling and reload logic. | | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | Cerbos deployment patterns ==================== Cerbos can be deployed as a service or as a sidecar. Which mode to choose depends on your requirements. | | Cerbos PDP is a stateless system that makes decisions solely based on information passed to it by the calling application. This assumes that the communication link between the application and the PDP is secure and tamper-proof. We strongly recommend enabling TLS for the communication link regardless of the deployment model. | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#service)Service model ![Service model](_images/service_deployment.png) * Central policy decision point shared by a group of applications. * Cerbos can be upgraded independently from the applications — reducing maintenance overhead. * In a busy environment, careful capacity planning would be required to ensure that the central Cerbos endpoint does not become a bottleneck. ## [](#sidecar)Sidecar model ![Sidecar model](_images/sidecar_deployment.png) * Each application instance gets its own Cerbos instance — ensuring high performance and availability. * Upgrades to Cerbos would require a rolling update to all the application instances. * Policy updates could take slightly longer to propagate to all the individual application instances — resulting in a period where both the old and new policies are in effect at the same time. ## [](#daemonset)DaemonSet model ![DaemonSet model](_images/daemonset_deployment.png) * Each cluster node gets its own Cerbos instance — ensuring high performance and efficient resource usage. * Policy updates must roll out to nodes individually — resulting in a period where both the old and new policies are in effect at the same time. * When deployed as a daemonset the service `internalTrafficPolicy` defaults to `Local`. This causes all requests to the service to be forced to the local node for minimum latency. Upgrades to Cerbos could result in application seeing a short outage to the cerbos instance on their own node, client retries may be neccessary. If this is unacceptable you can set `service.internalTrafficPolicy` to `Cluster`. You may be able to improve availability via the `service.kubernetes.io/topology-mode: Auto` annotation. | | For a review of production deployment patterns, scaling characteristics, or operational practices, [speak to us](https://www.cerbos.dev/workshop?utm%5Fcampaign=brand%5Fcerbos&utm%5Fsource=documentation&utm%5Fmedium=text). [Cerbos Hub](https://docs.cerbos.dev/cerbos-hub/) provides managed policy distribution, audit log aggregation, and PDP monitoring. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#choosing)Choosing a deployment model | Service | Sidecar | DaemonSet | | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | Latency | Network hop to central service | Local (localhost/loopback) | Local (same node) | | Resource efficiency | Shared instance, lower total overhead | One instance per pod, higher total overhead | One instance per node, moderate overhead | | Scaling | Scale independently of applications | Scales with application pods | Scales with cluster nodes | | Upgrades | Independent of application deployments | Requires rolling update of application pods | Independent of application deployments | | Policy propagation | Updates propagate to each replica in the service group individually | Updates propagate to each sidecar individually | Updates propagate to each node individually | | Availability | Depends on how many replicas are in the service | High — each pod has its own instance | High — each node has its own instance | | Best for | Non-Kubernetes deployments such as bare metal or Kubernetes deployments where it’s preferable to manage and scale a single service rather than multiple sidecars. | Latency-sensitive applications, strict isolation requirements | Less resource consumption than sidecars (one per node compared to one per pod) with similar latency characteristics. | For most Kubernetes deployments, the **sidecar** model provides the best balance of performance and reliability. Each application pod gets its own Cerbos instance, eliminating network latency concerns and single points of failure. The **service** model is the most flexible method of deployment. It can be as simple as a single instance on a VM or as complicated as a globally-distributed and load-balanced service deployed to multiple data centres. The **DaemonSet** model is efficient for large deployments where running a sidecar per pod would be wasteful. It provides node-local performance without the overhead of one Cerbos instance per application pod. | | Cerbos is designed to handle high request volumes with low latency. In benchmarks, the network hop to a central service is typically sub-millisecond within a cluster. Start with the simplest model that meets your requirements. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Deploy Cerbos as DaemonSet ==================== You can use the [Cerbos Helm chart](../installation/helm.html) to deploy Cerbos as a daemonset inside your Kubernetes cluster by setting the Helm `type` value to `daemonset`. By default, the [internal traffic policy](#https://kubernetes.io/docs/concepts/services-networking/service-traffic-policy/) is set to `Local`. You can change this by setting `service.internalTrafficPolicy` explicitly. Refer to the [Helm chart instructions](../installation/helm.html) to learn more about using the Cerbos Helm chart. Deploy Cerbos as a service ==================== You can use the [Cerbos Helm chart](../installation/helm.html) to deploy Cerbos as a service inside your Kubernetes cluster. Refer to the [Helm chart instructions](../installation/helm.html) for more details. Deploy Cerbos as a sidecar ==================== The sidecar deployment model might be a preferrable option under the following circumstances: * You have a self-contained application that does not need to share policies with other applications in your environment. * You prefer to ship policy changes as application updates by bundling the two together. * You are concerned about network latency. Cerbos supports serving the API over a Unix domain socket. This allows your application container to securely communicate with the Cerbos service with no network overhead. Because the Cerbos server is only listening over a Unix domain socket, no other applications in your network will be able to communicate with it — thus providing secrecy as a bonus side effect. The following example illustrates a Kubernetes deployment with Cerbos as a sidecar. | | We are using [ghostunnel](https://github.com/ghostunnel/ghostunnel) as the application container for demonstration purposes only. In a real production deployment the Cerbos endpoint should not be exposed to the network. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ```yaml --- # Config map used to configure Cerbos. apiVersion: v1 kind: ConfigMap metadata: name: cerbos-sidecar-demo labels: app.kubernetes.io/name: cerbos-sidecar-demo app.kubernetes.io/component: cerbos app.kubernetes.io/version: "0.0.1" data: ".cerbos.yaml": |- server: # Configure Cerbos to listen on a Unix domain socket. httpListenAddr: "unix:/sock/cerbos.sock" storage: driver: disk disk: directory: /policies watchForChanges: false --- # Application deployment with Cerbos as a sidecar. # Note that in this example we are simply proxying requests received # by the main application (application container) to the Cerbos # sidecar (`cerbos` container) for demonstration purposes. In a real # production deployment the main application would not expose Cerbos # to the outside world at all. It would communicate with the Cerbos # sidecar privately to make policy decisions about the actions that # it is performing. # # Bonus: You can re-purpose this example to deploy Cerbos in an # environment that requires SPIFFE workload identities and/or # regular certificate rotation and access restrictions. See the # ghostunnel documentation at https://github.com/ghostunnel/ghostunnel # for more information. apiVersion: apps/v1 kind: Deployment metadata: name: cerbos-sidecar-demo labels: app.kubernetes.io/name: cerbos-sidecar-demo app.kubernetes.io/component: cerbos-sidecar-demo app.kubernetes.io/version: "0.0.1" spec: replicas: 1 selector: matchLabels: app.kubernetes.io/name: cerbos-sidecar-demo app.kubernetes.io/component: cerbos-sidecar-demo template: metadata: labels: app.kubernetes.io/name: cerbos-sidecar-demo app.kubernetes.io/component: cerbos-sidecar-demo spec: containers: ######################################################################## # Application container. Replace with your own application definition. # ######################################################################## - name: application image: "ghostunnel/ghostunnel" imagePullPolicy: IfNotPresent args: - "server" - "--listen=:3592" - "--target=unix:/sock/cerbos.sock" - "--cert=/certs/tls.crt" - "--key=/certs/tls.key" - "--disable-authentication" ports: - name: http containerPort: 3592 livenessProbe: httpGet: path: /_cerbos/health port: http scheme: HTTPS readinessProbe: httpGet: path: /_cerbos/health port: http scheme: HTTPS volumeMounts: # Mount the shared volume containing the socket - name: sock mountPath: /sock - name: certs mountPath: /certs ################## # Cerbos sidecar # ################## - name: cerbos image: "ghcr.io/cerbos/cerbos:0.55.0" imagePullPolicy: IfNotPresent args: - "server" - "--config=/config/.cerbos.yaml" - "--log-level=INFO" volumeMounts: # Mount the shared volume containing the socket - name: sock mountPath: /sock - name: config mountPath: /config readOnly: true - name: policies mountPath: /policies volumes: # Shared volume containing the socket. - name: sock emptyDir: {} - name: config configMap: name: cerbos-sidecar-demo - name: certs secret: secretName: cerbos-sidecar-demo - name: policies emptyDir: {} --- # Use cert-manager to issue a certificate to the application. apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: cerbos-sidecar-demo labels: app.kubernetes.io/name: cerbos-sidecar-demo app.kubernetes.io/component: cerbos-sidecar-demo app.kubernetes.io/version: "0.0.1" spec: isCA: true secretName: cerbos-sidecar-demo dnsNames: - cerbos-sidecar-demo.default.svc.cluster.local - cerbos-sidecar-demo.default.svc - cerbos-sidecar-demo.default - cerbos-sidecar-demo issuerRef: name: selfsigned-cluster-issuer kind: ClusterIssuer group: cert-manager.io ``` Deploy Cerbos to Serverless/FaaS environments ==================== ## [](#aws-lambda)AWS Lambda | | Cerbos is a CPU-bound application. On AWS Lambda environments CPU is not individually configurable and is determined as a function of allocated memory (see [Configure Lambda function memory](https://docs.aws.amazon.com/lambda/latest/dg/configuration-memory.html)). If you’re experiencing slow response times from Cerbos, increase the memory allocation. Even though neither Cerbos nor your code requires a lot of memory, the architecture of AWS Lambda forces this counter-intuitive tuning advice. Because each application is different, we can’t provide any recommendations. Monitor your application and tune the memory allocation until the performance is satisfactory for your needs. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Cerbos provides two deployment options for AWS Lambda: ### [](#%5Fcerbos%5Flambda%5Ffunction)Cerbos Lambda Function Deploy Cerbos as a standalone AWS Lambda function that handles authorization requests directly. This deployment pattern is suitable for centralized authorization services. Download the appropriate `cerbos_aws_lambda_func` tarball from the [releases page](https://github.com/cerbos/cerbos/releases/tag/v0.55.0). It contains a specially-built binary named `bootstrap` that can be deployed as either a Zip package or a container image. Deploying it as a zip package requires creating a zip file with the `bootstrap` binary at the root. See [Deploy Go Lambda functions with .zip file archives](https://docs.aws.amazon.com/lambda/latest/dg/golang-package.html) for more information. Lambda Function deployment with a zip package ```shell mkdir -p dist sam deploy --template sam.yml --stack-name ${CERBOS_STACK_NAME:-Cerbos} --resolve-s3 \ --capabilities CAPABILITY_IAM --no-confirm-changeset --no-fail-on-empty-changeset ``` SAM template for Lambda Function deployment with a zip package ```yaml --- AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: Cerbos Lambda Function Globals: Function: Timeout: 5 Resources: CerbosFunction: Type: AWS::Serverless::Function Properties: Runtime: provided.al2 CodeUri: dist/ Handler: bootstrap Architectures: - arm64 MemorySize: 1024 Events: CheckResources: Type: HttpApi Properties: Path: /api/check/resources Method: POST PlanResources: Type: HttpApi Properties: Path: /api/plan/resources Method: POST HealthCheck: Type: HttpApi Properties: Path: / Method: GET Environment: Variables: CERBOS_LOG_LEVEL: debug XDG_CACHE_HOME: /tmp CERBOS_CONFIG: /.cerbos.yaml Outputs: CerbosFunctionAPI: Description: "API Gateway endpoint URL for Cerbos Function" Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com" CerbosFunction: Description: "Cerbos Lambda Function ARN" Value: !GetAtt CerbosFunction.Arn CerbosFunctionIamRole: Description: "IAM Role created for the Cerbos Lambda function" Value: !GetAtt CerbosFunctionRole.Arn ``` To deploy as a container, build an image using a Dockerfile similar to the following: ```dockerfile FROM public.ecr.aws/lambda/provided:al2023 COPY bootstrap /bootstrap ENTRYPOINT [ "/bootstrap" ] ``` See [Deploy Go Lambda functions with container images](https://docs.aws.amazon.com/lambda/latest/dg/go-image.html) for more information. ### [](#%5Fcerbos%5Flambda%5Fextension%5Flayer)Cerbos Lambda Extension (Layer) Lambda function deployed as a Zip package can have up to five layers. Primary use case for [layers](https://docs.aws.amazon.com/lambda/latest/dg/chapter-layers.html) is managing dependencies and configurations. Additionally, a layer can be used to deploy a Lambda function [extension](https://docs.aws.amazon.com/lambda/latest/dg/lambda-extensions.html). Deploying Cerbos PDP as a Lambda extension allows it to run on the same host with your application function. This deployment pattern is similar to a sidecar deployment. Download the appropriate `cerbos_aws_lamba_ext` tarball from the [releases page](https://github.com/cerbos/cerbos/releases/tag/v0.55.0). It contains a specially-built `cerbosext` binary that can be deployed as a Zip layer. The (extension) layer then can be attached to your AWS Lambda function, if it is deployed as a Zip package. See [adding layers](https://docs.aws.amazon.com/lambda/latest/dg/adding-layers.html) for more information. Lambda Extension deployment ```shell mkdir -p layer/extensions mv cerbosext layer/extensions/ sam deploy --template sam.yml --stack-name ${CERBOS_STACK_NAME:-Cerbos} --resolve-s3 \ --capabilities CAPABILITY_IAM --no-confirm-changeset --no-fail-on-empty-changeset ``` SAM template for Lambda Extension deployment ```yaml --- AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: Cerbos server Globals: Function: Timeout: 5 Resources: CerbosExtensionLayer: Type: AWS::Serverless::LayerVersion Properties: LayerName: cerbos-extension Description: Cerbos extension layer ContentUri: ./layer (1) CompatibleRuntimes: - provided.al2 - provided.al2023 CompatibleArchitectures: - arm64 Outputs: CerbosExtensionLayerArn: Description: "Cerbos Extension Layer ARN" Value: !Ref CerbosExtensionLayer ``` | **1** | Layer is a directory containing the cerbosext binary | | ----- | ---------------------------------------------------- | ### [](#%5Fconfiguration)Configuration | | Cerbos Lambda extension overrides server.httpListenAddr and server.grpcListenAddr to "unix:/tmp/cerbos.http.sock" and "unix:/tmp/cerbos.grpc.sock" respectively. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | The Cerbos function/extension can be configured with environment variables. ```yaml CERBOS_LOG_LEVEL: info XDG_CACHE_HOME: /tmp CERBOS_CONFIG: "..." (1) ``` | **1** | Path to the Cerbos configuration file | | ----- | ------------------------------------- | Set the following environment variables to connect to [Cerbos Hub](https://www.cerbos.dev/product-cerbos-hub). ```yaml CERBOS_HUB_DEPLOYMENT_ID: "..." CERBOS_HUB_CLIENT_ID: "..." CERBOS_HUB_CLIENT_SECRET: ".." ``` ### [](#%5Fcomparison%5Ffunction%5Fvs%5Fextension)Comparison: Function vs Extension | Aspect | Lambda Function | Lambda Extension | | ---------------------- | ----------------------------- | --------------------------------------------- | | **Deployment Pattern** | Centralized service | Sidecar pattern | | **Resource Sharing** | Dedicated Lambda execution | Shares execution with application function | | **Cold Start** | Independent cold start | Shared cold start with application | | **Network Overhead** | API Gateway + Lambda runtime | Direct process communication | | **Use Case** | Central authorization service | Embedded authorization for specific functions | | **Scaling** | Independent scaling | Scales with application function | Both deployment options use AWS SAM (Serverless Application Model) templates for easy deployment and include example configurations for policies, logging, and API Gateway integration. Deploy Cerbos as a systemd service ==================== The [Cerbos Linux packages](../installation/binary.html#linux-packages) will automatically create a systemd service during installation. If you are using the tarballs to create a custom installation, you can modify the following sample systemd service definition to match your requirements. ```toml [Unit] Description=Cerbos Policy Decision Point [Service] ExecStart=/usr/local/bin/cerbos server --config=/etc/cerbos.yaml ProtectSystem=full ProtectHome=true PrivateUsers=true PrivateTmp=true DynamicUser=yes [Install] WantedBy=multi-user.target ``` Refer to [systemd documentation](https://www.freedesktop.org/software/systemd/man/systemd.exec.html) for more information about available configuration options. Why we built Cerbos this way ==================== Welcome! The purpose of this section is to give some insight into how decisions were made when designing and building Cerbos, for the more _curious_ of our users. * [Why Cerbos PDP runs as a separate process](why%5Fcerbos%5Fruns%5Fas%5Fa%5Fseparate%5Fprocess.html) Why Cerbos runs as a separate process ==================== If you’re used to traditional authorization approaches, you’d be surprised to find that Cerbos PDP is not a library that you can embed into your application. Instead, Cerbos PDP is designed to be run as a sidecar or a service alongside your application. There are several reasons why we have chosen this approach. To provide a bit of background, let’s consider how modern software development works in the era of cloud native computing. Nowadays, the trend is towards microservice architectures where system functionality is split between multiple services that are fairly independent of each other. They are probably owned by different teams within the organization and even developed using different programming languages and tools. Automated CI/CD pipelines deploy new versions of these services many times a day. ![organization](_images/organisation.png) In these dynamic, polyglot environments, the emergent pattern for providing cross-cutting concerns such as service discovery, resiliency, observability and security in a standardized way is through the use of sidecars or other microservices. Frameworks such as [Dapr](https://dapr.io) and service meshes such as [Istio](https://istio.io) and [Linkerd](https://linkerd.io) are examples of software that employ this pattern. Authorization is one of those cross-cutting concerns that needs to be standardized and centrally managed across the organization. If authorization rules for the same resource are even slightly different between two services, then that creates a security issue. In a polyglot environment, the implementation of access rules would be duplicated between each programming language. This is a waste of effort and an inevitable source of inconsistencies and bugs due to how programmers interpret the specifications or how the particular programming language deals with certain data types or special cases. Changing access rules for the whole organization requires coordinated effort to develop, test and roll out those changes across the whole fleet. Debugging authorization problems in such an environment is quite difficult because no one has overall visibility of the whole system. The access logic is hidden away in code in multiple repositories. Unless the developers have been extremely disciplined, the quality of debugging aids such as traces, audit trails and tests would vary wildly as well. ## [](#%5Fenter%5Fcerbos%5Fpdp)Enter Cerbos PDP…​ Cerbos PDP is designed to address most of the above problems: * Access policies are human-readable and stored in a central repository so that all stakeholders have visibility over the security rules implemented in their organization. * Logs, traces, metrics and audit trails are available out of the box and there are supplementary tools such as a policy testing framework, linter and a REPL for debugging issues. * Cerbos PDP automatically detects changes to policies and updates itself on the fly. This makes rollout of access policy changes easy and almost instantaneous. For most changes, this means that the dependent services don’t need to have their code updated and rolled out to production. It saves development time and deployment headaches. * Offering Cerbos PDP as a decoupled API allows it to be used by any application or service written in any language while providing a consistent experience across the board. Cerbos PDP facilitates sharing access control logic across different services and applications and gets rid of inherent code duplication, inconsistent implementations, version drift and maintenance burden. By not having to worry about wrapping and shipping Cerbos PDP features into language specific, embeddable libraries, we can focus our time and energy into optimizing the product and building new features using a smaller set of libraries and utilities provided by the language of our choice. We can test these features much more thoroughly because we have full control over all the integration points. We don’t have to be concerned about integrating or being compatible with an almost unlimited set of libraries and frameworks available for every programming language. And we don’t have to expend effort figuring out how to share common code across different languages, fight with language quirks and performance hotspots like foreign function interfaces and concurrency primitives. Glossary of Cerbos terms ==================== ACTION Any application-defined operation that could be performed on a `**RESOURCE**`. Actions could be coarse-grained like `create`, `update`, `delete`, `view` or fine-grained like `view:public`, `update:invoice_amt`. What actions are possible is determined by the application developers and they can use [Cerbos policies](../policies/index.html) to define the rules that must be satisfied in order for a given `**PRINCIPAL**` to perform one of those actions on a `**RESOURCE INSTANCE**`. ATTRIBUTE A piece of information about a `**PRINCIPAL**` or a `**RESOURCE INSTANCE**` that is useful for making an access decision. Cerbos PDP is stateless and has no access to your application data. In order to make access decisions, Cerbos PDP needs to know relevant information about the users and the objects they are trying to access. This information is sent as `attributes` in the Cerbos request. For example, if you want to restrict users from a particular geography to access only objects from that same geography, you might define a Cerbos rule condition like `request.resource.attr.geography == request.principal.attr.geography`. Then, in the Cerbos API request, you must send the `geography` attribute (as determined by your application) for both the principal and the resource instance. CONDITION Cerbos policy rules can make dynamic, context-aware decisions by evaluating conditional logic against the `**ATTRIBUTES**` sent through the API request. See [Conditions](../policies/conditions.html) for details. DERIVED ROLE Most applications have a statically defined set of roles such as `admin`, `writer`, `employee` and so on. Cerbos derived roles are a feature in which these static roles can be dynamically augmented with context-awareness. For example, someone with the `employee` role can be augmented to `us_employee` by checking whether their location is in the USA. Cerbos policies can then be written to target the `us_employee` derived role instead of the `employee` role — which removes repetition of logic across policy files. See [Derived roles](../policies/derived%5Froles.html) for details. PDP Policy decision point. Essentially, where policies are executed and decisions are made. When you start a Cerbos PDP server through one of the distribution artefacts (binary, container, Helm chart), you start a PDP. PRINCIPAL A human or an automated process that wants to perform one or more `**ACTIONS**` on one or more `**RESOURCE INSTANCES**`. Typically called a "user" in most settings but Cerbos uses the term `Principal` to avoid any ambiguity about whether the user is human or not. You can create [principal policies](../policies/principal%5Fpolicies.html) to create exceptions for particular users. RESOURCE A kind or a category of application objects with similar characteristics. The concept is very similar to a `class` in object-oriented programming. For example, in an inventory system, `Invoice` is a resource. Your system might have thousands or millions of invoices (`**RESOURCE INSTANCES**`: similar to `objects` in object-oriented programming) but there would only be a single Cerbos [resource policy](../policies/resource%5Fpolicies.html) for `Invoice` which encodes all the access rules. | | Sometimes the term resource is used to refer to a **RESOURCE INSTANCE** when the meaning is obvious from the context (the [API request fields](../api/index.html) are a good example of this). | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | RESOURCE INSTANCE A specific item of a `**RESOURCE**` kind. If a `**RESOURCE**` is a `class`, a `**RESOURCE INSTANCE**` is an `object` of that class. For example, an invoice that was issued to "Acme Corp." with ID "I23456" is a `**RESOURCE INSTANCE**`. When making access decisions using Cerbos, you need to send information about the _resource instances_ to the Cerbos `CheckResources` API endpoint. The Cerbos `**PDP**` would then use the appropriate resource policy (determined by the `kind` specified in the resource instance) to process the information and make an access decision. Policy authoring ==================== ## [](#%5Ftips%5Ffor%5Fworking%5Fwith%5Fpolicies)Tips for working with policies * Policies can be in either YAML or JSON formats. Accepted file extensions are `.yml`, `.yaml` or `.json`. All other extensions are ignored. * The JSON schemas for Cerbos files are available at: * Policy * `` * [Test suite](compile.html#testing) * `` * [Principal test fixtures](compile.html#fixtures) * `` * [Resources test fixtures](compile.html#fixtures) * `` * [Auxiliary data test fixtures](compile.html#fixtures) * `` * If you prefer to always use the latest version, they can be accessed at: * `` * `` * `` * `` * `` ## [](#%5Fpolicy%5Fstructure)Policy structure * The policy header is common for all policy types: * `apiVersion`: Required. Must be `api.cerbos.dev/v1`. * `description`: Optional. Description of the policy. * `disabled`: Optional. Set to `true` to make the Cerbos engine ignore this policy file. * `metadata.sourceFile`: Optional. Set to the source of the policy data for auditing purposes. * `metadata.annotations`: Optional. Key-value pairs of strings holding free-form data for auditing purposes. * Resource names, actions, and principal names can be hierarchical. Use `:` as the delimiter. For example: `app:component:resource`. * Wildcard matches are allowed on certain fields. Wildcards respect the hierarchy delimiter `:`. * [Scoped policies](scoped%5Fpolicies.html) (optional) are handy for use cases like multi-tenancy where you may want to override particular rules for some tenants. * See [Conditions](conditions.html) to learn how to write conditions in policy rules. * See [Schemas](schemas.html) to learn how you can define schemas for validating requests. * See [Best practices](best%5Fpractices.html) to check out a growing collection of snippets detailing the optimal way to write policies. ## [](#%5Fediting%5Fpolicies)Editing policies The quickest and the easiest way to get familiar with Cerbos policies is to use the [online playground](https://play.cerbos.dev). It provides an IDE-like experience with an interactive editor, examples, code snippets, test cases and other useful utilities to help you design policies. ### [](#%5Feditor%5Fconfigurations)Editor configurations Editors with support for the [Language Server Protocol (LSP)](https://microsoft.github.io/language-server-protocol/) can make use of the [YAML language server](https://github.com/redhat-developer/yaml-language-server) implementation when working with Cerbos policies. Simply add the following line at the beginning of your policy file to get context-sensitive code suggestions and validation messages from the editor. ```yaml # yaml-language-server: $schema=https://api.cerbos.dev/latest/cerbos/policy/v1/Policy.schema.json ``` The same method can be used for [tests](compile.html#testing): ```yaml # yaml-language-server: $schema=https://api.cerbos.dev/latest/cerbos/policy/v1/TestSuite.schema.json ``` [Resource fixtures](compile.html#fixtures) for tests: ```yaml # yaml-language-server: $schema=https://api.cerbos.dev/latest/cerbos/policy/v1/TestFixture/Resources.schema.json ``` [Principal fixtures](compile.html#fixtures) for tests: ```yaml # yaml-language-server: $schema=https://api.cerbos.dev/latest/cerbos/policy/v1/TestFixture/Principals.schema.json ``` [Auxiliary data fixtures](compile.html#fixtures) for tests: ```yaml # yaml-language-server: $schema=https://api.cerbos.dev/latest/cerbos/policy/v1/TestFixture/AuxData.schema.json ``` YAML language server also supports per-directory settings. If all your Cerbos policies are contained in a specific directory, you can configure the editor to always use the correct schema for the YAML files in that directory. Refer to the [YAML language server documentation](https://github.com/redhat-developer/yaml-language-server#language-server-settings=) for more information. Example: Apply the schema to all files in the /cerbos directory ```yaml yaml.schemas: { "https://api.cerbos.dev/latest/cerbos/policy/v1/Policy.schema.json": "/cerbos/*", "https://api.cerbos.dev/latest/cerbos/policy/v1/TestSuite.schema.json": "/cerbos/**/*_test.yaml", "https://api.cerbos.dev/latest/cerbos/policy/v1/TestFixture/Resources.schema.json": "/cerbos/**/testdata/resources.yaml", "https://api.cerbos.dev/latest/cerbos/policy/v1/TestFixture/Principals.schema.json": "/cerbos/**/testdata/principals.yaml", "https://api.cerbos.dev/latest/cerbos/policy/v1/TestFixture/AuxData.schema.json": "/cerbos/**/testdata/auxdata.yaml" } ``` JSON files can specify the schema using the `$schema` top-level property. ```json "$schema": "https://api.cerbos.dev/latest/cerbos/policy/v1/Policy.schema.json", ``` #### [](#%5Fneovim)Neovim Refer to your plugin manager documentation to figure out how to install [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig/tree/master) and [configure the yaml-language-server](https://github.com/neovim/nvim-lspconfig/blob/master/doc/server%5Fconfigurations.md#yamlls). Plugins such as [mason-lspconfig](https://github.com/williamboman/mason-lspconfig.nvim) can automatically download and install language servers as well. The following is an example of using [lazy.nvim](https://github.com/folke/lazy.nvim) and [mason.nvim](https://github.com/williamboman/mason.nvim) to install and configure yaml-language-server. It follows the [recommended way of configuring lazy plugins](https://github.com/folke/lazy.nvim#-structuring-your-plugins). \~/.config/nvim/lua/plugins/lspconfig.lua ```lua return { { "neovim/nvim-lspconfig", dependencies = { { "williamboman/mason.nvim", }, { "williamboman/mason-lspconfig.nvim", opts = { ensure_installed = { "yamlls" }, }, }, }, opts = { servers = { yamlls = { settings = { yaml = { schemas = { "https://api.cerbos.dev/latest/cerbos/policy/v1/Policy.schema.json": "/cerbos/*", "https://api.cerbos.dev/latest/cerbos/policy/v1/TestSuite.schema.json": "/cerbos/**/*_test.yaml", "https://api.cerbos.dev/latest/cerbos/policy/v1/TestFixture/Resources.schema.json": "/cerbos/**/testdata/resources.yaml", "https://api.cerbos.dev/latest/cerbos/policy/v1/TestFixture/Principals.schema.json": "/cerbos/**/testdata/principals.yaml", "https://api.cerbos.dev/latest/cerbos/policy/v1/TestFixture/AuxData.schema.json": "/cerbos/**/testdata/auxdata.yaml" }, }, }, }, }, }, } } ``` #### [](#%5Fjetbrains%5Fides)JetBrains IDEs Navigate to the **Preferences** **Languages & Frameworks** **Schemas and DTDs** **JSON Schema Mappings** in JetBrains IDE of your choice. Add an entry with the following configuration: Name: Cerbos Schema file or URL: https://api.cerbos.dev/latest/cerbos/policy/v1/Policy.schema.json Schema version: JSON Schema Version 7 File path pattern: cerbos/* ![JetBrains JSON Schema Mappings menu](_images/jetbrains-menu.png) | | In the example above, the schema is applied to all files in the cerbos directory. | | ------------------------------------------------------------------------------------ | #### [](#%5Fvisual%5Fstudio%5Fcode)Visual Studio Code If you are new to Visual Studio Code, refer to the[documentation](https://code.visualstudio.com/docs/getstarted/settings) for more information about how to change settings. Install the YAML language server extension from After the extension is installed, hit Ctrl+, or **File** **Preferences** **Settings** to edit settings. Expand **Extensions** **YAML**, click `Edit in settings.json` under `Yaml: Schemas`. and add the following snippet: ```json { "yaml.schemas": { "https://api.cerbos.dev/latest/cerbos/policy/v1/Policy.schema.json": "cerbos/*", "https://api.cerbos.dev/latest/cerbos/policy/v1/TestSuite.schema.json": "/cerbos/**/*_test.yaml", "https://api.cerbos.dev/latest/cerbos/policy/v1/TestFixture/Resources.schema.json": "/cerbos/**/testdata/resources.yaml", "https://api.cerbos.dev/latest/cerbos/policy/v1/TestFixture/Principals.schema.json": "/cerbos/**/testdata/principals.yaml", "https://api.cerbos.dev/latest/cerbos/policy/v1/TestFixture/AuxData.schema.json": "/cerbos/**/testdata/auxdata.yaml" } } ``` | | In the example above, the schema is applied to all files in the cerbos directory. | | ------------------------------------------------------------------------------------ | | | For a review of policy design against a specific use case, [book a policy review session with Cerbos](https://www.cerbos.dev/workshop?utm%5Fcampaign=brand%5Fcerbos&utm%5Fsource=documentation&utm%5Fmedium=text). | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Best practices and recipes ==================== A collection of tips and code snippets designed to help you write cleaner, more optimised Cerbos policies. ## [](#%5Fmodelling%5Fpolicies)Modelling policies With Cerbos, access rules are always resource-oriented and the policies you write map to these resources within your system. A _resource_ can be anything, and the way you model your policies is up you — you can achieve the same logical outcome in numerous ways; action-led, role-led, attribute-led, or with combinations thereof. That said, some patterns will lend themselves more naturally to certain scenarios — let’s take a look at some different approaches. Consider this business model: | _Actions_ | _Roles_ | | | | | | --------- | ----------- | ----------- | ---- | --- | - | | IT\_ADMIN | JR\_MANAGER | SR\_MANAGER | USER | CFO | | | run | x | x | x | | | | view | x | x | x | x | x | | edit | x | x | | | | | save | x | x | | | | | share | x | x | x | | | Representing this as a resource policy could be achieved in a variety of ways. Let’s take a look at each: ### [](#%5Faction%5Fled)Action-led Here, we focus on an action, and list all the roles that can perform that action: ```yaml # Principals in the following three roles can perform the `run` action - actions: - "run" effect: EFFECT_ALLOW roles: - JR_MANAGER - SR_MANAGER - CFO # All principals can perform the `view` action - actions: - "view" effect: EFFECT_ALLOW roles: - ["*"] ``` This approach might be suitable if any of the following apply to your system: * Your roles are "similar" in what they can do like `JR_MANAGER` and `SR_MANAGER`; it’s likely that `JR_MANAGER` will have a subset of the permissions of `SR_MANAGER`. There will of course be duplication in either direction, but it’s often easier to reason about this from an action perspective. * You have "high-risk" actions — you want to be able to tell at a glance which roles have access to a particular action. The act of explicitly listing roles per action makes it much more difficult to accidentally give unwanted permissions to the wrong user. * You have a relatively high number of roles to a low number of actions. ### [](#%5Frole%5Fled)Role-led Alternatively, we can focus on a role, and list all the actions the role can perform: ```yaml # These three actions can be performed by principals in the `JR_MANAGER` role - actions: - "run" - "view" - "share" effect: EFFECT_ALLOW roles: - JR_MANAGER ``` You might opt for a role-led approach if: * You have distinct roles where it’s rare for your roles to share common actions. * You have a relatively low number of roles to a high number of actions. ### [](#%5Fhybrid)Hybrid Perhaps we want to use a combination of the two: ```yaml # Principals in the `SR_MANAGER` or `CFO` roles can perform all actions - actions: - "*" effect: EFFECT_ALLOW roles: - SR_MANAGER - CFO ``` This might apply if your scenario doesn’t strictly fall into one of the previous two sections; individually, or at all. ### [](#%5Fblanket%5Fallow%5Fgranular%5Fdeny)Blanket allow, granular deny We can opt to explicitly state which actions a user **cannot** do: ```yaml # Principals in the `JR_MANAGER` role can perform all actions, other than `edit` and `save` - actions: - "*" effect: EFFECT_ALLOW roles: - "JR_MANAGER" - actions: - "edit" - "save" effect: EFFECT_DENY roles: - "JR_MANAGER" ``` This would suit scenarios where a principal can perform _nearly_ every action, and you want to explicitly list disallowed actions. ### [](#%5Fattribute%5Fled)Attribute-led Consider the following hypothetical scenario: An organization models its resources as specific _data sets_. Each data set is unique, as are the principals trying to access them. The organization uses JWTs extensively to manage and transmit identity/contextual information. The resource policies map 1:1 to each data set, and access is governed by arbitrary information (in this case, passed within the JWT). Given the dynamic nature of audiences, it’s not practical to enumerate all roles that have access. What we could do instead is to globally allow all roles and actions and then determine access based on attributes passed in the JWT. Take a look at the following example policy: ```yaml apiVersion: api.cerbos.dev/v1 resourcePolicy: resource: "data_set" version: default rules: - actions: ["*"] roles: ["*"] effect: EFFECT_ALLOW condition: match: all: of: - expr: has(request.auxData.jwt.aud) - expr: > "my.custom.audience" in request.auxData.jwt.aud ``` In the above, we blanket-allow all actions and roles, but specifically rely on the `aud` key parsed from the JWT to determine access. ## [](#%5Fadding%5Fself%5Fservice%5Fcustom%5Froles)Adding self-service custom roles Imagine this scenario: you’re an admin in a multi-tenant system, and you want a method by which you can copy an existing role, and then select which permissions/actions to enable or disable for each. There are two ways of approaching this: ### [](#%5Fstatic%5Fpolicies%5Fdynamic%5Fcontext)Static Policies / Dynamic Context This is the _idiomatic_ way of solving this use-case in Cerbos. In the vast majority of cases, it is possible to have the policies statically defined and to pass in dynamic context as attributes of a principal. This dynamic context can be any arbitrary data such as the principal’s location, age, or specific roles it has within the context of an organizational unit (a department, a tenant or a project, for example). This contextual data would be retrieved at request time from another service or a data store. Let’s look at an example. Here is a resource policy for a resource of type `"workspace"`: workspace.yaml ```yaml apiVersion: "api.cerbos.dev/v1" resourcePolicy: version: "default" resource: "workspace" rules: - actions: - workspace:view - pii:view effect: EFFECT_ALLOW roles: - USER condition: match: expr: P.attr.workspaces[R.id].role == "OWNER" ``` Notice how the condition relies on context passed in within the `P.attr.workspaces` map, with the key being the resource ID, and the value being a predefined value `"OWNER"`. We can grant access to a principal with the `USER` role, by constructing the following request payload: * cURL * .NET * Go * Java * JS * PHP * Python * Ruby * Rust ```shell cat <() { { "workspaceA", AttributeValue.MapValue(new Dictionary() { {"role", AttributeValue.StringValue("OWNER")} }) }, { "workspaceB", AttributeValue.MapValue(new Dictionary() { {"role", AttributeValue.StringValue("MEMBER")} }) } })), ResourceAction.NewInstance("workspace", "workspaceA") .WithActions(actions), ResourceAction.NewInstance("workspace", "workspaceB") .WithActions(actions) ); foreach (string n in new string[] { "workspaceA", "workspaceB" }) { var r = result.Find(n); Console.Write(String.Format("\nResource: {0}\n", n)); foreach (var i in r.GetAll()) { String action = i.Key; Boolean isAllowed = i.Value; Console.Write(String.Format("\t{0} -> {1}\n", action, isAllowed ? "EFFECT_ALLOW" : "EFFECT_DENY")); } } } } ``` ```go package main import ( "context" "log" "github.com/cerbos/cerbos-sdk-go/cerbos" ) func main() { c, err := cerbos.New("localhost:3593", cerbos.WithPlaintext()) if err != nil { log.Fatalf("Failed to create client: %v", err) } principal := cerbos.NewPrincipal("123", "USER") // We use map[string]any as strictly typed nested maps aren't supported principal.WithAttr("workspaces", map[string]map[string]any{ "workspaceA": { "role": "OWNER", }, "workspaceB": { "role": "MEMBER", }, }) kind := "workspace" actions := []string{"workspace:view", "pii:view"} batch := cerbos.NewResourceBatch() batch.Add(cerbos.NewResource(kind, "workspaceA"), actions...) batch.Add(cerbos.NewResource(kind, "workspaceB"), actions...) resp, err := c.CheckResources(context.Background(), principal, batch) if err != nil { log.Fatalf("Failed to check resources: %v", err) } log.Printf("%v", resp) } ``` ```java package demo; import static dev.cerbos.sdk.builders.AttributeValue.mapValue; import static dev.cerbos.sdk.builders.AttributeValue.stringValue; import java.util.Map; import dev.cerbos.sdk.CerbosBlockingClient; import dev.cerbos.sdk.CerbosClientBuilder; import dev.cerbos.sdk.CheckResult; import dev.cerbos.sdk.builders.Principal; import dev.cerbos.sdk.builders.ResourceAction; public class App { public static void main(String[] args) throws CerbosClientBuilder.InvalidClientConfigurationException { CerbosBlockingClient client=new CerbosClientBuilder("localhost:3593").withPlaintext().buildBlockingClient(); for (String n : new String[]{"workspaceA", "workspaceB"}) { CheckResult cr = client.batch( Principal.newInstance("123", "USER") .withAttribute("workspaces", mapValue(Map.of( "workspaceA", mapValue(Map.of( "role", stringValue("OWNER") )), "workspaceB", mapValue(Map.of( "role", stringValue("MEMBER") )) ))) ) .addResources( ResourceAction.newInstance("workspace","workspaceA") .withActions("workspace:view", "pii:view"), ResourceAction.newInstance("workspace","workspaceB") .withActions("workspace:view", "pii:view") ) .check().find(n).orElse(null); if (cr != null) { System.out.printf("\nResource: %s\n", n); cr.getAll().forEach((action, allowed) -> { System.out.printf("\t%s -> %s\n", action, allowed ? "EFFECT_ALLOW" : "EFFECT_DENY"); }); } } } } ``` ```javascript const { GRPC: Cerbos } = require("@cerbos/grpc"); const cerbos = new Cerbos("localhost:3593", { tls: false }); (async() => { const kind = "workspace"; const actions = ["workspace:view", "pii:view"]; const cerbosPayload = { principal: { id: "123", roles: ["USER"], attributes: { workspaces: { workspaceA: { role: "OWNER", }, workspaceB: { role: "MEMBER", } }, }, }, resources: [ { resource: { kind: kind, id: "workspaceA", }, actions: actions, }, { resource: { kind: kind, id: "workspaceB", }, actions: actions, }, ], }; const decision = await cerbos.checkResources(cerbosPayload); console.log(decision.results) })(); ``` ```php build(); $principal = Principal::newInstance("123") ->withRole("USER") ->withAttribute("workspaces", [ "workspaceA" => [ "role" => "OWNER" ], "workspaceB" => [ "role" => "MEMBER" ] ]); $type = "workspace"; $resourceAction1 = ResourceAction::newInstance($type, "workspaceA") ->withAction("workspace:view") ->withAction("pii:view"); $resourceAction2 = ResourceAction::newInstance($type, "workspaceB") ->withAction("workspace:view") ->withAction("pii:view"); $checkResourcesResult = $client->checkResources($principal, array($resourceAction1, $resourceAction2), null, null); echo json_encode($checkResourcesResult, JSON_PRETTY_PRINT); ?> ``` ```python import json from cerbos.sdk.client import CerbosClient from cerbos.sdk.model import Principal, Resource, ResourceAction, ResourceList from fastapi import HTTPException, status principal = Principal( "123", roles=["USER"], attr={ "workspaces": { "workspaceA": { "role": "OWNER", }, "workspaceB": { "role": "MEMBER", } } } ) actions = ["workspace:view", "pii:view"] resource_list = ResourceList( resources=[ ResourceAction( Resource( "workspaceA", "workspace", ), actions=actions, ), ResourceAction( Resource( "workspaceB", "workspace", ), actions=actions, ), ], ) with CerbosClient(host="http://localhost:3592") as c: try: resp = c.check_resources(principal=principal, resources=resource_list) resp.raise_if_failed() except Exception: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Unauthorized" ) print(json.dumps(resp.to_dict(), sort_keys=False, indent=4)) ``` ```ruby # frozen_string_literal: true require "cerbos" require "json" client = Cerbos::Client.new("localhost:3593", tls: false) kind = "workspace" actions = ["workspace:view", "pii:view"] r1 = { kind: kind, id: "workspaceA" } r2 = { kind: kind, id: "workspaceB" } decision = client.check_resources( principal: { id: "123", roles: ["USER"], attributes: { workspaces: { workspaceA: { role: "OWNER" }, workspaceB: { role: "MEMBER" } } } }, resources: [ { resource: r1, actions: actions }, { resource: r2, actions: actions } ] ) puts JSON.pretty_generate({ results: [ { resource: r1, actions: { "workspace:view": decision.allow?(resource: r1, action: "workspace:view"), "pii:view": decision.allow?(resource: r1, action: "pii:view") } }, { resource: r2, actions: { "workspace:view": decision.allow?(resource: r2, action: "workspace:view"), "pii:view": decision.allow?(resource: r2, action: "pii:view") } } ] }) ``` ```rust use cerbos::sdk::attr::{attr, StructVal}; use cerbos::sdk::model::{Principal, Resource, ResourceAction, ResourceList}; use cerbos::sdk::{CerbosAsyncClient, CerbosClientOptions, CerbosEndpoint, Result}; #[tokio::main] async fn main() -> Result<()> { let opt = CerbosClientOptions::new(CerbosEndpoint::HostPort("localhost", 3593)).with_plaintext(); let mut client = CerbosAsyncClient::new(opt).await?; let principal = Principal::new("123", ["USER"]).with_attributes([attr( "workspaces", StructVal([ ("workspaceA", StructVal([("role", "OWNER")])), ("workspaceB", StructVal([("role", "MEMBER")])), ]), )]); let actions: [&str; 2] = ["workspace:view", "pii:view"]; let kind = "workspace"; let resp = client .check_resources( principal, ResourceList::new_from([ ResourceAction(Resource::new("workspaceA", kind), actions), ResourceAction(Resource::new("workspaceB", kind), actions), ]), None, ) .await?; println!("{:?}", resp.response); Ok(()) } ``` You can find a full (and extended) example of the above in our [SaaS Workspace Policy playground example](https://play.cerbos.dev/p/IJxlK6131f642ND65F1EhPmiT18Ap1A5). ### [](#%5Fdynamic%5Fpolicies)Dynamic Policies There might be circumstances where you want to create or update resources and actions on the fly; an example of this might be a multi-tenant platform that provides tenants the ability to manage their own policies. If this is the case, then you can use the [Admin API](../api/admin%5Fapi.html) configured alongside a mutable [database storage engine](../configuration/storage.html#sqlite3) to provide this functionality. This would be handled within your application layer, with the desired policy contents provided to the PDP via the API. For a full example implementation, check out [this demo](https://github.com/cerbos/demo-admin-api). ## [](#%5Fpolicy%5Frepository%5Flayout)Policy repository layout Cerbos expects the policy repository to have a particular directory layout. * The directory must only contain Cerbos policy files, policy test files and schemas. Any other YAML or JSON files will cause Cerbos to consider the policy repository as invalid. * If you use [schemas](schemas.html), the `_schemas` directory must be a top-level directory at the root of the policy repo. * All policy tests must have a file name ending in `_test` and a `.yaml`, `.yml` or `.json` extension. * Directories named `testdata` can be used to store test data for policy tests. Cerbos will not attempt to locate any policy files inside those directories. * Hidden files and directories (names starting with `.`) are ignored. A typical policy repository might resemble the following: . ├── _schemas │ ├── principal.json │ └── resources │ ├── leave_request.json │ ├── purchase_order.json │ └── salary_record.json ├── derived_roles │ ├── backoffice_roles.yaml │ └── common_roles.yaml ├── principal_policies │ └── auditor_audrey.yaml └── resource_policies ├── finance │ ├── purchase_order.yaml │ └── purchase_order_test.yaml └── hr ├── leave_request.yaml ├── leave_request_test.yaml ├── salary_record.yaml ├── salary_record_test.yaml └── testdata ├── auxdata.yaml ├── principals.yaml └── resources.yaml Validating and testing policies ==================== ## [](#%5Fvalidating%5Fpolicies)Validating policies You can use the Cerbos compiler to make sure that your policies are valid before pushing them to a production Cerbos instance. We recommend setting up a git hook or a CI step to run the Cerbos compiler before you push any policy changes to production. ```sh docker run -i -t -v /path/to/policy/dir:/policies ghcr.io/cerbos/cerbos:0.55.0 compile /policies ``` ## [](#testing)Testing policies You can write optional tests for policies and run them as part of the compilation stage to make sure that the policies do exactly what you expect. Tests are defined using the familiar YAML format as well. A test file must have `_test` suffix in the name and one of the following file extensions: `yaml`, `yml`, or `json`. For example, `album_test.yml`, `album_test.yaml` or `album_test.json`. Test suite definition ```yaml --- name: AlbumObjectTestSuite (1) description: Tests for verifying the album:object resource policy (2) options: now: "2022-08-02T15:00:00Z" (3) defaultPolicyVersion: staging (4) defaultScope: "" (5) lenientScopeSearch: true (6) strictEvaluation: true (7) globals: (8) my_global_var: foo principals: (9) alicia: id: aliciaID roles: - user bradley: id: bradleyID roles: - user principalGroups: (10) everyone: principals: - alicia - bradley resources: (11) alicia_album: id: XX125 kind: album:object policyVersion: default attr: owner: aliciaID public: false flagged: false bradley_album: id: XX250 kind: album:object policyVersion: staging attr: owner: bradleyID public: false flagged: false resourceGroups: (12) all_albums: resources: - alicia_album - bradley_album auxData: (13) validJWT: jwt: iss: my.domain aud: ["x", "y"] myField: value tests: (14) - name: Accessing an album (15) options: (16) now: "2022-08-03T15:00:00Z" (17) defaultPolicyVersion: production (18) defaultScope: "" (19) lenientScopeSearch: false (20) strictEvaluation: true (21) globals: (22) my_global_var: bar input: (23) principals: (24) - alicia - bradley resources: (25) - alicia_album - bradley_album actions: (26) - view - delete auxData: validJWT (27) expected: (28) - principal: alicia (29) resource: alicia_album (30) actions: (31) view: EFFECT_ALLOW delete: EFFECT_ALLOW outputs: (32) - action: view (33) expected: (34) - src: resource.album.vdefault#view-rule val: key1: value1 key2: ["value2", "value3"] - src: resource.album.vdefault#token-lifetime val: 1h - principal: bradley resource: bradley_album actions: view: EFFECT_ALLOW delete: EFFECT_ALLOW - name: Using groups input: principalGroups: (35) - everyone resourceGroups: (36) - all_albums actions: - download expected: - principalGroups: (37) - everyone resourceGroups: (38) - all_albums actions: download: EFFECT_DENY ``` | **1** | Name of the test suite. | | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **2** | Description of the test suite. | | **3** | Optional RFC3339 timestamp to be used as the return value of the now function. Applies to all tests in the suite unless overridden locally. | | **4** | Optionally set [default policy version](../configuration/engine.html#default%5Fpolicy%5Fversion) for this test suite. | | **5** | Optionally set [default scope](../configuration/engine.html#default%5Fscope) for this test suite. | | **6** | Optionally set [lenient scope search](../configuration/engine.html#lenient%5Fscopes) for this test suite. | | **7** | Optionally enable [strict evaluation](../configuration/engine.html#strict%5Fevaluation) for this test suite. | | **8** | Optionally set [globals](../configuration/engine.html#globals) for this test suite. | | **9** | Map of principal fixtures. The key is a string that can be used to refer to the associated principal. | | **10** | Map of principal groups. The key is a string that can be used to refer to the associated group of principal fixtures. | | **11** | Map of resource fixtures. The key is a string that can be used to refer to the associated resource. | | **12** | Map of resource groups. The key is a string that can be used to refer to the associated group of resource fixtures. | | **13** | Map of (optional) auxiliary data fixtures required to evaluate some requests. The key is a string that can be used to refer to the associated auxData. | | **14** | List of tests in this suite. | | **15** | Name of the test. | | **16** | Optionally set options that apply to just this test. Test-specific options are not merged with suite-wide options, so any unspecified values revert to the default. | | **17** | Optional RFC3339 timestamp to be used as the return value of the now function. | | **18** | Optionally set [default policy version](../configuration/engine.html#default%5Fpolicy%5Fversion) for this test. | | **19** | Optionally set [default scope](../configuration/engine.html#default%5Fscope) for this test suite. | | **20** | Optionally set [lenient scope search](../configuration/engine.html#lenient%5Fscopes) for this test. | | **21** | Optionally enable [strict evaluation](../configuration/engine.html#strict%5Fevaluation) for this test. | | **22** | Optionally set [globals](../configuration/engine.html#globals) for this test. | | **23** | Input to the policy engine. | | **24** | List of keys of principal fixtures to test. | | **25** | List of keys of resource fixtures to test. | | **26** | List of actions to test. | | **27** | Key of auxiliary data fixture to test (optional). | | **28** | List of outcomes expected for each principal and resource. If a principal+resource pair specified in input is not listed in expected, then EFFECT\_DENY is expected for all actions for that pair. | | **29** | Key of the principal fixture under test. Use principals instead of principal if you want to specify identical expectations for multiple principals. | | **30** | Key of the resource fixture under test. Use resources instead of resource if you want to specify identical expectations for multiple resources. | | **31** | Expected outcomes for each action for each principal+resource pair. If an action specified in input is not listed, then EFFECT\_DENY is expected for that action. | | **32** | Optional list of [output values](outputs.html) to match. | | **33** | Name of the action that would produce the output. | | **34** | List of expected output values. | | **35** | List of keys of principal groups to test. You can provide this instead of, or as well as, principals. | | **36** | List of keys of resource groups to test. You can provide this instead of, or as well as, resources. | | **37** | Key of the principal group under test. You can provide this instead of, or as well as, principal or principals. | | **38** | Key of the resource group under test. You can provide this instead of, or as well as, resource or resources. | ### [](#fixtures)Sharing test fixtures It is possible to share principals, resources and auxData blocks between test suites stored in the same directory. Create a `testdata` directory in the directory containing your test suite files, then define shared resources, principals and auxData in `testdata/resources.yml`, `testdata/principals.yml`, `testdata/auxdata.yml` respectively (`yaml` and `json` extensions are also supported). tests ├── album_object_test.yaml ├── gallery_object_test.yaml ├── slideshow_object_test.yaml └── testdata ├── auxdata.yaml ├── principals.yaml └── resources.yaml An example of `testdata/principals.yml` ```yaml --- principals: # required john: id: johnID roles: - user - moderator principalGroups: # optional moderators: principals: - john ``` An example of `testdata/resources.yml` ```yaml --- resources: # required alicia_album: id: XX125 kind: "album:object" attr: owner: aliciaID public: false flagged: false resourceGroups: # optional all_albums: resources: - alicia_album ``` An example of `testdata/auxdata.yml` ```yaml --- auxData: # required validJWT: jwt: iss: my.domain aud: ["x", "y"] myField: value ``` | | [YAML anchors and overrides](https://www.educative.io/blog/advanced-yaml-syntax-cheatsheet#anchors) are a great way to reduce repetition and reuse definitions in test cases. For example, the following definitions are equivalent: Without anchors and overrides With anchors and overrides resources: alicias\_album1: id: "XX125" kind: "album:object" attr: owner: "alicia" public: false flagged: false alicias\_album2: id: "XX525" kind: "album:object" attr: owner: "alicia" public: false flagged: false alicias\_album3: id: "XX925" kind: "album:object" attr: owner: "alicia" public: false flagged: false resources: alicias\_album1: id: "XX125" kind: "album:object" attr: &alicia\_album\_attr owner: "alicia" public: false flagged: false alicias\_album2: id: "XX525" kind: "album:object" attr: <<: \*alicia\_album\_attr alicias\_album3: id: "XX925" kind: "album:object" attr: <<: \*alicia\_album\_attr | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ### [](#%5Frunning%5Ftests)Running tests The `compile` command automatically discovers test files in the policy repository. ```sh docker run -i -t \ -v /path/to/policy/dir:/policies \ ghcr.io/cerbos/cerbos:0.55.0 compile /policies ``` The output format can be controlled using the `--output` flag, which accepts the values `tree` (default), `list` and `json`. The `--color` flag controls the coloring of the output. To produce machine readable output from the tests, pass `--output=json --color=never` to the command. By default, all discovered tests are run. Use the `--skip-tests` flag to skip all tests or use `--test-filter` to run only tests matching specific criteria. The `--test-filter` flag accepts a filter expression with the following dimensions: * `suite` \- filter by test suite name, * `test` \- filter by test name, * `principal` \- filter by principal key, * `resource` \- filter by resource key, * `action` \- filter by action name. Each dimension accepts one or more glob patterns. Multiple dimensions are separated by semicolons, and multiple values within a dimension are separated by commas. Example: Running tests for a specific suite and principal ```sh docker run -i -t \ -v /path/to/policy/dir:/policies \ ghcr.io/cerbos/cerbos:0.55.0 compile --test-filter='suite=AlbumObjectTestSuite;principal=alicia' /policies ``` Example: Running tests matching glob patterns ```sh docker run -i -t \ -v /path/to/policy/dir:/policies \ ghcr.io/cerbos/cerbos:0.55.0 compile --test-filter='test=*Delete*;action=view,edit' /policies ``` Multiple `--test-filter` flags can be combined. All filter dimensions are merged together. ```sh docker run -i -t \ -v /path/to/policy/dir:/policies \ ghcr.io/cerbos/cerbos:0.55.0 compile --test-filter='principal=alice,bob' --test-filter='action=view' /policies ``` You can mark entire suites or individual tests in a suite with `skip: true` to skip them during test runs. Example: Skipping a test ```yaml --- name: AlbumObjectTestSuite description: Tests for verifying the album:object resource policy tests: - name: View private album skip: true skipReason: "Policy under review" input: principals: ["alicia"] resources: ["alicia_private_album"] actions: ["view"] expected: - principal: alicia resource: alicia_private_album actions: view: EFFECT_ALLOW ``` ## [](#ci-environments)Validating and testing policies in CI environments Because Cerbos artefacts are distributed as self-contained containers and binaries, you should be able to easily integrate Cerbos into any CI environment. Simply configure your workflow to execute the commands described in the sections above using either the Cerbos container (you may need to configure mount points to suit your repo structure) or the binary. ### [](#%5Fgithub%5Factions)GitHub Actions * [cerbos-setup-action](https://github.com/cerbos/cerbos-setup-action): Install `cerbos` and `cerbosctl` binaries into your workflow tools cache * [cerbos-compile-action](https://github.com/cerbos/cerbos-compile-action): Compile and (optionally) test Cerbos policies Example workflow ```yaml --- name: PR Check on: pull_request: branches: - main jobs: cerbosCheck: name: Check Cerbos policies runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Setup Cerbos uses: cerbos/cerbos-setup-action@v1 with: version: latest - name: Compile and test policies uses: cerbos/cerbos-compile-action@v1 with: policyDir: policies ``` See for an example of Cerbos GitHub Actions being used in a workflow. ### [](#%5Fgitlab%5Fci)GitLab CI Example pipeline ```yaml --- stages: - prepare - compile download-cerbos: stage: prepare script: - curl https://github.com/cerbos/cerbos/releases/download/v0.55.0/cerbos_0.55.0_Linux_x86_64.tar.gz -L --output /tmp/cerbos.tar.gz - tar -xf /tmp/cerbos.tar.gz -C ./ - chmod +x ./cerbos artifacts: paths: - cerbos compile-job: stage: compile dependencies: ["download-cerbos"] script: - ./cerbos compile ./policies ``` ### [](#%5Fdagger)Dagger The [Dagger](https://dagger.io) Cerbos module can be installed by running `dagger install github.com/cerbos/dagger-cerbos`. This module provides a `compile` function for compiling and testing Cerbos policy repositories and a `server` service for starting a Cerbos server. ```yaml # Compile and run tests on a policy repository dagger -m github.com/cerbos/dagger-cerbos call compile --policy-dir=./cerbos # Start a Cerbos server with the default disk driver dagger -m github.com/cerbos/dagger-cerbos call server --policy-dir=./cerbos up # Start a Cerbos server instance configured to use an in-memory SQLite policy repository dagger -m github.com/cerbos/dagger-cerbos call server --config=storage.driver=sqlite3,storage.sqlite3.dsn=:memory:,server.adminAPI.enabled=true up # View usage information dagger -m github.com/cerbos/dagger-cerbos call compile --help dagger -m github.com/cerbos/dagger-cerbos call server --help ``` | | [Cerbos Hub](https://docs.cerbos.dev/cerbos-hub/) provides private, browser-based, collaborative playgrounds for authoring and testing policies with advanced features like execution traces for debugging condition logic. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | Conditions ==================== Conditions are CEL expressions evaluated at request time against principal, resource, and request attributes. A rule with a condition applies only when the expression evaluates to true. A powerful feature of Cerbos policies is the ability to define conditions that are evaluated against the data provided in the request. Conditions are written using the [Common Expression Language (CEL)](https://github.com/google/cel-spec/blob/master/doc/intro.md). | | Cerbos ships with an interactive REPL that can be used to experiment with writing CEL conditions. It can be started by running cerbos repl. See [the REPL documentation](../cli/cerbos.html#repl) for more information. | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Every condition expression must evaluate to a boolean true/false value. A condition block in a policy can contain either a single condition expression, or multiple expressions combined using the `all`, `any`, or `none` operators. These logical operators may be nested. Condition block ```yaml condition: match: all: of: - expr: request.resource.attr.status == "PENDING_APPROVAL" - expr: > "GB" in request.resource.attr.geographies ``` ## [](#top%5Flevel%5Fidentifiers)Top-level identifiers Within a condition expression, you have access to several top-level identifiers: `request` Data provided in the check or plan request (principal, resource, and auxiliary data). `runtime` Additional data computed while evaluating the policy. `variables` Variables declared in the [variables section of the policy](variables.html#variables). `constants` Variables declared in the [constants section of the policy](variables.html#constants). `globals` Global variables declared in the [policy engine configuration](../configuration/engine.html#globals). There are also single-letter aliases available to allow you to write terser expressions: `P` `request.principal` `R` `request.resource` `V` `variables` `C` `constants` `G` `globals` The `request` object ```yaml request: principal: (1) id: alice (2) roles: (3) - employee attr: (4) geography: GB resource: (5) kind: leave_request (6) id: XX125 (7) attr: (8) owner: alice auxData: (9) jwt: (10) iss: acme.corp jwts: (11) token_a: claims: iss: acme.corp sub: foo token_b: claims: iss: acme.corp sub: bar ``` | **1** | The principal whose permissions are being checked. | | ------ | ----------------------------------------------------------------------------------- | | **2** | ID of the principal. | | **3** | Static roles that are assigned to the principal by your identity management system. | | **4** | Free-form context data about the principal. | | **5** | The resource on which the principal is performing actions. | | **6** | Resource kind. | | **7** | ID of the resource instance. | | **8** | Free-form context data about the resource instance. | | **9** | [Auxiliary data sources](../configuration/auxdata.html). | | **10** | JWT claims. Mutually exclusive with jwts. | | **11** | Named JWT claims. Since Cerbos 0.55.0 only. Mutually exclusive with jwt. | The `runtime` object ```yaml runtime: effectiveDerivedRoles: (1) - owner - gb_employee ``` | **1** | [Derived roles](derived%5Froles.html) that were assigned to to the principal by Cerbos while evaluating the policy. This is only populated in expressions in resource policies, and only includes derived roles that are referenced in at least one policy rule. | | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#%5Fexpressions%5Fand%5Fblocks)Expressions and blocks Single boolean expression ```yaml condition: match: expr: P.id.matches("^dev_.*") ``` `all` operator: all expressions must evaluate to true (logical AND) ```yaml condition: match: all: of: - expr: R.attr.status == "PENDING_APPROVAL" - expr: > "GB" in R.attr.geographies - expr: P.attr.geography == "GB" ``` `any` operator: only one of the expressions has to evaluate to true (logical OR) ```yaml condition: match: any: of: - expr: R.attr.status == "PENDING_APPROVAL" - expr: > "GB" in R.attr.geographies - expr: P.attr.geography == "GB" ``` `none` operator: none of the expressions should evaluate to true (logical negation) ```yaml condition: match: none: of: - expr: R.attr.status == "PENDING_APPROVAL" - expr: > "GB" in R.attr.geographies - expr: P.attr.geography == "GB" ``` Nesting operators ```yaml condition: match: all: of: - expr: R.attr.status == "DRAFT" - any: of: - expr: R.attr.dev == true - expr: R.attr.id.matches("^[98][0-9]+") - none: of: - expr: R.attr.qa == true - expr: R.attr.canary == true ``` The above nested block is equivalent to the following: ```yaml condition: match: expr: > (R.attr.status == "DRAFT" && (R.attr.dev == true || R.attr.id.matches("^[98][0-9]+")) && !(R.attr.qa == true || R.attr.canary == true)) ``` Quotes in expressions Single and double quotes have special meanings in YAML. To avoid parsing errors when your expression contains quotes, use the YAML block scalar syntax or wrap the expression in parentheses. ```yaml expr: > "GB" in R.attr.geographies ``` ```yaml expr: ("GB" in R.attr.geographies) ``` | | A CEL **runtime error** while evaluating a condition (for example comparing incompatible types such as R.attr.amount > 1000 when amount is a string, or referencing a missing attribute) causes the condition to be treated as not satisfied. For ALLOW rules this fails closed: the rule simply does not match. For DENY rules it currently fails **open**: the deny is skipped, so a matching ALLOW can still grant the request. From Cerbos v0.55 a DENY rule whose condition raises a runtime error will fail closed (the deny is applied). Cerbos v0.54 logs every runtime error raised during evaluation and includes the errors in audit log entries so you can find and fix affected policies before upgrading. The [engine.celErrorLogLevel](../configuration/engine.html#cel%5Ferror%5Flog%5Flevel) setting controls the level of the log messages. | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#%5Fpolicy%5Fvariables)Policy variables To avoid duplication in condition expressions, you can define [variables and constants in policies](variables.html). ## [](#auxdata)Auxiliary data If you have [auxiliary data sources configured](../configuration/auxdata.html), they can be accessed using `request.auxData`. Accessing JWT claims ```yaml "cerbie" in request.auxData.jwt.aud && request.auxData.jwt.iss == "cerbos" ``` Accessing named JWT claims (Cerbos 0.55.0 and above) ```yaml "cerbie" in request.auxData.jwts.token_a.claims.aud && request.auxData.jwts.token_b.claims.iss == "cerbos" ``` | | When multiple JWTs are referenced (since Cerbos 0.55.0 only), the syntax is request.auxData.jwts.TOKEN\_NAME.claims.CLAIM\_NAME or request.auxData.jwts\["TOKEN\_NAME"\].claims\["CLAIM\_NAME\]. Note the claims mandatory field between the token name and the claim name. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ## [](#%5Foperators)Operators | | CEL has many builtin functions and operators. The fully up-to-date list can be found at . | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Operator | Description | | -------- | -------------------------------- | | ! | Logical negation (NOT) | | \- | Subtraction/numeric negation | | != | Unequals | | % | Modulo | | && | Logical AND | | \|| | Logical OR | | \* | Multiplication | | + | Addition/concatenation | | / | Division | | <= | Less than or equal to | | < | Less than | | \== | Equals | | \>= | Greater than or equal to | | \> | Greater than | | in | Membership in lists or maps | | ? : | Ternary condition (if-then-else) | ## [](#%5Fdurations)Durations | | Duration values must be specified in one of the following units. Larger units like days, weeks or years are not supported because of ambiguity around their meaning due to factors such as daylight saving time transitions. Suffix Unit ns Nanoseconds us Microseconds ms Milliseconds s Seconds m Minutes h Hours | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Test data ```json ... "resource": { "kind": "leave_request", "attr": { "cooldownPeriod": "3750s", "lastAccessed": "2021-04-20T10:00:20.021-05:00" } } ... ``` | Function | Description | Example | | --------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | | duration | Convert a string to a duration. The string must contain a valid duration suffixed by one of ns, us, ms, s, m or h. E.g. 3750s | duration(R.attr.cooldownPeriod).getSeconds() == 3750 | | getHours | Get hours from a duration | duration(R.attr.cooldownPeriod).getHours() == 1 | | getMilliseconds | Get milliseconds from a duration | duration(R.attr.cooldownPeriod).getMilliseconds() == 3750000 | | getMinutes | Get minutes from a duration | duration(R.attr.cooldownPeriod).getMinutes() == 62 | | getSeconds | Get seconds from a duration | duration(R.attr.cooldownPeriod).getSeconds() == 3750 | | timeSince | Time elapsed since the given timestamp to current time on the server. This is a Cerbos extension to CEL | timestamp(R.attr.lastAccessed).timeSince() > duration("1h") | ## [](#hierarchies)Hierarchies | | The hierarchy functions are Cerbos-specific extensions to CEL. | | ----------------------------------------------------------------- | Test data ```json ... "principal": { "id": "john", "roles": ["employee"], "attr": { "scope": "foo.bar.baz.qux", } }, "resource": { "kind": "leave_request", "attr": { "scope": "foo.bar", } } ... ``` | Function | Description | Example | | ----------------- | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | | hierarchy | Convert a dotted string or a string list to a hierarchy | hierarchy("a.b.c") == hierarchy(\["a","b","c"\]) | | hierarchy | Convert a delimited string representation to a hierarchy | hierarchy("a:b:c", ":").size() == 3 | | ancestorOf | Returns true if the first hierarchy shares a common prefix with the second hierarchy | hierarchy("a.b").ancestorOf(hierarchy("a.b.c.d")) == true | | commonAncestors | Returns the common ancestor hierarchy | hierarchy(R.attr.scope).commonAncestors(hierarchy(P.attr.scope)) == hierarchy("foo.bar") | | descendentOf | Mirror function of ancestorOf | hierarchy("a.b.c.d").descendentOf(hierarchy("a.b")) == true | | immediateChildOf | Returns true if the first hierarchy is a first-level child of the second hierarchy | hierarchy("a.b.c").immediateChildOf(hierarchy("a.b")) == true && hierarchy("a.b.c.d").immediateChildOf(hierarchy("a.b")) == false | | immediateParentOf | Mirror function of immediateChildOf | hierarchy("a.b").immediateParentOf(hierarchy("a.b.c")) == true && hierarchy("a.b").immediateParentOf(hierarchy("a.b.c.d")) == false | | overlaps | Returns true if one of the hierarchies is a prefix of the other | hierarchy("a.b.c").overlaps(hierarchy("a.b.c.d.e")) == true && hierarchy("a.b.x").overlaps(hierarchy("a.b.c.d.e")) == false | | siblingOf | Returns true if both hierarchies share the same parent | hierarchy("a.b.c").siblingOf(hierarchy("a.b.d")) == true | | size | Returns the number of levels in the hierarchy | hierarchy("a.b.c").size() == 3 | | \[\] | Access a level in the hierarchy | hierarchy("a.b.c.d")\[1\] == "b" | ## [](#%5Fip%5Faddresses)IP addresses | | The inIPAddrRange is a Cerbos-specific extension function to CEL. | | -------------------------------------------------------------------- | Test data ```json ... "principal": { "id": "elmer_fudd", "attr": { "ipv4Address": "192.168.0.10", "ipv6Address": "2001:0db8:0000:0000:0000:0000:1000:0000" } } ... ``` | Function | Description | Example | | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | cidr().containsCIDR | Returns whether the CIDR contains the given CIDR. Since v0.55.0 only. | cidr("192.168.0.0/8").containsCIDR(cidr("192.168.1.0/24")) == true | | cidr().containsIP | Returns whether the CIDR contains the given IP. Since v0.55.0 only. Use inIPAddrRange for older versions. | cidr("2001:db8::/48").containsIP(P.attr.ipv6Address) == true | | cidr().isMask | Returns whether the CIDR is a mask. Since v0.55.0 only. | cidr("192.168.0.0/8").isMask() == true | | cidr().masked | Returns the masked CIDR for the value. Since v0.55.0 only. | cidr("192.168.1.5/24").masked() == cidr("192.168.1.0/24") | | cidr().prefixLength | Returns the prefix length of the CIDR. Since v0.55.0 only. | cidr("192.168.1.0/24").prefixLength() == 24 | | inIPAddrRange | Check whether the IP address is in the range defined by the CIDR | P.attr.ipv4Address.inIPAddrRange("192.168.0.0/24") && P.attr.ipv6Address.inIPAddrRange("2001:db8::/48") | | ip().family | Returns the IP family. Since v0.55.0 only. | ip(P.attr.ipv4Address) == 4 | | ip().isGlobalUnicast | Returns true if the IP address is a global unicast address. Since v0.55.0 only. | ip(P.attr.ipv6Address).isGlobalUnicast() == true | | ip().isLinkLocalMulticast | Returns true if the IP address is a link local multicast address. Since v0.55.0 only. | ip(P.attr.ipv6Address).isLinkLocalMulticast() == false | | ip().isLinkLocalUnicast | Returns true if the IP address is a link local unicast address. Since v0.55.0 only. | ip(P.attr.ipv6Address).isLinkLocalUnicast() == false | | ip().isLoopback | Returns true if the IP address is a loopback address. Since v0.55.0 only. | ip(P.attr.ipv4Address).isLoopback() == false | | ip().isUnspecified | Returns true if the IP address is unspecified (either 0.0.0.0 or ::). Since v0.55.0 only. | ip(P.attr.ipv6Address).isUnspecified() == false | | ip.isCanonical | Check whether the IPv6 string conforms to [RFC 5952](https://www.rfc-editor.org/rfc/rfc5952.html). Since v0.55.0 only. | ip.isCanonical(P.attr.ipv6Address) == false | | isCIDR | Returns true if the string is an IPv4 or IPv6 CIDR. Since v0.55.0 only. | isCIDR("::1/128") == true | | isIP | Returns true if the string is an IPv4 or IPv6 address. Since v0.55.0 only. | isIP(P.attr.ipv4Address) == true | ## [](#%5Flists%5Fmaps%5Fand%5Fsets)Lists, maps, and sets Test data ```json ... "principal": { "id": "elmer_fudd", "attr": { "id": "125", "teams": ["design", "communications", "product", "commercial"], "limits": { "design": 10, "product": 25 }, "clients": { "acme": {"active": true}, "bb inc": {"active": true} } } } ... ``` | Operator/Function | Description | Example | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | + | Concatenates lists | P.attr.teams + \["design", "engineering"\] | | \[\] | Index into a list or a map | P.attr.teams\[0\] == "design" && P.attr.clients\["acme"\]\["active"\] == true | | all | Check whether all elements in a list match the predicate. | P.attr.teams.all(t, size(t) > 3) && \[1, 2, 3\].all(i, j, i < j) | | distinct | Returns the distinct elements of a list | \[1, 2, 2, 3, 3, 3\].distinct() == \[1, 2, 3\] | | except | Produces the set difference of two lists | P.attr.teams.except(\["design", "engineering"\]) == \["communications", "product", "commercial"\] | | exists | Check whether at least one element matching the predicate exists in a list or map. | P.attr.teams.exists(t, t.startsWith("comm")) && P.attr.limits.exists(k, v, k == "design" && v > 0) | | exists\_one | Check that only one element matching the predicate exists. | P.attr.teams.exists\_one(t, t.startsWith("comm")) == false && P.attr.limits.exists\_one(k, v, k == "design" && v > 0) == false | | filter | Filter a list using the predicate. | size(P.attr.teams.filter(t, t.matches("^comm"))) == 2 | | flatten | Flattens a list. If an optional depth is provided, the list is flattened to the specified level | \[1,2,\[\],\[\],\[3,4\]\].flatten() == \[1, 2, 3, 4\] && \[1,\[2,\[3,\[4\]\]\]\].flatten(2) == \[1, 2, 3, \[4\]\] | | hasIntersection | Checks whether the lists have at least one common element | hasIntersection(\["design", "engineering"\], P.attr.teams) | | in | Check whether the given element is contained in the list or map | ("design" in P.attr.teams) && ("acme" in P.attr.clients) | | intersect | Produces the set intersection of two lists | intersect(\["design", "engineering"\], P.attr.teams) == \["design"\] | | isSubset | Checks whether the list is a subset of another list | \["design", "engineering"\].isSubset(P.attr.teams) == false | | lists.range | Returns a list of integers from 0 to n-1 | lists.range(5) == \[0, 1, 2, 3, 4\] | | map | Transform each element in a list | "DESIGN" in P.attr.teams.map(t, t.upperAscii()) | | reverse | Returns the elements of a list in reverse order | \[5, 3, 1, 2\].reverse() == \[2, 1, 3, 5\] | | sets.contains | Checks whether the first list contains all the elements of the second list. Since v0.55.0 only. | sets.contains(P.attr.teams, \["design"\]) == true | | sets.equivalent | Checks whether for every item in the first list, there exists an equal item in the second list. Since v0.55.0 only. | sets.equivalent(P.attr.teams, \["commercial", "product", "communications", "design"\]) == true | | sets.intersects | Checks whether the lists have at least one item in common. Since v0.55.0 only. Use hasIntersection for older versions. | sets.intersects(\["design", "engineering"\], P.attr.teams) == true | | size | Number of elements in a list or map | size(P.attr.teams) == 4 && size(P.attr.clients) == 2 | | slice | Returns a new sub-list using the indexes provided | \[1,2,3,4\].slice(1, 3) == \[2, 3\] | | sort | Sorts a list with comparable elements | \[3, 2, 1\].sort() == \[1, 2, 3\] | | sortBy | Sorts a list by a key value, i.e., the order is determined by the result of an expression applied to each element of the list | \[{ "name": "foo", "score": 0 },{ "name": "bar", "score": -10 },{ "name": "baz", "score": 1000 }\].sortBy(e, e.score).map(e, e.name) == \["bar", "foo", "baz"\] | | transformList | Converts a map or a list into a list value. The output expression determines the contents of the output list. Elements in the list may optionally be filtered | \[1, 2, 3\].transformList(i, v, i > 0, 2 \* v) == \[4, 6\] &&\[1, 2, 3\].transformList(i, v, 2 \* v) == \[2, 4, 6\] | | transformMap | Converts a map or a list into a map value. The key remains unchanged and only the value is changed. | \[1, 2, 3\].transformMap(i, v, i > 0, 2 \* v) == {1: 4, 2: 6} | | transformMapEntry | Converts a map or a list into a map value; however, this transform expects the entry expression be a map literal. Elements in the map may optionally be filtered | {'greeting': 'hello'}.transformMapEntry(k, v, {v: k}) == {'hello': 'greeting'} | ## [](#%5Fmath)Math | Function | Description | Example | | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | math.abs | Returns the absolute value of the numeric type provided as input | math.abs(1.2) == 1.2 && math.abs(-2) == 2 | | math.bitAnd | Performs a bitwise-AND operation over two int or uint values | math.bitAnd(3u, 2u) == 2u && math.bitAnd(3, 5) == 1 && math.bitAnd(-3, -5) == -7 | | math.bitNot | Function which accepts a single int or uint and performs a bitwise-NOT ones-complement of the given binary value | math.bitNot(1) == -1 && math.bitNot(-1) == 0 && math.bitNot(0u) == 18446744073709551615u | | math.bitOr | Performs a bitwise-OR operation over two int or uint values | math.bitOr(1u, 2u) == 3u && math.bitOr(-2, -4) == -2 | | math.bitShiftLeft | Perform a left shift of bits on the first parameter, by the amount of bits specified in the second parameter. The first parameter is either a uint or an int. The second parameter must be an int | math.bitShiftLeft(1, 2) == 4 && math.bitShiftLeft(-1, 2) == -4 && math.bitShiftLeft(1u, 2) == 4u && math.bitShiftLeft(1u, 200) == 0u | | math.bitShiftRight | Perform a right shift of bits on the first parameter, by the amount of bits specified in the second parameter. The first parameter is either a uint or an int. The second parameter must be an int | math.bitShiftRight(1024, 2) == 256 && math.bitShiftRight(1024u, 2) == 256u && math.bitShiftLeft(1024u, 64) == 0u | | math.bitXor | Performs a bitwise-XOR operation over two int or uint values | math.bitXor(3u, 5u) == 6u && math.bitXor(1, 3) == 2 | | math.ceil | Compute the ceiling of a double value | math.ceil(1.2) == 2.0 && math.ceil(-1.2) == -1.0 | | math.floor | Compute the floor of a double value | math.floor(1.2) == 1.0 && math.floor(-1.2) == -2.0 | | math.greatest | Get the greatest valued number present in the arguments | math.greatest(\[1, 3, 5\]) == 5 && math.greatest(1, 3, 5) == 5 | | math.isFinite | Returns true if the value is a finite number | !math.isFinite(0.0/0.0) && math.isFinite(1.2) | | math.isInf | Returns true if the input double value is -Inf or +Inf | math.isInf(1.0/0.0) && !math.isInf(1.2) | | math.isNaN | Returns true if the input double value is NaN, false otherwise | math.isNaN(0.0/0.0) && !math.isNaN(1.2) | | math.least | Get the least valued number present in the arguments | math.least(\[1, 3, 5\]) == 1 && math.least(1, 3, 5) == 1 | | math.round | Rounds the double value to the nearest whole number with ties rounding away from zero, e.g. 1.5 → 2.0, -1.5 → -2.0 | math.round(1.2) == 1.0 && math.round(1.5) == 2.0 && math.round(-1.5) == -2.0 | | math.sign | Returns the sign of the numeric type, either -1, 0, 1 | math.sign(1.2) == 1.0 && math.sign(-2) == -1 && math.sign(0) == 0 | | math.trunc | Truncates the fractional portion of the double value | math.trunc(1.2) == 1.0 && math.trunc(-1.2) == -1.0 | ## [](#paths)Paths | | The path functions are Cerbos-specific extensions to CEL. | | ------------------------------------------------------------ | | Function | Description | Example | | -------------- | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | basePath | Returns the last element of a path | basePath("/path/to/dir") == "dir" | | dirPath | Returns the directory portion of a path | dirPath("/path/to/file.txt") == "/path/to" | | extPath | Returns the file extension of a path including the leading dot | extPath("/path/to/file.txt") == ".txt" | | joinPath | Joins a list of path segments into a single path | joinPath(\["/path", "to", "file.txt"\]) == "/path/to/file.txt" | | pathHasPrefix | Returns true if the second path is a prefix of the first path | pathHasPrefix("/path/to/dir", "/path/to") == true && "/path/to/dir".pathHasPrefix("/path/to") == true | | pathMatch | Returns true if path matches a given glob pattern | pathMatch("/path/to/file.zip", "/path/to/\*.zip") == true && "/path/to/file.zip".pathMatch("/path/to/\*.zip") == true | | pathMatchAnyOf | Returns true if path matches any of the given glob patterns | pathMatchAnyOf("/path/to/file.zip", \["/path/to/\*.tar.gz", "/path/to/\*.zip"\]) == true && "/path/to/file.zip".pathMatchAnyOf(\["/path/to/\*.tar.gz", "/path/to/\*.zip"\]) == true | | relPath | Returns the relative path from a base path to a target path | relPath("/path/to", "/path/to/file.txt") == "file.txt" | | volumeName | Returns leading volume name for Windows paths | volumeName("C:\\path\\to\\dir") == "C:" | ## [](#spiffe)SPIFFE | | The SPIFFE functions are Cerbos-specific extensions to CEL. | | -------------------------------------------------------------- | Test data ```json ... "principal": { "id": "spiffe://cerbos.dev/ns/privileged/sa/curl", "roles": ["api"], } ... ``` | Function | Description | Example | | ---------------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | spiffeID.isMemberOf | Check whether the ID belongs to given trust domain | spiffeID(P.id).isMemberOf(spiffeTrustDomain("spiffe://cerbos.dev")) | | spiffeID.path | Get the path element of ID | spiffeID(P.id).path() == "/ns/privileged/sa/curl" | | spiffeID.trustDomain | Get the trust domain of ID | spiffeID(P.id).trustDomain() == spiffeTrustDomain("spiffe://cerbos.dev") | | spiffeMatchAny | Match any SPIFFE ID | spiffeMatchAny().matchesID(spiffeID(P.id)) == true | | spiffeMatchExact | Match a single SPIFFE ID | spiffeMatchExact(spiffeID("spiffe://cerbos.dev/ns/privileged/sa/curl")).matchesID(spiffeID(P.id)) == true | | spiffeMatchOneOf | Match any one of SPIFFE IDs | spiffeMatchOneOf(\["spiffe://cerbos.dev/ns/privileged/sa/curl", "spiffe://cerbos.dev/ns/privileged/sa/foo"\]).matchesID(spiffeID(P.id)) == true | | spiffeMatchTrustDomain | Match any ID from the trust domain | spiffeMatchTrustDomain(spiffeTrustDomain("spiffe://cerbos.dev")).matchesID(spiffeID(P.id)) == true | | spiffeTrustDomain.id | Fully qualified trust domain ID | spiffeTrustDomain("cerbos.dev").id() == "spiffe://cerbos.dev" | | spiffeTrustDomain.name | Name of trust domain | spiffeTrustDomain("spiffe://cerbos.dev").name() == "cerbos.dev" | ## [](#%5Fstrings)Strings Test data ```json ... "resource": { "kind": "leave_request", "attr": { "id": "125", "department": "marketing" } } ... ``` | Function | Description | Example | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | | base64.encode | Encode as base64 | base64.encode(bytes("hello")) == "aGVsbG8=" | | base64.decode | Decode base64 | base64.decode("aGVsbG8=") == bytes("hello") | | charAt | Get the character at given index | R.attr.department.charAt(1) == 'a' | | contains | Check whether a string contains the given substring | R.attr.department.contains("arket") | | endsWith | Check whether a string has the given suffix | R.attr.department.endsWith("ing") | | format | Format a string with the given arguments | "department\_%s\_%d".format(\["marketing", 1\]) | | indexOf | Index of the first occurrence of the given character | R.attr.department.indexOf('a') == 1 | | json.encode | Encode as JSON. Since v0.55.0 only. | json.encode(R.attr) == '{"id":"125","department":"marketing"}' | | lastIndexOf | Index of the last occurrence of the given character | R.attr.department.lastIndexOf('g') == 8 | | lowerAscii | Convert ASCII characters to lowercase | "MARKETING".lowerAscii() == R.attr.department | | matches | Check whether a string matches a [RE2](https://github.com/google/re2/wiki/Syntax) regular expression | R.attr.department.matches("^\[mM\].\*g$") | | regex.extractAll | Extract all occurrences of a [RE2](https://github.com/google/re2/wiki/Syntax) regular expression match. Since v0.55.0 only. | regex.extractAll("a bb a cc", "a") == \["a", "a"\] | | regex.replace | Replace all occurrences of a [RE2](https://github.com/google/re2/wiki/Syntax) regular expression match. Since v0.55.0 only. | regex.replace("banana", "a", "i") == "binini" | | regex.replace | Replace occurrences of a [RE2](https://github.com/google/re2/wiki/Syntax) regular expression match with a limit. Since v0.55.0 only. | regex.replace("banana", "a", "i", 1) == "binana" && regex.replace("banana", "b(a).\*", r"\\1") == "a" | | replace | Replace all occurrences of a substring | R.attr.department.replace("market", "engineer") == "engineering" | | replace | Replace with limits. Limit 0 replaces nothing, -1 replaces all. | "engineering".replace("e", "a", 1) == "angineering" && "engineering".replace("e", "a", -1) == "anginaaring" | | size | Get the length of the string | size(R.attr.department) == 9 | | split | Split a string using a delimiter | "a,b,c,d".split(",")\[1\] == "b" | | split | Split a string with limits. Limit 0 returns an empty list, 1 returns a list containing the original string. | "a,b,c,d".split(",", 2)\[1\] == "b,c,d" | | startsWith | Check whether a string has the given prefix | R.attr.department.startsWith("mark") | | substring | Selects a substring from the string | R.attr.department.substring(4) == "eting" && R.attr.department.substring(4, 6) == "et" | | trim | Remove whitespace from beginning and end | " marketing ".trim() == "marketing" | | upperAscii | Convert ASCII characters to uppercase | R.attr.department.upperAscii() == "MARKETING" | ## [](#%5Ftimestamps)Timestamps | | All timestamp getters (getHours, getMinutes, getDayOfWeek, and similar) take a time zone parameter. If omitted, the 'UTC' time zone is used by default. | | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | Test data ```json ... "resource": { "kind": "leave_request", "attr": { "lastAccessed": "2021-04-20T10:00:20.021-05:00", "lastUpdateTime": "2021-05-01T13:34:12.024Z", } } ... ``` | Function | Description | Example | | --------------- | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | | timestamp | Convert an RFC3339 formatted string to a timestamp | timestamp(R.attr.lastAccessed).getFullYear() == 2021 | | getDate | Get day of month from a timestamp | timestamp(R.attr.lastAccessed).getDate() == 20 | | getDayOfMonth | Get day of month from a timestamp. Returns a zero-based value | timestamp(R.attr.lastAccessed).getDayOfMonth() == 19 | | getDayOfWeek | Get day of week from a timestamp. Returns a zero-based value where Sunday is 0 | timestamp(R.attr.lastAccessed).getDayOfWeek() == 2 | | getDayOfYear | Get day of year from a timestamp. Returns a zero-based value | timestamp(R.attr.lastAccessed).getDayOfYear() == 109 | | getFullYear | Get full year from a timestamp | timestamp(R.attr.lastAccessed).getFullYear() == 2021 | | getHours | Get hours from a timestamp | timestamp(R.attr.lastAccessed).getHours("-05:00") == 10 | | getMilliseconds | Get milliseconds from a timestamp | timestamp(R.attr.lastAccessed).getMilliseconds() == 21 | | getMinutes | Get minutes from a timestamp | timestamp(R.attr.lastAccessed).getMinutes("UTC") == 5 | | getMonth | Get month from a timestamp. Returns a zero-based value where January is 0 | timestamp(R.attr.lastAccessed).getMonth("NZ") == 3 | | getSeconds | Get seconds from a timestamp | timestamp(R.attr.lastAccessed).getSeconds() == 20 | | now | Current time on the server. This is a Cerbos extension to CEL | now() > timestamp(R.attr.lastAccessed) | | timeSince | Time elapsed since the given timestamp to current time on the server. This is a Cerbos extension to CEL | timestamp(R.attr.lastAccessed).timeSince() > duration("1h") | Example: Assert that more than 36 hours has elapsed between last access time and last update time ```yaml timestamp(R.attr.lastUpdateTime) - timestamp(R.attr.lastAccessed) > duration("36h") ``` Example: Add a duration to a timestamp ```yaml timestamp(R.attr.lastUpdateTime) + duration("24h") == timestamp("2021-05-02T13:34:12.024Z") ``` | | For patterns that translate condition logic into database queries, see [Filtering database results with Cerbos query plans](https://www.cerbos.dev/blog/filtering-database-results-with-cerbos-query-plans). For implementation review, [speak to us](https://www.cerbos.dev/workshop?utm%5Fcampaign=brand%5Fcerbos&utm%5Fsource=documentation&utm%5Fmedium=text). | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Debugging authorization decisions ==================== When a request returns an unexpected `EFFECT_ALLOW` or `EFFECT_DENY`, Cerbos provides several tools to help you understand why. ## [](#include-meta)Using `includeMeta` in requests Add `"includeMeta": true` to your `CheckResources` request to receive metadata about how Cerbos reached each decision. Request with `includeMeta` ```json { "requestId": "debug-example", "includeMeta": true, "principal": { "id": "alice", "roles": ["employee"], "attr": { "department": "marketing" } }, "resources": [ { "resource": { "kind": "leave_request", "id": "xx125", "policyVersion": "20210210", "attr": { "owner": "alice" } }, "actions": ["view", "approve"] } ] } ``` The response includes a `meta` block for each resource: Response meta block ```json { "meta": { "actions": { "view": { "matchedPolicy": "resource.leave_request.v20210210/acme.corp", "matchedScope": "acme", "effectiveDerivedRoles": ["owner"] }, "approve": { "matchedPolicy": "resource.leave_request.v20210210/acme.corp" } } } } ``` `matchedPolicy` The full identifier of the policy that produced the decision. `matchedScope` The scope that was active when the decision was made (relevant when using [scoped policies](scoped%5Fpolicies.html)). `effectiveDerivedRoles` The [derived roles](derived%5Froles.html) that were activated for the principal on this resource. If a role you expected is missing, check the derived roles definition and its conditions. ## [](#audit-logging)Audit logging Enable decision logging to record every authorization decision made by the Cerbos PDP. This is useful for post-hoc analysis and for correlating decisions with application behaviour. Minimal audit configuration ```yaml audit: enabled: true decisionLogsEnabled: true backend: file file: path: stdout ``` Every API response includes a `cerbosCallId` field. Use this identifier to locate the corresponding entry in the audit log. See [audit configuration](../configuration/audit.html) for the full set of options including filtering, retention, and alternative backends. ## [](#repl)Testing CEL expressions with the REPL The Cerbos REPL lets you evaluate CEL expressions interactively, which is useful for debugging [conditions](conditions.html) that aren’t behaving as expected. You can load request data and test expressions against it without running a full policy evaluation cycle. See [the REPL documentation](../cli/cerbos.html#repl) for usage details. ## [](#unexpected-deny)Common causes of unexpected DENY If a request is denied when you expect it to be allowed, check the following: No matching policy No policy exists for the `kind` and `policyVersion` combination in the request. Use `includeMeta` to confirm whether a policy was matched. Missing derived roles import The [derived roles](derived%5Froles.html) policy is not imported into the resource policy via `importDerivedRoles`. The derived roles must be explicitly imported before they can take effect. Condition evaluated to false A [condition](conditions.html) on the matching rule returned `false`. Verify that the attribute names and types in the request match what the condition expects. Use the Cerbos REPL to execute the rule with your input or write a policy test and execute it with the `--verbose` option to see how the expression is evaluated. Explicit DENY matched A `DENY` rule matched for the same action. In Cerbos, deny always takes precedence over allow when both apply to the same role and action. See [how Cerbos evaluates requests](evaluation.html). Role mismatch The principal’s `roles` in the request don’t match any rule in the policy. Roles are case-sensitive and must match exactly. They must be the actual role names and not the derived role names. ## [](#unexpected-allow)Common causes of unexpected ALLOW Overly broad wildcards A rule using actions: \[''\] or `roles: ['']` matches more than intended. Narrow the rule to list specific actions or roles. Derived role condition too permissive A [derived role](derived%5Froles.html) condition is matching more principals than expected. Review the condition expression and test it in the [REPL](../cli/cerbos.html#repl). ## [](#validation)Validating and inspecting policies Use `cerbos compile` to validate policies and run tests before deploying them. This catches syntax errors, missing imports, and failing test cases. See [validating and testing](compile.html) for the full guide on policy compilation and test suites. Derived roles ==================== Derived roles are computed at request evaluation time from the principal’s base roles and request attributes. They augment static IdP-assigned roles with context-dependent designations. Traditional RBAC roles are usually broad groupings with no context awareness. They are static and they are provided by the Identity Provider(IDP), not by Cerbos. Cerbos provides derived roles as a way of augmenting those broad roles with contextual data to provide more fine-grained control at runtime. For example, a person with the broad `manager` role can be augmented to `manager_of_scranton_branch` by taking into account the geographic location (or another factor) and giving that derived role bearer extra privileges on resources that belong to the Scranton branch. | | Derived roles are dynamically determined at runtime by matching the principal’s roles sent in the [API request](../api/index.html#check-resources) to the parentRoles specified in the derived roles definitions. Don’t use the derived role names as roles in the API request as Cerbos only expects that field to contain "normal" roles. | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ```yaml --- apiVersion: "api.cerbos.dev/v1" description: |- Common dynamic roles used within the Apatr app derivedRoles: name: apatr_common_roles (1) constants: import: (2) - apatr_common_constants local: (3) corporate_network_ip_range: 10.20.0.0/16 variables: import: (4) - apatr_common_variables local: (5) flagged_resource: request.resource.attr.flagged definitions: - name: owner (6) parentRoles: ["user"] (7) condition: (8) match: expr: request.resource.attr.owner == request.principal.id - name: abuse_moderator parentRoles: ["moderator"] condition: match: expr: variables.flagged_resource - name: corporate_user parentRoles: ["user"] condition: match: expr: request.principal.attr.ip_address.inIPAddrRange(constants.corporate_network_ip_range) ``` | **1** | Name to use when importing this set of derived roles. | | ----- | ---------------------------------------------------------------------------------------------------------------------------------------- | | **2** | [Constant definitions](variables.html#export-constants) to import (optional). | | **3** | [Local constant definitions](variables.html#local-constants) (optional). | | **4** | [Variable definitions](variables.html#export) to import (optional). | | **5** | [Local variable definitions](variables.html#local) (optional). | | **6** | Descriptive name for this derived role. | | **7** | The static roles (from the identity provider) to which this derived role applies to. The special value \* can be used to match any role. | | **8** | An (optional) set of expressions that should evaluate to true for this role to activate. | Understanding derived roles To explain the concept of derived roles, consider this example from the DC Comics universe: when billionaire playboy Bruce Wayne wears the bat costume he becomes Batman, the caped crusader. Becoming Batman gives Bruce extra privileges like being able to beat up criminals without any consequences and driving a tank through the streets of Gotham. In Cerbos terms, Batman is the `derived role` and Bruce Wayne is the `parentRole`. The `condition` for activating the Batman derived role is: `Bruce Wayne is wearing the bat costume`. Cerbos only ever deals with Bruce Wayne because he’s the only real person in this scenario. However, Cerbos is smart enough to treat him as Batman whenever he’s wearing his costume. ```yaml --- apiVersion: "api.cerbos.dev/v1" derivedRoles: name: gotham_city definitions: - name: batman parentRoles: ["bruce_wayne"] condition: match: expr: P.attr.isWearingBatCostume ``` | | For the tradeoffs between derived roles and dynamic attribute checks when modelling hierarchy-based permissions, see [Mastering hierarchy-based permissions with Cerbos](https://www.cerbos.dev/blog/mastering-hierarchy-based-permissions-with-cerbos-policy-defined-roles-vs-dynamic-attributes). To discuss a specific permission model, [speak to us](https://www.cerbos.dev/workshop?utm%5Fcampaign=brand%5Fcerbos&utm%5Fsource=documentation&utm%5Fmedium=text). | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#troubleshooting)Troubleshooting derived roles ### [](#%5Fmy%5Fderived%5Frole%5Fisnt%5Fbeing%5Factivated)My derived role isn’t being activated If a derived role isn’t taking effect, work through this checklist: 1. **The derived roles policy is imported into the resource policy.** The resource policy must include an `importDerivedRoles` entry matching the `name` field of the derived roles policy. ```yaml resourcePolicy: resource: "document" version: "default" importDerivedRoles: - my_derived_roles # must match the `name` in the derived roles policy rules: - actions: ["edit"] effect: EFFECT_ALLOW derivedRoles: - owner ``` 2. **The principal’s static roles match `parentRoles`.** The roles sent in the API request must include at least one of the roles listed in the derived role’s `parentRoles` field. These are case-sensitive. 3. **The condition evaluates to true.** If the derived role has a condition, verify that the attribute names and types in the request match what the expression expects. Use the [Cerbos REPL](../cli/cerbos.html#repl) to test expressions interactively. 4. **The resource policy has a rule referencing the derived role.** A rule must include the derived role name in its `derivedRoles` list for it to grant or deny access. ### [](#%5Fhow%5Fdo%5Fi%5Fsee%5Fwhich%5Fderived%5Froles%5Fwere%5Factivated)How do I see which derived roles were activated? Add `"includeMeta": true` to the `CheckResources` request. The response `meta` block includes an `effectiveDerivedRoles` list showing which derived roles were activated for the principal on each resource. See [debugging authorization decisions](debugging.html) for more details. ### [](#%5Fa%5Fderived%5Frole%5Fdoes%5Fnot%5Fautomatically%5Fgrant%5Faccess)A derived role does not automatically grant access Defining a derived role only creates a named condition. You must still write a rule in a resource policy that references the derived role in its `derivedRoles` list for it to have any effect on authorization decisions. ### [](#%5Fderived%5Froles%5Fand%5Fwildcards)Derived roles and wildcards The special value `*` can be used in `parentRoles` to match any static role. This is useful for derived roles based purely on resource attributes (for example, ownership) regardless of the principal’s static role. ```yaml definitions: - name: owner parentRoles: ["*"] condition: match: expr: R.attr.owner == P.id ``` How Cerbos evaluates requests ==================== Understanding how Cerbos evaluates authorization requests helps you write correct policies and diagnose unexpected results. ## [](#default-deny)Default deny Cerbos is deny-by-default. If no policy exists for the resource kind and version in the request, or if no rule within a matching policy grants access, the result is `EFFECT_DENY`. There is no concept of "no opinion" — the absence of an explicit allow is a deny. ## [](#policy-lookup)Policy lookup When a request arrives, Cerbos looks up the resource policy matching the resource `kind` and `version` from the request. If a `scope` is provided, Cerbos looks for the most specific scoped policy first, then walks up the scope hierarchy. For example, with scope `a.b.c`, Cerbos checks for policies in this order: 1. scope `a.b.c` 2. scope `a.b` 3. scope `a` 4. no scope See [scoped policies](scoped%5Fpolicies.html) for details on how scoped policy evaluation works. ## [](#rule-matching)Rule matching For each action in the request, Cerbos evaluates all rules in the matched policy. A rule matches when all of the following are true: 1. The rule’s `actions` match the requested action. Wildcards are supported and respect the `:` delimiter (e.g. `view:*` matches `view:public` but not `view`). 2. The rule’s `roles` or `derivedRoles` include at least one of the principal’s static roles or activated derived roles. 3. If the rule has a [condition](conditions.html), the condition evaluates to `true`. ## [](#conflict-resolution)Conflict resolution This is the most important part of the evaluation model. | | For a single role and action, if both ALLOW and DENY rules match, the result is **DENY**. Deny always takes precedence. | | -------------------------------------------------------------------------------------------------------------------------- | However, if the principal has multiple roles and at least one of those roles evaluates to `EFFECT_ALLOW` for the action (with no `EFFECT_DENY` for that same role), the overall result is `EFFECT_ALLOW`. This prevents scenarios like an admin user being locked out because they also have a less-privileged role. ### [](#%5Fexample%5Fmultiple%5Froles)Example: multiple roles A principal has roles `["admin", "user"]`. ```yaml rules: - actions: ["delete"] effect: EFFECT_ALLOW roles: ["admin"] ``` * Rule: ALLOW `admin` to `delete` — matches. * No rule allows `user` to `delete`. * Result: **EFFECT\_ALLOW** — because the `admin` role has an allow. ### [](#%5Fexample%5Fdeny%5Foverrides%5Fallow)Example: deny overrides allow A principal has role `["manager"]`. ```yaml rules: - actions: ["approve"] effect: EFFECT_ALLOW roles: ["manager"] condition: match: expr: R.attr.status == "PENDING" - actions: ["approve"] effect: EFFECT_DENY roles: ["manager"] condition: match: expr: R.attr.amount > 10000 ``` If the resource has `status: "PENDING"` and `amount: 15000`, both rules match for the `manager` role. The result is **EFFECT\_DENY** because deny takes precedence over allow for the same role. ## [](#derived-roles)Derived roles [Derived roles](derived%5Froles.html) are evaluated before resource policy rules. A principal is assigned derived roles based on the conditions in the derived roles policy. These derived roles can then be referenced in resource policy rules. The derived roles policy must be imported into the resource policy via `importDerivedRoles` for it to take effect. ## [](#principal-policies)Principal policies [Principal policies](principal%5Fpolicies.html) are evaluated **before** resource policies. When a principal policy matches the requesting principal, Cerbos evaluates its rules for the requested actions first. For each action where the principal policy produces an explicit `EFFECT_ALLOW` or `EFFECT_DENY`, that decision is final — the resource policy is not consulted for that action. Actions the principal policy leaves undecided (no matching rule, or a matching rule whose condition did not hold) fall through to resource policy evaluation. ## [](#role-policies)Role policies [Role policies](role%5Fpolicies.html) apply as an **additive constraint** on top of resource policies. They do not produce `EFFECT_ALLOW` or `EFFECT_DENY` directly — instead, the `allowActions` list acts as an allow-list of actions permitted for that role on each resource. Any action not in the list is an implicit `EFFECT_DENY` for that role. For an action to be allowed, it must be permitted by **both** the role policy and a matching [resource policy](resource%5Fpolicies.html). A role policy can narrow access granted by a resource policy but cannot grant access the resource policy does not already allow — which is why a resource policy is always required. If a role policy declares `parentRoles`, it inherits and narrows those roles' permissions recursively. ## [](#evaluation-order)Evaluation order summary 1. If a [principal policy](principal%5Fpolicies.html) matches the requesting principal, evaluate it first. For each action with an explicit `EFFECT_ALLOW` or `EFFECT_DENY` from the principal policy, that is the final result and no further evaluation happens for that action. 2. For actions the principal policy did not decide, look up the resource policy for the resource kind and version (considering [scope](scoped%5Fpolicies.html)). 3. Evaluate [derived roles](derived%5Froles.html) to determine which apply to the principal. 4. Evaluate all matching rules in the resource policy. If any [role policies](role%5Fpolicies.html) apply to the principal’s roles, their `allowActions` lists further restrict which actions are permitted for those roles. 5. Apply conflict resolution: deny overrides allow for the same role; allow from any role wins across roles. 6. Any action without an explicit `EFFECT_ALLOW` is `EFFECT_DENY`. Cerbos policies ==================== There are six kinds of Cerbos policies: [Derived roles](derived%5Froles.html) Traditional RBAC roles are usually broad groupings with no context awareness. Derived roles are a way of augmenting those broad roles with contextual data to provide more fine-grained control at runtime. For example, a person with the broad `manager` role can be augmented to `manager_of_scranton_branch` by taking into account the geographic location (or another factor) and giving that derived role bearer extra privileges on resources that belong to the Scranton branch. [Resource policies](resource%5Fpolicies.html) Defines rules for actions that can be performed on a given resource. A resource is an application-specific concept that applies to anything that requires access rules. For example, in an HR application, a resource can be as coarse-grained as a full employee record or as fine-grained as a single field in the record. [Principal policies](principal%5Fpolicies.html) Defines overrides for a specific user. [Role policies](role%5Fpolicies.html) Define rules specific to a given role. Rules are defined as a list of allowable actions that apply to a particular resource. [Exported variables](variables.html#export) Defines variables to be reused in condition expressions in other policies. [Exported constants](variables.html#export-constants) Defines constants to be reused in condition expressions in other policies. Policies are evaluated based on the metadata passed in the request to the Cerbos PDP. See [Cerbos API](../api/index.html) for more information. | | View the latest documentation and example requests by accessing a running Cerbos instance using a browser (). The OpenAPI (Swagger) schema can be obtained by accessing /schema/swagger.json as well. | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Outputs ==================== You can define an optional expression to be evaluated when a policy rule is fully activated (`action`, `roles` and`derivedRoles` match and `condition` is satisfied) or partially activated (`condition` is not satisfied). The collected outputs from all the rules are included in the Cerbos API response. Outputs are supported on rules in [resource policies](resource%5Fpolicies.html),[principal policies](principal%5Fpolicies.html), and [role policies](role%5Fpolicies.html). The example below uses a resource policy; the `output` field has the same shape on all three. Output expressions are useful if you want to take specific actions in your application based on the triggered rules. For example, if your policy contains a rule that denies access if the request is issued outside working hours, it could output a string that explains the restriction. Your application could then display that back to the user so that they know the specific reason why the request was denied. Consider the following policy definition: ```yaml --- apiVersion: api.cerbos.dev/v1 resourcePolicy: version: "default" resource: "system_access" rules: - name: working-hours-only actions: ['*'] effect: EFFECT_DENY roles: ['*'] condition: match: expr: now().getHours() > 18 || now().getHours() < 8 output: when: ruleActivated: |- {"principal": P.id, "resource": R.id, "timestamp": now(), "message": "System can only be accessed between 0800 and 1800"} conditionNotMet: |- {"principal": P.id, "resource": R.id, "timestamp": now(), "message": "System can be accessed at this time"} ``` If a request is made outside working hours, the response from Cerbos would resemble the following: ```json { "requestId": "xx-010023-23459", "results": [ { "resource": { "id": "bastion_002", "kind": "system_access" }, "actions": { "login": "EFFECT_DENY" }, "meta": { "actions": { "login": { "matchedPolicy": "resource.system_access.vdefault" } } }, "outputs": [ { "src": "resource.system_access.vdefault#working-hours-only", "val": { "message": "System can only be accessed between 0800 and 1800", "principal": "john", "resource": "bastion_002", "timestamp": "2023-06-02T21:53:58.319506543+01:00" } } ] } ] } ``` If a request is made inside working hours, the response would resemble the following: ```json { "requestId": "xx-010023-23459", "results": [ { "resource": { "id": "bastion_002", "kind": "system_access" }, "actions": { "login": "EFFECT_ALLOW" }, "meta": { "actions": { "login": { "matchedPolicy": "resource.system_access.vdefault" } } }, "outputs": [ { "src": "resource.system_access.vdefault#working-hours-only", "val": { "message": "System can be accessed at this time", "principal": "john", "resource": "bastion_002", "timestamp": "2023-06-02T21:53:58.319506543+01:00" } } ] } ] } ``` | | Depending on the evaluation result of the expression(s) under the condition.match, the result of the expressionoutput.when.ruleActivated or output.when.conditionNotMet will be rendered in the output. | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Output expressions can be any valid CEL expression. You can return simple values such as strings, numbers and booleans or complex values such as maps and lists. | | Excessive use of output expressions could affect policy evaluation performance. If you use them for debugging purposes, remember to remove them before going to production. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | Principal policies ==================== Principal policies define overrides for a specific user. ```yaml --- apiVersion: "api.cerbos.dev/v1" principalPolicy: principal: daffy_duck (1) version: "dev" (2) scope: "acme.corp" (3) scopePermissions: SCOPE_PERMISSIONS_REQUIRE_PARENTAL_CONSENT_FOR_ALLOWS (4) constants: import: (5) - apatr_common_constants local: (6) test_department_id: 12345 variables: import: (7) - apatr_common_variables local: (8) is_dev_record: |- request.resource.attr.dev_record == true || request.resource.attr.department_id == constants.test_department_id rules: - resource: leave_request (9) actions: - name: dev_record_wildcard (10) action: "*" (11) condition: (12) match: expr: variables.is_dev_record effect: EFFECT_ALLOW output: (13) when: ruleActivated: |- "wildcard_override:%s".format([request.principal.id]) conditionNotMet: |- "wildcard_condition_not_met:%s".format([request.principal.id]) - resource: employee_profile actions: - name: view_employee_profile action: "*" condition: match: all: of: - expr: variables.is_dev_record - expr: request.resource.attr.public == true effect: EFFECT_ALLOW - resource: salary_record actions: - action: "*" effect: EFFECT_DENY ``` | **1** | Principal to whom this policy applies. | | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **2** | Version of this policy. Policies are uniquely identified by the principal name and version pair. You can have multiple policy versions for the same principal (e.g. production vs. staging). The version value default is special as it is the default fallback when no version is specified in the request. | | **3** | Optional [scope](scoped%5Fpolicies.html) for this policy. | | **4** | Optional [scope permission](scope%5Fpermissions.html) for this policy, defaults to SCOPE\_PERMISSIONS\_OVERRIDE\_PARENT. | | **5** | [Constant definitions](variables.html#export-constants) to import (optional). | | **6** | [Local constant definitions](variables.html#local-constants) (optional). | | **7** | [Variable definitions](variables.html#export) to import (optional). | | **8** | [Local variable definitions](variables.html#local) (optional). | | **9** | Resource to which this override applies. Wildcards are supported here. | | **10** | Optional name for the rule. | | **11** | Actions that can be performed on the resource. Wildcards are supported here. | | **12** | Optional conditions required to match this rule. | | **13** | Optional output for the action rule. You can define optional expressions to be evaluated as output depending on whether the rule is activated or not activated because of a condition failure. | Resource policies ==================== A resource policy contains all rules governing access to a single resource kind. Rules are evaluated per action and can reference roles, derived roles, and conditions. Resource policies define rules for actions that can be performed on a given resource. A resource is an application-specific concept that applies to anything that requires access rules. For example, in an HR application, a resource can be as coarse-grained as a full employee record or as fine-grained as a single field in the record. Multiple rules can be defined for the same action on a resource for different roles and/or with different conditions. If more than one rule matches a given input, the protocol for conflict resolution is as follows: * For a single role and an action, if some rules evaluated to `EFFECT_ALLOW` and some others evaluated to `EFFECT_DENY`, the overall effect is `EFFECT_DENY`. * If the principal has multiple roles and at least one of their roles evaluates to have `EFFECT_ALLOW` for the action, the overall effect is `EFFECT_ALLOW`. This prevents issues such as admin users inadvertently locking themselves out because they happen to have a less privileged role attached to them as well. ```yaml --- apiVersion: api.cerbos.dev/v1 resourcePolicy: resource: "album:object" (1) version: "default" (2) scope: "acme.corp" (3) scopePermissions: SCOPE_PERMISSIONS_REQUIRE_PARENTAL_CONSENT_FOR_ALLOWS (4) importDerivedRoles: - apatr_common_roles (5) constants: import: (6) - apatr_common_constants local: (7) corporate_network_ip_range: 10.20.0.0/16 variables: import: (8) - apatr_common_variables local: (9) is_corporate_network: |- request.principal.attr.ip_address.inIPAddrRange(constants.corporate_network_ip_range) rules: - actions: ['*'] (10) effect: EFFECT_ALLOW derivedRoles: - owner (11) - actions: ['view'] effect: EFFECT_ALLOW roles: - user (12) condition: match: expr: request.resource.attr.public == true output: (13) when: ruleActivated: |- "view_allowed:%s".format([request.principal.id]) conditionNotMet: |- "view_not_allowed:%s".format([request.principal.id]) - name: moderator_rule (14) actions: ['view', 'delete'] effect: EFFECT_ALLOW condition: match: expr: variables.is_corporate_network derivedRoles: - abuse_moderator schemas: (15) principalSchema: ref: cerbos:///principal.json (16) resourceSchema: ref: cerbos:///album/object.json (17) ``` | **1** | Kind of resource to which this policy applies. | | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **2** | Version of this policy. Policies are uniquely identified by the resource name and version pair. You can have multiple policy versions for the same resource (e.g. production vs. staging). The version value default is special as it is the default fallback when no version is specified in the request. | | **3** | Optional [scope](scoped%5Fpolicies.html) for this policy. | | **4** | Optional [scope permission](scope%5Fpermissions.html) for this policy, defaults to SCOPE\_PERMISSIONS\_OVERRIDE\_PARENT. | | **5** | Import a set of [derived roles](derived%5Froles.html) (optional). | | **6** | [Constant definitions](variables.html#export-constants) to import (optional). | | **7** | [Local constant definitions](variables.html#local-constants) (optional). | | **8** | [Variable definitions](variables.html#export) to import (optional). | | **9** | [Local variable definitions](variables.html#local) (optional). | | **10** | Actions can contain wildcards. Wildcards honour the : delimiter. E.g. a:\*:d would match a:x:d but not a:x. | | **11** | This rule applies to a derived role. | | **12** | Rules can also refer directly to static roles. The special value \* can be used to disregard roles when evaluating the rule. | | **13** | Optional output for the action rule. You can define optional expressions to be evaluated as output depending on whether the rule is activated or not activated because of a condition failure. | | **14** | Optional name for the rule. | | **15** | Optional section for defining schemas that apply to this resource kind. | | **16** | Optional schema for validating the principal attributes. | | **17** | Optional schema for validating the resource attributes. | | | For guidance on structuring resource policies for multi-tenant or hierarchical systems, [speak to us](https://www.cerbos.dev/workshop?utm%5Fcampaign=brand%5Fcerbos&utm%5Fsource=documentation&utm%5Fmedium=text). | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | | Each policy must be defined in its own file. You cannot define multiple policies in a single YAML or JSON file. See [policy repository layout](best%5Fpractices.html#%5Fpolicy%5Frepository%5Flayout) for details on organizing your policy files. | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Role policies ==================== Role policies define the set of actions a role is permitted to perform on each resource. The allowable actions list is exhaustive: any action not listed is denied for that role. Role policies are ABAC policies in which you specify a number of resources, each with a set of allowable actions that the role can carry out on the resource. Optionally, a condition can also be specified for each set of allowable actions. In the simple case, they allow you to author permissions from the view of an IdP role, rather than for a given resource. Role policies are not standalone. They apply as an additive constraint on top of [resource policies](resource%5Fpolicies.html), so an action is only allowed if it is permitted by both the role policy and a matching resource policy. A role policy cannot grant access that the resource policy does not already allow, which means a resource policy is always required. Unlike resource and principal policies, role policies do not define explicit `ALLOW` or `DENY` effects. Instead, the **allowable actions** act as an exhaustive list of actions allowed on each resource. Any resource and action pair not defined in this list is immediately denied for that role. The name of a role policy is effectively a custom role within the context of Cerbos. A role policy (custom role) can optionally define `parentRoles`, inheriting and narrowing their permissions by default. The policy can only define rules that are a strict subset of the parent role’s permissions and cannot introduce any extra rules beyond what the parent roles allow. They can immediately DENY an action but if they ALLOW an action, a parent policy higher up the scope chain must also ALLOW the same action. A parent role can be either an arbitrary IdP role or the name of another role policy within the system. Parent role resolution is recursive—if a custom role inherits from another custom role that also has parent roles, it inherits and narrows their permissions as well. ```yaml --- apiVersion: api.cerbos.dev/v1 rolePolicy: role: "acme_admin" (1) version: "default" (2) scope: "acme.hr.uk" (3) parentRoles: (4) - admin constants: import: (5) - apatr_common_constants local: (6) corporate_network_ip_range: 10.20.0.0/16 variables: import: (7) - apatr_common_variables local: (8) is_corporate_network: |- P.attr.ip_address.inIPAddrRange(constants.corporate_network_ip_range) rules: - resource: leave_request (9) allowActions: (10) - view:* (11) - deny - resource: salary_record name: salary_record_owner (12) allowActions: - edit condition: (13) match: expr: R.attr.owner == P.id && variables.is_corporate_network output: (14) when: ruleActivated: |- "salary_record_edited:%s".format([P.id]) conditionNotMet: |- "salary_record_edit_blocked:%s".format([P.id]) - resource: "*" (15) allowActions: ["create"] ``` | **1** | The role to which this policy applies. | | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **2** | Version of this policy. Policies are uniquely identified by the role name and version pair. You can have multiple policy versions for the same role (e.g. production vs. staging). The version value default is special as it is the default fallback when no version is specified in the request. | | **3** | Optional principal [scope](scoped%5Fpolicies.html) for this policy. | | **4** | The list of parent roles that the custom role inherits. | | **5** | [Constant definitions](variables.html#export-constants) to import (optional). | | **6** | [Local constant definitions](variables.html#local-constants) (optional). | | **7** | [Variable definitions](variables.html#export) to import (optional). | | **8** | [Local variable definitions](variables.html#local) (optional). | | **9** | The resource to which the following rule applies. | | **10** | The list of allowable actions that the role can carry out on the given resource. | | **11** | Wildcard actions are supported. | | **12** | Optional name for the rule. | | **13** | A condition that must be met for the action to be allowed. | | **14** | Optional output for the action rule. You can define optional expressions to be evaluated as output depending on whether the rule is activated or not activated because of a condition failure. | | **15** | Wildcard resources are also supported. | | | For a walkthrough of translating business access requirements into Cerbos policies, see [Mapping business requirements to authorization policy](https://www.cerbos.dev/blog/mapping-business-requirements-to-authorization-policy). | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Schemas ==================== Cerbos policies rely on context data about the principal and the resource(s) that are submitted through the `attr` fields of the [API request](../api/index.html). While the free-form nature of these fields gives you maximum flexibility to author policies that work on data of any shape or form, they can become difficult to reason about and make it harder to enforce system-wide standards and conventions. Using the [JSON Schema](http://json-schema.org) support built into Cerbos, you can define schemas for all your principal and resource attributes on a per-resource basis by specifying them in the resource policy. The Cerbos PDP will validate the incoming requests and either log warnings or completely reject them based on the schema enforcement configuration in effect. ## [](#%5Fdefine%5Fschemas)Define schemas Cerbos schemas are standard [JSON Schemas](http://json-schema.org/specification.html) (draft 2020-12). If you are using any of `disk`, `git` or `blob` [storage drivers](../configuration/storage.html) the schemas are expected to be in a special directory named `_schemas` located at the root of the storage directory or bucket. Use the [Admin API](../api/admin%5Fapi.html) to add or update schemas if you are using one of the database drivers. To avoid repetition, you can define common schema fragments inline using `$defs` or refer to other schemas using `$ref` (see ). When using `$ref` to refer to another schema stored in Cerbos storage, make sure to use an absolute URL with `cerbos` as the scheme. For example, use `cerbos:///common/address.json` to refer to a schema file stored in `_schemas/common/address.json` (if using one of the disk-based stores). This ensures that policies remain portable between different environments. customer.json: a schema that references another schema to avoid repetition ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "first_name": { "type": "string" }, "last_name": { "type": "string" }, "shipping_address": { "$ref": "cerbos:///address.json" }, "billing_address": { "$ref": "cerbos:///address.json" } }, "required": ["first_name", "last_name", "shipping_address", "billing_address"] } ``` address.json: the schema referenced by customer.json ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "street_address": { "type": "string" }, "city": { "type": "string" }, "state": { "type": "string" } }, "required": ["street_address", "city", "state"] } ``` ## [](#%5Fvalidate%5Frequests%5Fusing%5Fschemas)Validate requests using schemas First, update your resource policy to point to the schemas that should be used to validate requests for that resource kind. For example, the following resource policy requires all requests for `album:object` resource kind to be validated using `principal.json` for the principal attributes and `album/object.json` for the resource attributes respectively. Example ```yaml --- apiVersion: api.cerbos.dev/v1 resourcePolicy: importDerivedRoles: - apatr_common_roles resource: "album:object" rules: - actions: ['create'] effect: EFFECT_ALLOW derivedRoles: - owner - actions: ['view'] effect: EFFECT_ALLOW roles: - user condition: match: expr: request.resource.attr.public == true schemas: (1) principalSchema: (2) ref: cerbos:///principal.json resourceSchema: (3) ref: cerbos:///album/object.json ignoreWhen: (4) actions: ['create', 'delete:*'] ``` | **1** | Schema definition block. Optional. Leave this out if you do not want to use schema validation for this resource type. | | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **2** | Schema for validating the principal attributes. Optional. Leave this out if you do not want to validate the principal. | | **3** | Schema for validating the resource attributes. Optional. Leave this out if you do not want to validate the resource. | | **4** | Ignore block. Optional. Define the actions for which schema validation should be ignored. This is useful for special cases like CREATE where your resource might not have all the required attributes to pass schema validation. | Finally, [configure the schema enforcement level](../configuration/schema.html) of the Cerbos PDP to either `warn` or `reject` and restart it. Now the PDP will validate any requests where the matching resource policy has schemas specified. | | If enforcement level is reject and the request is invalid according to the schema, the effect for all actions will be set to EFFECT\_DENY. If enforcement level is warn, then Cerbos will still evaluate the policies and return the effects determined by the policy. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Example: CheckResourceSet API response containing validation errors ```json { "requestId": "test", "resourceInstances": { "XX125": { "actions": { "approve": "EFFECT_DENY", "create": "EFFECT_DENY", "defer": "EFFECT_ALLOW", "view:public": "EFFECT_ALLOW" }, "validationErrors": [ { "path": "/department", "message": "value must be one of \"marketing\", \"engineering\"", "source": "SOURCE_PRINCIPAL" }, { "path": "/department", "message": "value must be one of \"marketing\", \"engineering\"", "source": "SOURCE_RESOURCE" } ] } } } ``` Scope Permissions ==================== `scopePermissions` is a setting applied to resource and principal policies that impacts how rules are evaluated within a scope hierarchy. It defines whether policies in a given scope can **override** parent scope rules or whether they can only **restrict** the permissions granted by parent scopes. All resource or principal policies within the same scope **must** use the same `scopePermissions` setting. If conflicting settings are detected within a shared scope, a build-time error will occur. There are two available settings: * `SCOPE_PERMISSIONS_OVERRIDE_PARENT` * `SCOPE_PERMISSIONS_REQUIRE_PARENTAL_CONSENT_FOR_ALLOWS` | | By default, resource and principal policies use SCOPE\_PERMISSIONS\_OVERRIDE\_PARENT unless explicitly set otherwise. | | ------------------------------------------------------------------------------------------------------------------------ | ### [](#%5Fscope%5Fpermissions%5Foverride%5Fparent)SCOPE\_PERMISSIONS\_OVERRIDE\_PARENT This is the default evaluation strategy for scoped policies. Cerbos starts evaluating policies from the bottom of the scope chain and moves up. The first policy to produce a decision for a given action is the winner. Any policies further up the chain cannot influence that decision. * If an input matches a rule and its condition is met, the specified effect is applied (no need to check parents). * If a rule is matched but its condition is not met, or if a rule is not matched, evaluation continues up the hierarchy. ### [](#%5Fscope%5Fpermissions%5Frequire%5Fparental%5Fconsent%5Ffor%5Fallows)SCOPE\_PERMISSIONS\_REQUIRE\_PARENTAL\_CONSENT\_FOR\_ALLOWS When a policy is configured with `scopePermissions: SCOPE_PERMISSIONS_REQUIRE_PARENTAL_CONSENT_FOR_ALLOWS`, it **inherits and restricts** the permissions of parent scopes. Policies at this level must define rules within the maximum set of permissions allowed by parent policies—they cannot introduce new permissions that exceed what a parent scope already permits. In this mode, an `ALLOW` rule that matches an action doesn’t immediately generate an `ALLOW` decision. A parent policy higher up in the scope chain must also `ALLOW` that same action in order to produce a definitive decision. However, if a rule is matched but its condition is not met, the request is implicitly denied. * If an input is not matched, evaluation continues up the scope hierarchy. * If a rule is matched but its condition is not met, an implicit DENY is issued. * If a rule matches and the condition is met, evaluation continues to parent policies to verify that the action is also allowed at a higher level. Scoped policies ==================== | | Scoped Policies are optional and are only evaluated if a "scope" is passed in the request, and there are matching "scope" attributes defined in the policies. | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | | Empty scope can also be represented by setting the "scope" to value "." in the request. | | ------------------------------------------------------------------------------------------ | | | Resource and principal policies can define "scopePermissions", which affects how rules are applied across scopes. See the [scope permissions documentation](scope%5Fpermissions.html) for more details. | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Scoped policies offer a way to model hierarchical relationships that regularly occur in many situations. Typically, the requirement is to have a base set of policies that can then be overridden for specific cases. For example, a multi-tenant SaaS system could have a standard set of access rules that can then be customised to suit the requirements of different tenants. Another example is a large organization that might want to have regional or departmental customisations to their global access rules. ![hierarchy](_images/hierarchy.png) Cerbos resource and principal policies have an optional `scope` field that can be used to indicate that they are part of a set of policies that must be evaluated together. Additionally, resource and principal policies within the same scope must use the same `scopePermissions` setting to define how rules interact across scope levels. ```yaml apiVersion: api.cerbos.dev/v1 resourcePolicy: version: "default" scope: "acme.corp" (1) scopePermissions: SCOPE_PERMISSIONS_OVERRIDE_PARENT (2) resource: "album:object" rules: - actions: ['*'] effect: EFFECT_ALLOW roles: ["admin"] ``` | **1** | Scope definition | | ----- | ------------------------- | | **2** | Scope permissions setting | The value of `scope` is a dot-separated string where each dotted segment defines an ancestor. During policy evaluation, the Cerbos engine starts with the most specific scoped policy and moves up the hierarchy. NOTE: The value of the `scopePermissions` field affects the policy evaluation behaviour. See [scope permissions](scope%5Fpermissions.html) for more information. For example, consider a policy with the scope `a.b.c`. The Cerbos engine could process up to four policies to arrive at the final decision: * scope `a.b.c` * scope `a.b` * scope `a` * scope \`\` (no scope) To illustrate, consider the following Check request: ```json { "requestId": "test01", "actions": ["view", "comment"], "resource": { "kind": "album:object", "policyVersion": "default", "scope": "customer.abc", (1) "instances": { "XX125": { "attr": { "owner": "alicia", "public": false, "tags": ["x", "y"], } } } }, "principal": { "id": "alicia", "policyVersion": "default", "scope": "customer", (2) "roles": ["user"], "attr": { "geography": "GB" } } } ``` | **1** | Optional resource scope | | ----- | ------------------------ | | **2** | Optional principal scope | When processing the above request, the decision flow chart for the Cerbos engine would look like the following: ![decision flow](_images/decision_flow.png) ## [](#%5Fworking%5Fwith%5Fscoped%5Fpolicies)Working with scoped policies * The policy without any scope defined is always the base policy. It is used by default if a request does not specify any scope. * Scope permissions must be consistent within the same scope. If conflicting `scopePermissions` settings are detected in policies within a shared scope, a build-time error will occur. * Scope traversal behaviour depends on `scopePermissions`: * With `SCOPE_PERMISSIONS_OVERRIDE_PARENT`, the first policy to return a decision wins for each action. * With `SCOPE_PERMISSIONS_REQUIRE_PARENTAL_CONSENT_FOR_ALLOWS`, leaf nodes can only **restrict** access and must conform to parent permissions. * There must be no gaps in the policy chain. For example, if you define a policy with scope `a.b.c`, then policies with scopes `a.b`, `a`, and no-scope should also exist in the policy repository. * [Schemas](schemas.html) must be the same among all the policies in the chain. The schemas used to validate the request are taken from the base policy (policy without a scope). Schemas defined in other policies of the chain will be ignored. * First match wins (when using `SCOPE_PERMISSIONS_OVERRIDE_PARENT`): Scoped policies are evaluated from the most specific to the least specific. The first policy to produce a decision (ALLOW/DENY) for an action is the winner. The remaining policies cannot override the decision for that particular action. * Parent constraints apply (when using `SCOPE_PERMISSIONS_REQUIRE_PARENTAL_CONSENT_FOR_ALLOWS`): The most specific policies can only **restrict permissions** further, not grant new ones. * **Explicit imports for derived roles and variables**: Variables and derived roles imports are not inherited between policies. Explicitly import any derived roles and re-define any variables in each policy that requires them. * Unless [lenient scope search](../configuration/engine.html#lenient%5Fscopes) is enabled, a policy file matching the exact scope requested in the API request must exist in the store. Variables and constants ==================== ## [](#variables)Variables You can use variables to reduce duplication in [policy condition expressions](conditions.html). Variables may either be defined locally within a policy, or in a standalone `exportVariables` policy file that can be imported by other policies. ### [](#local)Defining local variables Local variables are only accessible from the policy they are defined. In particular, local variables defined for derived roles can’t be used in resource policies that import the derived roles. ```yaml --- apiVersion: api.cerbos.dev/v1 resourcePolicy: variables: local: (1) flagged_resource: request.resource.attr.flagged label: '"latest"' # assigning a string literal teams: '["red", "blue"]' # assigning an array literal lookup: '{"red": 9001, "blue": 0}' # assigning a map literal # ... ``` | **1** | Map of variable name to expression. | | ----- | ----------------------------------- | ### [](#export)Defining and importing exported variables To reuse variables between policies, they can be exported from a separate file. ```yaml --- apiVersion: api.cerbos.dev/v1 description: Common variables used within the Apatr app exportVariables: name: apatr_common_variables (1) definitions: (2) flagged_resource: request.resource.attr.flagged label: '"latest"' # assigning a string literal teams: '["red", "blue"]' # assigning an array literal lookup: '{"red": 9001, "blue": 0}' # assigning a map literal ``` | **1** | Name to use when importing this set of variables. | | ----- | ------------------------------------------------- | | **2** | Map of variable name to expression. | Other policies can then import the variables by name. ```yaml --- apiVersion: api.cerbos.dev/v1 resourcePolicy: variables: import: (1) - apatr_common_variables # ... ``` | **1** | List of names of variable sets to import. | | ----- | ----------------------------------------- | ### [](#%5Fusing%5Fvariables%5Fin%5Fpolicy%5Fconditions)Using variables in policy conditions Variables can be referenced via the `variables` (aliased to `V`) special variable in policy condition expressions. ```yaml --- condition: match: expr: variables.flagged_resource ``` Local and imported variable definitions are merged, and each variable is evaluated before any rule condition. If a variable is defined in more than one location, the policy will fail to compile. ### [](#%5Ftop%5Flevel%5Fvariables%5Ffield)Top-level variables field In earlier versions of Cerbos, local variables were defined in a top-level `variables` field in the policy file. This field is deprecated in favour of the `variables.local` section within the policy body. For backwards compatibility, the deprecated top-level field is merged with the `variables.local` section in derived roles, resource, and principal policies. ## [](#constants)Constants Variables are expressions that are evaluated at runtime. That makes them slightly awkward to use with literal values, because you have to quote the value to make it a valid [Common Expression Language (CEL)](https://github.com/google/cel-spec/blob/master/doc/intro.md) expression. Constants are an alternative to defining variables with literal values, which allow the values to be written using standard YAML or JSON syntax. ### [](#local-constants)Defining local constants Local constants are only accessible from the policy they are defined. In particular, local constants defined for derived roles can’t be used in resource policies that import the derived roles. ```yaml --- apiVersion: api.cerbos.dev/v1 resourcePolicy: constants: local: (1) label: latest teams: - red - blue lookup: red: 9001 blue: 0 # ... ``` | **1** | Map of constant name to value. | | ----- | ------------------------------ | ### [](#export-constants)Defining and importing exported constants To reuse constants between policies, they can be exported from a separate file. ```yaml --- apiVersion: api.cerbos.dev/v1 description: Common constants used within the Apatr app exportConstants: name: apatr_common_constants (1) definitions: (2) label: latest teams: - red - blue lookup: red: 9001 blue: 0 ``` | **1** | Name to use when importing this set of constants. | | ----- | ------------------------------------------------- | | **2** | Map of constant name to value. | Other policies can then import the constants by name. ```yaml --- apiVersion: api.cerbos.dev/v1 resourcePolicy: constants: import: (1) - apatr_common_constants # ... ``` | **1** | List of names of constant sets to import. | | ----- | ----------------------------------------- | ### [](#%5Fusing%5Fconstants%5Fin%5Fpolicy%5Fconditions)Using constants in policy conditions Constants can be referenced via the `constants` (aliased to `C`) special variable in policy condition expressions. ```yaml --- condition: match: expr: constants.lookup[request.principal.attr.team] > 9000 ``` Local and imported constant definitions are merged. If a constant is defined in more than one location, the policy will fail to compile. Field-level and column-level security ==================== Applications often need to control access not just at the resource level but at the field level. For example, an HR system might allow all employees to view a colleague’s name and department, but restrict salary information to HR staff and managers. ## [](#%5Fpattern%5F1%5Factions%5Fas%5Ffield%5Foperations)Pattern 1: Actions as field operations Model each field-action combination as a distinct action using the hierarchical action naming convention (`:` delimiter). Resource policy with field-level actions ```yaml apiVersion: api.cerbos.dev/v1 resourcePolicy: resource: "employee_record" version: "default" rules: - actions: ["name:view", "department:view", "title:view"] effect: EFFECT_ALLOW roles: ["employee"] - actions: ["salary:*"] effect: EFFECT_ALLOW roles: ["hr_admin"] - actions: ["salary:view"] effect: EFFECT_ALLOW derivedRoles: - direct_manager ``` Check multiple field-level actions in a single request: Request ```json { "principal": { "id": "alice", "roles": ["employee"] }, "resources": [ { "resource": { "kind": "employee_record", "id": "emp-001" }, "actions": ["name:view", "department:view", "salary:view"] } ] } ``` Response ```json { "results": [ { "resource": { "id": "emp-001", "kind": "employee_record" }, "actions": { "name:view": "EFFECT_ALLOW", "department:view": "EFFECT_ALLOW", "salary:view": "EFFECT_DENY" } } ] } ``` Alice can view name and department but not salary. | | Wildcards respect the : delimiter. A rule with actions: \["salary:\*"\] matches both salary:view and salary:edit. | | -------------------------------------------------------------------------------------------------------------------- | ## [](#%5Fpattern%5F2%5Foutput%5Fexpressions%5Ffor%5Fpermitted%5Ffields)Pattern 2: Output expressions for permitted fields Use [output expressions](../policies/outputs.html) to return the set of fields a user can access, so your application can dynamically build responses. Resource policy with output expressions ```yaml apiVersion: api.cerbos.dev/v1 resourcePolicy: resource: "employee_record" version: "default" rules: - name: base-fields actions: ["view"] effect: EFFECT_ALLOW roles: ["employee"] output: when: ruleActivated: |- {"viewable_fields": ["name", "department", "title"]} - name: sensitive-fields actions: ["view"] effect: EFFECT_ALLOW roles: ["hr_admin"] output: when: ruleActivated: |- {"viewable_fields": ["name", "department", "title", "salary", "ssn"]} ``` The response `outputs` array contains the field lists from all matched rules. Your application merges these to determine the full set of accessible fields. Response outputs ```json { "outputs": [ { "src": "resource.employee_record.vdefault#base-fields", "val": { "viewable_fields": ["name", "department", "title"] } } ] } ``` ## [](#%5Fchoosing%5Fa%5Fpattern)Choosing a pattern Actions as field operations Best when you need yes/no decisions per field and want to leverage Cerbos wildcard matching. Works well with [PlanResources](../api/index.html#resources-query-plan) for query-level filtering. Output expressions Best when you want to return a dynamic set of permitted fields from a single action check. More flexible but requires application-side logic to process the output. These patterns can be combined — for example, use action-based checks for write operations and output expressions for read operations. Filtering resources by permission ==================== Applications often need to display only the resources a user is authorized to see — for example, showing only the projects a user can view, or the documents they can edit. Because Cerbos is stateless and doesn’t store your data, it can’t return a list of resource IDs directly. Instead, Cerbos provides the `PlanResources` API, which returns a query plan that your application converts into a database filter. ## [](#%5Fthe%5Fapproach%5Fplanresources)The approach: PlanResources The [PlanResources API](../api/index.html#resources-query-plan) takes a principal, a resource kind, and an action. It returns an abstract syntax tree (AST) of the conditions that must be satisfied for the principal to access resources of that kind for that action. Your application converts this AST into a native database query. PlanResources request ```json { "requestId": "filter-example", "action": "view", "principal": { "id": "alice", "roles": ["user"], "attr": { "department": "engineering" } }, "resource": { "kind": "document", "policyVersion": "default" } } ``` ## [](#%5Fplan%5Fresult%5Fkinds)Plan result kinds The response contains one of three result kinds: `ALWAYS_ALLOWED` The principal can access all resources of this kind for this action. No filtering is needed. `ALWAYS_DENIED` The principal cannot access any resources of this kind. Return an empty result set. `CONDITIONAL` The response includes a filter condition as an AST. Convert this to a database query to fetch only the resources the principal can access. Example CONDITIONAL response ```json { "requestId": "filter-example", "action": "view", "resourceKind": "document", "filter": { "kind": "KIND_CONDITIONAL", "condition": { "expression": { "operator": "eq", "operands": [ { "variable": "request.resource.attr.department" }, { "value": "engineering" } ] } } } } ``` In this example, the plan tells your application to filter documents where `department = "engineering"`. ## [](#%5Fquery%5Fplan%5Fadapters)Query plan adapters Cerbos provides reference adapter implementations for common data layers as guidance for your own implementations. See [query plan adapters](query-plan-adapters/index.html) for the full list, which includes: * [Prisma](query-plan-adapters/prisma.html) (TypeScript) * [Drizzle](query-plan-adapters/drizzle.html) (TypeScript) * [Mongoose](query-plan-adapters/mongoose.html) (TypeScript) * [SQLAlchemy](query-plan-adapters/sqlalchemy.html) (Python) * [Convex](query-plan-adapters/convex.html) (TypeScript) * [LangChain / ChromaDB](query-plan-adapters/langchain-chromadb.html) (TypeScript) ## [](#%5Fwhen%5Fto%5Fuse%5Fcheckresources%5Fvs%5Fplanresources)When to use CheckResources vs PlanResources Use [CheckResources](../api/index.html#check-resources) when you already have specific resource instances and need a yes/no decision for each. Use `PlanResources` when you need to fetch resources from a data store and want to filter them by permission at the query level. | | For best performance, use PlanResources to push authorization filters into your database query rather than fetching all resources and checking each one individually with CheckResources. | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | | PlanResources evaluates policies based on the [conditions](../policies/conditions.html) defined in your policies. Conditions that reference resource attributes will appear in the AST. Conditions that reference only principal attributes are resolved immediately and won’t appear in the filter. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Modeling hierarchies and multi-tenancy ==================== Many applications need to model organizational hierarchies (departments, teams, regions) or multi-tenant access patterns. Cerbos supports these through a combination of principal attributes, hierarchy functions, scoped policies, and derived roles. ## [](#%5Forganizational%5Fhierarchies)Organizational hierarchies When a manager needs access to their reports' resources, or a regional admin needs access to resources in their region, pass hierarchy information as principal attributes and use the hierarchy CEL functions in conditions. ### [](#%5Fexample%5Fmanager%5Faccess%5Fto%5Fdirect%5Freports)Example: manager access to direct reports Derived role definition ```yaml apiVersion: "api.cerbos.dev/v1" derivedRoles: name: hr_roles definitions: - name: direct_manager parentRoles: ["manager"] condition: match: expr: > hierarchy(P.attr.managed_scope).immediateParentOf(hierarchy(R.attr.scope)) ``` Resource policy using the derived role ```yaml apiVersion: api.cerbos.dev/v1 resourcePolicy: resource: "leave_request" version: "default" importDerivedRoles: - hr_roles rules: - actions: ["view", "approve"] effect: EFFECT_ALLOW derivedRoles: - direct_manager ``` Request with hierarchy attributes ```json { "principal": { "id": "alice", "roles": ["manager"], "attr": { "managed_scope": "acme.engineering" } }, "resources": [ { "resource": { "kind": "leave_request", "id": "lr-001", "attr": { "scope": "acme.engineering.backend" } }, "actions": ["view", "approve"] } ] } ``` In this example, Alice manages `acme.engineering`. The leave request belongs to `acme.engineering.backend`, which is an immediate child of her scope, so the `direct_manager` derived role activates. ### [](#%5Favailable%5Fhierarchy%5Ffunctions)Available hierarchy functions | Function | Description | | ----------------- | -------------------------------------------------------- | | ancestorOf | Returns true if the hierarchy is an ancestor of another | | descendentOf | Returns true if the hierarchy is a descendent of another | | immediateChildOf | Returns true if the hierarchy is a first-level child | | immediateParentOf | Returns true if the hierarchy is a first-level parent | | commonAncestors | Returns the common ancestor hierarchy | | overlaps | Returns true if one hierarchy is a prefix of the other | | siblingOf | Returns true if both hierarchies share the same parent | See [hierarchy functions](../policies/conditions.html#hierarchies) for the full reference. ## [](#%5Fmulti%5Ftenancy%5Fwith%5Fscoped%5Fpolicies)Multi-tenancy with scoped policies When different tenants need different access rules, use [scoped policies](../policies/scoped%5Fpolicies.html) to define a base set of rules with tenant-specific overrides. Base policy (no scope) ```yaml apiVersion: api.cerbos.dev/v1 resourcePolicy: resource: "document" version: "default" rules: - actions: ["view"] effect: EFFECT_ALLOW roles: ["user"] ``` Scoped policy for tenant "acme" ```yaml apiVersion: api.cerbos.dev/v1 resourcePolicy: resource: "document" version: "default" scope: "acme" rules: - actions: ["view", "share"] effect: EFFECT_ALLOW roles: ["user"] ``` The request must include the scope on the resource (and optionally the principal) to activate tenant-specific rules: ```json { "principal": { "id": "bob", "roles": ["user"], "scope": "acme" }, "resources": [ { "resource": { "kind": "document", "id": "doc-001", "scope": "acme", "attr": {} }, "actions": ["view", "share"] } ] } ``` With scope `acme` on the resource, the scoped policy is evaluated and Bob can both view and share documents. Without a scope (or with a different scope), only the base policy applies and Bob can only view. | | Scopes can be set independently on both the resource and the principal. The resource scope controls which resource policy chain is evaluated, and the principal scope controls which principal policy chain is evaluated. See [scoped policies](../policies/scoped%5Fpolicies.html) for the full evaluation flow. | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#%5Ftenant%5Fspecific%5Fcustom%5Froles)Tenant-specific custom roles When tenants can define their own roles and permissions, use the "static policy / dynamic context" approach. Define a single set of policies and pass tenant-specific role assignments as principal attributes. Request with per-workspace role assignments ```json { "principal": { "id": "charlie", "roles": ["USER"], "attr": { "workspaces": { "ws-001": { "role": "EDITOR" }, "ws-002": { "role": "VIEWER" } } } } } ``` Condition checking the workspace role ```yaml condition: match: expr: P.attr.workspaces[R.id].role == "EDITOR" ``` This approach lets tenants manage role assignments in your application without changing policies. See [best practices](../policies/best%5Fpractices.html) for a detailed walkthrough of this pattern. ## [](#%5Fchoosing%5Fa%5Fpattern)Choosing a pattern Hierarchy functions Best for org structures, reporting lines, and geographic regions. Scoped policies Best when tenants need entirely different rule sets. Dynamic context Best when tenants customize permissions within the same rule structure. These patterns can be combined. For example, scoped policies for tenant-level rule overrides combined with hierarchy functions for org-structure checks within a tenant. For a full working example, see the [multi-tenant SaaS demo](https://github.com/cerbos/demo-multitenant-saas). Working with JWT claims in policies ==================== When [auxiliary data](../configuration/auxdata.html) is configured, Cerbos can verify and decode JWTs sent with requests. The decoded claims are available in policy conditions as `request.auxData.jwt`. This page covers common patterns for using JWT claims in [conditions](../policies/conditions.html). ## [](#%5Faccessing%5Fclaims)Accessing claims Claims are accessed using dot notation from the `request.auxData.jwt` object. Simple string claim ```yaml condition: match: expr: request.auxData.jwt.iss == "https://auth.example.com" ``` Nested claim ```yaml condition: match: expr: request.auxData.jwt.custom.department == "engineering" ``` | | Use the has() macro to safely check for the existence of optional claims before accessing them: has(request.auxData.jwt.custom) && request.auxData.jwt.custom.role == "admin". | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#%5Farray%5Fclaims)Array claims JWT claims like `roles`, `groups`, or `scope` are often arrays or space-delimited strings. Check if a value exists in an array claim ```yaml condition: match: expr: > "admin" in request.auxData.jwt.roles ``` Check if any value from one list exists in another ```yaml condition: match: expr: > request.auxData.jwt.groups.exists(g, g in ["engineering", "platform"]) ``` Space-delimited scope strings (common in OAuth2) ```yaml condition: match: expr: > "read:documents" in request.auxData.jwt.scope.split(" ") ``` ## [](#%5Fnumeric%5Fclaims)Numeric claims JWT numeric values may arrive as different types depending on the token issuer. Use explicit type conversion for safe comparisons: ```yaml condition: match: expr: > int(request.auxData.jwt.org_id) == int(R.attr.org_id) ``` ## [](#%5Fusing%5Fjwt%5Fclaims%5Fin%5Fderived%5Froles)Using JWT claims in derived roles A common pattern is to create [derived roles](../policies/derived%5Froles.html) based on JWT claims. Grant an `internal_user` derived role based on a custom claim ```yaml apiVersion: "api.cerbos.dev/v1" derivedRoles: name: jwt_roles definitions: - name: internal_user parentRoles: ["user"] condition: match: expr: > has(request.auxData.jwt.is_internal) && request.auxData.jwt.is_internal == true ``` ## [](#%5Fconfiguration)Configuration JWT verification must be configured in the Cerbos server config under `auxData`. See [AuxData configuration](../configuration/auxdata.html) for setup details including JWKS endpoints, local keys, and accepted issuers. | | JWT claims are read-only context data. JWKS keys are cached for performance, but individual JWTs are validated on every request. Cerbos does not store or cache the tokens themselves. | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | For a complete example of integrating Cerbos with a JWT-based authentication flow, see the [JWT tutorial](authentication/jwt/index.html). Integrating permission checks into your user interface ==================== It’s a common requirement to make permission checks in the user interface layer of your application. For example, you might want to hide the "Edit" button if the current user isn’t allowed to edit the corresponding resource. You can tackle this by checking the user’s permissions in the back end of your application and including the results in your API responses, by calling the Cerbos PDP directly from the browser, or by evaluating your policies in the browser. | | Checking permissions in the user interface is not a substitute for performing checks in the back end. | | -------------------------------------------------------------------------------------------------------- | ## [](#%5Fincluding%5Fpermissions%5Fin%5Fapi%5Fresponses)Including permissions in API responses You can add a `permissions` field to relevant API responses, and populate it by calling the Cerbos PDP’s [CheckResources](../api/index.html#check-resources) API with multiple actions. For example, an API response from a blog application might look like this: ```json { "blog_post": { "title": "Why are we building Cerbos?", "author": "Emre Baran & Charith Ellawala", "permissions": { "edit": true, "delete": false } } } ``` This pattern can be readily tailored to your requirements. It’s a great way to ensure that the front and back ends of your application agree on your policy rules. ## [](#%5Fcalling%5Fthe%5Fcerbos%5Fpdp%5Ffrom%5Fthe%5Fbrowser)Calling the Cerbos PDP from the browser The Cerbos PDP API is available via REST, so you can perform permissions checks directly from the browser. The [@cerbos/http JavaScript SDK](https://www.npmjs.com/package/@cerbos/http) wraps the REST API to make it easier to integrate into your application. | | Exposing the PDP to the internet has security and performance implications. An attacker could use the API to probe your authorization policies much more easily than through your user interface. You could mitigate this to some extent by keeping the PDP behind a reverse proxy that authenticates and rate-limits API calls. You might also want to use a separate deployment with only a subset of your policies. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#%5Fevaluating%5Fpolicies%5Fin%5Fthe%5Fbrowser)Evaluating policies in the browser You can use [Cerbos Hub’s embedded PDPs](https://docs.cerbos.dev/cerbos-hub/decision-points-embedded) to evaluate your authorization policies directly in the browser. This allows you to perform permission checks on the front end without changing the back end. ## [](#%5Fhandling%5Fcors)Handling CORS If you call the Cerbos PDP directly from a browser, you may encounter CORS (Cross-Origin Resource Sharing) errors. This happens when your web application and the Cerbos PDP are served from different origins. To resolve this, configure CORS in your Cerbos server configuration: ```yaml server: cors: allowedOrigins: - "https://your-app.example.com" allowedHeaders: - "content-type" - "authorization" ``` See [server configuration](../configuration/server.html) for all available CORS options. | | Avoid setting allowedOrigins to \* in production. Be as specific as possible with the origins you allow. | | ----------------------------------------------------------------------------------------------------------- | If your Cerbos PDP sits behind a reverse proxy (such as Nginx or Envoy), ensure that only one layer is adding CORS headers. Duplicate `Access-Control-Allow-Origin` headers will cause browsers to reject the response. ## [](#%5Fshould%5Fyou%5Fcall%5Fcerbos%5Ffrom%5Fthe%5Fbrowser)Should you call Cerbos from the browser? For most applications, the recommended approach is to call Cerbos from your back end and include the permission results in your API responses. This has several advantages: * Your Cerbos PDP doesn’t need to be exposed to the internet. * You avoid CORS configuration entirely. * Sensitive context data (like resource attributes) stays server-side. * Permission checks and data fetching happen in a single round trip. See [Including permissions in API responses](#%5Fincluding%5Fpermissions%5Fin%5Fapi%5Fresponses) above for this approach. Calling Cerbos directly from the browser can be useful during development or for applications that use the Cerbos PDP as a shared service accessible from multiple front-end clients. If you take this approach, be aware that any data sent in the request (principal attributes, resource attributes) is visible to the end user via browser developer tools. Install from binary ==================== Cerbos PDP binaries are available for multiple operating systems and architectures. See the [releases page](https://github.com/cerbos/cerbos/releases/tag/v0.55.0) for all available downloads. | OS | Arch | Bundle | | ----- | --------- | -------------------------------------- | | Linux | x86-64 | cerbos\_0.55.0\_Linux\_x86\_64.tar.gz | | Linux | arm64 | cerbos\_0.55.0\_Linux\_arm64.tar.gz | | MacOS | universal | cerbos\_0.55.0\_Darwin\_all.tar.gz | | MacOS | x86-64 | cerbos\_0.55.0\_Darwin\_x86\_64.tar.gz | | MacOS | arm64 | cerbos\_0.55.0\_Darwin\_arm64.tar.gz | You can download the binaries by running the following command. Substitute `` with the appropriate value from the above table. ```sh curl -L -o cerbos.tar.gz "https://github.com/cerbos/cerbos/releases/download/v0.55.0/" tar xvf cerbos.tar.gz chmod +x cerbos ``` | | Cerbos PDP binaries are signed using [sigstore](https://www.sigstore.dev) tools during the automated build process and the verification bundle is published along with the binary as .sigstore.json. The following example demonstrates how to verify the Linux X86\_64 bundle archive. \# Download the bundle archive curl -L \\ \-o cerbos\_0.55.0\_Linux\_x86\_64.tar.gz \\ "https://github.com/cerbos/cerbos/releases/download/v0.55.0/cerbos\_0.55.0\_Linux\_x86\_64.tar.gz" \# Download the verification bundle curl -L \\ \-o cerbos\_0.55.0\_Linux\_x86\_64.tar.gz.sigstore.json \\ "https://github.com/cerbos/cerbos/releases/download/v0.55.0/cerbos\_0.55.0\_Linux\_x86\_64.tar.gz.sigstore.json" \# Verify the signature cosign verify-blob \\ \--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \\ \--certificate-identity="https://github.com/cerbos/cerbos/.github/workflows/release.yaml@refs/tags/v0.55.0" \\ \--bundle="cerbos\_0.55.0\_Linux\_x86\_64.tar.gz.sigstore.json" \\ "cerbos\_0.55.0\_Linux\_x86\_64.tar.gz" | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#linux-packages)Linux packages Cerbos PDP DEB and RPM packages can be installed on any Linux distribution that supports one of those package formats. You can download the appropriate package for your system from the [releases page](https://github.com/cerbos/cerbos/releases/tag/v0.55.0). | | Cerbos PDP packages are currently only designed to work with systems where systemd is the init system. If you use a different init system, consider installing cerbos from the tarballs instead. | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | The packages install the `cerbos` and `cerbosctl` binaries to `/usr/local/bin` and create a systemd service to automatically start the Cerbos PDP server. The default configuration is setup to look for policies in `/var/cerbos/policies` but you can change this by editing `/etc/cerbos/yaml` and reloading the service with `sudo systemctl restart cerbos`. ```sh # Show status of the service sudo systemctl status cerbos # Restart the service sudo systemctl restart cerbos # View logs sudo journalctl -xeu cerbos.service ``` ## [](#homebrew)Homebrew You can install Cerbos PDP binaries using Homebrew as well. ```sh brew tap cerbos/tap brew install cerbos ``` ## [](#npm)npm You can install Cerbos PDP binaries from the npm registry. This removes a separate setup step for JavaScript projects and allows you to lock Cerbos PDP to a specific version to ensure a consistent development environment. [cerbos](https://www.npmjs.com/package/cerbos) and [cerbosctl](https://www.npmjs.com/package/cerbosctl) are available as separate packages. ```sh npm install --save-dev cerbos cerbosctl ``` Note that the npm packages rely on platform-specific optional dependencies, so make sure you don’t omit these when installing dependencies (for example, don’t pass the `--no-optional` flag to `npm`). ## [](#nix)Nix flake A [Nix flake](https://nixos.wiki/wiki/Flakes) is available at . ```none # Launch a Cerbos PDP server nix run github:cerbos/cerbos-flake#cerbos -- server --set=storage.disk.directory=/path/to/policy_directory # Launch a REPL nix run github:cerbos/cerbos-flake#cerbos -- repl # Launch cerbosctl nix run github:cerbos/cerbos-flake#cerbosctl # Start a Nix shell session with cerbos and cerbosctl installed nix shell github:cerbos/cerbos-flake ``` Run from container ==================== ```sh docker run --rm --name cerbos -p 3592:3592 ghcr.io/cerbos/cerbos:0.55.0 ``` | | Cerbos PDP images can be verified using [sigstore](https://www.sigstore.dev) tools as follows: cosign verify \\ \--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \\ \--certificate-identity="https://github.com/cerbos/cerbos/.github/workflows/release.yaml@refs/tags/v0.55.0" \\ ghcr.io/cerbos/cerbos:0.55.0 | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | By default, the container is configured to listen on ports 3592 (HTTP) and 3593 (gRPC) and watch for policy files on the volume mounted at `/policies`. You can override these by creating a new [configuration file](../configuration/index.html). Create a directory to hold the config file and policies. ```sh mkdir -p cerbos-quickstart/policies ``` Create a config file. ```sh cat > cerbos-quickstart/.cerbos.yaml < base.yaml exec kubectl kustomize ``` Test that the patch works as expected: ```sh helm template cerbos/cerbos --post-renderer ./kustomize.sh ``` Now you can install Cerbos with your patches: ```sh helm install cerbos cerbos/cerbos --version=0.55.0 --post-renderer=./kustomize.sh ``` ## [](#%5Fdeploy%5Fcerbos%5Fconfigured%5Fto%5Fread%5Fpolicies%5Ffrom%5Fa%5Fgithub%5Frepository)Deploy Cerbos configured to read policies from a GitHub repository * Follow the instructions at to create a personal access token (PAT) with `repo` permissions. * Create a new Kubernetes secret to hold the PAT ```sh PAT=YOUR_GITHUB_PAT kubectl create secret generic cerbos-github-token --from-literal=GITHUB_TOKEN=$PAT ``` * Create a new values file named `git-values.yaml` with the following contents: ```yaml envFrom: - secretRef: name: cerbos-github-token (1) cerbos: config: # Configure the git storage driver storage: driver: "git" git: protocol: https # Replace with the URL of your GitHub repo. url: https://github.com/cerbos/sample-policies.git # Replace with the branch name of your repo. branch: main # Remove or leave empty if the policies are not stored in a subdirectory. subDir: hr # Path to checkout. By default, /work is a Kubernetes emptyDir volume that is only available for the lifetime of the pod. # If you want the work directory to persist between pod restarts, specify the mount path of a persistent volume here. checkoutDir: /work # How often the remote repo should be checked for updates. updatePollInterval: 60s # Credentials used to login to the remote GitHub repo. We are using an environment variable mounted from the secret we created earlier. https: username: "cerbos" (2) password: "${GITHUB_TOKEN}" (3) ``` | **1** | Create an environment variable from the secret we created | | ----- | ---------------------------------------------------------------------------------- | | **2** | Username should be set to a string value (can be any value if using GitHub) | | **3** | Use the environment variable containing the PAT as the password to login to GitHub | * Deploy Cerbos PDP using the Helm chart ```sh helm install cerbos cerbos/cerbos --version=0.55.0 --values=git-values.yaml ``` ## [](#%5Fdeploy%5Fcerbos%5Fpdp%5Fconfigured%5Fto%5Fread%5Fpolicies%5Ffrom%5Fa%5Fmounted%5Fvolume)Deploy Cerbos PDP configured to read policies from a mounted volume Here we demonstrate how to use a `hostPath` volume to feed policies to a Cerbos PDP deployment. You can easily substitute the `hostPath` volume type with any other type of volumes supported by Kubernetes. See . * Create a new values file named `pv-values.yaml` with the following contents: ```yaml volumes: (1) - name: cerbos-policies hostPath: path: /data/cerbos-policies volumeMounts: (2) - name: cerbos-policies mountPath: /policies readOnly: true cerbos: config: storage: driver: "disk" disk: directory: /policies (3) watchForChanges: true ``` | **1** | Define a hostPath volume type | | ----- | ---------------------------------------------------------------------- | | **2** | Mount the volume to the container at the path /policies | | **3** | Configure Cerbos to read policies from the mounted /policies directory | * Deploy Cerbos PDP using the Helm chart ```sh helm install cerbos cerbos/cerbos --version=0.55.0 --values=pv-values.yaml ``` ## [](#%5Fdeploy%5Fa%5Fpdp%5Fconnected%5Fto%5Fcerbos%5Fhub)Deploy a PDP connected to Cerbos Hub | | Requires a [Cerbos Hub](https://www.cerbos.dev/product-cerbos-hub) account. [![Try Cerbos Hub](../_images/try_cerbos_hub.png)](https://hub.cerbos.cloud) | | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | * Create a new Kubernetes secret to hold the Cerbos Hub credentials ```sh kubectl create secret generic cerbos-hub-credentials \ --from-literal=CERBOS_HUB_CLIENT_ID=YOUR_CLIENT_ID \ (1) --from-literal=CERBOS_HUB_CLIENT_SECRET=YOUR_CLIENT_SECRET \ (2) --from-literal=CERBOS_HUB_DEPLOYMENT_ID=CERBOS_HUB_DEPLOYMENT_ID (3) ``` | **1** | Client ID from the Cerbos Hub credential | | ----- | ---------------------------------------------------------------- | | **2** | Client secret from the Cerbos Hub credential | | **3** | Cerbos Hub [Deployment ID](../../../cerbos-hub/deployments.html) | * Create a new values file named `hub-values.yaml` with the following contents: ```yaml cerbos: config: # Configure the Hub audit backend audit: enabled: true (1) backend: "hub" hub: storagePath: /audit_logs # Create environment variables from the secret. envFrom: - secretRef: name: cerbos-hub-credentials # Mount volume for locally buffering the audit logs. A persistent volume is recommended for production use cases. volumes: - name: cerbos-audit-logs emptyDir: {} volumeMounts: - name: cerbos-audit-logs mountPath: /audit_logs ``` | **1** | Enables audit log collection. See [Hub audit log collection documentation](../../../cerbos-hub/audit-log-collection.html) for information about masking sensitive fields and other advanced settings. | | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | * Deploy Cerbos using the Helm chart ```sh helm install cerbos cerbos/cerbos --version=0.55.0 --values=hub-values.yaml ``` Tutorial ==================== ## [](#%5Fcerbforce)Cerbforce 'Cerbforce' is the new killer CRM system that is taking the world by storm. It began as a small SaaS app that has grown into an enterprise-scale, multi-tenant, global powerhouse. It is now at the point where the existing basic permission model created at the start of development is no longer fit for purpose and [Cerbos](https://cerbos.dev/) has been selected as the system to implement. This tutorial walks through the decision-making process for implementing [Cerbos](https://cerbos.dev/). It covers setting up, defining the various resources and policies for the different objects and users in the system, and evolve them to make use of all of Cerbos' features. Running locally ==================== As the developers of Cerbforce began their investigation of the system, the first step was getting a Cerbos instance up and running locally. ## [](#%5Fcontainer)Container If you have Docker, you can simply use the published images. The container already ships with a default configuration that has a `disk` driver configured to look for policies mounted at `/policies`. Create an empty policy folder at `policies/`, and then run the following: ```sh docker run --rm --name cerbos -t \ -v $(pwd)/policies:/policies \ -p 3592:3592 \ ghcr.io/cerbos/cerbos:latest server ``` ## [](#%5Fbinary)Binary Alternatively, if you don’t have Docker running, you can opt to use the release binary directly which you can download from [here](../installation/binary.html). ### [](#%5Fconfig%5Ffile)Config file In order to run the binary, you’ll need to create a minimal server configuration file. The simplest configuration to get up and running (using a local folder for storage of policies) requires only the port and location to be set: ```yaml --- server: httpListenAddr: ":3592" storage: driver: "disk" disk: directory: policies ``` | | You can find the full configuration schema in the [Cerbos docs](../configuration/index.html). | | ------------------------------------------------------------------------------------------------ | Save this configuration to a file named `.cerbos.yaml`. You’ll also need to create an empty policy folder `policies/`. Now, extract the binary and run: ```sh ./cerbos server --config=.cerbos.yaml ``` Once started you can open `` to see the API documentation. Calling Cerbos ==================== | | The policies for this section can be found [on GitHub](https://github.com/cerbos/cerbos/tree/main/docs/modules/ROOT/examples/tutorial/04-calling-cerbos/cerbos). | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Now that you know the policies are valid, it is time to make your first call to Cerbos to make an authorization check. ## [](#%5Fstarting%5Fcerbos)Starting Cerbos To start you need to launch the server: ```sh # Using Container docker run --rm --name cerbos -t \ -v /tutorial:/tutorial \ -p 3592:3592 ghcr.io/cerbos/cerbos:latest server --config=/tutorial/.cerbos.yaml # Using Binary ./cerbos server --config=/tutorial/.cerbos.yaml ``` Once Cerbos has started up you should see an output confirming that there are 3 policies loaded and ready to start processing authorization checks: ```sh 2024-12-28T13:55:57.043+0600 INFO cerbos.server maxprocs: Leaving GOMAXPROCS=8: CPU quota undefined 2024-12-28T13:55:57.044+0600 INFO cerbos.server Loading configuration from .cerbos.yaml 2024-12-28T13:55:57.045+0600 WARN cerbos.otel Disabling OTLP traces because neither OTEL_EXPORTER_OTLP_ENDPOINT nor OTEL_EXPORTER_OTLP_TRACES_ENDPOINT is defined 2024-12-28T13:55:57.046+0600 INFO cerbos.disk.store Initializing disk store from /Users/username/tutorial/policies 2024-12-28T13:55:57.048+0600 INFO cerbos.index Found 3 executable policies 2024-12-28T13:55:57.048+0600 INFO cerbos.telemetry Telemetry disabled 2024-12-28T13:55:57.048+0600 INFO cerbos.grpc Starting gRPC server at :3593 2024-12-28T13:55:57.050+0600 INFO cerbos.http Starting HTTP server at :3592 ``` At this point how you make a request to the Cerbos instance is down to your preference - a simple cURL command or using a GUI such as Postman also works. ## [](#%5Fcerbos%5Fcheck%5Fcall)Cerbos check call A call to Cerbos contains 3 key bits of information: 1. The Principal - who is making the request 2. The Resources - a map of entities of a resource kind that are they requesting access too 3. The Actions - what actions are they trying to perform on the entities The request payload to the `/api/check/resources` endpoint takes these 3 bits of information as JSON: ```json { "principal": { "id": "user_1", // the user ID "roles": ["user"], // list of roles from user's profile "attr": {} // a map of attributes about the user - not used yet }, "resources": [ // an array of resources being accessed { "actions": ["read"], // the list of actions to be performed on the resource "resource": { // details about the resource "kind": "contact", // the type of the resource "id": "contact_1", // the ID of the specific resource instance "attr": {} // a map of attributes about the resource - not used yet } } ] } ``` To make the actual call as a cURL with the default server config: ```sh curl --location --request POST 'http://localhost:3592/api/check/resources' \ --header 'Content-Type: application/json' \ --data-raw '{ "principal": { "id": "user_1", "roles": ["user"], "attr": {} }, "resources": [ { "actions": ["read"], "resource": { "kind": "contact", "id": "contact_1", "attr": {} } } ] }' ``` The response object looks as follows where for each instance of the resource the authorization decision for each action is either `EFFECT_ALLOW` or `EFFECT_DENY` depending on the policies: ```json { "results": [ { "resource": { "id": "contact_1", "kind": "contact" }, "actions": { "read": "EFFECT_ALLOW" } } ], "cerbosCallId": "49KQ6456PRBLWYMXYDBKZM1F6H" } ``` You can find the Swagger definition of the Cerbos API via going to the root of the Cerbos instance - for example if running on the default port. ## [](#%5Fconclusion)Conclusion Now that you have made the first call to Cerbos you can move on to a way of checking policy logic without having to make individual calls each time by writing unit tests. Resource definition ==================== | | The policies for this section can be found [on GitHub](https://github.com/cerbos/cerbos/tree/main/docs/modules/ROOT/examples/tutorial/03-resource-definition/cerbos). | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ## [](#%5Fauthentication%5Froles)Authentication roles To begin with Cerbos needs to know about the basic roles which are provided by your authentication provider. In the case of Cerbforce, [Auth0](https://cerbos.dev/ecosystem/cerbos-auth0) provides a role of either `ADMIN` or `USER` for all profiles. This is important when starting to define access to resources below - for now just make a note of them. ## [](#%5Fresources)Resources The best place to start with defining [policies](../policies/index.html) is listing out all the resources and their actions that exist in the system. A resource is an entity type that users are authorized to access. In the case of Cerbforce some of the resources and actions are as follows: | Resource | Actions | | -------- | ---------------------------- | | User | Create, Read, Update, Delete | | Company | Create, Read, Update, Delete | | Contact | Create, Read, Update, Delete | With this as a start, you can begin creating your first Cerbos policy - a [resource policy](../policies/resource%5Fpolicies.html). ## [](#%5Fresource%5Fpolicies)Resource policies Taking the user resource as an example, the most basic resource policy can be defined like below: ```yaml apiVersion: api.cerbos.dev/v1 resourcePolicy: version: "default" resource: "user" rules: - actions: - create - read effect: EFFECT_ALLOW roles: - user - actions: - create - read - update - delete effect: EFFECT_ALLOW roles: - admin ``` The structure of a resource policy requires a name to be set on the `resource` key and then a list of rules is defined. A rule defines a list of actions on the resource, the effect of the rule (`EFFECT_ALLOW` or `EFFECT_DENY`) and then fields to state who this applies to - in this simple case a list of `roles` which is checked for in the roles of the user making the request. In this case, a request made for a principal with a role of `user` is granted only `create` and `read` actions whilst an `admin` role can also perform `update`, `delete` actions. The full documentation for resource policies can be found [here](../policies/resource%5Fpolicies.html). ## [](#%5Fwildcard%5Faction)Wildcard action To simplify things further, admins might need to be able to do every action. We can use a special `*` wildcard action to specify this succinctly: ```yaml --- apiVersion: api.cerbos.dev/v1 resourcePolicy: version: "default" resource: "user" rules: - actions: - create - read - update effect: EFFECT_ALLOW roles: - user - actions: - "*" effect: EFFECT_ALLOW roles: - admin ``` The `contact` and `company` resources have a similar structure at this stage and can be modeled as so: ```yaml --- apiVersion: api.cerbos.dev/v1 resourcePolicy: version: "default" resource: "contact" rules: - actions: - create - read - update effect: EFFECT_ALLOW roles: - user - actions: - "*" effect: EFFECT_ALLOW roles: - admin ``` ```yaml --- apiVersion: api.cerbos.dev/v1 resourcePolicy: version: "default" resource: "company" rules: - actions: - create - read - update effect: EFFECT_ALLOW roles: - user - actions: - "*" effect: EFFECT_ALLOW roles: - admin ``` ## [](#%5Fvalidating%5Fpolicies)Validating policies Now with the initial policies in place, you can run Cerbos in compile mode which validates the content of the policy files to ensure they are correct. If you are running Cerbos in a container then mount the folder containing your policies and run the `compile` command pointing to the folder of your policies. ```sh # Using Container docker run --rm --name cerbos -t \ -v /tutorial:/tutorial \ ghcr.io/cerbos/cerbos:latest compile /tutorial/policies # Using Binary ./cerbos compile /tutorial/policies ``` If the policies are valid then the process exits with no errors. If there is an issue, the error message points you to where you need to look and the specific problem to fix. ## [](#%5Fconclusion)Conclusion At this stage, a simple Roles-based Access Control (RBAC) model has been designed and the policies have been validated - next up is making an authorization call to Cerbos. Testing policies ==================== | | The policies for this section can be found [on GitHub](https://github.com/cerbos/cerbos/tree/main/docs/modules/ROOT/examples/tutorial/05-testing-policies/cerbos). | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Cerbos allows you to write [tests for policies](../policies/compile.html) and run them as part of the compilation stage to make sure that the policies do exactly what you expect. This saves the manual effort of running example requests over and over to ensure the policy logic is as you expect. A test suite defines a number of resources and principals and the expected result of actions for any combination of them. To define a test suite, create a `tests` folder inside your policy folder. In this folder, any number of tests can be defined as YAML but the file must end with `_test`. As an example, the `contact` policy states that a `user` can create, read and update a contact, but only an `admin` can delete them - therefore you can create a test suite for this like the below: ```yaml --- name: ContactTestSuite description: Tests for verifying the contact resource policy principals: admin: id: admin roles: - admin user: id: user roles: - user resources: contact: kind: contact id: contact tests: - name: Contact CRUD Actions input: principals: - admin - user resources: - contact actions: - create - read - update - delete expected: - principal: admin resource: contact actions: create: EFFECT_ALLOW read: EFFECT_ALLOW update: EFFECT_ALLOW delete: EFFECT_ALLOW - principal: user resource: contact actions: create: EFFECT_ALLOW read: EFFECT_ALLOW update: EFFECT_ALLOW delete: EFFECT_DENY ``` With this defined, you can now extend the compile command to also run the tests for example: ```sh # Using Container docker run --rm --name cerbos -t \ -v /tutorial:/tutorial \ -p 3592:3592 \ ghcr.io/cerbos/cerbos:latest compile /tutorial/policies # Using Binary ./cerbos compile /tutorial/policies ``` If everything is as expected the output of the tests should be green: ```none Test results = ContactTestSuite (contact_test.yaml) == 'Contact CRUD Actions' for resource 'contact_test' by principal 'user' [OK] == 'Contact CRUD Actions' for resource 'contact_test' by principal 'admin' [OK] ``` Full testing documentation can be found [here](../policies/compile.html). Adding conditions ==================== | | The policies for this section can be found [on GitHub](https://github.com/cerbos/cerbos/tree/main/docs/modules/ROOT/examples/tutorial/06-adding-conditions/cerbos). | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | In the previous section, an RBAC policy was created that allowed anyone with a `user` role to update a user resource - this isn’t what is intended as it would allow users to update other users' profiles. ```yaml apiVersion: api.cerbos.dev/v1 resourcePolicy: version: "default" resource: "user" rules: - actions: - create - read - update effect: EFFECT_ALLOW roles: - user # ....other conditions ``` This blanket approach is where using pure role-based access controls falls down as there is more nuanced required to meet the requirements. ## [](#%5Fconditions)Conditions Cerbos is a powerful Attribute-based Access Control system that can make contextual decisions at request time whether an action can be taken. In this scenario, Cerbforce’s business logic states that a user can only update their own user profile. To implement this a check needs to be made to ensure the ID of the user making the request matches the ID of the user resource being updated. [Conditions](../policies/conditions.html) in Cerbos are written in [Common Expression Language (CEL)](https://github.com/google/cel-spec/blob/master/doc/intro.md) which is a simple way of defining boolean logic of conditions. In this environment, there are two main bits of data provided that are of interest `request.principal` which is the information about the user making the request and `request.resource` which is the information about the resource being accessed. The data model for each of these is as follows: ```json // request.principal { "id": "somePrinicpalId", // the prinicpal ID "roles": ["user"], // the list of roles from the auth provider "attr": { // a map of attributes about the prinicpal } } // request.resource { "id": "someResourceId", // the resource ID "attr": { // a map of attributes about the resourece } } ``` Using this information a check to see if the principal ID is the same as the ID of the user resource being accessed can be defined as ```none request.resource.id == request.principal.id ``` Adding this to the policy request a new rule to be created that is just for the `update` and `delete` actions which are for the `user` role and has a single condition. ```json --- apiVersion: api.cerbos.dev/v1 resourcePolicy: version: "default" resource: "user" rules: - actions: - create - read effect: EFFECT_ALLOW roles: - user - actions: - update - delete effect: EFFECT_ALLOW roles: - user condition: match: expr: request.resource.id == request.principal.id # ....other conditions ``` Complex logic can be defined in conditions (or sets of conditions) which you can read more about in [the docs](https://docs.cerbos.dev/cerbos/latest/policies/conditions.html). ## [](#%5Fextending%5Ftests)Extending tests Now that you have a conditional policy, you can add these as test cases in the user tests. You can now define multiple `user` resources and principals and create test cases for ensuring the `update` action is allowed when the ID of the principal matches the ID of the resource, as well as checking that it isn’t allowed if the condition is not met. ```yaml --- name: UserTestSuite description: Tests for verifying the user resource policy principals: admin: id: admin roles: - admin user1: id: user1 roles: - user user2: id: user2 roles: - user resources: admin: kind: user id: admin user1: kind: user id: user1 user2: kind: user id: user2 tests: - name: User CRUD Actions input: principals: - admin - user1 - user2 resources: - admin - user1 - user2 actions: - create - read - update - delete expected: - principal: admin resource: admin actions: create: EFFECT_ALLOW read: EFFECT_ALLOW update: EFFECT_ALLOW delete: EFFECT_ALLOW - principal: admin resource: user1 actions: create: EFFECT_ALLOW read: EFFECT_ALLOW update: EFFECT_ALLOW delete: EFFECT_ALLOW - principal: admin resource: user2 actions: create: EFFECT_ALLOW read: EFFECT_ALLOW update: EFFECT_ALLOW delete: EFFECT_ALLOW - principal: user1 resource: admin actions: create: EFFECT_ALLOW read: EFFECT_ALLOW update: EFFECT_DENY delete: EFFECT_DENY - principal: user1 resource: user1 actions: create: EFFECT_ALLOW read: EFFECT_ALLOW update: EFFECT_ALLOW delete: EFFECT_ALLOW - principal: user1 resource: user2 actions: create: EFFECT_ALLOW read: EFFECT_ALLOW update: EFFECT_DENY delete: EFFECT_DENY - principal: user2 resource: admin actions: create: EFFECT_ALLOW read: EFFECT_ALLOW update: EFFECT_DENY delete: EFFECT_DENY - principal: user2 resource: user1 actions: create: EFFECT_ALLOW read: EFFECT_ALLOW update: EFFECT_DENY delete: EFFECT_DENY - principal: user2 resource: user2 actions: create: EFFECT_ALLOW read: EFFECT_ALLOW update: EFFECT_ALLOW delete: EFFECT_ALLOW ``` Derived roles ==================== | | The policies for this section can be found [on GitHub](https://github.com/cerbos/cerbos/tree/main/docs/modules/ROOT/examples/tutorial/07-derived-roles/cerbos). | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | The business requirements for Cerbforce state that only an owner of Contacts and Companies are allowed to delete them from the system. With Cerbos, the aim is to keep policies as simple as possible and not repeat logic across different resources, so in this situation, a [Derived Role](../policies/derived%5Froles.html) can enable help. Derived roles are a way of augmenting the broad roles which are attached to the user in the directory of the authentication system with contextual data to provide more fine-grained control at runtime. On every request, all the relevant derived role policies are evaluated and those matching roles are 'attached' to the user as Cerbos computes access. ## [](#%5Fowner%5Fderived%5Frole)Owner derived role In the Cerbforce data model, the `contact` and `company` both have an attribute called `ownerId` which is the ID of the user that created the record. Rather than adding a condition to both of these resource policies, you are going to create a derived role that gives the principal an additional `owner` role within the context of the request. The policy for this is as follows: ```yaml --- apiVersion: "api.cerbos.dev/v1" description: |- Common dynamic roles used within the Cerbforce app derivedRoles: name: cerbforce_derived_roles definitions: - name: owner parentRoles: ["user"] condition: match: expr: request.resource.attr.ownerId == request.principal.id ``` The structure is similar to a resource policy but rather than defining actions with conditions, it defines roles that are an extension of the listed `parentRoles` and can have any number of conditions as with resources. With this derived role policy setup a resource can import them and then make use of them in rules eg: ```yaml --- apiVersion: api.cerbos.dev/v1 resourcePolicy: version: "default" resource: "contact" importDerivedRoles: - cerbforce_derived_roles rules: - actions: - create - read effect: EFFECT_ALLOW roles: - user - actions: - update - delete effect: EFFECT_ALLOW derivedRoles: - owner - actions: - "*" effect: EFFECT_ALLOW roles: - admin ``` Full documentation can be found [here](../policies/derived%5Froles.html). Principal policies ==================== | | The policies for this section can be found [on GitHub](https://github.com/cerbos/cerbos/tree/main/docs/modules/ROOT/examples/tutorial/08-principal-policies/cerbos). | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | The final type of policy that Cerbos supports is a [principal policy](../policies/principal%5Fpolicies.html) which is a special type that allows user-specific overrides to be defined. In the case of Cerbforce there is a Data Protection Officer (DPO) that handles any data deletion requests. By default, they would not have any delete access to contacts unless they were the owner of the record or have the `admin` role. To overcome this a principal policy has been created which targets their userId and overrides this for the delete action on a contact resource: ```yaml --- apiVersion: "api.cerbos.dev/v1" principalPolicy: version: "default" principal: "dpo1" rules: - resource: contact actions: - name: contact_delete action: "delete" effect: EFFECT_ALLOW ``` With this policy in place, when an authorization check is made with the principal ID of `dpo1` the delete action on a `contact` resource is overridden to be allowed. Full documentation can be found [here](../policies/principal%5Fpolicies.html). Attribute schema ==================== | | The policies for this section can be found [on GitHub](https://github.com/cerbos/cerbos/tree/main/docs/modules/ROOT/examples/tutorial/09-attribute-schema/cerbos). | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | An additional check bit of business logic has been introduced for the `contact` resource which requires the `active` attribute of a contact to be set to `True` to be able to `update` or `delete` it. This is so that old contacts are kept for reporting purposes and can’t be accidentally deleted or updated. This now means there are two attributes of a `contact` resource that are now required for the policies to be computed - `ownerId` and `active`. If either of these is not included in the request to check permissions the result would not be as expected (defaulting to `EFFECT_DENY`). To prevent this mistake, it is possible to define a [schema](../policies/schemas.html) for the attributes of a principal and resources which Cerbos validates against at request time to ensure all fields are provided as expected. ## [](#%5Fdefining%5Fschema)Defining schema [Attribute schema](../policies/schemas.html) are defined in [JSON Schema (draft 2020-12)](https://json-schema.org/specification.html) and stored in a special `_schemas` sub-directory along side the policies For the contact resource the schema looks like the following: ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "ownerId": { "type": "string" }, "active": { "type": "boolean" } }, "required": ["ownerId", "active"] } ``` Once defined, it is then linked to the resource via adding a reference in the policy: ```yaml --- apiVersion: api.cerbos.dev/v1 resourcePolicy: version: "default" resource: "contact" importDerivedRoles: - cerbforce_derived_roles rules: - actions: - create - read effect: EFFECT_ALLOW roles: - user - actions: - update - delete effect: EFFECT_ALLOW derivedRoles: - owner condition: match: expr: request.resource.attr.active == true - actions: - "*" effect: EFFECT_ALLOW roles: - admin schemas: resourceSchema: ref: cerbos:///contact.json ``` The same can be done with attributes of a principal - you can find out more in [the documentation](../policies/schemas.html). ## [](#%5Fenforcing%5Fschema)Enforcing schema Validating the request against the schema is done at request time by the server - to enable this a [new schema configuration block](../configuration/schema.html) needs adding to the `.cerbos.yaml`. ```yaml schema: enforcement: reject ``` With this now in place, any request that is made to check authorization of a `contact` resource is rejected if the attributes are not provided or of the wrong type: _Request_ ```json { "principal": { "id": "user_1", "roles": ["user"], "attr": {} }, "resource": { "kind": "contact", "instances": { "contact_1": { "attr": { "ownerId": "user1" } } } }, "actions": ["read"] } ``` _Response_ ```json { "resourceInstances": { "contact_1": { "actions": { "read": "EFFECT_DENY" }, "validationErrors": [ { "message": "missing properties: 'active'", "source": "SOURCE_RESOURCE" } ] } } } ``` Integrating Cerbos ==================== With the policies now defined the authorization logic inside the app can be replaced with a call out to a running Cerbos instance. Cerbos has SDKs available for Go, Java, .NET, Node, PHP, Python, Ruby, and Rust. Documentation for these and other examples can be found [here](../api/index.html). All Cerbos integrations can be explored at [the Cerbos ecosystem page](https://www.cerbos.dev/ecosystem). | | For a review of integration architecture, query plan usage, SDK configuration, or production deployment patterns, [speak to us](https://www.cerbos.dev/workshop?utm%5Fcampaign=brand%5Fcerbos&utm%5Fsource=documentation&utm%5Fmedium=text). | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Tutorial: Writing policies for a simple photo-sharing service ==================== Getting started * We will use Docker to run the server and the compiler. ```shell docker pull ghcr.io/cerbos/cerbos:0.55.0 ``` * Create a file named `.cerbos.yaml` with the following contents: ```yaml --- server: httpListenAddr: ":3592" storage: driver: "disk" disk: directory: /photo-share/policies ``` * Create a directory named `policies` to hold the policies. | | You can find all the policies and tests used in this tutorial at . | | --------------------------------------------------------------------------------------------------------------------- | ## [](#%5Fthe%5Fapatr%5Fapplication)The Apatr application Apatr is a simple photo-sharing service that allows users to upload their photos and optionally share them with the rest of the world. Users sign-up to the service either by creating their own user account on the website or by signing-in with an identity provider (IDP) like Google or Facebook. Apatr uses a third-party identity management tool to manage these accounts and authenticating users to the site. Once they are logged-in, users can do the following: * Create albums to organize their photos * Upload photos to albums * Share albums or individual photos with other Apatr users * Share albums or individual photos with the internet Apatr also employs a team of moderators to investigate complaints and remove any illegal or offensive items from the site. To respect users privacy, moderators are only allowed to view photos or albums that are public or those that have been flagged as inappropriate by another user. Apatr’s identity provider allows defining roles for users. The roles currently defined in this system are: * `moderator`: Member of the moderator team * `user`: Authenticated users ## [](#%5Fresources%5Fand%5Factions)Resources and actions In the Apatr application, the most obvious resource hierarchy is the following: * Album * Photo * Caption * Comment * Description * User Profile __Album permissions matrix__ | Resource | Action | Allowed role | Condition | | ---------------- | --------------------------------------------------------------- | --------------------------------------------- | --------- | | **album:object** | create | user | | | delete | user | If user owns the album | | | moderator | If the album is flagged as inappropriate | | | | share | user | If user owns the album | | | unshare | user | If user owns the album | | | view | user | If user owns the album If the album is public | | | moderator | If the album is flagged as inappropriate If the album is public | | | | flag | user | If the album is public | | ## [](#%5Fderived%5Froles)Derived roles There are some recurring themes in the above permissions matrix. * People who have the `user` role can be either owners or viewers depending on the resource they are trying to access * Moderators get extra capabilities when the content is flagged as inappropriate These capabilities are determined based on contextual information. Let’s codify them so that they can be reused. ```yaml --- apiVersion: "api.cerbos.dev/v1" description: |- Common dynamic roles used within the Apatr app derivedRoles: name: apatr_common_roles (1) definitions: - name: owner (2) parentRoles: ["user"] (3) condition: match: expr: request.resource.attr.owner == request.principal.id (4) - name: abuse_moderator parentRoles: ["moderator"] condition: match: expr: request.resource.attr.flagged == true ``` | **1** | Name that we will use to import this set of roles | | ----- | ------------------------------------------------------------------------------------------ | | **2** | Descriptive name for this derived role | | **3** | The static roles (from the identity provider) to which this derived role applies to | | **4** | An expression that is applied to the request to determine when this role becomes activated | Save the above definition as `apatr_common_roles.yaml` in the `policies` directory. Run the compiler to make sure that the contents of the file are valid. ```shell docker run -it -v $(pwd):/photo-share ghcr.io/cerbos/cerbos:0.55.0 \ compile /photo-share/policies ``` ## [](#%5Fresource%5Fpolicies)Resource policies Let’s write a resource policy for the `album:object` resource. ```yaml --- apiVersion: api.cerbos.dev/v1 resourcePolicy: version: "default" (1) importDerivedRoles: - apatr_common_roles (2) resource: "album:object" rules: - actions: ['*'] effect: EFFECT_ALLOW derivedRoles: - owner - actions: ['view', 'flag'] effect: EFFECT_ALLOW roles: - user condition: match: expr: request.resource.attr.public == true - actions: ['view', 'delete'] effect: EFFECT_ALLOW derivedRoles: - abuse_moderator ``` | **1** | You can have multiple policy versions for the same resource (e.g. production vs. staging). If the request does not explicitly specify the version, the default policy takes effect. | | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **2** | Import the roles we defined earlier | Save the above policy definition as `resource_album_object.yaml` inside the `policies` directory. Run the compiler to make sure that the contents of the policies directory are valid. ```shell docker run -it -v $(pwd):/photo-share ghcr.io/cerbos/cerbos:0.55.0 compile /photo-share/policies ``` Let’s start the server and try out a request. ```shell docker run -it -v $(pwd):/photo-share -p 3592:3592 ghcr.io/cerbos/cerbos:0.55.0 \ server --config=/photo-share/.cerbos.yaml ``` | | If you like to use [Postman](https://www.postman.com), [Insomnia](https://insomnia.rest) or any other software that supports OpenAPI, the Cerbos OpenAPI definitions can be downloaded by accessing . | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Alicia trying to view her own album ```shell cat < for an example of using Cerbos GitHub Actions in a CI workflow to compile and test policies. | | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#%5Fusing%5Fschemas%5Fto%5Fenforce%5Ftype%5Fsafety%5Foptional)Using schemas to enforce type safety \[Optional\] The derived roles and resource policy rules we defined above rely on certain attributes being present in the `attr` sections of the incoming request. To ensure that API requests are strictly-typed and contain required attributes, we can define schemas for the principal and resource attributes sections. Create a new directory named `_schemas` inside the `policies` directory. ```sh mkdir policies/_schemas ``` Let’s add a JSON schema defining the data types and required fields for `album:object` resources. Create a file named `album_object.json` inside the `policies/_schemas` directory with the following contents: ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "owner": { "type": "string" }, "public": { "type": "boolean" }, "flagged": { "type": "boolean" } }, "required": [ "owner" ] } ``` Now update `policies/resource_album_object.yaml` to add the reference to the schema: ```yaml --- apiVersion: api.cerbos.dev/v1 resourcePolicy: version: "default" importDerivedRoles: - apatr_common_roles resource: "album:object" rules: - actions: ['*'] effect: EFFECT_ALLOW derivedRoles: - owner - actions: ['view', 'flag'] effect: EFFECT_ALLOW roles: - user condition: match: expr: request.resource.attr.public == true - actions: ['view', 'delete'] effect: EFFECT_ALLOW derivedRoles: - abuse_moderator schemas: resourceSchema: ref: cerbos:///album_object.json (1) ``` | **1** | Defines the schema to use for validating resource attributes | | ----- | ------------------------------------------------------------ | Update `.cerbos.yaml` to enable schema enforcement. ```yaml --- server: httpListenAddr: ":3592" storage: driver: "disk" disk: directory: /photo-share/policies schema: enforcement: reject ``` Now start the server again and send a request that does not conform to the schema. The server response should contain a list of validation errors. ```shell docker run -it -v $(pwd):/photo-share -p 3592:3592 ghcr.io/cerbos/cerbos:0.55.0 \ server --config=/photo-share/.cerbos.yaml ``` Invalid request ```shell cat <= v0.16 * `@cerbos/http` or `@cerbos/grpc` client * Convex 1.x * Node.js >= 20.0.0 ## [](#%5Finstallation)Installation ```bash npm install @cerbos/orm-convex ``` ## [](#%5Fsupported%5Foperators)Supported operators ### [](#%5Fdatabase%5Flevel%5Foperators)Database-level operators | Category | Operators | | ---------- | -------------------------------------------------------------- | | Logical | and, or, not — q.and(…​), q.or(…​), q.not(…​) | | Comparison | eq, ne, lt, le, gt, ge — q.eq, q.neq, q.lt, q.lte, q.gt, q.gte | | Membership | in — composed as q.or(q.eq(field, v1), q.eq(field, v2), …​) | | Existence | isSet — q.neq(field, undefined) or q.eq(field, undefined) | ### [](#%5Fpost%5Ffilter%5Foperators)Post-filter operators The following operators cannot be expressed as Convex database filters. When encountered, the adapter returns a `postFilter` function that evaluates them in JavaScript: | Category | Operators | | ---------- | -------------------------------------------------------------- | | String | contains, startsWith, endsWith | | Collection | hasIntersection, exists, exists\_one, all, filter, map, lambda | For `and(…​)` expressions with mixed operator types, the adapter splits the tree: database-pushable children go to `filter`, the rest go to `postFilter`. For `or(…​)` with any unsupported child, the entire expression goes to `postFilter` to avoid missing results. ### [](#%5Fallowpostfilter%5Fopt%5Fin)`allowPostFilter` opt-in By default, `queryPlanToConvex` throws when the query plan requires a `postFilter`. This is because post-filter operators cause data to be fetched before authorization filtering is fully applied. To opt in: ```typescript const { kind, filter, postFilter } = queryPlanToConvex({ queryPlan, mapper, allowPostFilter: true, }); ``` If your policies only use operators that Convex supports natively, `filter` alone enforces the full policy at the database level and this flag is not needed. ## [](#%5Fusage)Usage ```typescript import { queryPlanToConvex, PlanKind } from "@cerbos/orm-convex"; const queryPlan = await cerbos.planResources({ principal: { id: "user1", roles: ["USER"] }, resource: { kind: "document" }, action: "view", }); const { kind, filter, postFilter } = queryPlanToConvex({ queryPlan, mapper, allowPostFilter: true, }); if (kind === PlanKind.ALWAYS_DENIED) return []; if (kind === PlanKind.ALWAYS_ALLOWED && !postFilter) { return await ctx.db.query("documents").collect(); } let query = ctx.db.query("documents"); if (filter) query = query.filter(filter); let results = await query.collect(); if (postFilter) results = results.filter(postFilter); return results; ``` ## [](#%5Ffield%5Fmapper)Field mapper ```typescript const mapper = { "request.resource.attr.title": { field: "title" }, "request.resource.attr.status": { field: "status" }, }; // Or as a function: const mapper = (path) => ({ field: path.replace("request.resource.attr.", ""), }); ``` The `field` property rewrites a Cerbos path to a Convex document field. Dot notation is supported for nested fields. If the mapper is omitted, the adapter uses query plan paths as-is. ## [](#%5Flimitations)Limitations * String and collection operators are evaluated as a JavaScript `postFilter` after the database query returns. These conditions do not reduce the number of documents read from the database. * For `or(…​)` expressions where any child uses an unsupported operator, the entire OR is evaluated via `postFilter`. * The `in` operator is composed as multiple `eq` comparisons joined with `or`, which may be less efficient for large value lists. ## [](#%5Fsource%5Fcode)Source code [cerbos/query-plan-adapters/convex](https://github.com/cerbos/query-plan-adapters/tree/main/convex) Drizzle adapter ==================== The `@cerbos/orm-drizzle` package converts a Cerbos [PlanResources](../../api/index.html#resources-query-plan) response into a Drizzle ORM SQL expression. The resulting filter can be composed with your existing query builder chain. ## [](#%5Frequirements)Requirements * Cerbos >= v0.16 * `@cerbos/http` or `@cerbos/grpc` client * Drizzle ORM (SQLite, PostgreSQL, MySQL, PlanetScale) * Node.js >= 20.0.0 ## [](#%5Finstallation)Installation ```bash npm install @cerbos/orm-drizzle ``` ## [](#%5Fsupported%5Foperators)Supported operators * Logical: `and`, `or`, `not` * Comparison: `eq`, `ne`, `lt`, `gt`, `le`, `ge`, `in` * String: `contains`, `startsWith`, `endsWith` * Existence: `isSet` * Collection: `hasIntersection`, `exists`, `exists_one`, `all`, `filter` * Relation-aware mappings with `EXISTS` subqueries ## [](#%5Fusage)Usage ```typescript import { queryPlanToDrizzle, PlanKind } from "@cerbos/orm-drizzle"; import { eq, and } from "drizzle-orm"; import { resources } from "./schema"; const plan = await cerbos.planResources({ principal, resource: { kind: "document" }, action: "view", }); const result = queryPlanToDrizzle({ queryPlan: plan, mapper: { "request.resource.attr.status": resources.status, "request.resource.attr.owner": resources.ownerId, }, }); switch (result.kind) { case PlanKind.ALWAYS_DENIED: return []; case PlanKind.ALWAYS_ALLOWED: return await db.select().from(resources); case PlanKind.CONDITIONAL: return await db .select() .from(resources) .where(result.filter); } ``` ## [](#%5Ffield%5Fmapper)Field mapper The mapper associates Cerbos attribute references with Drizzle columns. It accepts: * A plain object with keys as Cerbos attribute references and values as Drizzle columns or SQL expressions * A function receiving the attribute reference and returning the column or expression * An object with a `column` property and an optional `transform` function for custom SQL translation ```typescript const result = queryPlanToDrizzle({ queryPlan, mapper: { "request.resource.attr.custom": { column: sql`lower(${resources.title})`, transform: ({ operator, value }) => { if (operator !== "eq") throw new Error("Unsupported"); return eq(sql`lower(${resources.title})`, value.toLowerCase()); }, }, }, }); ``` ## [](#%5Frelation%5Fmapping)Relation mapping The adapter wraps relation comparisons in `EXISTS` subqueries, automatically inferring relation fields when they match column names on the related table. ```typescript const result = queryPlanToDrizzle({ queryPlan, mapper: { "request.resource.attr.owner": { relation: { type: "one", table: owners, sourceColumn: resources.ownerId, targetColumn: owners.id, fields: { email: owners.email, }, }, }, "request.resource.attr.tags": { relation: { type: "many", table: resourceTags, sourceColumn: resources.id, targetColumn: resourceTags.resourceId, fields: { name: { relation: { type: "one", table: tags, sourceColumn: resourceTags.tagId, targetColumn: tags.id, field: tags.name, }, }, }, }, }, }, }); ``` ## [](#%5Fcollection%5Foperators)Collection operators * `hasIntersection` — translates to `column IN (…​)` for multi-valued attributes mapped as nested relations * `exists`, `exists_one`, `all` — generate correlated `EXISTS` subqueries with the lambda variable scoped to the relation * `filter` — Cerbos uses this during plan construction; the adapter discards the lambda since the filter is applied in Drizzle ## [](#%5Fsource%5Fcode)Source code [cerbos/query-plan-adapters/drizzle](https://github.com/cerbos/query-plan-adapters/tree/main/drizzle) Query plan adapters ==================== The Cerbos [PlanResources API](../../api/index.html#resources-query-plan) returns a query plan describing which resources a principal can access for a given action. Cerbos provides reference implementations of query plan adapters that convert that response into native query filters for your data layer, so authorization conditions are evaluated at the database level rather than in application code. Use these adapters as-is or as a starting point to build your own. ## [](#%5Favailable%5Fadapters)Available adapters | Adapter | Language | Target | | ----------------------------------------------- | ---------- | ----------------------------------------------------------- | | [Prisma](prisma.html) | TypeScript | Prisma ORM (PostgreSQL, MySQL, SQLite, SQL Server, MongoDB) | | [Drizzle](drizzle.html) | TypeScript | Drizzle ORM (PostgreSQL, MySQL, SQLite) | | [Mongoose](mongoose.html) | TypeScript | MongoDB via Mongoose | | [Convex](convex.html) | TypeScript | Convex reactive database | | [LangChain / ChromaDB](langchain-chromadb.html) | TypeScript | ChromaDB vector store via LangChain.js | | [SQLAlchemy](sqlalchemy.html) | Python | SQLAlchemy (PostgreSQL, MySQL, SQLite, Oracle, SQL Server) | LangChain / ChromaDB adapter ==================== The `@cerbos/langchain-chromadb` package converts a Cerbos [PlanResources](../../api/index.html#resources-query-plan) response into a [ChromaDB](https://www.trychroma.com/) `Where` filter object compatible with the LangChain.js Chroma vector store. This enables authorization-aware similarity searches where Cerbos policy conditions are pushed down as metadata filters. ## [](#%5Frequirements)Requirements * Cerbos >= v0.16 * `@cerbos/http` or `@cerbos/grpc` client * ChromaDB 3.x * Node.js >= 20.0.0 ## [](#%5Finstallation)Installation ```bash npm install @cerbos/langchain-chromadb ``` ## [](#%5Fsupported%5Foperators)Supported operators | Category | Operators | | ---------- | -------------------------------------------------------- | | Logical | and, or — $and, $or | | Negation | not — operator inversion via De Morgan’s law (see below) | | Comparison | eq, ne, lt, le, gt, ge — $eq, $ne, $lt, $lte, $gt, $gte | | Membership | in — $in | ### [](#%5Fnegation%5Fhandling)Negation handling ChromaDB has no `$not` or `$nor` filter. The adapter handles `not` expressions by inverting the inner operator: * `not(eq)` becomes `$ne`; `not(ne)` becomes `$eq` * `not(lt)` becomes `$gte`; `not(gt)` becomes `$lte`; `not(le)` becomes `$gt`; `not(ge)` becomes `$lt` * `not(in)` becomes `$nin` * `not(and(A, B))` becomes `$or[not(A), not(B)]` (De Morgan’s law) * `not(or(A, B))` becomes `$and[not(A), not(B)]` (De Morgan’s law) * `not(not(X))` becomes `X` (double negation elimination) ### [](#%5Funsupported%5Foperators)Unsupported operators ChromaDB stores flat scalar metadata. The following operators cannot be mapped and cause `queryPlanToChromaDB` to throw an error: * String: `contains`, `startsWith`, `endsWith` * Existence: `isSet` * Collection: `hasIntersection`, `exists`, `exists_one`, `all`, `filter`, `map`, `lambda`, `size` ## [](#%5Fusage)Usage ```typescript import { queryPlanToChromaDB, PlanKind } from "@cerbos/langchain-chromadb"; import { Chroma } from "@langchain/community/vectorstores/chroma"; import { OpenAIEmbeddings } from "@langchain/openai"; const queryPlan = await cerbos.planResources({ principal: { id: "user1", roles: ["USER"] }, resource: { kind: "document" }, action: "view", }); const result = queryPlanToChromaDB({ queryPlan, fieldNameMapper: { "request.resource.attr.department": "department", "request.resource.attr.public": "public", }, }); if (result.kind === PlanKind.ALWAYS_DENIED) { return []; } const chroma = await Chroma.fromExistingCollection( new OpenAIEmbeddings(), { collectionName: "my_collection" }, ); const filters = result.kind === PlanKind.CONDITIONAL ? result.filters : undefined; const matches = await chroma.similaritySearch("query", 10, filters); ``` ## [](#%5Ffield%5Fname%5Fmapper)Field name mapper The mapper translates Cerbos attribute references to ChromaDB metadata field names. It accepts an object or a function. ```typescript // Object mapper const result = queryPlanToChromaDB({ queryPlan, fieldNameMapper: { "request.resource.attr.aBool": "aBool", "request.resource.attr.aString": "title", }, }); // Function mapper const result = queryPlanToChromaDB({ queryPlan, fieldNameMapper: (fieldName) => fieldName.replace("request.resource.attr.", ""), }); ``` If a field is not found in the object mapper, the original Cerbos path is used as-is. ## [](#%5Fsource%5Fcode)Source code [cerbos/query-plan-adapters/langchain-chromadb](https://github.com/cerbos/query-plan-adapters/tree/main/langchain-chromadb) Mongoose adapter ==================== The `@cerbos/orm-mongoose` package converts a Cerbos [PlanResources](../../api/index.html#resources-query-plan) response into a MongoDB filter compatible with Mongoose’s `find()` method. ## [](#%5Frequirements)Requirements * Cerbos >= v0.16 * `@cerbos/http` or `@cerbos/grpc` client * Mongoose 8.x or 9.x * MongoDB 5.0+ * Node.js >= 20.0.0 ## [](#%5Finstallation)Installation ```bash npm install @cerbos/orm-mongoose ``` ## [](#%5Fsupported%5Foperators)Supported operators | Category | Operators | | ---------- | -------------------------------------------------------------------------------------- | | Logical | and, or, not — mapped to $and, $or, $nor | | Comparison | eq, ne, lt, le, gt, ge — mapped to $eq, $ne, $lt, $lte, $gt, $gte | | Membership | in, hasIntersection — $in for simple lists, $elemMatch for array relations | | String | contains, startsWith, endsWith — escaped regular expressions | | Existence | isSet, exists, exists\_one — $exists/$ne: null for scalars, $elemMatch for collections | | Collection | filter, lambda, map, all — scoped $elemMatch filters with lambda variable resolution | | | exists\_one behaves as "at least one element matches". Enforcing "exactly one" requires an aggregation pipeline, which is outside the scope of this adapter. | | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#%5Fusage)Usage ```typescript import { queryPlanToMongoose, PlanKind } from "@cerbos/orm-mongoose"; const queryPlan = await cerbos.planResources({ principal: { id: "user1", roles: ["USER"] }, resource: { kind: "document" }, action: "view", }); const result = queryPlanToMongoose({ queryPlan, mapper }); if (result.kind === PlanKind.ALWAYS_DENIED) { return []; } const filters = result.kind === PlanKind.CONDITIONAL ? result.filters : {}; const records = await MyModel.find(filters); ``` Combine with existing application filters using `$and`: ```typescript await MyModel.find({ $and: [filters ?? {}, { archived: false }] }); ``` ## [](#%5Ffield%5Fmapper)Field mapper ```typescript const mapper = { "request.resource.attr.title": { field: "title" }, "request.resource.attr.owner": { relation: { name: "owner", type: "one", field: "id" }, }, "request.resource.attr.tags": { relation: { name: "tags", type: "many", fields: { name: { field: "name" } }, }, }, }; ``` * `field` — rewrites a Cerbos path to a different field name in MongoDB * `valueParser` — transforms leaf values during filter construction (for example, converting strings to `ObjectId`) * `relation` — describes embedded documents (`type: "one"`) or arrays (`type: "many"`) * `fields` — nested overrides for lambda expressions such as `tag.name` ### [](#%5Fvalue%5Fparsing)Value parsing Use `valueParser` to convert values from the query plan into types MongoDB expects: ```typescript import { Types } from "mongoose"; const mapper = { "request.resource.attr.id": { field: "_id", valueParser: (value) => new Types.ObjectId(value), }, }; ``` ## [](#%5Fcollection%5Foperators)Collection operators Collection-aware operators (`filter`, `exists`, `exists_one`, `hasIntersection`, `map`, `all`) require the mapper to declare the relation with `type: "many"`. The adapter scopes lambda variables and uses the `fields` map when translating expressions: * `exists`, `exists_one`, `filter` — wrap the condition in `$elemMatch` * `hasIntersection` — supports both scalar arrays and arrays of objects; projects nested paths via `$elemMatch` * `all` — converts the lambda condition into a negated `$elemMatch` so all elements must satisfy the predicate ## [](#%5Fsource%5Fcode)Source code [cerbos/query-plan-adapters/mongoose](https://github.com/cerbos/query-plan-adapters/tree/main/mongoose) Prisma adapter ==================== The `@cerbos/orm-prisma` package converts a Cerbos [PlanResources](../../api/index.html#resources-query-plan) response into a Prisma `where` clause. The resulting filter object can be passed directly to `findMany`, `count`, or any Prisma method that accepts a `where` argument. ## [](#%5Frequirements)Requirements * Cerbos >= v0.40 * `@cerbos/http` or `@cerbos/grpc` client * Prisma >= 6.0 (v7 supported) * Node.js >= 20.0.0 ## [](#%5Finstallation)Installation ```bash npm install @cerbos/orm-prisma ``` ## [](#%5Fsupported%5Foperators)Supported operators ### [](#%5Fscalar%5Foperators)Scalar operators `and`, `or`, `not`, `eq`, `ne`, `lt`, `gt`, `lte`, `gte`, `in`, `startsWith`, `endsWith`, `contains`, `isSet` ### [](#%5Frelation%5Foperators)Relation operators `is`, `isNot`, `some`, `none`, `every`, `exists`, `exists_one`, `all`, `filter`, `except`, `hasIntersection` ### [](#%5Fadvanced%5Ffeatures)Advanced features * Deep nested relation support * Automatic field inference from attribute paths * Collection mapping and filtering * Lambda expression handling ## [](#%5Fusage)Usage ```typescript import { queryPlanToPrisma, PlanKind } from "@cerbos/orm-prisma"; const queryPlan = await cerbos.planResources({ principal: { id: "user1", roles: ["USER"] }, resource: { kind: "contact" }, action: "read", }); const result = queryPlanToPrisma({ queryPlan, mapper: { "request.resource.attr.ownerId": { field: "ownerId" }, "request.resource.attr.status": { field: "status" }, }, }); switch (result.kind) { case PlanKind.ALWAYS_DENIED: return []; case PlanKind.ALWAYS_ALLOWED: return await prisma.contact.findMany(); case PlanKind.CONDITIONAL: return await prisma.contact.findMany({ where: result.filters, }); } ``` ## [](#%5Ffield%5Fmapper)Field mapper The mapper translates Cerbos attribute references to Prisma field names. It accepts either an object or a function. ```typescript // Object mapper const result = queryPlanToPrisma({ queryPlan, mapper: { "request.resource.attr.status": { field: "status" }, }, }); // Function mapper const result = queryPlanToPrisma({ queryPlan, mapper: (attr) => ({ field: attr.replace("request.resource.attr.", ""), }), }); ``` ## [](#%5Frelation%5Fmapping)Relation mapping Relations are declared with their type and optional field configuration. Fields not explicitly mapped are inferred from the attribute path. ```typescript const result = queryPlanToPrisma({ queryPlan, mapper: { "request.resource.attr.owner": { relation: { name: "owner", type: "one", }, }, "request.resource.attr.tags": { relation: { name: "tags", type: "many", field: "name", }, }, }, }); ``` ### [](#%5Fnested%5Frelations)Nested relations Relations can be nested to arbitrary depth: ```typescript const result = queryPlanToPrisma({ queryPlan, mapper: { "request.resource.attr.categories": { relation: { name: "categories", type: "many", fields: { subCategories: { relation: { name: "subCategories", type: "many", fields: { name: { field: "name" }, }, }, }, }, }, }, }, }); ``` The adapter generates the nested `NOT`, `some`, `every`, and `none` structures that Prisma requires for the full relation chain. ### [](#%5Flambda%5Fexpressions)Lambda expressions Collection operators such as `exists` and `all` produce lambda expressions in the query plan. The adapter translates these into the corresponding Prisma relation filters: ```typescript const result = queryPlanToPrisma({ queryPlan, mapper: { "request.resource.attr.comments": { relation: { name: "comments", type: "many", fields: { author: { relation: { name: "author", type: "one", }, }, status: { field: "status" }, }, }, }, }, }); ``` ## [](#%5Fin%5Foperator%5Fnormalization)`in` operator normalization The adapter normalizes `in` expressions to match Prisma conventions: * Single values become equality comparisons: `{ field: "value" }` * Arrays remain `{ field: { in: […​] } }` * Relation-backed fields retain their relation structure while applying the appropriate operator at the leaf ## [](#%5Ftypes)Types ```typescript import { PlanKind, QueryPlanToPrismaResult } from "@cerbos/orm-prisma"; type QueryPlanToPrismaResult = | { kind: PlanKind.ALWAYS_ALLOWED | PlanKind.ALWAYS_DENIED } | { kind: PlanKind.CONDITIONAL; filters: Record }; type MapperConfig = { field?: string; relation?: { name: string; type: "one" | "many"; field?: string; fields?: { [key: string]: MapperConfig }; }; }; type Mapper = | { [key: string]: MapperConfig } | ((key: string) => MapperConfig); ``` ## [](#%5Fsource%5Fcode)Source code [cerbos/query-plan-adapters/prisma](https://github.com/cerbos/query-plan-adapters/tree/main/prisma) ## [](#%5Ftutorial%5Fexpress%5Fprisma%5Fcerbos)Tutorial: Express + Prisma + Cerbos This walkthrough builds a CRM API using Express, Prisma, and Cerbos. The business rules: * Admins can perform all actions * Users in the Sales department can read and create contacts * Only the user who created the contact can update and delete it The full source code is available at [github.com/cerbos/express-prisma-cerbos](https://github.com/cerbos/express-prisma-cerbos/). ### [](#%5Fprisma%5Fschema)Prisma schema ```bash mkdir express-prisma-cerbos cd express-prisma-cerbos npm i express @cerbos/grpc @prisma/client @cerbos/orm-prisma && npm i --save-dev @types/express ts-node ``` Create `prisma/schema.prisma`: ```prisma datasource db { provider = "sqlite" url = "file:./dev.db" } generator client { provider = "prisma-client-js" } model Contact { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt firstName String lastName String ownerId String active Boolean @default(false) marketingOptIn Boolean @default(false) } ``` Initialize the database: ```bash npx prisma migrate dev --name init ``` ### [](#%5Fcerbos%5Fpolicy)Cerbos policy Create `cerbos/policies/contacts.yaml`: ```yaml apiVersion: api.cerbos.dev/v1 resourcePolicy: version: default resource: contact rules: - actions: ["*"] effect: EFFECT_ALLOW roles: - admin - actions: ["read", "create"] effect: EFFECT_ALLOW roles: - user condition: match: expr: request.principal.attr.department == "Sales" - actions: ["update", "delete"] effect: EFFECT_ALLOW roles: - user condition: match: expr: request.resource.attr.ownerId == request.principal.id ``` Start the Cerbos PDP: ```bash docker run -i -t -p 3592:3592 \ -v $(pwd)/cerbos/policies:/policies \ ghcr.io/cerbos/cerbos:0.55.0 \ server ``` ### [](#%5Fauthorizing%5Findividual%5Fresources)Authorizing individual resources Use `checkResource` to authorize access to a single contact: ```typescript import { PrismaClient } from "@prisma/client"; import express from "express"; import { GRPC as Cerbos } from "@cerbos/grpc"; const prisma = new PrismaClient(); const cerbos = new Cerbos("localhost:3592", { tls: false }); const user = { id: "1", role: "user", department: "Sales", }; app.get("/contacts/:id", async ({ params }, res) => { const contact = await prisma.contact.findUnique({ where: { id: params.id }, }); if (!contact) return res.status(404).json({ error: "Contact not found" }); const decision = await cerbos.checkResource({ principal: { id: `${user.id}`, roles: [user.role], attributes: { department: user.department }, }, resource: { kind: "contact", id: contact.id + "", attributes: JSON.parse(JSON.stringify(contact)), }, actions: ["read"], }); if (decision.isAllowed("read")) { return res.json(contact); } else { return res.status(403).json({ error: "Unauthorized" }); } }); ``` ### [](#%5Ffiltering%5Fwith%5Fthe%5Fquery%5Fplan%5Fadapter)Filtering with the query plan adapter Use `planResources` with the Prisma adapter to retrieve only the contacts the principal is authorized to access: ```typescript import { queryPlanToPrisma, PlanKind } from "@cerbos/orm-prisma"; app.get("/contacts", async (req, res) => { const contactQueryPlan = await cerbos.planResources({ principal: { id: `${user.id}`, roles: [user.role], attributes: { department: user.department }, }, resource: { kind: "contact" }, action: "read", }); const queryPlanResult = queryPlanToPrisma({ queryPlan: contactQueryPlan, mapper: { "request.resource.attr.ownerId": { field: "ownerId" }, "request.resource.attr.department": { field: "department" }, "request.resource.attr.active": { field: "active" }, "request.resource.attr.marketingOptIn": { field: "marketingOptIn" }, }, }); let contacts; if (queryPlanResult.kind === PlanKind.ALWAYS_DENIED) { contacts = []; } else if (queryPlanResult.kind === PlanKind.ALWAYS_ALLOWED) { contacts = await prisma.contact.findMany({ select: { firstName: true, lastName: true, active: true, marketingOptIn: true, }, }); } else if (queryPlanResult.kind === PlanKind.CONDITIONAL) { contacts = await prisma.contact.findMany({ where: { AND: [{ ...queryPlanResult.filters }], }, select: { firstName: true, lastName: true, active: true, marketingOptIn: true, }, }); } return res.json({ contacts }); }); ``` ### [](#%5Frunning%5Fthe%5Fexample)Running the example Start the Cerbos PDP as described above, then start the Express server: ```bash npx ts-node src/index.ts ``` Test the endpoints: ```bash curl -i http://localhost:3000/contacts/1 curl -i http://localhost:3000/contacts ``` SQLAlchemy adapter ==================== The `cerbos-sqlalchemy` package converts a Cerbos [PlanResources](../../api/index.html#resources-query-plan) response into a SQLAlchemy `Select` instance. The resulting query object can be extended with additional `where` clauses or column selections before execution. ## [](#%5Frequirements)Requirements * Cerbos >= v0.16 * [Cerbos Python SDK](https://github.com/cerbos/cerbos-sdk-python) * SQLAlchemy >= 1.4 / 2.0 ## [](#%5Finstallation)Installation ```bash pip install cerbos-sqlalchemy ``` ## [](#%5Fsupported%5Foperators)Supported operators `and`, `or`, `not`, `eq`, `ne`, `lt`, `gt`, `le` (`lte`), `ge` (`gte`), `in` Other operators (for example, math operators) can be handled programmatically and attached to the query via `query.where(…​)`. ## [](#%5Fusage)Usage ```python from cerbos.sdk.client import CerbosClient from cerbos.sdk.model import Principal, ResourceDesc from cerbos_sqlalchemy import get_query from sqlalchemy import Column, Integer, String from sqlalchemy.orm import declarative_base Base = declarative_base() class LeaveRequest(Base): __tablename__ = "leave_request" id = Column(Integer, primary_key=True) department = Column(String(225)) geography = Column(String(225)) team = Column(String(225)) priority = Column(Integer) with CerbosClient(host="http://localhost:3592") as c: p = Principal( "john", roles={"employee"}, policy_version="20210210", attr={ "department": "marketing", "geography": "GB", "team": "design", }, ) rd = ResourceDesc("leave_request", policy_version="20210210") plan = c.plan_resources("view", p, rd) attr_map = { "request.resource.attr.department": LeaveRequest.department, "request.resource.attr.geography": LeaveRequest.geography, "request.resource.attr.team": LeaveRequest.team, "request.resource.attr.priority": LeaveRequest.priority, } query = get_query(plan, LeaveRequest, attr_map) ``` ### [](#%5Fmulti%5Ftable%5Fjoins)Multi-table joins When the `attr_map` references columns from more than one table, pass the join mapping as the fourth positional argument: ```python query = get_query( plan, Table1, { "request.resource.attr.foo": Table1.foo, "request.resource.attr.bar": Table2.bar, "request.resource.attr.bosh": Table3.bosh, }, [ (Table2, Table1.table2_id == Table2.id), (Table3, Table1.table3_id == Table3.id), ], ) ``` ### [](#%5Fextending%5Fthe%5Fquery)Extending the query ```python query = query.where(LeaveRequest.priority < 5) query = query.with_only_columns( LeaveRequest.department, LeaveRequest.geography, ) ``` ## [](#%5Foverriding%5Fdefault%5Foperators)Overriding default operators Override specific operator implementations for database-specific alternatives: ```python from sqlalchemy.sql.expression import any_ query = get_query( plan, some_table, attr_map={"request.resource.attr.foo": Table1.foo}, operator_override_fns={ "in": lambda c, v: c == any_(v), }, ) ``` ## [](#%5Fsource%5Fcode)Source code [cerbos/query-plan-adapters/sqlalchemy](https://github.com/cerbos/query-plan-adapters/tree/main/sqlalchemy) ## [](#%5Ftutorial%5Ffastapi%5Fsqlalchemy%5Fcerbos)Tutorial: FastAPI + SQLAlchemy + Cerbos This walkthrough builds a contact directory API using FastAPI, SQLAlchemy, and Cerbos. The application demonstrates both `CheckResources` for individual resource authorization and `PlanResources` for query-level filtering. The full source code is available at [github.com/cerbos/python-sqlalchemy-cerbos](https://github.com/cerbos/python-sqlalchemy-cerbos). ### [](#%5Fprerequisites)Prerequisites * Python 3.10 * [SQLAlchemy](https://docs.sqlalchemy.org/en/14/) 1.4 / 2.0 * [Docker](https://www.docker.com/products/docker-desktop/) ### [](#%5Fdata%5Fmodel)Data model The application has three entities: * `User` — the person interacting with the application * `Contact` — a person within a user’s directory (a user can have many contacts) * `Company` — the company a contact is employed with (a company can have many contacts) SQLAlchemy represents these as classes with column attributes and relationship declarations: ```python from sqlalchemy import Column, String, ForeignKey from sqlalchemy.orm import declarative_base, relationship Base = declarative_base() class User(Base): __tablename__ = "user" id = Column(String, primary_key=True) username = Column(String(255)) email = Column(String(255)) contacts = relationship("Contact", back_populates="owner") class Contact(Base): __tablename__ = "contact" id = Column(String, primary_key=True) owner_id = Column(String, ForeignKey("user.id")) owner = relationship("User", back_populates="contacts", lazy="joined") ``` The `ForeignKey` on the child table establishes the many-to-one side. `lazy="joined"` loads the related object at attribute access time. The full table definitions are in [app/models.py](https://github.com/cerbos/python-sqlalchemy-cerbos/blob/main/app/models.py). ### [](#%5Fdependency%5Finjection)Dependency injection FastAPI dependables retrieve the Cerbos `Principal` and database `Contact` from request context: ```python from fastapi import Depends, HTTPException, status from fastapi.security import HTTPBasic, HTTPBasicCredentials security = HTTPBasic() def get_principal(credentials: HTTPBasicCredentials = Depends(security)) -> Principal: username = credentials.username with Session() as s: user = s.scalars(select(User).where(User.username == username)).first() if user is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found", ) return Principal(user.id, roles={user.role}, attr={"department": user.department}) def get_db_contact(contact_id: str) -> Contact: with Session() as s: contact = s.scalars(select(Contact).where(Contact.id == contact_id)).first() if contact is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Contact not found", ) return contact ``` ### [](#%5Fauthorizing%5Findividual%5Fresources)Authorizing individual resources Use `CheckResources` via the `is_allowed` method for single-resource authorization: ```python @app.get("/contacts/{contact_id}") def get_contact( db_contact: Contact = Depends(get_db_contact), p: Principal = Depends(get_principal), ): r = get_resource_from_contact(db_contact) with CerbosClient(host="http://localhost:3592") as c: if not c.is_allowed("read", p, r): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Unauthorized" ) return db_contact ``` ### [](#%5Ffiltering%5Fwith%5Fthe%5Fquery%5Fplan%5Fadapter)Filtering with the query plan adapter Use `PlanResources` with the SQLAlchemy adapter to retrieve only the contacts the principal is authorized to access: ```python @app.get("/contacts") def get_contacts(p: Principal = Depends(get_principal)): with CerbosClient(host="http://localhost:3592") as c: rd = ResourceDesc("contact") plan = c.plan_resources("read", p, rd) query = get_query( plan, Contact, { "request.resource.attr.owner_id": User.id, "request.resource.attr.department": User.department, "request.resource.attr.is_active": Contact.is_active, "request.resource.attr.marketing_opt_in": Contact.marketing_opt_in, }, [(User, Contact.owner_id == User.id)], ) query = query.with_only_columns( Contact.id, Contact.first_name, Contact.last_name, Contact.is_active, Contact.marketing_opt_in, ) with Session() as s: rows = s.execute(query).fetchall() return rows ``` The `get_query` function accepts: 1. The query plan response 2. A primary SQLAlchemy `Table` or ORM entity (the `FROM` table) 3. The attribute map — Cerbos resource attribute strings mapped to SQLAlchemy columns 4. (Optional) Explicit table joins — required when the attribute map references columns from multiple tables ### [](#%5Frunning%5Fthe%5Fexample)Running the example Clone and start: ```bash git clone git@github.com:cerbos/python-sqlalchemy-cerbos.git cd python-sqlalchemy-cerbos cd cerbos && ./start.sh && cd .. pdm install pdm run demo ``` Test the endpoints: ```bash # Get all permitted contacts curl http://john@localhost:8000/contacts # Get a single contact (Sales user, owned contact) curl -i http://john@localhost:8000/contacts/1 # Delete a contact (owner) curl -i http://john@localhost:8000/contacts/1 -X DELETE # Create a contact (Sales user) curl -i http://john@localhost:8000/contacts/new \ -H 'Content-Type: application/json' \ -X POST \ -d '{"first_name": "frodo", "last_name": "baggins", "owner_id": "2", "company_id": "2"}' ``` Implementing authorization in RAG-based AI systems ==================== As companies implement AI applications, a Retrieval Augmented Generation (RAG) architecture is often used to give an LLM context from internal data. The challenge that consequently arises is how to provide the LLM with sufficient context without violating privacy and authorization policies. Companies need to ensure that AI agents can’t inappropriately access sensitive data or expose it to unauthorized users. ## [](#%5Fhow%5Fthe%5Fcerbos%5Fquery%5Fplan%5Fworks%5Fwith%5Frag)How the Cerbos query plan works with RAG RAG architecture can leverage large data sets of internal knowledge: documents, meeting notes, and resources to provide business-specific context to the LLM. This business data first has to be extracted from the system of record (ERP, CRM, HRIS, etc), go through an embedding process, and then be loaded into a vector store — which is a specialized database that can find similar items based on their vector embeddings. Vector stores also support storing metadata such as what the source system was, which department it belongs to and which region it’s associated with. These can all be leveraged for making authorization decisions. With the business data stored along with its associated metadata, a typical workflow would be to put a chatbot-style interface in front. The workflow that processes user input and produces results can use Cerbos to apply authorization logic to data retrieval. 1. The access filters applicable to the user in the current context are generated by the Cerbos Policy Decision Point (PDP). 2. The user input is vectorized using an embedding model and used to query the vector data store. A metadata filter is applied to the query using the Cerbos query plan to restrict the results. 3. The retrieved documents are injected into the prompt and fed to the LLM to generate the answer. ## [](#%5Fleveraging%5Fcerbos%5Fquery%5Fplan%5Ffor%5Fauthorization)Leveraging Cerbos Query Plan for authorization Cerbos can be used to enforce authorization policies on the data retrieval step in the RAG architecture. The Cerbos Query Plan can be used to generate filters based on the user’s identity and the metadata associated with the data. This ensures that the LLM only uses data that the user is authorized to access. As an example, consider a scenario where a LLM is used to provide information about work projects. A project is associated with a department and a region and only those users in the same department or region should be able to access it. The Cerbos policy for this could be defined as follows: ```yaml apiVersion: api.cerbos.dev/v1 resourcePolicy: version: "default" resource: "project" rules: - actions: ["read"] effect: EFFECT_ALLOW condition: all: of: - expr: R.attr.department == P.attr.department - expr: R.attr.region == P.attr.region # ... other rules ``` When a user issues a query using the chat interface, their authorization context is passed to the Cerbos PDP in a [PlanResources](../../../api/index.html#plan-resources) request to produce a plan for performing the `read` action on the `project` resource kind.`` ` `` ```json { "principal": { "id": "alice", "roles": [ "USER", "MANAGER" ], "attr": { "department": "FINANCE", "region": "EMEA" } }, "resource": { "kind": "project", }, "action": "read", } ``` The Query Plan generated by Cerbos would then include the following conditions - note that the actual response is completely dynamic and depends on the user’s identity and the policy defined in Cerbos: ```json { "action": "read", "resourceKind": "project", "filter": { "kind": "KIND_CONDITIONAL", "condition": { "expression": { "operator": "and", "operands": [ { "expression": { "operator": "eq", "operands": [ { "variable": "request.resource.attr.department" }, { "value": "FINANCE" } ] } }, { "expression": { "operator": "eq", "operands": [ { "variable": "request.resource.attr.region" }, { "value": "EMEA" } ] } } ] } } } } ``` This set of conditions then can be converted into a metadata filter that can be applied to the vector store query to ensure that only the data that the user is authorized to access is retrieved. Each vector store has its own syntax for defining filters. With the popular [Chroma](https://www.trychroma.com/) database system, a filter looks as follows. ```json { "$and": [ {"department": "FINANCE"}, {"region": "EMEA"} ] } ``` With this filter in place, any documents retired from the vector store would be limited to those that match the user’s department and region, ensuring that the LLM only receives data that the user is authorized to access. Implementing authorization in RAG-based AI systems is crucial to ensure that sensitive data is not exposed to unauthorized users. By leveraging Cerbos query plan, companies can ensure that their LLM use cases don’t inadvertently violate privacy and data security requirements. Tutorial: Using Cerbos with Auth0 ==================== An example application of integrating [Cerbos](https://cerbos.dev) with an [Express](https://expressjs.com/) server using [Auth0](https://auth0.com/) for authentication. ## [](#%5Fdependencies)Dependencies * Node.js * Docker for running the [Cerbos Policy Decision Point (PDP)](../../../installation/container.html) * An [Auth0](https://auth0.com/) account if you want to use your own ## [](#%5Fgetting%5Fstarted)Getting started 1. Clone the repo ```bash git clone git@github.com:cerbos/express-auth0-cerbos.git ``` 2. Start up the Cerbos PDP instance docker container. This will be called by the express app to check authorization. ```bash cd cerbos ./start.sh ``` 3. Install node dependencies ```bash npm install ``` 4. Start the express server ```bash node index.js ``` ## [](#%5Fauth0%5Frule%5Fto%5Fadd%5Froles%5Fto%5Ftoken)Auth0 Rule to add roles to token By default any roles set up in the Auth0 console are not passed through in the authentication token. To enable this, add the following rule to the Auth Pipeline in your Auth0 account. ```js function (user, context, callback) { const namespace = 'https://cerbos.cloud'; const assignedRoles = (context.authorization || {}).roles; let idTokenClaims = context.idToken || {}; let accessTokenClaims = context.accessToken || {}; idTokenClaims[`${namespace}/roles`] = assignedRoles; accessTokenClaims[`${namespace}/roles`] = assignedRoles; context.idToken = idTokenClaims; context.accessToken = accessTokenClaims; callback(null, user, context); } ``` ## [](#%5Fpolicies)Policies This example has a simple CRUD policy in place for a resource kind of`contact` \- like a CRM system would have. The policy file can be found in the `cerbos/policies` folder[here](https://github.com/cerbos/express-auth0-cerbos/blob/main/cerbos/policies/contact.yaml). Should you wish to experiment with this policy, you can try it in the[Cerbos Playground](https://play.cerbos.dev/p/g561543292ospj7w0zOrFx7H5DzhmLu2). The policy expects one of two roles to be set on the principal - `admin`and `user`. These roles are authorized as follows: | Action | User | Admin | | ------ | -------- | ----- | | list | Y | Y | | read | Y | Y | | create | Y | Y | | update | If owner | Y | | delete | If owner | Y | This business logic is represented in Cerbos as a resource policy. ```yaml --- apiVersion: api.cerbos.dev/v1 resourcePolicy: version: default resource: contact rules: - actions: ["read", "create"] effect: EFFECT_ALLOW roles: - admin - user - actions: ["update", "delete"] effect: EFFECT_ALLOW roles: - admin - actions: ["update", "delete"] effect: EFFECT_ALLOW roles: - user condition: match: expr: request.resource.attr.owner == request.principal.id ``` Tutorial: Using Cerbos with AWS Cognito ==================== An example application of integrating [Cerbos](https://cerbos.dev) with a [FastAPI](https://fastapi.tiangolo.com/) server using [AWS Cognito](https://aws.amazon.com/cognito/) for authentication. ## [](#%5Fdependencies)Dependencies * Python 3.10 * Docker for running the [Cerbos Policy Decision Point (PDP)](https://docs.cerbos.dev/cerbos/latest/installation/container.html) * A configured AWS Cognito User Pool ([set-up guide](https://docs.aws.amazon.com/cognito/latest/developerguide/getting-started-with-cognito-user-pools.html)) ## [](#%5Fgetting%5Fstarted)Getting started 1. Clone the repo ```bash git clone git@github.com:cerbos/python-cognito-cerbos.git ``` 2. Start up the Cerbos PDP instance docker container. This will be called by the FastAPI app to check authorization. ```bash cd cerbos ./start.sh ``` 3. Install python dependencies ```bash # from project root ./pw install ``` 4. Set the appropriate environment variables ```bash AWS_COGNITO_POOL_ID AWS_COGNITO_CLIENT_ID AWS_DEFAULT_REGION AWS_COGNITO_POOL_NAME # if you've configured your user pool with a client secret AWS_COGNITO_CLIENT_SECRET # optionally, to enable the hosted UI: AWS_COGNITO_HOSTED_UI_CALLBACK_URL # this needs to match the callback URL configured for the hosted UI AWS_COGNITO_HOSTED_UI_LOGOUT_URL ``` 5. Start the FastAPI dev server ```bash ./pw demo ``` ## [](#%5Fcognito%5Fconfiguration)Cognito Configuration ### [](#%5Fgroups)Groups This demo maps Cognito User Pool groups to Cerbos roles. The app will retrieve the groups from the access token, and use them to determine authorization. Any test users in your pool should be added to one or both of `admin` and/or `user` groups to demonstrate different access to the demo resources. ## [](#%5Fpolicies)Policies This example has a simple CRUD policy in place for a resource kind of`contact` \- like a CRM system would have. The policy file can be found in the `cerbos/policies` folder[here](https://github.com/cerbos/python-cognito-cerbos/blob/main/cerbos/policies/contact.yaml). Should you wish to experiment with this policy, you can try it in the[Cerbos Playground](https://play.cerbos.dev/p/g561543292ospj7w0zOrFx7H5DzhmLu2). The policy expects one of two roles to be set on the principal - `admin`and `user`. These roles are authorized as follows: | Action | User | Admin | | ------ | -------- | ----- | | list | Y | Y | | read | Y | Y | | create | Y | Y | | update | If owner | Y | | delete | If owner | Y | This business logic is represented in Cerbos as a resource policy. ```yaml --- apiVersion: api.cerbos.dev/v1 resourcePolicy: version: default resource: contact rules: - actions: ["read", "create"] effect: EFFECT_ALLOW roles: - admin - user - actions: ["update", "delete"] effect: EFFECT_ALLOW roles: - admin - actions: ["update", "delete"] effect: EFFECT_ALLOW roles: - user condition: match: expr: request.resource.attr.owner == request.principal.id ``` Tutorial: Using Cerbos with FusionAuth ==================== An example stack of integrating [Cerbos](https://cerbos.dev) with an [Express](https://expressjs.com/) server using [FusionAuth](https://fusionauth.com/) for authentication. This example is based off the [FusionAuth/fusionauth-example-node](https://github.com/fusionauth/fusionauth-example-node) repo. ## [](#%5Fdependencies)Dependencies * docker-compose ## [](#%5Fgetting%5Fstarted)Getting started 1. Clone the repo ```bash git clone git@github.com:cerbos/express-fusionauth-cerbos.git ``` ### [](#%5Fstart%5Fstack)Start Stack Start up the stack `docker compose up` \- this will take some time to pull down all the images and launch them, but once started the following services will be running. * FusionAuth * Cerbos * Node App * Postgres DB for FustionAuth on port `5432` ### [](#%5Fconfigure%5Ffusionauth)Configure FusionAuth This example is based off the[FusionAuth 5 Minute Guide](https://fusionauth.io/docs/v1/tech/5-minute-setup-guide/) \- and most of the steps have bee handled by the`docker compose` setup. The only manual steps required are creating the application. To do this, open up and complete the setup wizard, then: Once we arrive in the FusionAuth admin UI, the first thing we need to do is create an Application. An Application is something that a user can log into. This is the application we are building or that we are migrating to use FusionAuth. We’ll click the Application menu option on the left side of the page or the Setup button in the box at the top of the page. ![fusionauth dashboard applications](../../_images/fusionauth-dashboard-applications.png) This will take us to the listing page for Applications. Next, we’ll click the green plus button (the add button) at the top of the page: ![fusionauth application listing](../../_images/fusionauth-application-listing.png) On the Application form, we’ll need to provide a name for our Application (only used for display purposes) and a couple of items on the OAuth tab. We’ll start with a simple setup that allows existing users to log into your application. Therefore, we won’t need to define any roles or registration configuration. If we click on the OAuth tab, we’ll see these options: ![fusionauth application form](../../_images/fusionauth-application-form.png) Most of the defaults will work, but we also need to provide these items: * An authorized redirect URL. This is the route/controller in our application’s backend that will complete the OAuth workflow. This is also known as the 'Backend for Frontend' or BFF pattern, and is a lightweight proxy. In our example, we set this to``. We’ll show some Node.js example code below for this route. * Optionally, we can specify a valid Logout URL. This is where the user will be redirected to after they are logged out of FusionAuth’s OAuth front-end: our application. * We need to ensure that the Authorization Code grant is selected in the Enabled Grants. Next we need to add the roles that will be used by our policies. Back on the application listing page press the 'Manage Roles' button next to our application and add roles for `user` and `editor` (admin should already exist). These roles will be passed back with the user information to our application, and then passed onto Cerbos for use in authorization decisions. ![fusionauth add roles](../../_images/fusionauth-add-roles.png) Once we have all of this configured, we can then copy the Client ID and Client Secret and move to the next step. ### [](#%5Fconfigure%5Fnode%5Fapp)Configure Node App Now that our application has been created, we need to add the Client ID and Client Secret from FusionAuth into the top of `app/index.js` (line 12 & 13). These will be used to identify the app through the login flow. ### [](#%5Ftest%5Fthe%5Fapp)Test the app Now that everything is wired up you should be able to goto and press the login link to authenticate with your FusionAuth account. ## [](#%5Fpolicies)Policies This example has a simple CRUD policy in place for a resource kind of`contact` \- like a CRM system would have. The policy file can be found in the `cerbos/policies` folder[here](https://github.com/cerbos/express-fusionauth-cerbos/blob/main/cerbos/policies/contact.yaml). Should you wish to experiment with this policy, you can try it in the[Cerbos Playground](https://play.cerbos.dev/p/g561543292ospj7w0zOrFx7H5DzhmLu2). The policy expects one of two roles to be set on the principal - `admin`and `user`. These roles are authorized as follows: | Action | User | Admin | | ------ | -------- | ----- | | list | Y | Y | | read | Y | Y | | create | Y | Y | | update | If owner | Y | | delete | If owner | Y | ## [](#%5Frequest%5Fflow)Request Flow 1. User access the application and clicks `Login` 2. User is directed to the FusionAuth UI and authenticates 3. A token is returned back in the redirect URL to the application 4. That token is then exchanged for the user profile information 5. The user profile from FusionAuth being stored (user Id, email, roles etc). 6. Any requests to the `/contacts` endpoints fetch the data required about the resource being accessed from the data store 7. Call the Cerbos PDP with the principal, resource and action to check the authorization and then return an error if the user is not authorized. The [Cerbos package](https://www.npmjs.com/package/cerbos) is used for this. ```javascript --- const allowed = await cerbos.check({ principal: { //pass in the Okta user ID and groups id: req.userContext.userinfo.sub, roles: req.userContext.userinfo.groups, }, resource: { kind: "contact", instances: { //a map of the resource(s) being accessed [contact.id]: { attr: contact, }, }, }, actions: ["read"], //the list of actions being performed }); ``` if (!allowed.isAuthorized(contact.id, "read")) { return res.status(403).json({ error: "Unauthorized" }); } --- Implementation at this stage will be dependant on your business requirements. Tutorial: Using Cerbos with JWT ==================== An example application of integrating [Cerbos](https://cerbos.dev) with an[Express](https://expressjs.com/) server using [JSON Web Tokens](https://jwt.io/) \- via [express-jwt](https://github.com/auth0/express-jwt) \- for authentication. ## [](#%5Fdependencies)Dependencies * Node.js * Docker for running the [Cerbos Policy Decision Point (PDP)](../../../installation/container.html) ## [](#%5Fgetting%5Fstarted)Getting started 1. Clone the repo ```bash git clone git@github.com:cerbos/express-jwt-cerbos.git ``` 2. Start up the Cerbos PDP instance docker container. This will be called by the express app to check authorization. ```bash cd cerbos ./start.sh ``` 3. Install node dependencies ```bash npm install ``` 4. Start the express server ```bash npm run start ``` ## [](#%5Fpolicies)Policies This example has a simple CRUD policy in place for a resource kind of`contact` \- like a CRM system would have. The policy file can be found in the `cerbos/policies` folder[here](https://github.com/cerbos/express-jwt-cerbos/blob/main/cerbos/policies/contact.yaml). Should you wish to experiment with this policy, you can try it in the[Cerbos Playground](https://play.cerbos.dev/p/sZC611cf06deexP0q8CTcVufTVau1SA3). The policy expects one of two roles to be set on the principal - `admin`and `user`. These roles are authorized as follows: | Action | User | Admin | | ------ | ---- | ----- | | list | Y | Y | | read | Y | Y | | create | N | Y | | update | N | Y | | delete | N | Y | This business logic is represented in Cerbos as a resource policy. ```yaml apiVersion: api.cerbos.dev/v1 resourcePolicy: version: default resource: contact rules: - actions: ["read", "list"] roles: - admin - user effect: EFFECT_ALLOW - actions: ["create", "update", "delete"] roles: - admin effect: EFFECT_ALLOW ``` ## [](#%5Fjwt%5Fstructure)JWT Structure For this example a JWT needs to be generated to be passed in the authorization header. The payload of the token contains an array of roles which are passed into Cerbos to use for authorization - the structure is as follows: { sub: string, name: string, iat: number, roles: string[] // "user" and "admin" supported in this demo } [JWT.io](https://jwt.io) can be used generate a token for testing purposes - an[example is here](https://jwt.io/#debugger-io?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZXMiOlsiYWRtaW4iXSwiaWF0IjoxNTE2MjM5MDIyfQ.CQEEaSdswE2tou7MUeSe4-6kfe1imJXnbqhiMFsF13A). **Note:** The secret is hardcoded in this example to `yoursecret` and the algorithm is `HS256` \- you will need to set these for the signature to be valid. ![JWT](../../_images/jwt-token.png) ## [](#%5Frequest%5Fflow)Request Flow 1. HTTP request comes in and the `express-jwt` library validates the token and adds the payload to `req.user`. 2. The contents of the JWT token is mapped to the structure of the principal object required by Cerbos ```js // Extract data from the JWT (check DB etc) and create the principal object to be sent to Cerbos const jwtToPrincipal = ({ sub, iat, roles = [], ...rest }) => { return { id: sub, roles, attr: rest, }; }; ``` 1. Fetch the data required about the resource being accessed from the data store 2. Call the Cerbos PDP with the principal, resource and action to check the authorization and then return an error if the user is not authorized. The [Cerbos package](https://www.npmjs.com/package/cerbos) is used for this. ```js const allowed = await cerbos.check({ principal: jwtToPrincipal(req.user), resource: { kind: "contact", instances: { //a map of the resource(s) being accessed [contact.id]: { attr: contact, }, }, }, actions: ["read"], //the list of actions being performed }); // not authorized for read action if (!allowed.isAuthorized(contact.id, "read")) { return res.status(403).json({ error: "Unauthorized" }); } ``` 1. Serve the response if authorized ## [](#%5Fexample%5Frequests)Example Requests Once a JWT token has been generated requests can be made to the express server. ### [](#%5Flist%5Fcontacts)List contacts Allowed for `user` and `admin` roles ```bash curl -X GET 'http://localhost:3000/contacts' \ --header 'Authorization: Bearer ' ``` ### [](#%5Fget%5Fa%5Fcontact)Get a contact Allowed for `user` and `admin` roles ```bash curl -X GET 'http://localhost:3000/contacts/abc123' \ --header 'Authorization: Bearer ' ``` ### [](#%5Fcreate%5Fa%5Fcontact)Create a contact Allowed for `admin` role only ```bash curl -X POST 'http://localhost:3000/contacts/new' \ --header 'Authorization: Bearer ' ``` Should this request be made with the JWT roles set to `["admin"]` the response will be" ```json { "result": "Created contact" } ``` Should this request be made with the JWT roles set to `["user"]` the response will be: ```json { "error": "Unauthorized" } ``` ### [](#%5Fupdate%5Fa%5Fcontact)Update a contact Allowed for `admin` role only ```bash curl -X PATCH 'http://localhost:3000/contacts/abc123' \ --header 'Authorization: Bearer ' ``` Should this request be made with the JWT roles set to `["admin"]` the response will be" ```json { "result": "Contact updated" } ``` Should this request be made with the JWT roles set to `["user"]` the response will be: ```json { "error": "Unauthorized" } ``` ### [](#%5Fdelete%5Fa%5Fcontact)Delete a contact Allowed for `admin` role only ```bash curl -X DELETE 'http://localhost:3000/contacts/abc123' \ --header 'Authorization: Bearer ' ``` Should this request be made with the JWT roles set to `["admin"]` the response will be" ```json { "result": "Contact deleted" } ``` Should this request be made with the JWT roles set to `["user"]` the response will be: ```json { "error": "Unauthorized" } ``` Tutorial: Using Cerbos with Magic ==================== The demise of passwords has [long been predicted](https://www.forbes.com/sites/forbestechcouncil/2020/03/06/the-inevitable-death-of-passwords/) due to the ongoing leaks, hacks and breaches in recent years. There has been a lot of innovation in this space and [Magic](https://magic.link) has become a leader with their novel approach to eradicating the need to store passwords at all by making use of ‘magic links’ which are sent via the provided email address and log you in to the site once clicked. > Magic provides a key-based identity solution built on top of the Decentralized Identity (DID) standard, where users’ identities are self-sovereign by leveraging blockchain public-private key pairs. These keypairs are used to generate zero-knowledge proofs to authenticate users instead of having to rely on users providing passwords to Magic or any identity provider — Magic Whitepaper https://www.dropbox.com/s/3flqaszoigwis5b/Magic%20Whitepaper.pdf This approach is a great way to securely establish a user’s identity without running authentication infrastructure. At this point, you can use this identity to fetch extra data such as roles and group memberships about the user from your directory, profile store or other database system to further add context about the user. The exact mechanism for this is out of the scope of this article, but Active Directory, LDAP or just a plain old DB are all good places to store this extra user information. Once a user has authenticated (and the profile enriched with profile information) the next step is to establish what the user has permissions to do in the application - this is where Cerbos steps in and through it’s policy based approach can do context-aware authorization using the user (or principal in Cerbos speak) from Magic. ## [](#%5Fexample%5Fimplementation%5Fwith%5Fcerbos)Example Implementation with Cerbos Implementing this requires passing the token provided from the Magic Client SDK to your backend code and then verifying it with the Magic Admin SDK. As an example, we have forked Magic’s Node/Express/Passport demo repo and added in calls to Cerbos to demonstrate how the two systems can work together - you can find a live [demo here](https://demo-magiclink.cerbos.cloud/) and [view source code on GitHub](https://github.com/cerbos/express-magiclink-cerbos). The logical data flow for how this is implemented is as follows: 1. User visits site and and enters their email address 2. Magic send an email to that address with a link which authenticates the user 3. The website gets a call back when a user clicks the link from the email with the authenticated identity and token client side 4. Calls to the authenticated endpoint can now be made with the token passed as a Bearer token which is parsed by Passport.js’s Magic integration 5. App code fetches the resource being accessed from the data store 6. App sends the user information from the verified Magic token along with the resource and desired actions to the Cerbos PDP instance 7. Cerbos PDP evaluates the policies and returns an ALLOW or DENY 8. App conditionally returns based on the authorization response The key part of this are Steps #4-7 where the context about the principal and the resource is gathered and sent to Cerbos to determine the authorization. In this stage all the attributes about the resource and the user can now be used to make a decision. ## [](#%5Fconclusion)Conclusion Magic’s approach to passwordless authentication and identity is a game changer in how to secure your application, and when paired with Cerbos for authorization, it is possible to deploy context-aware access controls without complex rules or token-bloat. Tutorial: Using Cerbos with Okta ==================== An example application of integrating [Cerbos](https://cerbos.dev) with an [Express](https://expressjs.com/) server using [Okta](https://okta.com/) for authentication. ## [](#%5Fdependencies)Dependencies * Node.js * An [Okta](https://okta.com/) account --- For simplicity this demo is using the hosted Cerbos Demo PDP avaliable in the Playground so running the Cerbos container locally isn’t required. For production use cases a deployed Cerbos PDP is required and the code updated to point to your instance. You can read more about the deployment options [here](https://docs.cerbos.dev/cerbos/latest/deployment/index.html). --- ## [](#%5Fsetup)Setup ### [](#%5Finstall%5Fdeps)Install Deps 1. Clone the repo ```bash git clone git@github.com:cerbos/express-okta-cerbos.git ``` ### [](#%5Fcreate%5Fan%5Fokta%5Fapplication)Create an Okta Application In your Okta instance you need to create a new application. For this example we will be making use of Okta’s ExpressOIDC package so the application’s sign-in method needs to be `OIDC - OpenID Connect` and the application type is `Web Application`. ![Okta Create App](../../_images/okta-create-app.png) ### [](#%5Fset%5Fredirect%5Furls)Set Redirect URLs The default redirect URLs for sign-in and sign-out are correct if you are running this demo app on the default 8080 port. If you have chanaged this in your `.env` file then you will need to update accordingly. ![Okta App Settings](../../_images/okta-app-settings.png) ### [](#%5Fenabling%5Fgroups%5Fin%5Fthe%5Fokta%5Ftoken)Enabling Groups in the Okta Token By default the groups the user belongs to are not passed to the application in the Okta token - this needs enabling as these groups will be passed from Okta to Cerbos for use in authorization decisions. To do this, goto _Security > API_ in the sidebar, and edit the default_Authorization Server_. On this page, got the _Claims_ tab and press _Add Claim_. Add a new claim called groups which includes the groups of the user in the ID token. ![Okta Groups Claim](../../_images/okta-groups-claim.png) > In production you will likely want to filter this down, but for this example we are enabling all groups to be added to the token. ### [](#%5Fcreate%5Fan%5Fexample%5Fadmin%5Fgroup)Create an example `admin` group. In a new Okta account the only group that exists is the _Everyone_group. For our demo application policies we expect users to be in`admin` or `user` group as this is what is checked. Under _Directory > Groups_ press _Add Group_ and create the two groups and add your example users to them. ### [](#%5Fsetup%5Fenvironment%5Fvariables)Setup Environment Variables Make a copy of the `.env.sample` file and call it `.env`. You will then need to populate the feilds that begin with `OKTA_` with the information provided in the new application you created. PORT=8080 CERBOS_HOSTNAME=https://demo-pdp.cerbos.cloud CERBOS_PLAYGROUND=ygW612cc9c9xXOsOZjI40ovY2LZvXf43 OKTA_DOMAIN= OKTA_CLIENTID= OKTA_CLIENTSECRET= OKTA_APP_BASE_URL=http://localhost:8080 > This example is using the hosted Demo PDP of Cerbos and an example Playground instance. If you are running your own Cerbos PDP then update the `CERBOS_HOSTNAME` feild to your own instance and remove the`CERBOS_PLAYGROUND` feild. ### [](#%5Ftest%5Fthe%5Fapp)Test the app Now that everything is wired up you should be able to goto and press the login link to authenticate with your Okta account. ## [](#%5Fpolicies)Policies This example has a simple CRUD policy in place for a resource kind of`contact` \- like a CRM system would have. Should you wish to experiment with this policy, you can try it in the[Cerbos Playground](https://play.cerbos.dev/p/g561543292ospj7w0zOrFx7H5DzhmLu2). The policy expects one of two roles to be set on the principal - `admin`and `user`. These roles are authorized as follows: | Action | User | Admin | | ------ | ---- | ----- | | list | Y | Y | | read | Y | Y | | create | N | Y | | update | N | Y | | delete | N | Y | ## [](#%5Frequest%5Fflow)Request Flow 1. User access the application and clicks Login 2. User is directed to the Okta UI and authenticates 3. A token is returned back in the redirect URL to the application 4. That token is then exchanged for the user profile information 5. The user profile from Okta being stored (user Id, roles etc). 6. Any requests to the /contacts endpoints fetch the data required about the resource being accessed from the data store 7. Call the Cerbos PDP with the principal, resource and action to check the authorization and then return an error if the user is not authorized. The Cerbos package is used for this. ```javascript --- const allowed = await cerbos.check({ principal: { //pass in the Okta user ID and groups id: req.userContext.userinfo.sub, roles: req.userContext.userinfo.groups, }, resource: { kind: "contact", instances: { //a map of the resource(s) being accessed [contact.id]: { attr: contact, }, }, }, actions: ["read"], //the list of actions being performed }); ``` if (!allowed.isAuthorized(contact.id, "read")) { return res.status(403).json({ error: "Unauthorized" }); } --- Implementation at this stage will be dependant on your business requirements. Administration ==================== * [User management](user-management.html) * [Distribution licence](distribution-licence.html) Audit log collection ==================== With a simple configuration change, you can configure your PDPs to securely send audit logs to Cerbos Hub. This vastly simplifies the work that would otherwise be required to configure and deploy a log aggregation solution to securely collect, store and query audit logs from across your fleet. For embedded PDPs, decision logs can be captured locally using the SDK’s `onDecision` callback. See [ePDP SDK options](deployments-epdp-rules.html#%5Fsdk%5Foptions) for details. ## [](#%5Fenabling%5Fcollection)Enabling collection To get started, you need to obtain a set of client credentials. Navigate to the **Settings** → **Client credentials** tab of the deployment, click on **Generate a client credential** and generate a **Read & write** credential. Make sure to save the client secret in a safe place as it cannot be recovered. The client credentials can be provided to the PDP using environment variables or the configuration file. The environment variables to set are: | CERBOS\_HUB\_CLIENT\_ID | Client ID | | --------------------------- | ----------------------------------------------------------------------------- | | CERBOS\_HUB\_CLIENT\_SECRET | Client secret | | CERBOS\_HUB\_PDP\_ID | Optional. A unique name for the PDP. If not provided, a random value is used. | Alternatively, you can define these values in the Cerbos configuration file as follows: ```yaml hub: credentials: pdpID: "..." # Optional. Identifier for this Cerbos instance. clientID: "..." # ClientID clientSecret: "..." # ClientSecret ``` To enable audit log collection, configure the `hub` audit log backend with a local storage path. This local storage path is important for preserving the audit logs until they are safely saved to Cerbos Hub. If there are any network interruptions or if the PDP process crashes, the audit logs generated up to that point are saved on disk and will be sent to Cerbos Hub the next time the PDP starts. If you’re using a container orchestrator or a cloud-based solution to deploy Cerbos, attach a persistent storage volume at this path to ensure that the data does not get lost. ```yaml server: httpListenAddr: ":3592" # The port the HTTP server will listen on grpcListenAddr: ":3593" # The port the gRPC server will listen on hub: credentials: pdpID: "..." # Optional. Identifier for this Cerbos instance. clientID: "..." # ClientID clientSecret: "..." # ClientSecret audit: enabled: true backend: hub hub: storagePath: "..." # Local storage path for buffering the audit logs. ``` Refer to [Cerbos documentation](../cerbos/latest/configuration/audit.html) for details about common audit configurations that apply to all backends. You can configure an optional secondary output for the audit logs. This is useful for cases where you want to aggregate Cerbos audit logs into an existing silo for compliance and monitoring purposes while still taking advantage of the powerful features provided by Cerbos Hub to view and analyze log entries. The following snippet demonstrates how to configure a secondary output that writes audit entries to standard output of the container. ```yaml audit: enabled: true backend: hub hub: storagePath: "..." # Local storage path for buffering the audit logs. pipeOutput: # Secondary output configuration enabled: true backend: file # Any supported audit backend can be used here. file: # Configuration for the file backend which is used by the pipeOutput configuration above. path: stdout ``` | | To quickly try out the Cerbos Hub audit logs feature, you can use the following command. mkdir -p /tmp/cerbos && docker run --rm --name cerbos \\ \-p 3592:3592 -p 3593:3593 \\ \-e CERBOS\_HUB\_CLIENT\_ID="..." \\ \-e CERBOS\_HUB\_CLIENT\_SECRET="..." \\ \-v /tmp/cerbos:/audit\_logs \\ ghcr.io/cerbos/cerbos:latest server \\ \--set=audit.enabled=true \\ \--set=audit.backend=hub \\ \--set=audit.hub.storagePath=/audit\_logs | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#%5Faccessing%5Fthe%5Flogs)Accessing the logs Once collection is enabled, the logs can accessed via the _Audit logs_ tab in Cerbos Hub. From here you can select which logs to view: ### [](#%5Fdecision%5Flogs)Decision logs The decision logs are records of the authorization decisions made by the Cerbos PDP. These logs provide detailed information about each decision, including the inputs, Cerbos policies evaluated, and the `ALLOW/DENY` decisions along with any outputs from rule evaluation. Besides providing a comprehensive audit trail, these records can be used for troubleshooting purposes and to understand how Cerbos policies are used within your organization. The log list displays each entry with the timestamp, call ID, request kind (`CheckResources` or `PlanResources`), principal ID, resource kind, resource ID, and action decision. Toggle between the **Decision** view for a compact summary or **JSON** view for raw log data. #### [](#%5Flog%5Fentry%5Fdetails)Log entry details Click on any log entry to open the details panel, which shows: * **Principal**: The user or service that made the request, including their ID, roles, effective derived roles, and any attributes. * **Resource**: The resource being accessed, including kind, ID, scope, and attributes. * **Decisions**: The authorization outcome for each action, showing the effect (`ALLOW` or `DENY`) and which policy produced the decision. * **Outputs**: Any output values returned by policy rules. * **Raw JSON**: The complete log entry in JSON format. #### [](#%5Fdeep%5Flinking%5Fto%5Fpolicies)Deep linking to policies Policy names in audit log entries are clickable links. Click any policy name in the Decisions section to navigate directly to that policy in your policy store. This makes it easy to investigate authorization decisions and understand the policy logic behind each outcome without manually searching for the relevant policy file. #### [](#%5Ffiltering)Filtering Filter the logs by time range, PDP ID, policy source, principal, resource kind, action, or policy decision to find specific entries. ### [](#%5Faccess%5Flogs)Access logs ![Access logs](_images/audit_log_access.png) The access logs are records of all the API requests received by the Cerbos PDP. The valid `CheckResources` and `PlanResources` API calls would have a corresponding decision log entry with the the same call ID. API requests that are invalid or unauthenticated are logged as well and can be used for identifying misconfigured clients or unauthorized access attempts. You can filter the logs by a particular PDP and a time range. ## [](#%5Fexporting%5Flogs)Exporting logs Decision and access logs can be exported from Cerbos Hub as files, for long-term archiving or for analysis in your own tooling. Exports are encrypted before they leave Cerbos Hub, so only you can read them. | | Exporting requires the Owner role in the workspace or the organization. See [User management](user-management.html) for the full permissions matrix. Exports are not available for [on-premises deployments](on-premises.html), where the audit log data already resides in your own infrastructure. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ### [](#%5Fstarting%5Fan%5Fexport)Starting an export Apply the filters and time range you want on the _Decision logs_ or _Access logs_ tab, then click **Export**. The export dialog shows the filters that will be applied, so you can confirm you are exporting the entries you were looking at, and lets you adjust the time range before confirming. If you have not selected a time range, the export defaults to the last 24 hours. Exports are produced in the background. Click **Add to export queue** to start one, then follow its progress on the _Exports_ tab. You do not need to keep the page open. | | Cerbos Hub limits how many exports can be started from a workspace within an hour. When the limit is reached, starting another export fails and nothing is added to the queue. An export stops counting towards the limit an hour after it was created. | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ### [](#%5Fencryption%5Fkeys)Encryption keys Every export is encrypted with [age](https://age-encryption.org) before it is written to storage. You need the corresponding secret key to read the file afterwards. * **If you already have an age key pair**, paste the public portion (it begins with `age1`) into the **Public encryption key** field when starting the export. * **If you leave the field blank**, Cerbos Hub generates a key pair for you and displays it once, immediately after the export is queued. Copy both parts, or click **Download key pair** to save them as an `age.key` file in the format the `age` tools expect. | | Cerbos Hub does not retain the secret key, and it cannot be recovered or reissued. If you lose it, the export cannot be decrypted and you will need to run a new one. Store it somewhere safe, such as your organization’s secret manager. | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ### [](#%5Fthe%5Fexports%5Ftab)The Exports tab The _Exports_ tab lists every export requested in the workspace, showing the log type, who requested it, when it was created, when it expires, the public key it was encrypted for, and its status: * **In progress**: the export has been queued or is being generated. The list refreshes automatically while any export is in this state. * **Ready**: the export is complete. Click **Download** to retrieve the file. * **Failed**: the export could not be completed. Click **Retry** to reopen the export dialog with the same filters and time range already applied. * **Expired**: the file is past its retention period and is no longer available. Run a new export to obtain the same data. Expand a row to see the time range and filters the export was created with, the public key used to encrypt it, and the exact command needed to decrypt it. ### [](#%5Fdownloading%5Fand%5Fdecrypting)Downloading and decrypting The downloaded file is age-encrypted and gzip-compressed, and contains the exported entries as JSON Lines, one JSON object per log entry. To decrypt and read it, save your secret key as `age.key` and run: ```sh age -d -i age.key | gunzip ``` Because the output is JSON Lines, it can be piped directly into tools such as `jq`, or loaded into a data warehouse or SIEM without further conversion. ### [](#%5Fretention)Retention Completed exports remain available to download for seven days from the time they were generated. After that, the file is deleted and the export is marked as expired. Expiry applies only to the exported file; the underlying audit log data is unaffected and continues to be retained according to your plan, so the export can be re-created as long as the entries are still within that retention period. ## [](#%5Fmasking%5Fsensitive%5Ffields)Masking sensitive fields You can define masks to filter out sensitive or personally identifiable information (PII) that might be included in the audit log entries. Masked fields are removed locally at the PDP and are never transmitted to Cerbos Hub. Masks are defined using a subset of JSONPath syntax. ```yaml hub: credentials: pdpID: "..." # Optional. Identifier for this Cerbos instance. clientID: "..." # ClientID clientSecret: "..." # ClientSecret audit: enabled: true backend: hub hub: storagePath: "..." # Local storage path for buffering the audit logs. mask: # Fields to mask from CheckResources requests checkResources: - inputs[*].principal.attr.foo - inputs[*].auxData - outputs # Fields to mask from the metadata metadata: - authorization # Fields to mask from the peer information peer: - address - forwarded_for # Fields to mask from the PlanResources requests. planResources: - input.principal.attr.nestedMap.foo ``` | | Use the [local audit backend](../cerbos/latest/configuration/audit.html#local) along with [cerbosctl audit commands](../cerbos/latest/cli/cerbosctl.html#audit) to inspect your audit logs locally and determine the paths that need to be masked. | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | | Masks are applied before each entry is written to the local buffer on disk, so masked fields are neither stored locally nor transmitted to Cerbos Hub. To drop whole metadata keys such as authentication tokens, rather than mask specific field paths, use the top-level includeMetadataKeys and excludeMetadataKeys settings. Refer to [Cerbos audit configuration](../cerbos/latest/configuration/audit.html) for more details. | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#%5Fdata%5Fstorage)Data storage For Cerbos Hub cloud, audit logs are stored in Cerbos-managed infrastructure in the Netherlands (GCP `europe-west6`). Logs are retained according to your plan’s retention policy. For [on-premises deployments](on-premises.html), audit logs are stored in a ClickHouse instance running in your own infrastructure. You control data residency, retention, and access. Audit log data can be integrated with SIEM and UEBA systems by configuring the [secondary pipe output](#%5Fenabling%5Fcollection) to forward log entries to your existing log aggregation infrastructure. ## [](#%5Fadvanced%5Fconfiguration)Advanced configuration Advanced users can tune the local log retention period and other buffering settings. We generally do not recommend changing the default values unless absolutely necessary. ```yaml audit: enabled: true backend: hub hub: storagePath: "..." # Local storage path for buffering the audit logs. retentionPeriod: 168h # How long to keep buffered records on disk. advanced: bufferSize: 16 # Size of the memory buffer. Increasing this will use more memory and the chances of losing data during a crash. maxBatchSize: 16 # Write batch size. If your records are small, increasing this will reduce disk IO. flushInterval: 30s # Time to keep records in memory before committing. gcInterval: 15m # How often the garbage collector runs to remove old entries from the log. ``` Concepts ==================== Client credentials Used to establish an authenticated connection to Cerbos Hub using a client ID and a secret. Client credentials are scoped to either a deployment or a policy store, and can be read-only or read and write. They can be created from the **Client credentials** tab on any deployment or policy store detail page. Deployment A deployment is a specific configuration of policy stores (such as ‘production’ or ‘staging’) that can be connected to a set of PDPs. Each new change to the underlying store(s) results in a new policy build that’s automatically delivered to the PDPs if the tests are successful. Organization An Organization serves as the top-level entity in Cerbos Hub and provides centralized control over billing, access control, and Workspace management. Typically a business would have one Organization and a number of Workspaces underneath it. Policy bundle An encrypted file containing optimized binary representations of policies. When policies change in a policy store, Cerbos Hub validates the policies, runs any tests, and produces a policy bundle that is pushed to all connected PDPs assigned to that deployment. Bundles are versioned, allowing you to track exactly which policies were in effect at any point in time. Policy Decision Point (PDP) A stateless component that evaluates authorization requests against policies. The PDP does not call external services or fetch data on its own — the calling application must provide all relevant context (principal attributes, resource attributes, and any auxiliary data such as JWT tokens) in each request. Cerbos Hub supports two types of PDPs: Service PDP The open source Cerbos PDP instances that you run in your own infrastructure. Cerbos Hub pushes pre-compiled policy bundles to service PDPs, ensuring all your data remains within your network perimeter while reducing the overhead of policy updates. See [Service PDPs](decision-points.html). Embedded PDP A WebAssembly module that executes authorization logic locally within an application, without network calls to a Cerbos server. Embedded PDPs are suited for browser applications, edge functions, mobile apps, and other environments where latency or connectivity constraints make server-side authorization impractical. Each deployment can define multiple ePDP rules with independent policy filtering, authentication requirements, and IP allowlists. See [Embedded PDPs](deployments-epdp-rules.html). Policy playground A browser-based policy editor to quickly prototype, test, and collaborate on Cerbos policies. Features include execution traces for debugging, a permissions matrix view for visualizing access patterns, and real-time collaborative editing. You can create playgrounds from scratch, from templates, or from existing policy stores. See [Playgrounds](playground.html). Policy store A versioned, cloud-based storage container for Cerbos policies. A policy store can be either linked to a supported git provider for automatic mirroring or managed manually using the Cerbos Hub user interface, Cerbos Hub SDKs or the [cerbosctl](../cerbos/latest/cli/cerbosctl.html) utility. Multiple stores can be connected to a single deployment, making it easy to manage policies by teams or tenants or any other desired level of organization and combine them all at deployment time to distribute to PDPs. Synapse A data enrichment and integration layer that sits between callers and the PDP. It assembles the identity, resource, and relationship attributes that policies require, fetching them from external systems at decision time. It can also expose authorization endpoints for infrastructure systems such as API gateways and data platforms through configurable route extensions. See the [Synapse documentation](../synapse/latest/index.html). Workspace A Workspace encompasses a set of users, policy stores, deployments, and playgrounds to help organize your work by teams, departments, tenants, or any other desired form of separating responsibilities. ePDP rule A configuration within a deployment that defines how embedded PDP bundles are built and served. Each rule specifies policy filtering criteria (resources, actions, scopes, roles, versions), authentication requirements, and IP allowlists. A deployment can have multiple ePDP rules for different clients or use cases. See [ePDP rules](deployments-epdp-rules.html). ## [](#%5Fmulti%5Ftenancy%5Fpatterns)Multi-tenancy patterns Cerbos Hub supports several approaches for managing authorization across multiple tenants. ### [](#%5Fscoped%5Fpolicies)Scoped policies Use [scoped policies](../cerbos/latest/policies/scoped%5Fpolicies.html) to define tenant-specific authorization rules within a single policy store. Each tenant is assigned a scope (such as `tenant-acme`, `tenant-globex`), and policies can override or extend base rules at any level in the scope hierarchy. For service PDPs, the requesting application passes the tenant’s scope in each authorization check. For embedded PDPs, [dynamic scope filtering](deployments-epdp-rules.html#%5Fdynamic%5Fscopes) delivers only the policies relevant to a specific tenant, reducing bundle size and preventing cross-tenant policy exposure. ### [](#%5Fmultiple%5Fpolicy%5Fstores)Multiple policy stores For organizations that need stronger isolation between tenants, create a separate [policy store](policy-stores.html) per tenant or tenant group. Each store is independently versioned and can be managed by different teams. Stores are combined in a [deployment](deployments.html), which builds a unified policy bundle and pushes it to all connected PDPs. ### [](#%5Fcicd%5Fpipeline)CI/CD pipeline Changes to policies follow a git-based workflow: 1. A policy change is pushed to the connected git repository, or uploaded via the Hub API or SDKs 2. Cerbos Hub detects the change, validates policy syntax, and runs all tests 3. If tests pass, a new policy bundle is built and pushed to all connected PDPs within seconds 4. If tests fail, the build is blocked and the previous bundle remains active For dynamic policy management — such as operators changing tenant policies at runtime — use the Cerbos Hub SDKs or the [cerbosctl](../cerbos/latest/cli/cerbosctl.html) CLI to programmatically update policy stores without requiring direct git access. Service Policy Decision Points ==================== Service policy decision points (PDPs) are open source Cerbos instances running in your infrastructure, managed by Cerbos Hub. Cerbos Hub acts as a control plane for your PDP fleet, handling policy compilation, testing, and distribution so your PDPs can focus on serving authorization requests. ### [](#%5Fwhy%5Fuse%5Fcerbos%5Fhub%5Ffor%5Fpdps)Why use Cerbos Hub for PDPs? Without Cerbos Hub, each PDP must independently detect policy changes, fetch source files, parse, compile, and load them. This creates operational overhead and can lead to version drift across your fleet. With Cerbos Hub: | Pre-compiled bundles | Policies are compiled once by Cerbos Hub and pushed as optimized bundles. PDPs skip parsing and compilation entirely, reducing startup time and memory usage. | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Instant fleet-wide updates | Push-based distribution means all PDPs receive updates within seconds, with no polling delays or staggered rollouts. | | Validated before deployment | Policies are tested automatically before distribution. Failed tests block deployment, preventing broken policies from reaching production. | | Centralized visibility | Monitor all connected PDPs from a single dashboard. See which version each instance is running, when it last connected, and access its audit logs. | | Built-in resilience | PDPs cache bundles locally and continue operating if the Cerbos Hub API is temporarily unavailable. New instances can start immediately using bundles served from a CDN, independent of the API. | For environments where running a Cerbos server is impractical, such as browser applications or edge functions, see [Embedded PDPs](deployments-epdp-rules.html). ## [](#%5Fwhen%5Fto%5Fuse%5Fservice%5Fpdps)When to use service PDPs Service PDPs are the standard deployment model for server-side authorization. Use them when: ### [](#%5Fbackend%5Fservices)Backend services API servers, microservices, and backend applications that need to authorize requests. The PDP runs alongside your application and responds to authorization checks with sub-millisecond latency. ### [](#%5Fcentralized%5Fauthorization)Centralized authorization Organizations that want a dedicated authorization service that multiple applications can query. Deploy one or more PDPs and have all services send authorization requests to them. ### [](#%5Fhigh%5Fvolume%5Fworkloads)High-volume workloads Service PDPs handle thousands of authorization checks per second. The compiled policy format and in-memory evaluation ensure consistent performance under load. ### [](#%5Fdata%5Fsensitive%5Fenvironments)Data-sensitive environments All authorization data stays within your network. Cerbos Hub only pushes policy bundles; your request data, principal attributes, and resource attributes never leave your infrastructure. ## [](#%5Fhow%5Fit%5Fworks)How it works When a PDP connects to Cerbos Hub, it establishes a two-way communication channel: 1. The PDP authenticates using client credentials scoped to a deployment 2. Cerbos Hub pushes the current policy bundle to the PDP 3. The PDP loads the bundle and begins serving authorization requests 4. When policies change, Cerbos Hub pushes a notification to all connected PDPs 5. Each PDP downloads and activates the new bundle Because there is no polling, all PDPs converge on the same policy version within seconds of a change. ## [](#%5Fdeployment%5Fpatterns)Deployment patterns Cerbos supports multiple deployment patterns depending on your requirements. ### [](#%5Fservice%5Fmodel)Service model A central Cerbos deployment shared by multiple applications. * Cerbos can be upgraded independently from applications * Simpler policy management with a single point of control * Requires capacity planning to avoid bottlenecks ### [](#%5Fsidecar%5Fmodel)Sidecar model Each application instance gets its own Cerbos container. * High performance with no network overhead between application and PDP * Upgrades require rolling updates to all application instances * Policy updates propagate to each sidecar independently Cerbos supports serving the API over a Unix domain socket, allowing secure communication with no network overhead. See [Kubernetes sidecar deployment](../cerbos/latest/deployment/k8s-sidecar.html) for details. ### [](#%5Fdaemonset%5Fmodel)DaemonSet model Each Kubernetes node gets one Cerbos instance, shared by all pods on that node. * Efficient resource usage compared to sidecars * Lower latency than centralized service * Policy updates roll out node by node See [Kubernetes DaemonSet deployment](../cerbos/latest/deployment/k8s-daemonset.html) for configuration details. ## [](#%5Fdeploying%5Fa%5Fpdp)Deploying a PDP A PDP requires three pieces of configuration to connect to Cerbos Hub: | Deployment ID | Identifies which deployment’s policies to load | | ------------- | ---------------------------------------------- | | Client ID | Authentication credential | | Client secret | Authentication credential | These can be configured via environment variables or a configuration file. ### [](#%5Fenvironment%5Fvariables)Environment variables The simplest method to get a connected PDP running: ```shell docker run --rm --name cerbos \ -p 3592:3592 -p 3593:3593 \ -e CERBOS_HUB_DEPLOYMENT_ID="..." \ -e CERBOS_HUB_CLIENT_ID="..." \ -e CERBOS_HUB_CLIENT_SECRET="..." \ -e CERBOS_HUB_PDP_ID="..." \ ghcr.io/cerbos/cerbos:latest server ``` | CERBOS\_HUB\_DEPLOYMENT\_ID | The deployment ID to load policies from. Find this on the deployment page in Cerbos Hub. | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | CERBOS\_HUB\_CLIENT\_ID | Client ID from the deployment’s **Client credentials** tab. | | CERBOS\_HUB\_CLIENT\_SECRET | Client secret from the deployment’s **Client credentials** tab. | | CERBOS\_HUB\_PDP\_ID | Optional. A name for this PDP instance, shown in the Cerbos Hub monitoring page. If not provided, a random value is used. | ### [](#%5Fconfiguration%5Ffile)Configuration file For production deployments, use a configuration file: ```yaml server: httpListenAddr: ":3592" grpcListenAddr: ":3593" hub: credentials: pdpID: "payments-service-1" # Optional. Identifier for this PDP instance. clientID: "..." clientSecret: "..." storage: driver: hub hub: remote: deploymentID: "..." # The deployment ID to load policies from ``` Start the PDP with the configuration file: ```shell docker run --rm --name cerbos \ -v $(pwd):/conf \ -p 3592:3592 -p 3593:3593 \ ghcr.io/cerbos/cerbos:latest server --config=/conf/.cerbos.yaml ``` See [Configuration](../cerbos/latest/configuration/index.html) for the full configuration reference. ### [](#%5Fobtaining%5Fcredentials)Obtaining credentials To generate client credentials: 1. Navigate to the deployment in Cerbos Hub 2. Select the **Client credentials** tab 3. Click **Generate a client credential** 4. Choose **Read only** for PDPs that only need to receive bundles 5. Save the client secret; it cannot be recovered after creation ## [](#%5Fproduction%5Fdeployment)Production deployment Cerbos runs anywhere containers run: Kubernetes, Docker Compose, Amazon ECS, systemd, or directly as a binary. The examples below focus on Kubernetes as the most common deployment target. For other platforms, see [Cloud platform deployment](../cerbos/latest/deployment/cloud-platforms.html) (AWS Marketplace, Fly.io) and [systemd deployment](../cerbos/latest/deployment/systemd.html). ### [](#%5Fkubernetes)Kubernetes #### [](#%5Fhelm)Helm Create a secret for the Hub credentials: ```shell kubectl create secret generic cerbos-hub-credentials \ --from-literal=CERBOS_HUB_CLIENT_ID="..." \ --from-literal=CERBOS_HUB_CLIENT_SECRET="..." \ --from-literal=CERBOS_HUB_DEPLOYMENT_ID="..." ``` Create a values file (`hub-values.yaml`): ```yaml cerbos: config: audit: enabled: true backend: hub hub: storagePath: /audit_logs envFrom: - secretRef: name: cerbos-hub-credentials volumes: - name: cerbos-audit-logs emptyDir: {} volumeMounts: - name: cerbos-audit-logs mountPath: /audit_logs ``` Deploy with Helm: ```shell helm repo add cerbos https://download.cerbos.dev/helm-charts helm install cerbos cerbos/cerbos --values=hub-values.yaml ``` | | For AWS Marketplace (EKS/ECS), the Helm chart handles metering automatically. See [Cloud platform deployment](../cerbos/latest/deployment/cloud-platforms.html) for setup instructions. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ## [](#%5Freliability)Reliability Cerbos Hub is designed for high availability. However, if the Cerbos Hub API becomes temporarily unavailable: * **Running PDPs** continue serving requests using the last downloaded bundle while attempting to reconnect in the background * **New PDPs** can start immediately because the last successfully built bundle for each deployment is served from a CDN, independent of the Cerbos Hub API. The bundle is encrypted with your client credentials, so only your PDPs can decrypt it. Once the PDP has loaded the bundle and is serving requests, it establishes a connection to the Cerbos Hub API in the background to receive push notifications about new builds. ### [](#%5Flocal%5Fbundle%5Fcaching)Local bundle caching For additional resilience, configure a cache directory to persist bundles to disk: ```yaml storage: driver: hub hub: remote: deploymentID: "..." cacheDir: /var/cerbos/hub # Directory to cache downloaded bundles ``` Mount a persistent volume at this path when running in containers or Kubernetes. Caching avoids re-downloading an unchanged bundle on restart. It does not enable an offline start: a PDP still contacts Cerbos Hub at startup to fetch its bundle, and fails to start if Hub cannot be reached. See [Reliability](reliability.html) for network requirements and connectivity monitoring. ### [](#%5Ffallback%5Fto%5Fgit)Fallback to git As a last resort, switch the PDP to use the git storage driver and read policies directly from your policy repository: ```yaml storage: driver: git git: protocol: https url: https://github.com/your-org/policies.git branch: main checkoutDir: /tmp/cerbos/policies updatePollInterval: 60s ``` ## [](#%5Fhealth%5Fchecks)Health checks Cerbos exposes health check endpoints for container orchestrators and load balancers. ### [](#%5Fhttp%5Fhealth%5Fendpoint)HTTP health endpoint ```none GET /_cerbos/health ``` Returns HTTP 200 when the PDP is healthy and ready to serve requests. ### [](#%5Fgrpc%5Fhealth)gRPC health Cerbos implements the standard gRPC health checking protocol on port 3593. ### [](#%5Fkubernetes%5Fprobes)Kubernetes probes Configure liveness and readiness probes: ```yaml livenessProbe: httpGet: path: /_cerbos/health port: 3592 initialDelaySeconds: 5 periodSeconds: 10 readinessProbe: httpGet: path: /_cerbos/health port: 3592 initialDelaySeconds: 5 periodSeconds: 10 ``` ## [](#%5Fmonitoring)Monitoring ### [](#%5Fconnected%5Fpdps)Connected PDPs The **Decision points** tab on each deployment page shows all recently connected PDP instances: * **PDP ID**: The identifier for the instance * **Build**: Which policy bundle version is active * **Sessions**: Number of active connections * **Cerbos version**: The PDP software version * **Last seen**: When the PDP last communicated with Cerbos Hub * **Audit logs**: Link to view audit logs from this PDP ### [](#%5Fprometheus%5Fmetrics)Prometheus metrics Cerbos exposes metrics at `/_cerbos/metrics` in Prometheus format. Monitor PDP connectivity using the `cerbos_dev_hub_connected` gauge: | 1 | PDP is connected to Cerbos Hub | | - | ----------------------------------------- | | 0 | PDP is disconnected (using cached bundle) | Additional metrics for Hub connectivity: | cerbos\_dev\_store\_bundle\_updates\_count | Number of bundle updates received from Cerbos Hub | | ------------------------------------------ | ------------------------------------------------- | | cerbos\_dev\_store\_bundle\_op\_latency | Time to perform bundle operations | See [Observability](../cerbos/latest/configuration/observability.html) for the full list of available metrics. ## [](#%5Frelated%5Ffeatures)Related features | [Audit log collection](audit-log-collection.html) | Send authorization decision logs from PDPs to Cerbos Hub for analysis and compliance. | | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | [Deployments](deployments.html) | Configure which policy stores contribute to a deployment and view build history. | | [Policy stores](policy-stores.html) | Manage the policy repositories that feed into deployments. | | [Cerbos deployment patterns](../cerbos/latest/deployment/index.html) | Detailed guides for service, sidecar, and DaemonSet deployment models. | Embedded Policy Decision Points ==================== Embedded policy decision points (ePDPs) execute authorization logic locally using WebAssembly, without requiring network calls to a Cerbos PDP. Policy bundles are downloaded from Cerbos Hub and evaluated in-process by a WASM runtime. For server-side authorization using the open source Cerbos PDP, see [Service PDPs](decision-points.html). | | The WASM file from @cerbos/embedded-server contains only the policy evaluation engine — it does **not** contain your policies. Policies are fetched separately from Cerbos Hub at runtime by @cerbos/embedded-client and held in memory. You only need to update the WASM file when you upgrade the @cerbos/embedded-server package version. Policy updates happen automatically in the background (default: every 60 seconds) with no server restart required. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ## [](#%5Fwhen%5Fto%5Fuse%5Fembedded%5Fpdps)When to use embedded PDPs ePDPs address scenarios where server-side authorization is impractical due to latency, connectivity, or architectural constraints. ### [](#%5Fbrowser%5Fapplications)Browser applications Web applications often need to make authorization decisions to determine which UI elements to render. A document editor might show or hide the "Delete" button based on whether the current user has permission to delete the document. Making a network request for each visibility check introduces latency and creates a dependency on backend availability. With an ePDP, the browser downloads a policy bundle once and evaluates all permission checks locally. The bundle updates automatically when policies change, and the application continues to function if the network is temporarily unavailable. ### [](#%5Fedge%5Fcomputing)Edge computing Edge functions and CDN workers operate in environments where round-trips to origin servers are expensive. An edge function that needs to authorize a request before proxying it to the backend can evaluate policies locally, reducing total request latency. ### [](#%5Fmobile%5Fapplications)Mobile applications Mobile applications operate with intermittent connectivity. An ePDP allows the application to make authorization decisions while offline, using the most recently downloaded policy bundle. When connectivity is restored, the bundle updates automatically. React Native applications can use the JavaScript SDK today. Native iOS and Android SDK support is planned. ### [](#%5Fserverless%5Ffunctions)Serverless functions Serverless functions incur cold start latency when loading external dependencies. An ePDP embedded in the function bundle eliminates the need for network calls to an authorization service during function execution. ### [](#%5Fserver%5Fside%5Fapplications%5Fnext%5Fjs%5Fexpress%5Fetc)Server-side applications (Next.js, Express, etc.) The embedded PDP is designed for client-side authorization, such as determining which UI elements to show or hide based on permissions. For server-side authorization — API routes, server actions, middleware — use a [service PDP](decision-points.html) with the [@cerbos/grpc](https://www.npmjs.com/package/@cerbos/grpc) or [@cerbos/http](https://www.npmjs.com/package/@cerbos/http) package instead. Service PDPs run in a trusted environment and provide features not available in ePDPs, including audit logging to Cerbos Hub. Use the ePDP in the browser for presentational checks and a service PDP on the server for enforcement. ## [](#%5Fepdp%5Frules)ePDP rules An ePDP rule defines: * Which subset of policies to include in the bundle * Under what conditions the bundle can be accessed * What security controls apply to bundle downloads Each deployment can have multiple ePDP rules. This allows different bundle configurations for different clients or environments. For example: * A rule serving a minimal bundle for browser clients, containing only UI-relevant policies * A rule serving a full bundle for edge workers, containing all policies * A rule per tenant in a multi-tenant application, each filtered to that tenant’s scope ### [](#%5Fcreating%5Fa%5Frule)Creating a rule To create an ePDP rule: 1. Navigate to the [deployment](deployments.html) in Cerbos Hub 2. Select the **Embedded PDP rules** tab 3. Click **Create rule** 4. Enter a unique name for the rule 5. Configure access control and policy bundle filtering options 6. Click **Save rule** After saving, the rule appears in the list with its rule ID displayed. Click **Copy** next to the rule ID to copy it for use in your client application. ### [](#%5Fenabling%5Fand%5Fdisabling%5Frules)Enabling and disabling rules Each rule has an enabled/disabled toggle. Disabled rules cannot serve bundles. Requests for a disabled rule’s bundle return an error. To disable a rule, toggle the switch on the rule card. A confirmation dialog appears before the rule is disabled. Use this to: * Temporarily suspend access to a bundle without deleting the rule configuration * Prepare a rule configuration before making it available to clients * Revoke access in response to a security concern ### [](#%5Fmanaging%5Frules)Managing rules Each rule card provides actions: | Edit | Modify the rule configuration. | | --------- | -------------------------------------------------------------------------------------------------- | | Duplicate | Create a new rule with the same configuration. Useful for creating variations of an existing rule. | | Delete | Permanently remove the rule. A confirmation dialog appears before deletion. | ## [](#%5Ffiltering%5Fpolicies)Filtering policies By default, an ePDP bundle contains all policies from the deployment. Filters reduce the bundle to only the policies required by the client application. Filtering serves two purposes: | Performance | Smaller bundles download faster and consume less memory. A browser application checking permissions on three resource types does not need policies for the fifty other resource types in the system. | | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Security | Bundles may be served to environments you do not fully control, such as end-user browsers. Filtering prevents exposure of server-side authorization logic that clients do not need. | Filters are applied when the bundle is requested. The source policies in the deployment are not modified. ### [](#%5Fresources%5Fand%5Factions)Resources and actions The **Resources and actions** setting specifies which authorization rules to include based on the resource types and actions they govern. If no filter entries are defined, all resources and actions are included. Click **Add filter entry** to create a filter. Each entry can specify: * Resources only: Include all rules for the specified resource types, regardless of action * Actions only: Include all rules for the specified actions, regardless of resource type * Both: Include only rules matching both the specified resources and actions Multiple filter entries can be defined. The resulting bundle includes rules matching any of the entries. Example: Browser application A browser application checks three permissions: * `document:view` * `document:edit` * `folder:view` Configure two filter entries: 1. Resources: `document`, Actions: `view`, `edit` 2. Resources: `folder`, Actions: `view` The bundle includes only the rules necessary for these checks. Rules governing `document:delete`, `folder:create`, or other resource types are excluded. Example: Action-based filter An application checks the `view` action across many resource types but never checks `delete` or `admin` actions. Configure one filter entry: * Actions: `view` The bundle includes `view` rules for all resource types, excluding rules for other actions. ### [](#%5Fscopes)Scopes Scope filtering supports multi-tenant architectures and hierarchical permission structures. Cerbos policies can be scoped to specific contexts. A policy with scope `acme.eu` applies only when requests specify that scope or a descendant scope like `acme.eu.prod`. Scope filtering determines which scoped policies appear in the bundle. The **Scopes** setting offers three options: | All | Include policies for all scopes. No filtering is applied. | | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | | Specific | Include policies matching specified scope patterns. Configure this in the rule definition. | | Require specific scope at fetch time | Require clients to specify scopes when downloading the bundle. The bundle is filtered dynamically based on the requested scopes. | #### [](#%5Fstatic%5Fscopes%5Fspecific)Static scopes (Specific) Select **Specific** and enter scope patterns in the rule definition. The bundle includes policies matching those patterns. Use static scopes when: * The set of scopes is small and known in advance * All clients using this rule need the same scopes * Scopes do not change frequently Example A rule serving bundles for European customers specifies scope pattern `acme.eu`. The bundle includes: * Policies with scope `acme.eu` * Policies with scope `acme` (ancestor) * Policies with no scope (root) Policies scoped to `acme.us` or `acme.ap` are excluded. #### [](#%5Fdynamic%5Fscopes)Dynamic scopes Select **Require specific scope at fetch time** to require clients to specify scopes when downloading the bundle. The bundle is filtered to the requested scopes on each request. | | If the rule has scope patterns configured, the scopes requested by clients must match one of those patterns. Requests for scopes outside the allowed patterns are rejected. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | Use dynamic scopes when: * The set of scopes is large, such as one scope per customer tenant * Scopes change frequently as tenants are added or removed * Different clients need different scopes from the same rule Example A multi-tenant application has thousands of customer tenants, each with a unique scope like `tenant-a`, `tenant-b`, etc. Rather than creating thousands of ePDP rules, create one rule with dynamic scopes enabled. When client code initializes: ```typescript const cerbos = new Embedded({ policies: { ruleId: "", scopes: [currentTenant.scopeId], }, wasm, }); ``` The bundle returned contains only policies applicable to that tenant’s scope. #### [](#%5Fscope%5Fhierarchy)Scope hierarchy When a scope is requested, ancestor scopes are included automatically. Requesting scope `acme.eu.prod` includes policies for: * `acme.eu.prod` (exact match) * `acme.eu` (parent) * `acme` (grandparent) * Root scope (no scope specified) This ensures that inherited policies from parent scopes are available for evaluation. ### [](#%5Froles)Roles The **Roles** setting specifies which roles to include. Select **Specific** and enter role names to filter. Only rules referencing the specified roles are included in the bundle. If set to **All**, all roles are included. Example A browser application is used only by users with `viewer` or `editor` roles. Administrative actions are performed through a separate interface. Configure role filter: `viewer`, `editor` The bundle excludes rules that apply only to `admin` or other roles, reducing bundle size and avoiding exposure of administrative authorization logic. ### [](#%5Fversions)Versions The **Versions** setting specifies which policy versions to include. Select **Specific** and enter version identifiers to filter. Cerbos policies support versioning, allowing different rule sets for different API versions or feature flags. If your policies use versions, you can filter the bundle to include only the versions relevant to the client. If set to **All**, all versions are included. Example An application has migrated from policy version `v1` to `v2`, but some legacy clients still use `v1`. * Rule for modern clients: version filter `v2` * Rule for legacy clients: version filter `v1` ## [](#%5Fsecurity%5Fcontrols)Security controls ePDP bundles contain compiled authorization logic. Depending on your security requirements, this logic may need protection from unauthorized access. ### [](#%5Fthreat%5Fmodel)Threat model Consider what is exposed if an ePDP bundle is obtained by an unauthorized party: * **Policy structure**: Which resource types and actions exist in the system * **Authorization rules**: The conditions under which access is granted or denied * **Role definitions**: Which roles exist and what permissions they grant * **Attribute checks**: Which principal and resource attributes influence decisions For some applications, this information is not sensitive. A publicly documented API with straightforward role-based access control may have no authorization logic worth protecting. For other applications, authorization rules are proprietary or security-sensitive. An attacker studying the rules might identify edge cases to exploit or gain insight into system architecture. Evaluate your requirements and apply controls accordingly. ### [](#%5Fauthentication)Authentication The **Authentication** setting controls whether credentials are required to download bundles. | Public access | Bundles are served without authentication. Any client with the rule ID can download the bundle. | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Client credential | Clients must provide valid credentials to download bundles. Credentials are a [client ID and client secret](concepts.html#%5Fclient%5Fcredentials), issued from the deployment’s **Client credentials** tab in Cerbos Hub. These are the same credentials used by [service PDPs](decision-points.html). | #### [](#%5Fbrowser%5Fapplications%5F2)Browser applications Client credentials must not be exposed to end users. Browser JavaScript is readable by users, and credentials embedded in client-side code are not secret. For browser applications requiring authenticated bundles: | Backend-for-frontend | Your server fetches the bundle using credentials and serves it to the browser. The browser never sees the credentials. | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | Server-side rendering | Evaluate permissions on the server and send only the results to the browser. The browser does not load the ePDP bundle at all. | | Accept public bundles | If the authorization logic is not sensitive, disable authentication and serve bundles directly to browsers. | #### [](#%5Fserver%5Fside%5Fapplications)Server-side applications Node.js servers, edge workers with secret storage, and other server-side environments can safely use credentials. Store credentials in environment variables or a secrets manager, not in source code. ### [](#%5Fip%5Fallowlist)IP allowlist The **IP allowlist** specifies CIDR ranges permitted to download bundles. Requests from IP addresses outside these ranges are rejected, regardless of authentication status. Both IPv4 and IPv6 ranges are supported. __Table 1\. Examples__ | CIDR | Description | | --------------- | ------------------------------------------------------- | | 203.0.113.0/24 | Single IPv4 /24 block | | 203.0.113.42/32 | Single IPv4 address | | 2001:db8::/32 | IPv6 block | | 0.0.0.0/0 | All IPv4 addresses (effectively disables the allowlist) | Use the IP allowlist to: * Limit bundle access to your corporate network * Restrict access to known CDN or edge provider IP ranges * Add defense-in-depth alongside authentication The IP allowlist applies to the bundle download request, not to subsequent policy evaluations. Once a client has downloaded a bundle, it can evaluate policies from any network location. ## [](#%5Fjavascript%5Fsdk)JavaScript SDK The [@cerbos/embedded-client](https://www.npmjs.com/package/@cerbos/embedded-client) package provides a client for loading and evaluating ePDP bundles in JavaScript environments. ### [](#%5Finstallation)Installation ```bash npm install @cerbos/embedded-client @cerbos/embedded-server ``` The `@cerbos/embedded-server` package contains the WebAssembly module that executes policy evaluation. This module is the decision engine only — it does not contain your policies. Policies are fetched separately from Cerbos Hub at runtime by `@cerbos/embedded-client` and stored in memory. When policies change in Cerbos Hub, updated bundles are downloaded automatically without requiring a server restart or redeployment. ### [](#%5Finitializing%5Fthe%5Fclient)Initializing the client The client requires two inputs: | policies | Source of the policy bundle, typically a rule ID pointing to Cerbos Hub | | -------- | ----------------------------------------------------------------------- | | wasm | Source of the WebAssembly module | ```typescript import { Embedded } from "@cerbos/embedded-client"; import wasm from "@cerbos/embedded-server/server.wasm?init"; const cerbos = new Embedded({ policies: { ruleId: "" }, wasm, }); ``` Replace `` with the rule ID from the Embedded PDP rules tab in Cerbos Hub. The client begins downloading the policy bundle immediately upon construction. The first authorization check waits for the download to complete. ### [](#%5Fchecking%5Fpermissions)Checking permissions The client provides several methods for authorization checks. #### [](#%5Fisallowed)isAllowed Check if a single action is allowed: ```typescript const allowed = await cerbos.isAllowed({ principal: { id: "user-123", roles: ["editor"], attr: { department: "engineering", clearanceLevel: 3, }, }, resource: { kind: "document", id: "doc-456", attr: { owner: "user-789", confidential: false, }, }, action: "edit", }); if (allowed) { // Show edit button } ``` #### [](#%5Fcheckresource)checkResource Check multiple actions on a single resource: ```typescript const result = await cerbos.checkResource({ principal: { id: "user-123", roles: ["editor"], }, resource: { kind: "document", id: "doc-456", }, actions: ["view", "edit", "delete", "share"], }); const canView = result.isAllowed("view"); const canEdit = result.isAllowed("edit"); const canDelete = result.isAllowed("delete"); const canShare = result.isAllowed("share"); ``` #### [](#%5Fcheckresources)checkResources Check actions across multiple resources: ```typescript const result = await cerbos.checkResources({ principal: { id: "user-123", roles: ["editor"], }, resources: [ { resource: { kind: "document", id: "doc-1" }, actions: ["view", "edit"], }, { resource: { kind: "document", id: "doc-2" }, actions: ["view", "edit"], }, { resource: { kind: "folder", id: "folder-1" }, actions: ["view"], }, ], }); const canEditDoc1 = result.isAllowed({ resource: { kind: "document", id: "doc-1" }, action: "edit", }); ``` #### [](#%5Fplanresources)planResources When listing resources — for example, showing a user all documents they can view — use `planResources` instead of calling `isAllowed` or `checkResource` for each item individually. The query planner returns a set of conditions that can be applied to filter resources efficiently, avoiding a round-trip per item. Generate a query plan for filtering resources: ```typescript const plan = await cerbos.planResources({ principal: { id: "user-123", roles: ["editor"], }, resource: { kind: "document" }, action: "view", }); switch (plan.kind) { case "KIND_ALWAYS_ALLOWED": // User can view all documents break; case "KIND_ALWAYS_DENIED": // User cannot view any documents break; case "KIND_CONDITIONAL": // User can view documents matching plan.condition break; } ``` ### [](#%5Fauthenticated%5Faccess)Authenticated access When the rule uses **Client credential** authentication, provide credentials: ```typescript import { Embedded } from "@cerbos/embedded-client"; import wasm from "@cerbos/embedded-server/server.wasm?init"; const cerbos = new Embedded({ policies: { ruleId: "", credentials: { clientId: process.env.CERBOS_HUB_CLIENT_ID, clientSecret: process.env.CERBOS_HUB_CLIENT_SECRET, }, }, wasm, }); ``` The `credentialsFromEnv` helper reads credentials from standard environment variables: ```typescript import { Embedded } from "@cerbos/embedded-client"; import { credentialsFromEnv } from "@cerbos/hub"; import wasm from "@cerbos/embedded-server/server.wasm?init"; const cerbos = new Embedded({ policies: { ruleId: "", credentials: credentialsFromEnv(), }, wasm, }); ``` This reads `CERBOS_HUB_CLIENT_ID` and `CERBOS_HUB_CLIENT_SECRET` from `process.env`. ### [](#%5Fdynamic%5Fscopes%5F2)Dynamic scopes When the rule uses **Require specific scope at fetch time**, specify scopes when creating the client: ```typescript const cerbos = new Embedded({ policies: { ruleId: "", scopes: ["tenant-acme.department-eng", "tenant-acme.department-sales"], }, wasm, }); ``` The bundle returned includes only policies applicable to the specified scopes and their ancestors. In this example, the bundle includes policies for `tenant-acme.department-eng`, `tenant-acme.department-sales`, `tenant-acme` (shared ancestor), and the root scope. To change scopes, create a new client instance. Scopes cannot be changed on an existing client. ### [](#%5Fautomatic%5Fbundle%5Fupdates)Automatic bundle updates The SDK polls Cerbos Hub for bundle updates. When policies change and a new bundle is available, it is downloaded and activated automatically. No server restart or redeployment is required — the update happens transparently in the background. The default polling interval is 60 seconds. Configure a different interval: ```typescript const cerbos = new Embedded({ policies: { ruleId: "", interval: 300, // Poll every 5 minutes }, wasm, }); ``` The minimum interval is 10 seconds. Values below 10 are increased to 10. To disable automatic updates, set `interval: 0`. The client uses the bundle downloaded at initialization and never checks for updates. #### [](#%5Fupdate%5Fnotifications)Update notifications Register a callback to be notified when updates complete: ```typescript const cerbos = new Embedded({ policies: { ruleId: "", onUpdate: (error) => { if (error) { console.error("Policy update failed:", error); } else { console.info("Policies updated"); } }, }, wasm, }); ``` The callback receives an error object if the update failed, or `undefined` if it succeeded. Update failures are silent by default. The client continues using the previously loaded bundle. Use the `onUpdate` callback to log failures or trigger alerts. #### [](#%5Fdeferred%5Factivation)Deferred activation By default, downloaded updates are activated immediately. To control when updates take effect, use the `PolicyLoader` class directly: ```typescript import { Embedded, PolicyLoader } from "@cerbos/embedded-client"; import wasm from "@cerbos/embedded-server/server.wasm?init"; const loader = new PolicyLoader({ ruleId: "", activateOnLoad: false, onUpdate: (error) => { if (!error) { console.info("New policy bundle ready for activation"); } }, }); const cerbos = new Embedded({ policies: loader, wasm, }); // Later, when ready to apply the update: loader.activate(); ``` This is useful for: * Activating updates only on page navigation to avoid layout shifts * Batching updates with other application state changes * Implementing user-facing "Refresh policies" functionality #### [](#%5Fstopping%5Fupdates)Stopping updates To stop polling for updates: ```typescript loader.stop(); ``` Call this when the client is no longer needed, such as during application shutdown or when navigating away from a page. ### [](#%5Floading%5Fthe%5Fwebassembly%5Fmodule)Loading the WebAssembly module The WebAssembly module must be loaded and provided to the client. The loading mechanism depends on your build tooling and runtime environment. #### [](#%5Fvite)Vite Vite supports WebAssembly imports with the `?init` suffix: ```typescript import wasm from "@cerbos/embedded-server/server.wasm?init"; const cerbos = new Embedded({ policies: { ruleId: "" }, wasm, }); ``` #### [](#%5Fwebpack)Webpack Configure Webpack to handle `.wasm` files as assets, then import: ```typescript import wasmUrl from "@cerbos/embedded-server/server.wasm"; const cerbos = new Embedded({ policies: { ruleId: "" }, wasm: wasmUrl, }); ``` Webpack configuration varies by version. Consult the Webpack documentation for WebAssembly handling. #### [](#%5Frspack)Rspack Configure Rspack to handle `.wasm` files as `asset/resource`: ```typescript // rspack.config.ts export default defineConfig({ module: { rules: [ { test: /\.wasm$/, type: "asset/resource", }, ], }, }); ``` Then import and use the WASM URL: ```typescript import wasmUrl from "@cerbos/embedded-server/server.wasm"; const cerbos = new Embedded({ policies: { ruleId: "" }, wasm: wasmUrl, }); ``` #### [](#%5Fnext%5Fjs%5Fturbopack)Next.js (Turbopack) Next.js with Turbopack does not support direct `.wasm` imports. Copy the WASM binary to the `public/` directory and reference it by URL. Create a copy script at `scripts/copy-cerbos-wasm.mjs`: ```javascript import { copyFile, mkdir } from "node:fs/promises"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const source = fileURLToPath( import.meta.resolve("@cerbos/embedded-server/server.wasm"), ); const destination = join( process.cwd(), "public", "wasm", "cerbos-epdp-server.wasm", ); await mkdir(dirname(destination), { recursive: true }); await copyFile(source, destination); ``` Add `postinstall` and `prebuild` hooks to `package.json` to run the script automatically: ```json { "scripts": { "postinstall": "node scripts/copy-cerbos-wasm.mjs", "prebuild": "node scripts/copy-cerbos-wasm.mjs" } } ``` Then pass the public URL to the client: ```typescript import { Embedded } from "@cerbos/embedded-client"; const cerbos = new Embedded({ policies: { ruleId: "" }, wasm: "/wasm/cerbos-epdp-server.wasm", }); ``` The WASM file only changes when you upgrade the `@cerbos/embedded-server` package — it does not change when policies are updated. Re-run `npm install` (or your package manager’s install command) after upgrading to ensure the copied binary matches the installed version. #### [](#%5Fnode%5Fjs)Node.js Read the module from the filesystem: ```typescript import { readFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; import { Embedded } from "@cerbos/embedded-client"; const cerbos = new Embedded({ policies: { ruleId: "" }, wasm: readFile( fileURLToPath(import.meta.resolve("@cerbos/embedded-server/server.wasm")), ), }); ``` #### [](#%5Furl)URL Fetch the module from a URL: ```typescript const cerbos = new Embedded({ policies: { ruleId: "" }, wasm: "https://cdn.example.com/cerbos-server.wasm", }); ``` The module is fetched using the Fetch API and compiled using `WebAssembly.instantiateStreaming`. #### [](#%5Fprecompiled%5Fmodule)Precompiled module If you have a precompiled `WebAssembly.Module`: ```typescript const cerbos = new Embedded({ policies: { ruleId: "" }, wasm: precompiledModule, }); ``` ### [](#%5Fconfiguration%5Foptions)Configuration options | Option | Default | Description | | -------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | defaultPolicyVersion | "default" | Policy version applied to requests that do not specify one. Requests can override this per-call. | | defaultScope | "" | Scope applied to requests that do not specify one. Requests can override this per-call. | | globals | {} | Global variables passed to policy conditions. Use this for environment-specific values like feature flags or deployment region. | | lenientScopeSearch | false | When enabled, if a policy with the exact requested scope is not found, the engine tries ancestor scopes in order. When disabled, the exact scope must exist. | | schemaEnforcement | NONE | Validation level for input schemas defined in policies. NONE skips validation. WARN validates and includes errors in the response. REJECT denies the action if validation fails. | | onDecision | (none) | Callback invoked after each authorization decision. Receives a decision log entry containing the request, response, and evaluation metadata. Use for local logging or analytics. | | decodeJWTPayload | (throws) | Function to verify and decode JWTs passed as auxiliary data. Required if your policies reference JWT claims. See the SDK documentation for implementation examples. | ### [](#%5Ferror%5Fhandling)Error handling The client throws errors for initialization failures and unrecoverable conditions: ```typescript import { NotOK, Status } from "@cerbos/core"; try { const cerbos = new Embedded({ policies: { ruleId: "" }, wasm, }); await cerbos.isAllowed({ /* ... */ }); } catch (error) { if (error instanceof NotOK) { switch (error.code) { case Status.UNAUTHENTICATED: // Credentials required but not provided, or invalid break; case Status.PERMISSION_DENIED: // IP address not in allowed list break; case Status.NOT_FOUND: // Rule ID does not exist break; case Status.FAILED_PRECONDITION: // Rule is disabled break; case Status.INVALID_ARGUMENT: // Scope parameter required but not provided break; } } } ``` Authorization check methods (`isAllowed`, `checkResource`, etc.) do not throw for denied permissions. A denied permission is a valid result, not an error. ## [](#%5Fconstraints%5Fand%5Flimitations)Constraints and limitations ### [](#%5Fbundle%5Fupdates)Bundle updates ePDP bundles are point-in-time snapshots of compiled policies. Changes to policies in Cerbos Hub do not affect already-downloaded bundles until the client downloads an update. The SDK’s automatic update mechanism handles this for long-running applications. For short-lived processes like serverless functions, each invocation downloads the current bundle. ### [](#%5Ffeature%5Fsupport)Feature support The WebAssembly runtime supports core Cerbos policy evaluation. Some features available in service PDPs are not supported: * **Audit logging to external sinks**: Decision logs can be captured via the `onDecision` callback but are not automatically sent to Cerbos Hub or other destinations. * **Admin API**: The embedded runtime does not expose administrative endpoints. * **Runtime configuration changes**: Options like `globals` and `schemaEnforcement` are fixed at client initialization. ### [](#%5Fengine%5Fsize)Engine size The `@cerbos/embedded-server` WASM file is several megabytes. This is the policy evaluation engine and is a one-time download that can be cached by the browser. It does not grow as your policies change. For frameworks that require copying the file (such as Next.js), use the `postinstall` script approach described [above](#%5Fnextjs%5Fturbopack) and add the copied WASM file to your `.gitignore` since the canonical copy is distributed via npm. ### [](#%5Fbundle%5Fsize)Bundle size Bundle size depends on policy complexity: * Number of resource types, actions, and roles * Complexity of conditions (CEL expressions) * Number of scopes * Schema definitions For browser applications, monitor bundle size as policies grow. Large bundles increase initial page load time. Use filtering to include only necessary policies. ### [](#%5Fplatform%5Fsupport)Platform support The JavaScript SDK supports browsers, Node.js, edge runtimes, and React Native. The WebAssembly module executes in any JavaScript host environment. | | Some edge runtimes have restrictions on WebAssembly APIs. For example, Cloudflare Workers does not support WebAssembly.instantiateStreaming. In such environments, you may need to bundle the WASM binary directly into your application or use a platform-specific loading mechanism. Consult your runtime’s documentation for WebAssembly support details. | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Native iOS and Android SDK support is planned. For other platforms (Go, Python, etc.), use [service PDPs](decision-points.html). ### [](#%5Fconcurrent%5Faccess)Concurrent access The `Embedded` client is safe to use concurrently. Multiple authorization checks can be in flight simultaneously. Creating multiple `Embedded` clients with the same `PolicyLoader` shares the underlying bundle. Updates are applied to all clients sharing the loader. Deployments ==================== A deployment is a specific configuration of [policy stores](policy-stores.html) (such as 'production' or 'staging') that can be connected to a set of PDPs. Each new change to the underlying store(s) results in a new policy build that’s automatically delivered to the [policy decision points (PDPs)](decision-points.html) if the tests are successful. Source agnostic inputs Populate a policy store from any Git provider, CI system, API, CLI, or direct upload, so your existing workflows remain intact. Multi-store composition Reference multiple stores in a deployment to separate ownership, for example security team versus product team, or to blend static Git-managed policies with dynamic API-driven rules. End-to-end automation Building, testing, and distribution of policies is fully managed by Cerbos Hub, giving you a consistent CI/CD style pipeline for authorization without the need for extra infrastructure. Strong versioning Every deployment attempt is attached to a set of immutable policy store versions, making it easy to audit exactly which policies were in effect at any given point in time and to revert any changes if needed. ## [](#%5Fdeployment%5Foverview)Deployment overview Each deployment page provides several tabs to manage and monitor your policy pipeline: Builds View the history of deployed versions, showing which policy bundles have been active and when. Each build shows the contributing policy stores and their versions. Policies Browse the current policies included in this deployment and view their contents. Decision points See the PDPs currently connected to this deployment. Each PDP shows its ID, the build it’s running, active sessions, Cerbos version, when it was last seen, and a link to its audit logs. Embedded PDP rules Configure [embedded policy decision points](deployments-epdp-rules.html) for this deployment. Each rule defines policy filtering criteria (resources, actions, scopes, roles, versions), authentication requirements, and IP allowlists. Multiple rules can serve different clients or environments from the same deployment. Client credentials Manage API credentials scoped to this deployment for PDP connections, audit log collection, and authenticated ePDP bundle access. Settings Configure deployment options including which policy stores contribute to builds, and [freeze or unfreeze](#%5Ffreezing%5Fand%5Frolling%5Fback) the deployment. ## [](#%5Fdeployed%5Fversions)Deployed versions The Builds tab displays the history of policy bundles that have been deployed. Each row in the table shows: Build reference A unique identifier for the build. Click to view detailed information about the build including test results and bundle contents. Active from The timestamp when this build was activated and pushed to connected PDPs. Active to The timestamp when this build was replaced by a newer version, or a dash if it is the currently active build. Included policies The policy stores and specific versions that contributed to this build. This makes it easy to trace exactly which policies were in effect at any point in time. ## [](#%5Ffreezing%5Fand%5Frolling%5Fback)Freezing and rolling back Freezing a deployment holds the live version in place and stops new builds reaching connected PDPs. Rolling back replaces the live version with an earlier build. Both actions require the `Owner` or `Developer` role in the workspace. See [User management](user-management.html) for details. ### [](#%5Ffreezing%5Fa%5Fdeployment)Freezing a deployment Open the deployment’s Settings tab and choose **Freeze deployment**. The version that is currently live stays live, and policy changes pushed to the linked stores are no longer deployed to connected PDPs while the deployment is frozen. A frozen deployment is labelled **Frozen** in the workspace deployment list and on the deployment page itself. Choose **Unfreeze deployment** on the same tab to resume. The latest version of your policies is built and deployed to connected PDPs, replacing whatever was live, including a version that was pinned by a rollback. ### [](#%5Frolling%5Fback%5Fto%5Fan%5Fearlier%5Fversion)Rolling back to an earlier version Each row in the [deployed versions](#%5Fdeployed%5Fversions) table other than the live one carries an action: Roll back Shown for versions older than the one that is currently live. Promote Shown for versions newer than the one that is currently live, which happens once a rollback has pinned the deployment to an older build. Selecting either deploys that version to connected PDPs immediately and freezes the deployment. The version you picked stays live until you unfreeze it, so a rollback is not undone by the next policy change that lands in a contributing store. If someone else changes the same deployment while you have the page open, your action is rejected and the page refreshes so you can check the current state before trying again. ## [](#%5Fbrowsing%5Fpolicies)Browsing policies The Policies tab shows the policies included in the deployment’s current build. A **Source** / **Effect matrix** toggle at the top of the tab switches between two ways of viewing them. ### [](#%5Fsource%5Fview)Source view The source view lists the policy files in the bundle and lets you open any file to read its contents exactly as it was compiled. Use the bundle selector to switch between builds when you need to inspect an earlier version. ### [](#%5Feffect%5Fmatrix%5Fview)Effect matrix view The effect matrix presents a resource’s permissions as a grid instead of as policy source. You choose the resource, policy version, and scope to view, and roles and actions are laid out on the axes, with each cell showing the effect that applies to that role-and-action combination in the compiled bundle: Allow The action is permitted for the role. Deny The action is denied for the role. Conditional The effect depends on a condition that is evaluated at request time against the principal and resource attributes. This gives you an at-a-glance view of a resource’s effective permissions without reading through every policy file, which is useful for reviewing changes and explaining access to others. Cells also indicate where a wildcard rule widens a match. A rule written against `*` or a pattern such as `foo:*` can apply to more specific actions or roles, and the matrix flags the cells that are influenced by such a rule so the broader grant is visible rather than hidden. This is an indication of which rules _match_ a cell, not a full partial evaluation of the policy. [Derived roles](../cerbos/latest/policies/derived%5Froles.html) and the custom roles defined by [role policies](../cerbos/latest/policies/role%5Fpolicies.html) each appear as their own rows rather than being folded into the roles they build on. A role policy narrows access rather than granting it, allowing an action only when it is permitted by both the role policy and a matching resource policy rule. To focus on part of a large matrix, filter by role or action, and select a cell to drill into the underlying rules — including any conditions — that produce its effect. #### [](#%5Flimitations)Limitations The matrix highlights the rules that match each cell rather than fully evaluating the policy, so keep these limitations in mind when reading it: Scope Effects reflect the selected scope only. Because [scoped policies](../cerbos/latest/policies/scoped%5Fpolicies.html) can override the effects defined higher in the scope hierarchy, switch scopes to review each one in turn. Derived roles Derived role rules are shown only in their own cells. They are not reflected in the cells of the parent roles they extend. Custom roles Role policy rules are shown only in their own cells. They are not reflected in the other cells whose effects they restrict. ### [](#%5Fdownloading%5Fpolicies)Downloading policies The Policies tab also has a **Download policies** button, which gives you the policy files from the build as a zip archive. Inside the archive, files are grouped into a folder per contributing store and keep the paths they have within that store, so you can tell where each policy came from when a deployment draws on more than one. The download follows the bundle selector, so switch to an earlier build to download exactly the files that build was running. Use this to review a build away from Hub, compare it against your policy repository, or keep a record of what was deployed. ## [](#%5Fbuild%5Flife%5Fcycle)Build life cycle Whenever Cerbos Hub detects a change in any policy store connected to a deployment, it launches a new policy build. 1. **In progress**: The policy build is queued and begins processing. 2. **Compilation**: Policies from all contributing stores are compiled together. If compilation fails, the error is surfaced so you can diagnose it quickly. 3. **Test execution**: After successful compilation, Cerbos Hub runs all policy tests found across the contributing stores. Failures are displayed with full logs for debugging. 4. **Bundle generation**: When compilation and tests pass, the bundle is generated and all PDPs assigned to this deployment receive a push notification to download and activate the new bundle immediately. | | While a deployment is [frozen](#%5Ffreezing%5Fand%5Frolling%5Fback), changes to the contributing policy stores are not deployed and the version that is currently live stays in place. Unfreezing the deployment builds and deploys the latest version of your policies. | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#%5Fworkspace%5Fissues)Workspace issues Cerbos Hub continuously checks your workspace for problems that need attention and surfaces them in the **issues bar** shown across every page. When something needs looking at, the issues bar highlights it, and each issue links through to the relevant page so you can resolve it. The following conditions raise an issue: Blocked API keys One or more of the API keys used to connect PDPs or collect audit logs have been blocked and need to be rotated or re-enabled. Stalled deployment A deployment has been mapped to a new policy store but no new build has been activated for it in the last 15 minutes, so connected PDPs may still be serving an older bundle. Store sync failures A [GitHub-connected policy store](policy-stores-git-github.html) is failing to sync, so recent changes in the repository are not being picked up. See [Troubleshooting](troubleshooting.html) for guidance on resolving these conditions. ## [](#%5Fbuild%5Fdetails)Build details Click on any build reference to view detailed information about that specific build. ### [](#%5Fbuild%5Fsummary)Build summary The top of the page displays key metrics at a glance: * **Status**: Whether the build succeeded or failed * **Build completed**: When the build finished * **Build time**: How long the build took to complete * **Test results**: Total tests run, passed, skipped, and failed ### [](#%5Fcompile%5Fand%5Ftest%5Fstages)Compile and test stages Expandable sections show the results of each build stage: * **Compile**: Shows whether policy compilation succeeded. Expand to see any compilation errors or warnings. * **Test**: Shows whether all policy tests passed. Expand to see detailed test results, including any failures with full output for debugging. ### [](#%5Fbundle%5Ffile%5Fexplorer)Bundle file explorer The file explorer displays the contents of the generated policy bundle. Browse the directory structure organized by contributing policy store, and click any file to view its contents. This lets you verify exactly which policies were included in the bundle and inspect their contents without leaving Cerbos Hub. Use the file explorer to: * Confirm the correct policy versions were bundled * Debug unexpected authorization behavior by examining the active policies * Audit which policies contributed to a specific deployment For details on creating policy stores and connecting PDPs to receive bundles, see the related guides: * [Policy stores](policy-stores.html) * [PDP configuration](decision-points.html) ## [](#%5Fbest%5Fpractices)Best practices Use meaningful names Name deployments after their purpose such as application, environment, or team, for example payments-service-production. Automate testing Include comprehensive test cases with each policy store to catch regressions before they reach production PDPs. Validate in staging Use staging deployments to verify policy changes in a pre-production environment before promoting to production. Distribution licence ==================== Commercial Cerbos components are distributed as container images and Helm charts from the Cerbos distribution registry. Each organization authenticates to the registry with a **distribution licence key**, which you manage from the **Distribution licence** section of your organization settings in Cerbos Hub. Your specific registry endpoint is shown alongside the licence key in Hub; use that value in place of `` below. [Cerbos Synapse](../synapse/latest/index.html) is currently distributed this way: a licence key is required to pull the [Synapse container image](../synapse/latest/install/container.html) and the [Synapse Helm chart](../synapse/latest/install/helm.html). ## [](#%5Fissuing%5Fand%5Fusing%5Fa%5Fkey)Issuing and using a key Generate a distribution licence key from the **Distribution licence** section of your organization settings. The key is used as the password when authenticating to the registry — the username is your organization ID: ```bash docker login \ --username= \ --password= ``` Treat the key like a secret. Anything that needs to pull images from the registry — your CI pipeline, container runtime, or local environment — uses it to authenticate. A key may be issued with or without an expiry; if it has an expiry, the registry stops accepting it after that time. ## [](#%5Frotating%5Fa%5Fkey)Rotating a key Rotating issues a new key while keeping the current one valid for a configurable grace period (up to 12 hours), so you can roll the new key out to your deployments before the old one stops working. Set the grace period to zero to revoke the previous key immediately. Already-running instances are unaffected during the grace period; only new registry pulls require the new key once the old one is revoked. ## [](#%5Frevoking%5Fa%5Fkey)Revoking a key Revoking invalidates the current key immediately and cannot be undone. Running instances keep working, but any new registry pull — for example pulling an updated Synapse image — fails until a new key is issued and configured. Revoke a key if you believe it has been exposed, then generate a replacement. ## [](#%5Favailability)Availability The distribution licence section appears for organizations with access to commercial Cerbos components. [Contact the Cerbos team](https://cerbos.dev/contact) for access. Getting started ==================== ## [](#%5Fprerequisites)Prerequisites * A set of Cerbos policies. An example set of policies are avaliable at . * Cerbos version 0.45.1 or higher. * Outbound internet access from your Cerbos instances so that they can connect to Cerbos Hub to fetch bundle updates and, if enabled, upload audit logs. ## [](#%5Fcreate%5Fa%5Fpolicy%5Fstore)Create a policy store Cerbos Hub uses policy stores to manage your policies. A policy store is a collection of policies and tests that can be built into a deployment and distributed to Cerbos PDPs. For the quick start, you can create a policy store using the browser and upload a ZIP file containing policies ([example](https://github.com/cerbos/example-cerbos-policy-repository/archive/refs/heads/main.zip)) or fork the GitHub [example repository](https://github.com/cerbos/example-cerbos-policy-repository) and connect it to Cerbos Hub. ### [](#%5Fupload%5Fpolicies%5Fvia%5Fbrowser)Upload policies via browser 1. Sign in to Cerbos Hub at and follow the on-boarding wizard to create an Organization and its first Workspace. 2. Inside the Workspace, select **Policy stores** then **New store**. 3. Give the store a clear name, for example `orders-service`, choose **Browser upload** as the source, and click **Create**. 4. In the store detail page, click **Upload files** and select a ZIP file containing your policies. The ZIP file should contain the policies in the root directory, not in a subdirectory. 5. Cerbos Hub immediately ingests the ZIP file, compiles the policies, and shows the first successful build. ### [](#%5Fgithub%5Frepository)GitHub repository 1. Sign in to Cerbos Hub at and follow the on-boarding wizard to create an Organization and its first Workspace. 2. Inside the Workspace, select **Policy stores** then **New store**. 3. Give the store a clear name, for example `orders-service`, choose **GitHub repository** as the source and connect to your GitHub account. 4. Pick the branch you want Hub to track, usually `main`, and save. Cerbos Hub immediately ingests the repository, compiles the policies, and shows the first successful build. | | You can create additional stores for other branches, teams or projects. | | -------------------------------------------------------------------------- | ## [](#%5Fcreate%5Fa%5Fdeployment)Create a Deployment Deployments package policies from one or more policy stores into versioned bundles that are automatically distributed to connected Cerbos PDPs. 1. Open **Deployments** then click **New deployment**. 2. Select the store you just created. 3. Click **Create**. Hub starts the initial build. When it finishes, note the deployment ID shown on the detail page. You will need this ID to configure the PDP. ## [](#%5Fgenerate%5Fclient%5Fcredentials)Generate client credentials Navigate to **Settings** → **Client credentials** and click **Generate a client credential**, giving it a name and select **Read & Write** so that policies can be pulled down and Audit Logs pushed back. Copy both the Client ID and Client secret. The secret is shown only once. ## [](#%5Fconfigure%5Fand%5Frun%5Fa%5Fcerbos%5Fpdp)Configure and run a Cerbos PDP You can pass the Hub connection settings as environment variables or in a YAML configuration file. The example below uses environment variables for a quick start: ```shell docker run --rm --name cerbos \ -p 3592:3592 -p 3593:3593 \ -e CERBOS_HUB_DEPLOYMENT_ID="..." \ -e CERBOS_HUB_CLIENT_ID="..." \ -e CERBOS_HUB_CLIENT_SECRET="..." \ ghcr.io/cerbos/cerbos:latest server ``` | CERBOS\_HUB\_DEPLOYMENT\_ID | Deployment ID from the deployment detail page in Cerbos Hub. | | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | CERBOS\_HUB\_CLIENT\_ID | Client ID from **Deployment → Client credentials**. | | CERBOS\_HUB\_CLIENT\_SECRET | Client secret from **Deployment → Client credentials**. | | CERBOS\_HUB\_PDP\_ID | Optional. A friendly name for this PDP instance, shown on the Cerbos Hub monitoring page. If omitted, a random identifier is generated. | ### [](#%5Fyaml%5Falternative)YAML alternative ```yaml server: httpListenAddr: ":3592" grpcListenAddr: ":3593" hub: credentials: pdpID: "orders-pdp-01" # Optional clientID: "..." clientSecret: "..." storage: driver: hub hub: remote: deploymentID: "..." # Deployment ID from Hub ``` Assuming you saved the file as .cerbos.yaml in the current directory, start Cerbos with: ```shell docker run --rm --name cerbos \ -v $(pwd):/conf \ -p 3592:3592 -p 3593:3593 \ ghcr.io/cerbos/cerbos:latest server --config=/conf/.cerbos.yaml ``` See [Configuration](../cerbos/latest/configuration/index.html) for advanced configuration options. ## [](#%5Fenable%5Faudit%5Flog%5Fcollection%5Foptional)Enable audit log collection (optional) Add the Hub audit backend to stream decision logs to Cerbos Hub: ```yaml audit: backend: hub hub: storagePath: "/var/cerbos/audit-buffer" # Local buffer used when the network is unavailable ``` Refer to [Audit log collection](audit-log-collection.html) for details on filtering sensitive fields and other advanced options. With a policy store connected, a deployment created, and at least one PDP running, you are ready to iterate on your policies. Push a change to the repository, watch Cerbos Hub build a new deployment version, and see the PDP update itself automatically within seconds. ## [](#%5Fnext%5Fsteps)Next steps | [Service PDPs](decision-points.html) | Production deployment patterns, Kubernetes setup, reliability features, and monitoring. | | ------------------------------------------------- | --------------------------------------------------------------------------------------- | | [Embedded PDPs](deployments-epdp-rules.html) | Run authorization in browsers, edge functions, and mobile apps using WebAssembly. | | [Audit log collection](audit-log-collection.html) | Stream decision logs to Cerbos Hub for analysis and compliance. | | [Policy stores](policy-stores.html) | Connect Git repositories, upload via CLI, or use the SDKs to manage policies. | Cerbos Hub ==================== Cerbos Hub is an end-to-end authorization platform comprising a policy control plane, a data enrichment layer, and a distributed policy engine. It covers the full authorization lifecycle: authoring, testing, versioning, distribution, data enrichment, decision evaluation, and audit. [Sign up for Cerbos Hub](https://hub.cerbos.cloud?utm%5Fcampaign=brand%5Fcerbos&utm%5Fsource=documentation&utm%5Fmedium=text&utm%5Fcontent=hub%5Fdocs%5Findex) to create an organization and its first workspace, then follow the [quick start guide](getting-started.html) to connect a policy store and a PDP. ## [](#%5Fplatform%5Fcomponents)Platform components | Component | Role | | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Cerbos Hub** — Policy Administration Point | Control plane for policy authoring, testing, versioning, distribution, and audit visibility. | | **[Cerbos Synapse](../synapse/latest/index.html)** — Data and integration layer | Enrichment layer that gathers identity and resource data from existing systems and delivers complete context to the policy engine. [Contact the Cerbos team](https://cerbos.dev/contact) for access. | | **Cerbos PDP** — Policy Decision Point | Open source authorization engine that evaluates requests against policies and returns access decisions. Stateless, high-performance, and built to scale horizontally. | | **Cerbos PEP SDKs** — Policy Enforcement Point | Language-native client libraries that connect applications directly to PDPs to enforce real-time access decisions. | ## [](#%5Fcerbos%5Fhub%5Ffeatures)Cerbos Hub features [Collaborative policy editing](playground.html) Cerbos Hub playgrounds provide private, collaborative, IDE-like development environments to help author and test policies with ease. Drag and drop policy and test files directly into the playground editor for fast loading and testing. [Managed build and release pipeline](deployments.html) Cerbos Hub automatically validates, tests, signs, and distributes every policy change, giving you a turnkey CI/CD pipeline without extra infrastructure. Policy execution traces are available for debugging complex authorization logic, showing a step-by-step view of how requests are evaluated. [Source agnostic policy stores](policy-stores.html) Populate policy stores from any source using any of the many integration methods available. [Coordinated rollout of policy changes](deployments.html) Cerbos Hub pushes new policy bundles to every connected PDP instance, ensuring fleet-wide consistency and eliminating manual polling or reload logic. [PDP monitoring](decision-points.html) Cerbos Hub shows which policies each PDP is serving, the exact bundle version, and when the instance was last seen. [Embedded policy decision points](deployments-epdp-rules.html) Evaluate policies locally in browsers, edge functions, and other JavaScript environments using WebAssembly. Configure multiple ePDP rules per deployment, each with independent policy filtering (by resource, action, scope, role, or version), authentication requirements, and IP allowlists. Dynamic scopes enable per-tenant bundles in multi-tenant applications. [Audit log aggregation](audit-log-collection.html) With one line of configuration you can stream PDP decision logs to Cerbos Hub, filter sensitive fields locally, and retain searchable history without running a separate log stack. [Organization usage dashboard](usage-dashboard.html) The organization-level usage dashboard aggregates metrics from all your workspaces, providing a unified view of request volumes, policy distribution, and usage trends across your organization. ## [](#%5Fcerbos%5Fhub%5Fand%5Fopen%5Fsource%5Fcerbos)Cerbos Hub and open source Cerbos The [open source Cerbos PDP](https://github.com/cerbos/cerbos) is a fully functional authorization engine that can run standalone, reading policies from a git repository, database, or local filesystem. No license or Hub account is required. Cerbos Hub is an optional control plane that adds managed CI/CD, fleet-wide push-based updates, collaborative policy editing, embedded PDP support, and centralized audit log collection on top of the open source PDP. Switching to Hub requires only a configuration change — the PDP binary is the same. ## [](#%5Fhow%5Fit%5Fworks)How it works Cerbos Hub is a cloud-hosted management control plane, while Cerbos instances and the data they process remain strictly inside your network perimeter. Switching to Cerbos Hub requires only a minor configuration change to your existing Cerbos deployment. After the switch, PDPs receive optimized policy bundles from Cerbos Hub instead of compiling policies locally. ![How Cerbos Hub works](_images/how_cerbos_hub_works.png) 1. Make a change to policies and submit it to a policy store through Git, a CI pipeline, an API call, a CLI upload, or a direct drag and drop in the browser. 2. Cerbos Hub detects the update and starts a new build. 3. Validate and compile the policies. 4. Run all policy tests found in the store. 5. Generate a compact encrypted policy bundle. 6. Increment the deployment version and notify every PDP that is assigned to this deployment that a new bundle is available. 7. PDP instances download the new bundle and start serving it immediately. Optionally, configure Cerbos Hub as an audit backend for the PDPs. Logs are streamed securely, with sensitive data removed locally before leaving your network perimeter. ## [](#%5Fget%5Fstarted)Get started * [Quick start guide](getting-started.html) — Create a policy store, deployment, and connect your first PDP. * [Service PDPs](decision-points.html) — Production deployment patterns for server-side authorization. * [Embedded PDPs](deployments-epdp-rules.html) — Client-side authorization for browsers and edge functions. * [Synapse](../synapse/latest/index.html) — Authorization data enrichment and infrastructure integrations. Insights ==================== The Insights page turns the authorization decisions flowing through your workspace into a set of visualizations, giving you an at-a-glance picture of what Cerbos is doing in production. Where the [audit log](audit-log-collection.html) lets you inspect individual decisions, Insights aggregates those same decisions into charts and rankings so you can spot trends, surface anomalies, and understand which principals and resources are driving authorization traffic. The visualizations are built entirely from the decision data your PDPs already send to Cerbos Hub — there is nothing extra to configure. Once [audit log collection](audit-log-collection.html) is enabled and decisions start arriving, the Insights page populates automatically. | | Insights is only available to workspace members who can view audit logs. The page draws on the same decision data as the audit log, so access follows the same rules: it is visible to users with the **Owner** or **Analyst** role (see [User management](user-management.html)), and only once [audit log collection](audit-log-collection.html) has been enabled for the workspace. Members without audit log access will not see the **Insights** tab in the workspace navigation. | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ![Insights overview](_images/insights_overview.png) All times on the page are shown in UTC. ## [](#%5Fwhy%5Fit%5Fmatters)Why it matters A table of raw audit logs answers the question "what happened in this specific request?". Insights answers the broader questions that matter when you are operating an authorization service: * **Is Cerbos working?** Live charts of allows and denies make it immediately obvious that decisions are flowing and policies are being enforced. * **What changed?** A sudden swing in the ratio of denies to allows can point to a stricter-than-intended policy change, a misconfigured client, or an application bug such as a typo in a resource or action name. * **Who and what is driving traffic?** Rankings of the busiest principals and resources help you understand adoption, plan for capacity, and notice unexpected usage patterns. This makes Insights useful both for day-to-day operational monitoring and for demonstrating, at a glance, that your authorization layer is healthy. ## [](#%5Fdecision%5Factivity%5Fcharts)Decision activity charts ### [](#%5Fhourly%5Fdecisions)Hourly decisions A stacked bar chart of allowed and denied decisions per hour over the **last 7 days**, with allows and denies shown as separate colours in each bar. The fine-grained, hourly view is best for spotting short-lived spikes and anomalies, for example a burst of denials following a deployment. Use the toggle in the top right of the chart to switch between **Allowed**, **Denied**, and a **Combined** view that shows both. ![Hourly decisions](_images/insights_hourly_decisions.png) ### [](#%5Fdaily%5Fdecisions)Daily decisions The same breakdown of allowed and denied decisions, aggregated per day over the **last 30 days**, with the same **Allowed** / **Denied** / **Combined** toggle. The daily granularity smooths out hourly noise and is better suited to spotting longer-term trends in authorization traffic. ![Daily decisions](_images/insights_daily_decisions.png) ### [](#%5Fdaily%5Factive%5Fprincipals)Daily active principals A line chart of the number of distinct principals making authorization requests each day over the **last 30 days**. Track this to understand adoption, identify growth or seasonal patterns, and notice unexpected drops in usage. ![Daily active principals](_images/insights_active_principals.png) ## [](#%5Factivity%5Frankings)Activity rankings Three tables rank the busiest entities in your workspace over the **last 30 days**, each ordered by request count. Every row has a **View** link that opens the [audit log](audit-log-collection.html) filtered to that entity, so you can go straight from a ranking to the decisions behind it. ### [](#%5Fmost%5Factive%5Fprincipals)Most active principals The top principals by request volume, showing the principal ID and its total request count. Useful for identifying your heaviest consumers and for catching a single principal generating an unexpected amount of traffic. ![Most active principals](_images/insights_top_principals.png) ### [](#%5Fmost%5Factive%5Fresource%5Fkinds)Most active resource kinds The top resource kinds by request volume, showing the resource kind and its total request count. This reveals which parts of your application are most frequently subject to authorization checks. ![Most active resource kinds](_images/insights_top_resource_kinds.png) ### [](#%5Fmost%5Factive%5Fresource%5Faction%5Fpairs)Most active resource & action pairs The top resource-kind-and-action combinations by request volume, showing the resource kind, the action, and the total request count. This gives the most detailed view of what is actually being checked — for example, `document:view` versus `document:delete`. ![Most active resource and action pairs](_images/insights_top_resource_action_pairs.png) ## [](#%5Fdrilling%5Finto%5Fthe%5Fdetail)Drilling into the detail The activity tables link through to the [audit log](audit-log-collection.html) via the **View** link on each row, pre-filtered to match the principal, resource kind, or resource-and-action pair you selected. When a ranking surfaces something worth investigating, such as an unusually busy principal, you can jump straight from the aggregated view into the underlying decisions without rebuilding the filter by hand. Aperture by Tailscale ==================== Aperture is Tailscale’s AI gateway. It sits between AI agents and LLM providers, routing requests through the tailnet with visibility into agent activity. The Cerbos integration adds fine-grained, policy-driven authorization to every tool call that passes through Aperture. When an agent makes a tool call, Aperture intercepts the request before it reaches the LLM. It extracts the user identity, tool name, and tool parameters and forwards them to a Cerbos PDP running on the tailnet. Cerbos evaluates the request against authorization policies and returns an allow or deny decision. Aperture enforces that decision: permitted tool calls proceed, denied tool calls are blocked before execution. ## [](#%5Farchitecture)Architecture User -> AI Agent -> Tailscale Aperture -> LLM | ^ Cerbos PDP Allow/Deny | Aperture | AI gateway and enforcement point. Intercepts tool calls, forwards metadata to Cerbos, and enforces the authorization decision. | | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Cerbos PDP | Evaluates tool calls against authorization policies. Enriches requests with context from external systems (identity providers, HR platforms, on-call systems) before making a decision. | | Cerbos Hub | Manages the policy lifecycle: authoring, version control, testing, distribution, and audit logging. Every decision is logged back to Hub as audit evidence. | ## [](#%5Fwhat%5Fis%5Fevaluated)What is evaluated Every tool call produces an authorization request containing: | Principal attributes | The identity of whoever or whatever initiated the tool call, sourced from Tailscale. This can be enriched at decision time with real-time context from external systems such as Okta, Workday, ServiceNow, or PagerDuty: department, cost center, on-call status, project assignments. | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Resource attributes | The tool name and its parameters (the database being queried, the CRM object being accessed, the API endpoint being called), along with metadata about the model and provider. | | Request context | Runtime signals such as time of day, device posture, and Tailscale app capabilities. | The policy engine evaluates all inputs together. A decision reflects who or what is making the call, what they are attempting, and what the organization knows about both at that moment. ## [](#%5Fprerequisites)Prerequisites * A [Tailscale](https://tailscale.com) account with an [Aperture](https://tailscale.com/aperture) instance set up * Docker installed on a machine connected to your tailnet ## [](#%5Fsetup)Setup ### [](#%5Fconfigure%5Ftailscale%5Facls)Configure Tailscale ACLs Define a tag and service for the Cerbos container in your tailnet access controls: 1. Create a tag (e.g. `tag:tainaron-host`) 2. Create a service named `cerbos-tainaron` on port 443 with that tag 3. Add the following ACL grants: ```json { "tagOwners": { "tag:tainaron-host": ["autogroup:member"] }, "autoApprovers": { "services": { "svc:cerbos-tainaron": ["tag:tainaron-host"] } }, "grants": [{ "src": ["*"], "dst": ["svc:cerbos-tainaron"], "ip": ["*"] }] } ``` 4. Generate a Tailscale auth key with the tag assigned. Ephemeral mode is recommended. ### [](#%5Fconnect%5Fcerbos%5Fhub%5Ffrom%5Faperture)Connect Cerbos Hub from Aperture 1. In your Aperture instance, select the Cerbos integration. This redirects you to Cerbos Hub to create a workspace linked to your Aperture instance. 2. Complete the Cerbos Hub sign-up. The workspace is automatically provisioned with a deployment and credentials for your Aperture instance. ### [](#%5Fdownload%5Fthe%5Fconfiguration%5Ffile)Download the configuration file After the workspace is provisioned, the Cerbos Hub dashboard provides a configuration file with your credentials pre-filled. 1. Navigate to the Aperture configuration page in your workspace 2. Click **Generate and download config file** 3. Open the downloaded `aperture.yaml` in a text editor 4. Replace `` with the Tailscale auth key generated in the previous step The configuration file contains the Cerbos Hub credentials, PDP settings, and the Aperture extension configuration: ```yaml server: tailscale: authKey: serviceName: cerbos-tainaron pdp: inProcess: audit: enabled: true backend: "hub" hub: storagePath: /tmp/hub_auditlog hub: credentials: clientID: clientSecret: storage: driver: "hub" hub: remote: deploymentID: extensions: routeExtensions: aperture: extension: extensionURL: /extensions/tailscale/aperture.star routes: "/aperture": ["GET", "POST"] ``` Alternatively, all credentials can be passed as environment variables instead of embedding them in the configuration file: | TS\_AUTHKEY | The Tailscale auth key that was generated above. | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | CERBOS\_HUB\_DEPLOYMENT\_ID | The deployment ID to load policies from. Find this on the deployment page in Cerbos Hub. | | CERBOS\_HUB\_CLIENT\_ID | Client ID from the deployment’s **Client credentials** tab. | | CERBOS\_HUB\_CLIENT\_SECRET | Client secret from the deployment’s **Client credentials** tab. | | CERBOS\_HUB\_PDP\_ID | Optional. A name for this PDP instance, shown in the Cerbos Hub monitoring page. If not provided, a random value is used. | When using environment variables, the configuration file reduces to: ```yaml pdp: inProcess: audit: enabled: true backend: "hub" hub: storagePath: /tmp/hub_auditlog storage: driver: "hub" extensions: routeExtensions: aperture: extension: extensionURL: /extensions/tailscale/aperture.star routes: "/aperture": ["GET", "POST"] ``` ### [](#%5Frun%5Fthe%5Fcerbos%5Fcontainer)Run the Cerbos container From the directory where you saved `aperture.yaml`, start the Cerbos service: ```shell docker run \ --rm \ --tmpfs /tmp \ -v $(pwd)/aperture.yaml:/config/aperture.yaml \ cerbos/tainaron:latest server --conf.path=/config/aperture.yaml ``` The container joins your tailnet automatically and begins accepting authorization requests. ### [](#%5Fconfigure%5Faperture)Configure Aperture Open the Aperture settings page at from a device on your tailnet. Add a hook named `cerbos` pointing to your Cerbos service: ```json { "hooks": { "cerbos": { "url": "https://cerbos-tainaron/ext/aperture" } } } ``` Add a grant to route tool calls to Cerbos for authorization. Set the `src` array to the users whose traffic should be evaluated, or use `*` to apply to all users: ```json { "src": ["*"], "grants": [ { "hook": { "match": { "providers": ["*"], "models": ["*"], "events": ["tool_call_entire_request"] }, "hook": "cerbos", "fields": ["tools"] } } ] } ``` ### [](#%5Fverify%5Fthe%5Fconnection)Verify the connection After starting the container, check the Cerbos Hub dashboard to confirm the PDP has connected. The **Decision points** tab on your deployment page shows all connected PDP instances. ## [](#%5Frecommended%5Frollout)Recommended rollout | Observe | Start with a default allow-all policy. All tool calls are evaluated and logged, but nothing is blocked. Use the [audit data](audit-log-collection.html) to build an evidence base of what agents are doing. | | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Author policies | Write rules that reflect the organization’s requirements. Start narrow: restrict a single tool or constrain a specific parameter. Expand coverage incrementally. | | Enforce | Push policies through Cerbos Hub. They take effect immediately across all connected PDPs. No agent modifications required. | ## [](#%5Fpolicy%5Fexamples)Policy examples Cerbos policies are declarative and version-controlled. Each rule targets a set of actions (tool names), applies to specific roles, and specifies conditions under which the rule takes effect. Conditions can reference any attribute of the principal, the resource, or the request context. A single policy can express requirements such as the following: 1. **Read-only tools are always permitted.** File reads, searches, and documentation lookups are allowed unconditionally for all authenticated users. 2. **CRM access is scoped by team and operation.** An agent can read account metadata from Salesforce via an MCP tool, but only for users in the sales org. Updating deal stages or exporting contact lists requires a more privileged role. 3. **Production database access is restricted.** An agent calling a database tool against a production connection string is denied unless the user is currently on call, verified in real time against PagerDuty. The same query against a staging environment is permitted for any engineer. 4. **HR and compensation data require explicit authorization.** Tools that access BambooHR, Workday, or any system containing employee PII are denied by default. Access is granted only to users in HR or People Ops, and only from managed devices. 5. **Different rules apply to different clients.** A tool call from a sanctioned coding agent (Claude Code) is permitted, while the same call from an unsanctioned client is denied. The user-agent is an input to the policy. These rules compose. A single tool call can be evaluated against multiple conditions simultaneously. The policy engine resolves the outcome deterministically. ## [](#%5Faudit%5Flogging)Audit logging Every authorization decision creates a log entry in Cerbos Hub recording: * The principal: who or what triggered the tool call * The resource: which tool, with what parameters * The decision: allow or deny * The policy: which version, which rule matched * The context: all attributes that were evaluated This is a per-decision record that traces from a specific tool call to a specific policy evaluation. See [Audit log collection](audit-log-collection.html) for details on accessing and exporting audit data. ## [](#%5Ffurther%5Freading)Further reading * [Aperture documentation](https://tailscale.com/aperture) * [Service PDPs](decision-points.html) * [Audit log collection](audit-log-collection.html) * [Deployments](deployments.html) On-premises deployment ==================== Cerbos Hub can be deployed on-premises in your own infrastructure, giving you full control over your authorization management platform while maintaining all the features of the cloud-hosted service. ## [](#%5Foverview)Overview On-premises deployment is suitable for organizations that: * Require data residency within their own infrastructure * Have regulatory or compliance requirements that prevent use of cloud services * Need to integrate Hub deeply with internal systems * Want to operate Hub in air-gapped or restricted network environments The on-premises deployment includes all core Cerbos Hub features: * Policy stores and deployments * Policy validation and testing * Bundle generation and distribution to PDPs * Audit log collection and analysis * User management and access control ## [](#%5Fgetting%5Fstarted)Getting started On-premises deployment of Cerbos Hub is available for enterprise customers. To learn more about deploying Cerbos Hub in your own infrastructure, please [contact us](https://cerbos.dev/contact). Our team will work with you to understand your requirements and help you get set up. Collaborative policy playgrounds ==================== Cerbos Hub playgrounds are fully-interactive, private development environments that provide an IDE-like experience for authoring policies. You can quickly create or edit policies and test fixtures with instant feedback on syntax issues and test failures. Take advantage of the powerful collaborative editing features to pair with colleagues to develop new authorization rules or use it as a sandbox to train new team members on Cerbos policies. ## [](#%5Fimporting%5Ffiles)Importing files Drag and drop files directly into the playground editor: * **Individual files**: Drop policy files (`.yaml`, `.yml`) or test files directly into the file tree * **Zip archives**: Drop a zip file containing an entire policy directory structure to load multiple files at once * **Folders**: Drop entire folders to import complete policy sets This makes it easy to quickly load local policies for testing, share examples with colleagues, or migrate existing policies into the playground environment. ## [](#%5Fcreating%5Fa%5Fplayground)Creating a playground Login to Cerbos Hub and select one of the organizations you’re a member of. Click on the **Playgrounds** tab to view existing playgrounds or to create a new one. When creating a new playground, you have a number of options: * Create an empty playground to start from scratch * Generate a starter RBAC policy by answering a few questions about your user roles, resources and actions * Start with an example template set of policies covering common use cases * Create from an existing policy store to experiment with production policies in a safe environment ### [](#%5Fcreate%5Ffrom%5Fa%5Fpolicy%5Fstore)Create from a policy store You can create a playground directly from an existing policy store, making it easy to experiment with production policies without affecting live authorization decisions. This is useful for: * Testing policy changes before deploying them * Debugging authorization issues with real policy configurations * Training team members using actual policies as examples To create a playground from a store, navigate to the policy store and click **Create playground**. The playground is pre-populated with all policies and tests from the store. After creating a playground, click on the **Video tutorial** button on the top right corner of the screen to learn the basics. ## [](#%5Freadme%5Ffiles)README files Playgrounds support README.md files that display as rendered markdown when opened. When you open a playground containing a README and no file is selected, the README displays automatically. README files can include: * Relative links to other files in the playground (click to open in the editor) * External links (opens with a confirmation dialog) * Standard markdown formatting Use README files to document your playground examples, explain policy structure, or provide onboarding instructions for team members. ## [](#%5Fplayground%5Fengine%5Fsettings)Playground engine settings The playground engine settings in the Settings tab allow you to configure the [Cerbos PDP engine](../cerbos/latest/configuration/engine.html) used when evaluating policy during development. Policy execution traces are available, providing a detailed, step-by-step view of how Cerbos evaluates a request, including every rule, condition, and variable evaluation. This helps you debug complex authorization logic and understand exactly why a specific decision was made. * **Default policy version**: When a request does not explicitly specify the policy version, the Cerbos engine attempts to find a matching policy that has its version set to `default`. You can change this fallback value by setting the default policy version. * **Lenient scope search**: When lenient scope search is enabled, if a policy with scope `a.b.c` does not exist in the store, Cerbos will attempt to find scopes `a.b`, `a` and ``` `` ``` in that order. * **Strict evaluation**: By default, an error raised while evaluating a condition or a variable leaves the expression unsatisfied and evaluation carries on, so a rule that would have denied an action is skipped. Typical causes are referencing an attribute that is not present and comparing incompatible types. When strict evaluation is enabled, such errors deny the affected action instead. This setting applies only to the [Explore tab](#%5Fexplore%5Ftab) and the [Try the API](#%5Ftry%5Fthe%5Fapi) section; test files control it per suite or per test case with `options.strictEvaluation`. See [strict evaluation](../cerbos/latest/configuration/engine.html#strict%5Fevaluation) for details of how the deny is scoped. * **Globals**: Global variables are a way to pass environment-specific information to policy conditions. Values defined here are exposed to policy conditions via the `globals` object. You can find full details of these settings in the [Cerbos configuration reference](../cerbos/latest/configuration/engine.html). ## [](#%5Fexplore%5Ftab)Explore tab The Explore tab lets you interactively test authorization decisions against your policies. Select a fixtures folder containing test data, then choose a principal and resource to evaluate. The panel displays: * **Actions and effects**: Each action shows its authorization result (`ALLOW` or `DENY`). Click **Trace** next to any action to see the detailed evaluation path. * **Effective derived roles**: Shows which derived roles were activated for the selected principal, helping you understand role-based access decisions. Click **\+ Add action** to test additional actions, or **Create auxiliary data fixtures** to add test data for your evaluations. ### [](#%5Fschema%5Fvalidation)Schema validation If your policies reference [schemas](../cerbos/latest/policies/schemas.html), the principal and resource you select are validated against them when the request is evaluated. Any violation is reported in an alert directly beneath the relevant fixture selector, listing the attribute paths that failed and why. Validation problems with the request as a whole are reported the same way, above the list of actions. While a validation error is outstanding, every action is shown in an undetermined state rather than as `ALLOW` or `DENY`. This distinguishes a request that could not be evaluated from one that was denied. Correct the fixture, or adjust the schema, and the effects are recalculated immediately. ### [](#%5Fpermissions%5Fmatrix%5Fview)Permissions matrix view The matrix view displays permission evaluation results in a comprehensive grid format, showing how policies affect different principal and resource combinations at a glance. This is useful for: * Visualizing access patterns across multiple principals and resources * Demonstrating authorization behavior during policy reviews or demos * Quickly identifying unexpected permission gaps or grants Toggle between the standard explore view and the matrix view using the view selector in the Explore tab. ## [](#%5Ftest%5Fresults)Test results The bottom panel shows test execution results with two tabs: * **Problems**: Displays any syntax errors or compilation issues in your policies. See [Problems](#%5Fproblems) below. * **Test results**: Shows the outcome of all policy tests. Filter by **Passed**, **Failed**, **Skipped**, or **Errored** to focus on specific results. Click on any test file to see detailed results. For failed tests, a side-by-side diff view highlights the differences between expected and actual outputs. ### [](#%5Fproblems)Problems The Problems tab lists everything that prevented your policies from compiling, grouped by file. Each entry gives the kind of error, the line and column it occurred at, and a description of what went wrong. Click an entry to jump straight to that position in the editor, where the same errors are also marked inline on the offending lines. Diagnostics cover the full range of compilation failures, not only malformed YAML: * Invalid [condition expressions](../cerbos/latest/policies/conditions.html), including expressions that parse but fail to compile. * Undefined, redefined, cyclical, or invalidly named [variables](../cerbos/latest/policies/variables.html#variables) and [constants](../cerbos/latest/policies/variables.html#constants). * Unknown or ambiguous [derived roles](../cerbos/latest/policies/derived%5Froles.html). * Imports that cannot be resolved, and problems with [scoped policies](../cerbos/latest/policies/scoped%5Fpolicies.html) and scope inheritance. * Invalid [schema](../cerbos/latest/policies/schemas.html) references, empty [outputs](../cerbos/latest/policies/outputs.html), and malformed resource rules. Where a problem relates to a documented policy feature, the entry links to the relevant page of the Cerbos policy documentation, so you can go from the error to the reference material without searching for it. ## [](#%5Fexecution%5Ftraces)Execution traces When debugging complex authorization logic, execution traces provide a detailed, step-by-step view of how Cerbos evaluates a request. Traces show every rule, condition, and variable evaluation, helping you understand exactly why a specific decision was made. To view execution traces, click the **Trace** button next to any action in the Explore tab. Expand individual trace entries to see the evaluation path, including which rules matched and how conditions were evaluated. ## [](#%5Ftry%5Fthe%5Fapi)Try the API One of the key features of the playground is the ability to try out authorization checks against your policies without having to run a local Policy Decision Point (PDP). In the Implement tab of the sidebar, you can experiment with both the Check API and the Plan API. This section allows you to select from your test fixtures and view the request structure needed to call the PDP, as well as the expected response. When evaluating policies, the effective derived roles for a user are displayed, helping you understand which derived roles were activated for that user during the request. ![Try the API](_images/playground_try_api.png) ## [](#%5Fconnect%5Fa%5Fpdp%5Fto%5Fa%5Fplayground)Connect a PDP to a playground For developers looking to test the integration of their application, Cerbos Hub offers a Playground PDP connection. This feature allows you to start up a PDP locally in your development environment and connect it to your current playground instance. Any changes you make in the playground are immediately reflected in your local PDP, enabling fast iteration and providing a real-time feedback loop for your integration efforts. Instructions for starting up a local PDP can be found in the Implement tab under "Connect a PDP" ![Connect a PDP](_images/playground_connect_pdp.png) | | Note that while the Playground PDP connection is an excellent tool for rapid development and testing, it’s not intended for production use. When you’re ready to release your application to production environments, you should [create a workspace](getting-started.html) with a policy store and configure the PDPs to receive bundle updates via Cerbos Hub’s fully managed CI/CD pipeline. It also allows you to take advantage of Cerbos Hub audit log collection to effortlessly store and analyze all the decisions made by your PDPs. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#%5Ffrom%5Fplayground%5Fto%5Fproduction)From playground to production Changes made in the playground are not automatically pushed to your policy store or git repository. To deploy playground policies to production, export them and commit to your connected store. 1. Export policies from the playground using the **Export** option in the file toolbar. Each policy is exported as an individual file. 2. Commit and push the exported policy files to your connected [policy store](policy-stores.html) repository. Organizing files into directories such as `resource_policies/`, `principal_policies/`, and `derived_roles/` is recommended but not required. 3. Cerbos Hub automatically detects the change, validates the policies, runs any tests, and produces a new policy bundle. 4. The bundle is pushed to all connected PDPs assigned to that [deployment](deployments.html). | | Use the playground’s **Implement** tab to see the exact API request and response structure for your policies. This helps verify authorization behavior before exporting to production. | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Policy stores: CLI upload (binary) ==================== ## [](#%5Finstallation)Installation `cerbosctl` binaries are available for multiple operating systems and architectures. See the [releases page](https://github.com/cerbos/cerbos/releases/tag/v0.45.1) for all available downloads. | OS | Arch | Bundle | | ----- | --------- | ----------------------------------------- | | Linux | x86-64 | cerbosctl\_0.45.1\_Linux\_x86\_64.tar.gz | | Linux | arm64 | cerbosctl\_0.45.1\_Linux\_arm64.tar.gz | | MacOS | universal | cerbosctl\_0.45.1\_Darwin\_all.tar.gz | | MacOS | x86-64 | cerbosctl\_0.45.1\_Darwin\_x86\_64.tar.gz | | MacOS | arm64 | cerbosctl\_0.45.1\_Darwin\_arm64.tar.gz | You can download the binaries by running the following command. Substitute `` with the appropriate value from the above table. ```sh curl -L -o cerbosctl.tar.gz "https://github.com/cerbos/cerbos/releases/download/v0.45.1/" tar xvf cerbosctl.tar.gz chmod +x cerbosctl mv cerbosctl /usr/local/bin/ # or somewhere on your PATH ``` | | Cerbos binaries are signed using [sigstore](https://www.sigstore.dev) tools during the automated build process and the verification bundle is published along with the binary as .bundle. The following example demonstrates how to verify the Linux X86\_64 bundle archive. \# Download the bundle archive curl -L \\ \-o cerbosctl\_0.45.1\_Linux\_x86\_64.tar.gz \\ "https://github.com/cerbos/cerbos/releases/download/v0.45.1/cerbosctl\_0.45.1\_Linux\_x86\_64.tar.gz" \# Download the verification bundle curl -L \\ \-o cerbosctl\_0.45.1\_Linux\_x86\_64.tar.gz.bundle \\ "https://github.com/cerbos/cerbos/releases/download/v0.45.1/cerbosctl\_0.45.1\_Linux\_x86\_64.tar.gz.bundle" \# Verify the signature cosign verify-blob \\ \--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \\ \--certificate-identity="https://github.com/cerbos/cerbos/.github/workflows/release.yaml@refs/tags/v0.45.1" \\ \--bundle="cerbosctl\_0.45.1\_Linux\_x86\_64.tar.gz.bundle" \\ "cerbosctl\_0.45.1\_Linux\_x86\_64.tar.gz" | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#%5Fusage)Usage The `cerbosctl` CLI tool can be used to upload policies to a policy store in Cerbos Hub. First generate a set of client credentials for the policy store in Cerbos Hub - you can do this in the **Client credentials** section in the UI. Make sure to select the `Read & Write` option when creating the credentials to allow uploading policies. Then export the following environment variables with the values from the generated client credentials and the store ID: ```sh export CERBOS_HUB_CLIENT_ID=... export CERBOS_HUB_CLIENT_SECRET=... export CERBOS_HUB_STORE_ID=... ``` The following command uploads policy files from the current directory and replaces all the files in the store. ```sh cerbosctl hub store replace-files . ``` ## [](#%5Ffull%5Fcli%5Freference)Full CLI Reference ```none Usage: cerbosctl hub store --store-id=STRING --client-id=STRING --client-secret=STRING [flags] Interact with Cerbos Hub managed stores. Requires an existing managed store and the API credentials to access it. The store ID and credentials can be provided using either command-line flags or environment variables. Flags: -h, --help Show context-sensitive help. --store-id=STRING ID of the store to operate on ($CERBOS_HUB_STORE_ID) --client-id=STRING Client ID of the access credential ($CERBOS_HUB_CLIENT_ID) --client-secret=STRING Client secret of the access credential ($CERBOS_HUB_CLIENT_SECRET) Commands: hub store list-files --store-id=STRING --client-id=STRING --client-secret=STRING [flags] List store files hub store get-files --store-id=STRING --client-id=STRING --client-secret=STRING --output-path=STRING ... [flags] Download files from the store hub store download --store-id=STRING --client-id=STRING --client-secret=STRING [flags] Download the entire store hub store replace-files --store-id=STRING --client-id=STRING --client-secret=STRING [flags] Overwrite the store with the given set of files hub store add-files --store-id=STRING --client-id=STRING --client-secret=STRING ... [flags] Add files to the store hub store delete-files --store-id=STRING --client-id=STRING --client-secret=STRING ... [flags] Delete files from the store ``` Policy stores: CLI upload (Container) ==================== The `cerbosctl` CLI tool is also available as a Docker container image: Via ghcr.io ```sh docker run --rm -it ghcr.io/cerbos/cerbosctl:latest hub store ``` Via docker.io ```sh docker run --rm -it docker.io/cerbos/cerbosctl:latest hub store ``` ## [](#%5Fusage)Usage The `cerbosctl` container can be used to upload policies to a policy store in Cerbos Hub. First generate a set of client credentials for the policy store in Cerbos Hub - you can do this in the **Client credentials** section in the UI. Make sure to select the `Read & Write` option when creating the credentials to allow uploading policies. Then export the following environment variables with the values from the generated client credentials and the store ID: ```sh export CERBOS_HUB_CLIENT_ID=... export CERBOS_HUB_CLIENT_SECRET=... export CERBOS_HUB_STORE_ID=... ``` The following command uploads policy files from the policies directory and replaces all the files in the store. ```sh docker run -it {cerbosctl-docker-img} \ -e CERBOS_HUB_CLIENT_ID=$CERBOS_HUB_CLIENT_ID \ -e CERBOS_HUB_CLIENT_SECRET=$CERBOS_HUB_CLIENT_SECRET \ -e CERBOS_HUB_STORE_ID=$CERBOS_HUB_STORE_ID \ -v $(pwd):/policies \ hub store replace-files /policies . ``` ## [](#%5Ffull%5Fcli%5Freference)Full CLI Reference ```none Usage: cerbosctl hub store --store-id=STRING --client-id=STRING --client-secret=STRING [flags] Interact with Cerbos Hub managed stores. Requires an existing managed store and the API credentials to access it. The store ID and credentials can be provided using either command-line flags or environment variables. Flags: -h, --help Show context-sensitive help. --store-id=STRING ID of the store to operate on ($CERBOS_HUB_STORE_ID) --client-id=STRING Client ID of the access credential ($CERBOS_HUB_CLIENT_ID) --client-secret=STRING Client secret of the access credential ($CERBOS_HUB_CLIENT_SECRET) Commands: hub store list-files --store-id=STRING --client-id=STRING --client-secret=STRING [flags] List store files hub store get-files --store-id=STRING --client-id=STRING --client-secret=STRING --output-path=STRING ... [flags] Download files from the store hub store download --store-id=STRING --client-id=STRING --client-secret=STRING [flags] Download the entire store hub store replace-files --store-id=STRING --client-id=STRING --client-secret=STRING [flags] Overwrite the store with the given set of files hub store add-files --store-id=STRING --client-id=STRING --client-secret=STRING ... [flags] Add files to the store hub store delete-files --store-id=STRING --client-id=STRING --client-secret=STRING ... [flags] Delete files from the store ``` Policy stores: CLI upload (Homebrew) ==================== ## [](#%5Finstallation)Installation `cerbosctl` binaries are available via Homebrew for simple installation on macOS. To install the `cerbosctl` CLI tool, run the following command: ```sh brew tap cerbos/tap brew install cerbos ``` ## [](#%5Fusage)Usage The `cerbosctl` CLI tool can be used to upload policies to a policy store in Cerbos Hub. First generate a set of client credentials for the policy store in Cerbos Hub - you can do this in the **Client credentials** section in the UI. Make sure to select the `Read & Write` option when creating the credentials to allow uploading policies. Then export the following environment variables with the values from the generated client credentials and the store ID: ```sh export CERBOS_HUB_CLIENT_ID=... export CERBOS_HUB_CLIENT_SECRET=... export CERBOS_HUB_STORE_ID=... ``` The following command uploads policy files from the current directory and replaces all the files in the store. ```sh cerbosctl hub store replace-files . ``` ## [](#%5Ffull%5Fcli%5Freference)Full CLI Reference ```none Usage: cerbosctl hub store --store-id=STRING --client-id=STRING --client-secret=STRING [flags] Interact with Cerbos Hub managed stores. Requires an existing managed store and the API credentials to access it. The store ID and credentials can be provided using either command-line flags or environment variables. Flags: -h, --help Show context-sensitive help. --store-id=STRING ID of the store to operate on ($CERBOS_HUB_STORE_ID) --client-id=STRING Client ID of the access credential ($CERBOS_HUB_CLIENT_ID) --client-secret=STRING Client secret of the access credential ($CERBOS_HUB_CLIENT_SECRET) Commands: hub store list-files --store-id=STRING --client-id=STRING --client-secret=STRING [flags] List store files hub store get-files --store-id=STRING --client-id=STRING --client-secret=STRING --output-path=STRING ... [flags] Download files from the store hub store download --store-id=STRING --client-id=STRING --client-secret=STRING [flags] Download the entire store hub store replace-files --store-id=STRING --client-id=STRING --client-secret=STRING [flags] Overwrite the store with the given set of files hub store add-files --store-id=STRING --client-id=STRING --client-secret=STRING ... [flags] Add files to the store hub store delete-files --store-id=STRING --client-id=STRING --client-secret=STRING ... [flags] Delete files from the store ``` Policy stores: CLI upload (npx) ==================== ## [](#%5Finstallation)Installation `cerbosctl` binaries are published to npm for easy installation. To install the `cerbosctl` CLI tool, run the following command: ```sh npm install -g cerbosctl ``` Alternatively, you can use `npx` to run the CLI without installing it globally. ## [](#%5Fusage)Usage The `cerbosctl` CLI tool can be used to upload policies to a policy store in Cerbos Hub. First generate a set of client credentials for the policy store in Cerbos Hub - you can do this in the **Client credentials** section in the UI. Make sure to select the `Read & Write` option when creating the credentials to allow uploading policies. Then export the following environment variables with the values from the generated client credentials and the store ID: ```sh export CERBOS_HUB_CLIENT_ID=... export CERBOS_HUB_CLIENT_SECRET=... export CERBOS_HUB_STORE_ID=... ``` The following command uploads policy files from the current directory and replaces all the files in the store. ### [](#%5Fvia%5Fglobal%5Fnpm%5Finstallation)via global NPM installation ```sh cerbosctl hub store replace-files . ``` ### [](#%5Fvia%5Fnpx)via npx ```sh npx cerbosctl hub store replace-files . ``` ## [](#%5Ffull%5Fcli%5Freference)Full CLI Reference ```none Usage: cerbosctl hub store --store-id=STRING --client-id=STRING --client-secret=STRING [flags] Interact with Cerbos Hub managed stores. Requires an existing managed store and the API credentials to access it. The store ID and credentials can be provided using either command-line flags or environment variables. Flags: -h, --help Show context-sensitive help. --store-id=STRING ID of the store to operate on ($CERBOS_HUB_STORE_ID) --client-id=STRING Client ID of the access credential ($CERBOS_HUB_CLIENT_ID) --client-secret=STRING Client secret of the access credential ($CERBOS_HUB_CLIENT_SECRET) Commands: hub store list-files --store-id=STRING --client-id=STRING --client-secret=STRING [flags] List store files hub store get-files --store-id=STRING --client-id=STRING --client-secret=STRING --output-path=STRING ... [flags] Download files from the store hub store download --store-id=STRING --client-id=STRING --client-secret=STRING [flags] Download the entire store hub store replace-files --store-id=STRING --client-id=STRING --client-secret=STRING [flags] Overwrite the store with the given set of files hub store add-files --store-id=STRING --client-id=STRING --client-secret=STRING ... [flags] Add files to the store hub store delete-files --store-id=STRING --client-id=STRING --client-secret=STRING ... [flags] Delete files from the store ``` Policy stores: file rules and limits ==================== A policy store accepts only policy, schema, test suite, and test data files, and enforces size and count limits on every upload. The other files a policy repository ordinarily contains, such as CI configuration, documentation, and hidden files, are rejected or skipped according to the rules below. ## [](#%5Faccepted%5Ffiles)Accepted files Every file is checked against these rules when you upload: Extensions Only `.json`, `.yaml`, and `.yml` are accepted (the check is case-insensitive). Any other extension is unsupported: a `README.md`, `Makefile`, or `.go` file is not a store file. Hidden paths Any path with a component beginning with a dot is rejected, so `.git/`, `.github/`, and files such as `.gitlab-ci.yml` never enter the store. Schemas Files under `_schemas/` must be JSON. A YAML file under `_schemas/` is rejected. Test data Files under `testdata/` must be named `principals`, `resources`, or `auxdata` (the spellings `auxData` and `aux_data` are also accepted), with any accepted extension. Any other filename under `testdata/` is rejected. Test suites A file whose name ends in `_test` before the extension, such as `album_test.yaml`, is a test suite. Policies Every other accepted file is a policy. A test suite named `tests.yaml` is therefore read as a policy and rejected as malformed. | | There is no .cerbosignore or ignore-file mechanism. A file is accepted or rejected by exactly the rules above. | | ----------------------------------------------------------------------------------------------------------------- | ## [](#%5Ftests%5Fand%5Fthe%5Fruntime%5Fbundle)Tests and the runtime bundle Keep tests and test data in the store alongside the policies they exercise. Test suites and `testdata/` fixtures are first-class store content, and Cerbos Hub runs them automatically. * **Tests run when a deployment builds a bundle, not when you upload.** Upload validates only that each file parses as a policy, schema, test suite, or fixture. The suites are executed later, when a deployment that references the store builds a bundle. A failing test blocks that bundle, and the previously built bundle stays active. * **Test files never reach the runtime bundle.** The bundle builder excludes test suites and `testdata/`, so including them in the store costs nothing at runtime and you do not need to strip them before uploading. ## [](#%5Fuploading%5Fwith%5Fthe%5Fcli)Uploading with the CLI The `cerbosctl hub store` commands operate on a store’s files: | Command | Purpose | | ------------- | --------------------------------------------------------------------- | | upload-git | Apply the file changes between two commits of a local Git repository. | | replace-files | Overwrite the whole store with the given set of files. | | add-files | Add or update specific files. | | delete-files | Remove specific files. | | get-files | Download specific files. | | list-files | List the files in the store. | | download | Download the entire store. | `upload-git` works from committed history, not the working tree, so uncommitted edits are never uploaded. With no arguments it applies the changes between the commit recorded in the store’s current version and `HEAD`, and records the new commit details in the store. Use `--from` and `--to` to select an explicit commit range. When the store has no recorded commit details, `upload-git` replaces the store with the files at the target commit instead. `replace-files` accepts a directory, a `.zip` archive, or `-` to read a zip from standard input: ```sh cerbosctl hub store replace-files ./policies cerbosctl hub store replace-files policies.zip cat policies.zip | cerbosctl hub store replace-files - ``` Given a directory, `replace-files` zips the entire tree and uploads it, and the store classifies and filters the files on receipt. `add-files` instead filters as it walks the directory, dropping hidden and unsupported files before upload. ### [](#%5Fhow%5Fuploads%5Fhandle%5Frejected%5Ffiles)How uploads handle rejected files The two bulk commands differ in how they treat a file that breaks a rule: replace-files A file with an unsupported extension, a hidden path, or a bad `testdata`/`_schemas` name is **skipped** and returned in an ignored-files list. The upload fails only when no usable files remain (`no usable files`). This makes it safe to point at a whole repository. add-files A rejected file fails the **entire batch**, and nothing is uploaded. The exception is the directory walk described above: hidden and unsupported files are dropped locally and never become part of the batch. Bad `testdata` and `_schemas` names are not checked locally, so they reach the API and do fail the batch. A file that is otherwise valid but has invalid **contents** (a malformed policy, an invalid schema, or an invalid test suite, reported as `test suite is invalid`) is a hard failure in both commands. Content errors are never skipped. ## [](#%5Flimits)Limits Uploads are bounded by these limits: | Limit | Value | Applies to | | -------------------- | --------------- | ----------------------- | | Zipped upload size | 15 MiB | replace-files | | File size | 5 MiB | every file | | Total extracted size | 50 MiB | a replace-files archive | | Path length | 1024 characters | every file path | | Operations per batch | 25 | add-files, delete-files | | Files per request | 10 | get-files | Cerbos Hub GitHub Integration ==================== The Cerbos Hub GitHub integration allows you to manage your policies in a GitHub repository. This integration supports both public and private repositories, enabling you to store your policies securely and manage them using Git workflows. ## [](#%5Fprerequisites)Prerequisites Before you can use the Cerbos Hub GitHub integration, you need to have the following: * A GitHub account. * A GitHub repository where you want to store your policies. * Permission to add a GitHub App to your repository. ## [](#%5Fsetting%5Fup%5Fthe%5Fgithub%5Fintegration)Setting Up the GitHub Integration To set up the GitHub integration, follow these steps: 1. Go to [Cerbos Hub](https://hub.cerbos.cloud) and sign in. 2. Inside a workspace, create a new policy store by clicking on "Policy Stores" in the sidebar. 3. In the Import tab, select "GitHub" as the source for your policy store. 4. Follow the prompts to authorize the Cerbos Hub to access your GitHub account. 5. Select the repository you want to use for storing your policies. 6. Configure the branch or tag for the integration to track, and optionally a directory where your policies will be stored.![GitHub connection setup](_images/policy_store_github_connection_setup.png) 7. Click "Save" to complete the setup. ## [](#%5Fgithub%5Forganization%5Fip%5Fallow%5Flists)GitHub organization IP allow lists If your GitHub organization has an IP allow list enabled, you must permit the addresses Cerbos Hub connects from. Otherwise connecting the repository fails, and any store that is already connected stops syncing, with GitHub rejecting the request with a `403` response. The current addresses are `34.65.34.39` and `34.65.140.62`. Cerbos Hub also shows them while you connect a repository — both when installing the GitHub App and when choosing the repository — so you can copy them without leaving the flow. To manage your allow list automatically, fetch the addresses from [hub.cerbos.cloud/meta](https://hub.cerbos.cloud/meta), which returns them as an `egressIps` array: ```json {"egressIps":["34.65.34.39","34.65.140.62"]} ``` | | Treat that endpoint as the source of truth rather than hard-coding the addresses into your own configuration. | | ---------------------------------------------------------------------------------------------------------------- | | | This allow list is unrelated to the [IP allowlist on an ePDP rule](deployments-epdp-rules.html#%5Fip%5Fallowlist), which controls who may download embedded PDP bundles from Cerbos Hub. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#%5Fsyncing%5Fa%5Fsubdirectory)Syncing a subdirectory If your policies are stored in a subdirectory of your repository, you can configure the store to sync only that path. This is useful when your authorization policies are part of a larger monorepo or when you want to organize policies into separate directories. When configuring the GitHub connection, specify the path to the directory containing your policies in the directory field. Cerbos Hub will only sync files from that directory and its subdirectories. You can also sync hidden directories (those starting with a dot) if needed. For example, if your repository structure looks like this: my-repo/ ├── src/ ├── docs/ └── policies/ └── cerbos/ ├── resource_policies/ └── derived_roles/ You would set the directory to `policies/cerbos` to sync only the Cerbos policy files. ## [](#%5Fusing%5Fthe%5Fgithub%5Fintegration)Using the GitHub Integration Once the GitHub integration is set up, you can monitor and manage your policies directly in the GitHub connection tab. The integration will automatically sync changes made to the policies in your GitHub repository. ![GitHub connection status](_images/policy_store_github_connection.png) To reconfigure the GitHub integration, you can click on the "Update configuration" button in the GitHub connection tab. This allows you to change the repository, branch, or directory settings. Policy stores: .NET SDK ==================== The .NET SDK for policy stores allows you to interact with policy stores programmatically using .NET. This SDK provides a set of functions and types that make it easy to upload, manage, and retrieve policies from a policy store. Find the .NET SDK on GitHub: Policy stores: Go SDK ==================== The Go SDK for policy stores allows you to interact with policy stores programmatically using Go. This SDK provides a set of functions and types that make it easy to upload, manage, and retrieve policies from a policy store. Find the Go SDK on GitHub: Policy stores: Java SDK ==================== The Java SDK for policy stores allows you to interact with policy stores programmatically using Java. This SDK provides a set of functions and types that make it easy to upload, manage, and retrieve policies from a policy store. Find the Java SDK on GitHub: Policy stores: JavaScript @cerbos/hub SDK ==================== The `@cerbos/hub` JavaScript SDK for policy stores allows you to interact with policy stores programmatically using JavaScript/TypeScript. This SDK provides a set of functions and types that make it easy to upload, manage, and retrieve policies from a policy store. Find the JavaScript SDK on GitHub: Policy stores: PHP SDK ==================== The PHP SDK for policy stores allows you to interact with policy stores programmatically using PHP. This SDK provides a set of functions and types that make it easy to upload, manage, and retrieve policies from a policy store. Find the PHP SDK on GitHub: Policy stores: Python SDK ==================== The Python SDK for policy stores allows you to interact with policy stores programmatically using Python. This SDK provides a set of functions and types that make it easy to upload, manage, and retrieve policies from a policy store. Find the Python SDK on GitHub: Policy stores: Rust SDK ==================== The Rust SDK for policy stores allows you to interact with policy stores programmatically using Rust. This SDK provides a set of functions and types that make it easy to upload, manage, and retrieve policies from a policy store. Find the Rust SDK on GitHub: Policy stores: Browser Upload ==================== The browser upload feature allows you to manually upload policy files directly to a policy store using the Cerbos Hub web interface. You can quickly add policies by selecting a ZIP file or by dragging and dropping the file into the designated area, without using a Git repository or CI/CD pipeline. ## [](#%5Fuploading%5Fpolicies)Uploading Policies ![Browser upload](_images/policy_store_upload.png) To upload policies using the browser, follow these steps: 1. Go to [Cerbos Hub](https://hub.cerbos.cloud) and sign in. 2. Inside a workspace, create a new policy store by clicking on "Policy Stores" in the sidebar. 3. In the Import tab, select "Browser upload" as the source for your policy store. 4. Click on the "Upload files" button to select a ZIP file of your policies or drag and drop the ZIP file into the designated area. 5. Once the file is uploaded, the policies will be processed and added to the policy store. 6. You can then view and manage the uploaded policies in the policy store Policies tab. | | Uploading policies via the browser does a full replace of the existing policies in the store. If you want to append or update specific policies, consider using a Git repository or CI/CD pipeline instead. | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Policy stores ==================== Policy stores are flexible containers for Cerbos policy files managed inside Cerbos Hub. A store decouples policy storage from any specific source control system, letting each team choose the workflow that best fits its needs while Cerbos Hub guarantees validation, versioning, and secure delivery to [deployments](deployments.html). ## [](#%5Fwhy%5Fuse%5Fpolicy%5Fstores)Why use policy stores Clear boundaries and ownership Create one store per team, product, tenant, or environment so each group owns just the policies that matter to them, reducing cognitive load. Independent update cadence Teams can update their store at any time without blocking others, a new deployment is built only when you choose to combine the stores. Layered policy logic Combine multiple stores in a single deployment to apply global guard rails, platform defaults, application-level rules, and tenant overrides in a predictable hierarchy. Source agnostic workflows Populate a store from Git, a CI pipeline, the Cerbos Hub API, the [cerbosctl CLI](../cerbos/latest/cli/cerbosctl.html), or a direct UI upload, no GitHub lock-in. Full visibility and auditability View every policy file in Hub, see which store and commit contributed to a deployment, and trace any PDP decision back to the exact policy version. ## [](#%5Fsupported%5Fingestion%5Fmethods)Supported ingestion methods | Method | Typical use case | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Git repository | Structured policy as code managed with pull requests and reviews. If you are using GitHub, you can connect your repository directly to a policy store in Cerbos Hub. | | CI or CD pipeline | Push the policies produced by a build job, for example when generating service-specific policies or promoting between environments. | | Cerbos Hub SDKs | Programmatic updates such as per-tenant roles, user-defined permissions, or event-driven changes. | | cerbosctl CLI | Local scripting, quick one-off uploads, or integration into existing tooling. | | Browser upload | Ad hoc tweaks, demos, or importing legacy policy files. | ## [](#%5Flife%5Fcycle%5Finside%5Fcerbos%5Fhub)Life-cycle inside Cerbos Hub 1. Create a store in **Cerbos Hub** workspace. 2. Add or update policy files using any supported ingestion method. 3. Automatic validation ensures policies are correctly formatted. 4. Reference the store in one or more deployments to build a versioned bundle. ## [](#%5Fstore%5Fdetail%5Fpage)Store detail page Each policy store has a detail page with the following tabs: Import Add policies to the store by uploading files, connecting to a Git repository, or using other ingestion methods. Policies Browse the current contents of the store in a file tree. Select a version from the dropdown to view historical snapshots. Click **Create playground** to experiment with the policies in an isolated environment—this creates a playground pre-populated with all policies and tests from the store. Version history View a chronological list of all versions, showing when each was created and what changed. Client credentials Manage API credentials scoped to this store for programmatic access via SDKs or the CLI. Each credential shows its type (read-only or read and write), usage statistics, and when it was last used. Settings Configure store options and manage connections to external sources like GitHub. ## [](#%5Fbest%5Fpractices)Best practices Use meaningful names Name stores after their responsibility, for example security-global, platform-core, payments-service, or tenant-alpha. Keep tests with policies store-specific tests catch regressions early and run automatically on every change. Protect production stores Restrict write access and require review for stores that feed your production deployment. Isolate dynamic inputs Create a dedicated store for API-driven or user-defined policies to avoid mixing static and dynamic files. Review store composition Periodically confirm that each deployment references only the stores it needs and in the correct order. Release notes ==================== ## [](#%5F2026%5F09%5F08)2026-09-08 ### [](#%5Fa%5Frefreshed%5Flook%5Ffor%5Fthe%5Fcerbos%5Fhub%5Fconsole)A refreshed look for the Cerbos Hub console The Cerbos Hub console has been restyled throughout. Colours, typography and spacing now come from a single design system, so buttons, forms, tables and dialogs are consistent across the console. A navigation header runs across the top of every page, holding the organization switcher, the help menu and your account menu, and sidebars can be collapsed to give the content more room. Page structure and functionality are unchanged, so existing links and bookmarks continue to work. ### [](#%5Flight%5Fand%5Fdark%5Fthemes%5Ffor%5Fthe%5Fconsole)Light and dark themes for the console The console can now be displayed in a light or a dark theme. Open the account menu at the top right of any page and choose **Light theme**, **Dark theme**, or **System theme**, which follows the setting of your operating system. The preference is stored in your browser, so it does not carry over to other browsers or devices, and it does not affect other members of your organization. ## [](#%5F2026%5F08%5F25)2026-08-25 ### [](#%5Ffreeze%5Fa%5Fdeployment%5Fand%5Froll%5Fback%5Fto%5Fan%5Fearlier%5Fversion)Freeze a deployment and roll back to an earlier version You can now hold a [deployment](deployments.html#%5Ffreezing%5Fand%5Frolling%5Fback) on a known-good version of your policies instead of always tracking the latest build. Freezing a deployment from its **Settings** tab keeps whatever is currently live in place: policy changes pushed to the linked stores are no longer built and delivered to connected PDPs until you unfreeze it. Frozen deployments are labelled in the deployment list and on the deployment page itself, so it is clear at a glance why new changes are not going out. The list of deployed versions now also offers a **Roll back** action on any earlier version and a **Promote** action on any newer one. Either choice makes that version live on connected PDPs immediately and freezes the deployment, so the version you picked stays put until you decide to resume. Unfreezing builds and deploys the latest version of your policies again, replacing whatever was pinned. Both freezing and rolling back are available to workspace Owners and Developers. ### [](#%5Flimit%5Fon%5Fhow%5Fmany%5Faudit%5Flog%5Fexports%5Fcan%5Fbe%5Fstarted%5Fper%5Fhour)Limit on how many audit log exports can be started per hour Cerbos Hub now limits how many [audit log exports](audit-log-collection.html#%5Fexporting%5Flogs) can be started from a workspace within an hour. When the limit is reached, starting another export fails and nothing is added to the queue. An export stops counting towards the limit an hour after it was created, so you can start more once the earlier ones fall outside that window. Exports that are already queued or in progress are not affected. ## [](#%5F2026%5F08%5F17)2026-08-17 ### [](#%5Fstrict%5Fevaluation%5Fsetting%5Ffor%5Fplaygrounds)Strict evaluation setting for playgrounds The Settings tab of a [playground](playground.html#%5Fplayground%5Fengine%5Fsettings) now has a **Strict evaluation** toggle, mirroring the [same setting](../cerbos/latest/configuration/engine.html#strict%5Fevaluation) on a PDP. By default, an error raised while evaluating a condition or a variable leaves the expression unsatisfied and evaluation carries on, so a rule that would have denied an action is skipped. With strict evaluation enabled, those errors deny the affected action instead, so a broken condition surfaces while you are still writing the policy. The toggle applies to the [Explore tab](playground.html#%5Fexplore%5Ftab) and the [Try the API](playground.html#%5Ftry%5Fthe%5Fapi) section, so you can compare both behaviours against the same policies. Test files are not affected, because each test suite and test case carries its own `options.strictEvaluation` setting. ### [](#%5Fplayground%5Fsettings%5Fare%5Fno%5Flonger%5Freset%5Fwhen%5Fa%5Fplayground%5Floads)Playground settings are no longer reset when a playground loads Settings you save on the Settings tab of a [playground](playground.html#%5Fplayground%5Fengine%5Fsettings) (the default policy version, globals, lenient scope search, and strict evaluation) now survive reopening it. Previously the built-in defaults could be applied over the top of your saved values while the playground was loading, reverting your configuration. Saved values now take precedence, and defaults are only filled in for settings you have never set. ## [](#%5F2026%5F08%5F12)2026-08-12 ### [](#%5Fpolicy%5Flinks%5Fin%5Faudit%5Flog%5Fentries%5Fopen%5Fthe%5Fright%5Ffile)Policy links in audit log entries open the right file The policy names shown in the details of an [audit log](audit-log-collection.html) entry now link straight to that policy file in the deployment that made the decision, with the correct build already selected. Previously the link could land on the deployment’s Policies tab without opening the file, leaving you to find it yourself. ## [](#%5F2026%5F08%5F11)2026-08-11 ### [](#%5Fexport%5Faudit%5Flogs%5Ffrom%5Fcerbos%5Fhub)Export audit logs from Cerbos Hub You can now take your decision and access logs out of Cerbos Hub for long-term archiving or analysis in your own tooling. Press **Export** on the [Audit logs](audit-log-collection.html#%5Fexporting%5Flogs) page and the export is queued in the background using the time range and filters you already have applied, so you get exactly the entries you were looking at. A new **Exports** tab lists every export with its status, the filters it used and its expiry date, and offers a download button as soon as it is ready. Exporting is available to workspace and organization Owners. Exports are encrypted with [age](https://age-encryption.org) before they leave Cerbos Hub. You can supply your own public key, or let Cerbos Hub generate a key pair for you and save it when the export starts. The secret key is never retained by Cerbos Hub, so store it somewhere safe. Expanding a row on the Exports tab shows the exact command to decrypt and read the file, which contains your log entries as gzip-compressed JSON Lines. Completed exports remain available to download for seven days, and a failed export can be retried with the same filters in one click. ### [](#%5Fclearer%5Fpolicy%5Fcompilation%5Ferrors%5Fin%5Fplaygrounds)Clearer policy compilation errors in playgrounds Compilation problems in a [playground](playground.html) are now reported where they actually occur. Each error is marked inline on the offending line in the editor, and the [Problems](playground.html#%5Fproblems) panel shows its line and column alongside a fuller description, so you can click through to the exact source rather than hunting for it. Several classes of problem that were previously reported vaguely, or not at all, are now surfaced properly. These include invalid expressions in conditions, undefined or redefined variables and constants, unknown and ambiguous derived roles, and problems with scopes and imports. Where a diagnostic relates to a documented concept, it links straight to the relevant Cerbos policy documentation. ### [](#%5Fschema%5Fvalidation%5Ferrors%5Fshown%5Fin%5Fplaygrounds)Schema validation errors shown in playgrounds If a principal or resource fixture does not satisfy the schema attached to your policy, the [Explore tab](playground.html#%5Fschema%5Fvalidation) now says so directly beneath the fixture selector. Previously a schema violation would quietly influence the outcome, leaving you to work out why a check did not behave as expected. Actions affected by the failure are left in an undetermined state until the error is resolved, making it clear that the result is a fixture problem rather than a policy decision. Validation errors on the request itself are reported the same way, above the action results. ## [](#%5F2026%5F07%5F27)2026-07-27 ### [](#%5F2026%5F07%5F27)Cerbos Hub egress IP addresses shown when connecting a GitHub repository If your GitHub organization restricts access with an IP allow list, the [GitHub connection flow](policy-stores-git-github.html#%5Fgithub%5Forganization%5Fip%5Fallow%5Flists) now tells you which IP addresses Cerbos Hub connects from, so you can add them without leaving the wizard. The addresses are shown both when installing the GitHub App and when picking a repository, which means you see them even if the app is already installed. They are also published in machine-readable form at [hub.cerbos.cloud/meta](https://hub.cerbos.cloud/meta), so you can keep your allow list up to date automatically. ## [](#%5F2026%5F07%5F16)2026-07-16 ### [](#%5F2026%5F07%5F16)Download the policies in a deployment The Policies tab of a [deployment](deployments.html#%5Fdownloading%5Fpolicies) now has a **Download policies** button that gives you the policy files from the deployed build as a zip archive. The archive groups the files into a folder per contributing store, so you get an exact copy of what the deployment is running and can tell where each policy came from. This is useful for reviewing a build offline, comparing it against your repository, or keeping a record of what was deployed. ## [](#%5F2026%5F07%5F13)2026-07-13 ### [](#%5F2026%5F07%5F13)Issues surfaced for your workspaces The issues bar in Cerbos Hub now flags problems with your deployment workspaces, so you can catch and fix them without digging through settings. It surfaces blocked API keys, deployments that have been mapped to a new store but have not activated a new build for 15 minutes, and stores that are failing to sync with GitHub. Each issue links through to the relevant page for the details. ## [](#%5F2026%5F07%5F08)2026-07-08 ### [](#%5F2026%5F07%5F08)Audit log entries with empty values now display correctly Fixed an issue where an audit log entry containing an empty (null) value could fail to load. These entries now render correctly, so no decision is missing from your audit trail. ## [](#%5F2026%5F06%5F30)2026-06-30 ### [](#%5F2026%5F06%5F30)Effect matrix view for deployment policies The Policies tab of a [deployment](deployments.html) now has a **Source** / **Effect matrix** toggle. The effect matrix lays out a resource’s roles and actions as a grid and shows, at a glance, whether each role-and-action combination is allowed, denied, or conditional in the deployed bundle, so you can review effective permissions without reading through every policy file. Cells also flag where a wildcard rule (such as or `foo:`) widens the match, and you can filter the grid and drill into a cell to see the underlying rules. ### [](#%5Fdistribution%5Flicence%5Fkeys%5Ffor%5Fcerbos%5Fsynapse)Distribution licence keys for Cerbos Synapse Organizations with [Cerbos Synapse](../synapse/latest/index.html) access can now manage their [distribution licence key](distribution-licence.html) from organization settings. A licence key authenticates your Synapse instances with the Cerbos distribution registry so they can pull updates, used as the password for `docker login` against the registry endpoint shown in Hub. You can generate a key, rotate it with a configurable grace period (up to 12 hours) during which the previous key keeps working, and revoke it immediately. Synapse is enabled per organization; [contact the Cerbos team](https://cerbos.dev/contact) for access. ## [](#%5F2026%5F06%5F18)2026-06-18 ### [](#%5F2026%5F06%5F18)Output errors in test results, the playground, and audit logs Policy output errors now show up wherever you inspect a decision: in test results, in the playground’s Explore tab, and in the audit log entry slideout. When an output expression fails to evaluate you see the error inline instead of an empty result, which makes output blocks much easier to debug. ### [](#%5Finsights%5Fand%5Fusage%5Faccess%5Faligned%5Fwith%5Faudit%5Flogs)Insights and Usage access aligned with audit logs Access to the [Insights](insights.html) and Usage pages now follows the same permissions as audit logs, so they are available to workspace Owners and Analysts. This keeps every view of your decision data governed by one consistent permission. ### [](#%5Fwider%5Faudit%5Flogs%5Fand%5Finsights%5Ftables)Wider audit logs and Insights tables The audit logs page no longer caps its width, so tables can use the full width of large displays. Tables on the Insights page now scroll horizontally as well, so long action names are no longer truncated. ### [](#%5Faudit%5Flog%5Ffixes)Audit log fixes Fixed several audit log issues: the **Jump to latest** button now reliably reloads the most recent entries, scroll position is kept when paging back through earlier entries, and the table no longer ends up empty after a filter is added and then removed. ## [](#%5F2026%5F05%5F26)2026-05-26 ### [](#%5F2026%5F05%5F26)Navigation reflects your permissions Organization and workspace navigation now hides entries for areas you do not have access to, so the menus show only the pages you can actually open. ### [](#%5Fcorrect%5Fpolicy%5Fversion%5Fin%5Faudit%5Flogs)Correct policy version in audit logs Fixed the policy version shown on audit log entries, which was being read from the wrong field in the decision summaries data. Entries now display the correct policy version. ## [](#%5F2026%5F05%5F20)2026-05-20 ### [](#%5F2026%5F05%5F20)Insights The new Insights page gives each workspace an at-a-glance view of the authorization decisions flowing through it. It charts allowed and denied decisions by hour and by day, tracks daily active principals, and ranks the most active principals, resource kinds, and resource-and-action pairs. Insights is built from the decision data your PDPs already send to Cerbos Hub, so there is nothing extra to configure, and it is available to workspace members with audit log access once [audit log collection](audit-log-collection.html) is enabled. See [Insights](insights.html) for the full breakdown. ## [](#%5F2026%5F05%5F11)2026-05-11 ### [](#%5F2026%5F05%5F11)Improved keyboard navigation Organization list items now show a visible focus outline when navigated with the keyboard, making it easier to track your position when moving through the list without a mouse. ## [](#%5F2026%5F04%5F30)2026-04-30 ### [](#%5F2026%5F04%5F30)Cerbos policy skill in the playground The [playground](playground.html) now highlights the [Cerbos policy skill](#%5Fcerbos%5Fpolicy%5Fskill%5Ffor%5Fcoding%5Fagents), the agent skill that helps coding agents write Cerbos authorization policies. A new promo box points you to it while you are authoring policies. ## [](#%5F2026%5F04%5F21)2026-04-21 ### [](#%5F2026%5F04%5F21)Cerbos policy skill for coding agents The Cerbos policy skill is a new agent skill that generates complete authorization policies from plain-language descriptions. Describe your access requirements in everyday language and the skill produces schemas, derived roles, resource policies, and test suites in a single pass, then validates its own output and fixes any errors it finds. It works with Claude Code, Cursor, VS Code, Codex, and other coding agents, and follows least-privilege, deny-by-default patterns drawn from production deployments, which makes it especially useful for getting started without picking up bad habits. Install it with `npx skills add cerbos/skills --skill cerbos-policy`, or read more on the [Cerbos blog](https://www.cerbos.dev/blog/agent-skill-for-writing-authorization-policies). ## [](#%5F2026%5F04%5F14)2026-04-14 ### [](#%5F2026%5F04%5F14)Audit log filtering by PDP and policy source Audit logs can now be filtered by PDP instance ID and policy source. This makes it easier to isolate authorization decisions from specific PDP deployments or trace decisions back to the policy store that produced them. The audit logs page no longer applies a default three-month time filter when opened. You now see the full range of available logs by default, making it faster to start exploring recent decisions without adjusting filters first. ## [](#%5F2026%5F04%5F08)2026-04-08 ### [](#%5F2026%5F04%5F08)Deployment and table UI fixes Fixed broken tabs on the deployment detail page and resolved an issue where tables could scroll past their bounds on some browsers. ## [](#%5F2026%5F03%5F31)2026-03-31 ### [](#%5F2026%5F03%5F31)Organization search The organization list page now includes a search field, letting you quickly find organizations by name. This is particularly useful for users who belong to many organizations. ### [](#%5Fplayground%5Fsave%5Fperformance)Playground save performance Saving playgrounds is now significantly faster. File contents are only transmitted when they have actually changed, reducing latency when saving large playgrounds with many policy files. ## [](#%5F2026%5F03%5F10)2026-03-10 ### [](#%5Fbuild%5Fpage%5Flive%5Fupdates)Build page live updates The build detail page now polls automatically while a build is queued or in progress, keeping the status up to date without a manual refresh. ## [](#%5F2026%5F03%5F05)2026-03-05 ### [](#%5Faudit%5Flog%5Ftimeline)Audit log timeline The [audit logs](audit-log-collection.html) page now includes an interactive timeline visualization. When no time filter is applied, it defaults to the last three months; otherwise it reflects the active time range. Click individual bars to zoom into a time range, or drag to select a custom range. Bucket and tick intervals adjust dynamically based on the selected range. ## [](#%5F2026%5F02%5F24)2026-02-24 ### [](#%5Fbundler%5Fspecific%5Fepdp%5Fdeploy%5Fsnippets)Bundler-specific ePDP deploy snippets The [embedded PDP](deployments-epdp-rules.html) deploy modal now displays tailored code snippets for each bundler: Vite, Webpack, Rspack, Next.js (Turbopack), and Node.js. Each tab shows the correct WASM loading pattern for that environment. ## [](#%5F2026%5F02%5F17)2026-02-17 ### [](#%5Ftailscale%5Fintegration)Tailscale integration Cerbos Hub now integrates with Tailscale Aperture. A dedicated landing page and onboarding flow guides you through linking your tailnet to a new workspace, configuring your [PDP deployment](deployments.html), and verifying the connection. Invited users can join Tailscale-linked workspaces directly, and the configuration remains accessible after initial setup for managing multiple deployments. ### [](#%5Fnavigation%5Fand%5Fcopy%5Fupdates)Navigation and copy updates The workspace sidebar now labels the support link as "Documentation" for clarity, and a link to the pricing page has been added to the billing section. ## [](#%5F2026%5F01%5F28)2026-01-28 ### [](#%5Frbac%5Fpolicy%5Fgenerator%5Fimprovements)RBAC policy generator improvements The RBAC policy generator now displays role names more clearly by rotating the rule matrix headers, accommodating longer role names. Fixed a navigation issue in the wizard form that prevented users from going back to previous steps. ### [](#%5Fvariable%5Fexpression%5Fstability)Variable expression stability Fixed an issue where invalid variable expressions could cause unexpected errors. The system now handles malformed expressions gracefully, improving reliability when authoring policies. ## [](#%5F2026%5F01%5F27)2026-01-27 ### [](#%5Fembedded%5Fpdp%5Fdeploy%5Fhelper)Embedded PDP deploy helper A new deployment helper modal guides you through deploying embedded PDPs. The helper dynamically shows scope filters and client credentials based on your rule requirements. Rules requiring authentication cannot run in-browser, so the helper clearly indicates when server-side deployment is necessary. ### [](#%5Fplayground%5Freadme%5Fimprovements)Playground README improvements Enhanced the markdown viewer styling in the Playground. README content now updates in real-time when collaborators edit the file. Drag-and-drop file uploads now correctly handle markdown files. ## [](#%5F2026%5F01%5F26)2026-01-26 ### [](#%5Fnew%5Fembedded%5Fpdp%5Farchitecture)New embedded PDP architecture A completely redesigned [embedded policy decision point (ePDP)](deployments-epdp-rules.html) system replaces the previous implementation. The new architecture introduces ePDP rules, which provide fine-grained control over policy bundles served to client applications. Key capabilities: * **Per-deployment rules**: Each deployment can define multiple ePDP rules, allowing different bundle configurations for different clients or environments * **Policy filtering**: Specify which resources, actions, scopes, roles, and versions to include, reducing bundle size and limiting exposure of server-side authorization logic * **Dynamic scope filtering**: Require clients to specify scopes at fetch time, enabling multi-tenant architectures where each tenant receives only their applicable policies * **Access controls**: Configure authentication requirements (public access or client credential) and IP allowlists per rule A new JavaScript SDK (`@cerbos/embedded-client`) replaces the previous `@cerbos/embedded` package, with support for automatic bundle updates, deferred activation, and improved error handling. See the [SDK documentation](deployments-epdp-rules.html#%5Fjavascript%5Fsdk) for migration details. ## [](#%5F2026%5F01%5F21)2026-01-21 ### [](#%5Fpermissions%5Fmatrix%5Fview)Permissions matrix view The playground now includes a matrix check view that displays permission evaluation results in a comprehensive grid format. This makes it easier to visualize how policies affect different principal and resource combinations during demos and policy development. ### [](#%5Fplayground%5Freadme%5Fsupport)Playground README support Playgrounds now support README.md files that display as rendered markdown when opened. READMEs can include relative links to other files in the playground and external links (with a confirmation dialog). When opening a playground with a README and no file selected, the README displays automatically, making it easier to document and share playground examples. == 2026-01-05 ### [](#%5Fcreate%5Fplayground%5Ffrom%5Fa%5Fstore)Create playground from a store You can now create a playground directly from an existing policy store, making it easy to experiment with production policies in a safe, isolated environment. This feature streamlines the workflow for testing policy changes before deploying them, allowing you to iterate quickly without affecting live authorization decisions. You can also use a store’s policies as a starting point for developing new authorization rules, reducing the time needed to build and validate complex policy structures. ## [](#%5F2025%5F12%5F23)2025-12-23 ### [](#%5Faudit%5Flog%5Fdeep%5Flinking)Audit log deep linking Click on a policy referenced in an audit log entry to navigate directly to that policy in your policy store. This makes it easier to investigate authorization decisions and understand the policy logic that led to a specific outcome. ### [](#%5Fplayground%5Fzip%5Ffile%5Fsupport)Playground zip file support You can now drag and drop zip files containing policies and tests directly into the playground. This simplifies loading entire policy sets for testing and collaboration. ## [](#%5F2025%5F12%5F01)2025-12-01 ### [](#%5Ftest%5Fcase%5Foutput%5Fdiff%5Fview)Test case output diff view When a test case fails, you can now view a side-by-side diff comparing the expected output with the actual output. This visual comparison makes it significantly easier to identify exactly what went wrong—whether it’s a missing permission, an unexpected denial, or an incorrect output value. By highlighting the specific differences, you can quickly understand the root cause and fix failing tests with confidence. ### [](#%5Fexecution%5Ftraces%5Fin%5Fplayground%5Fexplore%5Ftab)Execution traces in playground explore tab Execution traces are now available directly in the playground’s explore tab, providing real-time visibility into how Cerbos evaluates your authorization requests. As you experiment with different principals, resources, and actions, you can inspect the complete policy evaluation path to understand exactly how decisions are made. This makes it easier to debug complex policy logic and verify that your authorization rules behave as expected before deploying them. ### [](#%5Faccessibility%5Fimprovements)Accessibility improvements Icon-only buttons throughout the interface now have proper accessibility labels, ensuring screen reader users can navigate and interact with all controls effectively. Additionally, identifiers such as policy names, resource kinds, and action names are now displayed using a fixed-width font, improving readability and making it easier to scan lists of technical values. ## [](#%5F2025%5F11%5F06)2025-11-06 ### [](#%5Fcombobox%5Finput%5Fimprovements)Combobox input improvements Fixed an issue with combobox components when using multiselect and free-input modes together. This resolves problems that could occur when selecting multiple values while also entering custom text, ensuring a smoother form input experience throughout the Hub interface. ## [](#%5F2025%5F10%5F27)2025-10-27 ### [](#%5Fstore%5Fimport%5Fwizard%5Ffix)Store import wizard fix Fixed an issue that prevented the store import wizard from completing successfully in certain scenarios. The wizard now reliably guides you through importing policies from external sources, ensuring a smooth onboarding experience when migrating existing authorization rules into Cerbos Hub. ## [](#%5F2025%5F10%5F21)2025-10-21 ### [](#%5Fexpired%5Finvitation%5Fhandling)Expired invitation handling The system now properly handles expired invitations, displaying clear messages when users attempt to accept an invitation that is no longer valid. This prevents confusion and guides users to request a new invitation from their organization administrator, streamlining the team onboarding process. ## [](#%5F2025%5F10%5F08)2025-10-08 ### [](#%5Fon%5Fpremises%5Fdeployment)On-premises deployment Cerbos Hub is now available for on-premises deployment, giving you full control over your authorization management platform while maintaining all the features of the cloud-hosted service. On-premises deployment is available for enterprise customers. [Contact us](https://cerbos.dev/contact) to learn more. ### [](#%5Fworkspace%5Fname%5Fvalidation)Workspace name validation Workspace names now require a minimum length, with immediate validation feedback in the creation form. This prevents issues that could arise from very short or ambiguous workspace names, helping teams maintain clear and consistent naming conventions across their organization. ### [](#%5Finvitation%5Fmanagement%5Fimprovements)Invitation management improvements Improved handling when inviting users who are already members of an organization. The system now prevents duplicate invitations and provides clearer error messages explaining why an invitation cannot be sent. This reduces confusion for administrators managing team access and ensures invitation workflows remain straightforward. ### [](#%5Fbuild%5Fstatus%5Ffix)Build status fix Fixed an issue where the wrong build status was displayed in the UI for certain policy stores. Build indicators now accurately reflect the current state of your policy compilation, giving you reliable feedback on whether your latest changes have been successfully processed. ## [](#%5F2025%5F09%5F09)2025-09-09 ### [](#%5Fplayground%5Fdrag%5Fand%5Fdrop%5Fsupport)Playground drag-and-drop support You can now drag-and-drop policy and test files from your local machine directly into the playground editor. This makes it easier to quickly load and test local policy files without needing to copy and paste. ## [](#%5F2025%5F09%5F01)2025-09-01 ### [](#%5Fpolicy%5Fexecution%5Ftraces)Policy execution traces Debugging complex authorization logic is now easier with policy execution traces. This feature provides a detailed, step-by-step view of how Cerbos evaluates a request, showing every rule, condition, and variable evaluation. Traces are available in the playground for real-time testing and in your deployment builds to provide production-level visibility, helping you quickly pinpoint why a specific decision was made. ### [](#%5Forganization%5Fusage%5Fdashboard)Organization usage dashboard A new organization-level usage dashboard is now available, aggregating authorization metrics from all your workspaces into a single view. This dashboard provides a complete picture of request volumes, policy distribution, and usage trends across your entire organization. It helps platform owners and engineering leaders understand adoption patterns, identify which teams might need support, and discover optimization opportunities. ## [](#%5F2025%5F08%5F04)2025-08-04 ### [](#%5Fusage%5Fdashboard)Usage dashboard The new [Usage Dashboard](usage-dashboard.html) provides comprehensive observability into your authorization service. It allows you to monitor key metrics, view trends, and gain insights into how your Cerbos Hub workspace is utilized. ### [](#%5Fapi%5Fkey%5Flifecycle)API key lifecycle Should an API key become blocked due to misconfiguration, you can now unblock it directly from the API key list. ## [](#%5F2025%5F07%5F17)2025-07-17 A major launch of the [Policy Stores](policy-stores.html) feature in Cerbos Hub, which allows you to store and manage your authorization policies more effectively. You can read more in the announcement blog post: [Updated Cerbos Hub](https://www.cerbos.dev/blog/updated-cerbos-hub-complete-authorization-solution-for-your-identity-fabrics). ## [](#%5F2025%5F07%5F09)2025-07-09 ### [](#%5Faudit%5Flogs%5Fgeneral%5Favailability)Audit logs general availability The audit logs feature is now generally available. The beta badge has been removed and the feature is ready for production use. ## [](#%5F2025%5F06%5F24)2025-06-24 ### [](#%5Fgithub%5Fbacked%5Fpolicy%5Fstores)GitHub-backed policy stores Policy stores can now be connected directly to GitHub repositories. Changes pushed to your repository are automatically synced to the store, and you can configure which branch and subdirectory to track. See [GitHub integration](policy-stores-git-github.html) for details. ## [](#%5F2025%5F06%5F03)2025-06-03 ### [](#%5Fimport%5Fpolicies%5Fwizard)Import policies wizard A new guided wizard helps you import existing policies into a managed policy store. Upload a zip file or connect to a Git repository to quickly populate a new store. ## [](#%5F2025%5F05%5F16)2025-05-16 ### [](#%5Fbundle%5Ffile%5Fexplorer)Bundle file explorer View the contents of policy bundles directly in Cerbos Hub. The build details page now includes a file explorer showing all policies included in the bundle and the ability to inspect individual files. ## [](#%5F2025%5F04%5F28)2025-04-28 ### [](#%5Forganization%5Fdeletion)Organization deletion You can now delete an organization in Cerbos Hub. To delete an organization, navigate to the organization settings page and click on the "Delete organization" button. Please note that this action is irreversible and will permanently delete all data associated with the organization. ## [](#%5F2025%5F03%5F19)2025-03-19 ### [](#%5Frole%5Fpolicy%5Fand%5Fconstants%5Ftemplates)Role policy and constants templates The playground template picker now includes examples for role policies and policy constants, making it easier to get started with these advanced policy features. ## [](#%5F2025%5F03%5F12)2025-03-12 ### [](#%5Fplayground)Playground The effective derived roles for a user are now displayed in the playground when evaluating policies. This feature helps you understand which derived roles were activated for that user during that request. ### [](#%5Fembedded%5Fpolicy%5Fdecision%5Fpoint)Embedded Policy Decision Point Time-based functions used in condition expressions such as `getHours` and `getMinutes` default to UTC unless the time zone is explicitly provided as an argument to the function. It’s recommended to review your policies to make sure that time calculations use the correct time zone. Refer to [timestamps documentation](../cerbos/latest/policies/conditions.html#%5Ftimestamps) to identify the affected functions. ## [](#%5F2025%5F02%5F26)2025-02-26 ### [](#%5Fembedded%5Fpolicy%5Fdecision%5Fpoint%5F2)Embedded Policy Decision Point We’ve introduced support for capturing audit decision logs from the Cerbos Hub Embedded Policy Decision Points (ePDP) using the latest version of the [Cerbos Javascript SDK](https://github.com/cerbos/cerbos-sdk-javascript). This feature enables organizations to track and analyze authorization decisions made locally in embedded environments, ensuring complete visibility and auditability, without relying on a centralized PDP or Cerbos Hub. ## [](#%5F2025%5F02%5F01)2025-02-01 The Builds section of Cerbos Hub has been renamed Policies. The Policies section now includes all the features previously available in Builds, such as policy versioning, policy history, and policy deployment. The Builds section has been removed from the Cerbos Hub navigation. ## [](#%5F2025%5F01%5F28)2025-01-28 ### [](#%5Fplayground%5F2)Playground Added support for [globals](../cerbos/latest/configuration/engine.html#%5Fglobals) in playground engine settings. Global variables defined in the [playground settings](playground.html) are exposed to policy conditions via the `globals` object. Reliability ==================== Cerbos Hub is designed for high availability. All PDPs continue to operate independently even when Cerbos Hub experiences disruptions. ## [](#network-requirements)Network requirements PDPs reach Cerbos Hub over outbound HTTPS on port 443\. Allow these hosts through any egress proxy or firewall: | Host | Port | Purpose | | ---------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------- | | api.cerbos.cloud | 443 | Bundle update stream and audit log ingest. Uses the Connect protocol over HTTP/2; the update stream is a long-lived HTTP/2 stream. | | cdn.cerbos.cloud | 443 | All bundle content downloads: the bootstrap bundle at startup, and every bundle delivered by a subsequent policy update. Plain HTTPS. | All connections to Cerbos Hub require TLS 1.3; a proxy that downgrades below TLS 1.3 is refused. The PDP honors the standard `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` environment variables. ## [](#%5Fpush%5Fbased%5Fupdates)Push-based updates When a PDP connects to Cerbos Hub, it establishes a two-way communication channel used to receive the initial policy bundle and subsequent update notifications. Because there is no polling, all PDPs converge on the same policy version within seconds of a change. ## [](#%5Fdisconnection%5Fhandling)Disconnection handling If Cerbos Hub becomes temporarily unavailable: | Running PDPs | Continue serving requests using the last downloaded bundle while attempting to reconnect in the background. Authorization decisions are unaffected. | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | | New PDPs | Can start with the last successfully built bundle, served from a separate high-availability fallback service. | ## [](#%5Flocal%5Fbundle%5Fcaching)Local bundle caching For additional resilience, configure a cache directory to persist bundles to disk: ```yaml storage: driver: hub hub: remote: deploymentID: "..." cacheDir: /var/cerbos/hub # Directory to cache downloaded bundles ``` Mount a persistent volume at this path when running in containers or Kubernetes. Caching avoids re-downloading an unchanged bundle on restart. It does not enable an offline start: a PDP still contacts Cerbos Hub at startup to fetch its bundle, and fails to start if Hub cannot be reached. ## [](#%5Ffallback%5Fto%5Fgit)Fallback to git As a last resort, switch the PDP to read policies directly from your Git repository: ```yaml storage: driver: git git: protocol: https url: https://github.com/your-org/policies.git branch: main checkoutDir: /tmp/cerbos/policies updatePollInterval: 60s ``` This bypasses Cerbos Hub entirely, though you lose pre-compilation, testing, and centralized management. ## [](#%5Fmonitoring%5Fconnectivity)Monitoring connectivity Monitor PDP connectivity using the `cerbos_dev_hub_connected` Prometheus metric: | 1 | PDP is connected to Cerbos Hub | | - | ----------------------------------------- | | 0 | PDP is disconnected (using cached bundle) | Additional metrics for bundle operations: | cerbos\_dev\_store\_bundle\_updates\_count | Number of bundle updates received from Cerbos Hub | | ------------------------------------------------ | ------------------------------------------------- | | cerbos\_dev\_store\_bundle\_op\_latency | Time to perform bundle operations | | cerbos\_dev\_store\_bundle\_fetch\_errors\_count | Count of errors during bundle downloads | See [Observability](../cerbos/latest/configuration/observability.html) for the full list of available metrics. Troubleshooting ==================== ## [](#%5Fpdp%5Fconnection%5Fissues)PDP connection issues ### [](#%5Fpdp%5Ffails%5Fto%5Fconnect%5Fto%5Fcerbos%5Fhub)PDP fails to connect to Cerbos Hub **Symptoms**: PDP logs show connection errors or timeouts when starting. **Causes and solutions**: | Invalid credentials | Verify the client ID and client secret are correct. Generate new credentials from the deployment’s **Client credentials** tab if needed. | | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Wrong deployment ID | Confirm the deployment ID matches the deployment you want to connect to. Find the ID on the deployment detail page. | | Network restrictions | Ensure outbound HTTPS (port 443) is allowed to the Cerbos Hub hosts (api.cerbos.cloud and cdn.cerbos.cloud); see [Network requirements](reliability.html#network-requirements). Check firewalls, security groups, and network policies. | | Expired credentials | Client credentials do not expire, but they can be revoked. Check the credential status in Cerbos Hub. | ### [](#%5Fpdp%5Fshows%5Fas%5Fdisconnected%5Fin%5Fcerbos%5Fhub)PDP shows as disconnected in Cerbos Hub **Symptoms**: The PDP appears in the Decision points tab but shows as disconnected or has a stale "Last seen" timestamp. **Causes and solutions**: | Network interruption | The PDP will automatically reconnect when network access is restored. Check for intermittent connectivity issues. | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | PDP process crashed | Verify the PDP container or process is running. Check logs for crash reasons. | | Load balancer timeout | Some load balancers close idle connections. The PDP maintains a heartbeat, but aggressive timeouts may cause disconnections. | ## [](#%5Fpolicy%5Fcompilation%5Ferrors)Policy compilation errors ### [](#%5Fbuild%5Ffails%5Fwith%5Fcompilation%5Ferrors)Build fails with compilation errors **Symptoms**: The deployment build fails and shows compilation errors in the build details. **Causes and solutions**: | Invalid YAML syntax | Check for indentation errors, missing colons, or invalid characters. Use a YAML linter locally before pushing. | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Invalid policy structure | Ensure policies follow the [Cerbos policy schema](../cerbos/latest/policies/index.html). Common issues include misspelled keys or missing required fields. | | Duplicate policy definitions | Each resource/action combination must be unique within a scope. Check for conflicting policies across stores. | | Invalid CEL expressions | Condition expressions must be valid CEL syntax. Test expressions in the playground before deploying. | ### [](#%5Ftests%5Ffail%5Fduring%5Fbuild)Tests fail during build **Symptoms**: Compilation succeeds but tests fail, blocking deployment. **Causes and solutions**: | Policy behavior changed | Review the test diff in the build details to understand what changed. Update tests if the new behavior is intentional. | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | Missing test fixtures | Ensure all principals, resources, and auxiliary data referenced in tests exist in the test fixtures. | | Incorrect expected outcomes | Verify the expected actions and effects in your test cases match the policy logic. | ## [](#%5Fembedded%5Fpdp%5Fissues)Embedded PDP issues ### [](#%5Fbundle%5Ffetch%5Ffails%5Fin%5Fbrowser)Bundle fetch fails in browser **Symptoms**: The ePDP client throws errors when fetching the policy bundle. **Causes and solutions**: | CORS blocking requests | Browser security prevents cross-origin requests. ePDP bundles are served with appropriate CORS headers, but ensure you’re using the correct endpoint. | | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | Authentication required | If the ePDP rule requires authentication, provide credentials when initializing the client. Check the rule’s access control settings. | | IP not in allowlist | If the rule has an IP allowlist, ensure the client’s IP address is included. For browser clients, this is the end user’s IP. | | Rule disabled | Verify the ePDP rule is enabled in the deployment settings. | | Invalid rule ID | Confirm the rule ID matches an existing rule in the deployment. | ### [](#%5Fscope%5Fparameter%5Frequired)Scope parameter required **Symptoms**: Bundle fetch fails with "scope parameter required" error. **Causes and solutions**: The ePDP rule has **Require scopes at fetch time** enabled. You must provide scope parameters when initializing the client: ```typescript const cerbos = new Embedded({ policies: { ruleId: "", scopes: ["tenant-acme"], }, wasm, }); ``` ### [](#%5Fauthorization%5Freturns%5Funexpected%5Fresults)Authorization returns unexpected results **Symptoms**: The ePDP allows or denies actions differently than expected. **Causes and solutions**: | Filtered policies | The ePDP bundle may not contain all policies. Check the rule’s filtering settings (resources, actions, scopes, roles, versions) to ensure required policies are included. | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Stale bundle | If policies were recently updated, the client may still have an old bundle. For long-running applications, enable automatic updates. For serverless, each invocation fetches the current bundle. | | Missing globals | If policies reference global variables, ensure they are passed in the client configuration. | | Scope mismatch | If using scopes, verify the request scope matches policies in the filtered bundle. | ## [](#%5Faudit%5Flog%5Fcollection%5Fissues)Audit log collection issues ### [](#%5Faudit%5Flogs%5Fnot%5Fappearing%5Fin%5Fcerbos%5Fhub)Audit logs not appearing in Cerbos Hub **Symptoms**: The PDP is connected but no audit logs appear in the Audit logs tab. **Causes and solutions**: | Audit backend not configured | Ensure audit.backend is set to hub in the PDP configuration. | | --------------------------------- | ----------------------------------------------------------------------------------------------------- | | Audit disabled | Verify audit.enabled is true. | | Storage path missing | The audit.hub.storagePath must be set to a writable directory. Without this, logs cannot be buffered. | | Credentials lack write permission | Audit log upload requires **Read & write** credentials. Read-only credentials cannot upload logs. | ### [](#%5Flogs%5Fdelayed%5For%5Fmissing%5Fentries)Logs delayed or missing entries **Symptoms**: Audit logs appear with significant delay or some entries are missing. **Causes and solutions**: | Network issues | Logs are buffered locally and uploaded when connectivity is available. Check for intermittent network problems. | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Storage volume full | If the local storage path runs out of space, new logs cannot be buffered. Monitor disk usage on the storage volume. | | PDP crashed before sync | Logs are periodically synced to Cerbos Hub. If the PDP crashes between syncs, recent entries may be lost. Use a persistent volume to preserve buffered logs across restarts. | ## [](#%5Fgithub%5Fintegration%5Fissues)GitHub integration issues ### [](#%5Frepository%5Faccess%5Fblocked%5Fby%5Fan%5Forganization%5Fip%5Fallow%5Flist)Repository access blocked by an organization IP allow list **Symptoms**: Connecting a repository fails, or a connected store stops syncing, and GitHub rejects the request with a `403` mentioning that your organization has an IP allow list enabled. **Cause and solution**: Cerbos Hub connects to GitHub from a fixed set of addresses, which your organization’s allow list must permit. Add them as described in [GitHub organization IP allow lists](policy-stores-git-github.html#%5Fgithub%5Forganization%5Fip%5Fallow%5Flists), or fetch them from [hub.cerbos.cloud/meta](https://hub.cerbos.cloud/meta) if you manage the allow list automatically. | | This is a different setting from the ePDP rule allowlist covered under [Bundle fetch fails in browser](#%5Fbundle%5Ffetch%5Ffails%5Fin%5Fbrowser). That one governs who may download bundles from Cerbos Hub; this one governs whether Cerbos Hub may reach your repository. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ### [](#%5Fbuilds%5Farent%5Ftriggered%5Fwhen%5Fmultiple%5Ftags%5Fare%5Fpushed)Builds aren’t triggered when multiple tags are pushed GitHub has a [known limitation](https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#push) where connected apps like Cerbos Hub won’t receive notifications for repository changes if more than three tags are pushed simultaneously. To avoid this issue, limit the maximum number of references pushed at once to three. See the [GitHub documentation](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/managing-the-push-policy-for-your-repository#limiting-how-many-branches-and-tags-can-be-updated-in-a-single-push) for configuration details. ### [](#%5Frepository%5Fchanges%5Fnot%5Fdetected)Repository changes not detected **Symptoms**: Pushing to the connected repository doesn’t trigger a new build. **Causes and solutions**: | Wrong branch | Verify you’re pushing to the branch configured in the policy store settings. | | ----------------------- | ----------------------------------------------------------------------------------------------- | | Webhook disabled | Check that the GitHub webhook for Cerbos Hub is active in your repository settings. | | No policy files changed | Builds only trigger when policy files (.yaml, .yml) change. Changes to other files are ignored. | Usage dashboard ==================== The Usage Dashboard provides comprehensive observability into your authorization service, allowing you to monitor key metrics, view trends, and gain insights into how your Cerbos Hub workspace is utilized. At the organization level, the dashboard aggregates authorization metrics from all your workspaces into a single view, giving platform owners and engineering leaders a complete picture of request volumes, policy distribution, and usage trends across the entire organization. ## [](#%5Fsummary%5Fmetrics)Summary metrics The top of the dashboard displays three key metrics for the current month, each showing the change from the previous month: Active principals The number of unique principals (users, services, or applications) making authorization requests. Track this to understand adoption and identify unexpected usage patterns. Decisions The total number of authorization decisions made via the Check API. A single API call can check multiple actions, so this count may exceed the number of calls. Query plans The total number of Plan API calls used to generate query filters for data access. ## [](#%5Ftrend%5Fcharts)Trend charts ### [](#%5Factive%5Fprincipals)Active principals A line chart showing the number of unique principals over time. Use this to identify growth trends, seasonal patterns, or unexpected drops in usage. ### [](#%5Fcheck%5Fcalls)Check calls A stacked bar chart showing Check API activity by month, broken down by outcome: * **Check calls**: Total number of Check API requests * **Allowed actions**: Decisions that granted access * **Denied actions**: Decisions that rejected access A sudden spike in denied actions could indicate policy changes that are stricter than intended, requests for policies that don’t exist, or application issues such as typos in resource or action names. ### [](#%5Fplan%5Fcalls)Plan calls A stacked bar chart showing Plan API activity by month, broken down by outcome: * **Plan calls**: Total number of Plan API requests * **Allowed actions**: Plans that grant unconditional access * **Denied actions**: Plans that deny access unconditionally * **Conditional actions**: Plans that include conditions to filter data This breakdown helps you understand how query planning is being used to generate database filters and conditional access patterns. ## [](#%5Fusage%5Fdetails)Usage details The table at the bottom provides month-by-month metrics with the following columns: | Column | Description | | ----------------- | ----------------------------------------------------------------------------------- | | Month | The calendar month for this row of data | | Call type | Whether the row represents Check or Plan API calls | | Unique principals | Count of distinct principals making requests of this type | | Total calls | Number of API calls made | | Call breakdown | Visual breakdown showing allowed, denied, and (for Plan calls) conditional outcomes | User management ==================== A Cerbos Hub user can have a role at the organization level and an optional set of roles for each workspace. All of these roles are considered when determining the permissions for any particular user. ## [](#%5Forganization%5Froles)Organization roles Organization roles, except for the `Member` role, apply to all workspaces within the organization. Users with organizational role of `Member` must be explicitly granted workspace roles in order to access a workspace. | Action | Owner | Developer | Analyst | Viewer | Member | | ----------------------------- | ----- | --------- | ------- | ------ | ------ | | View organization | ✅ | ✅ | ✅ | ✅ | ✅ | | Modify organization | ✅ | ❌ | ❌ | ❌ | ❌ | | Manage members | ✅ | ❌ | ❌ | ❌ | ❌ | | Invite a member | ✅ | ❌ | ❌ | ❌ | ❌ | | Create a workspace | ✅ | ✅ | ✅ | ✅ | ✅ | | Create a playground | ✅ | ✅ | ✅ | ✅ | ✅ | | Update a playground | ✅ | ✅ | ✅ | ✅ | ✅ | | Delete a playground | ✅ | ✅ | ✅ | ✅ | ✅ | | Export a playground | ✅ | ✅ | ✅ | ✅ | ✅ | | Connect a PDP to a playground | ✅ | ✅ | ✅ | ✅ | ✅ | ## [](#%5Fworkspace%5Froles)Workspace roles Permissions assigned at the organization level are inherited by all workspaces. Additionally, a user can be assigned specific roles within a workspace, potentially granting more permissions for that particular workspace only. | Action | Owner | Developer | Analyst | Viewer | | ------------------------ | ----- | --------- | ------- | ------ | | View a workspace | ✅ | ✅ | ✅ | ✅ | | View builds | ✅ | ✅ | ✅ | ✅ | | View decision points | ✅ | ✅ | ✅ | ✅ | | View issues | ✅ | ✅ | ✅ | ✅ | | View audit logs | ✅ | ❌ | ✅ | ❌ | | View usage | ✅ | ❌ | ✅ | ❌ | | Export audit logs | ✅ | ❌ | ❌ | ❌ | | Manage API keys | ✅ | ✅ | ❌ | ❌ | | Manage deployments | ✅ | ✅ | ❌ | ❌ | | Manage policy stores | ✅ | ✅ | ❌ | ❌ | | Reset encryption key | ✅ | ❌ | ❌ | ❌ | | Manage workspace members | ✅ | ❌ | ❌ | ❌ | | Modify workspace | ✅ | ❌ | ❌ | ❌ | | Delete a workspace | ✅ | ❌ | ❌ | ❌ | ## [](#%5Fdeleting%5Fan%5Forganization)Deleting an organization Organizations can be deleted by navigating to the organization settings page and clicking **Delete organization**. This action is irreversible and permanently deletes all data associated with the organization, including all workspaces, policy stores, deployments, and audit logs. Cerbos Synapse ==================== Synapse is a modular, enrichment proxy that complements the [open source Cerbos PDP](https://docs.cerbos.dev/cerbos/latest/index.html). It can either be dropped in place of the existing PDPs or deployed as a standalone gateway in front of the existing fleet. It exposes the same Cerbos API (both http and gRPC) on a single port and, in its default configuration, behaves exactly the same way as a PDP. Its power lies in the ability to configure request enrichment pipelines, HTTP handlers for integrating with third-party authorization APIs and native Envoy external authorization support. Coupled with CI/CD pipelines and audit log capture provided by [Cerbos Hub](https://docs.cerbos.dev/cerbos-hub/), Synapse enables building comprehensive security solutions tailored to the needs of your organization. ![How Synapse works](_images/tainaron.png) Synapse has several extension points that support multiple extension types: * Native components built, distributed, and supported by Cerbos engineers * Custom scripts written using [Starlark](https://github.com/bazelbuild/starlark), a lightweight Python dialect * Custom components built using any language that compiles to web assembly (WASM) * Declarative mapping using Common Expression Language (CEL), the same language used to author conditions in Cerbos policies (select extension points only) ## [](#%5Flearn%5Fmore)Learn more * [Proxy extensions](extensions/proxy-extensions.html) * [Route extensions](extensions/route-extensions.html) * [Envoy extension](extensions/envoy-extension.html) * [Data sources](extensions/data-sources.html) Quick Start ==================== [Cerbos](https://www.cerbos.dev/) is an open-source authorization engine. Policies written in YAML declare which actions a **principal** (the user or service making a request) may perform on a **resource** (the thing being accessed). At runtime, an application asks the Cerbos PDP "can principal P do action A on resource R?" and the PDP evaluates the applicable policies to return an allow or deny decision. Synapse is a proxy that sits in front of Cerbos. It exposes the same Cerbos API, so calling applications remain unchanged. Before each request reaches the PDP, Synapse can look up data from other systems, add attributes to principals or resources, rewrite requests, or modify responses. Lookups that would otherwise live in every application are centralised in the proxy. This guide covers running Synapse locally and writing a first proxy extension in Starlark. The extension looks up user data from an external API and attaches it to the principal before the embedded Cerbos PDP evaluates a policy. Other extension types are covered in their own guides. The finished setup runs Synapse in front of an embedded Cerbos PDP. Each `CheckResources` request is intercepted, the principal is enriched with attributes fetched from a public test API, and a policy evaluates the enriched request. The client sends the same call it would send to Cerbos directly; no application code changes. Synapse processes each Cerbos API request in three stages: 1. The caller sends a `CheckResources` or `PlanResources` request. 2. Synapse runs the configured proxy extensions, which may modify the request (for example, by adding attributes to the principal) and later modify the response. 3. The augmented request is handed to the PDP, which evaluates the relevant policies and returns a decision. | | This guide runs Synapse with an embedded Cerbos PDP in a single container. Synapse also supports running as a gateway in front of an existing Cerbos PDP fleet; see [Configuration](configuration/reference.html) for the pdp.external configuration. The proxy extension written here behaves the same way in either topology. | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#%5Fbefore%5Fyou%5Fbegin)Before you begin You will need: * Docker * A Synapse licence. If you do not already have one, sign up at [cerbos.dev/workshop](https://www.cerbos.dev/workshop). Your licence credentials are issued together with the URL of the Cerbos distribution repository, substitute that URL wherever `CERBOS_DISTRIBUTION_REPO` appears in this guide. * `curl` and a JSON formatter such as `python3 -m json.tool` or `jq` (optional, for reading responses). Log in to the Cerbos distribution repository: ```console $ docker login CERBOS_DISTRIBUTION_REPO --username=YOUR_LICENCE_USER --password=YOUR_LICENCE_KEY ``` ## [](#%5Fcreate%5Fa%5Fworking%5Fdirectory)Create a working directory Create a directory that will hold the configuration, policies and extension code: ```console $ mkdir -p synapse-quickstart/policies synapse-quickstart/extensions $ cd synapse-quickstart ``` The `policies` directory will hold the Cerbos policy files loaded by the embedded PDP. The `extensions` directory will hold the Starlark source for the proxy extension written later in this guide. ## [](#%5Fwrite%5Fa%5Fpolicy)Write a policy Create `policies/invoice.yaml` with a single resource policy that allows an employee to view an invoice when the invoice belongs to the same company as the principal: ```yaml apiVersion: api.cerbos.dev/v1 resourcePolicy: resource: invoice version: default rules: - actions: - view effect: EFFECT_ALLOW roles: - employee condition: match: expr: request.resource.attr.company == request.principal.attr.company ``` The condition compares a `company` attribute on the resource to a `company` attribute on the principal. Neither attribute is built in to Cerbos: the caller is expected to send them as part of the request, or an extension must inject them. This guide takes the second approach. | | This guide loads policies from local disk for quick iteration. Production deployments should distribute policies through Cerbos Hub, which publishes signed, versioned bundles and applies updates to connected instances without restarts. The extension configuration is unaffected by the choice of policy source. See [Connecting Synapse to Cerbos Hub](configuration/cerbos-hub.html) for details. | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#%5Fstart%5Fsynapse)Start Synapse Create `.synapse.yaml` with the minimum configuration needed to run Synapse with an in-process PDP that loads policies from disk: ```yaml server: listenAddress: ":3594" (1) pdp: inProcess: (2) storage: driver: "disk" disk: directory: /policies (3) audit: (4) enabled: true backend: file file: path: stdout ``` | **1** | Synapse listens for Cerbos API traffic on port 3594 (HTTP and gRPC share the same port). | | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **2** | Run the Cerbos PDP embedded in the same process as Synapse, instead of forwarding to an external PDP. | | **3** | Path inside the container where the PDP should load policies from. This directory is bind-mounted in the next step. | | **4** | Enable the PDP audit log and write it to standard output. Every request produces an access entry with the caller metadata and a decision entry with the full evaluation inputs, outputs and matched policy. Writing to stdout keeps the audit trail alongside the rest of the container output. Switching to a file or a log aggregator is a matter of changing the backend field. | Start the container with the three bind mounts for the config, policies and extensions directories: ```console $ docker run --rm --name synapse-qs -p 3594:3594 \ -v $(pwd)/.synapse.yaml:/config/.synapse.yaml:ro \ -v $(pwd)/extensions:/extensions:ro \ -v $(pwd)/policies:/policies:ro \ -e SYNAPSE_CONFIG=/config/.synapse.yaml \ CERBOS_DISTRIBUTION_REPO/synapse/synapse:latest \ server --log.level=debug ``` The container runs in the foreground and log output streams to the terminal. Open a second terminal for the curl commands in the next section. Press Ctrl+C in the original terminal to stop Synapse. ## [](#%5Fmake%5Fa%5Fcheck%5Frequest)Make a check request In a new terminal, from the `synapse-quickstart` directory, create `check-request.json`: ```json { "requestId": "qs-001", "principal": { "id": "1", "roles": ["employee"] }, "resources": [ { "actions": ["view"], "resource": { "kind": "invoice", "id": "inv-42", "attr": { "company": "Romaguera-Crona" } } }, { "actions": ["view"], "resource": { "kind": "invoice", "id": "inv-99", "attr": { "company": "Other Inc" } } } ] } ``` Notice that the principal has no `attr` block at all. The request only tells Synapse that principal `1` holds the `employee` role. Send the request: ```console $ curl -s -X POST http://localhost:3594/api/check/resources \ -H 'Content-Type: application/json' \ -d @check-request.json ``` Response ```json { "requestId": "qs-001", "results": [ { "resource": { "id": "inv-42", "kind": "invoice" }, "actions": { "view": "EFFECT_DENY" } }, { "resource": { "id": "inv-99", "kind": "invoice" }, "actions": { "view": "EFFECT_DENY" } } ] } ``` Both resources are denied. The policy condition references `request.principal.attr.company`, and the request did not supply it, so the comparison fails for every resource. This is the baseline behaviour without any extension in play. Switch to the terminal running the container. Every request produces two audit lines: an `access` entry with the caller metadata, and a `decision` entry with the full evaluation inputs, outputs, and matched policies. The decision entry records the inputs the PDP actually evaluated, making it the starting point for investigating unexpected decisions. Once the extension is added in the next section, the principal attributes in the decision entry will include the values fetched from the external API rather than the values the client sent. Sending the audit log to Cerbos Hub instead of stdout retains this trail centrally; see [Connecting Synapse to Cerbos Hub](configuration/cerbos-hub.html). ## [](#%5Fadd%5Fa%5Fproxy%5Fextension)Add a proxy extension Create `extensions/enrich_principal.star`: ```python load("http", "http") (1) def augment_check_request(req): (2) return _augment(req) def augment_plan_request(req): (3) return _augment(req) def _augment(req): if not hasattr(req, "principal"): return req resp = http.get("https://jsonplaceholder.typicode.com/users/" + req.principal.id) (4) if resp.status_code != 200: return req user = resp.json() (5) req.principal.attr = { (6) "company": user["company"]["name"], } return req ``` | **1** | Load the HTTP module from the Starlark standard library. See [Developing Starlark extensions](extensions/starlark-development.html) for the full list of modules available to extensions. | | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **2** | augment\_check\_request is called before every CheckResources request is passed to the PDP. The extension can read and mutate the request in place, then return it. | | **3** | augment\_plan\_request handles the equivalent hook for PlanResources. The same enrichment logic is reused so both call types see the same principal. | | **4** | Call [JSONPlaceholder](https://jsonplaceholder.typicode.com/), a public test API that returns fake user data for IDs 1–10. | | **5** | Parse the response body as JSON. resp.json() returns a native Starlark value. | | **6** | Replace the principal’s attributes with a dictionary containing the company field drawn from the external record. In a real integration this would be identity or directory data. | | | Starlark extensions can make outbound HTTP calls without further configuration. The WASM runtime sandboxes network access. The equivalent extension written in any language compiled to WASM must list the target host under allowedHosts in the extension configuration. See [Developing Webassembly extensions](extensions/wasm-development.html) for the WASM equivalent. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Register the extension by replacing the contents of `.synapse` with: ```yaml server: listenAddress: ":3594" pdp: inProcess: storage: driver: "disk" disk: directory: /policies audit: enabled: true backend: file file: path: stdout extensions: proxyExtensions: enrichPrincipal: (1) extensionURL: /extensions/enrich_principal.star (2) required: true (3) ``` | **1** | Unique name for this extension. It appears in Synapse log output, which is helpful when several extensions are configured. | | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **2** | Path inside the container. The .star suffix tells Synapse to load it with the Starlark runtime. See [extension URL format](extensions/url-format.html) for the other supported schemes. | | **3** | Marks the extension as required. A failure inside a required extension causes Synapse to terminate the chain and return an error to the caller, rather than silently forwarding an unenriched request. | | | Proxy extensions enrich the **attributes** attached to principals and resources. Roles should be set by the caller (typically derived from JWT claims, a header set by an API gateway, or an auth middleware in the calling application) before the request reaches Synapse. Nothing in Synapse stops an extension from writing to principal.roles, but doing so moves authentication into the authorization layer: roles are no longer traceable to the identity provider in audit logs, and any bug in the extension becomes a privilege-escalation path. | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#%5Frestart%5Fand%5Fverify)Restart and verify Stop the running container with Ctrl+C and start it again with the same `docker run` command as before. The extension is loaded at startup, which you can see in the container logs: ```json {"severity":"DEBUG","extension":"enrichPrincipal","message":"Downloading extension"} {"severity":"DEBUG","extension":"enrichPrincipal","runtime":"starlark","message":"Downloaded extension"} ``` Synapse emits structured JSON logs at runtime. To filter for extension-related lines, pipe `docker logs` through `grep enrichPrincipal` or a JSON query tool such as `jq`. Send the same request again: ```console $ curl -s -X POST http://localhost:3594/api/check/resources \ -H 'Content-Type: application/json' \ -d @check-request.json ``` Response ```json { "requestId": "qs-001", "results": [ { "resource": { "id": "inv-42", "kind": "invoice" }, "actions": { "view": "EFFECT_ALLOW" } }, { "resource": { "id": "inv-99", "kind": "invoice" }, "actions": { "view": "EFFECT_DENY" } } ] } ``` The decision for `inv-42` has flipped to `EFFECT_ALLOW`; `inv-99` remains denied. The client payload is identical. Synapse intercepted the request, ran the Starlark extension, fetched JSONPlaceholder user `1`, and set `principal.attr.company` to `Romaguera-Crona`. The PDP then evaluated the policy against the enriched principal. The `company` attribute on `inv-42` matches the principal and the view action is permitted. The attribute on `inv-99` does not match, so the action is denied. Change the `id` in `check-request.json` to any value between `1` and `10`, or change the `company` attributes on the resources. Every run fetches fresh data from the external API and the decisions reflect the new inputs. Synapse does not need to be restarted between requests. ## [](#%5Fbuild%5Fa%5Ftest%5Fsuite%5Fto%5Fensure%5Frepeatability)Build a test suite to ensure repeatability Manually restarting the container and sending requests via curl becomes cumbersome very quickly. Build test suites using the built-in [testing framework](extensions/testing.html) to automate the process and codify the requirements of your extension. Create a test suite for your extension by adding a new file named `extensions/enrich_principal_test.star` with the following contents. extensions/enrich\_principal\_test.star ```python test_suite = struct( name = "Test principal enrichment", synapse_config = testing.load_synapse_config("/config/.synapse.yaml") (1) ) def test_enrichment(context): request = struct( (2) requestId = "qs-001", principal = struct( id = "1", roles = ["employee"] ), resources = [ struct( actions = ["view"], resource = struct( kind = "invoice", id = "inv-42", attr = { "company": "Romaguera-Crona" } ) ), struct( actions = ["view"], resource = struct( kind = "invoice", id = "inv-99", attr = { "company": "Other Inc" } ) ) ] ) have = context.check_resources(request) (3) return testing.assert(have.results[0].actions["view"] == "EFFECT_ALLOW" and have.results[1].actions["view"] == "EFFECT_DENY" ) (4) ``` | **1** | Configure this test suite to use a Synapse instance with this configuration. | | ----- | --------------------------------------------------------------------------------------------------------------------------------- | | **2** | The request to send. See [the documentation](extensions/testing.html#testdata) for information about loading test data from disk. | | **3** | Send the request to the test server. | | **4** | Verify the expected result. | Run the test suite. ```console $ docker run \ --rm --name synapse-test \ -v $(pwd)/.synapse.yaml:/config/.synapse.yaml:ro \ -v $(pwd)/extensions:/extensions:ro \ -v $(pwd)/policies:/policies:ro \ CERBOS_DISTRIBUTION_REPO/synapse/synapse:latest \ test /extensions ``` == PASS:Test principal enrichment (/extensions/enrich_principal_test.star) Passed=1 Failed=0 Skipped=0 --- PASS:enrichment | Assertion is True Refer to [Testing custom extensions](extensions/testing.html) for more details about authoring tests. ## [](#%5Fadapt%5Fthe%5Fextension%5Fto%5Fyour%5Fown%5Fapi)Adapt the extension to your own API The extension above targets a public demonstration API. Retargeting it at an internal service is typically a URL change. Production integrations require additional handling: * **Authentication.** Do not hardcode credentials in the Starlark source. Pass them through the `configuration` block of the extension definition in `.synapse.yaml` and read them inside the extension via the `context.extension_config` dictionary. See [Developing Starlark extensions](extensions/starlark-development.html) for the full context API. * **Reachability.** The HTTP call runs inside the Synapse container. Internal hostnames must resolve and be routable from that container. In Kubernetes this usually means a service name inside the same namespace, or a configured egress path. * **TLS.** If the internal API uses a private CA, the CA bundle must be available to the container at a path the Starlark runtime can trust. Self-signed certificates will fail by default. * **Failure handling.** Decide whether an extension outage should fail the authorization decision. If the attribute is advisory, set `required: false` so the chain continues when the lookup fails. If the attribute is load-bearing for policy decisions, keep `required: true` so Synapse returns an error rather than falling back to a potentially unsafe decision. For the full surface area of proxy extension configuration (including priorities when multiple extensions are chained, data mounts, and response hooks), see [Proxy extensions](extensions/proxy-extensions.html). ## [](#%5Fnext%5Fsteps)Next steps * [Proxy extensions](extensions/proxy-extensions.html) covers the full configuration reference, priority ordering, and the response-side hooks (`augment_check_response`, `augment_plan_response`). * [Starlark](extensions/starlark-development.html) documents every module available to extensions, including `json`, `hashlib`, and the `cerbos.*` helpers for data-source lookups and cache access. * [WASM](extensions/wasm-development.html) describes the alternative runtime for extensions written in any language that compiles to WebAssembly. * Build test suites for your extensions using the built-in [testing framework](extensions/testing.html). * [Configuration](configuration/reference.html) is the authoritative reference for the Synapse configuration file, including how to use an external Cerbos PDP instead of the embedded one. * The Cerbos PDP ships client SDKs for most major languages. The [Cerbos API reference](https://docs.cerbos.dev/cerbos/latest/api/index.html) lists them; all of them work against the Synapse endpoint unchanged. Connecting Synapse to Cerbos Hub ==================== [Cerbos Hub](../../../cerbos-hub/index.html) is the managed service for distributing, auditing and governing Cerbos policies. The in-process Cerbos PDP can source policies from Hub as optimized, versioned bundles and can send audit logs to Hub for centralised retention, search and analysis. The audit trail Hub receives captures each request as the PDP evaluated it, including every attribute that Synapse’s proxy extensions added before the decision. Each entry is annotated with the Synapse version, the list of extensions that ran, and any custom instance metadata configured on the instance. The record therefore reflects the inputs that produced the decision, not only the inputs the client sent. This page covers the Synapse-specific aspects of connecting to Hub. For account provisioning, policy authoring workflows, deployment management and playground usage, see the [Cerbos Hub documentation](../../../cerbos-hub/index.html). ## [](#%5Fprerequisites)Prerequisites * A Cerbos Hub account with at least one workspace. * A deployment or playground that provides the policy bundle. * Client credentials for the selected Hub source. Audit log collection requires **Read & write** deployment credentials. * Outbound HTTPS access to `api.cerbos.cloud` and `cdn.cerbos.cloud` on port 443\. See [Network requirements](../../../cerbos-hub/reliability.html#network-requirements). If you do not yet have a Hub account, sign up at [hub.cerbos.cloud](https://hub.cerbos.cloud/). ## [](#%5Fpolicy%5Fdistribution)Policy distribution Configure the in-process Cerbos PDP to use the `hub` storage driver and point it at a deployment: ```yaml pdp: inProcess: hub: credentials: clientID: "${CERBOS_HUB_CLIENT_ID}" (1) clientSecret: "${CERBOS_HUB_CLIENT_SECRET}" pdpID: "synapse-1" (2) storage: driver: "hub" (3) hub: remote: deploymentID: "YOUR_DEPLOYMENT_ID" (4) cacheDir: /var/lib/synapse/hub-cache (5) ``` | **1** | Client credentials for the selected Hub source. The environment variables can be used instead of defining the fields in the configuration file. | | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | **2** | Optional identifier for the PDP in Hub. A generated identifier is used when this value is omitted. | | **3** | Use the hub storage driver. | | **4** | The deployment ID from Hub. Use playgroundID instead when connecting to a playground. | | **5** | Optional directory for caching policy bundles. The directory must exist before Synapse starts. | Only one of `deploymentID` or `playgroundID` may be set at a time. See [Cerbos Hub storage](../../../cerbos/latest/configuration/storage.html#hub) for the complete storage configuration. | | A hub section at the root of the Synapse configuration is ignored. Place it under pdp.inProcess. | | --------------------------------------------------------------------------------------------------- | ## [](#%5Fsupplying%5Fcredentials)Supplying credentials Credentials can be configured under `pdp.inProcess.hub.credentials` or with environment variables. Store client secrets in the secret-management system provided by the deployment platform. | Variable | Purpose | | --------------------------- | -------------------------------------------------------------------------------------------------- | | CERBOS\_HUB\_CLIENT\_ID | Client ID for the selected Hub source. | | CERBOS\_HUB\_CLIENT\_SECRET | Client secret paired with the client ID. | | CERBOS\_HUB\_PDP\_ID | Optional identifier for this Synapse instance. Defaults to a generated value. | | CERBOS\_HUB\_DEPLOYMENT\_ID | Optional deployment ID. Selects the hub storage driver and overrides the configured deployment ID. | | CERBOS\_HUB\_PLAYGROUND\_ID | Optional playground ID. Selects the hub storage driver and overrides the configured playground ID. | When `deploymentID` is configured, the client credential must belong to that deployment. Policy store credentials cannot fetch deployment bundles. For `playgroundID`, use the connection details under **Implement** → **Connect a PDP** in the playground. When running under Docker, pass the credentials through on the command line for local testing or through the orchestrator’s secret mechanism for production: ```console $ docker run --rm --name synapse -p 3594:3594 \ -e CERBOS_HUB_CLIENT_ID \ -e CERBOS_HUB_CLIENT_SECRET \ -e SYNAPSE_CONFIG=/config/.synapse.yaml \ -v $(pwd)/.synapse.yaml:/config/.synapse.yaml:ro \ CERBOS_DISTRIBUTION_REPO/synapse/synapse:latest \ server ``` Under the [Helm chart](../install/helm.html), create a Kubernetes Secret containing the credentials and reference it from the chart’s `envFrom` configuration. ### [](#%5Fselecting%5Fa%5Fdeployment%5Ffrom%5Fthe%5Fenvironment)Selecting a deployment from the environment Synapse applies `CERBOS_HUB_DEPLOYMENT_ID` and `CERBOS_HUB_PLAYGROUND_ID` before starting the in-process PDP. `CERBOS_HUB_DEPLOYMENT_ID` takes precedence if both variables are set. Do not combine `CERBOS_HUB_DEPLOYMENT_ID` with a configured `playgroundID`, or `CERBOS_HUB_PLAYGROUND_ID` with a configured `deploymentID`. Both IDs remain set and prevent the PDP from starting. | | A standalone Cerbos PDP uses these variables only when the corresponding configuration field is empty. The variables do not select the storage driver for a standalone PDP. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ## [](#%5Faudit%5Flogging%5Fto%5Fhub)Audit logging to Hub Hub can also receive the PDP’s audit trail. Configure the in-process Cerbos PDP to use the `hub` audit backend: ```yaml pdp: inProcess: audit: enabled: true backend: hub hub: storagePath: /var/lib/synapse/audit (1) mask: (2) metadata: - authorization checkResources: - inputs[*].principal.attr.ssn ``` | **1** | Local buffer for audit entries awaiting delivery. Use persistent storage so buffered entries survive container restarts. | | ----- | ------------------------------------------------------------------------------------------------------------------------ | | **2** | Optional field masks that remove sensitive data from audit entries before they are sent to Hub. | Each audit entry that Hub receives contains two records per request: * An **access** entry with the caller identity, transport metadata and method name. * A **decision** entry with the request inputs, evaluation outputs, matched policies and decision reasons. The inputs recorded in the decision entry are the inputs after proxy extensions have run. If an extension fetched a principal’s department from an internal directory and added it as an attribute, that attribute appears in the audit record. The Hub console supports searching and filtering decisions on these enriched attributes. See [Hub audit log collection](../../../cerbos-hub/audit-log-collection.html) for field masking and other audit options. ### [](#%5Fcontext%5Fadded%5Fby%5Fsynapse)Context added by Synapse Synapse annotates each request’s `requestContext` before it reaches the PDP, and those annotations flow through to every audit entry Hub ingests. Three categories are added automatically: | Annotation key | Value | Source | | ----------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | cerbos.dev/synapse/version | The running Synapse version. | Always added. | | cerbos.dev/synapse/extensions | The list of proxy extension names that processed this request, in the order they ran. | Added whenever one or more proxy extensions are configured. | | _your custom keys_ | Arbitrary string, number or boolean values. | Populated from the audit.instanceAnnotations section of the Synapse configuration. | Custom instance annotations are declared at the top level of `.synapse.yaml`: ```yaml audit: instanceAnnotations: environment: "production" region: "eu-west-1" cluster: "auth-primary" ``` These annotations identify the Synapse instance that handled the call, the region or cluster it ran in, the Synapse version deployed at the time, and the extensions that contributed to the enriched request. When Hub is the audit destination, this context is searchable and filterable in the Hub console. The same `CERBOS_HUB_CLIENT_ID` and `CERBOS_HUB_CLIENT_SECRET` credentials are used for policy distribution and audit logging. Audit log collection requires **Read & write** credentials. ## [](#%5Favailability%5Fand%5Fmonitoring)Availability and monitoring If the Hub connection is interrupted after startup, the PDP continues to evaluate requests with its current bundle and applies updates after reconnecting. The readiness endpoint reports PDP readiness rather than Hub connectivity. Use the `cerbos_dev_hub_connected` metric to monitor the connection. See [Cerbos Hub reliability](../../../cerbos-hub/reliability.html) for bundle distribution, caching and connectivity behavior. ## [](#%5Fusing%5Fhub%5Fwith%5Fan%5Fexternal%5Fcerbos%5Fpdp)Using Hub with an external Cerbos PDP When Synapse is configured as a gateway in front of an existing Cerbos PDP fleet (`pdp.external`), Hub configuration lives on the upstream PDP, not on Synapse. Synapse itself does not need Hub credentials in that topology. It forwards authorization traffic to the PDP, and the PDP handles bundle fetching and audit shipping. The top-level `audit.instanceAnnotations` setting remains available because Synapse adds those values before forwarding the request. See [Cerbos Hub storage](../../../cerbos/latest/configuration/storage.html#hub) for configuring the external PDP. ## [](#%5Ftroubleshooting)Troubleshooting * **Authentication failures at startup.** Check that the client ID and client secret are available to the process and that the credentials belong to the configured deployment or playground. * **Bundle not found.** Verify the deployment or playground ID in Hub. * **Bundle updates not applied.** Confirm that `disableAutoUpdate` is unset or `false`, and monitor the Hub connection with `cerbos_dev_hub_connected`. * **Configuration appears to be ignored.** Confirm that `hub`, `storage` and `audit` are nested under `pdp.inProcess`. Observability ==================== Synapse is instrumented with OpenTelemetry metrics and traces. To obtain the data via OTLP, configure the exporters using [OpenTelemetry environment variables](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/). ## [](#metrics)Metrics | Environment variable | Description | | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | OTEL\_EXPORTER\_OTLP\_METRICS\_ENDPOINT or OTEL\_EXPORTER\_OTLP\_ENDPOINT | Address of the OTLP metrics receiver (for example: ). If not defined, OTLP metrics are disabled. | | OTEL\_EXPORTER\_OTLP\_METRICS\_INSECURE or OTEL\_EXPORTER\_OTLP\_INSECURE | Skip validating the TLS certificate of the endpoint | | OTEL\_EXPORTER\_OTLP\_METRICS\_CERTIFICATE or OTEL\_EXPORTER\_OTLP\_CERTIFICATE | Path to the certificate to use for validating the server’s TLS credentials. | | OTEL\_EXPORTER\_OTLP\_METRICS\_CLIENT\_CERTIFICATE or OTEL\_EXPORTER\_OTLP\_CLIENT\_CERTIFICATE | Path to the client certificate to use for mTLS | | OTEL\_EXPORTER\_OTLP\_METRICS\_CLIENT\_KEY or OTEL\_EXPORTER\_OTLP\_CLIENT\_KEY | Path to the client key to use for mTLS | | OTEL\_EXPORTER\_OTLP\_METRICS\_PROTOCOL or OTEL\_EXPORTER\_OTLP\_PROTOCOL | OTLP protocol. Supported values are grpc and http/protobuf. Defaults to grpc. | | OTEL\_METRIC\_EXPORT\_INTERVAL | The export interval in milliseconds. Defaults to 60000. | | OTEL\_METRIC\_EXPORT\_TIMEOUT | Timeout for exporting the data in milliseconds. Defaults to 30000. | | OTEL\_METRICS\_EXPORTER | Set to otlp to enable the OTLP exporter. Defaults to prometheus which enables the scrape endpoint at /\_cerbos/metrics. Set to none to disable metrics. | Refer to for more information about exporter configuration through environment variables. Note that the OpenTelemetry Go SDK used by Synapse might not have full support for some of the environment variables listed on the OpenTelemetry specification. | | OTEL\_METRICS\_EXPORTER and OTEL\_EXPORTER\_OTLP\_METRICS\_ENDPOINT are the only required environment variables to enable OTLP metrics. | | ------------------------------------------------------------------------------------------------------------------------------------------ | ## [](#traces)Traces | Environment variable | Description | | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | OTEL\_SERVICE\_NAME | Service name reported in the traces. Defaults to Synapse. | | OTEL\_TRACES\_EXPORTER | Set to otlp to enable the OTLP exporter. Defaults to none. | | OTEL\_TRACES\_SAMPLER | [Trace sampler](https://opentelemetry.io/docs/specs/otel/trace/sdk/#sampling). Defaults to parentbased\_always\_off. Supported values: always\_on Record every trace. always\_off Don’t record any traces. traceidratio Record a fraction of traces based on ID. Set OTEL\_TRACES\_SAMPLER\_ARG to a value between 0 and 1 to define the fraction. parentbased\_always\_on Record all traces except those where the parent span is not sampled. parentbased\_always\_off Don’t record any traces unless the parent span is sampled. parentbased\_traceidratio Record a fraction of traces where the parent span is sampled. Set OTEL\_TRACES\_SAMPLER\_ARG to a value between 0 and 1 to define the fraction. | | OTEL\_TRACES\_SAMPLER\_ARG | Set the sampling ratio when OTEL\_TRACES\_SAMPLER is a ratio-based sampler. Defaults to 0.1. | | OTEL\_EXPORTER\_OTLP\_TRACES\_ENDPOINT or OTEL\_EXPORTER\_OTLP\_ENDPOINT | Address of the OTLP collector (for example: ). If not defined, traces are disabled. | | OTEL\_EXPORTER\_OTLP\_TRACES\_INSECURE or OTEL\_EXPORTER\_OTLP\_INSECURE | Skip validating the TLS certificate of the endpoint | | OTEL\_EXPORTER\_OTLP\_TRACES\_CERTIFICATE or OTEL\_EXPORTER\_OTLP\_CERTIFICATE | Path to the certificate to use for validating the server’s TLS credentials. | | OTEL\_EXPORTER\_OTLP\_TRACES\_CLIENT\_CERTIFICATE or OTEL\_EXPORTER\_OTLP\_CLIENT\_CERTIFICATE | Path to the client certificate to use for mTLS | | OTEL\_EXPORTER\_OTLP\_TRACES\_CLIENT\_KEY or OTEL\_EXPORTER\_OTLP\_CLIENT\_KEY | Path to the client key to use for mTLS | | OTEL\_EXPORTER\_OTLP\_TRACES\_PROTOCOL or OTEL\_EXPORTER\_OTLP\_PROTOCOL | OTLP protocol. Supported values are grpc and http/protobuf. Defaults to grpc. | Refer to for more information about exporter configuration through environment variables. Note that the OpenTelemetry Go SDK used by Synapse might not have full support for some of the environment variables listed on the OpenTelemetry specification. | | OTEL\_TRACES\_EXPORTER and OTEL\_EXPORTER\_OTLP\_TRACES\_ENDPOINT are the only required environment variables to enable OTLP trace exports. | | ---------------------------------------------------------------------------------------------------------------------------------------------- | Health probes ==================== Synapse exposes health and metrics endpoints on the address configured by `server.listenAddress` (`:3594` by default). ## [](#%5Fendpoints)Endpoints | Path | Method | Description | | ----------------- | ------ | --------------------------------------------------------------------------------------------------------------- | | /\_cerbos/live | GET | Returns 200 OK while the process is running. This endpoint does not check the configured PDP. | | /\_cerbos/ready | GET | Returns 200 OK when the configured PDP is ready to serve requests and 503 Service Unavailable otherwise. | | /\_cerbos/health | GET | Compatibility alias for /\_cerbos/ready. This path can be used by probes migrated from a standalone Cerbos PDP. | | /\_cerbos/metrics | GET | Prometheus metrics endpoint. See [Observability](observability.html). | These endpoints accept `GET` requests. A `HEAD` request returns `405 Method Not Allowed`. ## [](#readiness)Readiness The readiness check reports the health of the configured PDP. For `pdp.inProcess`, it remains unavailable until the policy store is initialized. This includes the initial bundle download for the `hub` storage driver and the initial bucket clone for the `blob` storage driver. For `pdp.external`, readiness follows the health of the external PDP. After initialization, readiness does not track connectivity to the policy source. For example, if the connection to Cerbos Hub is interrupted, the PDP continues to serve requests using its current bundle and `/_cerbos/ready` continues to return `200`. Use the `cerbos_dev_hub_connected` metric to monitor Hub connectivity. If the in-process PDP cannot start, Synapse exits. If an external PDP is unavailable, Synapse continues running but remains unready. Check the container logs for startup and connectivity errors. ## [](#%5Fkubernetes%5Fprobes)Kubernetes probes Use separate liveness and readiness probes: ```yaml livenessProbe: httpGet: path: /_cerbos/live port: 3594 scheme: HTTP (1) readinessProbe: httpGet: path: /_cerbos/ready port: 3594 scheme: HTTP (1) ``` | **1** | Use HTTPS when TLS is configured for the Synapse server. | | ----- | -------------------------------------------------------- | The Synapse Helm chart configures these endpoints by default. When migrating manifests from the standalone Cerbos PDP, `/_cerbos/health` remains available for compatibility, but separate liveness and readiness probes are recommended. Configuration ==================== If no configuration is provided via the `--conf.path` flag, Synapse starts on port 3594 and attempts to load Cerbos policies from a directory named `policies` in the current path. While the `--conf.set` flag can be used to override configuration values from the command line, it is recommended to use a configuration file for better readability. | | The pdp.inProcess block embeds a full Cerbos PDP. It accepts every option the standalone Cerbos PDP accepts — storage drivers, audit backends, schema enforcement, server hardening, and so on. For the authoritative reference on what goes inside pdp.inProcess, see the [Cerbos PDP configuration documentation](https://docs.cerbos.dev/cerbos/latest/configuration/). The reference below covers only the Synapse-specific keys that wrap it. | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#%5Fexample%5Fconfiguration)Example configuration The following snippet illustrates a typical configuration for Synapse. ```yaml server: listenAddress: ":3594" tls: cert: /certs/tls.crt key: /certs/tls.key pdp: inProcess: storage: driver: "disk" disk: directory: /policies schema: enforcement: warn audit: enabled: true backend: file file: path: stderr extensions: dataDir: /tmp/data cacheDir: /tmp/cache dataSources: litestreamds: extension: extensionURL: system://sqldb configuration: litestream: replicationURL: s3://bucket/mydb.db envoyExternalAuthz: enabled: true extension: extensionURL: /extensions/envoy.wasm proxyExtensions: importantExtension: extensionURL: /extensions/important.wasm?checksum=sha256:dcbbdeaf3d6ddd64ca8ec1026e7addb877a420fbc1be9a73d246839df8935b4a priority: 1 required: true configuration: environment: staging dataMount: /data routeExtensions: caddy: extension: extensionURL: /extensions/caddy.wasm routes: "/caddy/auth": ["POST"] ``` ## [](#%5Freference)Reference The following section enumerates all possible configuration options for Synapse. Please note that some configuration sections are mutually exclusive with each other. ```yaml --- audit: instanceAnnotations: {"instance": "eu-west-002"} # InstanceAnnotations are key-value pairs of static information about this instance that should be added to the Cerbos requests for audit purposes. cache: backend: "inmem" # Backend sets the cache implementation to use. Valid values are "inmem" or "redis". inmem: # InMem configures the in-memory cache. maxSize: 1024 redis: # Redis configures a Redis-backed cache. connectionURL: redis://localhost:6379/0?dial_timeout=3&read_timeout=6s&max_retries=2 extensions: cacheDir: ${XDG_CACHE_DIR} # CacheDir sets the directory to use for caching downloaded extensions and other artifacts. dataDir: ${XDG_DATA_DIR} # DataDir sets the global directory under which the extensions can store their custom data. dataSources: example: extension: extensionURL: system://sqldb # DataSources are common functions usable by other extensions. See the data sources documentation for the full configuration. envoyExternalAuthz: # EnvoyExternalAuthz configures the Envoy external authorization endpoint. See the envoy extension documentation for the full configuration. enabled: false # Enabled sets whether the Envoy endpoint is enabled or not. extension: # Extension defines a pluggable extension to use for handling Envoy requests. Mutually exclusive with mapping. configuration: {} # Configuration holds any custom configuration values needed by the extension. dataMount: /data # DataMount defines the virtual file system root for the extension. disabled: false # Disabled prevents the extension from being loaded. extensionURL: ${PWD}/extensions/ext.wasm?checksum=sha256:dcbbdeaf3d6ddd64ca8ec1026e7addb877a420fbc1be9a73d246839df8935b4a # Required. ExtensionURL defines the URL to obtain the extension from. mapping: # Mapping uses declarative configuration to handle Envoy requests. Mutually exclusive with extension. request: # Required. Request configures handling of requests sent to the adapter. action: "request.method.lowerAscii()" # Required. Action is a CEL expression to extract the action from the incoming request. auxData: # AuxData configures how to extract the auxiliary data from the incoming request. jwt: # JWT configures how to extract the JWT from the incoming request. Mutually exclusive with jwts. keySetID: "'gateway'" # KeySetID is a CEL expression to extract the ID of the keyset to be used to verify the JWT from the incoming request. token: "request.header['Authorization'].replace('Bearer ', '', 1)" # Required. Token is a CEL expression to extract the JWT from the incoming request. jwts: token_a: token: "request.header['Authorization'].replace('Bearer ', '', 1)" # JWTs configures how to extract named JWTs from the incoming request. Mutually exclusive with jwt. principal: # Required. Principal configures how to extract the principal from the incoming request. attr: "{'ip': request.header['X-Forwarded-For'].split(', ', 2)[0]}" # Attr is a CEL expression or map of CEL expressions to extract the principal's attributes from the incoming request. id: "request.header['X-User-Id']" # Required. ID is a CEL expression to extract the principal's ID from the incoming request. policyVersion: "'default'" # PolicyVersion is a CEL expression to extract the principal's policy version from the incoming request. roles: "['anonymous']" # Required. Roles is a CEL expression or list of CEL expressions to extract the principal's roles from the incoming request. scope: "request.header['X-Tenant-Id']" # Scope is a CEL expression to extract the principal's scope from the incoming request. requestID: "request.header['X-Request-Id']" # RequestID is a CEL expression to extract the request ID from the incoming request. resource: # Required. Resource configures how to extract the resource from the incoming request. attr: "request.body.json" # Attr is a CEL expression or map of CEL expressions to extract the resource's attributes from the incoming request. id: "request.body.json.id" # Required. ID is a CEL expression to extract the resource's ID from the incoming request. kind: "'route'" # Required. Kind is a CEL expression to extract the resource's kind from the incoming request. policyVersion: "'default'" # PolicyVersion is a CEL expression to extract the resource's policy version from the incoming request. scope: "request.header['X-Tenant-Id']" # Scope is a CEL expression to extract the resource's scope from the incoming request. response: "{'status': {'code': check.allow ? google.rpc.Code.OK : google.rpc.Code.PERMISSION_DENIED}}" # Response is a CEL expression returning a envoy.service.auth.v3.CheckResponse proxyExtensions: proxyName: extensionURL: /extensions/foo.wasm # ProxyExtensions configure interceptor chains for Cerbos Check/Plan requests. See the proxy extensions documentation for the full configuration. routeExtensions: routeName: extensionURL: /extensions/foo.wasm # RouteExtensions configure arbitrary HTTP handlers for providing external authorization capabilities via Cerbos policies. See the route extensions documentation for the full configuration. workDir: ${TMPDIR} # WorkDir sets the directory for creating sockets and other temporary files. pdp: external: # External configures the server to connect to PDP(s) running elsewhere. grpcAddress: cerbos:3593 httpAddress: cerbos:3592 tls: authority: cerbos.example.com # Authority overrides the TLS authority for the connection. caCert: /path/to/ca.crt # CACert sets the path to a CA certificate file to use for validating the certificates. disabled: false # Disabled switches off TLS and attempts to connect to the PDP using a plaintext connection. Not recommended for production use. insecure: false # Insecure switches off verification of issuer and host of TLS certificates. Not recommended for production use. timeout: 5s inProcess: storage: driver: disk # InProcess configures a Cerbos PDP that runs on the same process as this server. workDir: /tmp/tainaron # WorkDir is the directory to use for creating sockets and other files required to run the PDP. server: cors: allowedHeaders: ['content-type'] # AllowedHeaders is the contents of the allowed-headers header. allowedOrigins: ['*'] # AllowedOrigins is the contents of the allowed-origins header. disabled: false # Disabled sets whether CORS is disabled. maxAge: 10s # MaxAge is the max age of the CORS preflight check. debugAddress: 127.0.0.1:6666 disableDocs: false disableMeta: false listenAddress: :3594 tls: caCert: /path/to/ca.crt # CACert is the path to the optional CA certificate for verifying client requests. cert: /path/to/tls.crt # Cert is the path to the TLS certificate file. key: /path/to/tls.key # Key is the path to the TLS private key file. tailscale: authKey: ${TS_AUTHKEY} # AuthKey to use to register the service. Defaults to environment variable TS_AUTHKEY. disabled: false # Disabled prevents the Tailscale service from being launched even if TS_AUTHKEY is defined in the environment. hostName: cerbos-001 # HostName overrides the host name of this instance on the tailnet. Defaults to system host name. serviceName: svc:cerbos-synapse # ServiceName sets the name of the Tailscale service. Defaults to cerbos-synapse. servicePort: 443 # ServicePort sets the port of the Tailscale service. Defaults to 443. timeouts: idle: 300s # Idle sets the server idle timeout. read: 60s # Read sets server read timeout. readHeader: 30s # ReadHeader sets server read header timeout. shutdown: 30s # Shutdown sets the time to wait for the server to gracefully shutdown. write: 30s # Write sets server write timeout. udsFileMode: 0o766 ``` Run as a Tailscale service ==================== Synapse instances can be configured to automatically join your tailnet and host a [Tailscale service](https://tailscale.com/docs/features/tailscale-services). ## [](#%5Fprerequisites)Prerequisites * In your Tailscale configuration, define a new tag to apply to the Synapse hosts. * [Define a new Tailscale service](https://tailscale.com/docs/features/tailscale-services#step-1-define-a-tailscale-service). Set the port to 443 and the ACL tag to the tag you created above. * Define an ACL to allow your tailnet users to access the service. Assuming that you named the service `cerbos-synapse` and the tag `synapse-host`, the following sample ACL allows everyone on your tailnet access to the service. Modify it as necessary to suit your requirements. ```json { "tagOwners": { "tag:synapse-host": ["autogroup:member"], }, "autoApprovers": { "services": { "svc:cerbos-synapse": ["tag:synapse-host"], }, }, "grants": [ { "src": ["*"], "dst": ["svc:cerbos-synapse"], "ip": ["*"], }, ] } ``` * [Generate an auth key](https://tailscale.com/docs/features/access-control/auth-keys#generate-an-auth-key). Enable tags and assign the tag created above. Enabling ephemeral mode is recommended as well. ## [](#%5Flaunch%5Fsynapse%5Finto%5Fthe%5Ftailnet)Launch Synapse into the tailnet Once the above prerequisites are complete, launch Synapse with `TS_AUTHKEY` environment variable set to the auth key. Synapse will automatically join the tailnet and register as a host of the service (assuming it’s named `cerbos-synapse`). See Synapse logs or the Tailscale services console to obtain the address that can be used to access the service. ### [](#%5Fcustomizing%5Fthe%5Fconfiguration)Customizing the configuration You can customize the service name, service port and the host name on the tailnet by changing the Synapse configuration file. ```yaml server: tailscale: authKey: ${TS_AUTHKEY} # AuthKey to use to register the service. Defaults to environment variable TS_AUTHKEY. disabled: false # Disabled prevents the Tailscale service from being launched even if TS_AUTHKEY is defined in the environment. hostName: cerbos-001 # HostName overrides the host name of this instance on the tailnet. Defaults to system host name. serviceName: svc:cerbos-synapse # ServiceName sets the name of the Tailscale service. Defaults to svc:cerbos-synapse. servicePort: 443 # ServicePort sets the port of the Tailscale service. Defaults to 443. ``` Building custom Synapse extensions ==================== You can build your own extensions using Starlark scripts or any programming language that compiles to web assembly (WASM). | | Prefer to let an AI agent do the scaffolding? The [Cerbos Agent Skill](https://github.com/cerbos/skills) for Synapse extensions can generate the extension, configuration and tests to run it locally. You can install the skill with npx skills add cerbos/skills --skill cerbos-synapse-extension. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | * [Scripting extensions with Starlark](starlark-development.html) * [Developing WASM extensions](wasm-development.html) * [Tutorial: Build a WASM extension in Go](wasm-go-tutorial.html) * [Tutorial: Build a WASM extension in TypeScript](wasm-typescript-tutorial.html) * [Testing extensions](testing.html) Data sources ==================== Data sources are reusable implementations of common patterns that can be used by the other extension types. They could be thought of as pluggable functions that can encapsulate anything such as accessing a database, accessing an external API, interacting with a legacy system or calling into an LLM. Extensions running on Synapse call into a data source using its unique name defined in the configuration. See [Data sources development documentation](wasm-development.html#datasources) for more details about the request and response formats. ## [](#%5Fbuilt%5Fin%5Fsqldb%5Fdata%5Fsource)Built-in sqldb data source Synapse ships with a built-in `sqldb` datasource that can be used to query Litestream, MySQL, Postgres or SQLite databases. The `query` field for the lookup request should be a string containing an SQL query. The query could contain named parameters prefixed by a colon (`:`) and the values for those parameters should be passed in the `queryParameters` field as an object whose keys are the parameter names. If `cacheOptions` is defined, the result is cached in the Synapse cache. | | For a worked example that calls the sqldb data source from a Starlark proxy extension to enrich CheckResources requests, see [the Starlark proxy extension example](starlark-development.html#proxy-example-data-source). | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | The `result` field of the response would be one of the following: * `null` if the query returned no results * Queries that return a single row: a JSON object with column names as the keys. * Queries that return multiple rows: a JSON array containing objects with column names as the keys. ### [](#%5Flitestream)Litestream Synapse can fetch a Litestream backup from any of the standard Litestream replication destinations and enable read-only querying of the retrieved copy. Note that VFS support is not currently available. Therefore, in order to fetch a new copy of the database, Synapse needs to be restarted. Similar to the standard Litestream service, credentials required for accessing the replica are read from the environment or through URL parameters. Refer to for details. ```yaml extensions: dataSources: myLitestreamDB: (1) extension: extensionURL: system://sqldb configuration: litestream: (2) replicationURL: "s3://litestream/fruits.db" (3) ``` | **1** | Unique name for the data source. | | ----- | -------------------------------------------------------------------------------------------- | | **2** | Use the Litestream backend with sqldb | | **3** | URL of the replica. See for more information. | ### [](#%5Fmysql)MySQL Provides access to a MySQL database. The connection string must be in the following format. [username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN] See for the list of supported parameters. Environment variables can be used to provide sensitive values such as passwords. ```yaml extensions: dataSources: myMySQLDB: (1) extension: extensionURL: system://sqldb configuration: mysql: (2) connectionString: "${MYSQL_USER}:${MYSQL_PASSWORD}@tcp(localhost:3306)/cerbos" (3) connectionPool: (4) maxLifeTime: 600s maxIdleTime: 300s maxOpen: 5 maxIdle: 1 ``` | **1** | Unique name for the data source | | ----- | ----------------------------------- | | **2** | Use the MySQL backend with sqldb | | **3** | MySQL connection string | | **4** | Connection pool settings (optional) | If your MySQL server requires TLS or if you want to use RSA key pair-based password exchange, you can configure those settings as follows: TLS certificates ```yaml extensions: dataSources: myMySQLDB: extension: extensionURL: system://sqldb configuration: mysql: connectionString: "${MYSQL_USER}:${MYSQL_PASSWORD}@tcp(localhost:3306)/cerbos?tls=mysecuretls" tls: mysecuretls: caCert: /path/to/ca_certificate.crt cert: /path/to/certificate.crt key: /path/to/private.key ``` Server public key ```yaml extensions: dataSources: myMySQLDB: extension: extensionURL: system://sqldb configuration: mysql: connectionString: "${MYSQL_USER}:${MYSQL_PASSWORD}@tcp(localhost:3306)/cerbos?serverPubKey=mypubkey" serverPubKey: mypubkey: /path/to/server_public_key.pem ``` ### [](#postgresql)PostgreSQL ```yaml extensions: dataSources: myPostgresDB: (1) extension: extensionURL: system://sqldb configuration: postgresql: (2) connectionString: "postgres://${PG_USER}:${PG_PASSWORD}@localhost:5432/postgres?sslmode=disable&search_path=cerbos" (3) connectionPool: (4) maxLifeTime: 600s maxIdleTime: 300s maxOpen: 5 maxIdle: 1 ``` | **1** | Unique name for the data source | | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **2** | Use the postgres backend with sqldb | | **3** | Connection string for the database. See [Postgres connstring documentation](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING) for more information. | | **4** | Connection pool settings (optional) | ### [](#%5Fsqlite)SQLite ```yaml extensions: dataSources: mySQLiteDB: (1) extension: extensionURL: system://sqldb configuration: sqlite: (2) connectionString: "file:/tmp/cerbos.sqlite?mode=rwc&cache=shared&_fk=true" (3) ``` | **1** | Unique name for the data source | | ----- | ----------------------------------------------------------------------------- | | **2** | Use the sqlite backend with sqldb | | **3** | Connection string for the database. See . | ## [](#%5Fcustom%5Fdata%5Fsources)Custom data sources ```yaml extensions: dataSources: myAwesomeDataSource: (1) extension: extensionURL: "/extensions/awesome.wasm" (2) configuration: (3) environment: staging ``` | **1** | Unique name for the data source | | ----- | -------------------------------------------------------------------------------------------------- | | **2** | URL to fetch the extension from. See [extension URL format](url-format.html) for more information. | | **3** | Optional configuration values for the extension | ### [](#%5Fbuilding%5Fa%5Fcustom%5Fdata%5Fsource)Building a custom data source See [Building custom Synapse extensions](building.html). Envoy extension ==================== Synapse implements the [Envoy external authorization API](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http%5Ffilters/ext%5Fauthz%5Ffilter) natively and its behaviour can be configured either declratively or programmatically. A typical Envoy configuration to use Synapse as the external authorization service is as follows. Envoy configuration ```yaml static_resources: listeners: - name: listener_0 address: socket_address: address: 0.0.0.0 port_value: 8080 filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager codec_type: AUTO http_filters: - name: envoy.filters.http.ext_authz typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz transport_api_version: V3 grpc_service: envoy_grpc: cluster_name: synapse - name: envoy.filters.http.router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router route_config: ... clusters: - name: synapse type: STATIC typed_extension_protocol_options: envoy.extensions.upstreams.http.v3.HttpProtocolOptions: "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions explicit_http_config: http2_protocol_options: {} load_assignment: cluster_name: synapse endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 127.0.0.1 port_value: 3594 transport_socket: name: envoy.transport_sockets.tls typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext sni: synapse.localhost common_tls_context: validation_context: trust_chain_verification: ACCEPT_UNTRUSTED ``` ## [](#%5Fdeclarative%5Fmapping)Declarative mapping Map Envoy `Check` requests to Cerbos `CheckResources` and back using CEL expressions. ```yaml extensions: envoyExternalAuthz: enabled: true (1) mapping: (2) request: requestID: 'request.attributes.request.http.headers["x-request-id"]' principal: id: 'request.attributes.request.http.headers["x-user-id"]' roles: '["user"]' resource: id: '"foo"' kind: '"request"' action: "request.attributes.request.http.method" ``` | **1** | Enable the Envoy Check endpoint on the server | | ----- | ----------------------------------------------------------------------------- | | **2** | Declarative mapping of request and response. See below for mapping reference. | ### [](#%5Fmapping%5Freference)Mapping reference | | Documentation about Cerbos request and response formats can be found at . For Envoy Check request and response formats, refer to . | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | * `request` (required): * `requestID`: a CEL expression returning a string. * `principal` (required): * `id` (required): a CEL expression returning a string. * `roles` (required): a CEL expression returning a list of strings, or a list of CEL expressions each returning a string. * `attr`: a CEL expression returning a map of strings to values, or a map of strings to CEL expressions each returning a value. * `policyVersion`: a CEL expression returning a string. * `scope`: a CEL expression returning a string. * `resource` (required): * `kind` (required): a CEL expression returning a string. * `id` (required): a CEL expression returning a string. * `attr`: a CEL expression returning a map of strings to values, or a map of strings to CEL expressions each returning a value. * `policyVersion`: a CEL expression returning a string. * `scope`: a CEL expression returning a string. * `action` (required): a CEL expression returning a string. * `auxData`: * `jwt`: * `token`: a CEL expression returning a string. * `keySetID`: a CEL expression returning a string. * `response`: * `status`: a CEL expression returning an HTTP status code as an integer. Defaults to 200 when allowed and 403 when denied. * `headers`: a CEL expression returning a map of strings to strings, or a map of strings to CEL expressions each returning a string. * `body`: a CEL expression returning bytes or a string. Request expressions have access to the incoming HTTP request in the CEL variable `request`, which has the following fields: * `method`: the request’s HTTP method as an uppercase string. * `header`: map of HTTP request headers. Keys are header names in [canonical form](https://pkg.go.dev/net/http#CanonicalHeaderKey) and values are strings (comma-separated when multiple headers with the same name are present in the request). * `headers`: map of HTTP request headers. Keys are header names in [canonical form](https://pkg.go.dev/net/http#CanonicalHeaderKey) and values are lists of strings. * `body`: object with the following fields: * `bytes`: the raw request body * `text`: the request body as a string * `json`: the JSON-decoded request body Response expressions have access to the `CheckResources` result in the CEL variable `check` which has the following fields: * `requestId`, `principal`, `resource`, and `action`: the inputs produced by the request expressions. * `cerbosCallId`: the ID for audit logging as a string. * `allow`: whether the action is allowed as a boolean. * `outputs`: the output from the policy as a map of source (`policy-id#rule-name`) to value. * `validationErrors`: errors encountered when validating the principal and resource against the corresponding schemas as a list of [cerbos.schema.v1.ValidationError messages](https://github.com/cerbos/cerbos/blob/main/api/public/cerbos/schema/v1/schema.proto). Using the YAML scalar block syntax `>` for CEL expressions is recommended to avoid quoting issues. `action: "view"` would be interpreted as the CEL expression `view` (which would fail to compile because a variable named `view` is not defined), when the desired setting was the CEL expression `"view"` (which just returns the intended string literal). Minimal configuration ```yaml extensions: envoyExternalAuthz: enabled: true mapping: request: requestID: 'request.attributes.request.http.headers["x-request-id"]' principal: id: 'request.attributes.request.http.headers["x-user-id"]' roles: '["user"]' resource: id: '"foo"' kind: '"request"' action: "request.attributes.request.http.method" ``` Customizing the response ```yaml extensions: envoyExternalAuthz: enabled: true mapping: request: requestID: 'request.attributes.request.http.headers["x-request-id"]' principal: id: 'request.attributes.request.http.headers["x-user-id"]' roles: '["user"]' resource: id: '"foo"' kind: '"request"' action: "request.attributes.request.http.method" response: > check.allow ? {"status": {"code": google.rpc.Code.OK }} : { "status": {"code": google.rpc.Code.OK }, "deniedResponse": { "status": {"code": 401 }, "body": json.encode(check.outputs["resource.example.v1#route"].body) } } ``` ## [](#%5Fprogrammatic%5Fmapping)Programmatic mapping ```yaml extensions: envoyExternalAuthz: enabled: true (1) extension: extensionURL: /extensions/envoy.wasm (2) configuration: (3) environment: staging ``` | **1** | Enable the Envoy Check endpoint. | | ----- | -------------------------------------------------------------------------------------------------- | | **2** | URL to fetch the extension from. See [extension URL format](url-format.html) for more information. | | **3** | Configuration values needed by the extension. \[Optional\] | ### [](#%5Fbuilding%5Fan%5Fenvoy%5Fextension)Building an Envoy extension See [Building custom Synapse extensions](building.html). Extensions ==================== Synapse exposes four extension points. Each sits at a different stage of the authorization request lifecycle and addresses a different integration problem. Choosing the correct type is the first step when adding a capability to Synapse. This page summarises the four types, the integration shape each one addresses, and the implementation options available across them. Each section links to the corresponding configuration reference. ## [](#%5Fproxy%5Fextensions)Proxy extensions Proxy extensions intercept `CheckResources` and `PlanResources` requests travelling through Synapse, and the corresponding responses on the way back. They can add, modify or remove attributes on the principal and resources before the PDP evaluates the request, and they can rewrite or redact fields on the response before it reaches the caller. Calling applications keep using the standard Cerbos API — the enrichment happens transparently. Proxy extensions suit integrations of the form: look up data, attach it to the request, hand the request to the PDP. Common examples: * Fetching a principal’s department, team, region or tier from an internal directory or IAM service. * Loading resource metadata (owner, sensitivity, project) that is not carried in the API call. * Redacting sensitive fields from `outputs` blocks before they reach untrusted callers. * Attaching audit context (tenant, trace ID, correlation ID) that downstream systems will index on. See [Proxy extensions](proxy-extensions.html). ## [](#%5Froute%5Fextensions)Route extensions Route extensions register custom HTTP endpoints under `` Synapse’s `/ext/ `` path and translate their requests into Cerbos authorization calls. They exist because many systems — API gateways, query engines, message queues, legacy apps — know how to delegate authorization to an HTTP endpoint, but each expects its own request and response shape. Route extensions suit third-party systems that already delegate authorization over HTTP but expect a protocol other than Cerbos’s. Common examples: * Plugging Trino, Kafka, or a custom gateway into Cerbos without modifying the upstream client. * Exposing a simplified "can-this-user-do-that" endpoint to internal tooling. * Running multiple translation layers at once — each on its own `/ext/` path — from the same Synapse instance. See [Route extensions](route-extensions.html). ## [](#%5Fenvoy%5Fextension)Envoy extension The Envoy extension implements the [Envoy external authorization API](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http%5Ffilters/ext%5Fauthz%5Ffilter) natively, so an Envoy proxy (or any Envoy-based ingress like Gloo, Contour, or Istio’s filter chain) can delegate authorization decisions to Synapse without a translation layer. The mapping from Envoy’s `CheckRequest` to Cerbos’s request is configured declaratively or programmatically on Synapse, not in Envoy. The Envoy extension applies when authorization must happen at the edge or inside a service mesh, and the data plane already terminates at Envoy. See [Envoy extension](envoy-extension.html). ## [](#%5Fdata%5Fsources)Data sources Data sources are not wired into the request path directly. They are reusable, named lookup functions that **other** extensions call into via a `data_source_lookup()` call. A single data source — say, "employees" — can be shared by a proxy extension that enriches principals, a route extension that answers custom queries, and an envoy extension that mediates service-mesh traffic. Data sources suit cases where several extensions call into the same underlying system (a database, an external API, an LLM) and the integration should live in one place. Synapse includes a built-in `sqldb` data source covering SQLite, MySQL, PostgreSQL and Litestream; custom data sources can be written in any of the supported runtimes. See [Data sources](data-sources.html). ## [](#%5Fpicking%5Fthe%5Fright%5Ftype)Picking the right type The following table maps common integration shapes to the extension type that fits best. | Integration shape | Extension type | | ------------------------------------------------------------------------------------------------- | ------------------------------------------ | | Enrich a Cerbos request before it reaches the PDP | Proxy extension | | Modify or redact a Cerbos response before it reaches the caller | Proxy extension | | Expose a custom authorization endpoint for a tool that already supports HTTP-based external authz | Route extension | | Translate a non-Cerbos authorization protocol into a Cerbos decision | Route extension | | Authorize Envoy traffic via its external authorization filter | Envoy extension | | Share a database or API lookup across multiple other extensions | Data source | | Cache or deduplicate lookups that every application would otherwise do | Proxy extension calling into a data source | If more than one row applies, start with the type closer to the top. ## [](#%5Fexecution%5Fmodel)Execution model ### [](#%5Fproxy%5Fextensions%5Fexecute%5Fas%5Fa%5Fpipeline)Proxy extensions execute as a pipeline Multiple proxy extensions can be configured against a single Synapse instance. On the request side, they execute in descending order of the `priority` field. On the response side, they execute in reverse. Each extension receives the request (or response) as produced by the preceding extension, so one extension can enrich the principal, another can attach resource metadata, and a third can add audit context, with each seeing the output of those that ran before it. A failure inside an extension marked `required: true` terminates the pipeline and Synapse returns an error to the caller. Non-required extensions that fail are skipped and the pipeline continues with the unmodified request. See [Proxy extensions](proxy-extensions.html) for the full priority and failure semantics. ### [](#%5Fextensions%5Fcalling%5Fthe%5Fpdp%5Fbypass%5Fthe%5Fproxy%5Fpipeline)Extensions calling the PDP bypass the proxy pipeline When an extension calls `cerbos.check_resources()` or `cerbos.plan_resources()` from its own code — for example, a route extension translating a third-party protocol into a Cerbos decision — the call is routed directly to the PDP. The proxy extension pipeline does not run again for these internal calls. This prevents an infinite-loop condition where a proxy extension delegating to the PDP would re-trigger itself. Enrichment required from inside an extension must be applied in the extension’s own code, or through a [data source](data-sources.html) shared with the proxy pipeline, before the PDP call is issued. ## [](#%5Fimplementation%5Foptions)Implementation options Every extension type supports multiple implementation runtimes. The choice is independent of the extension type. | Runtime | When to use | Trade-offs | | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | **Native** | Built, distributed and supported by Cerbos. Use these whenever one of them already covers your integration — for example, the built-in sqldb data source. | No development work, but the set of native extensions is fixed. | | **Declarative CEL mapping** | Available on route and envoy extensions. Use when the mapping between the incoming protocol and a Cerbos request can be expressed as a set of CEL expressions against request fields. | No runtime to operate, fastest iteration. Limited to what CEL can express. | | **Starlark** | Use for custom logic that is expressible in a Python-like scripting language — HTTP lookups, JSON manipulation, conditional enrichment. The [Quick Start](../quickstart.html) uses a Starlark proxy extension. | No compilation step, fast iteration. Single-threaded, slower than WASM for CPU-heavy workloads, limited library ecosystem. | | **WebAssembly (WASM)** | Use when you need a full programming language, an existing library ecosystem, or CPU-intensive logic. Extensions can be written in Go, Java, Python or TypeScript/JavaScript and compiled to WASM. | Requires a build pipeline and deployment of binaries. Richer ecosystem, better performance, stronger sandboxing. | Both Starlark and WASM extensions can call into data sources. Complex integrations are typically structured as one data source wrapping the system integration and one or more Starlark or WASM extensions handling the policy-specific logic. See [Building custom Synapse extensions](building.html) for language-specific guides on writing Starlark and WASM extensions. ## [](#%5Fnext%5Fsteps)Next steps * [Quick Start](../quickstart.html) — write your first proxy extension in Starlark against a public test API. * [Proxy extensions](proxy-extensions.html), [Route extensions](route-extensions.html), [Envoy extension](envoy-extension.html), [Data sources](data-sources.html) — configuration reference for each type. * [Starlark](starlark-development.html) and [WASM](wasm-development.html) — developer guides for the two custom-extension runtimes. Extension manifest ==================== The manifest of an extension provides structured metadata about the extension that’s useful for documenting its purpose and data dictionary. Information provided by the active extensions is available for inspection via the `/_cerbos/meta` endpoint of the Synapse server. ## [](#%5Fproviding%5Fa%5Fmanifest)Providing a manifest ### [](#starlark)Starlark extensions Implement a function named `manifest` that returns either a `struct` or `dict` structured in the format described in [Manifest format](#manifest-format). ```python def manifest(): return struct(api_version = 1, name = "test", version = "1.0.0", owner = "cerbos.dev/synapse", description = "Test extension") ``` ### [](#wasm)WASM extensions Implement a function named `manifest` that returns a JSON blob structured in the format described in [Manifest format](#manifest-format). Go example ```go //go:wasmexport manifest func manifest() int32 { m := Manifest{ APIVersion: 1, Name: "test", Version: "1.0.0", Owner: "cerbos.dev/synapse", Description: "Test extension", } if err := pdk.OutputJSON(m); err != nil { pdk.SetError(err) return 1 } return 0 } ``` ## [](#manifest-format)Manifest format | Field | Type | Description | | ------------- | -------------------- | --------------------------------------------------------- | | apiVersion | uint | Manifest API version. Required. Must be set to 1. | | name | string | Name of the extension. Required. | | version | string | Extension version. Required. | | owner | string | Team or person responsible for maintaining the extension. | | description | string | High-level description of the extension’s purpose. | | fieldMappings | list of FieldMapping | Describe how the extension maps data to PDP requests. | __FieldMapping__ | Field | Type | Description | | ----------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | targets | map of field path to target type | Describe the paths of PDP request/responses being modified. The key is the JSON path of the field such as resource.kind and the value is the target. Valid values for target are: TARGET\_CHECK\_RESOURCES\_REQUEST TARGET\_CHECK\_RESOURCES\_RESPONSE TARGET\_PLAN\_RESOURCES\_REQUEST TARGET\_PLAN\_RESOURCES\_RESPONSE TARGET\_AUTHZEN\_EVALUATION\_REQUEST TARGET\_AUTHZEN\_EVALUATION\_RESPONSE TARGET\_AUTHZEN\_EVALUATION\_BATCH\_REQUEST TARGET\_AUTHZEN\_EVALUATION\_BATCH\_RESPONSE | | description | string | Description of this enrichment. | | operation | enum | The type of enrichment such as adding a value, overwriting an existing value and so on. Valid values are: OPERATION\_ADD OPERATION\_REMOVE OPERATION\_OVERWRITE OPERATION\_APPEND | | value | Value | Describe how the value is computed. Values can be static or dynamic. See below for the schema of this field. | | jsonSchema | object | A JSON schema snippet describing this field. | | metadata | object | Key-value pairs of additional metadata you want to attach to this enrichment description. | __Value__ | Field | Type | Description | | ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------- | | staticValue | any | The static value being assigned to this field. Mutually exclusive with computedValue. | | computedValue | ComputedValue | Describes a value computed dyanmically from the request. Mutually exclusive with staticValue. See below for schema. | __ComputedValue__ | Field | Type | Description | | ----------- | ----------- | ----------------------------------------- | | description | string | Describe the computation. | | sources | string list | Sources of data used for the computation. | ### [](#%5Fexample)Example Example manifest from the built-in Aperture extension ```json { "apiVersion": "1", "name": "aperture", "version": "dev", "owner": "cerbos.dev/synapse", "description": "Integration point for Aperture by Tailscale", "fieldMappings": [ { "targets": { "requestId": "TARGET_CHECK_RESOURCES_REQUEST" }, "description": "Request ID from Aperture metadata", "operation": "OPERATION_ADD", "value": { "computedValue": { "sources": [ "metadata.request_id" ] } }, "jsonSchema": { "type": "string" } }, { "targets": { "principal.id": "TARGET_CHECK_RESOURCES_REQUEST" }, "description": "Principal ID from Aperture metadata", "operation": "OPERATION_ADD", "value": { "computedValue": { "sources": [ "metadata.login_name" ] } }, "jsonSchema": { "type": "string" } }, { "targets": { "principal.roles": "TARGET_CHECK_RESOURCES_REQUEST" }, "description": "Principal roles (hardcoded)", "operation": "OPERATION_ADD", "value": { "staticValue": [ "user" ] }, "jsonSchema": { "items": { "type": "string" }, "type": "array" } }, { "targets": { "principal.attr.stable_node_id": "TARGET_CHECK_RESOURCES_REQUEST" }, "description": "Stable node ID from Aperture metadata", "operation": "OPERATION_ADD", "value": { "computedValue": { "sources": [ "metadata.stable_node_id" ] } }, "jsonSchema": { "type": "string" } }, { "targets": { "principal.attr.tailnet_name": "TARGET_CHECK_RESOURCES_REQUEST" }, "description": "Tailenet name from Aperture metadata", "operation": "OPERATION_ADD", "value": { "computedValue": { "sources": [ "metadata.tailnet_name" ] } }, "jsonSchema": { "type": "string" } }, { "targets": { "principal.attr.user_agent": "TARGET_CHECK_RESOURCES_REQUEST" }, "description": "User agent from Aperture metadata", "operation": "OPERATION_ADD", "value": { "computedValue": { "sources": [ "metadata.user_agent" ] } }, "jsonSchema": { "type": "string" } }, { "targets": { "resource.id": "TARGET_CHECK_RESOURCES_REQUEST" }, "description": "Resource ID", "operation": "OPERATION_ADD", "value": { "staticValue": "aperture_hook" }, "jsonSchema": { "type": "string" } }, { "targets": { "resource.kind": "TARGET_CHECK_RESOURCES_REQUEST" }, "description": "Resource Kind", "operation": "OPERATION_ADD", "value": { "staticValue": "aperture_hook" }, "jsonSchema": { "type": "string" } }, { "targets": { "resource.policyVersion": "TARGET_CHECK_RESOURCES_REQUEST" }, "description": "Resource policy version", "operation": "OPERATION_ADD", "value": { "staticValue": "production" }, "jsonSchema": { "type": "string" } }, { "targets": { "resource.scope": "TARGET_CHECK_RESOURCES_REQUEST" }, "description": "Resource scope", "operation": "OPERATION_ADD", "value": { "staticValue": "" }, "jsonSchema": { "type": "string" } }, { "targets": { "resource.attr": "TARGET_CHECK_RESOURCES_REQUEST" }, "description": "Resource attributes", "operation": "OPERATION_ADD", "value": { "computedValue": { "sources": [ "metadata" ] } }, "jsonSchema": { "type": "object" } }, { "targets": { "actions": "TARGET_CHECK_RESOURCES_REQUEST" }, "description": "Resource actions", "operation": "OPERATION_ADD", "value": { "staticValue": [ "hook_event" ] }, "jsonSchema": { "items": { "type": "string" }, "type": "array" } } ] } ``` Proxy extensions ==================== Proxy extensions are interceptors for PDP requests (`CheckResources` and `PlanResources`) and responses. Synapse exposes the same gRPC and HTTP API endpoints as the regular Cebos PDP without validation which allows applications to send partial `CheckResources/PlanResources` requests to the service. When Synapse receives a Cerbos API request, it is fed through the list of configured proxy extensions in the order of priority defined in the configuration. These extensions can modify the request by adding, updating or removing items before it is processed by the PDP. Similarly, the response from the PDP is fed through the list of configured extensions as well, allowing them to modify the response before it’s sent to the caller. This ability to modify requests and responses unlocks many capabilities: * Lookup common and/or sensitive data points such as principal attributes from a single place which provides better security isolation, better performance, reduced resource utilization and increased visibility and accountability. * Redact sensitive information from responses. * Generate comprehensive logs, events, metrics and other internal data points required for monitoring or compliance purposes. * Integrate with legacy systems to source/sink data. * Roll out changes without modifying or restarting applications. For example, adding attributes required to enforce new requirements set by the organization or seamlessly migrating to a different provider for the same kind of data. ## [](#%5Fconfiguring%5Fproxy%5Fextensions)Configuring proxy extensions Proxy extensions are defined under the `extensions.proxyExtensions` key in the Synapse configuration file. ```yaml extensions: dataDir: /global/data (1) proxyExtensions: lookupPrincipal: (2) extensionURL: "https://example.com/synapse/idp.wasm?checksum=sha256:677cce2788330f16f27981130eaa50aa4189beab2121903bcea5b3c4c918dccc" (3) priority: 100 (4) required: true (5) dataMount: /data (6) configuration: (7) environment: staging lookupFoo: extensionURL: "/extensions/foo.crbs?checksum=sha256:cb766ca03213d0ccb6ffab76e1988ad8e906f8033107d816f51abcfa85d4f507" priority: 90 ``` | **1** | This is the global data directory to be used by Synapse. All extension data mounts will be created under this directory. | | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **2** | Unique name for the extension. | | **3** | URL to fetch the extension from. See [extension URL format](url-format.html) for more information. | | **4** | Priority of the extension. Higher values run earlier for requests and later for responses. | | **5** | Optional flag to mark the extension as required. If a required extension fails, Synapse terminates the chain and returns an error back to the caller. | | **6** | Optional data mount. This virtual path will be available inside the extension for read/write operations. | | **7** | Optional configuration values specific to the extension. | The `dataMount` option defines the virtual file system available to extensions. Extension code can only read/write data to this path. For example, if the `dataMount` is `/data`, then the extension can create or read any files under `/data` such as `/data/foo.txt`. If `dataMount` is not specified, reading or writing to any file path will fail. Locally, the data directory will be created under the global `dataDir` with the name of the extension (e.g. `/global/data/lookupPrincipal`) and persisted between restarts. Requests pass through matching extensions in descending priority order. Responses pass through the same extensions in reverse order. Extensions with the same priority are ordered by fully qualified name; use distinct priorities when the order is significant. An extension failure is ignored unless `required` is set. A required extension failure stops the chain and returns an error to the caller. ## [](#%5Fbuilding%5Fproxy%5Fextensions)Building proxy extensions See [Building custom Synapse extensions](building.html). Route extensions ==================== Route extensions are a way to register arbitrary HTTP handlers on Synapse to provide external authorization endpoints. Many systems such as gateways, proxies, query engines and queues have support for delegating access control to another system by calling into a specific endpoint with information about the access decision to be made. Route extensions enable using Cerbos policies to make those decisions by defining how to map the requests and responses to the format required by each component. Synapse route extensions can be defined in two ways. * Declarative mapping * Programmatic mapping using pluggable implementations provided by Cerbos or developed by the user Route definitions Regardless of the mapping type used, the routes to which they should apply are defined as a map of paths to allowed HTTP methods. | | All route extensions are served under the /ext/ prefix of Synapse server. This is implicitly prepended to the paths defined in the routes section. Therfore, if you define the route as /foo/bar, it will be served from the Synapse server at /ext/foo/bar. Path patterns are global. Take care to avoid using the same or semantically equivalent paths for your route extensions. If multiple paths match a given request, the path that appeared first in the Synapse configuration under the routeExtensions section wins. | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ```yaml extensions: routeExtensions: foo: routes: "/team-a/foo": [] (1) "/team-b/{baz}": ["GET"] (2) ``` | **1** | Activate the extension for requests sent via any HTTP method to /ext/team-a/foo. This is an exact match. | | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **2** | Activate the extension for GET requests to /ext/team-b/. The curly braces around the second path component defines a wildcard that matches any value. | ## [](#route-patterns)Route patterns Synapse matches the paths in a `routes` map using the [gorilla/mux](https://github.com/gorilla/mux) pattern syntax. Literal paths are exact matches and do not include subpaths. ### [](#pattern-syntax)Pattern syntax | Pattern | Matches | Notes | | ---------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------- | | /orders/status | The literal path only | Exact match. | | {name} | A single path segment | Default regex is \[^/\]+. /orders/{version} matches /ext/orders/v1 but not /ext/orders/v1/billing. | | \\{name:pattern} | A segment matching an inline regex | Accepts a Go regular expression, for example \\{id:\[0-9\]+}. | | \\{name:.\*} | Zero or more characters, including / | Multi-segment wildcard. /orders/\\{rest:.\*} matches /ext/orders/v1/billing/invoices. | | | Use \\{name:.\*} for a multi-segment wildcard. The form \\{name…​} matches a single segment. | | ----------------------------------------------------------------------------------------------- | ### [](#%5Fmethods%5Fand%5Fpath%5Fvariables)Methods and path variables Specify methods in the value of the map, for example `"/foo": ["GET"]`. An empty list allows all methods. Host patterns and method prefixes in the path are not supported. The `/ext/` prefix is prepended to every pattern and is **not** stripped before the extension runs, so the path the extension sees includes it (for example `/ext/orders/v1`). Matched path variables are not included separately in the extension input. Use `req.path` when the extension needs a value from the path. ### [](#route-overlap)Overlapping routes and registration order Routes from all route extensions share one router. If patterns overlap, the first registered pattern handles the request; the most specific pattern is not selected automatically. Registration order is not guaranteed, so configure non-overlapping patterns. Exact duplicate route strings are rejected at startup. ### [](#%5Fcatch%5Fall%5Froutes)Catch-all routes When teams own separate API prefixes, each prefix can delegate to one catch-all route. The extension then owns the endpoint mapping within that prefix: ```yaml extensions: routeExtensions: orders: extension: extensionURL: /extensions/orders.star routes: "/orders": [] (1) "/orders/{path:.*}": [] (2) ``` | **1** | Handles the bare /ext/orders path. | | ----- | ------------------------------------------------------------------------------------- | | **2** | Handles /ext/orders/ and paths below it. The extension validates the path and method. | Use a disjoint prefix for each catch-all route. An empty method list allows all methods, so the extension must reject unsupported path and method combinations. ## [](#%5Fdeclarative%5Fmapping)Declarative mapping This extension allows users to define mappings for the request/response conversion using CEL expressions. ```yaml extensions: routeExtensions: foo: (1) mapping: request: (2) requestID: 'request.header["X-Request-Id"]' principal: id: '"abc"' roles: '["user"]' attr: "request.body.json" resource: id: '"test"' kind: '"request"' action: "request.method" response: (3) status: "check.allow ? 200 : 403" body: "check.requestID" routes: (4) "/bar": ["GET", "POST"] "/baz/{baz}": [] ``` | **1** | Unique name for the instance | | ----- | ------------------------------------------------------------------------------------------------- | | **2** | Section for defining how to map the incoming HTTP request into a Cerbos CheckResources request | | **3** | Section for defining how to map the Cerbos response into the HTTP response expected by the caller | | **4** | Routes to which this mapping applies. See [Route patterns](#route-patterns). | ### [](#%5Fmapping%5Freference)Mapping reference | | See [Cerbos request and response formats](../../../cerbos/latest/api/index.html#%5Frequest%5Fand%5Fresponse%5Fformats). | | -------------------------------------------------------------------------------------------------------------------------- | * `request` (required): * `requestID`: a CEL expression returning a string. * `principal` (required): * `id` (required): a CEL expression returning a string. * `roles` (required): a CEL expression returning a list of strings, or a list of CEL expressions each returning a string. * `attr`: a CEL expression returning a map of strings to values, or a map of strings to CEL expressions each returning a value. * `policyVersion`: a CEL expression returning a string. * `scope`: a CEL expression returning a string. * `resource` (required): * `kind` (required): a CEL expression returning a string. * `id` (required): a CEL expression returning a string. * `attr`: a CEL expression returning a map of strings to values, or a map of strings to CEL expressions each returning a value. * `policyVersion`: a CEL expression returning a string. * `scope`: a CEL expression returning a string. * `action` (required): a CEL expression returning a string. * `auxData`: * `jwt`: * `token`: a CEL expression returning a string. * `keySetID`: a CEL expression returning a string. * `response`: * `status`: a CEL expression returning an HTTP status code as an integer. Defaults to 200 when allowed and 403 when denied. * `headers`: a CEL expression returning a map of strings to strings, or a map of strings to CEL expressions each returning a string. * `body`: a CEL expression returning bytes or a string. Request expressions have access to the incoming HTTP request in the CEL variable `request`, which has the following fields: * `method`: the request’s HTTP method as an uppercase string. * `header`: map of HTTP request headers. Keys are header names in [canonical form](https://pkg.go.dev/net/http#CanonicalHeaderKey) and values are strings (comma-separated when multiple headers with the same name are present in the request). * `headers`: map of HTTP request headers. Keys are header names in [canonical form](https://pkg.go.dev/net/http#CanonicalHeaderKey) and values are lists of strings. * `body`: object with the following fields: * `bytes`: the raw request body * `text`: the request body as a string * `json`: the JSON-decoded request body Response expressions have access to the `CheckResources` result in the CEL variable `check` which has the following fields: * `requestId`, `principal`, `resource`, and `action`: the inputs produced by the request expressions. * `cerbosCallId`: the ID for audit logging as a string. * `allow`: whether the action is allowed as a boolean. * `outputs`: the output from the policy as a map of source (`policy-id#rule-name`) to value. * `validationErrors`: errors encountered when validating the principal and resource against the corresponding schemas as a list of [cerbos.schema.v1.ValidationError messages](https://github.com/cerbos/cerbos/blob/main/api/public/cerbos/schema/v1/schema.proto). Using the YAML scalar block syntax `>` for CEL expressions is recommended to avoid quoting issues. `action: "view"` would be interpreted as the CEL expression `view` (which would fail to compile because a variable named `view` is not defined), when the desired setting was the CEL expression `"view"` (which just returns the intended string literal). Minimal configuration ```yaml extensions: routeExtensions: foo: mapping: request: principal: id: > request.header["X-User-Id"] roles: - > request.header["X-User-Role"] resource: kind: > request.body.json.kind id: > request.body.json.id action: > "view" ``` Customizing the response extensions: routeExtensions: foo: mapping: request: # ... response: status: > check.allow ? 204 : 404 headers: X-Cerbos-Call-Id: > check.cerbosCallId body: > json.encode({"outputs": check.outputs}) ## [](#%5Fprogrammatic%5Fmapping)Programmatic mapping ```yaml extensions: routeExtensions: infra: (1) extension: extensionURL: "/extensions/infra.wasm?checksum=sha256:2b2e0ddc150e8563127ba9bd13199519b357c85ca5b3de598c4bcd5729599c6c" (2) configuration: (3) environment: staging routes: (4) "/team-a/foo": [] "/team-b/{baz}": ["GET"] ``` | **1** | Unique name for the extension | | ----- | -------------------------------------------------------------------------------------------------- | | **2** | URL to fetch the extension from. See [extension URL format](url-format.html) for more information. | | **3** | Configuration values needed by the extension. \[Optional\] | | **4** | Routes to which this mapping applies. See [Route patterns](#route-patterns). | ### [](#%5Fbuilding%5Froute%5Fextensions)Building route extensions See [Building custom Synapse extensions](building.html). Developing Starlark extensions ==================== [Starlark](https://github.com/google/starlark-go/blob/master/doc/spec.md) is a lightweight Python dialect that can be used to drive the Synapse extension logic. They are easy to write and test because they do not require any intermediate compilation stages or specialised tools. | | For an end-to-end walkthrough of building a Starlark proxy extension and running it against Synapse, see the [Quick Start](../quickstart.html). This page is the reference for the Starlark runtime and the request/response shapes each extension type receives. | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | To use a Starlark script as the extension implementation, set the `extensionURL` configuration field to a [valid extension URL](url-format.html) in one of the following forms: * `/path/to/script.star` (File name must have a `.star` extension) * `starlark+http://scripts.example.com/synapse/envoy?checksum=sha256:677cce2788330f16f27981130eaa50aa4189beab2121903bcea5b3c4c918dccc` (Protocol of the URL must be prefixed with `starlark+`) All Synapse extension points support running Starlark scripts. Each request is served by a fresh instance of a script and there’s no shared global state. Each extension type requires a specific set of functions that must be implemented by the script. See below for details about the API contracts for each extension type. ## [](#%5Fstarlark%5Fprimer)Starlark primer Starlark is a lightweight dialect of Python with syntax compatible with Python 3\. Refer to the [language specification](https://github.com/google/starlark-go/blob/master/doc/spec.md) for supported operators, keywords and types. Synapse extends the base language with [extra functions](#stdlib) and [loadable modules](#loadable-modules) that are useful for authoring extensions. The built-in [REPL](#repl) can be used to experiment with the language and debug your scripts. References * [Starlark language specification](https://github.com/google/starlark-go/blob/master/doc/spec.md) * [Introduction to Starlark and online playground](https://starlark-lang.org) * [Synapse standard library](#stdlib) ### [](#%5Fconstructing%5Finputoutput%5Fmessages%5Ffor%5Fextensions)Constructing input/output messages for extensions The [API contracts](#api-contracts) for the extensions require constructing specific message types whose schemas are defined as [protocol buffers (protobufs)](https://protobuf.dev/programming-guides/proto3/). The mapping rules for constructing protobuf values in Starlark are as follows: | Protobuf type | Starlark constructor | | ----------------------------------------------------------------------------------------------------- | ---------------------------------- | | [Scalar value types such as strings and ints](https://protobuf.dev/programming-guides/proto3/#scalar) | Corresponding Starlark scalar type | | Message objects | struct() | | Maps | dict or the {} short-hand | | repeated fields | list or the \[\] short-hand | | Enums | String value of the enum | | google.protobuf.Value | Corresponding Starlark scalar type | For example, a [Cerbos CheckResources request](https://buf.build/cerbos/cerbos-api/docs/main%3Acerbos.request.v1#cerbos.request.v1.CheckResourcesRequest) would be constructed as follows: ```python struct( (1) request_id = "test", (2) principal = struct(id = "john", roles = ["employee"]), (3) resources = [struct( (4) actions = ["view"], (5) resource = struct( kind = "invoice", id = "XX125", attr = { (6) "owner": "john", "department": "IT", "geography": "GB", "groups": ["it_admins", "employees"] } ) )] ) ``` | **1** | CheckResources is a protobuf Message | | ----- | ------------------------------------------- | | **2** | request\_id field is a string | | **3** | principal is another Message type | | **4** | resources is a repeated Message field | | **5** | actions is a repeated string field | | **6** | attr is a map | ### [](#reading-values)Reading values from messages Extensions also receive protobuf messages, including incoming requests, Cerbos responses and policy outputs. Scalars are mapped to the corresponding Starlark type. Enum fields are mapped to their string names: ```python if resp.results[0].actions["*"] == "EFFECT_ALLOW": # ... ``` Dynamically typed `google.protobuf.Value` fields (the values in an `attr` map or a policy output `out.val`, for example) are converted according to the runtime type of the stored value. Inspect them with the built-in `type()` function. | google.protobuf.Value | type() result | | --------------------- | ------------- | | String | "string" | | Boolean | "bool" | | Number | "float" | | List | "list" | | Struct / map | "dict" | | Null | "NoneType" | | | Every number in a google.protobuf.Value is read as a floating-point value: an integer such as 42 is read as 42.0 and type() reports "float". Convert it with int() where an integer is required. | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ### [](#field-naming)Field naming: struct versus dict Protobuf field names are snake\_case. The `struct()` constructor accepts either the snake\_case field name or its camelCase equivalent, so `aux_data` and `auxData` refer to the same field. A plain `{}` dict accepts only the exact snake\_case field name. ```python struct(aux_data = ...) # resolves struct(auxData = ...) # also resolves -- same field {"aux_data": ...} # resolves {"auxData": ...} # does NOT resolve to the aux_data field ``` When reading a field from a request message, use the snake\_case field name: ```python token = req.aux_data.jwt.token # not req.auxData.jwt.token ``` ## [](#api-contracts)API contracts for extensions The following sections describe the required functions that must be implemented by each extension type. Every extension type also supports a [manifest function](manifest.html) to document the extension. It is optional but highly recommended. ### [](#%5Fdata%5Fsource%5Fextensions)Data source extensions A script that implements a [data source](data-sources.html) must export a function named `lookup`. This function will be called by Synapse with an object in the following shape: Input ```python struct( data_source = "myDataSource", (1) query = "SELECT department FROM employees WHERE id = :employee_id", (2) query_parameters = { (3) "employee_id": "simon" }, cache_options = struct( (4) cache_key = "simon", (5) cache_expiry = time.minute, (6) if_not_exists = True (7) ) ) ``` | **1** | Name of the data source to query | | ----- | ------------------------------------------------------------------------------------------------------------------------------------------ | | **2** | Query to execute on the data source. This is specific to the data source. The value can be any type — including complex objects and arrays | | **3** | Optional query parameters as a dictionary with string keys and values of any type | | **4** | Optional cache options for caching the result of the lookup | | **5** | Cache key for this lookup | | **6** | Optional duration for caching the result of the lookup | | **7** | Optional flag to prevent overwriting an existing cache key | The function must return a struct that contains a field named `result` with the result of the lookup. ```python def lookup(req): result = {"output": "hello from starlark lookup", "query": req.query} return struct(result = result) ``` ### [](#envoy)Envoy extension An Envoy extension must export a function named `envoy_check` that accepts an object in the shape of [Envoy check request](https://buf.build/envoyproxy/envoy/docs/main:envoy.service.auth.v3#envoy.service.auth.v3.CheckRequest). The return value from the function should be one of the following: | Direct response | An [Envoy Check response](https://buf.build/envoyproxy/envoy/docs/main:envoy.service.auth.v3#envoy.service.auth.v3.CheckResponse) that will be sent as-is back to Envoy. | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Cerbos mapping | Construct a Cerbos CheckResources request from the Envoy Check request and the Check responses corresponding to ALLOW and DENY outcomes. Synapse handles sending the Cerbos request to the PDP and sending the Envoy response that corresponds to the result. | | Cerbos request | Construct a Cerbos CheckResources request from the Envoy Check request. Synapse sends the request to the PDP and invokes the extension again with the result. This second invocation allows the extension to produce a dynamic Check response by injecting values from the Cerbos response into it (e.g. policy outputs). | #### [](#%5Fdirect%5Fresponse)Direct response Return a complete [Envoy check response](https://buf.build/envoyproxy/envoy/docs/main:envoy.service.auth.v3#envoy.service.auth.v3.CheckResponse) to return back to the caller. This is useful for situations where you know the exact response to send such as a blanket deny for a hard-coded path. ```python def envoy_check(req): path = req.attributes.request.http.path # Deny the request if the path starts with /restricted if path.startswith("/restricted/"): return struct(envoy_check_response = struct( status = struct(code = 7), denied_response = struct(body = "Go away") )) # Allow the request to go through with one of the headers removed return struct(envoy_check_response = struct( status = struct(code = 0), ok_response = struct( headers_to_remove = ["x-confidential-header"] ) )) ``` The following example invokes a PDP call for any paths that are in the `secure_paths` configuration value of the extension. Extension configuration The following snippet demonstrates how to provide the `secure_paths` configuration for the extension using the Synapse configuration file. See [Configuration](../configuration/reference.html) for the full configuration reference. ```yaml extensions: envoyExternalAuthz: enabled: true extension: extensionURL: /extensions/envoy.star configuration: secure_paths: - "/admin/dashboard" - "/secure/path" ``` Example ```python def envoy_check(req): http_req = req.attributes.request.http path = http_req.path if path in context.extension_config["secure_paths"]: return struct(cerbos_mapping = struct( check_request = struct( principal = struct(id = "daffy", roles = ["user"]), resources = [struct( resource = struct(id = "x", kind = "request", attr = {"path": path}), actions = [http_req.method] )] ), allow_response = struct( status = struct(code = 0), ok_response = struct( headers_to_remove = ["bar"] ) ), deny_response = struct( status = struct(code = 7), denied_response = struct( body = "no go" ) ) )) return struct(envoy_check_response = struct( status = struct(code = 0), ok_response = struct( headers_to_remove = ["foo"] ) )) ``` #### [](#%5Fcerbos%5Fmapping)Cerbos mapping Return a struct that contains a Cerbos `CheckRequest` and the Envoy check response to send based on the Cerbos response. Synapse sends the Cerbos request to the configured PDP (applying any configured proxy extensions) and responds with the appropriate Envoy response based on whether Cerbos returns ALLOW or DENY. ```python def envoy_check(req): # Add code here to inspect the request and extract the information you need to pass to Cerbos # Return the Cerbos request and the corresponding Envoy response to return based on the decision from the PDP return struct(cerbos_mapping = struct( check_request = struct( (1) principal = struct( id = "daffy", roles = ["duck"] ), resources = [ struct( actions = ["GET"], resource = struct( id = "http", kind = "request", attr = { "path": "/foo" } ) ) ] ), allow_response = struct( (2) status = struct(code = 0) ), deny_response = struct( (3) status = struct(code = 7), denied_response = struct(body = "Go away") ) )) ``` | **1** | Cerbos CheckResources request to send | | ----- | ------------------------------------------------------ | | **2** | Envoy response to send if Cerbos returns EFFECT\_ALLOW | | **3** | Envoy response to send if Cerbos returns EFFECT\_DENY | #### [](#%5Fcerbos%5Fcheck%5Frequest)Cerbos check request Return a Cerbos `CheckRequest` to send to the PDP (applying any configured proxy extensions). In this mode, the extension must implement a second function named `map_cerbos_response` to map the `CheckResources` response to an Envoy response. Use this mode when you need to write complex mapping logic to convert a Cerbos response to an Envoy response. ```python def envoy_check(req): (1) # Add code here to inspect the request and extract the information you need to pass to Cerbos # Return the Cerbos request return struct(cerbos_check_request = struct( principal = struct( id = "daffy", roles = ["duck"] ), resources = [ struct( actions = ["GET"], resource = struct( id = "http", kind = "request", attr = { "path": "/foo" } ) ) ] )) def map_cerbos_response(resp): (2) # Inspect the Cerbos response here and construct the appropriate Envoy response # Return the constructed Envoy response return struct( status = struct(code = 0), ok_response = struct( headers_to_remove = ["baz"] ) ) ``` | **1** | First entrypoint which receives the Envoy Check request and returns a Cerbos CheckResources request | | ----- | -------------------------------------------------------------------------------------------------------------- | | **2** | Second entrypoint which receives the Cerbos CheckResources response and returns the final Envoy Check response | Synapse will send the Cerbos `CheckResources` request returned by the `envoy_check` function to the PDP and then invoke the extension a second time by calling the `map_cerbos_response` function with an object of the following shape: ```python struct( envoy_request = struct( ... ), (1) cerbos_request = struct( ... ), (2) cerbos_response = struct( ... ) (3) ) ``` | **1** | Original Envoy Check request that triggered this chain | | ----- | ------------------------------------------------------ | | **2** | Cerbos CheckResources request | | **3** | Cerbos CheckResources response | The `map_cerbos_response` function should take the above input and return a struct representing an [Envoy check response](https://buf.build/envoyproxy/envoy/docs/main:envoy.service.auth.v3#envoy.service.auth.v3.CheckResponse) which will be sent back to the caller. ### [](#proxy)Proxy extensions A [proxy extension](proxy-extensions.html) must export at least one of the following functions. | augment\_authzen\_evaluation\_batch\_request | Modify an AuthZEN AccessEvaluations request | | --------------------------------------------- | -------------------------------------------- | | augment\_authzen\_evaluation\_batch\_response | Modify an AuthZEN AccessEvaluations response | | augment\_authzen\_evaluation\_request | Modify an AuthZEN AccessEvaluation request | | augment\_authzen\_evaluation\_response | Modify an AuthZEN AccessEvaluation response | | augment\_check\_request | Modify a CheckResources request | | augment\_check\_response | Modify a CheckResources response | | augment\_plan\_request | Modify a PlanResources request | | augment\_plan\_response | Modify a PlanResources response | Each function is invoked with an object in the shape of a `AccessEvaluation`/`CheckResources`/`PlanResources` request or response. The function can modify the object as required and return it back. Request/Response schemas * [AuthZEN AccessEvaluationBatchRequest](https://buf.build/cerbos/cerbos-api/docs/main%3Aauthzen.authorization.v1#authzen.authorization.v1.AccessEvaluationBatchRequest) * [AuthZEN AccessEvaluationBatchResponse](https://buf.build/cerbos/cerbos-api/docs/main%3Aauthzen.authorization.v1#authzen.authorization.v1.AccessEvaluationBatchResponse) * [AuthZEN AccessEvaluationRequest](https://buf.build/cerbos/cerbos-api/docs/main%3Aauthzen.authorization.v1#authzen.authorization.v1.AccessEvaluationRequest) * [AuthZEN AccessEvaluationResponse](https://buf.build/cerbos/cerbos-api/docs/main%3Aauthzen.authorization.v1#authzen.authorization.v1.AccessEvaluationResponse) * [Cerbos CheckResourcesRequest](https://buf.build/cerbos/cerbos-api/docs/main%3Acerbos.request.v1#cerbos.request.v1.CheckResourcesRequest) * [Cerbos CheckResourcesResponse](https://buf.build/cerbos/cerbos-api/docs/main%3Acerbos.response.v1#cerbos.response.v1.CheckResourcesResponse) * [Cerbos PlanResourcesRequest](https://buf.build/cerbos/cerbos-api/docs/main%3Acerbos.request.v1#cerbos.request.v1.PlanResourcesRequest) * [Cerbos PlanResourcesResponse](https://buf.build/cerbos/cerbos-api/docs/main%3Acerbos.response.v1#cerbos.response.v1.PlanResourcesResponse) #### [](#%5Fexample%5Finject%5Fhard%5Fcoded%5Fattributes%5Finto%5Fcerbos%5Fcheckresourcesplanresources%5Fcalls)Example: inject hard-coded attributes into Cerbos CheckResources/PlanResources calls The following example injects hard-coded attributes to a known principal. ```python def handle_augment(req): if req.principal.id == "john": req.principal.attr = { "geography": "GB", "department": "IT" } return req def augment_check_request(req): return handle_augment(req) def augment_plan_request(req): return handle_augment(req) ``` #### [](#proxy-example-data-source)Example: enrich principals from a data source Consider a Cerbos policy that requires attributes about the principal. resource.yaml ```yaml apiVersion: api.cerbos.dev/v1 resourcePolicy: resource: "invoice" version: "default" rules: - actions: ["view"] effect: EFFECT_ALLOW roles: ["user"] condition: match: expr: P.attr.employee.department == R.attr.department ``` A Synapse proxy extension can read the `principal.id` from each `CheckResources` and `PlanResources` request, look up the principal from a data source named `employees` (for example, the [built-in sqldb data source backed by PostgreSQL](data-sources.html#postgresql)), and inject the found attributes into `principal.attr.employee` for policies to use. ```python QUERY = "SELECT department, country, manager_email FROM employees WHERE id = :uid" def enrich(req): if req.principal.id == "": return req resp = cerbos.data_source_lookup( (1) datasource = "employees", query = QUERY, query_parameters = {"uid": req.principal.id}, (2) cache_key = "employee:" + req.principal.id, (3) cache_expiry = 5 * time.minute, ) if resp == None or resp.result == None: (4) return req req.principal.attr["employee"] = resp.result return req def augment_check_request(req): return enrich(req) def augment_plan_request(req): return enrich(req) ``` | **1** | Call into the data source by its configured name. The same cerbos.data\_source\_lookup function works for any configured data source, built-in or custom. | | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **2** | Bind query parameters using query\_parameters rather than string-concatenating values into the SQL. Named placeholders (:uid) keep the query safe from injection. | | **3** | cache\_key and cache\_expiry are optional. When set, repeated lookups for the same principal are served from the Synapse cache without hitting the database. | | **4** | resp.result is None if the query returned no rows, a single object (column to value) for a single-row result, or a list of objects for multi-row results. | #### [](#proxy-preserve-attributes)Preserve caller-supplied attributes Assigning a new map to `req.principal.attr` replaces all existing attributes. Assign individual keys to preserve caller-supplied values: ```python def augment_check_request(req): if req.principal.id == "": return req if "client_type" not in req.principal.attr: req.principal.attr["client_type"] = _derive(req) return req ``` Declared protobuf message fields always exist in Starlark, so `hasattr(req, "principal")` does not test whether the caller supplied a principal. Test `req.principal.id` instead. ### [](#route)Route extensions A [route extension](route-extensions.html) must export a function named `handle_http_route` which receives an object describing the HTTP request received and return one of the supported result types to produce the HTTP response. | Direct response | A HTTP response that will be sent as-is back to the caller. | | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Cerbos mapping | Construct a Cerbos CheckResources request from the HTTP request and the HTTP responses corresponding to ALLOW and DENY outcomes. Synapse handles sending the Cerbos request to the PDP and responding to the caller with the HTTP response that corresponds to the result. | | Cerbos request | Construct a Cerbos CheckResources request from the HTTP request. Synapse sends the request to the PDP and invokes the extension again with the result. This second invocation allows the extension to produce a dynamic HTTP response by injecting values from the Cerbos response into it (e.g. policy outputs). | Route extensions are mounted under the `/ext/` path prefix at the Synapse listen address, and that prefix is not stripped before the request reaches the extension. For a route configured as `/foo` and a client request to `POST /ext/foo?a=av&b=bv1&b=bv2`, the input takes the following form. ```python struct( method = "POST", (1) headers = { (2) "X-Forwarded-For": struct(values = ["127.0.0.1:2090"]) }, raw_url = "https://example.com/ext/foo?a=av&b=bv1&b=bv2", (3) host = "example.com", (4) path = "/ext/foo", (5) query_params = { (6) "a": struct(values = ["av"]), "b": struct(values = ["bv1", "bv2"]) }, body = "Hello" (7) ) ``` | **1** | HTTP method. | | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **2** | Headers and their values. Header names use [canonical form](https://stringcase.org/cases/train/) (X-Forwarded-For). This field is a protobuf map; use indexing or the in operator because .get is not available. | | **3** | The URL including the /ext/ prefix and any query parameters. This will be a relative URL without the protocol and host unless it’s an absolute request sent via a proxy. | | **4** | Host segment from the URL. | | **5** | Path segment, including the /ext/ prefix. | | **6** | Query parameters from the URL. This field is also a protobuf map. | | **7** | Request body as a bytes string. | The return value of the function must be one of the following types. #### [](#%5Fdirect%5Fresponse%5F2)Direct response Return the complete HTTP response that should be sent back to the caller. ```python def handle_http_route(req): # Add code here to process the request and determine a response. # Return the response return struct(http_response = struct( status = 401, (1) headers = { (2) "Content-Type": struct(values = ["application/text"]) }, body = "Access denied" (3) )) ``` | **1** | HTTP status code | | ----- | ------------------------------ | | **2** | Headers to add to the response | | **3** | Body of the response | #### [](#%5Fcerbos%5Fmapping%5F2)Cerbos mapping Return an object that contains a Cerbos `CheckResources` request and the HTTP response to send based on the response to that Cerbos request. Synapse sends the Cerbos request to the configured PDP (applying any configured proxy extensions) and responds with the appropriate HTTP response based on whether Cerbos returns ALLOW or DENY. ```python def handle_http_route(req): # Add code here to examine the request and construct a Cerbos CheckResources request from it. # Return the Cerbos request and the HTTP response to send based on the Cerbos response. return struct(cerbos_mapping = struct( check_request = struct( (1) principal = struct( id = "daffy", roles = ["duck"] ), resources = [ struct( actions = ["GET"], resource = struct( id = "http", kind = "request", attr = { "path": "/foo" } ) ) ] ), allow_response = struct( (2) status = 200, body = "Welcome" ), deny_response = struct( (3) status = 401, body = "Access denied" ) )) ``` | **1** | Cerbos CheckResources request | | ----- | ----------------------------------------------------- | | **2** | HTTP response to send if Cerbos returns EFFECT\_ALLOW | | **3** | HTTP response to send if Cerbos returns EFFECT\_DENY | ##### [](#%5Fexample)Example ```python def handle_http_route(req): return struct(cerbos_mapping = struct( check_request = struct( principal = struct(id = "daffy", roles = ["user"]), resources = [struct( resource = struct(id = "x", kind = "request", attr = {"path": req.path}), actions = [req.method] )] ), allow_response = struct( status = 200, headers = {"foo": struct(values = ["foo"])}, body = "Welcome" ), deny_response = struct( status = 403, headers = {"foo": struct(values = ["foo"])}, body = "No entry" ) )) ``` #### [](#%5Fcerbos%5Frequest)Cerbos request Return an object that contains a Cerbos `CheckResources` request. In this mode, the extension must export a second function named `handle_cerbos_response` that accepts the `CheckResources` response and returns a HTTP response. Use this mode when you want to construct a complex HTTP response using values from the Cerbos response. ```python def handle_http_route(req): # Add code here to inspect the request and construct a Cerbos CheckResources request from it. # Return the Cerbos request constructed above return struct(check_request = struct( principal = struct( id = "daffy", roles = ["duck"] ), resources = [ struct( actions = ["GET"], resource = struct( id = "http", kind = "request", attr = { "path": "/foo" } ) ) ] )) def handle_cerbos_response(resp): # Add code here to inspect the Cerbos response and construct the appropriate HTTP response # Return the constructed HTTP response return struct(http_response = struct( status = 401, headers = { "Content-Type": struct(values = ["application/text"]) }, body = "Access denied" )) ``` Synapse sends the Check request to the PDP and then invokes the `handle_cerbos_response` function of the extension with the following information. ```python struct( http_request = struct( ... ), (1) cerbos_request = struct( ... ), (2) cerbos_response = struct( ... ) (3) ) ``` | **1** | Original HTTP request that triggered this route extension | | ----- | --------------------------------------------------------- | | **2** | Cerbos CheckResources request | | **3** | Cerbos CheckResources response | `handle_cerbos_response` should return a HTTP response to send back to the caller. ```python struct( status = 401, headers = { "Content-Type": struct(values = ["application/json"]) }, body = "Access denied" ) ``` ## [](#jwt-verification)Passing JWTs for verified claims Synapse extensions do not verify JWTs. To make verified claims available to a policy, pass the raw token to the configured PDP in the request’s `aux_data`. The PDP verifies the signature against a configured JWKS key set before exposing the claims to policies. ```python resp = cerbos.check_resources(struct( principal = struct(id = "john", roles = ["user"]), resources = [struct( actions = ["view"], resource = struct(kind = "invoice", id = "XX125") )], aux_data = struct( jwt = struct( token = raw_token, (1) key_set_id = "my-idp" (2) ) ) )) ``` | **1** | The raw, encoded JWT to forward to the PDP for verification. | | ----- | ------------------------------------------------------------ | | **2** | The id of the JWKS key set used to verify the token. | | | If pdp.inProcess is omitted or empty, Synapse disables JWT signature verification for the in-process PDP. Configure a JWKS key set to verify tokens before using their claims in policies: pdp: inProcess: auxData: jwt: keySets: \- id: my-idp remote: url: https://issuer.example.com/.well-known/jwks.json acceptableTimeSkew: 2s The key set id must match the key\_set\_id passed in aux\_data. When pdp.inProcess is configured without a key set, verification fails and no claims are extracted. See [Configuration](../configuration/reference.html) for the Synapse-specific pdp.inProcess keys. See [Cerbos PDP configuration](../../../cerbos/latest/configuration/index.html) for the complete JWT verification configuration. | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#repl)Starlark REPL Synapse ships with a REPL (read, evaluate, print loop) to help with debugging Starlark scripts. ```sh Synapse starlark repl [--exec] [SCRIPT_FILE] ``` Without any arguments, the command starts the REPL where you can write expressions and evaluate them. If started with a script file as the argument, it executes the script and populates the globals with symbols exported by the script (variables, function definitions and so on). If the `--exec` argument is provided, the REPL exits after executing the script and printing its outputs. Press Ctrl+D to exit the REPL. You can load script files from disk using the `load` function. >>> load("my_script.star", "my_script") >>> my_script.foo("bar") # Executes the `foo` function exported by the script. ## [](#stdlib)Starlark standard environment In addition to the [built-in language constructs](https://github.com/google/starlark-go/blob/master/doc/spec.md), the following variables and functions are available to use by any script. | Name | Description | | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | cerbos.cache\_delete(key) | Delete the cached value from Synapse cache | | cerbos.cache\_get(key) | Return the cached value from Synapse cache | | cerbos.cache\_set(key, value, expiry?, if\_not\_exists?) | Save a value (string or bytes) to shared Synapse cache with optional expiry and existence check | | cerbos.check\_resources(req) | Do a CheckResources call to the PDP. Use the struct function to construct the request | | cerbos.data\_source\_lookup(datasource, query, query\_parameters?, cache\_key?, cache\_expiry?, cache\_if\_not\_exists?) | Do a lookup using one of the configured data sources. Cache options are passed as flat keyword arguments on the caller side; the **inbound** request struct received by a data source’s lookup function nests them under cache\_options | | cerbos.plan\_resources(req) | Do a PlanResources call to the PDP. Use the struct function to construct the request | | context.extension\_config | The optional configuration map attached to the extension definition in the Synapse configuration file | | context.extension\_kind | The kind of the extension. One of datasource, envoy, proxy or route | | context.extension\_name | The name of this extension as defined in the Synapse configuration file | | math.ceil(x) | Ceiling of x | | math.fabs(x) | Absolute value of x as a float | | math.floor(x) | Floor of x | | math.mod(x, y) | Value of x modulo y | | math.pow(x, y) | Value of x raised to the power of y | | math.remainder(x, y) | Remainder of x/y | | math.round(x) | Round x to nearest integer | | struct() | Create structs. E.g. struct(k1 = "foo", k2 = "bar) | | time.from\_timestamp(sec, nsec?) | Converts the given Unix time corresponding to the number of seconds and (optionally) nanoseconds since January 1, 1970 UTC into an object of type Time | | time.hour | A constant representing a duration of one hour | | time.is\_valid\_timezone(loc) | Reports whether loc is a valid time zone name | | time.microsecond | A constant representing a duration of one microsecond | | time.millisecond | A constant representing a duration of one millisecond | | time.minute | A constant representing a duration of one minute | | time.nanosecond | A constant representing a duration of one nanosecond | | time.now() | Returns the current local time | | time.parse\_duration(d) | Parses the given duration string. For more details, refer to | | time.parse\_time(x, format?, location?) | Parses the given time string using a specific time format and location. The expected arguments are a time string (mandatory), a time format (optional, set to RFC3339 by default, e.g. "2021-03-22T23:20:50.52Z") and a name of location (optional, set to UTC by default). For more details, refer to and . | | time.second | A constant representing a duration of one second | | time.time(year?, month?, day?, hour?, minute?, second?, nanosecond?, location?) | Returns the Time corresponding to yyyy-mm-dd hh:mm:ss + nsec nanoseconds in the appropriate zone for that time in the given location. All the parameters are optional. | ### [](#loadable-modules)Loadable modules Optional modules can be imported using the `load` function. ```python # Load the "json" module as "json" which allows you to invoke its functions like `json.dumps` and so on. load("json", "json") print(json.dumps(x)) # Load the "hashlib" module as "hash" which allows you to invoke its functions like `hash.sha256` and so on. load("hashlib", hash="hashlib") print(hash.sha256(c)) ``` The following modules are available to load. #### [](#%5Fbase64)`base64` | Function | Description | Signature | | ------------- | ---------------------------------- | ---------------------------------------- | | base64.decode | Decode base64 string to plain text | decode(src,encoding="standard") → string | | base64.encode | Encode string to base64 | encode(src,encoding="standard") → string | Valid values for `encoding` are `standard`, `standard_raw`, `url` and `url_raw`. [Source](https://github.com/1set/starlet/blob/master/lib/base64/README.md) #### [](#%5Fcsv)`csv` | Function | Description | Signature | | --------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | csv.read\_all | Read CSV string into list of string lists | read\_all(source, comma=",", comment="", lazy\_quotes=False, trim\_leading\_space=False, fields\_per\_record=0, skip=0, limit=0) → \[\]\[\]string | | csv.write\_all | Write list of string lists to CSV string | write\_all(source, comma=",") → string | | csv.write\_dict | Write list of dictionaries to CSV string | write\_dict(data, header, comma=",") → string | [Source](https://github.com/1set/starlet/blob/master/lib/csv/README.md) #### [](#%5Fhashlib)`hashlib` | Function | Description | Signature | | -------------- | ---------------------- | --------------------- | | hashlib.md5 | Calculate MD5 hash | md5(data) → string | | hashlib.sha1 | Calculate SHA-1 hash | sha1(data) → string | | hashlib.sha256 | Calculate SHA-256 hash | sha256(data) → string | | hashlib.sha512 | Calculate SHA-512 hash | sha512(data) → string | [Source](https://github.com/1set/starlet/blob/master/lib/hashlib/README.md) #### [](#%5Fhttp)`http` | Function | Description | Signature | | ----------------- | ------------------------------------------ | -------------------------------- | | http.call | Perform HTTP request with specified method | call(method, url, …​) → response | | http.delete | Perform HTTP DELETE request | delete(url, …​) → response | | http.get | Perform HTTP GET request | get(url, …​) → response | | http.get\_timeout | Get current timeout setting | get\_timeout() → float | | http.options | Perform HTTP OPTIONS request | options(url, …​) → response | | http.patch | Perform HTTP PATCH request | patch(url, …​) → response | | http.post | Perform HTTP POST request | post(url, …​) → response | | http.postForm | Perform HTTP POST request with form data | postForm(url, …​) → response | | http.put | Perform HTTP PUT request | put(url, …​) → response | | http.set\_timeout | Set global timeout for requests | set\_timeout(timeout) | [Source](https://github.com/1set/starlet/blob/master/lib/http/README.md) #### [](#%5Fjson)`json` | Function | Description | Signature | | ---------------- | --------------------------------------------- | ------------------------------------------------- | | json.decode | Convert JSON to value with optional default | decode(x,default?) → any | | json.dumps | Convert value to JSON string with indentation | dumps(obj, indent=0) → string | | json.encode | Convert value to JSON | encode(x) → string | | json.eval | Evaluate JSONPath expression | eval(data, expr) → value | | json.indent | Pretty-print JSON with indentation | indent(str, prefix="", indent="\\t") → string | | json.path | Query JSON using JSONPath | path(data, path) → list | | json.try\_decode | Decode JSON with error handling | try\_decode(x) → tuple | | json.try\_dumps | Dump JSON with error handling | try\_dumps(obj, indent=0) → tuple | | json.try\_encode | Encode JSON with error handling | try\_encode(x) → tuple | | json.try\_eval | Evaluate JSONPath with error handling | try\_eval(data, expr) → tuple | | json.try\_indent | Indent JSON with error handling | try\_indent(str, prefix="", indent="\\t") → tuple | | json.try\_path | Query JSONPath with error handling | try\_path(data, path) → tuple | [Source](https://github.com/1set/starlet/blob/master/lib/json/README.md) #### [](#%5Foauth)`oauth` | Function | Description | Signature | | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | oauth.client\_credentials\_client | Create an HTTP client that attaches a token obtained using OAuth client credentials flow to every request. The scopes optional parameter is a list of scopes for the token and endpoint\_params is an optional dictionary of key-value parameters required by the OAuth endpoint. See below for explanation of persist\_key parameter. The returned object has methods that are identical to the functions exposed by the http module. | client\_credentials\_client(token\_url, client\_id, client\_secret, scopes?, endpoint\_params?, persist\_key?) → http\_client | | | Synapse extensions are isolated instances and any objects created by a handler function are request-scoped (they survive only for the duration of the request). However, the oauth module allows callers to specify an optional persist\_key parameter to help persist the authenticated client for longer and share it with multiple requests. This is to avoid constantly re-authenticating with the upstream OAuth server for every request. The runtime only considers the value of the persist\_key when deciding whether to create a new instance of the client or reuse and existng one. Therefore, ensure that the persist\_key is unique per combination of parameters required for performing OAuth flows. | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Example ```python load("oauth", "oauth") def do_oauth_request(): client = oauth.client_credentials_client(token_url=TOKEN_URL, client_id=CLIENT_ID, client_secret=CLIENT_SECRET, scopes=[SCOPE], endpoint_params={"foo": "bar"}, persist_key=PERSIST_KEY) resp = client.get(AUTHENTICATED_ENDPOINT) return resp.status_code ``` #### [](#%5Frandom)`random` | Function | Description | Signature | | ---------------- | -------------------------------------- | ---------------------------------------------------------------- | | random.choice | Return random element from sequence | choice(seq) → any | | random.choices | Return k-sized list of random elements | choices(population, weights=None, cum\_weights=None, k=1) → list | | random.randb32 | Generate random base32 string | randb32(n, sep) → string | | random.randbytes | Generate random byte string | randbytes(n) → bytes | | random.randint | Return random integer in range | randint(a,b) → int | | random.random | Return random float in \[0.0, 1.0) | random() → float | | random.randstr | Generate random string from charset | randstr(chars, n) → string | | random.shuffle | Shuffle sequence in place | shuffle(x) | | random.uniform | Return random float in range | uniform(a, b) → float | | random.uuid | Generate random UUID | uuid() → string | [Source](https://github.com/1set/starlet/blob/master/lib/random/README.md) #### [](#%5Fre)`re` | Function | Description | Signature | | ---------- | --------------------------------- | --------------------------------------------------- | | re.compile | Compile regex pattern | compile(pattern) → Pattern | | re.findall | Find all matches in string | findall(pattern, text, flags=0) → list | | re.match | Match pattern at string start | match(pattern, string, flags=0) → match | | re.search | Search for pattern in string | search(pattern,string,flags=0) → match | | re.split | Split string by pattern | split(pattern, text, maxsplit=0, flags=0) → list | | re.sub | Replace pattern matches in string | sub(pattern, repl, text, count=0, flags=0) → string | [Source](https://github.com/1set/starlet/blob/master/lib/re/README.md) #### [](#%5Fstats)`stats` | Function | Description | Signature | | ------------------------------- | ------------------------------------ | -------------------------------------------- | | stats.average | Calculate mean (alias) | average(data) → float | | stats.correlation | Calculate correlation coefficient | correlation(data1, data2) → float | | stats.covariance | Calculate covariance | covariance(data1, data2) → float | | stats.covariance\_population | Calculate population covariance | covariance\_population(data1, data2) → float | | stats.euclidean\_distance | Calculate Euclidean distance | euclidean\_distance(data1, data2) → float | | stats.geometric\_mean | Calculate geometric mean | geometric\_mean(data) → float | | stats.harmonic\_mean | Calculate harmonic mean | harmonic\_mean(data) → float | | stats.manhattan\_distance | Calculate Manhattan distance | manhattan\_distance(data1, data2) → float | | stats.max | Find maximum value | max(data) → float | | stats.mean | Calculate arithmetic mean | mean(data) → float | | stats.median | Find median value | median(data) → float | | stats.midrange | Calculate midrange | midrange(data) → float | | stats.min | Find minimum value | min(data) → float | | stats.mode | Find most frequent values | mode(data) → list | | stats.pearson | Calculate Pearson correlation | pearson(data1, data2) → float | | stats.percentile | Calculate percentile | percentile(data, p) → float | | stats.percentile\_nearest\_rank | Calculate percentile by nearest rank | percentile\_nearest\_rank(data, p) → float | | stats.population\_variance | Calculate population variance | population\_variance(data) → float | | stats.sample | Randomly sample elements | sample(data, take, replace=False) → list | | stats.sample\_variance | Calculate sample variance | sample\_variance(data) → float | | stats.sigmoid | Apply sigmoid function | sigmoid(data) → list | | stats.softmax | Apply softmax function | softmax(data) → list | | stats.standard\_deviation | Calculate standard deviation | standard\_deviation(data) → float | | stats.stddev | Calculate standard deviation (alias) | stddev(data) → float | | stats.stddev\_sample | Calculate sample standard deviation | stddev\_sample(data) → float | | stats.sum | Calculate sum | sum(data) → float | | stats.trimean | Calculate trimean | trimean(data) → float | | stats.variance | Calculate variance | variance(data) → float | [Source](https://github.com/1set/starlet/blob/master/lib/stats/README.md) #### [](#%5Fstring)`string` | Function | Description | Signature | | ---------------- | ------------------------------------ | --------------------------------- | | string.codepoint | Get Unicode codepoint at index | codepoint(s, index) → string | | string.escape | Escape HTML entities | escape(str) → string | | string.find | Find first occurrence of substring | find(s, sub) → int | | string.index | Find first occurrence (raises error) | index(s, sub) → int | | string.length | Get length in Unicode code points | length(obj) → int | | string.quote | Shell-escape string | quote(str) → string | | string.reverse | Reverse string | reverse(str) → string | | string.rfind | Find last occurrence of substring | rfind(s, sub) → int | | string.rindex | Find last occurrence (raises error) | rindex(s, sub) → int | | string.substring | Extract substring | substring(s, start, end) → string | | string.unescape | Unescape HTML entities | unescape(str) → string | | string.unquote | Shell-unescape string | unquote(str) → string | [Source](https://github.com/1set/starlet/blob/master/lib/string/README.md) Testing custom extensions ==================== The typical development loop for custom extensions would be as follows: 1. Write the extension code 2. Start a Synapse instance with the extension loaded 3. Send requests to Synapse using a Cerbos SDK or a tool like cURL to exercise the extension Synapse includes a testing framework to simplify the above process. It enables test-driven development and automated testing of custom extensions under various conditions. Tests are written as Starlark scripts but they can be used to test any kind of WASM or Starlark extension. For each test suite, the test runner starts a fresh instance of Synapse using the configuration defined in the suite and executes the test functions against that instance. There is a rich library of utility functions available to craft and send requests to exercise the extensions under test. Other ways of testing Using the testing framework provided by Synapse is not mandatory. Most programming languages have support for starting a Docker container programmatically for tests. The [Testcontainers project](https://docs.docker.com/testcontainers/) is one such example. You can author tests in your preferred language and use a container testing library to spin up Synapse containers you need. Most CI systems also have a concept of "services" where you can configure the CI runner to start a container of your choice and make it available for the duration of the test run. Refer to the documentation of your CI system for more information. ## [](#%5Fa%5Fsimple%5Ftest%5Fsuite)A simple test suite The test runner expects test suites to be defined in files that have names ending with `_test.star` such as `my_awesome_test_suite_test.star`. Test cases must be functions that begin with the prefix `test_` and accept a single `context` parameter. Any function that doesn’t start with `test_` will be ignored by the test runner so you are free to define other functions that don’t begin with `test_` to help you organize your test code. The following example shows a simple test suite with a test case named `always_pass` that always passes and another one called `always_fail` that always fails. example\_test.star ```python # This test case always passes def test_always_pass(context): return testing.ok("I always pass") # This test case always fails def test_always_fail(context): return testing.fail("I always fail") # This function is ignored by the test runner because it doesn't start with 'test_' def ignored(): pass ``` Save the contents of the above as `example_test.star` and execute the test suite. ```console $ docker run \ -v $(pwd):/tests:ro \ CERBOS_DISTRIBUTION_REPO/synapse/synapse:latest \ test /tests ``` It should produce output similar to the following: == FAIL:example (/tests/example_test.star) Passed=1 Failed=1 Skipped=0 --- FAIL:always_fail | I always fail --- PASS:always_pass | I always pass The above example is a bare minimum test suite to illustrate how the test runner works. Read on to learn how to write more useful tests that actually exercise your extensions. ## [](#%5Fdefining%5Ftest%5Fsuite%5Fparameters)Defining test suite parameters The test runner provisions a fresh Synapse instance for each test suite. By default it is a basic server with no extensions and an in-process PDP configured to read policies from a directory named `policies` in the current working directory. You can provide your own Synapse configuration to use in the test suite by defining a `test_suite` object. example\_test.star ```python test_suite = struct( synapse_config = testing.load_synapse_config("synapse.yaml"), ) ``` The above example instructs the test runner to start a Synapse instance using the configuration read from the file named `synapse.yaml`. It should be a valid configuration file as documented in [Configuration](../configuration/reference.html). It is also possible to provide Synapse configuration inline as a Starlark dictionary. example\_test.star ```python test_suite = struct( synapse_config = { "pdp": { "inProcess": { "storage": { "driver": "disk", "disk": { "directory": "${SYNAPSE_ROOT}/policies" } } } } } ) ``` | | The test runner sets the SYNAPSE\_ROOT environment variable to the directory of the test file. You can use this in the configuration file to reference other files that are stored relative to the test file. | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | By default the test suite name is derived from the file name. You can provide a more descriptive name using the `name` attribute. example\_test.star ```python test_suite = struct( name = "Tests for extension foo", synapse_config = testing.load_synapse_config("synapse.yaml") ) ``` Set the `skip` attribute to make the test runner skip executing the tests defined in this test suite. example\_test.star ```python test_suite = struct( name = "Tests for extension foo", synapse_config = testing.load_synapse_config("synapse.yaml"), skip = True ) ``` If an attribute named `test_cases` is defined, it specifies the test functions that belong to this test suite. It overrides the implicit test discovery mechanism and the test runner will only run the functions that are listed in the `test_cases` attribute. example\_test.star ```python def always_fail(context): return testing.fail("I always fail") test_suite = struct( name = "Tests for extension foo", synapse_config = testing.load_synapse_config("synapse.yaml"), test_cases = { "always_pass": lambda context: testing.ok("I always pss"), (1) "always_fail": always_fail (2) } ) ``` | **1** | A test function defined inline as a lambda | | ----- | ---------------------------------------------------------- | | **2** | Reference to a test function defined elsewhere in the file | | | In the example above, the always\_fail function needs to be declared first before it’s referenced by the test\_suite object because Starlark is a dialect of Python that always evaluates top to bottom. Things must be declared before they are referenced. | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#%5Fthe%5Ftesting%5Fmodule)The `testing` module Every test suite has access to the built-in `testing` module which provides utilty functions for writing Synapse tests. __Table 1\. Functions available in the testing module__ | Name | Description | | ----------------------------------- | ------------------------------------------------------------------------------------------------------ | | testing.assert(expression) | Assert that the expression evaluates to true | | testing.load\_synapse\_config(path) | Load a Synapse configuration file from the given path | | testing.load\_testdata(path) | Load a test data file from the give path (see [reusing test data](#testdata) section for more details) | | testing.ok(message) | Return this to explicitly mark the test as passed | | testing.fail(message) | Return this to explicitly mark the test as failed | | testing.skip(message) | Return this to explicitly mark the test as skipped | ## [](#%5Fsetting%5Fthe%5Fstatus%5Ffor%5Fa%5Ftest)Setting the status for a test The return value of the test function determines its status. If the function returns nothing, a `True` value or `testing.ok()`, it’s considered to have passed. Triggering a runtime error, returning `False` or `testing.fail()` marks the test as failed. Returning `testing.skip()` marks the test as skipped. ## [](#%5Fthe%5Ftest%5Fcase%5Fcontext)The test case context The `context` object passed to each test case function contains helper methods to make requests to the Synapse instance of that test suite. example\_test.star ```python test_suite = struct( name = "Illustrate how to exercise different extension types", synapse_config = testing.load_synapse_config("synapse.yaml"), ) # Send a Cerbos CheckResources request to exercise configured proxy extensions def test_check_resources(context): request = struct( request_id = "test", principal = struct(id = "john", roles = ["employee"]), resources = [struct( actions = ["view"], resource = struct( kind = "invoice", id = "XX125", attr = { "owner": "john", "department": "IT", "geography": "GB" } ) )] ) have = context.check_resources(request) return testing.assert(have.results[0].actions["view"] == "EFFECT_ALLOW") # Send a Cerbos PlanResources request to exercise configured proxy extensions def test_plan_resources(context): request = struct( request_id = "test", principal = struct(id = "john", roles = ["employee"]), actions = ["view"], resource = struct(kind = "invoice") ) have = context.plan_resources(request) return testing.assert(set(have.actions) == set(["view"]) and have.filter.kind == "KIND_CONDITIONAL") # Send an AuthZEN access evaluation request to exercise configured proxy extensions def test_access_evaluation(context): request = struct( subject = struct( type = "user", id = "john", properties = {"cerbos.roles": ["employee"]} ), resource = struct( type = "invoice", id = "XX125", properties = { "owner": "john", "department": "IT", "geography": "GB" } ), action = struct(name = "view") ) have = context.access_evaluation(request) return testing.assert(have.decision) # Send an AuthZEN access evaluation batch request to exercise configured proxy extensions def test_access_evaluation_batch(context): request = struct( subject = struct( type = "user", id = "john", properties = {"cerbos.roles": ["employee"]} ), resource = struct( type = "invoice", id = "XX125", properties = { "owner": "john", "department": "IT", "geography": "GB" } ), evaluations = [struct(action = struct(name = "view"))] ) have = context.access_evaluation_batch(request) return testing.assert(have.evaluations[0].decision) # Send an extenal authz request to the Envoy extension def test_envoy_check(context): request = struct( attributes = struct( source = struct( principal = "spiffe://cerbos.dev/cerbie" ), request = struct( http = struct( id = "foo", method = "GET", path = "/foo?op=check_response" ), ), ), ) have = context.envoy_check(request) return testing.assert(have.status.code == 0) # Send an HTTP request to a configured route extension def test_route_extension(context): have = context.http_get("/ext/foo", {"op": "http_response"}) return testing.assert(have.status_code == 200 and have.body() == "Welcome") ``` __Table 2\. Functions available in the context object__ | Name | Description | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | | context.access\_evaluation(request) | Send an AuthZEN access evaluation request | | context.access\_evaluation\_batch(request) | Send an AuthZEN access evaluation batch request | | context.check\_resources(request) | Send a Cerbos CheckResources request | | context.envoy\_check(request) | Send an Envoy Check request | | context.http\_delete(path, params=None, headers=None, auth=(), body=None, json\_body=None, form\_body=None, form\_encoding="", timeout=30, allow\_redirects=True, verify=True) | Send an HTTP DELETE request to the path. | | context.http\_get(path, params=None, headers=None, auth=(), body=None, json\_body=None, form\_body=None, form\_encoding="", timeout=30, allow\_redirects=True, verify=True) | Send an HTTP GET request to the path. | | context.http\_head(path, params=None, headers=None, auth=(), body=None, json\_body=None, form\_body=None, form\_encoding="", timeout=30, allow\_redirects=True, verify=True) | Send an HTTP HEAD request to the path. | | context.http\_options(path, params=None, headers=None, auth=(), body=None, json\_body=None, form\_body=None, form\_encoding="", timeout=30, allow\_redirects=True, verify=True) | Send an HTTP OPTIONS request to the path. | | context.http\_patch(path, params=None, headers=None, auth=(), body=None, json\_body=None, form\_body=None, form\_encoding="", timeout=30, allow\_redirects=True, verify=True) | Send an HTTP PATCH request to the path. | | context.http\_post(path, params=None, headers=None, auth=(), body=None, json\_body=None, form\_body=None, form\_encoding="", timeout=30, allow\_redirects=True, verify=True) | Send an HTTP POST request to the path. | | context.http\_put(path, params=None, headers=None, auth=(), body=None, json\_body=None, form\_body=None, form\_encoding="", timeout=30, allow\_redirects=True, verify=True) | Send an HTTP PUT request to the path. | | context.plan\_resources(request) | Send a Cerbos PlanResources request | ## [](#testdata)Reusing test data You can reuse common requests and responses by defining them in a test data file. They can be loaded into the test suite using the `testing.load_testdata` function. The test data file must be a YAML or JSON file in the following format: testdata.yaml ```yaml testData: (1) req_john: (2) checkResourcesRequest: (3) requestId: "john" principal: id: "john" roles: ["employee"] resources: - resource: id: "XX125" kind: "invoice" attr: owner: "john" department: "Music" geography: "GB" actions: ["view"] resp_john: checkResourcesResponse: requestId: "john" results: - resource: id: "XX125" kind: "invoice" actions: view: EFFECT_ALLOW ``` | **1** | Required. The file’s root object must be named testData. | | ----- | -------------------------------------------------------------------------------------------------- | | **2** | Identifier for the data object. This is the name you use to reference different test data objects. | | **3** | Type of the data. See below for acceptable values. | The following types of data can be included in a test data file. | Type | Schema | | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | authzenEvaluationBatchRequest | [authzen.authorization.v1.AccessEvaluationBatchRequest](https://buf.build/cerbos/cerbos-api/docs/main%3Aauthzen.authorization.v1#authzen.authorization.v1.AccessEvaluationBatchRequest) | | authzenEvaluationBatchResponse | [authzen.authorization.v1.AccessEvaluationBatchResponse](https://buf.build/cerbos/cerbos-api/docs/main%3Aauthzen.authorization.v1#authzen.authorization.v1.AccessEvaluationBatchResponse) | | authzenEvaluationRequest | [authzen.authorization.v1.AccessEvaluationRequest](https://buf.build/cerbos/cerbos-api/docs/main%3Aauthzen.authorization.v1#authzen.authorization.v1.AccessEvaluationRequest) | | authzenEvalationResponse | [authzen.authorization.v1.AccessEvaluationResponse](https://buf.build/cerbos/cerbos-api/docs/main%3Aauthzen.authorization.v1#authzen.authorization.v1.AccessEvaluationResponse) | | checkResourcesRequest | [cerbos.request.v1.CheckResourcesRequest](https://buf.build/cerbos/cerbos-api/docs/main%3Acerbos.request.v1#cerbos.request.v1.CheckResourcesRequest) | | checkResourcesResponse | [cerbos.response.v1.CheckResourcesResponse](https://buf.build/cerbos/cerbos-api/docs/main%3Acerbos.response.v1#cerbos.response.v1.CheckResourcesResponse) | | envoyCheckRequest | [envoy.service.auth.v3.CheckRequest](https://buf.build/envoyproxy/envoy/docs/main:envoy.service.auth.v3#envoy.service.auth.v3.CheckRequest) | | envoyCheckResponse | [envoy.service.auth.v3.CheckResponse](https://buf.build/envoyproxy/envoy/docs/main%3Aenvoy.service.auth.v3#envoy.service.auth.v3.CheckResponse) | | planResourcesRequest | [cerbos.request.v1.PlanResourcesRequest](https://buf.build/cerbos/cerbos-api/docs/main%3Acerbos.request.v1#cerbos.request.v1.PlanResourcesRequest) | | planResourcesResponse | [cerbos.response.v1.PlanResourcesResponse](https://buf.build/cerbos/cerbos-api/docs/main%3Acerbos.response.v1#cerbos.response.v1.PlanResourcesResponse) | example\_test.star ```python testdata = testing.load_testdata("check-resources-test-data.yaml") (1) def test_external_test_data(context): request = testdata["req_john"] (2) have = context.check_resources(request) want = testdata["resp_john"] return testing.assert(have.results[0].actions["view"] == want.results[0].actions["view"]) ``` | **1** | Load test data from file. This can be done inside the test function as well but creating a global variable allows it to be shared between all test functions. | | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **2** | Retrieve one of the named test data objects by its identifier. | ## [](#%5Fparameterized%5Ftests)Parameterized tests To avoid repetition, you can generate test cases within the test suite itself. The following example illustrates generating test cases using a test data file. The test data in this case is organized into request and response pairs (IDs prefixed with `req_` and `resp_`). The script iterates through all the requests and generates test cases for each that checks whether the actual response matches the expected response. example\_test.star ```python def build_test_case(request, expected): def test_case(context): actual = context.check_resources(request) for (i, expected_result) in enumerate(expected.results): for (action, expected_effect) in dict(expected_result.actions).items(): if actual.results[i].actions[action] != expected_effect: return testing.fail("Action {} of result {} should be {} but is {}".format(action, i, expected_effect, actual.results[i].actions[action])) return test_case def build_test_case_list(): testdata = testing.load_testdata("check-resources-test-data.yaml") test_cases = {} for (name, data) in testdata.items(): if name.startswith("req_"): test_name = name.removeprefix("req_") expected_resp = testdata.get("resp_" + test_name) if expected_resp: test_cases[test_name] = build_test_case(data, expected_resp) return test_cases test_suite = struct( name = "CheckResources tests", synapse_config = testing.load_synapse_config("synapse.yaml"), test_cases = build_test_case_list() (1) ) ``` | **1** | Call the build\_test\_case\_list() function to generate the set of test cases for this suite. | | ----- | --------------------------------------------------------------------------------------------- | ## [](#%5Floading%5Fmodules)Loading modules You can `load` any of the [supported modules](starlark-development.html#loadable-modules) for use in tests. example\_test.star ```python load("re", "re") def test_regex(context): return testing.assert(re.match("f.*", "foo")) ``` ## [](#%5Frunning%5Fa%5Fsubset%5Fof%5Ftests)Running a subset of tests Multiple directories and files can be passed as arguments to the `test` command. When an argument is a directory, it’s recursively searched to discover all files that have the `_test.star` suffix. If you want to run just a single test suite, pass the full path to the file. If you want to run only test cases that match a particular pattern, use the `--match` argument to pass a regular expression. ```console # Discover all the test suites under /tests and run them $ docker run \ -v $(pwd):/tests:ro \ CERBOS_DISTRIBUTION_REPO/synapse/synapse:latest \ test /tests # Run only extensionx_test.star and extensiony_test.star suites $ docker run \ -v $(pwd):/tests:ro \ CERBOS_DISTRIBUTION_REPO/synapse/synapse:latest \ test /tests/extensionx/extensionx_test.star /tests/extensiony/extensiony_test.star # Run only test cases whose names begin with "enrich" $ docker run \ -v $(pwd):/tests:ro \ CERBOS_DISTRIBUTION_REPO/synapse/synapse:latest \ test --match='^enrich' /tests ``` If you need to further process the test results (using a script, for instance) pass `--output=jsonl` to make the test runner produce output in JSONLines format. ## [](#%5Fdebugging%5Ffailures)Debugging failures Pass `--verbose` to see the logs from Synapse instance and any output produced by the test cases through `print()`. ```console $ docker run \ -v $(pwd):/tests:ro \ CERBOS_DISTRIBUTION_REPO/synapse/synapse:latest \ test --verbose /tests ``` You can use the [Starlark REPL](starlark-development.html#repl) to help debug issues with your test scripts. Try out tricky expressions in the REPL to figure out how they behave or load the test script in and invoke its functions by hand to test them. ```console $ docker run \ -v $(pwd):/tests:ro \ CERBOS_DISTRIBUTION_REPO/synapse/synapse:latest \ starlark repl /tests/fail_test.star ``` The above command loads the test script into the REPL. Any functions defined in the script will be available for you to call from the REPL. Please note that some elements such as the full `context` object passed to tests during actual test runs won’t be available here to use. You may need to decouple the code that you want to debug from the test framework and pass in mock data to simulate the conditions during the actual test run. ## [](#%5Ffurther%5Freading)Further reading Even though you might be developing extensions in WASM, in order to do integration testing with Synapse you need to be aware of Starlark basics. 1. Most of the scripting information in [Developing Starlark extensions](starlark-development.html) is applicable for testing as well 2. Refer to the [Starlark spec](https://github.com/google/starlark-go/blob/master/doc/spec.md) for language primitives Extension URLs ==================== Synapse extensions are self-contained, compiled code modules. They can be loaded from many different sources including the local file system, S3-compatible storage, and HTTP servers. For security purposes, it’s highly recommended that you only use extensions from trusted sources and pin them to specific versions using checksums. A typical extension URL takes the following form: The `checksum` parameter is optional but is highly recommended for security. It can be appended to any URL kind listed below — including plain file paths. Supported URL formats Built-in extensions (where supported) * `system://sqldb` Local files * `/path/to/extension.wasm` * `/path/to/extension.star?checksum=sha256:677cce2788330f16f27981130eaa50aa4189beab2121903bcea5b3c4c918dccc` HTTP * `` * `` * `` Git * `wasm+git::https://github.com/org/repo//extension.wasm` * `wasm+git::ssh://git@github.com/org/repo//extension.wasm?ref=v1.0.0` Amazon S3 * `wasm+s3::https://s3.amazonaws.com/bucket-name/example.wasm` Google Cloud Storage * `starlark+gcs::https://www.googleapis.com/storage/v1/bucket-name/example.star` Synapse selects the runtime from the URL path: `.star` for Starlark, `.wasm` for WebAssembly (WASM), and native otherwise. Getter URLs such as `git::`, `s3::` and `gcs::` require a `native+`, `starlark+` or `wasm+` runtime prefix because the artifact path is not available during runtime selection. | HTTP | Credentials are read from URL user information or \~/.netrc. | | -------------------- | ------------------------------------------------------------ | | Amazon S3 | Credentials are read from the standard AWS credential chain. | | Google Cloud Storage | Credentials are read from Application Default Credentials. | Use the `s3::` or `gcs::` getter to authenticate an object-store request. A plain HTTPS URL is fetched without object-store authentication. Azure Blob Storage URLs are not supported. ## [](#loading)Loading and refresh Synapse fetches and compiles extensions during startup. Restart Synapse after changing an extension artifact, including a local file or a file mounted through a Kubernetes ConfigMap. Reloading the policy store does not reload extensions. Developing Webassembly extensions ==================== Synapse supports Webassembly (WASM) extensions using the [Extism framework](https://extism.org). While it’s possible to develop extensions without any framework, it’s recommended to use one of the supported [Extism plugin developments kits](https://extism.org/docs/concepts/pdk) as they provide helpers for sending JSON back and forth, reading configuration and other useful functions. | | WASM extensions should be reactor modules that are long-lived. Note that Synapse pools multiple instances of a single extension in order to handle parallel requests (WASM memory is not thread-safe). This is an important consideration to keep in mind when creating long-lived resources such as database connections as there will be multiple independent copies of each WASM module alive at the same time on a single Synapse instance. | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#%5Fcommon%5Ffunctionality)Common functionality ### [](#%5Flifecycle%5Fmanagement)Lifecycle management If the WASM module exports a `cerbosInit` function, Synapse invokes it to initialize the instance. Note that Synapse maintains an internal pool of instances to reduce load times and handle multi-threading so this function will be invoked multiple times (i.e. it’s not a singleton). It’s useful for allocating long-lived, per-instance, reusable resources such as database connections. When Synapse is gracefully shut down, the `cerbosDeinit` function is invoked on all instances of the module if it’s exported. It can be used to perform cleanup tasks such as closing database connections acquired during `cerbosInit`. Both `cerbosInit` and `cerbosDeinit` take no arguments and return an `i32` where `0` denotes success and any non-zero value denotes failure. Custom configuration set in the Synapse configuration file under the extension’s `configuration` field is accessible via the Extism PDK’s config helpers (`pdk.GetConfig` in Go, `Config.get` in JavaScript/TypeScript). The following snippet demonstrates how these functions might be implemented in a Go WASM module. ```go var sqlite *sqlx.DB //go:wasmexport cerbosInit func cerbosInit() int32 { connectionStr, exists := pdk.GetConfig("connectionString") if !exists { pdk.SetError(errors.New("connectionString configuration is required")) return 1 } db, err = sqlx.Connect("sqlite3", connectionStr) if err != nil { pdk.SetError(err) return 1 } sqlite = db return 0 } //go:wasmexport cerbosDeinit func cerbosDeinit() int32 { if err := sqlite.Close(); err != nil { pdk.SetError(err) return 1 } return 0 } ``` ### [](#%5Flogging)Logging Log messages emitted by the Extism PDK are routed to Synapse’s logger and tagged with the configured extension name. Messages below the Synapse log level set via `--log.level` are suppressed. Use the PDK’s log helpers from inside any export to surface diagnostic output through the host: * Go: `pdk.Log(pdk.LogDebug, "msg")` (levels: `LogTrace`, `LogDebug`, `LogInfo`, `LogWarn`, `LogError`). * JavaScript/TypeScript: `console.debug("msg")` (also `console.trace`, `console.log`/`info`, `console.warn`, `console.error`). ### [](#host-funcs)Host functions Synapse exposes the following host functions that can be called by any extension. They are all exported under the module name `extism:host/user`. `cacheDelete` Delete a value from the cache. Inputs * `key`: Pointer to a string Outputs * An integer value. Non-zero value indicates failure. `cacheGet` Get a value from the cache. Inputs * `key`: Pointer to a string Outputs * Pointer to a byte array containing the value. `cacheSet` Save a value to cache. Inputs * `key`: Pointer to a string * `value`: Pointer to a byte array * `duration`: Integer value defining cache duration in milliseconds Outputs * An integer value. Non-zero value indicates failure. `cacheSetIfNotExists` Save a value to cache if it doesn’t already exist. Inputs * `key`: Pointer to a string * `value`: Pointer to a byte array * `duration`: Integer value defining cache duration in milliseconds Outputs * An integer value. Non-zero value indicates failure. `checkResources` Perform a Cerbos `CheckResources` call. Inputs * `request`: Pointer to a byte array containing the JSON-encoded `CheckResources` request Outputs * Pointer to a byte array containing the JSON-encoded `CheckResources` response `dataSourceLookup` Lookup information from a data source. Inputs * `request`: Pointer to a byte array containing the JSON-encoded lookup request. See [Data sources](data-sources.html). Outputs * Pointer to a byte array containing the JSON-encoded lookup response. See [Data sources](data-sources.html). `planResources` Perform a Cerbos `PlanResources` call. Inputs * `request`: Pointer to a byte array containing the JSON-encoded `PlanResources` request Outputs * Pointer to a byte array containing the JSON-encoded `PlanResources` response The following snippet illustrates how to access host functions from a Go WASM module. ```go //go:wasmimport extism:host/user cacheSet func cacheSet(uint64, uint64, uint32) int32 func writeToCache(key string value []byte) { keyMem := pdk.AllocateString(key) defer keyMem.Free() valueMem := pdk.AllocateBytes(value) defer valueMem.Free() result := cacheSet(keyMem.Offset(), valueMem.Offset(), 10000) if result != 0 { // handle error } } ``` ### [](#manifest)Manifest Provide a [manifest function](manifest.html) to document the extension. This is optional but highly recommended. ## [](#datasource)Data source extension A data source extension must export a function named `lookup` that accepts a pointer to a JSON-encoded lookup request and returns a pointer to a JSON-encoded lookup result. Lookup request ```json { "dataSource": "myDataSource", (1) "query": "SELECT department FROM employees WHERE id = :employee_id", (2) "queryParameters": { (3) "employee_id": "simon" }, "cacheOptions": { (4) "cacheKey": "simon", (5) "cacheExpiry": "300s", (6) "ifNotExists": true (7) } } ``` | **1** | Name of the data source to query | | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | **2** | Query to execute on the data source. This is specific to the data source. The value can be any valid JSON value, including complex objects and arrays. | | **3** | Optional query parameters | | **4** | Optional cache options for caching the result of the lookup | | **5** | Cache key for this lookup | | **6** | Optional duration for caching the result of the lookup | | **7** | Optional flag to prevent overwriting an existing cache key | Only the `dataSource` and `query` fields are required. The `query` field can be any valid JSON value, including complex nested objects. The API contract for the `query` field is specific to each data source implementation as it depends on the logic that each data source encapsulates. The response from the data source only contains the single field `result`. It could contain any valid JSON value including objects and arrays. The format of the result depends on the data source. Lookup response ```json { "result": [ {"id": 1, "first_name": "Daffy"}, {"id": 2, "first_name": "Elmer"} ] } ``` The following snippet demonstrates a very simple implementation of a data source that echoes the query back to the caller. ```go type LookupRequest struct { DataSource string `json:"dataSource"` Query json.RawMessage `json:"query"` } type LookupResponse struct { Result json.RawMessage `json:"result"` } //go:wasmexport lookup func lookup() int32 { var req LookupRequest if err := pdk.InputJSON(&req); err != nil { pdk.SetError(err) return 1 } resp := LookupResponse{Result: req.Query} if err := pdk.OutputJSON(resp); err != nil { pdk.SetError(err) return 1 } return 0 } ``` ## [](#envoy)Envoy extension An Envoy extension must export a function named `envoyCheck` that accepts a JSON-encoded [Envoy check request](https://buf.build/envoyproxy/envoy/docs/main:envoy.service.auth.v3#envoy.service.auth.v3.CheckRequest) and returns one of the following responses. ### [](#%5Fdirect%5Fresponse)Direct response Return a complete [Envoy check response](https://buf.build/envoyproxy/envoy/docs/main:envoy.service.auth.v3#envoy.service.auth.v3.CheckResponse) to return back to the caller. ```json { "envoyCheckResponse": { "status": {"code": 7}, "deniedResponse": { "body": "Go away" } } } ``` ### [](#%5Fcerbos%5Fmapping)Cerbos mapping Return a JSON object that contains a Cerbos `CheckRequest` and the Envoy check response to send based on the Cerbos response. Synapse sends the Cerbos request to the configured PDP (applying any configured proxy extensions) and responds with the appropriate Envoy response based on whether Cerbos returns ALLOW or DENY. ```json { "cerbosMapping": { "checkRequest": { (1) "principal": { "id": "daffy_duck", "roles": ["duck"] }, "resources": [{ "actions": ["GET"], "resource": { "id": "http", "kind": "request", "attr": { "path": "/foo" }, } }] }, "allowResponse": { (2) "status": {"code": 0} }, "denyResponse": { (3) "status": {"code": 7}, "deniedResponse": { "body": "Go away" } } } } ``` | **1** | Cerbos CheckResources request to send | | ----- | ------------------------------------------------------ | | **2** | Envoy response to send if Cerbos returns EFFECT\_ALLOW | | **3** | Envoy response to send if Cerbos returns EFFECT\_DENY | ### [](#%5Fcerbos%5Fcheck%5Frequest)Cerbos check request Return a Cerbos `CheckRequest` to send to the PDP (applying any configured proxy extensions). In this mode, the extension must implement a second function named `envoyMapCerbosResponse` to map the `CheckResources` response to an Envoy response. Use this mode when you need to write complex mapping logic to convert a Cerbos response to an Envoy response. ```json { "cerbosCheckRequest": { "principal": { "id": "daffy_duck", "roles": ["duck"] }, "resources": [{ "actions": ["GET"], "resource": { "id": "http", "kind": "request", "attr": { "path": "/foo" }, } }] } } ``` Synapse will send the above request to the PDP and invoke the extension a second time by calling the `envoyMapCerbosResponse` function with a pointer to the following JSON-encoded content. ```json { "envoyRequest": { ... }, (1) "cerbosRequest": { ... }, (2) "cerbosResponse": { ... } (3) } ``` | **1** | Original Envoy Check request that triggered this chain | | ----- | ------------------------------------------------------ | | **2** | Cerbos CheckResources request | | **3** | Cerbos CheckResources response | The `envoyMapCerbosResponse` function should take the above input and return a pointer to a JSON-encoded [Envoy check response](https://buf.build/envoyproxy/envoy/docs/main:envoy.service.auth.v3#envoy.service.auth.v3.CheckResponse) which will be sent back to the caller. ```json { "status": {"code": 7}, "deniedResponse": { "body": "Denied by policy secops-global" } } ``` ## [](#proxy)Proxy extension A proxy extension must export at least one of the following functions. (It’s legal to implement more than one.) `augmentAuthzenEvaluationBatchRequest` Modify an AuthZEN AccessEvaluations request `augmentAuthzenEvaluationBatchResponse` Modify an AuthZEN AccessEvaluations response `augmentAuthzenEvaluationRequest` Modify an AuthZEN AccessEvaluation request `augmentAuthzenEvaluationResponse` Modify an AuthZEN AccessEvaluation response `augmentCheckRequest` Modify a CheckResources request `augmentCheckResponse` Modify a CheckResources response `augmentPlanRequest` Modify a PlanResources request `augmentPlanResponse` Modify a PlanResources response Each function is invoked with a pointer to a JSON-encoded `AccessEvaluation`/CheckResources\`/`PlanResources` request or response. It must return a pointer to a JSON-encoded value of the same type. Refer to for details about the request and response types. ## [](#route)Route extension A route extension must export a function named `handleHTTPRoute` which receives a pointer to the JSON-encoded HTTP request. Route extensions are mounted under the `/ext/` path prefix at the Synapse listen address, and that prefix is not stripped before the request reaches the extension. For a route configured as `/foo` and a client request to `POST /ext/foo?a=av&b=bv1&b=bv2`, the input takes the following form. ```json { "method": "POST", (1) "headers": { (2) "X-Forwarded-For": { "values": ["127.0.0.1:2090"] } }, "rawUrl": "https://example.com/ext/path/to/foo?a=av&b=bv1&b=bv2", (3) "host": "example.com", (4) "path": "/ext/path/to/foo", (5) "queryParams": { (6) "a": { "values": ["av"] }, "b": { "values": ["bv1", "bv2"] } }, "body": "aGVsbG8K" (7) } ``` | **1** | HTTP method. | | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **2** | Headers and their values. Header names are in [canonical form, also known as train case](https://stringcase.org/cases/train/) (X-Forwarded-For). | | **3** | The URL including the /ext/ prefix and any query parameters. This will be a relative URL without the protocol and host unless it’s an absolute request sent via a proxy. | | **4** | Host segment from the URL. | | **5** | Path segment, including the /ext/ prefix. | | **6** | Query parameters from the URL. | | **7** | Request body as a base64-encoded string. | The return value of the function must be one of the following values. ### [](#%5Fdirect%5Fresponse%5F2)Direct response Return a complete HTTP response to be sent back to the caller. ```json { "httpResponse": { "status": 401, (1) "headers": { (2) "Content-Type": { "values": ["application/text"] } }, "body": "Z28gYXdheQo=" (3) } } ``` | **1** | HTTP status code | | ----- | ------------------------------ | | **2** | Headers to add to the response | | **3** | Base64-encoded body | ### [](#%5Fcerbos%5Fmapping%5F2)Cerbos mapping Return a JSON object that contains a Cerbos `CheckResources` request and the HTTP response to send based on the Cerbos response. Synapse sends the Cerbos request to the configured PDP (applying any configured proxy extensions) and responds with the appropriate HTTP response based on whether Cerbos returns ALLOW or DENY. ```json { "cerbosMapping": { "checkRequest": { (1) "principal": { "id": "daffy_duck", "roles": ["duck"] }, "resources": [{ "actions": ["GET"], "resource": { "id": "http", "kind": "request", "attr": { "path": "/foo" }, } }] }, "allowResponse": { (2) "status": 200, "body": "aGVsbG8K" }, "denyResponse": { (3) "status": 401, "body": "Z28gYXdheQo=" } } } ``` | **1** | Cerbos CheckResources request | | ----- | ----------------------------------------------------- | | **2** | HTTP response to send if Cerbos returns EFFECT\_ALLOW | | **3** | HTTP response to send if Cerbos resturns EFFECT\_DENY | ### [](#%5Fcerbos%5Frequest)Cerbos request Return a JSON object that contains the Cerbos `CheckResources` request. In this mode, the extension must export a second function named `handleCerbosResponse` that accepts the `CheckResources` response and returns a HTTP response. Use this mode when you want to construct a complex HTTP response using values from the Cerbos response. ```json { "checkRequest": { (1) "principal": { "id": "daffy_duck", "roles": ["duck"] }, "resources": [{ "actions": ["GET"], "resource": { "id": "http", "kind": "request", "attr": { "path": "/foo" }, } }] } } ``` | **1** | Cerbos CheckResources request | | ----- | ----------------------------- | Synapse sends the Check request to the PDP and then invokes the `handleCerbosResponse` function of the extension with the following information. ```json { "httpRequest": { ... }, (1) "cerbosRequest": { ... }, (2) "cerbosResponse": { ... } (3) } ``` | **1** | Original HTTP request that triggered this route extension | | ----- | --------------------------------------------------------- | | **2** | Cerbos CheckResources request | | **3** | Cerbos CheckResources response | `handleCerbosResponse` should return a HTTP response to send back to the caller. ```json { "status": 401, "headers": { "Content-Type": { "values": ["application/json"] } }, "body": "eyJtZXNzYWdlIjogIkRlbmllZCBieSBwb2xpY3kgc2Vjb3BzLWdsb2JhbCJ9Cg==" } ``` Build a WASM extension in Go ==================== This tutorial walks through building a minimal Synapse proxy extension in Go, compiling it to WebAssembly, and loading it into a running Synapse instance. For the payload shapes and full list of exports for every extension type, see [Developing Webassembly extensions](wasm-development.html). Synapse loads WASM modules using the [Extism](https://extism.org) runtime. This guide uses the [Extism Go PDK](https://github.com/extism/go-pdk), which provides convenience wrappers for Go developers. There are versions of the PDK available for [several other languages](https://extism.org/docs/concepts/pdk/). If your preferred language doesn’t have a PDK but supports compiling the code into a WASI reactor, it’s still possible to develop a Synapse extension by implementing the Extism ABI in your code. Details of that are beyond the scope of this documentation. | | Synapse creates multiple instances of each extension to serve parallel requests. Each instance has its own globals and is isolated from the other instances. The only way to share state is through [the built-in cache](wasm-development.html#host-funcs) or through an external data store. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ## [](#%5Fbefore%5Fyou%5Fbegin)Before you begin You need: * [Go 1.24](https://go.dev/dl/) or later * Docker * A Synapse licence. If you do not already have one, sign up at [cerbos.dev/workshop](https://www.cerbos.dev/workshop). Your credentials are issued together with the URL of the Cerbos distribution repository; substitute that URL wherever `CERBOS_DISTRIBUTION_REPO` appears below. * `curl` and a JSON formatter such as `python3 -m json.tool` or `jq` (optional). Log in to the Cerbos distribution repository: ```console $ docker login CERBOS_DISTRIBUTION_REPO --username=YOUR_LICENCE_USER --password=YOUR_LICENCE_KEY ``` ## [](#%5Fcreate%5Fthe%5Fproject)Create the project ```console $ mkdir -p wasm-go-tutorial/policies wasm-go-tutorial/extensions wasm-go-tutorial/src $ cd wasm-go-tutorial/src $ go mod init example.com/synapse-extension $ go get github.com/extism/go-pdk@latest $ go get github.com/tidwall/sjson@latest $ cd .. ``` `go-pdk` provides the Extism input/output helpers and memory allocator. `sjson` is used to modify the request JSON without defining Go structs for every field. ## [](#%5Fwrite%5Fthe%5Fextension)Write the extension Create `src/main.go`: ```go package main import ( "github.com/extism/go-pdk" "github.com/tidwall/sjson" ) //go:wasmexport cerbosInit (1) func cerbosInit() int32 { return 0 } //go:wasmexport cerbosDeinit func cerbosDeinit() int32 { return 0 } //go:wasmexport augmentCheckRequest (2) func augmentCheckRequest() int32 { input := pdk.Input() (3) modified, err := sjson.SetBytes(input, "principal.attr.company", "Romaguera-Crona") if err != nil { pdk.SetError(err) return 1 } pdk.Output(modified) (4) return 0 } //go:wasmexport augmentCheckResponse func augmentCheckResponse() int32 { pdk.Output(pdk.Input()) return 0 } func main() {} (5) ``` | **1** | The //go:wasmexport directive controls the WASM symbol name. It must match the Synapse ABI exactly (cerbosInit, augmentCheckRequest, and so on). The Go function name can differ. | | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **2** | One of the proxy extension hooks. Synapse invokes it before every CheckResources request reaches the PDP. | | **3** | pdk.Input() returns the JSON-encoded request as \[\]byte. | | **4** | pdk.Output is mandatory on every successful invocation, even for pass-through. Returning 0 without output would be considered an error. | | **5** | Required by the Go toolchain but never called when the module runs as an Extism reactor. Leave it empty. | ## [](#%5Fbuild%5Fthe%5Fmodule)Build the module ```console $ GOOS=wasip1 GOARCH=wasm go build -C src -o ../extensions/proxy.wasm -buildmode=c-shared main.go ``` `GOOS=wasip1 GOARCH=wasm` targets WebAssembly with the WASI preview 1 ABI. `-buildmode=c-shared` produces a reactor module that exports its symbols instead of running `main` and exiting. | | [TinyGo](https://tinygo.org/) produces modules roughly 10× smaller: tinygo build -o extensions/proxy.wasm -target wasip1 -buildmode=c-shared src/main.go. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ## [](#%5Fwrite%5Fa%5Fpolicy)Write a policy Create `policies/invoice.yaml`: ```yaml apiVersion: api.cerbos.dev/v1 resourcePolicy: resource: invoice version: default rules: - actions: [view] effect: EFFECT_ALLOW roles: [employee] condition: match: expr: request.resource.attr.company == request.principal.attr.company ``` The extension supplies `principal.attr.company`; the caller supplies `request.resource.attr.company`. ## [](#%5Fconfigure%5Fand%5Frun%5Fsynapse)Configure and run Synapse Create `.synapse.yaml`: ```yaml server: listenAddress: ":3594" pdp: inProcess: storage: driver: "disk" disk: directory: /policies audit: enabled: true backend: file file: path: stdout extensions: proxyExtensions: principalEnricher: (1) extensionURL: /extensions/proxy.wasm (2) required: true (3) ``` | **1** | Name for this extension as it appears in logs. | | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **2** | Path inside the container. The .wasm suffix tells Synapse to load the file with the WASM runtime. See [Extension URLs](url-format.html) for other supported schemes. | | **3** | A failure inside a required extension short-circuits the chain and returns an error to the caller. Set this to false for advisory enrichment. | Start the container: ```console $ docker run --rm --name synapse-wasm-go -p 3594:3594 \ -v $(pwd)/.synapse.yaml:/config/.synapse.yaml:ro \ -v $(pwd)/extensions:/extensions:ro \ -v $(pwd)/policies:/policies:ro \ -e SYNAPSE_CONFIG=/config/.synapse.yaml \ CERBOS_DISTRIBUTION_REPO/synapse/synapse:latest \ server --log.level=debug ``` Logs should show: ```json {"severity":"DEBUG","extension":"principalEnricher","runtime":"wasm","message":"Downloaded extension"} ``` In a second terminal, send a request without a `principal.attr` block: ```console $ curl -s -X POST http://localhost:3594/api/check/resources \ -H 'Content-Type: application/json' \ -d '{ "requestId": "wasm-go-001", "principal": {"id": "1", "roles": ["employee"]}, "resources": [{ "actions": ["view"], "resource": {"kind": "invoice", "id": "inv-42", "attr": {"company": "Romaguera-Crona"}} }] }' ``` Response ```json { "requestId": "wasm-go-001", "results": [ { "resource": {"id": "inv-42", "kind": "invoice"}, "actions": {"view": "EFFECT_ALLOW"} } ] } ``` The extension injected `principal.attr.company` before the PDP evaluated the policy. Remove the extension or change the value and the decision flips to `EFFECT_DENY`. ## [](#%5Flanguage%5Fspecific%5Fnotes)Language-specific notes * Export names are case-sensitive and must match the Synapse ABI exactly. The `//go:wasmexport` directive controls the symbol. * Only export the hooks you implement. Missing exports are ignored. Hooks that are expected to produce output must do so using `pdk.Output` even if it’s simply echoing the input. * Host functions are declared with `//go:wasmimport extism:host/user `, invoked by passing shared-memory offsets, and read back with `pdk.FindMemory`. Full list and signatures: [extensions/wasm-development.adoc#host-funcs](wasm-development.html#host-funcs). * `encoding/json` works inside WASM but produces large binaries. `gjson` / `sjson` keep the binary small when you only need to read or mutate a few fields. * `pdk.Log(pdk.LogDebug, "msg")` writes to the Synapse log, tagged with the extension name. Levels below `--log.level` are dropped on the host side. * Outbound HTTP via `pdk.NewHTTPRequest` is blocked unless the target is listed in `allowedHosts` in `.synapse.yaml`. ## [](#%5Fgoing%5Ffurther)Going further Refer to [Developing Webassembly extensions](wasm-development.html) for details about the functions that should be exported by different extension types and common operations such as calling host functions. Build a WASM extension in TypeScript ==================== This tutorial walks through building a minimal Synapse proxy extension in TypeScript, compiling it to WebAssembly with the [Extism JS PDK](https://github.com/extism/js-pdk), and loading it into a running Synapse instance. For the payload shapes and full list of exports for every extension type, see [Developing Webassembly extensions](wasm-development.html). The Extism JS PDK bundles a QuickJS interpreter plus your JavaScript into a single `.wasm` file. The result behaves identically to a natively-compiled WASM module from Synapse’s perspective; the runtime differences (no `btoa`, no Node APIs) only matter when writing the extension. | | Synapse creates multiple instances of each extension to serve parallel requests. Each instance has its own globals and is isolated from the other instances. The only way to share state is through [the built-in cache](wasm-development.html#host-funcs) or through an external data store. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ## [](#%5Fbefore%5Fyou%5Fbegin)Before you begin You need: * [Node.js 22](https://nodejs.org/en/download) or later * Docker * A Synapse licence. If you do not already have one, sign up at [cerbos.dev/workshop](https://www.cerbos.dev/workshop). Your credentials are issued together with the URL of the Cerbos distribution repository; substitute that URL wherever `CERBOS_DISTRIBUTION_REPO` appears below. * The [extism-js CLI](https://github.com/extism/js-pdk) on your `PATH`. Install the binary for your platform from the releases page and confirm with `extism-js --version`. * `curl` and a JSON formatter such as `python3 -m json.tool` or `jq` (optional). Log in to the Cerbos distribution repository: ```console $ docker login CERBOS_DISTRIBUTION_REPO --username=YOUR_LICENCE_USER --password=YOUR_LICENCE_KEY ``` ## [](#%5Fcreate%5Fthe%5Fproject)Create the project The npm project sits at the root alongside `policies/` and `extensions/`, with TypeScript sources under `src/`: ```console $ mkdir -p wasm-ts-tutorial/policies wasm-ts-tutorial/extensions wasm-ts-tutorial/src $ cd wasm-ts-tutorial $ npm init -y $ npm install --save-dev @extism/js-pdk esbuild typescript $ npm pkg set type=commonjs $ npm pkg set scripts.build="node esbuild.js && extism-js dist/index.js -i src/index.d.ts -o extensions/proxy.wasm" ``` The PDK requires CommonJS output. The `build` script bundles the TypeScript with esbuild, then wraps the bundle into a WASM reactor module at `extensions/proxy.wasm`. Create `esbuild.js`: ```javascript const esbuild = require('esbuild'); esbuild.build({ entryPoints: ['src/index.ts'], outdir: 'dist', bundle: true, format: 'cjs', target: ['es2020'], }); ``` Create `tsconfig.json`: ```json { "compilerOptions": { "target": "es2020", "module": "commonjs", "strict": true, "esModuleInterop": true, "moduleResolution": "node" }, "include": ["src/**/*.ts"] } ``` ## [](#%5Fdeclare%5Fthe%5Fabi%5Fcontract)Declare the ABI contract Create `src/index.d.ts`. This file tells `extism-js` which WASM symbols to export and which host functions the module may call. ```typescript /// (1) declare module "main" { (2) export function cerbosInit(): void; export function cerbosDeinit(): void; export function augmentCheckRequest(): void; export function augmentCheckResponse(): void; } declare module "extism:host" { (3) interface user { cacheGet(keyPtr: PTR): PTR; cacheSet(keyPtr: PTR, valuePtr: PTR, ttlMs: I32): I32; dataSourceLookup(reqPtr: PTR): PTR; } } ``` | **1** | Required on the first line. Pulls in Host, Memory, Config, PTR, I32 types from the PDK. | | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | **2** | Every exported WASM symbol must be listed here in camelCase. The Synapse ABI has no snake\_case names. | | **3** | Every host function the module calls must be declared on interface user. Omitting one you call fails at link time; declaring one you never use is harmless. | | | extism-js generates a WASM stub for every function declared under declare module "main". If the TypeScript source does not export a matching function, the stub traps at runtime with unreachable. Only declare hooks you actually implement. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ## [](#%5Fwrite%5Fthe%5Fextension)Write the extension Create `src/index.ts`: ```typescript export function cerbosInit(): void {} (1) export function cerbosDeinit(): void {} export function augmentCheckRequest(): void { const req = JSON.parse(Host.inputString()); (2) req.principal = req.principal ?? {}; req.principal.attr = { ...(req.principal.attr ?? {}), company: "Romaguera-Crona", }; Host.outputString(JSON.stringify(req)); (3) } export function augmentCheckResponse(): void { Host.outputString(Host.inputString()); (4) } ``` | **1** | Lifecycle hooks are optional but harmless when empty. extism-js requires every declared export to have an implementation. | | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **2** | Host.inputString() returns the JSON-encoded CheckResources request. | | **3** | Host.outputString Send the modified payload from this extension. Even in a pass-through scenario, this function must be called with the unmodified payload to complete the execution of the extension. | | **4** | Passthrough for responses. Omit the export (and its declaration in index.d.ts) if you don’t modify responses. | ## [](#%5Fbuild%5Fthe%5Fmodule)Build the module ```console $ npm run build ``` This produces `extensions/proxy.wasm`. Rerun after every source change. ## [](#%5Fwrite%5Fa%5Fpolicy)Write a policy Create `policies/invoice.yaml`: ```yaml apiVersion: api.cerbos.dev/v1 resourcePolicy: resource: invoice version: default rules: - actions: [view] effect: EFFECT_ALLOW roles: [employee] condition: match: expr: request.resource.attr.company == request.principal.attr.company ``` The extension supplies `principal.attr.company`; the caller supplies `request.resource.attr.company`. ## [](#%5Fconfigure%5Fand%5Frun%5Fsynapse)Configure and run Synapse Create `.synapse.yaml`: ```yaml server: listenAddress: ":3594" pdp: inProcess: storage: driver: "disk" disk: directory: /policies audit: enabled: true backend: file file: path: stdout extensions: proxyExtensions: principalEnricher: (1) extensionURL: /extensions/proxy.wasm (2) required: true (3) ``` | **1** | Name for this extension as it appears in logs. | | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **2** | Path inside the container. The .wasm suffix tells Synapse to load the file with the WASM runtime. See [Extension URLs](url-format.html) for other supported schemes. | | **3** | A failure inside a required extension short-circuits the chain and returns an error to the caller. Set this to false for advisory enrichment. | Start the container: ```console $ docker run --rm --name synapse-wasm-ts -p 3594:3594 \ -v $(pwd)/.synapse.yaml:/config/.synapse.yaml:ro \ -v $(pwd)/extensions:/extensions:ro \ -v $(pwd)/policies:/policies:ro \ -e SYNAPSE_CONFIG=/config/.synapse.yaml \ CERBOS_DISTRIBUTION_REPO/synapse/synapse:latest \ server --log.level=debug ``` Logs should show: ```json {"severity":"DEBUG","extension":"principalEnricher","runtime":"wasm","message":"Downloaded extension"} ``` In a second terminal, send a request without a `principal.attr` block: ```console $ curl -s -X POST http://localhost:3594/api/check/resources \ -H 'Content-Type: application/json' \ -d '{ "requestId": "wasm-ts-001", "principal": {"id": "1", "roles": ["employee"]}, "resources": [{ "actions": ["view"], "resource": {"kind": "invoice", "id": "inv-42", "attr": {"company": "Romaguera-Crona"}} }] }' ``` Response ```json { "requestId": "wasm-ts-001", "results": [ { "resource": {"id": "inv-42", "kind": "invoice"}, "actions": {"view": "EFFECT_ALLOW"} } ] } ``` The extension injected `principal.attr.company` before the PDP evaluated the policy. Remove the extension or change the value and the decision flips to `EFFECT_DENY`. ## [](#%5Flanguage%5Fspecific%5Fnotes)Language-specific notes * Export names must be camelCase (`cerbosInit`, not `cerbos_init`). They are case-sensitive. * Every declaration under `declare module "main"` needs a matching `export function` in the source. Mismatches produce a WASM module that traps at runtime with `unreachable`. * The extism-js runtime has no `btoa`, `atob`, `setTimeout`, `fetch`, or Node APIs. Use `Host.arrayBufferToBase64` / `Host.base64ToArrayBuffer`, plus `TextEncoder` / `TextDecoder`. * `console.debug("msg")` (and `console.log`/`info`/`warn`/`error`/`trace`) writes to the Synapse log, tagged with the extension name. Levels below `--log.level` are dropped on the host side. * Host functions are invoked via `Host.getFunctions()` and require manual memory handling (`Memory.fromString`, `Memory.find`). Full list and signatures: [extensions/wasm-development.adoc#host-funcs](wasm-development.html#host-funcs). * Outbound HTTP via `Http.request` is blocked unless the target is listed in `allowedHosts` in `.synapse.yaml`. ## [](#%5Fgoing%5Ffurther)Going further Refer to [Developing Webassembly extensions](wasm-development.html) for details about the functions that should be exported by different extension types and common operations such as calling host functions. Running Synapse in a container ==================== Synapse is distributed as a container image from the Cerbos distribution repository. Running the image directly covers deployments outside Kubernetes: a single Docker host, `docker compose`, or any container orchestrator that does not use Helm. For Kubernetes deployments, use the [Helm chart](helm.html) instead. The [Quick Start](../quickstart.html) covers end-to-end setup with a policy, an extension and a sample request. Return here once the deployment needs to be customised. ## [](#%5Fpull%5Fthe%5Fimage)Pull the image A valid Synapse licence is required to pull the image. If you do not already have one, sign up at [cerbos.dev/workshop](https://www.cerbos.dev/workshop). Your licence credentials are issued together with the URL of the Cerbos distribution repository, substitute that URL wherever `CERBOS_DISTRIBUTION_REPO` appears below. See [Managing licence keys](licence-keys.html) for generating, rotating, and revoking your licence key in Cerbos Hub. Log in to the Cerbos distribution repository: ```console $ docker login CERBOS_DISTRIBUTION_REPO --username=YOUR_LICENCE_USER --password=YOUR_LICENCE_KEY ``` Pull the image: ```console $ docker pull CERBOS_DISTRIBUTION_REPO/synapse/synapse:latest ``` For production deployments, pin to a specific version rather than `latest` so that rolling restarts are deterministic. Your Cerbos account team can confirm the current stable version and the upgrade cadence. ## [](#%5Frun%5Fsynapse)Run Synapse Start the container with the default configuration. It listens on port 3594 for both HTTP and gRPC traffic on the Cerbos API, and loads any policies found under `/policies`: ```console $ docker run --rm --name synapse -p 3594:3594 \ CERBOS_DISTRIBUTION_REPO/synapse/synapse:latest ``` This default configuration verifies that the image starts. Deployment requires a configuration file specifying the PDP, policies, and extensions. ## [](#%5Fprovide%5Fa%5Fconfiguration%5Ffile)Provide a configuration file Synapse accepts a YAML configuration file passed via the `--conf.path` flag or `SYNAPSE_CONFIG` environment variable. The following `docker run` invocation bind-mounts a working directory into the container and provides a configuration file from it: ```console $ docker run --rm --name synapse -p 3594:3594 \ -v $(pwd)/.synapse.yaml:/config/.synapse.yaml:ro \ (1) -v $(pwd)/extensions:/extensions:ro \ (2) -e SYNAPSE_CONFIG=/config/.synapse.yaml \ (3) CERBOS_DISTRIBUTION_REPO/synapse/synapse:latest \ server (4) ``` | **1** | Mount the Synapse configuration file. | | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **2** | Directory containing extension source or binaries referenced by extension URLs such as /extensions/my\_extension.star. Only required when extensions are configured. | | **3** | If a config file is provided, use the SYNAPSE\_CONFIG environment variable to specify its location. This enables sharing the configuration with both the service and the default Docker health check. | | **4** | The server subcommand starts the Synapse server. | The contents of the configuration file determine how the embedded Cerbos PDP sources its policies. Two options are supported: fetching them from [Cerbos Hub](https://docs.cerbos.dev/cerbos-hub/) as signed, versioned bundles, or loading them from a directory on the local filesystem. Hub is the recommended option for any deployment beyond local development. ### [](#%5Fsource%5Fpolicies%5Ffrom%5Fcerbos%5Fhub%5Frecommended)Source policies from Cerbos Hub (recommended) Cerbos Hub publishes signed policy bundles, applies updates to connected Synapse instances without restarts, and centralises policy authoring and audit history. Configure the embedded PDP with the `hub` storage driver and a deployment ID, then pass Hub credentials to the container as environment variables. See [Connecting Synapse to Cerbos Hub](../configuration/cerbos-hub.html) for the complete configuration, including audit log shipping and credential handling. ### [](#%5Fsource%5Fpolicies%5Ffrom%5Flocal%5Fdisk%5Fdevelopment%5Fonly)Source policies from local disk (development only) For local iteration, loading policies from a bind-mounted directory removes the Hub round trip and applies changes as soon as files are saved. The [Quick Start](../quickstart.html) uses this approach. It is not suitable for production. ```yaml server: listenAddress: ":3594" pdp: inProcess: storage: driver: "disk" disk: directory: /policies watchForChanges: true (1) ``` | **1** | When set to true, the embedded PDP reloads policies whenever the files in the mounted directory change. Useful during development. | | ----- | ---------------------------------------------------------------------------------------------------------------------------------- | Add a bind mount for the policies directory to the `docker run` command: ```console $ docker run --rm --name synapse -p 3594:3594 \ -v $(pwd)/.synapse.yaml:/config/.synapse.yaml:ro \ (1) -v $(pwd)/extensions:/extensions:ro \ -v $(pwd)/policies:/policies:ro \ -e SYNAPSE_CONFIG=/config/.synapse.yaml \ (3) CERBOS_DISTRIBUTION_REPO/synapse/synapse:latest \ server ``` See [Configuration](../configuration/reference.html) for the Synapse-specific configuration schema, and the [Cerbos PDP configuration documentation](https://docs.cerbos.dev/cerbos/latest/configuration/) for the options available inside the `pdp.inProcess` block (TLS, observability, audit backends, schema enforcement). ## [](#%5Froute%5Fto%5Fan%5Fexternal%5Fcerbos%5Fpdp)Route to an external Cerbos PDP If an existing Cerbos PDP fleet is already running, Synapse can be deployed as a gateway in front of it instead of running an embedded PDP. Replace the `pdp.inProcess` block with `pdp.external` and point it at the PDP’s gRPC endpoint: ```yaml server: listenAddress: ":3594" pdp: external: grpcAddress: cerbos-pdp:3593 (1) httpAddress: cerbos-pdp:3592 (2) ``` | **1** | gRPC endpoint of the upstream Cerbos PDP. Synapse forwards authorization traffic to this address. | | ----- | ------------------------------------------------------------------------------------------------- | | **2** | HTTP endpoint of the upstream Cerbos PDP. Used for admin and health-check traffic. | Extensions behave the same in either mode. A proxy extension that enriches the principal runs before the request is forwarded to the external PDP; no extension code changes. ## [](#%5Fusing%5Fsynapse%5Fwith%5Fdocker%5Fcompose)Using Synapse with Docker Compose The following `compose.yaml` runs Synapse alongside an application container on the same Docker network. The application can reach Synapse at ``: ```yaml services: synapse: image: CERBOS_DISTRIBUTION_REPO/synapse/synapse:latest command: - server ports: - "3594:3594" volumes: - ./.synapse.yaml:/config/.synapse.yaml:ro - ./policies:/policies:ro - ./extensions:/extensions:ro environment: - SYNAPSE_CONFIG=/config/.synapse.yaml app: image: your-app:latest environment: CERBOS_URL: "http://synapse:3594" depends_on: - synapse ``` The same three bind mounts — configuration, policies and extensions — apply under Compose. For deployments with multiple Synapse instances or shared upstream services (for example, an external Cerbos PDP running in its own container), define each service under the top-level `services` key and rely on Docker’s default network for service-to-service name resolution. Installing Synapse using the Helm chart ==================== You need a valid licence key to obtain the Synapse Helm chart and container image. See [Managing licence keys](licence-keys.html) for generating, rotating, and revoking your licence key in Cerbos Hub. * Log in to the Cerbos distribution repository ```console $ docker login CERBOS_DISTRIBUTION_REPO --username=YOUR_LICENCE_USER --password=YOUR_LICENCE_KEY ``` * Inspect the chart and create a values file to suit your requirements (optional) ```console $ helm show values oci://CERBOS_DISTRIBUTION_REPO/synapse/cerbos-synapse:VERSION ``` * Install the chart ```console $ helm install \ --set=licence.user=YOUR_LICENCE_USER \ --set=licence.key=YOUR_LICENCE_KEY \ cerbos-synapse \ oci://CERBOS_DISTRIBUTION_REPO/synapse/cerbos-synapse:VERSION ``` ## [](#%5Fconvert%5Fexisting%5Fcerbos%5Fpdp%5Fhelm%5Fvalues%5Fto%5Fsynapse%5Fvalues)Convert existing Cerbos PDP Helm values to Synapse values You can deploy Synapse as a gateway in front of an existing Cerbos PDP fleet by configuring the `pdp.external` configuration key (see [Configuration](../configuration/reference.html)). However, if you prefer to replace PDPs with Synapse instances, the container ships with a utility to convert the existing PDP Helm values file into the values file format for Synapse. | | Please note that this is a best-effort translation using a set of rules that cover common deployments. You should carefully review the changes and test the chart installation in a development environment to ensure that it works as intended. | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Assuming that your existing values file is named `cerbos-pdp.yaml` in the current directory, the following command will print out the equivalent Synapse Helm values file. ```console $ docker run -it -v $(pwd):/work CERBOS_DISTRIBUTION_REPO/synapse/synapse -- helm migrate /work/cerbos-pdp.yaml ``` Managing licence keys ==================== A valid licence key is required to pull Synapse images and Helm charts from the Cerbos distribution repository. Licence keys are not specific to Synapse — they grant access to commercial Cerbos components generally, and each organization manages its key from the **Distribution licence** section of its organization settings in Cerbos Hub. See [Distribution licence](../../../cerbos-hub/distribution-licence.html) in the Cerbos Hub documentation for generating, rotating, and revoking keys. Whenever you see the following placeholders in the documentation, replace them with actual values from the licence settings screen. * `CERBOS_DISTRIBUTION_REPO`: registry endpoint displayed on the screen * `YOUR_LICENCE_USER`: Cerbos Hub organization ID * `YOUR_LICENCE_KEY`: Licence key displayed on the screen | | Treat the licence key like a secret and store it securely. Do not share it with anyone outside your organization. | | -------------------------------------------------------------------------------------------------------------------- | Guardrails for Aperture by Tailscale ==================== Synapse ships with a built-in system route extension for integrating with [Aperture by Tailscale](https://tailscale.com/docs/aperture) to secure AI workloads. When configured as a [guardrail](https://tailscale.com/docs/aperture/how-to/set-up-guardrails), it can enforce Cerbos policies on LLM requests before they leave your perimeter. The requirements for enabling the Aperture integration are: 1. [Sign up to Aperture by Tailscale](https://tailscale.com/docs/aperture/get-started) 2. [Configure Synapse to run as a Tailscale service](../configuration/tailscale.html) 3. [Configure a Synapse route extension](#configure-route-extension) 4. [Configure an Aperture hook to send requests to Synapse](#configure-hook) 5. [Define policy rules](#define-policy) ## [](#configure-route-extension)Configure the Aperture route extension Add the following configuration block to Synapse configuration file. ```yaml extensions: routeExtensions: aperture: extension: extensionURL: "system://aperture" routes: "/aperture": ["POST"] ``` You can optionally configure the route extension with the Cerbos resource policy, version and scope it should use for making access decisions. ```yaml extensions: routeExtensions: aperture: extension: extensionURL: "system://aperture" configuration: resourceKind: "aperture_hook" (1) resourcePolicyVersion: "production" (2) resourceScope: "acme.ai" (3) routes: "/aperture": ["POST"] ``` | **1** | Resource policy name. Defaults to aperture\_hook. | | ----- | ------------------------------------------------- | | **2** | Resource policy version. Defaults to default. | | **3** | Resource policy scope. Defaults to empty. | | | If the resource policy does not exist, LLM requests will be _allowed_ to go through. This is different from the standard PDP behaviour of denying access if no policy is found. | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## [](#configure-hook)Configure an Aperture hook Once Synapse is configured with the Aperture route extension as described above, the final step is to enforce policies by configuring your Aperture gateway to send requests to the endpoint. Refer to for full details. The minimal configuration required is as follows: ```json { "hooks": { "cerbos-synapse": { "url": "https://cerbos-synapse.tail9999999.ts.net/ext/aperture", (1) }, }, "grants": [{ "src": ["*"], "app": {"tailscale.com/cap/aperture": [{"models": "**", "send_hooks": [{ "name": "cerbos-synapse", "events": ["pre_request"], "send": [ "tools", "user_message", "estimated_cost", "grants", "quotas", ], }]}]}, }] } ``` | **1** | Replace with the full service URL. See [Run as a Tailscale service](../configuration/tailscale.html). | | ----- | ----------------------------------------------------------------------------------------------------- | ## [](#define-policy)Define a policy Synapse converts Aperture hook payloads into Cerbos `CheckResources` requests of the following shape: | CheckResources field | Value | Description | | ------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | requestID | metadata.request\_id | Taken from hook metadata object | | principal.id | metadata.login\_name | Taken from hook metadata object | | principal.roles | "user" | Hardcoded to "user" | | principal.attr.stable\_node\_id | metadata.stable\_node\_id | Taken from hook metadata object | | principal.attr.tailnet\_name | metadata.tailnet\_name | Taken from hook metadata object | | principal.attr.user\_agent | metadata.user\_agent | Taken from hook metadata object | | resource.id | "aperture\_hook" | Hardcoded to "aperture\_hook" | | resource.kind | "aperture\_hook" | Can be changed by setting the extension’s resourceKind configuration value | | resource.policyVersion | "default" | Can be changed by setting the extension’s resourcePolicyVersion configuration value | | resource.scope | "" | Can be changed by setting the extension’s resourceScope configuration value | | resource.attr.metadata | metadata | The entire metadata object from the hook request | | resource.attr.\* | \* | The other optional fields such as tool\_calls and user\_message included in the hook. See | If the specified policy (`aperture_hook` by default) does not exist, the LLM request is allowed to proceed. If the policy exists, the decision returned by the policy execution is converted to an `allow` or `block` [response expected by Aperture](https://tailscale.com/docs/aperture/configuration#hook-response-format). Policy authors can optionally override the hook response using policy outputs. Note that if a policy output is produced, it’s merged with the default hook response and overwrites any existing fields. Therefore, be aware of copy-paste errors where `EFFECT_ALLOW` rules still produce outputs with `{"action": "block"}` and vice versa. Example policy restricting access to top secret files ```yaml --- apiVersion: api.cerbos.dev/v1 resourcePolicy: resource: aperture_hook version: production rules: - actions: ["hook_event"] effect: EFFECT_DENY roles: ["*"] condition: match: expr: |- R.attr.tool_calls.exists(tc, tc.params.command.contains("/top/secret")) output: when: ruleActivated: |- {"message": "Cannot access top secret data"} - actions: ["hook_event"] effect: EFFECT_ALLOW roles: ["*"] output: when: ruleActivated: |- {"message": "Allowed by Cerbos policy"} ``` ## [](#%5Fadvanced%5Fuses)Advanced uses The built-in Aperture route extension provided by Synapse is designed for general-purpose use. If you require more advanced handling that cannot be easily defined using the primitives provided by Cerbos policies (classifying prompts using another LLM to obtain a risk score, for example), a custom extension would be more suited for those purposes. Refer to [custom extension development documentation](../extensions/building.html) for more information. Securing Claude Code with Synapse ==================== Synapse ships with a demo route extension that can be used as a [Claude Code hook handler](https://code.claude.com/docs/en/hooks) to control Claude Code through Cerbos policies. Administrators can use MDM or [server-managed settings](https://code.claude.com/docs/en/server-managed-settings) to enforce hooks over the whole organization and ensure that security requirements are being met. To enable the route extension, add the following configuration block to the Synapse configuration file. ```yaml extensions: routeExtensions: claude: extension: extensionURL: "system://claude" routes: "/claude": ["POST"] ``` Now configure Claude Code hooks to invoke the above endpoint. There are [multiple ways to do this](https://code.claude.com/docs/en/hooks#hook-locations) dependening on your preference. For production deployments, consider using MDM or [server-managed settings](https://code.claude.com/docs/en/server-managed-settings) as it prevents users from overriding the configuration. The following snippet demonstrates a typical hook configuration. ```json { "hooks": { "PermissionRequest": [ { "matcher": "*", "hooks": [ { "type": "http", "statusMessage": "Checking with Synapse", "url": "https://Synapse.example.com:3594/ext/claude", "headers": { "X-Claude-User-Roles": "claude" } } ] } ], "PreToolUse": [ { "matcher": "*", "hooks": [ { "type": "http", "statusMessage": "Checking with Synapse", "url": "https://Synapse.example.com:3594/ext/claude", "headers": { "X-Claude-User-Roles": "claude" } } ] } ], "PostToolUse": [ { "matcher": "*", "hooks": [ { "type": "http", "statusMessage": "Checking with Synapse", "url": "https://Synapse.example.com:3594/ext/claude", "headers": { "X-Claude-User-Roles": "claude" } } ] } ], "PostToolUseFailure": [ { "matcher": "*", "hooks": [ { "type": "http", "statusMessage": "Checking with Synapse", "url": "https://Synapse.example.com:3594/ext/claude", "headers": { "X-Claude-User-Roles": "claude" } } ] } ] } } ``` With this configuration, every tool call made by Claude Code will be checked by Synapse. Since there are no policies defined yet, everything will be allowed by default. If audit log capture is enabled, all the requests will be logged and available for analysis. This is a good way to do a pass-through or a dry-run to gather data about usage before writing policies. ## [](#%5Fwriting%5Fcerbos%5Fpolicies)Writing Cerbos policies | | Currently, Claude Code only supports [HTTP hooks for a subset of events](https://code.claude.com/docs/en/hooks#prompt-based-hooks). If you need to secure other events, use a command hook that simply uses curl to call the Synapse endpoint. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | When the hook endpoint is called, Synapse converts the input to a Cerbos `CheckResources` request of the following form. * `principal.id` ⇒ If the request includes a `X-Claude-User` header, the value from that header. Otherwise, `claude_user`. * `principal.roles` ⇒ If the request includes a `X-Claude-User-Roles` header, the value(s) from that header. Otherwise, `claude_user`. * `resource.kind` ⇒ Hook event name. E.g. `PreToolUse`. * `resource.attr` ⇒ Hook payload. See [hook event documentation](https://code.claude.com/docs/en/hooks#hook-events) for more details. * `resource.scope` ⇒ Optional. Set only if the `X-Cerbos-Scope` header is present in the request. * `action` ⇒ For tool call hooks (`PermissionRequest`, `PreToolUse`, `PostToolUse`, `PostToolUseFailure`) the name of the tool (e.g. `Bash`). For other hooks, it’s fixed to `event`. If a policy is found and returns a decision of either `EFFECT_ALLOW` or `EFFECT_DENY`, it’s converted to the appropriate allow or deny response for the hook. It’s possible to provide extra information to attach to the response (e.g. deny reason, extra context etc.) using policy outputs. Note that the output from the policy is merged verbatim into the hook response and it must be a valid response as expected by that particular hook. Invalid or incorrect output values might cause Claude Code to ignore the response from the Hook and continue the action even if it has been denied. The following example demonstrates a `PreToolUse` policy that prevents reads or writes from sensitive locations and bans Bash altogether. ```yaml --- apiVersion: api.cerbos.dev/v1 resourcePolicy: resource: PreToolUse version: default rules: - actions: ["Read", "Write"] effect: EFFECT_DENY roles: ["*"] condition: match: any: of: - expr: |- R.attr.tool_input.file_path.startsWith("/etc") - expr: |- R.attr.tool_input.file_path.startsWith("/top/secret") output: when: ruleActivated: |- {"hookSpecificOutput": {"permissionDecision": "deny", "permissionDecisionReason": "Accessing top secret data is forbidden"}} - actions: ["Bash"] effect: EFFECT_DENY roles: ["*"] output: when: ruleActivated: |- {"hookSpecificOutput": {"permissionDecision": "deny", "permissionDecisionReason": "No Bash for you"}} - actions: ["*"] effect: EFFECT_ALLOW roles: ["*"] output: when: ruleActivated: |- {"hookSpecificOutput": {"permissionDecision": "allow", "permissionDecisionReason": "Go ahead"}} ``` With the above policy in place, attempting to do an action that invokes Bash would fail. ● Bash(mktemp -d /foo/tmp.XXXXXX) ⎿  PreToolUse:Bash hook returned blocking error ⎿  No Bash for you ⎿  Error: No Bash for you ● The Bash tool is being blocked by a pre-tool hook (No Bash for you). I don't have another way to create a directory without it. You may want to check your hooks configuration or create it manually with: mkdir /foo/tmp_