S2
Application Infrastructure Azure

SimpleSAMLphp 2 on Ubuntu 24.04 LTS on Azure User Guide

| Product: SimpleSAMLphp 2 on Ubuntu 24.04 LTS on Azure

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.

Salt, admin hash and SAML signing key, all minted on this VM at first boot

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.

Exposed ports and the per-VM details file

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

  1. In the Azure Portal, choose Create a resource and search the Marketplace for SimpleSAMLphp 2 on Ubuntu 24.04 LTS by cloudimg.
  2. Select the image and choose Create.
  3. 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.
  4. Under Administrator account, choose SSH public key and supply your own key. The username can be anything; this guide uses azureuser.
  5. Under Inbound port rules, allow SSH (22) and HTTPS (443).
  6. 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

SimpleSAMLphp services and published SAML metadata

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 administrator sign in

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 authenticating an end user

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

The completed SAML round trip, showing the returned assertion attributes

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

A real sign in and the signed SAML 2.0 assertion this instance issued

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:

This instance's own SAML 2.0 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.