Op
Applications Azure

OpenFGA on Ubuntu 24.04 on Azure User Guide

| Product: OpenFGA on Ubuntu 24.04 LTS on Azure

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 — alice is a viewer of the roadmap document — describe the types and relations that exist in an authorization model, and then ask the server questions at request time: may this user do this thing to this object?

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

The cloudimg image delivers OpenFGA 1.20.0 on Ubuntu 24.04, served over HTTPS, backed by PostgreSQL, with a unique API token generated on the first boot of your VM. Backed by 24/7 cloudimg support.

What is included:

  • 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. 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.

The OpenFGA stack running, with its exact listening sockets

Prerequisites

An active Azure subscription, an SSH key pair, and a VNet plus 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. 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.

Step 1: Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for OpenFGA by cloudimg and 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) and HTTPS (443). Then select Review + create and Create.

Step 2: Deploy from the Azure CLI

az vm create \
  --resource-group my-resource-group \
  --name my-openfga \
  --image cloudimg:openfga-ubuntu-24-04:default:latest \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

Then open the ports you need:

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

Step 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 whichever name you chose at deployment) is the only way in.

Step 4: Confirm the OpenFGA stack is running

systemctl is-active postgresql openfga nginx
active
active
active

Check the version that is installed:

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 it is redirected and the timestamp trimmed.)

And confirm the API answers through nginx:

curl -sk https://127.0.0.1/healthz
ok

Step 5: Check what the network can reach

This is worth doing deliberately on an authorization server. 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 an executable assertion of that exact set, so you can re-check it at any time — after your own configuration changes, for example:

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

Step 6: Retrieve the first-boot API token

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

sudo cat /root/openfga-credentials.txt
# OpenFGA on Ubuntu 24.04 LTS by cloudimg
# 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

The per-VM credentials file, readable only by root

Copy the token somewhere safe — a secret manager rather than a file on disk — and treat it as you would a database password.

Step 7: Confirm the API refuses everything but your token

This image exists to make the following true. OpenFGA's upstream default is no authentication: run it with no flags and the entire management and query API is open to anyone who can reach the port. Here, a request with no credential is refused:

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

So is a request carrying 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

And 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, which 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

The security posture: unauthenticated and wrong-key requests refused

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, and 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; everything below does.

Step 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": "cloudimg-starter"
}

Keep that store id to hand — 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}"

Step 9: Write an authorization model

An authorization model declares the object types in your application and the relations they support. This one says there are 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 far more readable 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

Step 10: Write relationships and ask questions

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

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 the whole server exists to answer. Alice is the owner, and the model says owners are also viewers, so this 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 question about 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":""}

Writing a relationship and running a Check query

You can also ask the reverse question — which documents may alice view? — which is what you use 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"]}

Step 11: Connect an application

Point an SDK at your VM using 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. Every one of them takes the same API token.

The gRPC API

OpenFGA also serves gRPC, on 127.0.0.1:8081. It is deliberately bound to the loopback address in this image, because 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 it off-box, change grpc.addr in /etc/openfga/config.yaml to 0.0.0.0:8081, put TLS in front of it, restart the service, and open the port in your network security group. Do not expose it unprotected: it carries the same authority as the REST API.

Step 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. With 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.

Step 13: Security model

The posture this image ships, and the reasoning behind each part:

  • 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. 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. Note that 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:

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, so you can add a second key, migrate your applications to it, and then remove the first — a rotation with no downtime.

Step 14: Back up your data

Everything — stores, authorization models and relationship tuples — lives in PostgreSQL. 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.

Step 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, verify its checksum against the checksums.txt published with that release, 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

Always check the OpenFGA release notes for schema changes before upgrading a production instance.

Support

cloudimg provides 24/7 support for this image: deployment, upgrades, TLS certificates and custom domains, SDK and API integration, authorization model design, and scaling. Contact support@cloudimg.co.uk.

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