Quick Start
Cerbos 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:
-
The caller sends a
CheckResourcesorPlanResourcesrequest. -
Synapse runs the configured proxy extensions, which may modify the request (for example, by adding attributes to the principal) and later modify the response.
-
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 for the pdp.external configuration. The proxy extension written here behaves the same way in either topology.
|
Before you begin
You will need:
-
Docker
-
A Synapse licence. If you do not already have one, sign up at cerbos.dev/workshop. Your licence credentials are issued together with the URL of the Cerbos distribution repository, substitute that URL wherever
CERBOS_DISTRIBUTION_REPOappears in this guide. -
curland a JSON formatter such aspython3 -m json.toolorjq(optional, for reading responses).
Log in to the Cerbos distribution repository:
$ docker login CERBOS_DISTRIBUTION_REPO --username=YOUR_LICENCE_USER --password=YOUR_LICENCE_KEY
Create a working directory
Create a directory that will hold the configuration, policies and extension code:
$ 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.
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:
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 for details. |
Start Synapse
Create config.yaml with the minimum configuration needed to run Synapse with an in-process PDP that loads policies from disk:
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:
$ docker run --rm --name synapse-qs -p 3594:3594 \
-v $(pwd)/config.yaml:/config/config.yaml:ro \
-v $(pwd)/policies:/policies:ro \
-v $(pwd)/extensions:/extensions:ro \
CERBOS_DISTRIBUTION_REPO/synapse/synapse:latest \
server --conf.path=/config/config.yaml --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.
Make a check request
In a new terminal, from the synapse-quickstart directory, create check-request.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:
$ curl -s -X POST http://localhost:3594/api/check/resources \
-H 'Content-Type: application/json' \
-d @check-request.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.
Add a proxy extension
Create extensions/enrich_principal.star:
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 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, 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. |
Register the extension by replacing the contents of config.yaml with:
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 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, by contrast, are 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. Extensions cannot add roles to a request that arrives without them. |
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:
{"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:
$ curl -s -X POST http://localhost:3594/api/check/resources \
-H 'Content-Type: application/json' \
-d @check-request.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.
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
configurationblock of the extension definition inconfig.yamland read them inside the extension via thecontext.extension_configdictionary. See Developing Starlark extensions 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: falseso the chain continues when the lookup fails. If the attribute is load-bearing for policy decisions, keeprequired: trueso 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.
Next steps
-
Proxy extensions covers the full configuration reference, priority ordering, and the response-side hooks (
augment_check_response,augment_plan_response). -
Starlark documents every module available to extensions, including
json,hashlib, and thecerbos.*helpers for data-source lookups and cache access. -
WASM describes the alternative runtime for extensions written in any language that compiles to WebAssembly.
-
Configuration 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 lists them; all of them work against the Synapse endpoint unchanged.