privacyIDEA 3.14 on AWS User Guide
Overview
This guide covers launching and using the privacyIDEA 3.14 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.
privacyIDEA is an open source multi-factor authentication (MFA) server. It manages second factors for your users (TOTP and HOTP authenticator apps, push, WebAuthn and passkeys, SMS, email, hardware tokens and more) and answers one question for the systems in front of it: is this one-time password right for this user? Your VPN, RADIUS server, Linux PAM, LDAP proxy, identity provider or web application asks privacyIDEA through its REST API (/validate/check), and privacyIDEA answers yes or no, with every decision in a signed audit log.
The cloudimg image is a hardened, ready-to-use privacyIDEA server:
- nginx in front, privacyIDEA behind it. nginx is the only public listener (ports 80 and 443). privacyIDEA runs under gunicorn on
127.0.0.1:8080, loopback only. Its database, PostgreSQL 16, has no network port at all: privacyIDEA reaches it over a local Unix socket. - HTTPS from the first boot. First boot names the instance after its EC2 public DNS name and serves HTTPS with a certificate generated on your instance for that name (plus the public and private addresses). Port 80 only redirects to HTTPS and serves the Let's Encrypt challenge path. One command switches to your own name and a trusted certificate.
- Your instance's own secrets. First boot generates this instance's Flask secret key, password pepper, database password, the encryption key for token seeds and the audit-log signing key pair, creates the database and the one
adminaccount with a random password. The image itself has no configuration, no key, no database and no account: nothing is shared with any other instance. - The real client address, everywhere. privacyIDEA trusts only the local nginx as a proxy, and nginx sends it the address of the TCP connection, never a header a client could forge. The audit log, privacyIDEA's own client policies and the sign-in limits all see the real client.
- Sign-in limits per client. nginx allows 30 requests a minute per client address (bursts of 20) on the sign-in endpoint (
/auth), and 20 a second (bursts of 100) on/validate/*and everything else; the web interface's own files are never limited. - Server-side connections can't reach inside. privacyIDEA connects out by itself for features an admin configures: SMTP servers, RADIUS, LDAP and SQL user stores, other privacyIDEA servers, webhooks, SMS gateways and push notifications. Below the application, a firewall table refuses the instance metadata service, every private, link-local, shared and reserved range, every IPv6 address and this VPC's own ranges for privacyIDEA. You open the hosts you need (your directory server, your RADIUS server, your mail relay) in one file. Public internet hosts need no entry.
- Browser hardening. privacyIDEA's own Content-Security-Policy with
frame-ancestors 'none',X-Frame-Options: DENY,Strict-Transport-Security,X-Content-Type-Options,Referrer-PolicyandPermissions-Policy. The access log records paths only, never a query string, so a one-time password sent in a URL is not logged. - Locked down. TLS 1.2 and 1.3 only; the AMI requires IMDSv2, and privacyIDEA, nginx and PostgreSQL are blocked from the instance metadata service. privacyIDEA, nginx and PostgreSQL run with core dumps disabled (
LimitCORE=0). - RADIUS ready. privacyIDEA's RADIUS features (RADIUS tokens, RADIUS user stores and servers) need a RADIUS dictionary, which the privacyIDEA package does not include; the image ships the standard dictionary in
/etc/privacyidea. - Pinned and verified, on a fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on. See Step 14 for exactly what is installed.
What is on the image: privacyIDEA 3.14 (the official privacyidea release from PyPI, with every Python dependency pinned to an exact version and SHA-256 hash, in /opt/privacyidea/venv), gunicorn, PostgreSQL 16, nginx and nftables from Ubuntu 24.04, cloud-init and the AWS Systems Manager agent.
The database: PostgreSQL 16 from the Ubuntu 24.04 main archive (postgresql-16, with libpq5 as the client library privacyIDEA's psycopg driver loads). Ubuntu security updates for it install through apt and unattended-upgrades, like every other system package on the image. The image ships no database: first boot creates this instance's own PostgreSQL cluster, the privacyidea database and its account, with a password generated on your instance.
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 that gives the instance a public IPv4 address (or reach it over your VPN or Direct Connect).
- A security group that allows TCP 22 (SSH) from your own networks only, and TCP 443 (privacyIDEA) and TCP 80 (redirects to HTTPS) from the networks whose administrators, users and applications use privacyIDEA. To get a certificate from Let's Encrypt, port 80 must be reachable from the internet while it runs.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick privacyIDEA 3.14 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large, your key pair, and a security group as above.
With the AWS CLI (replace the values in angle brackets):
aws ec2 run-instances --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> \
--associate-public-ip-address \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=privacyidea}]'
First boot takes about a minute after the instance is running: it generates the keys, the certificate and the database, creates the admin account, proves the admin sign-in on the loopback address and only then opens the web port.
Step 2: Connect to your instance
| Operating system | SSH user |
|---|---|
| Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
The login message shows the address of privacyIDEA and the commands this guide uses.
Step 3: Check the services
systemctl is-active privacyidea nginx postgresql@16-main
active
active
active
Only SSH, HTTP and HTTPS are public. privacyIDEA listens on the loopback address, and PostgreSQL listens on no TCP port at all (it serves its local Unix socket only):
sudo ss -tln | grep -E ':(22|80|443|8080|5432) '
LISTEN 0 2048 127.0.0.1:8080 0.0.0.0:*
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
LISTEN 0 4096 0.0.0.0:22 0.0.0.0:*
LISTEN 0 511 0.0.0.0:443 0.0.0.0:*
LISTEN 0 511 [::]:80 [::]:*
LISTEN 0 4096 [::]:22 [::]:*
LISTEN 0 511 [::]:443 [::]:*
A built-in self-test signs in as the admin over HTTPS, checks that a wrong password is refused and that the admin endpoints refuse a request without a token, and asks /validate/check about a user that does not exist:
sudo privacyidea-selftest
privacyidea-selftest: wrong password 401, admin login 200 (role admin), /system/ with token 200, 7 admin endpoints 401 unauthenticated, unknown user refused (400)
PRIVACYIDEA_SELFTEST_OK
Step 4: Read the credentials file and the certificate fingerprint
sudo cat /root/privacyidea-credentials.txt
The file (root only, mode 0600) holds this instance's address, the SHA-256 fingerprint of its certificate and the password of the admin account:
# privacyIDEA 3.14 on this instance (cloudimg). Sign in to the WebUI as PRIVACYIDEA_ADMIN_USERNAME with
# PRIVACYIDEA_ADMIN_PASSWORD. The certificate is self-signed for this instance: compare its SHA-256 fingerprint
# with PRIVACYIDEA_TLS_SHA256. A new admin password at any time: sudo privacyidea-admin
PRIVACYIDEA_URL=https://ec2-203-0-113-10.compute-1.amazonaws.com/
PRIVACYIDEA_HOST=ec2-203-0-113-10.compute-1.amazonaws.com
PRIVACYIDEA_ADMIN_USERNAME=admin
PRIVACYIDEA_TLS_SHA256=4F:2A:91:...:7D:0C (32 hex pairs)
PRIVACYIDEA_NAME_SOURCE=public-dns-name
PRIVACYIDEA_ADDRESS=203.0.113.10
PRIVACYIDEA_ADDRESS_SOURCE=imdsv2-public-ipv4
GUIDE=https://www.cloudimg.co.uk/guides/privacyidea-3-14-on-ubuntu-24-04-aws/
PRIVACYIDEA_ADMIN_PASSWORD=<a random 24-character password>
The certificate is self-signed for this instance. Before you type the password into a browser, check that the fingerprint your browser shows (click the padlock or the warning, then the certificate details) is the PRIVACYIDEA_TLS_SHA256 value from this file. To check from your own computer instead:
openssl s_client -connect <public-ip>:443 -servername <public-dns-name> </dev/null 2>/dev/null | openssl x509 -noout -fingerprint -sha256
To give the admin a new random password at any time (it goes into the same file, never on screen):
sudo privacyidea-admin
privacyidea-admin: the local admin 'admin' has a new password, stored in /root/privacyidea-credentials.txt (PRIVACYIDEA_ADMIN_PASSWORD)
Step 5: Sign in to the web interface
Open https://<public-dns-name>/ (the PRIVACYIDEA_URL from the credentials file) and accept the certificate once you have checked its fingerprint. privacyIDEA opens its sign-in page; sign in as admin with PRIVACYIDEA_ADMIN_PASSWORD.

privacyIDEA opens on its Dashboard (authentication activity, token usage, the server's status). Select Token in the top bar for the token list. A fresh instance has no tokens, no users and no policies:

Step 6: Enroll your first token
Select Enroll Token on the empty token list (or Token > Enrollment), choose the type TOTP (a time-based one-time password for an authenticator app), optionally add a description, and select Enroll Token. privacyIDEA generates the token's seed, stores it encrypted with this instance's key, and shows it once as a QR code in the Token Successfully Enrolled dialog, with the token's serial number. Scan it with any authenticator app. The QR code (and the OTP Key panel below it) carries the token's secret, so treat it like a password: enroll where nobody can see the screen, or select Regenerate QR Code if someone did (the code is masked in this screenshot):

A token not yet assigned to a user can still be tested: the next step checks a one-time password against its serial number.
Step 7: Check a one-time password over the API
Applications ask privacyIDEA through its REST API. First sign in to the API as the admin (POST /auth returns a signed JSON Web Token that the admin endpoints accept); the password is read from the credentials file into a file only root can read, never onto a command line:
H=$(sudo sed -n 's/^PRIVACYIDEA_HOST=//p' /root/privacyidea-credentials.txt)
sudo cat /etc/ssl/privacyidea/privacyidea.crt > ~/privacyidea-instance.crt
F=$(mktemp); chmod 600 "$F"
{ printf 'username=admin&password='; sudo sed -n 's/^PRIVACYIDEA_ADMIN_PASSWORD=//p' /root/privacyidea-credentials.txt | tr -d '\n'; } > "$F"
TOKEN=$(curl -s --cacert ~/privacyidea-instance.crt --data-binary @"$F" -H 'Content-Type: application/x-www-form-urlencoded' "https://$H/auth" | jq -r .result.value.token)
shred -u "$F"
curl -s --cacert ~/privacyidea-instance.crt -H "Authorization: $TOKEN" "https://$H/token/" | jq '.result.value.count'
1
A wrong password is refused with HTTP 401:
H=$(sudo sed -n 's/^PRIVACYIDEA_HOST=//p' /root/privacyidea-credentials.txt)
curl -s -o /dev/null -w '%{http_code}\n' --cacert ~/privacyidea-instance.crt --data 'username=admin&password=wrong' "https://$H/auth"
401
To check a one-time password, applications call /validate/check with the user (or, for a token not yet assigned, the serial) and the code. With the token from Step 6 (replace <serial> and <otp> with the serial number from the dialog, for example TOTP0001ABCD, and the six digits your app shows now):
H=$(sudo sed -n 's/^PRIVACYIDEA_HOST=//p' /root/privacyidea-credentials.txt)
curl -s --cacert ~/privacyidea-instance.crt --data 'serial=<serial>&pass=<otp>' "https://$H/validate/check" | jq '.result'
{
"authentication": "ACCEPT",
"status": true,
"value": true
}
A wrong or reused code answers "value": false with "authentication": "REJECT". Send the code in the request body (--data, a POST) as above, not in the URL.
Step 8: Connect your user store
privacyIDEA sits in front of the directory you already have. Go to Users > Resolvers to create a resolver: a connection to Active Directory or another LDAP server, a SQL database, an HTTP service or a flat file. Then create a realm under Users > Realms that contains the resolver. Users of the realm appear under Users, and you can enroll tokens for them, or let them enroll their own after signing in to the same web interface.
Your directory server is almost certainly at a private address, often inside this VPC. privacyIDEA may only reach it once you open it (see Step 11): put its address in /etc/cloudimg/privacyidea-egress-allow and restart.
Every administrative action and every authentication is written to the audit log, signed with this instance's audit key, under Audit.
Step 9: Your domain and a trusted certificate
- Create a DNS record for your name (for example an
Arecordmfa.example.comto the instance's Elastic IP address). - Point privacyIDEA at it. The first name becomes the name nginx serves and the certificate covers; any names after it keep working too:
sudo privacyidea-set-domain mfa.example.com "$(sudo sed -n 's/^PRIVACYIDEA_HOST=//p' /root/privacyidea-credentials.txt)"
- Get a certificate for it. With Let's Encrypt, nginx already serves the HTTP challenge directory, so certbot's webroot mode works without stopping anything (port 80 must be reachable from the internet while it runs):
sudo apt-get install -y certbot
sudo certbot certonly --webroot -w /var/www/cloudimg-acme -d mfa.example.com --email you@example.com --agree-tos -n \
--deploy-hook 'privacyidea-set-domain --tls-cert /etc/letsencrypt/live/mfa.example.com/fullchain.pem --tls-key /etc/letsencrypt/live/mfa.example.com/privkey.pem mfa.example.com'
The deploy hook installs the certificate now and again after every automatic renewal. With a certificate from another authority, run sudo privacyidea-set-domain --tls-cert <fullchain.pem> --tls-key <private-key.pem> mfa.example.com yourself; it checks that the key matches and that the certificate covers the name. Without --tls-cert, privacyidea-set-domain issues a new self-signed certificate for the names you give.
nginx answers only the names you configured: a request for any other host name, including one forged in the Host header, gets no answer.
Step 10: Sign-in limits and client addresses
nginx limits each client address. Past the limit a client gets HTTP 429 until its allowance refills, while other clients carry on:
grep -E '^limit_req' /etc/nginx/conf.d/cloudimg-privacyidea.conf
limit_req_zone $binary_remote_addr zone=pi_auth:10m rate=30r/m;
limit_req_zone $binary_remote_addr zone=pi_validate:10m rate=20r/s;
limit_req_zone $binary_remote_addr zone=pi_all:10m rate=20r/s;
limit_req_zone $binary_remote_addr zone=pi_static:10m rate=100r/s;
limit_req_status 429;
The limit is per client IP address (an IPv6 client is limited per address), not per account. privacyIDEA's own failure counter is per token: after too many wrong one-time passwords (10 by default) it locks the token until an admin resets it, whatever address the attempts came from. Tune both to your users: /etc/nginx/conf.d/cloudimg-privacyidea.conf for nginx (then sudo systemctl reload nginx), the token's Max Fail under Token or a policy for privacyIDEA.
The limits, the audit log and privacyIDEA's client policies use the address of the TCP connection that reached nginx. If you put a load balancer in front of privacyIDEA, every client then has the load balancer's address: keep the limits per client by using a Network Load Balancer with client IP preservation, or adjust the nginx configuration for your load balancer.
Step 11: Outbound connections
privacyIDEA connects to the servers an admin configures: SMTP for email tokens and notifications, LDAP, SQL or HTTP user stores, RADIUS, remote privacyIDEA servers, SMS gateways, webhooks and Firebase push. Internet hosts work as they are. The instance metadata service, private, link-local, shared and reserved ranges, every IPv6 address and this VPC's own ranges are refused for privacyIDEA by an nftables table (inet cloudimg_privacyidea) and by systemd, so an admin setting (or a stolen admin session) cannot make privacyIDEA probe your network or read the instance's credentials.
To let privacyIDEA reach a private host, for example your domain controller and your RADIUS server, list them in /etc/cloudimg/privacyidea-egress-allow, one address or CIDR per line (IPv4 ranges no wider than /16; the metadata service can never be opened), and restart:
echo 10.0.1.25 | sudo tee -a /etc/cloudimg/privacyidea-egress-allow
sudo systemctl restart cloudimg-privacyidea-metadata privacyidea
This VPC's ranges are read from the instance metadata service at every boot. See them, and the firewall table privacyIDEA's account is held to:
cat /run/cloudimg-privacyidea/vpc-cidrs
sudo nft list table inet cloudimg_privacyidea | grep -E 'set (vpc4|allow4)' -A 3
# written by privacyidea-metadata at 2026-11-08T19:16:13Z
source imds
4 172.31.0.0/16
set allow4 {
type ipv4_addr
flags interval
}
--
set vpc4 {
type ipv4_addr
flags interval
elements = { 172.31.0.0/16 }
Step 12: Keys, the database and backups
privacyIDEA's configuration /etc/privacyidea/pi.cfg holds this instance's secret key, password pepper and database password; /etc/privacyidea/enckey encrypts every token seed in the database, and /etc/privacyidea/private.pem signs the audit log. Only root and the privacyidea service account can read them:
sudo stat -c '%a %U:%G %n' /etc/privacyidea /etc/privacyidea/pi.cfg /etc/privacyidea/enckey /etc/privacyidea/private.pem /etc/privacyidea/public.pem
750 root:privacyidea /etc/privacyidea
640 root:privacyidea /etc/privacyidea/pi.cfg
400 privacyidea:privacyidea /etc/privacyidea/enckey
400 privacyidea:privacyidea /etc/privacyidea/private.pem
644 root:root /etc/privacyidea/public.pem
Back up the keys with the database, and protect both: without enckey the token seeds in a database backup cannot be used, and with it they can. To back up (replace <backup-dir> with a directory only root can read):
sudo install -d -m 0700 <backup-dir>
sudo cp -p /etc/privacyidea/pi.cfg /etc/privacyidea/enckey /etc/privacyidea/private.pem /etc/privacyidea/public.pem <backup-dir>/
cd / && sudo -u postgres pg_dump --format=custom privacyidea | sudo tee <backup-dir>/privacyidea.dump >/dev/null
sudo privacyidea-manage ... runs privacyIDEA's pi-manage tool with this instance's configuration, as the service account and under the same outbound rules (for example sudo privacyidea-manage admin list). It refuses a password on the command line: use sudo privacyidea-admin to set one.
Step 13: Security notes
- Metadata service and private addresses. The AMI requires IMDSv2. systemd denies privacyIDEA, nginx, PostgreSQL and the first-boot service the instance metadata addresses. One small root service,
cloudimg-privacyidea-metadata, reads this VPC's ranges from the metadata service at every boot and loads them into the firewall table. This fails closed: if neither the metadata service nor cloud-init's copy of it gives this VPC's ranges, the service tries for about 30 seconds and privacyIDEA stays down rather than run without its VPC deny. The service then keeps retrying by itself (after 10 seconds, backing off to every 5 minutes), and the moment an attempt succeeds it starts privacyIDEA again (and first boot and nginx, if they were waiting): you do not need to restart anything by hand. Any other refusal is final, so a bad setting is never restarted in a loop: for example, ifpi.cfgloses its0640 root:privacyideamode, privacyIDEA stays stopped until you fix it and runsudo systemctl start privacyidea.sudo journalctl -u privacyidea -u cloudimg-privacyidea-metadata -bshows the reason. The one exception is a list this same instance read itself on an earlier boot: it is kept root-only in/var/lib/cloudimg/privacyidea-vpc-cidrs.lkg, tied to this instance's ID, used only when both sources are silent, and logged as a WARNING (a VPC's ranges rarely change; a stale list can only deny less of a VPC that grew).
systemctl show -p IPAddressDeny --value privacyidea | tr ' ' '\n' | sort -V | tr '\n' ' '; echo
0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 168.63.129.16/32 169.254.0.0/16 172.16.0.0/12 192.0.0.0/24 192.168.0.0/16 198.18.0.0/15 224.0.0.0/4 240.0.0.0/4 ::/0
- Where the secrets live. The secret key, pepper and database password are only in
pi.cfg(mode 0640,root:privacyidea); they are in no environment variable, command line, journal or log. All of them are generated on your instance at first boot. The instance has no swap, and core dumps are off. - The admin account. First boot creates the one local admin,
admin, with a random password before the web port opens; nobody else can create an account. privacyIDEA's admin realm setting (SUPERUSER_REALM) names a realm,super, that the image does not create, so a user from a directory you connect is never an admin by accident. - Response headers. A Content-Security-Policy built from privacyIDEA's own: scripts only from the instance itself (plus, by its hash, the one inline handler the 3.14 web interface uses to load its stylesheet), inline styles allowed because the 3.14 interface needs them, and
frame-ancestors 'none';X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy: same-origin, aPermissions-PolicyandStrict-Transport-Security. The admin API sends no CORS headers. - The legacy web interface. privacyIDEA 3.14 still contains its old AngularJS interface under
/static_old/; the image does not serve it (404). - Upgrades. Ubuntu security updates (including nginx, and PostgreSQL 16 from the Ubuntu 24.04 main archive) install automatically through
unattended-upgrades. privacyIDEA is a pinned release, so cloudimg publishes new versions of this AMI for new privacyIDEA releases.
Step 14: Licences and source
privacyIDEA is licensed under the GNU Affero General Public License v3.0 or later. The image runs it unmodified and records exactly what it was built from:
cat /usr/share/doc/cloudimg-privacyidea/SOURCE.txt
privacyIDEA 3.14 (AGPL-3.0-or-later), unmodified, as published on PyPI.
Corresponding source (AGPL-3.0 section 6 / 13): the release tag v3.14 = commit 31db0ecd87bbbb00b7ca43bd70d4a7f4fb846842
https://github.com/privacyidea/privacyidea/tree/v3.14
and the PyPI sdist privacyidea-3.14.tar.gz, sha256 9900ec9c94ca3d822551736c827f2aa2d74bc7acc45ee3b4c1ad346ab3cf06ce
https://pypi.org/project/privacyidea/3.14/#files
The installed wheel privacyidea-3.14-py3-none-any.whl has sha256 86a24cbffa8a7316eab93eb220b4d8710ea68af2390990d0dddd58d7bd6947b1 (requirements.lock).
cloudimg changes NO privacyIDEA source file. The appliance configuration (pi.cfg template, nginx, systemd units,
first-boot helpers) is in /usr/local/lib/cloudimg/privacyidea, /usr/local/sbin/privacyidea-* and /etc/nginx.
The licence text is in /usr/share/doc/cloudimg-privacyidea/LICENSE-privacyidea-AGPL-3.0.txt, the licences of the Python dependencies in dependency-licences.txt beside it, and the exact pinned set with hashes in requirements.lock. nginx, PostgreSQL and nftables are the Ubuntu packages (/usr/share/doc/<package>/copyright). The RADIUS dictionary is pyrad's (BSD licence). privacyIDEA is a trademark of its respective owner. All product and company names are trademarks or registered trademarks of their respective owners; cloudimg is not affiliated with or endorsed by them.
Support
cloudimg supports this image: email support@cloudimg.co.uk. See the identity, secrets and PKI listing guide for the other versions in this listing.