Deployment guide

SpiceDB

Overview

SpiceDB is an open source permissions database, inspired by the system Google described in its Zanzibar paper. Instead of scattering permission checks through your application code, you store relationships (for example, alice is a reader of the roadmap document). You describe in a schema how those relations compose into permissions. Then you ask the server at request time: may this user do this thing to this object?

SpiceDB answers CheckPermission for a single yes/no decision. It answers LookupResources or LookupSubjects when you need to filter, so "which documents may alice view?" is a single call rather than a fan-out of checks. Official SDKs are published for Go, Java, JavaScript, Python and Ruby, and the zed command line client is installed in this image.

The SecureAnyCloud image delivers SpiceDB 1.56.2 on Ubuntu 24.04. Both APIs serve TLS, PostgreSQL provides storage, and a unique pre-shared key is generated on the first boot of your VM.

What you get:

  • SpiceDB 1.56.2 as a single Go binary at /usr/local/bin/spicedb, run by systemd as the unprivileged spicedb user
  • The gRPC API on port 50051, the primary interface spoken by every official SDK and by zed, serving TLS with a certificate generated for your VM
  • The HTTP and JSON gateway on port 8443, also over TLS, for callers that prefer REST
  • zed 1.2.1, the official command line client, at /usr/local/bin/zed
  • PostgreSQL 16 on the loopback address only, so your schema and relationships survive restarts
  • A pre-shared key is mandatory, and yours is unique. SpiceDB will not start without one, and this image never bakes one in: the key, the database password and the TLS certificate are all generated on your VM's first boot and written to a root-only file
  • The Prometheus metrics and pprof endpoint bound to the loopback interface, and telemetry reporting disabled
  • A starter schema, written on first boot so there is something to explore immediately
  • postgresql and spicedb systemd services, enabled and active
  • 24/7 SecureAnyCloud support

SpiceDB and AuthZed are trademarks of their respective owners. All product and company names are trademarks or registered trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them. This image packages the unmodified open source software, which is distributed under the Apache License 2.0.

Prerequisites

Before you deploy, make sure you have an active Azure subscription, an SSH key pair, and a VNet and subnet in the target region. Standard_B2s (2 vCPU, 4 GiB RAM) runs SpiceDB comfortably for typical application authorization traffic. Choose a larger size for high check volumes or very large relationship graphs.

In the network security group, allow these inbound rules:

  • 22/tcp from your management network
  • 50051/tcp for the gRPC API
  • 8443/tcp if you intend to use the HTTP gateway
1

Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource and search the Marketplace for SpiceDB by SecureAnyCloud. Select Create.

On Basics, pick your subscription, resource group, region and size. Under Administrator account, choose SSH public key and paste your key. Under Inbound port rules, allow SSH (22). Then select Review + create and Create. Open 50051 and 8443 afterwards, as shown in the next step.

2

Deploy from the Azure CLI

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:spicedb-ubuntu-24-04:default:latest
az vm create \
  --resource-group my-resource-group \
  --name my-spicedb \
  --image sac1647283583153:spicedb-ubuntu-24-04:default:latest \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

Then open the ports you need. Treat these as you would a database port. Restrict the source to the application subnets that will call SpiceDB, not the whole internet:

az vm open-port --resource-group my-resource-group --name my-spicedb --port 50051 --priority 1001
az vm open-port --resource-group my-resource-group --name my-spicedb --port 8443 --priority 1002
3

Connect to your VM

ssh azureuser@<vm-public-ip>

The image has no other login account. The build account used to create the image is removed before capture, so azureuser (or the name you chose at deployment) is the only way in.

4

Confirm SpiceDB is running

systemctl is-active postgresql spicedb
active active

Check the versions that are installed:

spicedb version zed version --skip-version-check
spicedb v1.56.2 client: zed v1.2.1 service: (unknown)

Note that zed version reports service: (unknown) until you give it an endpoint and a token. The next steps do that.

5

Check what the network can reach

Check this deliberately on a permissions database. Only three TCP ports are reachable from outside the VM: SSH, the gRPC API and the HTTP gateway. PostgreSQL and the Prometheus/pprof endpoint are bound to the loopback address and cannot be reached from the network at all. List the listening sockets to see this:

sudo ss -Hltn | awk '{print $4}' | sort -u
*:50051 *:8443 0.0.0.0:22 127.0.0.1:5432 127.0.0.1:9090 127.0.0.53%lo:53 127.0.0.54:53 [::]:22

The image ships an executable assertion of that exact set, so you can re-check it at any time, for example after your own configuration changes:

sudo /usr/local/sbin/spicedb-port-check.sh
off-box TCP exactly [22 8443 50051]; Prometheus/pprof 9090 and PostgreSQL 5432 loopback-only dispatch-cluster 50053 not listening on any interface SPICEDB_PORTS_OK

Two of those ports deserve a note, because both differ from SpiceDB's own defaults:

  • Port 9090 serves Prometheus metrics and /debug/pprof/. Upstream enables it on every interface by default. This image binds it to 127.0.0.1, so it is available to anything running on the VM and to nobody else. To scrape it remotely, reach it through an SSH tunnel.
  • Port 50053 is the dispatch cluster, used when SpiceDB runs as a multi-node cluster that distributes and caches sub-problems between peers. A single VM does not need it, so it is disabled and not listening.
6

Retrieve the first-boot pre-shared key

No credential ships in this image. --grpc-preshared-key is a required flag with no default, and the server exits rather than starting without it. On its first boot, your VM generates its own 256-bit key, database password and TLS certificate. It writes the details to a root-only file:

sudo cat /root/spicedb-credentials.txt
# SpiceDB on Ubuntu 24.04 LTS by sac
# Generated on this VM's first boot — unique to this VM. Keep this file secret. # # SpiceDB cannot run without a pre-shared key: --grpc-preshared-key is a # required flag with no default. The key below is this VM's own, it is the only
# credential that reaches either API, and no other VM has it.
#
# zed --endpoint <vm-public-ip>:50051 --token "$SPICEDB_API_TOKEN" --certificate-path /etc/ssl/spicedb/spicedb.crt schema read
# SPICEDB_API_TOKEN=<your-token> SPICEDB_GRPC_ENDPOINT=<vm-public-ip>:50051 SPICEDB_HTTP_ENDPOINT=https://<vm-public-ip>:8443 SPICEDB_HOST=<vm-public-ip> SPICEDB_TLS_CERT=/etc/ssl/spicedb/spicedb.crt

Both that file and the environment file the service reads the key from are 0600 root:root:

sudo stat -c '%A %U:%G %n' /root/spicedb-credentials.txt /etc/spicedb/spicedb.env
-rw------- root:root /root/spicedb-credentials.txt -rw------- root:root /etc/spicedb/spicedb.env

Copy the key somewhere safe, such as a secret manager rather than a file on disk, and treat it as you would a database password. Every command below reads the key straight out of that file into a shell variable, so the value never appears in your shell history:

KEY=$(sudo grep '^SPICEDB_API_TOKEN=' /root/spicedb-credentials.txt | cut -d= -f2-)
echo "key length: ${#KEY} characters"
key length: 64 characters
7

Confirm both APIs refuse everything but your key

A request carrying no credential is refused:

curl -sk -o /dev/null -w '%{http_code}\n' -X POST https://127.0.0.1:8443/v1/schema/read -d '{}'
401

A request carrying the wrong key is refused too. SpiceDB answers 403 here rather than 401, because the credential was understood and rejected rather than missing:

curl -sk -o /dev/null -w '%{http_code}\n' -X POST \
  -H 'Authorization: Bearer not-the-right-token' \
  https://127.0.0.1:8443/v1/schema/read -d '{}'
403

Your own key is accepted:

KEY=$(sudo grep '^SPICEDB_API_TOKEN=' /root/spicedb-credentials.txt | cut -d= -f2-)
curl -sk -o /dev/null -w '%{http_code}\n' -X POST \
  -H "Authorization: Bearer ${KEY}" \
  https://127.0.0.1:8443/v1/schema/read -d '{}'
200

The image ships that whole proof as one command. It also checks the gRPC port and confirms that the metrics and pprof endpoint is not reachable from the network:

sudo /usr/local/sbin/spicedb-verify-auth.sh
HTTP gateway: no credential 401; empty bearer 401; wrong key 403; this VM's key accepted gRPC: this VM's key accepted, a wrong key refused Prometheus/pprof: 200 on loopback, unreachable on the external address SPICEDB_AUTH_OK
8

Read the starter schema

A schema declares the object types in your application, the relations they support, and how those relations compose into permissions. Your VM wrote a starter schema on first boot so there is something to work with.

zed speaks the gRPC API. The certificate was generated on your VM rather than issued by a public authority. Point zed at the certificate the image created, which is world-readable at /etc/ssl/spicedb/spicedb.crt:

KEY=$(sudo grep '^SPICEDB_API_TOKEN=' /root/spicedb-credentials.txt | cut -d= -f2-) zed --endpoint 127.0.0.1:50051 --token "${KEY}" \
  --certificate-path /etc/ssl/spicedb/spicedb.crt --skip-version-check \
  schema read
definition document { relation writer: user relation reader: user permission edit = writer permission view = reader + edit } definition user {}

Read the schema this way. A document has writers and readers. edit is granted to writers. view is granted to readers and to anyone who can edit. That last line is the part a permissions table cannot express. view is computed, so granting someone writer automatically grants them view without a second row being written anywhere.

To use your own schema, write it to a file and send it:

KEY=$(sudo grep '^SPICEDB_API_TOKEN=' /root/spicedb-credentials.txt | cut -d= -f2-)
cat > /tmp/my-schema.zed <<'SCHEMA' definition user {} definition team { relation member: user } definition document { relation writer: user | team#member relation reader: user | team#member permission edit = writer permission view = reader + edit } SCHEMA zed --endpoint 127.0.0.1:50051 --token "${KEY}" \
  --certificate-path /etc/ssl/spicedb/spicedb.crt --skip-version-check \
  schema write /tmp/my-schema.zed

user | team#member means a relation can be filled either by a user directly or by every member of a team. This is how group-based access is expressed without duplicating rows per user.

9

Write relationships and ask questions

A relationship is a single fact: this subject has this relation to this object. Write one:

KEY=$(sudo grep '^SPICEDB_API_TOKEN=' /root/spicedb-credentials.txt | cut -d= -f2-) zed --endpoint 127.0.0.1:50051 --token "${KEY}" \
  --certificate-path /etc/ssl/spicedb/spicedb.crt --skip-version-check \
  relationship touch document:roadmap reader user:alice

touch writes the relationship whether or not it already exists. Use create instead if you want the call to fail when the relationship is already there. That is the right choice when a duplicate would mean a bug in your application.

Now ask the question the whole system exists to answer:

KEY=$(sudo grep '^SPICEDB_API_TOKEN=' /root/spicedb-credentials.txt | cut -d= -f2-) zed --endpoint 127.0.0.1:50051 --token "${KEY}" \
  --certificate-path /etc/ssl/spicedb/spicedb.crt --skip-version-check \
  permission check --consistency-full document:roadmap view user:alice
true

Now check a user who was never granted anything:

KEY=$(sudo grep '^SPICEDB_API_TOKEN=' /root/spicedb-credentials.txt | cut -d= -f2-) zed --endpoint 127.0.0.1:50051 --token "${KEY}" \
  --certificate-path /etc/ssl/spicedb/spicedb.crt --skip-version-check \
  permission check --consistency-full document:roadmap view user:bob
false

To filter rather than decide, ask which documents alice may view. This takes one call, not one call per document:

KEY=$(sudo grep '^SPICEDB_API_TOKEN=' /root/spicedb-credentials.txt | cut -d= -f2-) zed --endpoint 127.0.0.1:50051 --token "${KEY}" \
  --certificate-path /etc/ssl/spicedb/spicedb.crt --skip-version-check \
  permission lookup-resources --consistency-full document view user:alice
roadmap
10

Consistency: why --consistency-full is in every command above

SpiceDB is a distributed system by design, and its default consistency is minimize_latency: it answers from a recent, quantized snapshot of the database (five seconds wide by default) so that checks are fast and cacheable. That is the right default for the overwhelming majority of permission checks, where a permission granted a moment ago becoming visible a moment later is fine.

It also means that a check issued immediately after the write that grants it can legitimately return false. That is not a bug, and it is the most common surprise for people new to SpiceDB. You have three options:

  • --consistency-full: evaluate at the newest revision in the database. It is always correct and the slowest option. Every example above uses it so the results are reproducible.
  • A ZedToken: every write returns one. Passing it back with --consistency-at-least guarantees that the answer reflects at least that write. Build applications around this option. Store the ZedToken alongside the resource you just changed, and pass it on subsequent checks for that resource.
  • The default: use it for checks that do not immediately follow a related write.

You can watch the difference. Write a relationship and check it at the default consistency, then at full consistency:

KEY=$(sudo grep '^SPICEDB_API_TOKEN=' /root/spicedb-credentials.txt | cut -d= -f2-) ZED="zed --endpoint 127.0.0.1:50051 --token ${KEY} --certificate-path /etc/ssl/spicedb/spicedb.crt --skip-version-check" ${ZED} relationship touch document:quarterly-plan reader user:dana >/dev/null echo "minimize-latency: $(${ZED} permission check --consistency-min-latency document:quarterly-plan view user:dana 2>&1 | tail -1)" echo "full: $(${ZED} permission check --consistency-full document:quarterly-plan view user:dana 2>&1 | tail -1)"
minimize-latency: false full: true

Wait a few seconds and run it again. The minimize-latency answer becomes true as the quantization window rolls forward. If the window has already rolled when you first run the commands, both lines read true. Run the pair a few times and you will see the difference appear and disappear. That variability is the point. A permission check that must reflect a write you have just made needs either --consistency-full or the ZedToken that write returned.

11

The same operations over the HTTP gateway

Everything above is available as JSON over HTTPS on port 8443, for callers that do not have a gRPC stack. The gateway uses the same server and the same authentication. It is a translation layer in front of the gRPC API, not a second permission model.

Write a relationship:

KEY=$(sudo grep '^SPICEDB_API_TOKEN=' /root/spicedb-credentials.txt | cut -d= -f2-)
curl -sk -X POST -H "Authorization: Bearer ${KEY}" -H 'Content-Type: application/json' \
  https://127.0.0.1:8443/v1/relationships/write \
  -d '{"updates":[{"operation":"OPERATION_TOUCH","relationship":{"resource":{"objectType":"document","objectId":"roadmap"},"relation":"writer","subject":{"object":{"objectType":"user","objectId":"carol"}}}}]}'

Then check a permission. Note that carol was granted writer, not reader. view is computed through edit, so the answer is still PERMISSIONSHIP_HAS_PERMISSION:

KEY=$(sudo grep '^SPICEDB_API_TOKEN=' /root/spicedb-credentials.txt | cut -d= -f2-)
curl -sk -X POST -H "Authorization: Bearer ${KEY}" -H 'Content-Type: application/json' \
  https://127.0.0.1:8443/v1/permissions/check \
  -d '{"consistency":{"fullyConsistent":true},"resource":{"objectType":"document","objectId":"roadmap"},"permission":"view","subject":{"object":{"objectType":"user","objectId":"carol"}}}' \
  | python3 -m json.tool
{ "checkedAt": { "token": "GhAKBENNQUoSCDMxMDg0OGVk" }, "permissionship": "PERMISSIONSHIP_HAS_PERMISSION", "partialCaveatInfo": null, "debugTrace": null, "optionalExpiresAt": null }

The checkedAt.token is the ZedToken described in Step 10. This is where you get one to store alongside your resource.

12

Connect from your application

From outside the VM, point your SDK or zed at the VM's address and pass the same key as a bearer token. The certificate is generated on the VM, so copy /etc/ssl/spicedb/spicedb.crt to the client machine and use it as the certificate authority:

scp azureuser@<vm-public-ip>:/etc/ssl/spicedb/spicedb.crt ./spicedb.crt zed context set my-spicedb <vm-public-ip>:50051 "<your-token>" --certificate-path ./spicedb.crt zed permission check --consistency-full document:roadmap view user:alice

zed context set stores the endpoint and token in your local zed configuration, so you do not have to repeat them. Remember that this writes the key to disk on the client machine.

In Go, the equivalent is the authzed-go client with grpcutil.WithCustomCerts and a BearerToken call credential; in Python, authzed.api.v1.Client with bearer_token_credentials. Every official SDK takes the same two inputs: the endpoint and the pre-shared key.

If you would rather not distribute the self-signed certificate, replace it with one from a public authority, as shown in the next step.

13

Use your own certificate and domain

Point a DNS record at the VM's public address, then obtain a certificate for it and tell SpiceDB to use it. Only the certificate and key paths change:

sudo apt-get install -y certbot
sudo certbot certonly --standalone -d spicedb.your-domain.com
sudo install -o root -g spicedb -m 0644 /etc/letsencrypt/live/spicedb.your-domain.com/fullchain.pem /etc/ssl/spicedb/spicedb.crt
sudo install -o root -g spicedb -m 0640 /etc/letsencrypt/live/spicedb.your-domain.com/privkey.pem /etc/ssl/spicedb/spicedb.key
sudo systemctl restart spicedb

Certbot's standalone mode needs port 80 reachable for the challenge. Open it for the duration of the request and close it afterwards. With a publicly trusted certificate in place, clients no longer need --certificate-path and can verify normally against the system trust store.

14

Back up your permissions data

Your schema and relationships live in the co-located PostgreSQL database. zed can export them in SpiceDB's own format, which is the portable option and the one to prefer:

KEY=$(sudo grep '^SPICEDB_API_TOKEN=' /root/spicedb-credentials.txt | cut -d= -f2-) sudo mkdir -p /var/backups/spicedb zed --endpoint 127.0.0.1:50051 --token "${KEY}" \
  --certificate-path /etc/ssl/spicedb/spicedb.crt --skip-version-check \
  backup create /var/backups/spicedb/spicedb-backup.zedbackup
sudo ls -la /var/backups/spicedb/

A database-level dump is an alternative that captures the datastore exactly as it is:

sudo -u postgres pg_dump -Fc spicedb -f /var/backups/spicedb/spicedb.dump

Copy either file off the VM, to Azure Blob Storage for example, on whatever schedule suits your recovery objective.

15

Upgrade SpiceDB

Replace the binary, run any datastore migration the new version needs, and restart. Check the release notes first. SpiceDB occasionally ships a migration that must be applied in a specific phase, and the release notes say so when that is the case.

curl -fsSL -o /tmp/spicedb.tar.gz https://github.com/authzed/spicedb/releases/download/v<version>/spicedb_<version>_linux_amd64.tar.gz tar -xzf /tmp/spicedb.tar.gz -C /tmp spicedb
sudo install -o root -g root -m 0755 /tmp/spicedb /usr/local/bin/spicedb
sudo -u postgres true && source /etc/spicedb/spicedb.env && sudo /usr/local/bin/spicedb datastore migrate head --datastore-engine postgres --datastore-conn-uri "${SPICEDB_DATASTORE_CONN_URI}"
sudo systemctl restart spicedb spicedb version

The operating system patches itself. Unattended-upgrades is enabled in the image, so security updates are applied automatically.

16

Rotate the pre-shared key

The key is read from /etc/spicedb/spicedb.env. Replace it, restart, and update your clients. SpiceDB accepts more than one key at a time. To rotate with no downtime, add the new key alongside the old one, migrate your clients, then remove the old key:

# Generate a replacement with:
openssl rand -hex 32
# Then put it in place of <your-token> below.
sudo sed -i 's/^SPICEDB_GRPC_PRESHARED_KEY=.*/SPICEDB_GRPC_PRESHARED_KEY=<your-token>/' /etc/spicedb/spicedb.env
sudo sed -i 's/^SPICEDB_API_TOKEN=.*/SPICEDB_API_TOKEN=<your-token>/' /root/spicedb-credentials.txt
sudo systemctl restart spicedb

Keep /root/spicedb-credentials.txt in step with the environment file, as above. The image's own health checks read the key from the credentials file, and they report a failure if the two disagree.

Troubleshooting

spicedb.service is not running. Look at the journal first:

sudo systemctl is-active spicedb postgresql
sudo systemctl show -p ExecMainStatus -p NRestarts spicedb
sudo journalctl -u spicedb -p warning -n 20 --no-pager
active active ExecMainStatus=0 NRestarts=0

Filtering to -p warning keeps that last command quiet on a healthy VM, so anything it prints is worth reading. SpiceDB logs in JSON (--log-format json). For the full picture, use sudo journalctl -u spicedb -n 50 --no-pager | jq -r 'select(.level!="info") | .message'. Alternatively, drop the filter entirely and pipe through jq . to make the output readable.

The most common causes are a missing or empty SPICEDB_GRPC_PRESHARED_KEY in /etc/spicedb/spicedb.env (SpiceDB refuses to start without one, by design), a certificate or key file that has been replaced with one the spicedb user cannot read, and PostgreSQL not being up.

A check returns false for a permission you just granted. That is the consistency default described in Step 10. Run the check again with --consistency-full, and use ZedTokens in your application.

A client cannot connect over gRPC. Confirm the port is open in the network security group, and that the client trusts the certificate. Either pass --certificate-path with a copy of /etc/ssl/spicedb/spicedb.crt, or install a publicly trusted certificate as in Step 13. A TLS failure and a firewall failure look similar from the client. Run zed with --log-level debug to tell them apart.

The whole appliance, checked in one command. This runs every posture check the image ships with, including feeding each one a known-bad input to prove the check itself still works:

sudo /usr/local/sbin/spicedb-selftest.sh
SPICEDB_SELFTEST_OK

Support

This image is maintained by SecureAnyCloud with 24/7 support for deployment, upgrades, certificates and custom domains, SDK integration, schema design and scaling. Contact sales@secureanycloud.com.

SpiceDB is free and open source software under the Apache License 2.0. There is no subscription or license key. The SecureAnyCloud charge covers packaging, hardening and support.

Deploy on Azure

Launch SpiceDB on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure