LTB Self Service Password 1.8.2 on AWS User Guide
Overview
This guide covers launching and using the LTB Self Service Password 1.8.2 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.
LTB Self Service Password (from the LDAP Tool Box project) is a small web application that lets the users of an LDAP directory change their own password in a browser, so a forgotten-password ticket or an expiry no longer needs an administrator. It can also send reset links by e-mail, ask security questions or send SMS codes when you configure them. This image gives you Self Service Password together with its own OpenLDAP directory on one instance, ready to use.
The cloudimg image is a hardened, ready-to-use password portal:
- Upstream Self Service Password 1.8.2. The release tarball from ltb-project.org (GPL-3.0-or-later), checked against its checksum and installed file for file, with Smarty 4.5.8 from its upstream release. Ubuntu's older Smarty packages are not used. Source, licences and the file list are on the image in
/usr/share/doc/cloudimg-self-service-password/. - HTTPS from the first boot. nginx is the only public listener (ports 80 and 443) and serves a certificate generated on your instance (an end-entity certificate, not a certificate authority); port 80 redirects to HTTPS. The address is set from the instance's EC2 public DNS name at first boot and can be changed to your own domain.
- Every secret is created on your instance. First boot creates the directory and generates the directory administrator password, the
cn=configpassword, the password of the portal's own service account and the keyphrase that encrypts reset tokens. Nothing is shared between instances. - Users prove their old password. Out of the box the portal only offers change my password: the user must enter the current password, and the portal then changes it as that user. Reset by e-mail, security questions and SMS stay off until you configure them (Step 10 shows the e-mail reset).
- A least-privilege service account. The portal looks users up with its own account, which can read
ou=peopleand can write (never read) passwords there, and nothing else. - An empty directory, created on your instance. The base
dc=example,dc=com(you can change it, Step 12) withou=people,ou=groupsandou=servicesonly: no demo users, and no directory from the build ships in the image. - OpenLDAP is not on the network. slapd listens on
127.0.0.1and its local socket only; anonymous bind and search are refused; passwords are stored as Argon2 hashes. - The portal's own files are never served. Only the portal page and its stylesheets, scripts and images are reachable over HTTPS; its configuration, secrets, templates, libraries and working files are outside the web root or return 404. PHP runs the portal as its own unprivileged user with access limited to its own directories and with process execution disabled.
- Real client addresses and rate limits. nginx hands the portal the real TCP peer, never a client's own
X-Forwarded-For, so the portal's audit log shows who changed what; password submissions are rate limited per client address, and the portal also limits attempts per user. - Metadata access blocked. Every service is blocked from the instance metadata service, and the AMI requires IMDSv2.
- Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: LTB Self Service Password 1.8.2, Smarty 4.5.8, OpenLDAP 2.6 (slapd, ldap-utils), nginx, PHP 8.3 (FPM), Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.
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 22 (SSH) from your own networks only, TCP 443 (the portal) from the networks your users are on, and TCP 80 (redirects to HTTPS) if you want plain
http://links to work. Never open 389 or 636: the directory listens on the loopback address only. - For production, a DNS name you control (for example
ssp.example.com) and a certificate from a certificate authority for it.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick LTB Self Service Password 1.8.2 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> \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=self-service-password}]'
First boot takes a few seconds after cloud-init has run. To use your own name or your own directory base from the start, put them in /etc/cloudimg/self-service-password-domain.preset and /etc/cloudimg/self-service-password-basedn.preset with user data (for example cloud-init write_files entries); otherwise first boot uses the instance's public DNS name and dc=example,dc=com.
Step 2: Connect to your instance
| Operating system | SSH user |
|---|---|
| Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
systemctl is-active self-service-password-firstboot slapd php8.3-fpm nginx
active
active
active
active
Only SSH, HTTP and HTTPS are public. OpenLDAP listens on the loopback address (and on its local socket); nothing listens on 636:
sudo ss -tln | awk '{print $4}' | grep -E ':(22|80|443|389|636)$' | sort -u
0.0.0.0:22
0.0.0.0:443
0.0.0.0:80
127.0.0.1:389
[::]:22
[::]:443
[::]:80
Step 4: Your address and passwords
sudo grep -v PASSWORD /root/self-service-password-credentials.txt
The file (root only, mode 0600) was written by first boot. Without its password lines it reads:
# LTB Self Service Password 1.8.2 - written on first boot by self-service-password-firstboot.service. Unique to this instance.
SSP_URL=https://ec2-18-212-72-80.compute-1.amazonaws.com/
LDAP_ADMIN_DN=cn=admin,dc=example,dc=com
LDAP_URL=ldap://127.0.0.1:389
LDAP_BASE_DN=dc=example,dc=com
LDAP_PEOPLE_DN=ou=people,dc=example,dc=com
LDAP_GROUPS_DN=ou=groups,dc=example,dc=com
LDAP_CONFIG_DN=cn=admin,cn=config
SSP_SERVICE_DN=cn=self-service-password,ou=services,dc=example,dc=com
TLS_SHA256=22:B3:39:C7:B1:A1:53:F2:ED:2E:EB:B0:57:69:2A:AC:CE:71:C5:8E:E3:FC:4C:20:32:F1:3E:A7:21:EA:F8:EC
TLS_SPKI_SHA256=u1/PZNFS9l85wDld1nr3vk5pHT5lwUR+VakaR2Utgk4=
#
# Users in ou=people change their own password at SSP_URL (login = uid, old password required). Add users as the
# directory administrator without a password on the command line, e.g.: sudo cloudimg-ldap ldapadd -f users.ldif
# SSP's own secrets (service-account password and token keyphrase): /etc/self-service-password/secrets.inc.php
# Reset by e-mail token, security questions and SMS are OFF; see the guide to enable the e-mail reset.
# OpenLDAP listens on 127.0.0.1 and ldapi:/// only; anonymous bind is disabled.
# The HTTPS certificate is self-signed and unique to this instance: compare its SHA-256 fingerprint (TLS_SHA256), or
# replace it with a CA-issued certificate for your own name. Your own name: sudo self-service-password-set-domain ssp.example.com
# Your own base DN (while ou=people and ou=groups are empty): sudo self-service-password-set-basedn dc=corp,dc=example,dc=com
# User guide: https://www.cloudimg.co.uk/guides/ltb-self-service-password-1-8-2-on-ubuntu-24-04-aws/
The file also holds three passwords, each unique to your instance; read the one you need when you need it, for example sudo grep '^LDAP_ADMIN_PASSWORD=' /root/self-service-password-credentials.txt:
| Line | What it is for |
|---|---|
LDAP_ADMIN_PASSWORD |
The directory administrator cn=admin,<base DN>: adding users, setting their first password. sudo cloudimg-ldap uses it for you (Step 6). |
LDAP_CONFIG_PASSWORD |
The directory's own configuration (cn=admin,cn=config), for ldapmodify on cn=config. Root can also use sudo ldapmodify -Y EXTERNAL -H ldapi:///. |
SSP_SERVICE_PASSWORD |
The portal's service account (SSP_SERVICE_DN). The portal reads it from /etc/self-service-password/secrets.inc.php; you do not need it day to day. |
The portal's settings are in /etc/self-service-password/: config.inc.local.php (the options, readable, no secrets), secrets.inc.php (the service password and the token keyphrase, readable by root and the portal only) and site.inc.php (this instance's name):
sudo ls -l /etc/self-service-password/ | awk 'NR>1{print $1, $3, $4, $NF}'
-rw-r--r-- root root config.inc.local.php
-rw-r----- root ssp secrets.inc.php
-rw-r--r-- root root site.inc.php
The certificate is self-signed and unique to your instance. Do not add it to a trust store: compare the fingerprint your browser shows with TLS_SHA256, or replace the certificate with one from a certificate authority (Step 11).
Step 5: Run the self-test
sudo self-service-password-selftest
It adds a throwaway directory user, then over HTTPS (pinned to the instance's own certificate) checks that a password change with a wrong old password is refused, that the change with the right old password succeeds, that the new password binds and is stored as an Argon2 hash while the old one no longer works, and that anonymous LDAP is refused; it removes the user again:
round trip OK: over pinned HTTPS a wrong old password is refused; the change with the right old password succeeds; the new password binds ({ARGON2}), the old one fails; anonymous LDAP refused
SSP_SELFTEST_OK
Step 6: Add a directory user
Users live in ou=people; their login in the portal is their uid. cloudimg-ldap runs any of the ldap* tools bound as the directory administrator, reading the password from the credentials file, so it never appears on a command line or in your shell history. Add a user:
cat > alice.ldif <<'EOF'
dn: uid=alice,ou=people,dc=example,dc=com
objectClass: inetOrgPerson
uid: alice
cn: Alice Example
sn: Example
mail: alice@example.com
EOF
sudo cloudimg-ldap ldapadd -f alice.ldif
adding new entry "uid=alice,ou=people,dc=example,dc=com"
Give the user a first password. This writes a random one to a root-only file (without a trailing newline: ldappasswd -T uses the whole file as the password) and sets it:
sudo sh -c 'umask 077; openssl rand -base64 18 | tr -d "\n" > /root/alice-initial-password.txt'
sudo cloudimg-ldap ldappasswd -T /root/alice-initial-password.txt uid=alice,ou=people,dc=example,dc=com
Hand the password over by a safe channel (sudo cat /root/alice-initial-password.txt), then delete the file with sudo shred -u /root/alice-initial-password.txt. The user now chooses their own password in the portal. To load many users, put them in one LDIF file; to remove a user, sudo cloudimg-ldap ldapdelete uid=alice,ou=people,dc=example,dc=com.
Step 7: Change the password in the browser
Open https://<your-instance-public-DNS-name>/ (the SSP_URL from Step 4). Your browser warns about the self-signed certificate: check that the fingerprint it shows matches TLS_SHA256, then continue. The page asks for the login, the old password and the new password twice, and shows the password policy (at least 12 characters, different from the login and from the old password):

A wrong old password is refused:

A new password that breaks the policy is marked and not submitted (the server refuses it as well):

With the right old password and a new password that meets the policy, the password is changed:

The old password stops working at once, in the portal and in the directory.
Step 8: Check the change from the command line
The directory stores the new password as an Argon2 hash:
sudo cloudimg-ldap ldapsearch -LLL -o ldif-wrap=no -b uid=alice,ou=people,dc=example,dc=com -s base userPassword | sed -n 's/^userPassword:: //p' | base64 -d | cut -c1-15; echo
{ARGON2}$argon2
The user can bind with the new password (you are prompted for it):
ldapwhoami -x -H ldap://127.0.0.1:389 -D uid=alice,ou=people,dc=example,dc=com -W
dn:uid=alice,ou=people,dc=example,dc=com
Every attempt is written to the portal's audit log, one JSON line each, with the client's real address (never a header the client sent) and no password:
sudo tail -n 1 /var/log/self-service-password/audit.log | python3 -c 'import json,sys; d=json.loads(sys.stdin.read()); print(d["action"], d["result"], d["user_dn"])'
change passwordchanged uid=alice,ou=people,dc=example,dc=com
Anonymous access to the directory is refused (the command succeeds only if the anonymous bind fails):
! ldapwhoami -x -H ldap://127.0.0.1:389 && echo "anonymous bind refused"
ldap_bind: Inappropriate authentication (48)
additional info: anonymous bind disallowed
anonymous bind refused
Step 9: What is served over HTTPS
Only the portal page (/, /index.php) and its stylesheets, scripts, fonts and images are served. Its configuration, secrets, libraries, templates and the individual PHP files are not:
for p in /conf/config.inc.local.php /config.inc.local.php /secrets.inc.php /lib/functions.inc.php /templates/change.tpl /change.php /; do printf '%-28s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code}' --cacert /etc/ssl/self-service-password/server.crt https://$(cat /etc/cloudimg/self-service-password-domain)$p)"; done
/conf/config.inc.local.php 404
/config.inc.local.php 404
/secrets.inc.php 404
/lib/functions.inc.php 404
/templates/change.tpl 404
/change.php 404
/ 200
The methods the portal offers:
sudo grep -E '^\$(use_change|use_tokens|use_questions|use_sms|change_sshkey|use_attributes|use_restapi) =' /etc/self-service-password/config.inc.local.php
$use_change = true;
$use_tokens = false;
$use_questions = false;
$use_sms = false;
$change_sshkey = false;
$use_attributes = false;
$use_restapi = false;
To limit who can reach the portal at all, restrict port 443 in the security group, or list your networks in /etc/nginx/cloudimg/ssp-access.conf (allow 203.0.113.0/24; lines followed by deny all;), then run sudo nginx -t && sudo systemctl reload nginx.
Step 10: Enable password reset by e-mail (optional)
With an SMTP relay you can let users who forgot their password request a reset link by e-mail. The link goes to the address stored in the user's mail attribute, is built from the instance's configured name (never from the request), works once and expires after one hour; it is encrypted with your instance's own keyphrase. A login and e-mail pair that does not match sends nothing and gets the same neutral answer.
- Allow outbound SMTP from the instance to your relay (security group egress and any network ACL).
- Put the relay password in the secrets file, which only root and the portal can read:
sudo nano /etc/self-service-password/secrets.inc.phpand add$mail_smtp_pass = '<your-smtp-password>';. - Add the settings to
/etc/self-service-password/config.inc.local.php(as root), below the existing lines:
$use_tokens = true;
$mail_from = "password-reset@example.com";
$mail_smtp_host = 'smtp.example.com';
$mail_smtp_port = 587;
$mail_smtp_secure = 'tls';
$mail_smtp_auth = true;
$mail_smtp_user = 'password-reset@example.com';
- Reload PHP-FPM:
sudo systemctl reload php8.3-fpm. The portal now shows an Email tab; the link in each mail points toSSP_URL.
Security questions and SMS codes are configured the same way (see /usr/share/self-service-password/conf/config.inc.php and the Self Service Password documentation). They are weaker than an e-mail link: enable them only if you need them.
Step 11: Use your own domain and certificate
Point a DNS name at the instance, then:
sudo self-service-password-set-domain ssp.example.com
It sets nginx's server name (requests for any other name are dropped), the name the portal is given and uses in reset links, and a new self-signed certificate for that name. To use a certificate from a certificate authority instead, put the certificate chain and key in /etc/ssl/self-service-password/server.crt and server.key (key mode 0600), then sudo nginx -t && sudo systemctl reload nginx. With Let's Encrypt, for example: open port 80 to the internet, sudo apt-get install -y certbot, sudo systemctl stop nginx, sudo certbot certonly --standalone -d ssp.example.com, sudo systemctl start nginx, then copy fullchain.pem and privkey.pem from /etc/letsencrypt/live/ssp.example.com/ into place. The current certificate:
openssl x509 -in /etc/ssl/self-service-password/server.crt -noout -subject -ext basicConstraints,keyUsage,extendedKeyUsage,subjectAltName
subject=CN = ec2-18-212-72-80.compute-1.amazonaws.com, O = cloudimg
X509v3 Subject Alternative Name:
DNS:ec2-18-212-72-80.compute-1.amazonaws.com, DNS:ip-172-31-86-194.ec2.internal, IP Address:18.212.72.80, IP Address:172.31.86.194, IP Address:127.0.0.1
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication
Step 12: Use your own directory base
While ou=people and ou=groups are empty, you can move the directory to your own base, for example dc=corp,dc=example,dc=com:
sudo self-service-password-set-basedn dc=corp,dc=example,dc=com
It re-creates the empty directory under the new base with the same instance passwords, points the portal at it and updates the credentials file. It refuses once a user or group exists, and rolls back if anything fails. To start with your own base, use the preset file from Step 1 instead.
Step 13: Check the security posture
Every service is blocked from the instance metadata service (the AMI also requires IMDSv2):
systemctl show -p IPAddressDeny slapd php8.3-fpm nginx self-service-password-firstboot
IPAddressDeny=168.63.129.16/32 ::ffff:169.254.169.254/128 169.254.169.254/32 fd00:ec2::254/128
IPAddressDeny=168.63.129.16/32 ::ffff:169.254.169.254/128 169.254.169.254/32 fd00:ec2::254/128
IPAddressDeny=168.63.129.16/32 ::ffff:169.254.169.254/128 169.254.169.254/32 fd00:ec2::254/128
IPAddressDeny=168.63.129.16/32 ::ffff:169.254.169.254/128 169.254.169.254/32 fd00:ec2::254/128
The portal makes outbound connections only to what you configure: the directory, an SMTP relay, an SMS gateway or a captcha service. All of them run inside PHP-FPM, which cannot reach the metadata service.
The versions on your instance:
grep -o '^\$version = "[0-9.]*"' /usr/share/self-service-password/htdocs/index.php; dpkg-query -W -f='${Package} ${Version}\n' slapd nginx php8.3-fpm
$version = "1.8.2"
nginx 1.24.0-2ubuntu7.19
php8.3-fpm 8.3.6-0ubuntu0.24.04.13
slapd 2.6.10+dfsg-0ubuntu0.24.04.1
Your security group should allow 22 from your own networks, 443 (and 80 for the redirect) from your users' networks only.
Step 14: Your data and backups
The directory lives in /var/lib/ldap, the portal's settings in /etc/self-service-password. A directory listing:
sudo slapcat -n 1 | grep '^dn:'
dn: dc=example,dc=com
dn: ou=people,dc=example,dc=com
dn: ou=groups,dc=example,dc=com
dn: ou=services,dc=example,dc=com
dn: cn=self-service-password,ou=services,dc=example,dc=com
dn: uid=alice,ou=people,dc=example,dc=com
For a full backup run sudo slapcat -n 0 -l config.ldif, sudo slapcat -n 1 -l data.ldif and sudo tar -C /etc -czf ssp-settings.tgz self-service-password, and keep them with /root/self-service-password-credentials.txt somewhere safe: they contain password hashes and the portal's secrets. EBS snapshots of the root volume are the simplest whole-instance backup.
Step 15: Licences and source
Self Service Password is licensed under the GNU General Public License, version 3 or later; Smarty under the GNU Lesser General Public License, version 3 or later; the bundled PHP and JavaScript libraries carry their own open source licences (listed in composer-installed.json):
ls /usr/share/doc/cloudimg-self-service-password/
LICENSE-self-service-password-GPL-3.0.txt
LICENSE-smarty-LGPL-3.0.txt
SOURCE.txt
composer-installed.json
smarty-libs.sha256
ssp-tree.sha256
SOURCE.txt names the upstream release tarballs and their checksums, and lists what cloudimg leaves out: upstream's development sample configuration, the background photographs (published under the Unsplash licence, which is not an open source licence; the page uses a plain background), and three fonts of the bundled captcha library that are not open source (replaced by copies of its open source fonts; the captcha is off). ssp-tree.sha256 lists every installed file with its checksum. OpenLDAP, nginx and PHP are Ubuntu packages (/usr/share/doc/<package>/copyright).
Step 16: Security notes
- Advisories. No published advisory affects Self Service Password 1.8.2; it carries upstream's fixes for the rate limiter on failed answers and the SMS code attempt limit. The bundled PHP and JavaScript libraries and Smarty 4.5.8 had no known advisory at build time.
- PHP from Ubuntu universe.
php8.3-fpmis built from Ubuntu's mainphp8.3source but published in the universe component. It receives the same security updates as the rest of PHP 8.3, and the image was checked for fixes available only through Ubuntu Pro (none). - Limits. Password submissions are limited per client address by nginx (10 a minute), and the portal allows 5 attempts per user and 10 per client address in 5 minutes. Someone who knows a login can therefore delay that user's change for a few minutes; they cannot change the password without the old one.
- The administrator is the directory root.
cn=adminis not subject to access rules. Keep its password safe and use it only for administration.
Support
cloudimg provides support for this image by e-mail at support@cloudimg.co.uk. Self Service Password is documented at https://self-service-password.readthedocs.io/. LDAP Tool Box (LTB) and Self Service Password are names of the LDAP Tool Box project; OpenLDAP is a registered trademark of the OpenLDAP Foundation. These names are used only to identify the software; cloudimg is not affiliated with or endorsed by the LDAP Tool Box project, Worteks or the OpenLDAP Foundation.