Deployment guide

Unleash 8.3.1

Overview

Unleash is an open-source feature-flag server. You create flags in its web UI, turn them on per environment, and roll them out gradually with strategies. Your applications ask Unleash (through its SDKs or its HTTP API) which flags are on.

This guide covers the Unleash 8.3.1 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud DevOps platforms listing on AWS Marketplace, and you pick it as the software version when you subscribe and launch. The image runs the open-source Unleash server 8.3.1 on Node.js 22 LTS, with its PostgreSQL 16 database on the same instance.

What you get:

  • No default account. The stock Unleash admin / unleash4all login is never created. First boot creates a single admin user 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, and the upstream default is the well-known super-secret. First boot generates a random secret for your instance. 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, because 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, and logins are limited more tightly. nginx always replaces the X-Forwarded-For header with the address it accepted the connection from.
  • No call-home. The Unleash 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, but never the EC2 instance metadata service, the instance itself or your private network. An nftables rule for the unleash user backs up the Unleash 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), so no database password exists. The database is created on your instance's first boot, and 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 the signed SHA-256 list from nodejs.org), 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 licenses of Unleash (AGPL-3.0) and Node.js (MIT) are in /usr/share/doc/sac-unleash/.
  • Default user: ubuntu
  • Architecture: x86_64
  • Recommended instance type: m5.large

Prerequisites

Before you launch the image, you need:

  • 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, and open 22 to your own networks only.
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, select your Region, and choose Launch through EC2. Select m5.large (a t3.medium is enough for a small team, and Unleash itself uses about 150 MiB of memory), your key pair, your subnet and the security group above.

To launch with the AWS CLI instead, 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}]'

Attach an Elastic IP to keep the server's address, and so its name and certificate, the same across stops and starts.

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>
3

Check the services

First boot takes under a minute. It initializes the PostgreSQL database, runs the Unleash database migrations, sets the server's name, and generates the certificate, the session secret and the admin password. Only then does it start 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/sac/data/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
4

Get your address and password

Run sudo cat /var/lib/sac/data/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/sac/data/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/sac/data/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). To use a certificate that browsers already trust, use your own domain (Step 12).

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. In the Email field, type admin (Unleash accepts a user name there). In the Password field, type the UNLEASH_ADMIN_PASSWORD from Step 4:

The Sign up link goes to the Unleash website, because your server has no self-registration. Forgot your password? sends nothing until you configure email in Unleash. After you sign in, change the password (your avatar, top right > View profile > Change password). The project overview lists the default project. Click New feature flag to create a flag. You can turn each flag on and off per environment (development and production):

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

6

Create a flag and read it as an application

Everything in the UI is also available in the Unleash admin API. The following commands, run on the instance, sign in as admin, create the flag new-checkout, turn it on in development, create a backend API token, and then request the flags the way an SDK does, with that token. The password is read from the credentials file into a 0600 file, never onto a command line:

C=/var/lib/sac/data/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
7

What is refused without a login

Unleash refuses the admin API, the client API without a valid token, and a session cookie that this instance did not sign, all with 401. Only the health check and the sign-in page answer anonymously:

H="$(sudo sed -n 's/^UNLEASH_HOST=//p' /var/lib/sac/data/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/sac/data/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.

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/sac/data/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 are limited to 12 a minute per address (bursts of 10), on top of the Unleash limit of 10 sign-in attempts a minute. The UI static files have their own bucket (100 a second, bursts of 500), so loading the UI never uses up the API allowance. 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-For header with the address it accepted the connection from, so a client cannot change the address that the rate limits and the logs use. If you put a load balancer in front of the instance, every client 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, a Content-Security-Policy and Referrer-Policy: no-referrer, and nginx does not advertise its version.

What nginx and Unleash log: the nginx 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).

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.

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 checks that file. It refuses to start Unleash if the file is missing, unreadable by anyone but root, malformed, empty or set to the public Unleash default super-secret. Unleash then stays down with an explanation in its journal instead of 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, run the following. 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.

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. The Unleash URL check refuses private, loopback and link-local addresses. As a backstop, 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 sac_unleash_egress | grep -oE 'comment "sac-(metadata6?|private|vpc6?|loopback)"' | sort -u \
  | tee /dev/stderr | grep -c . | grep -x 6 >/dev/null && echo "egress guard loaded"
comment "sac-loopback" comment "sac-metadata" comment "sac-metadata6" comment "sac-private" comment "sac-vpc" comment "sac-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/sac/unleash-egress-allow (one CIDR per line). Then set UNLEASH_ALLOW_PRIVATE_URL_IN_INTEGRATION=true in /etc/unleash/unleash.env and run:

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.

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. The following command 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.

13

Run the self-test

The image ships a self-test. It checks the listeners and confirms that no session, a wrong password and admin / unleash4all are refused. It also confirms 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 the self-test in a tight loop.

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, stop Unleash with sudo systemctl stop unleash. Then restore the dump with sudo -u postgres pg_restore --clean --if-exists -d unleash unleash-<date>.dump and start Unleash with sudo systemctl start unleash. The restored database brings its own users and passwords. The new instance keeps its own session secret.

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/sac-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 license. Their licenses are in /usr/share/doc/sac-unleash/, and the Unleash source is 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 the Ubuntu main archive. The image adds no third-party package repository.

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: sales@secureanycloud.com
  • Website: https://secureanycloud.com

The other versions on this listing are on the DevOps platforms guide.

Deploy on AWS

Launch Unleash 8.3.1 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.

View on Marketplace

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on AWS