SimpleSAMLphp 2 on Ubuntu 24.04 LTS on Azure User Guide
Overview
This guide covers the deployment and verification of SimpleSAMLphp 2.5.3.1 on Microsoft Azure using cloudimg's pre configured virtual machine image from the Azure Marketplace.
SimpleSAMLphp is a PHP implementation of SAML 2.0 that is both an identity provider and a service provider. It is the workhorse of single sign on in education and mid market enterprise: you can stand it up as the identity provider your own users sign in to, use it as a service provider in front of an application that does not speak SAML itself, or run it as a proxy between the two. It is open source, published under the GNU Lesser General Public License version 2.1 or later.
Every secret on this image is generated on your own virtual machine, at first boot. That matters more for a SAML implementation than for almost any other kind of software. An identity provider signs assertions with a private key, and every service that trusts your identity provider trusts that key. If the key were baked into the image, every customer who deployed it would hold the same one, and any of them could forge assertions accepted by any other's services. So this image ships no configuration at all: no config.php, no authsources.php, no signing key, no TLS key, no metadata. It ships the application and a set of configuration templates, and your first boot generates the rest.

SimpleSAMLphp is unusually strict about this by design, and this image leans on that. The software ships a placeholder secretsalt and refuses to run until it is changed, and its administrator password is stored as a hash produced by its own bin/pwgen.php rather than as plain text. On this image that hash is argon2id, generated on your machine, and the plain text password is written once to a root only file and kept nowhere else.
What's included:
- SimpleSAMLphp 2.5.3.1, released 9 August 2026, installed from the official full release tarball with its SHA-256 pinned and verified at build time
- PHP 8.3 from Ubuntu 24.04, satisfying SimpleSAMLphp 2.5's requirement of PHP 8.3 or newer. No third party PPA is used
- nginx terminating TLS on port 443 with a certificate generated on your machine at first boot, passing PHP to php-fpm over a UNIX socket
- A working SAML 2.0 identity provider with a signing key minted on your machine
- The built in service provider test bench, pre registered against that identity provider, so you can prove a complete round trip within a minute of the machine booting
simplesamlphp-roundtrip.py, which drives the whole sign in flow and checks the assertion that comes back- Ubuntu 24.04 LTS, fully patched at build time, with unattended security upgrades enabled
Exposed ports: 22 and 443, and nothing else. php-fpm listens on a UNIX socket, not a TCP port. There is deliberately no port 80 listener.

Before you deploy: choose your hostname
A SAML entity has an entity ID, and it is a permanent identifier. Every service provider you federate with records it, and the endpoints in your published metadata are built from your hostname. Changing it later means re keying and re registering with everyone you have exchanged metadata with.
If the machine's public IP address is fine for now, do nothing: first boot uses it automatically. If you already know the DNS name this instance will answer on, set it before the first boot by passing cloud init user data when you create the VM:
#cloud-config
write_files:
- path: /etc/cloudimg/simplesamlphp.conf
permissions: '0644'
content: |
SSP_HOSTNAME="sso.example.org"
First boot reads that file before it generates anything, so your entity IDs, your metadata endpoints and your TLS certificate all carry the right name from the outset.
Step 1: Deploy the Virtual Machine
Option A: Azure Portal
- In the Azure Portal, choose Create a resource and search the Marketplace for SimpleSAMLphp 2 on Ubuntu 24.04 LTS by cloudimg.
- Select the image and choose Create.
- Pick a resource group, a region and a VM size.
Standard_B2s(2 vCPU, 4 GiB) is the recommended size and is comfortable for this stack. - Under Administrator account, choose SSH public key and supply your own key. The username can be anything; this guide uses
azureuser. - Under Inbound port rules, allow SSH (22) and HTTPS (443).
- Review and create.
Option B: Azure CLI
az group create --name simplesamlphp-rg --location eastus
az vm create \
--resource-group simplesamlphp-rg \
--name simplesamlphp-vm \
--image cloudimg:simplesamlphp-ubuntu-24-04:default:latest \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
# then open the two ports this image uses
az vm open-port --resource-group simplesamlphp-rg --name simplesamlphp-vm --port 22 --priority 1001
az vm open-port --resource-group simplesamlphp-rg --name simplesamlphp-vm --port 443 --priority 1002
Step 2: Wait for the first boot to finish
First boot generates the secret salt, the administrator password and its hash, the demonstration user's password, the SAML signing key pair and the TLS certificate, then writes the configuration. It normally takes well under a minute.
Connect over SSH and confirm it has finished:
ssh azureuser@<vm-ip>
sudo test -f /var/lib/cloudimg/simplesamlphp-firstboot.done && echo "first boot complete"
Check the services and the published metadata:
systemctl is-active php8.3-fpm nginx
php -v | head -1
curl -sk -o /dev/null -w '%{http_code}\n' https://localhost/simplesaml/module.php/saml/idp/metadata
Expected output:
active
active
PHP 8.3.6 (cli) (built: Sep 2 2026 12:56:02) (NTS)
200

