SimpleSAMLphp 2
Overview
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.
This guide covers the deployment and verification of SimpleSAMLphp 2.5.3.1 on Microsoft Azure, using SecureAnyCloud's preconfigured virtual machine image from the Azure Marketplace.
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 strict about this by design, and this image leans on that. The software ships a default secretsalt and refuses to run until it is changed. 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 you get:
- 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, preregistered 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, and 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. Pass cloud-init user data when you create the VM:
#cloud-config write_files: - path: /etc/sac/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.
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 SecureAnyCloud.
- 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 the VM.
Option B: Azure CLI
az group create --name simplesamlphp-rg --location eastus
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:simplesamlphp-ubuntu-24-04:default:latest
az vm create \
--resource-group simplesamlphp-rg \
--name simplesamlphp-vm \
--image sac1647283583153: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 1002Wait 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. It then writes the configuration. This normally takes well under a minute.
Connect over SSH and confirm that it has finished:
ssh azureuser@<vm-ip>
sudo test -f /var/lib/sac/data/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/metadataThe output looks like this:
active active PHP 8.3.6 (cli) (built: Sep 2 2026 12:56:02) (NTS) 200Now 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.txtIt gives you the console URL, the administrator password, the demonstration user's password, and this instance's identity provider and service provider entity IDs.
Prove a complete SAML round trip
Do this 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-spYour browser warns about the certificate. This 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 password of the admin account first. The username is fixed 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 and posts it back to the service provider. The test bench renders the attributes it received:
That attribute table is the proof. The user authenticated at the identity provider, and the identity provider signed an assertion about them. The service provider validated the signature and accepted it, and read out those attributes.
You can run the same round trip from the command line. This checks the decoded assertion rather than the page, so use it for a monitoring check:
sudo bash -c 'C=/root/simplesamlphp-credentials.txt SAC_SSP_ADMIN_PW="$(sed -n "s/^SSP_ADMIN_PASSWORD=//p" $C)" \
SAC_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)"'The output looks like this:
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_OKThe script exits non-zero on failure. 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.
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/metadataThat URL is deliberately public, because 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.xmlTo trust a service provider in return, add its entry to /etc/simplesamlphp/metadata/saml20-sp-remote.php. The admin console's Federation > Metadata converter turns 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.phpKeep 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.phpRemove 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-fpmPoint the identity provider at your own directory
The image ships a single demonstration end user, so that 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. The ones you are most likely to want are already on disk, but not enabled:
ls /opt/simplesamlphp/modulesadmin consentAdmin debugsp exampleauth metarefresh radius sqlauth authorize core discopower ldap multiauth saml consentThe ldap and sqlauth modules are authentication sources. The radius module 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 not installed, because nothing in the shipped configuration uses it. Install it:
sudo apt-get update && sudo apt-get install -y php8.3-ldap php -m | grep -x ldap
sudo systemctl reload php8.3-fpmTurn 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-userpassA 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-fpmKeep 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.phpThen rerun the test bench from Step 3. It now authenticates against your directory rather than the demonstration account.
Confirm the demonstration account is gone
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 need a module outside the bundled fifteen, install it with Composer.
Ubuntu 24.04 packages Composer 2.2, whose plugin API is too old for SimpleSAMLphp 2.5. SimpleSAMLphp's 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. Run it as root with COMPOSER_ALLOW_SUPERUSER=1, because the application tree is root-owned.
Production hardening
Replace the self-signed TLS certificate
First boot generates a self-signed certificate so that 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/sac/tls/ssp.crt
sudo cp /etc/letsencrypt/live/sso.example.org/privkey.pem /etc/sac/tls/ssp.key
sudo chown root:www-data /etc/sac/tls/ssp.key && sudo chmod 0640 /etc/sac/tls/ssp.key
sudo nginx -t && sudo systemctl reload nginxCertbot'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, remember that this changes your entity ID and every service provider must be told. Set the hostname and rerun first boot:
echo 'SSP_HOSTNAME="sso.example.org"' | sudo tee /etc/sac/simplesamlphp.conf
sudo rm -f /var/lib/sac/data/simplesamlphp-firstboot.done
sudo systemctl restart simplesamlphp-firstboot.service
sudo systemctl restart php8.3-fpm nginxUse 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. It writes the new values to /root/simplesamlphp-credentials.txt.
Changing the salt invalidates every existing session. You must redistribute the new signing key 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 that 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 hashConfirm the stored hash accepts your new password and nothing else:
sudo /usr/local/sbin/simplesamlphp-hashcheck.php 123 admin "" passwordEvery 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_DONEWhat 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/sandbox404Monitoring and patching
Unattended security upgrades are enabled. After a reboot, check the stack:
systemctl is-active php8.3-fpm nginx
sudo /usr/local/sbin/simplesamlphp-port-check.shThe output looks like this:
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.logYour 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 -datesTroubleshooting
The browser shows "State information lost". SimpleSAMLphp keeps the pending authentication request in the PHP session and passes only its identifier in the URL. A stale bookmark, a back-button press or blocked cookies produce this error. 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 rerun 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/sac/sac-firstboot.log 2>/dev/null \
|| echo "no first boot log yet - the unit has not run on this machine"Support
SecureAnyCloud images come with 24/7 support. Raise a ticket at secureanycloud.com with your offer name, region and VM size. If the problem is authentication-related, include the output of the round trip command in Step 3.
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.txtSecureAnyCloud 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.
Deploy on Azure
Launch SimpleSAMLphp 2 on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com