SimpleSAMLphp is the ideal choice as a SAML 2.0 IdP for SUNY Federation. As it will be the core authentication mechanism for SUNY services, high-availability and load-balancing are an important consideration. Here is one approach to achieving this with HAProxy and Keepalived.

We intend to have a pair (at least) of Apache servers running identical SimpleSAMLphp installations. These will sit behind a pair of load-balancer machines running HAProxy and Keepalived, the first to balance HTTP traffic to the SAML servers, the second to provide virtual IP failover between the balancers. This forward-facing virtual IP will be stunnel-encryped, and advertised as our SAML Identity Provider (IdP).


Senior sysadmin Greg Kuchyt pioneered our use of these technologies for load-balancing and failover of our 389-DS LDAP services in a manner much like this, an approach that has served us well for many years, and which we have used for other services.

Our SAML setup is based on the document collection in the SUNY IDM Wiki, particularly the excellent Installing SimpleSAMLphp for SUNY Federation guide. Suffice it to say that I will not go into any detail on our IdP setup, instead covering only what SimpleSAMLphp changes are necessary to share session data for load-balancing and failover.

Shared Session Data with Memcache

On the SAML servers, in order to pave the way for balancing load across multiple machines, we need to ensure that SimpleSAMLphp session handling is switched over to memcache:

yum install memcached php-pecl-memcache
service memcached start
chkconfig memcached on

Note that SimpleSAMLphp uses PHP memcache and not the newer PHP memcached, but both use the system memcached daemon.

Firewalls will need to be opened to allow incoming memcache connections on port 11211.

In the SimpleSAMLphp configuration, set the store.type, and then adjust the memcache_store.servers array to taste (see docs). The following will cause all session data to be saved to the memcache servers on both boxes:


'store.type' => 'memcache',
'memcache_store.servers' => array(
                array('hostname' => ''),
                array('hostname' => ''),

Load-Balancing with HAProxy

On the balance servers we first install HAProxy. The following configuration uses the least connection algorithm to balance HTTP sessions between the two SAML servers behind them.


    mode                    http
    log                     global
    option                  httplog
    option                  dontlognull
    option http-server-close
    option forwardfor       except
    option                  redispatch
    retries                 3
    timeout http-request    10s
    timeout queue           1m
    timeout connect         10s
    timeout client          1m
    timeout server          1m
    timeout http-keep-alive 10s
    timeout check           10s
    maxconn                 3000

listen https
    balance leastconn
    option  httpclose
    option  forwardfor
    option  httplog
    option  httpchk         GET /
    server  saml1 check inter 5000 downinter 500
    server  saml2 check inter 5000 downinter 500

Note that hostnames are used here for clarity, but it is probably preferable to use IP addresses for server config lines. Also note that we bind our instance to localhost:4430, since we do not intend for public connections to hit this port directly. Instead they will hit SSL on port 443 provided by stunnel (later).

Start haproxy:

service haproxy start

We now have two balancers that will each balance HTTP connections to the SAML servers.

Failover with Keepalived

On the balance servers we next install Keepalived. On balance1, the following configuration establishes a virtual IP address to be maintained between the balance servers.


global_defs {
        notification_email {
        smtp_connect_timeout 30

vrrp_instance VI_1 {
        virtual_router_id 1
        state MASTER
        priority 100
        interface eth2


        authentication {
                auth_type AH
                auth_pass SomeKindofPasswordHere!
        virtual_ipaddress {
       brd dev eth2

On balance2, we install the same configuration with two key differences:

        state BACKUP
        priority 50

Start keepalived:

service keepalived start

We now have a front-facing virtual IP address providing failover between the balancers. You can view the status of the IP with: ip addr show.

SSL with Stunnel

On the balance servers we next install stunnel to encrypt communication on the virtual IP port 443.


#CAfile = /etc/pki/tls/certs/entrust-chain.crt
cert = /etc/pki/tls/certs/
key = /etc/pki/tls/private/
        # public virtual ip address
        accept =
        connect =
	verify = 0

An example self-signed cert:

openssl genrsa -aes256 -out pass.key 2048
openssl rsa -in pass.key -out server.key
openssl req -new -key server.key -x509 -out server.crt -days 999

Note that key and cert will need to be copied into the locations referenced above.

Start stunnel with the following, probably in an init script:

stunnel /etc/stunnel/https.conf

With stunnel running, we now have encrypted communication on our virtual IP.

Hacking SimpleSAMLphp for Offloaded SSL

SimpleSAMLphp makes use of internal functions to determine port number, HTTP versus HTTPS, and to build links based on those determinations. Because we offloaded SSL to our balancer, it throws off these functions, with the result that links and such are improperly constructed.

Discussion at!topic/simplesamlphp/m0aqJoURl7I suggests that there may be simpler ways to do this, eventually, but I settled on the changes represented in the following patch to lib/SimpleSAML/Utilities.php:

--- Utilities.dist.php	2013-04-08 04:44:05.000000000 -0400
+++ Utilities.php	2013-12-14 15:35:04.000000000 -0500
@@ -87,6 +87,8 @@
 	public static function isHTTPS() {
+return TRUE;
 		$url = self::getBaseURL();
 		$end = strpos($url,'://');
@@ -105,6 +107,8 @@
 	private static function getServerHTTPS() {
+return TRUE;
 		if(!array_key_exists('HTTPS', $_SERVER)) {
 			/* Not an https-request. */
 			return FALSE;
@@ -128,6 +132,8 @@
 	private static function getServerPort() {
+return '';
 		if (isset($_SERVER["SERVER_PORT"])) {
 			$portnumber = $_SERVER["SERVER_PORT"];
 		} else {

After applying the patch, we should find that all clicks everywhere remain based on