How to setup SSL for multiple Virtual Hosts in Apache using self signed certificates.
SSL Certificate Basics
There are 2 essential components of a Secure Socket Layer ( SSL), the private and public key. A SSL Certificate is a form of digital identity, it contains the public key and is signed by a trusted Certificate Authority (CA) that certifies the identity (domain) represented in the certificate. Before a SSL Certificate is issued by a CA, they will go through a series of steps to verify the identity of the company applying. The private key proves ownership of a certificate and should never be shared (more on this in the next section).
Root Certificate Authorities are an alliance of companies that decide who can be trusted and they hold a set of root certificates that are used to sign other certificates. These root certificates are self signed since there is no authority to verify the identity of the root. Browsers ship with a finite list of these trusted root certificates. A Root Certificate Authority can also decide to trust another company to issue certificates, these are called Intermediate Certificates. Intermediate Certificates can sign other certificates in contrast to normal Certificates which cannot.
Self Signed Certificates are in essence certificates that are signed by an unknown and hence untrusted authority. Browsers will explicitly warn the user when connecting securely to a website that is using a Self Signed Certificate.
How does SSL Work
As the diagram shows, the private key does prove ownership of the certificate. Without the private key counterpart of the public key contained in the certificate, the server would not be able to decrypt the Symmetric Encryption Key (SEK) generated by the browser. Subsequently it would not be able to decrypt the HTTP Request that is encrypted with the SEK.
Apache, SSL and Server Name Indication (SNI)
Apache is a popular and widely used web server that can be used to host multiple sites in the form of Virtual Hosts. A name based Virtual Host is a site configuration in Apache that is activated by the name in the Host header of an HTTP Request. Apache will inspect the incoming HTTP request and try to match the Host header to the ServerName or ServerAlias of a particular Virtual Host and subsequently serve a response based on the configuration of that Virtual Host. Below is an example of 2 name based Virtual Hosts. Note that multiple virtual hosts can be configured in a single file or one per file, usually in
/etc/apache2/sites-enabled/ depending on the distribution.
<VirtualHost www.monkey-app-1.local:80> ServerName www.monkey-app-1.local DocumentRoot "/var/www/www.monkey-app-1.local" </VirtualHost> <VirtualHost www.monkey-app-2.local:80> ServerName www.monkey-app-2.local DocumentRoot "/var/www/www.monkey-app-2.local" </VirtualHost>
SSL configuration is done inside the
VirtualHost configuration. The problem with this is that Apache needs to read the Host header of the incoming HTTP Request to determine which VirtualHost to use, but when the request is encrypted the Host header cannot be read until the request is decrypted. Apache will use the first VirtualHost to setup the encryption layer. This might be acceptable if Apache is configured with a wildcard certificate and all the VirtualHosts are within the same domain, but with a configuration like above the SSL limitation will become evident.
Server Name Indication or SNI is an extension to the SSL protocol that overcomes this problem by providing a way for the client (usually a browser) to include the requested Host in the initial message of the SSL handshake. SNI enables Apache to resolve the correct Virtual Host before setting up the encryption layer. SNI only works if the client supports the extension. The SSLStrictSNIVHostCheck directive can be used to force a 403 HTTP Response for clients that do not support SNI. Below is the example adapted to support SSL.
SSLStrictSNIVHostCheck on <VirtualHost www.monkey-app-1.local:443> ServerName www.monkey-app-1.local SSLEngine on SSLCertificateFile /secrets/www.monkey-app-1.local.crt SSLCertificateKeyFile /secrets/www.monkey-app-1.local.key DocumentRoot "/var/www/www.monkey-app-1.local" </VirtualHost> <VirtualHost www.monkey-app-2.local:443> ServerName www.monkey-app-2.local SSLEngine on SSLCertificateFile /secrets/www.monkey-app-2.local.crt SSLCertificateKeyFile /secrets/www.monkey-app-2.local.key DocumentRoot "/var/www/www.monkey-app-2.local" </VirtualHost>
Example Setup using Docker
The sample Docker project contains two sites, www.monkey-app-1.local and www.monkey-app-2.local plus the corresponding Virtual Host configuration as explained earlier in this post.
git clone https://github.com/monkey-codes/apache-ssl-sni cd apache-ssl-sni
To run the sample site, first generate a private key and self signed certificate for each of the sites in a Docker volume.
docker volume create --name monkey-ssl-secrets docker run --rm -t -i -v monkey-ssl-secrets:/secrets ubuntu /bin/bash apt-get update apt-get install openssl openssl genrsa -out "/secrets/www.monkey-app-1.local.key" 2048 openssl req -new -key "/secrets/www.monkey-app-1.local.key" -out "/secrets/www.monkey-app-1.local.csr" Country Name (2 letter code) [AU]:AU State or Province Name (full name) [Some-State]:QLD Locality Name (eg, city) :Brisbane Organization Name (eg, company) [Internet Widgits Pty Ltd]:Monkey Codes Organizational Unit Name (eg, section) : Common Name (e.g. server FQDN or YOUR name) :www.monkey-app-1.local.key Email Address : Please enter the following 'extra' attributes to be sent with your certificate request A challenge password : An optional company name : openssl x509 -req -days 3650 -in "/secrets/www.monkey-app-1.local.csr" -signkey "/secrets/www.monkey-app-1.local.csr" -out "/secrets/www.monkey-app-1.local.crt" #repeat the openssl commands for the 2nd site too, but replace with www.monkey-app-2.local exit
Note that the Common Name is set to the Host name of the site. Wildcard certificates, that support multiple sub domains, can be configured using
*.monkey-app-1.local as the Common Name during the openssl step that creates the Certificate Signing Request (CSR).
Now run the Docker Image and mount the volume that contains the secrets. The Docker Container also needs net-aliases to be able to resolve the 2 domain names www.monkey-app-1.local and www.monkey-app-2.local.
docker network create apache_ssl_sni docker run --rm -p 443:443 -v $(pwd)/vhost.conf:/opt/docker/etc/httpd/vhost.conf -v $(pwd)/sites:/var/www -v monkey-ssl-secrets:/secrets --net apache_ssl_sni --net-alias www.monkey-app-1.local --net-alias www.monkey-app-2.local webdevops/apache
The last step is to add the 2 hosts to the local
/etc/hosts file so that the browser can resolve the 2 domains. On OSX the domains should point to the IP of the Docker Machine VM (
docker-machine ip will show the ip).
echo $(docker-machine ip) www.monkey-app-1.local www.monkey-app-2.local >> /etc/hosts
Testing the setup
https://www.monkey-app-2.local in a browser should produce a warning since the certificate is not signed by a trusted authority.
Follow these instructions to add the self signed certificate to chrome, that will stop it from producing the above warning.
Clicking on Proceed to www.monkey-app-1.local (unsafe) will serve the sample application.