Unleash 8.3.1 on AWS User Guide
Overview
This guide covers launching and using the Unleash 8.3.1 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg DevOps platforms listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
Unleash is an open-source feature-flag server. You create flags in its web UI, turn them on per environment, roll them out gradually with strategies, and your applications ask Unleash (through its SDKs or its HTTP API) which flags are on. This image runs the open-source Unleash server 8.3.1 on Node.js 22 LTS, with its PostgreSQL 16 database on the same instance.
The cloudimg image runs Unleash as a private service:
- No default account. Unleash's stock
admin/unleash4alllogin is never created. First boot creates a singleadminuser with a password generated for your instance, stored only in a root-only file (Step 4). - A per-instance session secret. Unleash signs its login sessions with a secret; the upstream default is the well-known
super-secret. First boot generates a random one for your instance, and Unleash refuses to start if that secret is missing, unreadable, malformed or the default (Step 10). - HTTPS from the first boot. nginx serves Unleash on port 443 with a certificate generated on your instance's first boot. Port 80 only redirects to HTTPS, and a request for any name other than your instance's own gets no answer.
- Cross-site requests are refused. A web page on another site cannot use your logged-in browser session to change anything: nginx refuses state-changing requests that carry another site's
Origin(Step 8). - Rate limits that cannot be dodged. Every request is limited per client address, logins more tightly, and nginx always replaces the
X-Forwarded-Forheader with the address it accepted the connection from. - No call-home. Unleash's version check and telemetry are off, and the image makes no outbound connection on its own.
- Integrations reach public addresses only. Webhooks and other integrations can reach the internet, never the EC2 instance metadata service, the instance itself or your private network (an nftables rule for the
unleashuser backs up Unleash's own URL check). Step 11 shows how to allow a private address on purpose. - PostgreSQL on the instance, no database password. PostgreSQL 16 listens on its local socket only. Unleash connects as the role
unleash, which PostgreSQL identifies by its Linux user (peer authentication): no database password exists. The database is created on your instance's first boot; the image ships an empty one. - Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: Unleash server 8.3.1 (installed from npm with a pinned lockfile, every package checked against its published integrity hash, install scripts disabled), Node.js 22.23.4 LTS (the official nodejs.org build, checked against nodejs.org's signed SHA-256 list), PostgreSQL 16, nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent. npm itself is not on the image. The build record and the licences of Unleash (AGPL-3.0) and Node.js (MIT) are in /usr/share/doc/cloudimg-unleash/.
Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large
Prerequisites
- An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet.
- A security group that allows TCP 443 (the Unleash UI and API), TCP 80 (redirects to HTTPS) and TCP 22 (SSH). Open 443 to the networks your users and applications connect from; keep 22 open to your own networks only.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Unleash 8.3.1 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (a t3.medium is enough for a small team; Unleash itself uses about 150 MiB of memory), your key pair, your subnet and the security group above.
With the AWS CLI (replace the values in angle brackets):
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 <your-security-group> --subnet-id <your-subnet> \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=unleash}]'
An Elastic IP keeps the server's address, and so its name and certificate, the same across stops and starts.
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| Unleash 8.3.1 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
First boot takes under a minute: it initialises the PostgreSQL database, runs Unleash's database migrations, sets the server's name, generates the certificate, the session secret and the admin password, and only then starts Unleash and nginx. Check that everything is running:
systemctl is-active unleash-firstboot unleash-egress-guard postgresql@16-main unleash nginx \
&& sudo test -f /var/lib/cloudimg/unleash-firstboot.done && echo FIRSTBOOT_DONE
active
active
active
active
active
FIRSTBOOT_DONE
Only SSH and nginx listen on the network. Unleash (the node process) listens on the loopback address 127.0.0.1:4242 behind nginx, and PostgreSQL has no network port at all:
sudo ss -lntp | awk 'NR>1 && $4 !~ /^127\.0\.0\.5[34]/ {print $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort -u | tee /dev/stderr \
| awk '$1 !~ /^127\./ {sub(/.*:/, "", $1); print $1}' | sort -un | xargs | grep -x '22 80 443' >/dev/null && echo "network listeners: 22 80 443"
0.0.0.0:22 users:(("sshd"),("systemd"))
0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
127.0.0.1:4242 users:(("node"))
[::]:22 users:(("sshd"),("systemd"))
[::]:443 users:(("nginx"),("nginx"),("nginx"))
[::]:80 users:(("nginx"),("nginx"),("nginx"))
network listeners: 22 80 443
Step 4: Get your address and password
Run sudo cat /var/lib/cloudimg/unleash-credentials.txt to see your address and the admin password. The file is readable by root only and holds these settings, all generated for this instance:
sudo sed -n 's/^\(UNLEASH_[A-Z0-9_]*\)=.*/\1/p' /var/lib/cloudimg/unleash-credentials.txt | tee /dev/stderr \
| grep -x UNLEASH_ADMIN_PASSWORD >/dev/null && echo "credentials present"
UNLEASH_URL
UNLEASH_HOST
UNLEASH_ADMIN_USERNAME
UNLEASH_ADMIN_PASSWORD
UNLEASH_TLS_CERT
UNLEASH_TLS_SHA256_FINGERPRINT
UNLEASH_DATABASE
credentials present
UNLEASH_URL is https://<your instance's public DNS name>/, where the Unleash UI opens. The settings that are not secret can be shown on their own:
sudo grep -E '^UNLEASH_(URL|ADMIN_USERNAME|TLS_SHA256_FINGERPRINT)=' /var/lib/cloudimg/unleash-credentials.txt \
| tee /dev/stderr | grep -x 'UNLEASH_ADMIN_USERNAME=admin' >/dev/null
UNLEASH_URL=https://ec2-203-0-113-10.compute-1.amazonaws.com/
UNLEASH_ADMIN_USERNAME=admin
UNLEASH_TLS_SHA256_FINGERPRINT=3A:5F:...:C2
The certificate is self-signed and generated on your instance, so your browser warns about it until you trust it (Step 5). For a certificate everyone trusts already, use your own domain (Step 12).
Step 5: Sign in to the Unleash UI
Copy the certificate to your computer and check that it is the one first boot generated: the SHA-256 of its DER form must match UNLEASH_TLS_SHA256_FINGERPRINT (ignoring the colons).
scp -i <your-key.pem> ubuntu@<public-ip>:/etc/ssl/unleash/unleash.crt unleash.crt
openssl x509 -in unleash.crt -outform der | shasum -a 256
Add unleash.crt to your operating system's or browser's trusted certificates (on macOS, open it in Keychain Access and set it to Always Trust), then browse to UNLEASH_URL. Type admin in the Email field (Unleash accepts a user name there) and the UNLEASH_ADMIN_PASSWORD from Step 4 in Password:

The page's Sign up link goes to Unleash's own website (there is no self-registration on your server), and Forgot your password? sends nothing until you configure email in Unleash. After signing in, change the password (your avatar, top right > View profile > Change password). The project overview lists the default project; New feature flag creates a flag, and each flag can be turned on and off per environment (development and production):

Applications connect with an API token: Admin settings > API access > New API token creates a backend token (for server-side SDKs) or a frontend token, scoped to a project and an environment.

Step 6: Create a flag and read it as an application
Everything in the UI is also in Unleash's admin API. On the instance, this signs in as admin (the password is read from the credentials file into a 0600 file, never onto a command line), creates the flag new-checkout, turns it on in development, creates a backend API token, and then asks for the flags the way an SDK does, with that token:
C=/var/lib/cloudimg/unleash-credentials.txt
H="$(sudo sed -n 's/^UNLEASH_HOST=//p' "$C")"
W="$(mktemp -d)"; chmod 700 "$W"
A=(-s --cacert /etc/ssl/unleash/unleash.crt -H 'Content-Type: application/json' -H 'Accept: application/json')
sudo sed -n 's/^UNLEASH_ADMIN_PASSWORD=\(.*\)/{"username":"admin","password":"\1"}/p' "$C" > "$W/login.json"
curl "${A[@]}" -D "$W/hdr" -o /dev/null --data-binary @"$W/login.json" "https://$H/auth/simple/login"
awk 'tolower($1) == "set-cookie:" && index($2, "unleash-session=") == 1 { sub(/;.*/, "", $2); print "Cookie: " $2 }' "$W/hdr" | tr -d '\r' > "$W/cookie"
api() { curl "${A[@]}" -H @"$W/cookie" "$@"; }
api -o /dev/null -w 'create flag: %{http_code}\n' --data-binary '{"name":"new-checkout"}' "https://$H/api/admin/projects/default/features"
api -o /dev/null -w 'enable in development: %{http_code}\n' -X POST "https://$H/api/admin/projects/default/features/new-checkout/environments/development/on"
api --data-binary '{"tokenName":"guide-backend","type":"backend","environment":"development","projects":["*"]}' \
"https://$H/api/admin/api-tokens" | python3 -c 'import json, sys; print("Authorization: " + json.load(sys.stdin)["secret"])' > "$W/token"
curl -s --cacert /etc/ssl/unleash/unleash.crt -H @"$W/token" "https://$H/api/client/features" \
| python3 -c 'import json, sys; f = {x["name"]: x["enabled"] for x in json.load(sys.stdin)["features"]}; print("as the SDK sees it: new-checkout enabled =", f["new-checkout"])' \
| tee "$W/result"
api -o /dev/null -X POST "https://$H/logout"
grep -x 'as the SDK sees it: new-checkout enabled = True' "$W/result" >/dev/null && echo "flag round trip OK" && rm -rf "$W"
create flag: 201
enable in development: 200
as the SDK sees it: new-checkout enabled = True
flag round trip OK
From your own computer an SDK, or curl, uses the same address and the token you created (keep tokens out of shell history, for example in a file read with -H @file):
curl --cacert unleash.crt -H @token-header.txt https://<public-dns-name>/api/client/features
Step 7: What is refused without a login
The admin API, the client API without a valid token, and a session cookie that this instance did not sign are all refused with 401. Only the health check and the sign-in page answer anonymously:
H="$(sudo sed -n 's/^UNLEASH_HOST=//p' /var/lib/cloudimg/unleash-credentials.txt)"
c() { curl -s -o /dev/null -w '%{http_code}\n' --cacert /etc/ssl/unleash/unleash.crt "$@"; }
{ c "https://$H/api/admin/user"
c -H 'Authorization: default:development.not-a-token' "https://$H/api/client/features"
c -H 'Cookie: unleash-session=s%3Aforged.forged' "https://$H/api/admin/user"
c "https://$H/health"
} | xargs | tee /dev/stderr | grep -x '401 401 401 200' >/dev/null && echo "anonymous: admin API, bad token and forged session refused; health 200"
401 401 401 200
anonymous: admin API, bad token and forged session refused; health 200
The stock default login admin / unleash4all does not exist on this image, and a wrong password is refused:
H="$(sudo sed -n 's/^UNLEASH_HOST=//p' /var/lib/cloudimg/unleash-credentials.txt)"
for pw in unleash4all not-the-password; do
curl -s -o /dev/null -w '%{http_code}\n' --cacert /etc/ssl/unleash/unleash.crt -H 'Content-Type: application/json' \
--data-binary "{\"username\":\"admin\",\"password\":\"$pw\"}" "https://$H/auth/simple/login"
done | xargs | tee /dev/stderr | grep -x '401 401' >/dev/null && echo "default and wrong passwords refused"
401 401
default and wrong passwords refused
Unleash's metrics endpoint (/internal-backstage), its readiness probe and the OpenAPI documentation are not served, and the Edge endpoints need an Authorization header.
Step 8: The front door
nginx answers only for the server's own names, redirects port 80, refuses another site's Origin on a sign-in or any other state-changing request, and refuses WebSocket upgrades (Unleash uses none):
H="$(sudo sed -n 's/^UNLEASH_HOST=//p' /var/lib/cloudimg/unleash-credentials.txt)"
{ curl -s -o /dev/null -w '%{http_code}\n' -k -H 'Host: someone-else.example' https://127.0.0.1/
curl -s -o /dev/null -w '%{http_code}\n' "http://$H/"
curl -s -o /dev/null -w '%{http_code}\n' --cacert /etc/ssl/unleash/unleash.crt -H 'Origin: https://attacker.example' \
-H 'Content-Type: application/json' --data-binary '{"username":"admin","password":"x"}' "https://$H/auth/simple/login"
curl -s -o /dev/null -w '%{http_code}\n' --http1.1 --cacert /etc/ssl/unleash/unleash.crt -H 'Upgrade: websocket' -H 'Connection: Upgrade' "https://$H/api/admin/user"
} | xargs | tee /dev/stderr | grep -x '000 301 403 400' >/dev/null && echo "front door OK"
000 301 403 400
front door OK
- Rate limits. Every request is limited to 20 a second per client address (bursts of 200); sign-ins to 12 a minute per address (bursts of 10), on top of Unleash's own limit of 10 sign-in attempts a minute. The UI's static files have their own bucket (100 a second, bursts of 500), so loading the UI never uses up the API's allowance, and an HTTP/2 connection carries at most 32 requests at a time. Over a limit nginx answers
429. - X-Forwarded-For. nginx replaces any
X-Forwarded-Forheader with the address it accepted the connection from, so a client cannot change the address the rate limits and the logs use. If you put a load balancer in front of the instance, every client then shares the load balancer's address for the rate limit. - Headers. Responses carry
Strict-Transport-Security,X-Content-Type-Options: nosniff,X-Frame-Options: DENY, aContent-Security-PolicyandReferrer-Policy: no-referrer, and nginx does not advertise its version.
What is logged: nginx's access log (/var/log/nginx/access.log) records the client address, request path and status, without query strings, and its error log records only start-up problems. Unleash logs to the journal (journalctl -u unleash).
Step 9: The database
PostgreSQL 16 runs on the same instance and listens on its local socket only. Unleash connects as the role unleash to the database unleash, and PostgreSQL lets only the Linux user unleash sign in as that role, so no database password exists:
sudo -u postgres psql -X -tA -c "SELECT 'roles with a password: ' || count(*) FROM pg_authid WHERE rolpassword IS NOT NULL" \
-c "SELECT rolname, rolsuper, rolcreatedb, rolcreaterole FROM pg_roles WHERE rolname = 'unleash'" \
| tee /dev/stderr | grep -x 'roles with a password: 0' >/dev/null && echo "no database password"
roles with a password: 0
unleash|f|f|f
no database password
The data directory is /var/lib/postgresql/16/main on the instance's root volume. For a larger or separately snapshotted store, attach an EBS volume and move the data directory to it while PostgreSQL is stopped.
Step 10: The session secret and the start guard
The secret that signs Unleash's login sessions is in /etc/unleash/unleash-secret.env (root only, generated on first boot). Before every start, a guard refuses to start Unleash if that file is missing, unreadable by anyone but root, malformed, empty or set to Unleash's public default super-secret; Unleash then stays down with an explanation in its journal rather than running with a guessable secret:
sudo /usr/local/sbin/unleash-start-guard && echo "start guard: secret OK" \
&& sudo stat -c '%U:%G %a' /etc/unleash/unleash-secret.env | tee /dev/stderr | grep -x 'root:root 600' >/dev/null
start guard: secret OK
root:root 600
To replace the secret (every signed-in user is signed out):
printf 'UNLEASH_SECRET=%s\n' "$(openssl rand -hex 32)" | sudo tee /etc/unleash/unleash-secret.env >/dev/null
sudo systemctl restart unleash
Other settings (for example LOG_LEVEL) are in /etc/unleash/unleash.env; restart Unleash after a change.
Step 11: Integrations and outbound connections
Unleash integrations (webhooks, Slack, Microsoft Teams, Datadog) send events to the addresses you configure. On this image they can reach public internet addresses only. Unleash's own URL check refuses private, loopback and link-local addresses, and an nftables rule for the unleash user refuses connections to the EC2 instance metadata service (IPv4 and IPv6), the instance itself, private networks and your VPC even if that check were bypassed:
sudo nft list table inet cloudimg_unleash_egress | grep -oE 'comment "cloudimg-(metadata6?|private|vpc6?|loopback)"' | sort -u \
| tee /dev/stderr | grep -c . | grep -x 6 >/dev/null && echo "egress guard loaded"
comment "cloudimg-loopback"
comment "cloudimg-metadata"
comment "cloudimg-metadata6"
comment "cloudimg-private"
comment "cloudimg-vpc"
comment "cloudimg-vpc6"
egress guard loaded
To let an integration reach a server inside your network (for example a webhook receiver at 10.0.5.25), add its address to /etc/cloudimg/unleash-egress-allow (one CIDR per line), set UNLEASH_ALLOW_PRIVATE_URL_IN_INTEGRATION=true in /etc/unleash/unleash.env, then:
sudo systemctl reload unleash-egress-guard && sudo systemctl restart unleash
Unleash's version check and telemetry (CHECK_VERSION, SEND_TELEMETRY) are off in /etc/unleash/unleash.env. The Unleash UI loads its fonts from Google Fonts and user avatars from Gravatar in your browser; your browser makes those requests, not the instance.
Step 12: Use your own domain
Point a DNS name at the instance (an Elastic IP keeps it stable), then make it the server's address. This generates a new certificate for that name, pins nginx to it and sets UNLEASH_URL (the address in links Unleash generates):
sudo unleash-set-site flags.example.com
To use a certificate from a public CA instead, for example one issued by certbot with a DNS challenge, run unleash-set-site first, then replace /etc/ssl/unleash/unleash.crt (full chain) and /etc/ssl/unleash/unleash.key (mode 0600) and run sudo systemctl reload nginx.
Step 13: Run the self-test
The image ships a self-test that checks the listeners, that no session, a wrong password and admin / unleash4all are refused, that admin signs in with this instance's password over HTTPS, that a session signed with super-secret is refused while the same session signed with this instance's secret is accepted, and that a cross-site sign-in is refused:
sudo unleash-selftest | tail -n 1 | tee /dev/stderr | grep '^UNLEASH_SELFTEST_OK ' >/dev/null
UNLEASH_SELFTEST_OK no session, a wrong password and admin/unleash4all refused; admin logs in with this instance's password over https://ec2-203-0-113-10.compute-1.amazonaws.com/ (Secure HttpOnly session); a 'super-secret'-signed session refused (this instance's secret accepted); a cross-origin login refused
The self-test signs in three times; Unleash allows 10 sign-in attempts a minute, so do not run it in a tight loop.
Step 14: Back up and restore
Everything Unleash stores (flags, strategies, users, tokens, the event log) is in the PostgreSQL database unleash. Back it up with:
sudo -u postgres pg_dump -Fc unleash > unleash-$(date +%F).dump
To restore on a new instance of this image: sudo systemctl stop unleash, then sudo -u postgres pg_restore --clean --if-exists -d unleash unleash-<date>.dump and sudo systemctl start unleash. The restored database brings its own users and passwords; the new instance keeps its own session secret.
Step 15: Security notes and the build record
The certificate generated at first boot is a server certificate only: it cannot sign other certificates.
openssl x509 -in /etc/ssl/unleash/unleash.crt -noout -ext basicConstraints,extendedKeyUsage | tee /dev/stderr | grep 'CA:FALSE' >/dev/null
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Extended Key Usage:
TLS Web Server Authentication
The build record names every component and its checksum:
grep -E '^(unleash-server|node) ' /usr/share/doc/cloudimg-unleash/BUILD-INFO.txt | tee /dev/stderr | grep '^unleash-server 8\.3\.1 ' >/dev/null
unleash-server 8.3.1 (npm, github.com/Unleash/unleash, 2026-10-29), AGPL-3.0-or-later (unleash-LICENSE); installed
node 22.23.4 (nodejs.org node-v22.23.4-linux-x64.tar.xz, sha256 1805bcd35e6815559bd2eb97db438d3fdb0eb3a5e6a977b7c1badca76337e9ce = nodejs.org SHASUMS256.txt),
Unleash is licensed under the GNU AGPL 3.0 and Node.js under the MIT licence; their licences are in /usr/share/doc/cloudimg-unleash/, with the source of Unleash at github.com/Unleash/unleash. Unleash runs as its own unprivileged user unleash with no login shell, no capabilities, a read-only view of the system and no access to the credentials file or the TLS key. Unleash, nginx and PostgreSQL cannot reach the EC2 instance metadata service, and PostgreSQL cannot open any network connection. PostgreSQL 16 and nginx come from Ubuntu's main archive; the image adds no third-party package repository.
Step 16: Keep it up to date
Ubuntu security updates, including PostgreSQL and nginx, install automatically. To apply everything now:
sudo apt-get update && sudo apt-get upgrade -y
New Unleash and Node.js releases arrive as new versions of the AWS Marketplace listing: launch the new version, restore your database backup into it (Step 14), and run sudo unleash-set-site with your domain if you use one.
Support
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
The other versions on this listing are on the DevOps platforms guide.