Deployment guide

OpenFGA

Overview

OpenFGA is an open-source fine-grained authorization server, built on the relationship-based model described in Google's Zanzibar paper. Instead of scattering permission checks through your application code, you store relationships (for example, alice is a viewer of the roadmap document) and describe the types and relations that exist in an authorization model. At request time, you ask the server one question: may this user do this thing to this object?

OpenFGA is a CNCF project. Official SDKs are published for JavaScript, Go, Python, Java and .NET, and everything is reachable over an HTTP (REST) API and a gRPC API.

The SecureAnyCloud image delivers OpenFGA 1.20.0 on Ubuntu 24.04, served over HTTPS and backed by PostgreSQL. A unique API token is generated on the first boot of your VM, and 24/7 SecureAnyCloud support backs the image.

What you get:

  • OpenFGA 1.20.0 as a single Go binary at /usr/local/bin/openfga, run by systemd as the unprivileged openfga user and bound to 127.0.0.1:8080 only
  • nginx terminating TLS on port 443 with a certificate generated for your VM. Port 80 only redirects to HTTPS, plus a plain /healthz for load balancer probes
  • PostgreSQL 16 on the loopback address only, so stores, authorization models and relationship tuples survive restarts
  • Pre-shared key authentication enforced. OpenFGA's own default is no authentication at all. This image never runs in that posture, and the development Playground console is disabled
  • No shared credential: the API token, the database password and the TLS key are all generated on your VM's first boot and written to a root-only file
  • A starter store and a sample authorization model, created on first boot so there is something to explore immediately
  • postgresql, openfga and nginx systemd services, enabled and active

OpenFGA is a trademark of its respective owner. All product and company names are trademarks or registered trademarks of their respective holders. Their use 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

You need an active Azure subscription, an SSH key pair, and a VNet with a subnet in the target region.

Standard_B2s (2 vCPU, 4 GiB RAM) runs OpenFGA comfortably for typical application authorization traffic. Choose a larger size for high check volumes or very large relationship graphs.

Configure these network security group inbound rules: 22/tcp from your management network, 443/tcp for the API, and optionally 80/tcp, which only redirects to HTTPS.

1

Deploy from the Azure Marketplace

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

On the Basics tab, 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) and HTTPS (443).

Then select Review + create and Create.

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:openfga-ubuntu-24-04:default:latest
az vm create \
  --resource-group my-resource-group \
  --name my-openfga \
  --image sac1647283583153:openfga-ubuntu-24-04:default:latest \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

Open the port you need for the API:

az vm open-port --resource-group my-resource-group --name my-openfga --port 443 --priority 1001
3

Connect to your VM

Connect over SSH:

ssh azureuser@<vm-public-ip>

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

4

Confirm the OpenFGA stack is running

Check that PostgreSQL, OpenFGA and nginx are all active:

systemctl is-active postgresql openfga nginx
active active active

Check the installed version:

sudo /usr/local/bin/openfga version 2>&1 | sed 's#^[0-9/]* [0-9:]* ##'
OpenFGA version `v1.20.0` build from `73591ef16ce508623920d5b706286ffcdfb6841b` on `2026-09-08T20:19:22Z`

OpenFGA writes that line to standard error and prefixes it with the current time, which is why the command redirects the output and trims the timestamp.

Then confirm the API answers through nginx:

curl -sk https://127.0.0.1/healthz
ok
5

Check what the network can reach

On an authorization server, check the listening ports deliberately. Only three TCP ports are reachable from outside the VM: SSH, the HTTPS redirect and the API itself. The OpenFGA HTTP API, its gRPC API, its Prometheus metrics endpoint and PostgreSQL are all bound to the loopback address and cannot be reached from the network at all:

sudo ss -Hltn | awk '{print $4}' | sort -u
0.0.0.0:22 0.0.0.0:443 0.0.0.0:80 127.0.0.1:2112 127.0.0.1:5432 127.0.0.1:8080 127.0.0.1:8081 127.0.0.53%lo:53 127.0.0.54:53 [::]:22 [::]:443 [::]:80

The image ships a script that asserts that exact set of ports. Re-run it at any time, for example after your own configuration changes:

sudo /usr/local/sbin/openfga-port-check.sh
off-box TCP exactly [22 80 443]; API 8080, gRPC 8081, metrics 2112, PostgreSQL 5432 all loopback no Playground on 3000, no pprof on 3001 OPENFGA_PORTS_OK
6

Retrieve the first-boot API token

