Security AWS

LTB Self Service Password 1.8.2 on AWS User Guide

| Product: LTB Self Service Password 1.8.2 on Ubuntu 24.04 on AWS

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=config password, 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=people and 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) with ou=people, ou=groups and ou=services only: 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.1 and 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):

The LTB Self Service Password 1.8.2 change-password page with the login, old password and new password fields and the password policy

A wrong old password is refused:

Self Service Password refusing a wrong old password: Login and/or password incorrect

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

The password policy marking a seven-character new password as too short

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

Self Service Password confirming: Your password was 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.

  1. Allow outbound SMTP from the instance to your relay (security group egress and any network ACL).
  2. Put the relay password in the secrets file, which only root and the portal can read: sudo nano /etc/self-service-password/secrets.inc.php and add $mail_smtp_pass = '<your-smtp-password>';.
  3. 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';
  1. Reload PHP-FPM: sudo systemctl reload php8.3-fpm. The portal now shows an Email tab; the link in each mail points to SSP_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-fpm is built from Ubuntu's main php8.3 source 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=admin is 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.