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/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, 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-Forheader 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
unleashuser 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.
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.
Connect to your instance
Version SSH user
Unleash 8.3.1 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>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_DONEactive active active active active FIRSTBOOT_DONEOnly 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 443Get 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 presentUNLEASH_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/nullUNLEASH_URL=https://ec2-203-0-113-10.compute-1.amazonaws.com/ UNLEASH_ADMIN_USERNAME=admin UNLEASH_TLS_SHA256_FINGERPRINT=3A:5F:...:C2The 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).
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 256Add 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.
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 OKFrom 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/featuresWhat 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 200The 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 refusedUnleash's metrics endpoint (/internal-backstage), its readiness probe and the OpenAPI documentation are not served, and the Edge endpoints need an Authorization header.
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-Forheader 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, aContent-Security-PolicyandReferrer-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).
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 passwordThe 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.
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/nullstart guard: secret OK root:root 600To 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 unleashOther settings (for example LOG_LEVEL) are in /etc/unleash/unleash.env; restart Unleash after a change.
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 loadedTo 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 unleashUnleash'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.
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.comTo 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.
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/nullUNLEASH_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 refusedThe 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.
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).dumpTo 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.
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/nullX509v3 Basic Constraints: critical CA:FALSE X509v3 Extended Key Usage: TLS Web Server AuthenticationThe 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/nullunleash-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.
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 -yNew 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