A while back I had written two blog posts about setting up an Apache, MySQL and multiple, simultaneous PHP versions environment for macOS -or Linux, same concept- and for Windows. In the meantime HTTPS has been promoted to a near necessity and so being able to build and test a site on HTTPS is very desirable. Well, as it turns out, it’s perfectly possible too!
The following tutorial is written on Windows but it is applicable on macOS and Linux as well. You will need to adapt the paths in the commands and file names. Also, please bear in mind this is a fairly long and detailed read. You may want to set aside an hour or two to follow it through.
What you’ll need
The Apache server I’ve described in my blog posts, obviously. The OpenSSL executable (the instructions assume it’s somewhere in your path). The OpenSSL download page includes links for Windows and macOS downloads. Linux should have it already installed. If you’re on Ubuntu try sudo apt-get install openssl
ssl-cert
to make sure. Please note that Apache already includes a copy of OpenSSL. Therefore on Windows it might be sufficient to just add C:\Apache24\bin
to your path. Chrome or Firefox. You’ll need to install the generated certificate in your browser. You can probably do that in other browsers but, frankly, these two are the easiest.
Wildcard SSL certificates and custom Certification Authorities
As you remember, the server setup allows you to have a special domain name for each version of PHP. For example, if you’re testing a site on the ancient PHP 5.3 you’d be using the local53.web
domain, whereas if you’re testing it on PHP 5.4 you’d be using the local54.web
domain and so on and so forth. The name of the site would be in the subdomain part, e.g. mysite.local53.web
, mysite.local54.web
and so on. Luckily, we do not have to create an SSL certificate for each subdomain and domain combination. We do, however, need to create and install a wildcard SSL certificate for each one of the PHP version-specific domains. A wildcard SSL certificate is issued against the “star” subdomain, e.g. *.local53.web
, which tells the browser that it’s valid for all subdomains. The downside is that with six concurrent PHP versions (at the time of this writing I have PHP 5.3 to 7.1 inclusive) this gets complicated as I have to tell the operating system and Firefox to trust each one of these certificates. That’s 12 operations. Every time I add a PHP version I have to remember to do something in two different places. If I have to remember to do things I’ll end up making mistakes and waste my time. The solution to that is having a custom Certification Authority. In this case we only need to tell our operating system and Firefox to trust our Certification Authority. Since it signs our custom certificates they are implicitly trusted. That’s how certificates work in practice, too! When you buy an SSL certificate it’s signed by the issuing company’s certificate which is signed by their Certificate Authority root certificate. Your browser only knows and trusts the Certificate Authority root certificate. Since it sees that the chain of signatures goes all the way to this Certificate Authority root certificate it trusts your site’s certificate implicitly.
Creating a custom certification authority
We’ll create our Root Certification Authority in C:\Apache24\ca
. Inside it we’ll also have the intermediate
folder with our Intermediate Certification Authority which signs our wildcard SSL certificates. The signed wildcard SSL certificates themselves will be in the intermediate
subdirectory as well. Let’s start by making the necessary folders:
cd c:\apache24 mkdir ca ca\certs ca\crl ca\newcerts ca\private mkdir ca\intermediate ca\intermediate\certs ca\intermediate\crl ca\intermediate\newcerts ca\intermediate\private ca\intermediate\csr cd c:\apache24\ca copy nul index.txt echo 1000>serial copy nul intermediate/index.txt echo 1000>intermediate/serial
Our certificates will not work properly unless we use Subject Alternative Name (SAN) extensions. This is only possible by creating a custom openssl.cnf
file with the following contents, i.e. copy and paste the following to C:\Apache24\ca\openssl.cnf
:
# OpenSSL root CA configuration file. # Copy to `/root/ca/openssl.cnf`. # # This file is based on the instructions found in https://jamielinux.com/docs/openssl-certificate-authority/index.html [ ca ] # `man ca` default_ca = CA_default [ CA_default ] # Directory and file locations. dir = c:/apache24/ca certs = $dir/certs crl_dir = $dir/crl new_certs_dir = $dir/newcerts database = $dir/index.txt serial = $dir/serial RANDFILE = $dir/private/.rand # The root key and root certificate. private_key = $dir/private/ca.key.pem certificate = $dir/certs/ca.cert.pem # For certificate revocation lists. crlnumber = $dir/crlnumber crl = $dir/crl/ca.crl.pem crl_extensions = crl_ext default_crl_days = 30 # SHA-1 is deprecated, so use SHA-2 instead. default_md = sha256 name_opt = ca_default cert_opt = ca_default default_days = 375 preserve = no policy = policy_strict [ policy_strict ] # The root CA should only sign intermediate certificates that match. # See the POLICY FORMAT section of `man ca`. countryName = match stateOrProvinceName = match organizationName = match organizationalUnitName = optional commonName = supplied emailAddress = optional [ policy_loose ] # Allow the intermediate CA to sign a more diverse range of certificates. # See the POLICY FORMAT section of the `ca` man page. countryName = optional stateOrProvinceName = optional localityName = optional organizationName = optional organizationalUnitName = optional commonName = supplied emailAddress = optional [ req ] # Options for the `req` tool (`man req`). default_bits = 2048 distinguished_name = req_distinguished_name string_mask = utf8only # SHA-1 is deprecated, so use SHA-2 instead. default_md = sha256 # Extension to add when the -x509 option is used. x509_extensions = v3_ca [ req_distinguished_name ] # See <https://en.wikipedia.org/wiki/Certificate_signing_request>. countryName = Country Name (2 letter code) stateOrProvinceName = State or Province Name localityName = Locality Name 0.organizationName = Organization Name organizationalUnitName = Organizational Unit Name commonName = Common Name emailAddress = Email Address # Optionally, specify some defaults. countryName_default = CY stateOrProvinceName_default = Nicosia localityName_default = Egkomi 0.organizationName_default = Akeeba Ltd organizationalUnitName_default = Production Department emailAddress_default =This email address is being protected from spambots. You need JavaScript enabled to view it. [ v3_ca ] # Extensions for a typical CA (`man x509v3_config`). subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always,issuer basicConstraints = critical, CA:true keyUsage = critical, digitalSignature, cRLSign, keyCertSign [ v3_intermediate_ca ] # Extensions for a typical intermediate CA (`man x509v3_config`). subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always,issuer basicConstraints = critical, CA:true, pathlen:0 keyUsage = critical, digitalSignature, cRLSign, keyCertSign [ usr_cert ] # Extensions for client certificates (`man x509v3_config`). basicConstraints = CA:FALSE nsCertType = client, email nsComment = "OpenSSL Generated Client Certificate" subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment extendedKeyUsage = clientAuth, emailProtection [ server_cert ] # Extensions for server certificates (`man x509v3_config`). basicConstraints = CA:FALSE nsCertType = server nsComment = "OpenSSL Generated Server Certificate" subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer:always keyUsage = critical, digitalSignature, keyEncipherment extendedKeyUsage = serverAuth crlDistributionPoints = URI:http://local.web/intermediate.crl.pem [ crl_ext ] # Extension for CRLs (`man x509v3_config`). authorityKeyIdentifier=keyid:always [ ocsp ] # Extension for OCSP signing certificates (`man ocsp`). basicConstraints = CA:FALSE subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer keyUsage = critical, digitalSignature extendedKeyUsage = critical, OCSPSigning [ san_env ] subjectAltName=${ENV::SAN}
Now we are ready to start creating the certificate authority. All of our certificates will be encrypted with a password. It is set by the first command below. Change it to something other than changeme ;)
set CERTIFICATE_PASSWORD=changeme set SAN=DNS:local.web,DNS:*.local.web cd c:\apache24\ca openssl genrsa -aes256 -passout pass:%CERTIFICATE_PASSWORD% -out private/ca.key.pem 4096 openssl req -config openssl.cnf -key private/ca.key.pem -passin pass:%CERTIFICATE_PASSWORD% ^ -new -x509 -days 7300 -sha256 -extensions v3_ca ^ -subj "/CN=Windows Dev Box Root CA/O=Akeeba Ltd./OU=Production Department/C=CY/ST=Nicosia/L=Egkomi" ^ -out certs/ca.cert.pem
Just like before we need to create a custom openssl.cnf
file in the intermediate
folder with the following contents, i.e. copy and paste the following to C:\Apache24\ca\intermediateopenssl.cnf
:
# OpenSSL root CA configuration file. # # This file is based on the instructions found in https://jamielinux.com/docs/openssl-certificate-authority/index.html [ ca ] # `man ca` default_ca = CA_default [ CA_default ] # Directory and file locations. dir = c:/Apache24/ca/intermediate certs = $dir/certs crl_dir = $dir/crl new_certs_dir = $dir/newcerts database = $dir/index.txt serial = $dir/serial RANDFILE = $dir/private/.rand # The root key and root certificate. private_key = $dir/private/intermediate.key.pem certificate = $dir/certs/intermediate.cert.pem # For certificate revocation lists. crlnumber = $dir/crlnumber crl = $dir/crl/intermediate.crl.pem crl_extensions = crl_ext default_crl_days = 30 # SHA-1 is deprecated, so use SHA-2 instead. default_md = sha256 name_opt = ca_default cert_opt = ca_default default_days = 375 preserve = no policy = policy_loose [ policy_strict ] # The root CA should only sign intermediate certificates that match. # See the POLICY FORMAT section of `man ca`. countryName = match stateOrProvinceName = match organizationName = match organizationalUnitName = optional commonName = supplied emailAddress = optional [ policy_loose ] # Allow the intermediate CA to sign a more diverse range of certificates. # See the POLICY FORMAT section of the `ca` man page. countryName = optional stateOrProvinceName = optional localityName = optional organizationName = optional organizationalUnitName = optional commonName = supplied emailAddress = optional [ req ] # Options for the `req` tool (`man req`). default_bits = 2048 distinguished_name = req_distinguished_name string_mask = utf8only # SHA-1 is deprecated, so use SHA-2 instead. default_md = sha256 # Extension to add when the -x509 option is used. x509_extensions = v3_ca [ req_distinguished_name ] # See <https://en.wikipedia.org/wiki/Certificate_signing_request>. countryName = Country Name (2 letter code) stateOrProvinceName = State or Province Name localityName = Locality Name 0.organizationName = Organization Name organizationalUnitName = Organizational Unit Name commonName = Common Name emailAddress = Email Address # Optionally, specify some defaults. countryName_default = CY stateOrProvinceName_default = Nicosia localityName_default = Egkomi 0.organizationName_default = Akeeba Ltd organizationalUnitName_default = Production Department emailAddress_default =This email address is being protected from spambots. You need JavaScript enabled to view it. [ v3_ca ] # Extensions for a typical CA (`man x509v3_config`). subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always,issuer basicConstraints = critical, CA:true keyUsage = critical, digitalSignature, cRLSign, keyCertSign [ v3_intermediate_ca ] # Extensions for a typical intermediate CA (`man x509v3_config`). subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always,issuer basicConstraints = critical, CA:true, pathlen:0 keyUsage = critical, digitalSignature, cRLSign, keyCertSign [ usr_cert ] # Extensions for client certificates (`man x509v3_config`). basicConstraints = CA:FALSE nsCertType = client, email nsComment = "OpenSSL Generated Client Certificate" subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment extendedKeyUsage = clientAuth, emailProtection [ server_cert ] # Extensions for server certificates (`man x509v3_config`). basicConstraints = CA:FALSE nsCertType = server nsComment = "OpenSSL Generated Server Certificate" subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer:always keyUsage = critical, digitalSignature, keyEncipherment extendedKeyUsage = serverAuth [ crl_ext ] # Extension for CRLs (`man x509v3_config`). authorityKeyIdentifier=keyid:always [ ocsp ] # Extension for OCSP signing certificates (`man ocsp`). basicConstraints = CA:FALSE subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer keyUsage = critical, digitalSignature extendedKeyUsage = critical, OCSPSigning [ san_env ] subjectAltName=${ENV::SAN}
Next up, we’ll create the private key for our intermediate certificate, a CSR (certificate signing request) and sign it with the root CA certificate - thus having a valid certificate we can use to sign the actual wildcard certificates. Again, the certificates are encrypted with a password set in the command below. Change it to suit your needs.
Open a command prompt and run the following
set CERTIFICATE_PASSWORD=changeme set SAN=DNS:local.web,DNS:*.local.web cd c:\apache24\ca openssl genrsa -aes256 -passout pass:%CERTIFICATE_PASSWORD% -out intermediate/private/intermediate.key.pem 4096 REM Create a certificate sign request openssl req -config intermediate/openssl.cnf -new -sha256 ^ -key intermediate/private/intermediate.key.pem ^ -passin pass:%CERTIFICATE_PASSWORD% ^ -subj "/CN=Windows Dev Box Intermediate CA/O=Akeeba Ltd./OU=Production Department/C=CY/ST=Nicosia/L=Egkomi" ^ -out intermediate/csr/intermediate.csr.pem REM Sign the request with the master CA certificate openssl ca -config openssl.cnf -extensions v3_intermediate_ca -batch ^ -passin pass:%CERTIFICATE_PASSWORD% ^ -days 3650 -notext -md sha256 ^ -in intermediate/csr/intermediate.csr.pem ^ -out intermediate/certs/intermediate.cert.pem REM Create the CA chain file copy intermediate\certs\intermediate.cert.pem ^ + certs\ca.cert.pem intermediate\certs\ca-chain.cert.pem
Creating a wildcard certificate per domain
Each PHP version domain name (local53.web, local54.web, etc) gets its own wildcard certificate signed by the Intermediate CA we created above. This allows all sites, served as subdomains, to be usable through HTTPS. We assume that certificates are to be stored in C:\Apache24\ssl
.
Like before, you need to supply the password your certificates will be encrypted with. This is the first line of the commands below. Please change it.
For the rest of this section we assume that the domain you are issuing for is local53.web
. You MUST adjust this for each domain name. This is the second line of the commands below. Please remember to change it.
Open a command prompt and run the following
set CERTIFICATE_PASSWORD=changeme set CERTIFICATE_DOMAIN=local53.web set SAN=DNS:%CERTIFICATE_DOMAIN%,DNS:*.%CERTIFICATE_DOMAIN% cd c:\apache24\ca openssl genrsa -aes256 ^ -passout pass:%CERTIFICATE_PASSWORD% ^ -out intermediate\private\%CERTIFICATE_DOMAIN%.key.pem 2048 openssl req -config intermediate\openssl.cnf ^ -key intermediate\private\%CERTIFICATE_DOMAIN%.key.pem ^ -extensions san_env ^ -passin pass:%CERTIFICATE_PASSWORD% ^ -subj "/CN=*.%CERTIFICATE_DOMAIN%/O=Akeeba Ltd./OU=Production Department/C=CY/ST=Nicosia/L=Egkomi" ^ -new -sha256 -out intermediate\csr\%CERTIFICATE_DOMAIN%.csr.pem openssl ca -config intermediate/openssl.cnf -batch ^ -extensions server_cert -extensions san_env ^ -days 1835 -notext -md sha256 ^ -in intermediate/csr/%CERTIFICATE_DOMAIN%.csr.pem ^ -passin pass:%CERTIFICATE_PASSWORD% ^ -out intermediate/certs/%CERTIFICATE_DOMAIN%.cert.pem copy intermediate\certs\ca-chain.cert.pem c:\Apache24\ssl\ca-chain.crt openssl rsa -in intermediate/private/%CERTIFICATE_DOMAIN%.key.pem ^ -out c:\Apache24\ssl\%CERTIFICATE_DOMAIN%.key ^ -passin pass:%CERTIFICATE_PASSWORD% copy intermediate\certs\%CERTIFICATE_DOMAIN%.cert.pem c:\Apache24\ssl\%CERTIFICATE_DOMAIN%.crt
Enable SSL support in Apache
Edit your httpd.conf
file. If you’d followed my Windows tutorial that would be in C:\Apache24\conf\httpd.conf
.
Find this line:
# LoadModule ssl_module modules/mod_ssl.so
and remove the hash sign in front:
LoadModule ssl_module modules/mod_ssl.so
This tells Apache to load the SSL module which is required for HTTPS support.
Next, towards the top of the file, find the line
Listen 80
and change it to
Listen 80 <IfModule mod_ssl.c> Listen 443 </IfModule>
This tells Apache to listen not only to the HTTP port (80), but also the HTTPS (443) one as long as SSL support is enabled.
Restart Apache. It should load just fine.
Caveat: do NOT enable the default SSL configuration file for Apache e.g. C:\Apache24\conf\extra\httpd-ssl.conf
on Windows. That’s the file that has the line <VirtualHost _default_:443>
. This line tells Apache to use a default SSL certificate. This is exactly what we MUST NOT do. That bit got me until I found a mention of it in a DigitalOcean tutorial.
Add the correct SSL certificate to each PHP version
Now we need to edit our Apache virtual hosts file. According to my previous tutorial that file is in C:\Apache24\conf\extra\httpd-vhost.conf
At the top of the file we need to add two important lines:
NameVirtualHost *:80 NameVirtualHost *:443
They tell Apache that we want virtual hosts on both HTTP (port 80) and HTTPS (port 443).
Now find the block that starts with
# Dynamic virtual hosts using vhost_alias, PHP 5.3
Right below its closing </VirtualHost>
entry add the following:
# PHP 5.3 over HTTPS (local53.web)
<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerAdmin This email address is being protected from spambots. You need JavaScript enabled to view it.
ServerName www.local53.web
DocumentRoot "c:/Apache24/htdocs"
SSLEngine on
SSLCertificateFile "c:\Apache24\ssl\local53.web.crt"
SSLCertificateKeyFile "c:\Apache24\ssl\local53.web.key"
FcgidInitialEnv PATH "c:/php/5.3;C:/WINDOWS/system32;C:/WINDOWS;C:/WINDOWS/System32/Wbem;"
FcgidInitialEnv PHPRC "c:/php/5.3"
<Directory "c:/Apache24/htdocs">
<Files ~ "\.php$">
AddHandler fcgid-script .php
FcgidWrapper "c:/php/5.3/php-cgi.exe" .php
Options +ExecCGI
order allow,deny
allow from all
deny from none
</Files>
</Directory>
</VirtualHost>
<VirtualHost *:443>
ServerAlias *.local53.web
UseCanonicalName Off
VirtualDocumentRoot "c:/Apache24/htdocs/%1"
SSLEngine on
SSLCertificateFile "c:\Apache24\ssl\local53.web.crt"
SSLCertificateKeyFile "c:\Apache24\ssl\local53.web.key"
FcgidInitialEnv PATH "c:/php/5.3;C:/WINDOWS/system32;C:/WINDOWS;C:/WINDOWS/System32/Wbem;"
FcgidInitialEnv PHPRC "c:/php/5.3"
<Directory "c:/Apache24/htdocs">
<Files ~ "\.php$">
AddHandler fcgid-script .php
FcgidWrapper "c:/php/5.3/php-cgi.exe" .php
Options +ExecCGI
order allow,deny
allow from all
deny from none
</Files>
</Directory>
</VirtualHost>
</IfModule>
A keen observer will notice that this is neraly identical to what we had done to enable PHP 5.3 support with a few minor, yet important, changes:
- Everything is wrapped inside the
<IfModule mod_ssl.c>
statement. This prevents Apache from dying an undignified death on start-up should the SSL support be accidentally disabled. - The virtual host lines read
<VirtualHost *:443>
instead of<VirtualHost *:80>
. It makes sense, since we’re dealing with HTTPS that’s served over port 443, not port 80. - We added the lines starting with SSL in each virtual host definition. They tell Apache to turn on HTTPS for that virtual host and which certificate to use.
So now you know how simple it is to repeat that process on all other virtual hosts, covering all installed versions of PHP. My dev servers have six PHP versions right now: 5.3, 5.4, 5.5, 5.6, 7.0 and 7.1. So I have to repeat that process another five times. Considering that I have 15 sites on each server I guess it’s much better only having to create and install six certificates than 90!
Now restart Apache
Caveat: If you try to access the SSL site right now you’ll get an error about the certificate being insecure. That’s because we’re using a self-signed certificate. We’ll deal with that in the next step.
Install the Certificate Authority
All browsers share an operating system level certificate authority store. This means that adding our CA Root certificate on one browser will make it available on all browsers on your system, as well as other application. There are a few exceptions to that rule. Firefox does its own thing, so you MUST add the Root CA certificate to it separately. Moreover, some applications will not use the OS-wide CA store, e.g. Bash on Ubuntu on Windows, CLI tools like cURL and scripting languages such as PHP (which expect you to configure your own CA cache). The following instructions deal with web browsers only and only for the three major desktop operating systems. Adding CA roots to mobile OS ranges from almost impossible at worst to very complicated at best. If you need it (you develop mobile apps), you know how to do it.
Windows (Chrome, Opera, Internet Explorer, Microsoft Edge)
- Press the Windows key and R at the same time
- Type certmgr.msc and press ENTER
- You’ll get the UAC dialog. Click on Yes.
- Double click on the Trusted Root Certification Authorities folder entry. You now see a Certificates folder entry.
- Right click on the Certificates folder entry, choose All Tasks, Import.
- Select the Root CA certificate file you created, stored in c:\Apache24\ca\certs\ca.cert.pem
- You are asked about the certificate store, confirm it’s Trusted Root Certification Authorities.
- Accept everything and close all dialogs.
- Close and restart all browsers.
Firefox (all Operating Systems)
Unlike other browsers, Firefox uses its own certificate authority store. We need to add our CA root there:
- Click on the hamburger menu, Options
- Click on Advanced, then on the Certificates tab
- Click on the View Certificates button
- Click on the Authorities tab, then on the Import button
- Select the Root CA certificate file you created, stored in c:\Apache24\ca\certs\ca.cert.pem
- You see check boxes about the intended trust, select all of them.
- Accept everything and close all dialogs.
- Close and restart the browser.
Linux (Google Chrome and Opera)
- Go to Chrome settings, Show advanced settings, HTTPS/SSL, Manage Certificates.
- Now click on the Authorities or Trusted Root Certification Authorities tab depending on your operating system.
- Click the Import button.
- Select the Root CA certificate file you created, stored in c:\Apache24\ca\certs\ca.cert.pem
- You see check boxes about the intended trust, select all of them.
- Accept everything and close all dialogs.
- Close and restart the browser.
macOS (All browsers except Firefox)
- Copy the Root CA certificate file you created, stored in c:\Apache24\ca\certs\ca.cert.pem on your Windows computer, to your desktop (you may need to change the extension to .crt)
- Double click the certificate on your desktop. You will be asked to enter your macOS password.
- Add the certificate to the System keychain, not the Login keychain.
- Double click on the certificate you just added in Keychain Access.
- Expand the Trust section.
- Set When using this certificate to Always Trust.
- Restart all your browsers
Especially for Chrome, remember that it never really quits. It lingers in the background. You need to press CMD-Q twice in an open Google Chrome window to really quit it. If the instructions seem to not be working try logging off and logging back on.
Further ideas
You can use this in some production environments. Obviously it's not practical for live sites, as you'd have to ask every visitor to trust your arbitrary CA root. You could, however, use this for an Intranet where you exert control over the clients, meaning you can install custom CA roots on them as you see fit.
The principle of creating a custom Certificate Authority is absolutely not specific to Windows. You can apply this idea to any AMP stack on any operating system. I’m already using it on Linux and macOS.
Certificate Authorities are not just limited to HTTPS. They can be used, for example, to sign distributed code as I presented in the J and Beyond 2017 conference.
Please note that if you are using a CA root outside of a development environment you MUST use an air-gaped computer, protected by physical security, to prevent compromise of your CA root and the intermediate certificate. Only the signed site certificate should ever leave the air-gaped computer.