No credential ships in this image. On first boot, your VM generates its own 256-bit API token, database password and TLS certificate. It writes the credentials to a root-only file. Read it with sudo:

sudo cat /root/openfga-credentials.txt
# OpenFGA on Ubuntu 24.04 LTS by sac
# Generated on this VM's first boot — unique to this VM. Keep this file secret. # # OpenFGA ships with NO authentication by default. This image runs it with # pre-shared key authentication, and the key below is the ONLY credential that # reaches the API. Every request must carry it: # # curl -sk -H "Authorization: Bearer $OPENFGA_API_TOKEN" https://<vm-public-ip>/stores # OPENFGA_API_TOKEN=<your-token> OPENFGA_API_URL=https://<vm-public-ip> OPENFGA_HOST=<vm-public-ip>

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

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

Copy the token somewhere safe, preferably a secret manager rather than a file on disk. Treat it as you would a database password.

7

Confirm the API refuses everything but your token

OpenFGA's upstream default is no authentication. If you run it with no flags, the entire management and query API is open to anyone who can reach the port. This image refuses a request with no credential:

curl -sk -o /dev/null -w '%{http_code}\n' https://127.0.0.1/stores
401

It also refuses a request that carries the wrong token:

curl -sk -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer not-the-right-token" https://127.0.0.1/stores
401

Your own token is accepted:

curl -sk -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer <OPENFGA_API_TOKEN>" https://127.0.0.1/stores
200

The image ships that whole proof as one command. It also confirms the Playground is not listening:

sudo /usr/local/sbin/openfga-verify-auth.sh
unauthenticated 401; wrong key 401; empty bearer 401; per-VM key 200; no Playground OPENFGA_AUTH_OK

Why the Playground is disabled

OpenFGA ships a browser console called the Playground. It is a development tool, it is unauthenticated, and upstream has deprecated it. This image disables it.

OpenFGA itself refuses to start if the Playground is enabled alongside any authentication method other than none, so the unsafe combination cannot be configured by accident. Use the fga CLI, the SDKs or the REST API instead. The remaining steps use the REST API.

8

List your stores

A store is an isolated container for one application's authorization data: its model, its relationships and its history. Your VM created one on first boot so there is something to work with:

curl -sk -H "Authorization: Bearer <OPENFGA_API_TOKEN>" https://127.0.0.1/stores | jq '.stores[]? | {id, name}'
{ "id": "01M2YQSEKDA6CZJMW2P74XP6SE", "name": "sac-starter" }

Keep that store ID to hand, because most calls are scoped to a store:

STORE_ID=$(curl -sk -H "Authorization: Bearer <OPENFGA_API_TOKEN>" https://127.0.0.1/stores | jq -r '.stores[0].id')
echo "${STORE_ID}"
9

Write an authorization model

An authorization model declares the object types in your application and the relations they support. This model defines users and documents. A document has an owner and viewers, and every owner is implicitly also a viewer:

STORE_ID=$(curl -sk -H "Authorization: Bearer <OPENFGA_API_TOKEN>" https://127.0.0.1/stores | jq -r '.stores[0].id')
curl -sk -X POST -H "Authorization: Bearer <OPENFGA_API_TOKEN>" -H 'Content-Type: application/json' \
  -d '{ "schema_version": "1.1", "type_definitions": [ { "type": "user" }, { "type": "document", "relations": { "owner": { "this": {} }, "viewer": { "union": { "child": [ { "this": {} }, { "computedUserset": { "relation": "owner" } } ] } } }, "metadata": { "relations": { "owner": { "directly_related_user_types": [ { "type": "user" } ] }, "viewer": { "directly_related_user_types": [ { "type": "user" } ] } } } } ] }' "https://127.0.0.1/stores/${STORE_ID}/authorization-models" | jq -c .
{"authorization_model_id":"01M2YQSEKDA6CZJMW2P74XP6SF"}

The same model is easier to read in OpenFGA's own DSL, which the fga CLI and the VS Code extension accept:

model schema 1.1 type user type document relations define owner: [user] define viewer: [user] or owner
10

Write relationships and ask questions

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

STORE_ID=$(curl -sk -H "Authorization: Bearer <OPENFGA_API_TOKEN>" https://127.0.0.1/stores | jq -r '.stores[0].id')
MODEL_ID=$(curl -sk -H "Authorization: Bearer <OPENFGA_API_TOKEN>" "https://127.0.0.1/stores/${STORE_ID}/authorization-models" | jq -r '.authorization_models[0].id')
curl -sk -X POST -H "Authorization: Bearer <OPENFGA_API_TOKEN>" -H 'Content-Type: application/json' \
  -d "{\"writes\":{\"tuple_keys\":[{\"user\":\"user:alice\",\"relation\":\"owner\",\"object\":\"document:roadmap\"}]},\"authorization_model_id\":\"${MODEL_ID}\"}" \
  "https://127.0.0.1/stores/${STORE_ID}/write" | jq -c .
{}

Now ask the question OpenFGA exists to answer: may alice view the roadmap? Alice is the owner, and the model says owners are also viewers, so the check is allowed:

STORE_ID=$(curl -sk -H "Authorization: Bearer <OPENFGA_API_TOKEN>" https://127.0.0.1/stores | jq -r '.stores[0].id')
MODEL_ID=$(curl -sk -H "Authorization: Bearer <OPENFGA_API_TOKEN>" "https://127.0.0.1/stores/${STORE_ID}/authorization-models" | jq -r '.authorization_models[0].id')
curl -sk -X POST -H "Authorization: Bearer <OPENFGA_API_TOKEN>" -H 'Content-Type: application/json' \
  -d "{\"tuple_key\":{\"user\":\"user:alice\",\"relation\":\"viewer\",\"object\":\"document:roadmap\"},\"authorization_model_id\":\"${MODEL_ID}\"}" \
  "https://127.0.0.1/stores/${STORE_ID}/check" | jq -c .
{"allowed":true,"resolution":""}

Bob was never granted anything, so the same check for him is refused:

STORE_ID=$(curl -sk -H "Authorization: Bearer <OPENFGA_API_TOKEN>" https://127.0.0.1/stores | jq -r '.stores[0].id')
MODEL_ID=$(curl -sk -H "Authorization: Bearer <OPENFGA_API_TOKEN>" "https://127.0.0.1/stores/${STORE_ID}/authorization-models" | jq -r '.authorization_models[0].id')
curl -sk -X POST -H "Authorization: Bearer <OPENFGA_API_TOKEN>" -H 'Content-Type: application/json' \
  -d "{\"tuple_key\":{\"user\":\"user:bob\",\"relation\":\"viewer\",\"object\":\"document:roadmap\"},\"authorization_model_id\":\"${MODEL_ID}\"}" \
  "https://127.0.0.1/stores/${STORE_ID}/check" | jq -c .
{"allowed":false,"resolution":""}

You can also ask the reverse question: which documents may alice view? Use this to filter a list page:

STORE_ID=$(curl -sk -H "Authorization: Bearer <OPENFGA_API_TOKEN>" https://127.0.0.1/stores | jq -r '.stores[0].id')
MODEL_ID=$(curl -sk -H "Authorization: Bearer <OPENFGA_API_TOKEN>" "https://127.0.0.1/stores/${STORE_ID}/authorization-models" | jq -r '.authorization_models[0].id')
curl -sk -X POST -H "Authorization: Bearer <OPENFGA_API_TOKEN>" -H 'Content-Type: application/json' \
  -d "{\"type\":\"document\",\"relation\":\"viewer\",\"user\":\"user:alice\",\"authorization_model_id\":\"${MODEL_ID}\"}" \
  "https://127.0.0.1/stores/${STORE_ID}/list-objects" | jq -c .
{"objects":["document:roadmap"]}
11

Connect an application

Point an SDK at your VM and authenticate with the token from Step 6. In JavaScript:

import { OpenFgaClient } from '@openfga/sdk'; const fga = new OpenFgaClient({ apiUrl: 'https://your-openfga-host', // your VM's address storeId: process.env.FGA_STORE_ID, credentials: { method: 'api_token', config: { token: process.env.FGA_API_TOKEN }, }, }); const { allowed } = await fga.check({ user: 'user:alice', relation: 'viewer', object: 'document:roadmap', });

In Python:

from openfga_sdk import ClientConfiguration, OpenFgaClient from openfga_sdk.credentials import CredentialConfiguration, Credentials config = ClientConfiguration( api_url="https://your-openfga-host", store_id=os.environ["FGA_STORE_ID"], credentials=Credentials( method="api_token", configuration=CredentialConfiguration( api_token=os.environ["FGA_API_TOKEN"] ), ), )

Equivalent SDKs are published for Go, Java and .NET. Each one takes the same API token.

The gRPC API

OpenFGA also serves gRPC on 127.0.0.1:8081. This image deliberately binds it to the loopback address. The official SDKs and the fga CLI all use the HTTP API, so gRPC is not exposed to the network unless you choose to expose it.

If you need gRPC off-box, change grpc.addr in /etc/openfga/config.yaml to 0.0.0.0:8081, put TLS in front of it and restart the service. Then open the port in your network security group. Do not expose it unprotected, because it carries the same authority as the REST API.

12

Use a domain name and your own certificate

Point a DNS A record at your VM's public IP. Then replace the self-signed certificate generated on first boot with one for your domain. This example uses certbot:

sudo apt-get install -y certbot
sudo certbot certonly --standalone -d openfga.your-domain.com --agree-tos -m admin@your-domain.com -n sudo sed -i 's#/etc/ssl/openfga/openfga.crt#/etc/letsencrypt/live/openfga.your-domain.com/fullchain.pem#; s#/etc/ssl/openfga/openfga.key#/etc/letsencrypt/live/openfga.your-domain.com/privkey.pem#' /etc/nginx/sites-available/openfga
sudo nginx -t && sudo systemctl reload nginx

Nothing in OpenFGA's own configuration records the VM's address, so there is no application setting to update. Only nginx terminates TLS.

13

Security model

This image ships with the following security posture:

  • Pre-shared key authentication is enforced. OpenFGA's default of authn.method: none is never used. The key is 256 bits, generated on your VM's first boot, and stored only in /etc/openfga/openfga.env (0600 root:root) and /root/openfga-credentials.txt (0600 root:root). No key is baked into the image, so no two instances share one.
  • The Playground is disabled. It is an unauthenticated development console, and it is not listening on any interface.
  • Only ports 22, 80 and 443 are reachable off-box. The HTTP API, the gRPC API, the Prometheus metrics endpoint and PostgreSQL are all loopback-only. OpenFGA's upstream default would have published metrics on 0.0.0.0:2112.
  • nginx overwrites client-controlled address headers. X-Forwarded-For and X-Real-IP are set from the real peer address rather than appended to, so a caller cannot forge the address that appears in your audit trail. Forwarded and X-Forwarded-Prefix are cleared.
  • The service is unprivileged and confined. It runs as openfga with NoNewPrivileges, ProtectSystem=strict, an empty capability bounding set and a system-call filter.
  • The API does not serve until first boot has finished. openfga.service and nginx.service are gated on a marker that only the first-boot unit creates, so there is no window in which the server is up without a key.

To rotate the API token, replace it in the environment file and restart the service:

sudo bash -c 'NEW=$(openssl rand -hex 32); sed -i "s/^OPENFGA_AUTHN_PRESHARED_KEYS=.*/OPENFGA_AUTHN_PRESHARED_KEYS=${NEW}/" /etc/openfga/openfga.env; sed -i "s/^OPENFGA_API_TOKEN=.*/OPENFGA_API_TOKEN=${NEW}/" /root/openfga-credentials.txt; systemctl restart openfga'

OpenFGA accepts a comma-separated list of keys. You can add a second key, migrate your applications to it, and then remove the first key, which rotates the token with no downtime.

14

Back up your data

Everything lives in PostgreSQL: stores, authorization models and relationship tuples. A logical dump is the simplest backup:

sudo -u postgres pg_dump openfga | gzip > /var/backups/openfga-$(date +%F).sql.gz

Restore into an empty database with gunzip -c <backup-dir>/openfga-YYYY-MM-DD.sql.gz | sudo -u postgres psql openfga.

Back up /root/openfga-credentials.txt separately and securely. Without the API token, your applications cannot reach the server.

15

Logs, upgrades and maintenance

OpenFGA logs as structured JSON to the journal:

sudo journalctl -u openfga.service -n 5 --no-pager -o cat | cut -c1-140

Ubuntu security updates are applied automatically by unattended-upgrades. To apply them immediately:

sudo apt-get update && sudo apt-get install --only-upgrade -y

To upgrade OpenFGA itself, download the release you want and verify its checksum against the checksums.txt published with that release. Then replace the binary and restart. The migrate step is idempotent and safe to re-run:

sudo systemctl stop openfga
# download and verify the new openfga binary, then:
sudo install -o root -g root -m 0755 ./openfga /usr/local/bin/openfga
sudo -u openfga /usr/local/bin/openfga migrate
sudo systemctl start openfga

Check the OpenFGA release notes for schema changes before you upgrade a production instance.

Support

SecureAnyCloud provides 24/7 support for this image: deployment, upgrades, TLS certificates and custom domains, SDK and API integration, authorization model design, and scaling. Contact sales@secureanycloud.com.

Upstream documentation lives at openfga.dev/docs, and the source is at github.com/openfga/openfga.

Deploy on Azure

Launch OpenFGA 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