Now read the per VM details file. It is mode 0600 and owned by root, and it is the only place the plain text passwords exist:
sudo cat /root/simplesamlphp-credentials.txt
It gives you the console URL, the administrator password, the demonstration user's password, and this instance's identity provider and service provider entity IDs.
Step 3: Prove a complete SAML round trip
This is the thing worth doing first, because it exercises the whole product end to end: the administrator login, the service provider, the identity provider, and the signed assertion that passes between them.
Open the test bench in a browser:
https://<vm-ip>/simplesaml/module.php/admin/test/default-sp
Your browser will warn about the certificate. That is expected — first boot generated a self signed certificate for your machine, and Step 6 covers replacing it with a real one.
SimpleSAMLphp asks for the administrator password first. The username is fixed to admin for this login, so only the password field is editable — use SSP_ADMIN_PASSWORD from the credentials file.

The test bench then starts a real SAML authentication against this instance's own identity provider, which asks for an end user's credentials. Use SSP_DEMO_USERNAME and SSP_DEMO_PASSWORD from the same file.

The identity provider issues a signed assertion, posts it back to the service provider, and the test bench renders the attributes it received:

That attribute table is the proof. The user authenticated at the identity provider, the identity provider signed an assertion about them, the service provider validated the signature and accepted it, and those are the attributes it read out.
You can run the same round trip from the command line. This checks the decoded assertion rather than the page, so it is what to reach for in a monitoring check:
sudo bash -c 'C=/root/simplesamlphp-credentials.txt
CLOUDIMG_SSP_ADMIN_PW="$(sed -n "s/^SSP_ADMIN_PASSWORD=//p" $C)" \
CLOUDIMG_SSP_USER_PW="$(sed -n "s/^SSP_DEMO_PASSWORD=//p" $C)" \
python3 /usr/local/sbin/simplesamlphp-roundtrip.py \
"$(sed -n "s/^SSP_HOSTNAME=//p" $C)" \
--demo-user "$(sed -n "s/^SSP_DEMO_USERNAME=//p" $C)"'
Expected output:
PASS the test bench refused an unauthenticated request and asked for a login
PASS the administrator login was accepted
PASS an end-user login form was presented and submitted
PASS a SAMLResponse was POSTed back to the service provider
PASS SAML status is Success urn:oasis:names:tc:SAML:2.0:status:Success
PASS an Assertion was issued
PASS the response is signed
PASS an AuthnStatement is present
PASS the Issuer is this instance's identity provider
PASS the audience is the requesting service provider
PASS a NameID was issued
PASS the assertion carries the demonstration user's attributes
PASS the test bench rendered the authenticated user's attributes
SIMPLESAMLPHP_SAML_ROUNDTRIP_OK

The script exits non zero on failure, but check for the SIMPLESAMLPHP_SAML_ROUNDTRIP_OK marker rather than relying on the exit status alone — SimpleSAMLphp answers a refused login with HTTP 200 and the refusal in the page body, so a status code proves nothing here.
Step 4: Give your metadata to a service provider
The Federation tab lists the entities this instance hosts and trusts, with a link to each one's metadata:

Your identity provider's metadata is served at:
https://<your-hostname>/simplesaml/module.php/saml/idp/metadata
That URL is deliberately public — service providers and federation operators need to fetch it without a credential. Everything under the admin console requires the administrator password.
Fetch it and hand it to the service provider you want to federate with:
curl -sk https://localhost/simplesaml/module.php/saml/idp/metadata > my-idp-metadata.xml
To trust a service provider in return, add its entry to /etc/simplesamlphp/metadata/saml20-sp-remote.php. The admin console's Federation → Metadata converter will turn a service provider's XML metadata into the PHP array SimpleSAMLphp expects, which saves a lot of hand editing:
sudo nano /etc/simplesamlphp/metadata/saml20-sp-remote.php
Keep the file's ownership and mode as they are (root:www-data, 0640) and check it parses before relying on it:
sudo php -l /etc/simplesamlphp/metadata/saml20-sp-remote.php
Remove the test bench when you are done with it
The built in service provider exists to prove the appliance works. It is registered in saml20-sp-remote.php as the only entity with assertion encryption disabled, so that the appliance can read back its own assertion. Once you have registered your own service providers, remove it:
sudo nano /etc/simplesamlphp/metadata/saml20-sp-remote.php # delete the default-sp entry
sudo nano /etc/simplesamlphp/authsources.php # delete the default-sp authsource
sudo systemctl reload php8.3-fpm
Step 5: Point the identity provider at your own directory
The image ships a single demonstration end user so the round trip in Step 3 works out of the box. It is not an authentication source for production: it is SimpleSAMLphp's exampleauth module, and it stores its password in authsources.php.
You do not need to install anything to replace it. The official full release this image is built from bundles fifteen modules, and the ones you are most likely to want are already on disk — they are simply not enabled:
ls /opt/simplesamlphp/modules
admin consentAdmin debugsp exampleauth metarefresh radius sqlauth
authorize core discopower ldap multiauth saml
consent
ldap and sqlauth are authentication sources, radius authenticates against a RADIUS server, and metarefresh keeps federation metadata up to date automatically.
Enable the LDAP authentication source
PHP's LDAP extension is the one thing that is genuinely not installed, because nothing in the shipped configuration uses it:
sudo apt-get update && sudo apt-get install -y php8.3-ldap
php -m | grep -x ldap
sudo systemctl reload php8.3-fpm
Turn the module on in /etc/simplesamlphp/config.php by adding 'ldap' => true to the module.enable array, then add an LDAP authentication source to /etc/simplesamlphp/authsources.php:
sudo nano /etc/simplesamlphp/config.php # 'ldap' => true, and 'exampleauth' => false
sudo nano /etc/simplesamlphp/authsources.php # add your LDAP source, remove example-userpass
A minimal LDAP source looks like this — put it in authsources.php alongside admin:
'my-ldap' => [
'ldap:Ldap',
'connection_string' => 'ldaps://ldap.example.org',
'search.enable' => true,
'search.base' => ['ou=people,dc=example,dc=org'],
'search.username' => 'cn=readonly,dc=example,dc=org',
'search.password' => 'the-bind-password',
'attributes' => ['cn', 'sn', 'givenName', 'mail', 'uid'],
],
Point the identity provider at it and reload:
sudo nano /etc/simplesamlphp/metadata/saml20-idp-hosted.php # set 'auth' => 'my-ldap'
sudo systemctl reload php8.3-fpm
Keep the files' ownership and mode as they are (root:www-data, 0640) and check each one parses before relying on it:
sudo php -l /etc/simplesamlphp/config.php
sudo php -l /etc/simplesamlphp/authsources.php
sudo php -l /etc/simplesamlphp/metadata/saml20-idp-hosted.php
Then re run the test bench from Step 3 — it will now authenticate against your directory rather than the demonstration account.
Confirm the demonstration account is gone before you go live
sudo php -r '$config=null; require "/etc/simplesamlphp/config.php"; var_dump($config["module.enable"]["exampleauth"] ?? false);'
This must print bool(false) once you have switched over.
Adding a module that is not bundled
If you do need a module outside the bundled fifteen, install it with Composer. Note that Ubuntu 24.04 packages Composer 2.2, whose plugin API is too old for SimpleSAMLphp 2.5 — its own installer plugins require plugin API ~2.9, so apt Composer downloads a module without ever wiring it into the application. Use the official installer, and run it as root with COMPOSER_ALLOW_SUPERUSER=1 because the application tree is root owned.
Step 6: Production hardening
Replace the self signed TLS certificate
First boot generates a self signed certificate so the machine is usable immediately. Replace it with a real one — with a DNS name in place, Let's Encrypt is straightforward:
sudo apt-get update && sudo apt-get install -y certbot
sudo certbot certonly --standalone -d sso.example.org
sudo cp /etc/letsencrypt/live/sso.example.org/fullchain.pem /etc/cloudimg/tls/ssp.crt
sudo cp /etc/letsencrypt/live/sso.example.org/privkey.pem /etc/cloudimg/tls/ssp.key
sudo chown root:www-data /etc/cloudimg/tls/ssp.key && sudo chmod 0640 /etc/cloudimg/tls/ssp.key
sudo nginx -t && sudo systemctl reload nginx
Certbot's standalone mode needs port 80 briefly; open it in your network security group for the issuance and close it again afterwards.
Change the hostname, or regenerate every secret
If you need to move to a real hostname after first boot — remembering that this changes your entity ID and every service provider must be told — set the hostname and re run first boot:
echo 'SSP_HOSTNAME="sso.example.org"' | sudo tee /etc/cloudimg/simplesamlphp.conf
sudo rm -f /var/lib/cloudimg/simplesamlphp-firstboot.done
sudo systemctl restart simplesamlphp-firstboot.service
sudo systemctl restart php8.3-fpm nginx
Use restart, not start. The first boot unit is a oneshot with RemainAfterExit=yes, so after its first run it stays active and systemctl start silently does nothing.
This regenerates the salt, the administrator password, the demonstration password, the SAML signing key and the TLS certificate, and writes the new values to /root/simplesamlphp-credentials.txt. Changing the salt invalidates every existing session, and the new signing key must be redistributed to every service provider that trusts you.
Rotate the administrator password only
To change just the administrator password, hash the new one with SimpleSAMLphp's own tool and put the hash in the configuration. Pipe the password in on standard input so it never appears on a command line, where it would be visible in /proc and copied into auth.log:
printf '%s\n' 'your-new-password' | sudo php /opt/simplesamlphp/bin/pwgen.php
sudo nano /etc/simplesamlphp/config.php # replace auth.adminpassword with the printed hash
Confirm the stored hash accepts your new password and nothing else:
sudo /usr/local/sbin/simplesamlphp-hashcheck.php 123 admin "" password
Every candidate must report REJECTS:
HASH_ALGO argon2id
HASH_IS_DEFAULT_LITERAL no
CANDIDATE 123 REJECTS
CANDIDATE admin REJECTS
CANDIDATE (blank) REJECTS
CANDIDATE password REJECTS
HASHCHECK_DONE
What is deliberately not reachable
The administrator console, the diagnostics page, phpinfo, the federation and module listing and the metadata converter all require the administrator password. config.php, authsources.php and the signing key live in /etc/simplesamlphp, outside the web server's document root, and are not served at all.
SimpleSAMLphp also ships a sandbox demonstration page which upstream does not put behind a login. This image refuses it at nginx, so it returns 404:
curl -sk -o /dev/null -w '%{http_code}\n' https://localhost/simplesaml/module.php/admin/sandbox
404
Step 7: Monitoring and patching
Unattended security upgrades are enabled. Check the stack after a reboot with:
systemctl is-active php8.3-fpm nginx
sudo /usr/local/sbin/simplesamlphp-port-check.sh
Expected output:
active
active
PORTS_OK off-box TCP = {22 443}
SimpleSAMLphp logs to /var/log/simplesamlphp/simplesamlphp.log; every authentication attempt, successful or not, is recorded there:
sudo tail -5 /var/log/simplesamlphp/simplesamlphp.log
Your signing certificate is valid for three years from first boot. Check its expiry:
sudo openssl x509 -in /etc/simplesamlphp/cert/saml-signing.crt -noout -subject -dates
Troubleshooting
The browser shows "State information lost". SimpleSAMLphp keeps the pending authentication request in the PHP session and passes only its identifier in the URL, so a stale bookmark, a back button press or blocked cookies produce this. Start again from the beginning of the flow.
Redirects go to the wrong address. The URLs come from baseurlpath in /etc/simplesamlphp/config.php, which first boot set from the address it resolved. If that address was wrong, fix the hostname and re run first boot as shown in Step 6.
A service provider rejects your assertion. Its copy of your metadata is probably stale — most commonly after a hostname change or a key regeneration. Send it your current metadata from /simplesaml/module.php/saml/idp/metadata.
First boot did not finish. Read its log:
sudo journalctl -u simplesamlphp-firstboot.service --no-pager | tail -20
sudo tail -20 /var/log/cloudimg-firstboot.log 2>/dev/null \
|| echo "no first boot log yet - the unit has not run on this machine"
Support
cloudimg images come with 24/7 support. Raise a ticket at www.cloudimg.co.uk with your offer name, region and VM size, and include the output of the round trip command in Step 3 if the problem is authentication related.
Licence
SimpleSAMLphp is published under the GNU Lesser General Public License, version 2.1 or later. The licence text shipped with the release is copied into the image at /usr/share/doc/simplesamlphp/LICENSE, alongside COPYING and an inventory of every bundled dependency's licence at /usr/share/doc/simplesamlphp/BUNDLED-LICENCES.txt.
As COPYING notes, some of the bundled libraries carry other terms: the release this image ships bundles 77 packages across five licences — MIT, BSD-3-Clause, LGPL-2.1-only, LGPL-2.1-or-later and LGPL-3.0-or-later. You can read the inventory on the machine:
cat /usr/share/doc/simplesamlphp/BUNDLED-LICENCES.txt
cloudimg charges for the packaging, the per VM secret generation, the paired deployment guide and the support, not for the software itself. Your use of SimpleSAMLphp remains governed by its own licence.