OpenFGA 1.22.1 on AWS User Guide
Overview
This guide covers launching and using the OpenFGA 1.22.1 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg identity, secrets and PKI listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
OpenFGA is an open source authorization server, a Cloud Native Computing Foundation project inspired by Google's Zanzibar paper. Instead of writing permission checks into every application, you describe your permission model once (for example "a viewer of a document is anyone it was shared with, or its owner"), write relationship tuples as things happen ("user:anne is owner of document:roadmap"), and ask OpenFGA at request time: Check (can anne view the roadmap?), ListObjects (which documents can anne view?) and ListUsers. It has an HTTP (REST) API and a gRPC API, and SDKs for Go, JavaScript, Python, Java and .NET.
A stock OpenFGA server answers every request without authentication, binds its HTTP, gRPC, metrics and playground ports on every interface and keeps its data in memory. The cloudimg image makes it safe to run on the internet:
- A key unique to your instance. Preshared-key authentication is on. First boot creates a 64-character API key and writes it to one file,
/root/openfga-credentials.txt, readable by root only. Every API request needsAuthorization: Bearer <key>; without it OpenFGA answers401. The key is never on a command line, in a service's environment or in a log. - Nothing answers until first boot is done. The image ships with no key, no database and no certificate. OpenFGA and nginx start only after first boot has created them, and nginx starts only once OpenFGA refuses a request without the key.
- HTTPS from the first boot. nginx serves the HTTP API on port 443 with a certificate generated on your instance's first boot (an end-entity certificate naming the instance's public address, public DNS name and private address). Port 80 only redirects to HTTPS. OpenFGA itself listens on
127.0.0.1only. - A PostgreSQL datastore of your own. First boot creates a PostgreSQL 16 database for OpenFGA with a password unique to your instance (SCRAM, loopback only), so your stores, models and tuples survive restarts. A starter store with a simple user/document model is created for you.
- Internal ports closed. gRPC (8081), Prometheus metrics (2112) and PostgreSQL (5432) listen on loopback only. The playground and the profiler are switched off.
- Metadata service blocked. OpenFGA, nginx, PostgreSQL and the first boot services are blocked from the instance metadata service, and the AMI requires IMDSv2. OpenFGA does not fetch user-supplied URLs.
- Pinned and patched. OpenFGA 1.22.1 is the official release binary, pinned by its SHA-256. Ubuntu 24.04 LTS is fully patched at build time with unattended security upgrades switched on.
What is on the image: OpenFGA 1.22.1 (/usr/local/bin/openfga), PostgreSQL 16, nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.
Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large (t3.medium is enough for development and small workloads)
Prerequisites
- An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet.
- A security group that allows TCP 22 (SSH) from your administrators and TCP 443 (the API) from the hosts whose applications ask OpenFGA for decisions. Port 80 is optional (it only redirects to HTTPS). Never open 8080, 8081, 2112 or 5432: they listen on loopback only and refuse connections from the network even if a security group allows them.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick OpenFGA 1.22.1 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose your instance type, key pair, subnet and a security group as above.
With the AWS CLI (replace the values in angle brackets):
aws ec2 create-security-group --region <region> --vpc-id <your-vpc> \
--group-name openfga --description "OpenFGA: SSH from admins, HTTPS from applications"
aws ec2 authorize-security-group-ingress --region <region> --group-id <sg-id> --ip-permissions \
'IpProtocol=tcp,FromPort=22,ToPort=22,IpRanges=[{CidrIp=<your-admin-cidr>}]' \
'IpProtocol=tcp,FromPort=443,ToPort=443,IpRanges=[{CidrIp=<your-client-cidr>}]'
aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
--instance-type m5.large --key-name <your-key-pair> \
--security-group-ids <sg-id> --subnet-id <your-subnet> \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=openfga}]'
First boot takes well under a minute. It names the server after the instance's public IPv4 address (or its private address when it has no public one), and the certificate also covers the public DNS name and the private address. To use your own DNS name, see Step 10.
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| OpenFGA 1.22.1 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
OpenFGA, nginx and PostgreSQL run as systemd services. First boot leaves a marker when it has finished:
systemctl is-active openfga nginx postgresql@16-main
sudo test -f /var/lib/cloudimg/openfga-firstboot.done && echo FIRSTBOOT_DONE
active
active
active
FIRSTBOOT_DONE
The journal shows the order on first boot: the bootstrap-ready marker is written last, after the key, the database and the certificate exist; OpenFGA starts after it, nginx after OpenFGA (nginx refuses to start until OpenFGA rejects a request without the key and one with a wrong key), and the starter store is created at the end:
sudo journalctl -b -o short-iso -u openfga-firstboot -u openfga -u nginx -u openfga-seed \
| grep -E 'OPENFGA_BOOTSTRAP_READY|OPENFGA_SEED_DONE|Started (openfga|nginx)\.service' | cut -d' ' -f3-
openfga-firstboot.sh[849]: [2026-11-08T01:54:10+0000] openfga-firstboot: OPENFGA_BOOTSTRAP_READY 2026-11-08T01:54:10.400074745Z
systemd[1]: Started openfga.service - OpenFGA 1.22.1 authorization server (loopback only; nginx is the public door).
systemd[1]: Started nginx.service - A high performance web server and a reverse proxy server.
openfga-seed[1209]: [2026-11-08T01:54:14+0000] openfga-seed: OPENFGA_SEED_DONE 01M6WDT137H39449MSTCDNXEVN
Only SSH, HTTP (redirect) and HTTPS listen on the network. OpenFGA's HTTP API (8080), gRPC (8081), metrics (2112) and PostgreSQL (5432) listen on 127.0.0.1 only, and openfga-port-check confirms it:
sudo ss -tlnH | awk '{print $4}' | sort -u
sudo openfga-port-check --running
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
OPENFGA_PORT_CHECK_OK
openfga-selftest runs a complete round trip with your key (create a store, write a model and a tuple, Check allowed and denied, ListObjects, delete the store) and confirms that requests without a key or with a wrong key are refused:
sudo openfga-selftest
openfga-selftest: no key 401, wrong key 401; store + model + tuple; Check allowed/denied; ListObjects; store deleted
OPENFGA_SELFTEST_OK
Step 4: Read your instance's details
Everything you need to connect is in /root/openfga-credentials.txt (mode 0600, root only). This shows it without the key line:
sudo grep -v '^OPENFGA_API_TOKEN=' /root/openfga-credentials.txt
# OpenFGA 1.22.1 on this instance. Keep this file secret; copy the key into your secrets manager.
# Every API request needs the header: Authorization: Bearer <OPENFGA_API_TOKEN>
OPENFGA_API_URL=https://203.0.113.25
OPENFGA_HOST=203.0.113.25
OPENFGA_TLS_SHA256=1E:02:4F:A5:68:52:F3:A0:04:47:44:D1:BD:22:F6:8A:C6:A2:74:EA:39:49:24:22:FC:EA:E9:67:F0:DF:83:6B
OPENFGA_ADDRESS_SOURCE=imdsv2-public-ipv4
OPENFGA_STORE_ID=01M6WDT137H39449MSTCDNXEVN
GUIDE=https://www.cloudimg.co.uk/guides/openfga-1-22-1-on-ubuntu-24-04-aws/
| Field | Meaning |
|---|---|
OPENFGA_API_URL |
The HTTPS address of the API. |
OPENFGA_HOST |
The name the server answers to and the certificate is issued for. Requests for any other name are dropped. |
OPENFGA_API_TOKEN |
Your preshared API key (64 hex characters). Send it as Authorization: Bearer <key>. |
OPENFGA_TLS_SHA256 |
The SHA-256 fingerprint of the certificate, so clients can pin it. |
OPENFGA_STORE_ID |
The starter store first boot created (cloudimg-starter, with a simple user/document model). |
The key is unique to your instance and lives only in this file and in OpenFGA's configuration, /etc/openfga/config.yaml (root and the openfga service only). Store it in your secrets manager and never put it on a command line: the examples below read it from a header file instead.
Step 5: Call the API with your key
The certificate is self-signed and generated on your instance, so clients trust it by its file (or by its fingerprint). On the instance, copy the certificate and write the key into a header file that only you can read, then list the stores:
sudo cat /etc/ssl/openfga/openfga.crt > ~/openfga.crt
umask 077
sudo sed -n 's/^OPENFGA_API_TOKEN=/Authorization: Bearer /p' /root/openfga-credentials.txt > ~/.openfga.h
H=$(cat /etc/cloudimg/openfga-domain)
curl -s --cacert ~/openfga.crt --connect-to ::127.0.0.1:443 -H @"$HOME/.openfga.h" "https://$H/stores" \
| jq -c '.stores[] | {name, id}'
{"name":"cloudimg-starter","id":"01M6WDT137H39449MSTCDNXEVN"}
--connect-to ::127.0.0.1:443 sends the request to nginx on this instance while still using the server's name, so the commands work from the instance whatever its public address. From your workstation, copy the two files over SSH and call the public address directly (the security group must allow 443 from your address):
scp -i <your-key.pem> ubuntu@<public-ip>:openfga.crt ubuntu@<public-ip>:.openfga.h .
chmod 600 .openfga.h
curl -s --cacert openfga.crt -H @.openfga.h https://<public-ip>/stores
The OpenFGA SDKs and the fga command-line tool take the same two settings: the API URL (OPENFGA_API_URL) and the API token (OPENFGA_API_TOKEN), with the certificate added to the client's trust store. For example, with the OpenFGA CLI on your workstation (read the token from your secrets manager into the environment, not onto the command line):
export FGA_API_URL=https://<public-ip>
export FGA_API_TOKEN="$(cat my-openfga-token)"
export SSL_CERT_FILE=$PWD/openfga.crt
fga store list
Step 6: Model permissions and write relationships
Create a store for your application, give it an authorization model and write relationship tuples. This model has users, folders and documents: a document's viewers are the users it was shared with, its owner, and the viewers of its parent folder:
H=$(cat /etc/cloudimg/openfga-domain)
api() { curl -s --cacert ~/openfga.crt --connect-to ::127.0.0.1:443 -H @"$HOME/.openfga.h" -H 'Content-Type: application/json' "$@"; }
api -X POST -d '{"name":"docs-demo"}' "https://$H/stores" | jq -r .id > ~/.openfga-store
S=$(cat ~/.openfga-store)
cat > ~/model.json <<'MODEL'
{
"schema_version": "1.1",
"type_definitions": [
{ "type": "user" },
{ "type": "folder",
"relations": { "viewer": { "this": {} } },
"metadata": { "relations": { "viewer": { "directly_related_user_types": [ { "type": "user" } ] } } } },
{ "type": "document",
"relations": {
"parent": { "this": {} },
"owner": { "this": {} },
"viewer": { "union": { "child": [
{ "this": {} },
{ "computedUserset": { "relation": "owner" } },
{ "tupleToUserset": { "tupleset": { "relation": "parent" }, "computedUserset": { "relation": "viewer" } } } ] } } },
"metadata": { "relations": {
"parent": { "directly_related_user_types": [ { "type": "folder" } ] },
"owner": { "directly_related_user_types": [ { "type": "user" } ] },
"viewer": { "directly_related_user_types": [ { "type": "user" } ] } } } }
]
}
MODEL
api -X POST -d @"$HOME/model.json" "https://$H/stores/$S/authorization-models" | jq -r '"model: " + (.authorization_model_id | length | tostring) + "-character id"'
api -X POST -d '{"writes":{"tuple_keys":[
{"user":"user:anne","relation":"owner","object":"document:roadmap"},
{"user":"user:beth","relation":"viewer","object":"folder:engineering"},
{"user":"folder:engineering","relation":"parent","object":"document:spec"},
{"user":"user:carl","relation":"viewer","object":"document:spec"}]}}' "https://$H/stores/$S/write"; echo
api -X POST -d '{}' "https://$H/stores/$S/read" | jq -r '.tuples[].key | "\(.user) \(.relation) \(.object)"' | sort
model: 26-character id
{}
folder:engineering parent document:spec
user:anne owner document:roadmap
user:beth viewer folder:engineering
user:carl viewer document:spec
The write returns {} on success. The model is the JSON form of the OpenFGA modelling language; the OpenFGA documentation explains how to model your own application, and the fga model transform command converts the DSL to this JSON.
Step 7: Ask OpenFGA: Check and ListObjects
Now ask the questions your application asks at request time. Anne owns the roadmap, so she can view it; Beth views the engineering folder, so she can view the spec inside it but not the roadmap; Carl was given the spec only:
H=$(cat /etc/cloudimg/openfga-domain); S=$(cat ~/.openfga-store)
api() { curl -s --cacert ~/openfga.crt --connect-to ::127.0.0.1:443 -H @"$HOME/.openfga.h" -H 'Content-Type: application/json' "$@"; }
check() { api -X POST -d "{\"tuple_key\":{\"user\":\"$1\",\"relation\":\"$2\",\"object\":\"$3\"}}" "https://$H/stores/$S/check" \
| jq -r --arg q "$1 $2 $3" '"\($q): \(.allowed)"'; }
check user:anne viewer document:roadmap
check user:beth viewer document:spec
check user:beth viewer document:roadmap
check user:carl viewer document:roadmap
api -X POST -d '{"type":"document","relation":"viewer","user":"user:beth"}' "https://$H/stores/$S/list-objects" | jq -c '.objects | sort'
api -X POST -d '{"object":{"type":"document","id":"spec"},"relation":"viewer","user_filters":[{"type":"user"}]}' \
"https://$H/stores/$S/list-users" | jq -c '[.users[].object.id] | sort'
user:anne viewer document:roadmap: true
user:beth viewer document:spec: true
user:beth viewer document:roadmap: false
user:carl viewer document:roadmap: false
["document:spec"]
["beth","carl"]
ListObjects answers "which documents can Beth view?" and ListUsers answers "who can view the spec?".
Step 8: Requests without the key are refused
Every API call needs your key. A request without it, or with any other key, gets 401. The health check answers without a key so load balancers can probe it, and the unauthenticated playground is switched off:
H=$(cat /etc/cloudimg/openfga-domain)
umask 077; printf 'Authorization: Bearer %s\n' "$(openssl rand -hex 32)" > ~/.wrong.h
c() { curl -s -o /dev/null -w '%{http_code}' --cacert ~/openfga.crt --connect-to ::127.0.0.1:443 "$@"; }
echo "no key: $(c "https://$H/stores")"
echo "wrong key: $(c -H @"$HOME/.wrong.h" "https://$H/stores")"
echo "your key: $(c -H @"$HOME/.openfga.h" "https://$H/stores")"
echo "health check: $(c "https://$H/healthz")"
echo "playground: $(c -H @"$HOME/.openfga.h" "https://$H/playground")"
echo "port 3000: $(curl -s -m 3 -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/)"
rm -f ~/.wrong.h
no key: 401
wrong key: 401
your key: 200
health check: 200
playground: 404
port 3000: 000
The same applies to OpenFGA's own HTTP port and to gRPC on loopback: without the key, OpenFGA answers 401 on 8080 and refuses gRPC calls on 8081 (grpc-status 1010, "bearer token missing"; a wrong key gets 1500, "unauthenticated"). From the network, 8080, 8081, 2112 and 5432 refuse connections even when a security group allows them. Port 80 only redirects to HTTPS, and nginx drops requests for names other than OPENFGA_HOST and its other configured names.
Step 9: Reboot: your data stays
Your stores, models and tuples are in PostgreSQL on the instance's disk, and the key and certificate do not change on a reboot (first boot runs once):
sudo reboot
After the instance is back, the services are up and the tuples from Step 6 are still there:
H=$(cat /etc/cloudimg/openfga-domain); S=$(cat ~/.openfga-store)
api() { curl -s --cacert ~/openfga.crt --connect-to ::127.0.0.1:443 -H @"$HOME/.openfga.h" -H 'Content-Type: application/json' "$@"; }
systemctl is-active openfga nginx postgresql@16-main
api -X POST -d '{}' "https://$H/stores/$S/read" | jq '.tuples | length'
api -X POST -d '{"tuple_key":{"user":"user:beth","relation":"viewer","object":"document:spec"}}' "https://$H/stores/$S/check" | jq -c .
active
active
active
4
{"allowed":true,"resolution":""}
Step 10: Use your own name and certificate
The server answers to the name in OPENFGA_HOST. A stop and start (not a reboot) gives the instance a new public IPv4 address, so either attach an Elastic IP or give the server a DNS name. Point a DNS record at the instance first, then run openfga-set-domain with the name (and any other names or IPv4 addresses it should also answer to). It renders nginx for those names, issues a new certificate for them on the instance and updates the credentials file:
sudo openfga-set-domain openfga.example.com
sudo grep -E '^OPENFGA_(API_URL|HOST|TLS_SHA256)=' /root/openfga-credentials.txt
sudo cat /etc/ssl/openfga/openfga.crt > ~/openfga.crt
If you keep the address-based name without an Elastic IP, run sudo openfga-set-domain <new-public-ip> after each stop and start.
To use a certificate from your own CA instead, replace /etc/ssl/openfga/openfga.crt (the full chain) and /etc/ssl/openfga/openfga.key (mode 0600, root), then check and reload nginx with sudo nginx -t && sudo systemctl reload nginx. Run openfga-set-domain first so nginx answers to the name on the certificate.
Step 11: Operate and harden
Rotate the key. OpenFGA accepts every key in the keys list under authn.preshared in /etc/openfga/config.yaml. To rotate without downtime, add a new 64-character key (for example from openssl rand -hex 32) to the list next to the old one with sudoedit /etc/openfga/config.yaml, run sudo systemctl restart openfga, move your applications to the new key, then remove the old key, restart OpenFGA again and put the new key in /root/openfga-credentials.txt.
Metadata access. OpenFGA, nginx, PostgreSQL and the first boot services cannot reach the instance metadata service (IPv4, IPv6 and the IPv4-mapped and translated forms are all denied), and the instance requires IMDSv2 tokens. You can check the deny lists:
for u in openfga nginx postgresql@16-main openfga-firstboot openfga-seed; do
echo "$u: $(systemctl show -p IPAddressDeny --value "$u" | tr ' ' '\n' | sort | tr '\n' ' ')"
done
openfga: 168.63.129.16/32 169.254.169.254/32 2001::/32 2002::/16 64:ff9b:1::/48 64:ff9b::/96 ::ffff:0.0.0.0/96 fd00:ec2::254/128
nginx: 168.63.129.16/32 169.254.169.254/32 2001::/32 2002::/16 64:ff9b:1::/48 64:ff9b::/96 ::ffff:0.0.0.0/96 fd00:ec2::254/128
postgresql@16-main: 168.63.129.16/32 169.254.169.254/32 2001::/32 2002::/16 64:ff9b:1::/48 64:ff9b::/96 ::ffff:0.0.0.0/96 fd00:ec2::254/128
openfga-firstboot: 168.63.129.16/32 169.254.169.254/32 2001::/32 2002::/16 64:ff9b:1::/48 64:ff9b::/96 ::ffff:0.0.0.0/96 fd00:ec2::254/128
openfga-seed: 168.63.129.16/32 169.254.169.254/32 2001::/32 2002::/16 64:ff9b:1::/48 64:ff9b::/96 ::ffff:0.0.0.0/96 fd00:ec2::254/128
OpenFGA itself makes no outbound calls to user-supplied URLs.
Metrics. Prometheus metrics are on 127.0.0.1:2112 only. Scrape them from the instance (for example with the CloudWatch agent or a Prometheus agent), or reach them through an SSH tunnel such as ssh -L 2112:127.0.0.1:2112:
curl -s http://127.0.0.1:2112/metrics | grep -oE '^openfga_[a-z_]+' | sort -u | head -n 6
openfga_cache_item_count
openfga_cachecontroller_cache_hit_count
openfga_cachecontroller_cache_invalidation_count
openfga_cachecontroller_cache_total_count
openfga_check_cache_hit_count
openfga_check_cache_invalid_hit_count
gRPC is on 127.0.0.1:8081 for clients on the instance. To use gRPC from elsewhere, tunnel it over SSH (ssh -L 8081:127.0.0.1:8081) and send the same authorization: Bearer <key> metadata.
Backups. Back up the OpenFGA database with pg_dump and copy the file off the instance:
sudo -u postgres pg_dump -Fc openfga > ~/openfga-$(date +%F).dump
ls -l ~/openfga-*.dump
The database holds your stores, models and tuples. It does not hold the API key, so keep the key in your secrets manager too.
Logs. sudo journalctl -u openfga -u nginx -u openfga-firstboot. nginx logs requests without query strings and logs errors only at crit, and OpenFGA never logs the key.
Security group. Allow TCP 443 only from the hosts whose applications ask OpenFGA for decisions, and TCP 22 only from your administrators.
Updates. Unattended security upgrades stay on for Ubuntu, nginx and PostgreSQL. OpenFGA is pinned to the release binary this image was built with; new OpenFGA releases arrive as new versions of this listing. To move to a new version, back up the database, launch the new version and restore the dump into its database with sudo -u postgres pg_restore --clean --if-exists -d openfga <file>.
Support and licensing
OpenFGA is a Cloud Native Computing Foundation project distributed under the Apache License 2.0. The licence is in /usr/share/doc/cloudimg-openfga/:
openfga version 2>&1 | grep -o 'OpenFGA version.*'
grep -m 2 -E 'Apache License|Version 2.0' /usr/share/doc/cloudimg-openfga/LICENSE-openfga-Apache-2.0.txt | sed 's/^ *//'
OpenFGA version `v1.22.1` build from `44054f542f64e9b9589dd26a595081b3d2cfd942` on `2026-10-28T12:53:36Z`
Apache License
Version 2.0, January 2004
cloudimg provides the packaging, the first boot automation, the HTTPS front end and the hardening, with 24/7 support by email and live chat at support@cloudimg.co.uk.
All product names are trademarks of their respective owners. cloudimg is not affiliated with or endorsed by the OpenFGA project or the Cloud Native Computing Foundation.