I decided that I wanted the login for this site to be over SSL/TLS, instead of unencrypted – at this time, I decided against having the admin area also running under SSL. This article describes how to accomplish the following:
- Preparation of private key and certificate signing request (CSR)
- Obtaining a free SSL certificate (from StartCom)
- Create an SSL virtualhost site under Apache
- Forcing SSL logins under WordPress
- Redirect all other pages to the non-encrypted site
An SSL site, will use the https protocol and will transmit information over an encrypted connection. In order to so this, we require a private key on the server, and a certificate that can be sent to the client. To make the certificate we will require a certificate signing request (CSR). All of the above can be made with OpenSSL, however, certificates that are self-signed as opposed to being issued by recognized certificate authorities, will generate a warning (e.g. ‘This Connection is Untrusted’) in all major browsers. While there are some certificate authorities that will issue a free certificate (e.g. CACert), the certificates of these CAs are typically not installed by default in major browsers, and thus offer little advantage over a self-signed certificate.
Under Firefox, you can view the list of recognized certificate authorities:
- Go to Tools > Options > Advanced > Encryption
- Click View Certificates
- Go to the Authorities tab
The only CA (that I know of) that offers free SSL certificates (class 1), and is ‘trusted’ by major browsers is StartCom. For a private site, it can be just as effective to use a self-signed certificate, but for any site where many people will be accessing the secure pages, a certificate signed by a trusted CA is preferable.
Generating a Private Key and CSR
I opted to place my keys in the same folder as pre-existing keys: /etc/pki/tls
Note: most commands listed below must be run as root (sudo).
cd /etc/pki/tls/private
Generate a 2048 bit RSA key without a passphrase (you can generate a 4096 bit key for more security – the trade-off is that greater CPU usage is needed for encryption). Certificates that include a passphrase will require that the passphrase be entered each time the server is started.
openssl genrsa -out www.domain.com.key 2048
Let us make a directory for our CSRs, and switch to it:
mkdir /etc/pki/tls/csrs cd /etc/pki/tls/csrs
Generate a CSR from the private key created above, with a sha1 hash.
openssl req -new -key /etc/pki/tls/private/www.domain.com.key -out www.domain.com.csr -sha1
You will be asked to provide information about your organization (e.g. Country, State, Name, etc). Keep in mind that the location information should pertain to your organization not your server.
The Common Name refers to the domain name that the certificate will be issued for. Note that it should match exactly (including or excluding the www as specific to your site). The only exceptions to this are wildcard certificates. In some cases, certificates might be issued with support for an alt name (typically the top level domain).
Keep in mind that this step only specifies the message authentication digest (SHA1), the type of encryption (e.g. AES, 3DES, etc) is negotiated between the browser and server at the time of use.
Obtaining an SSL Certificate
Once you have generated a CSR, you can submit it to StartCom (https://www.startssl.com/). (Of course, at this point, you can chose another CA or opt to self-sign your certificate.) StartCom requires verification of domain ownership, which is accomplished by sending an email to the domain containing a verification code (the process is automated). If using StartCom, the CSR must be at least 2048 bits, and use a sha1 or better hash. The certificate is returned in a textbox – copy and paste the contents into a file (without modification) on the server to create the certificate (I used vi – note that nano/pico might not replicate the contents exactly). Note: the certificate will have different line breaks if pasted into Windows as compared to Linux – it appears that Linux line breaks are more common in certificates.
StartCom also provides its CA certificate and an intermediate certificate which must be chained together with the certificate issued. Download the two additional certificates, and save them to the same folder (/etc/pki/tls/certs
).
Setting up SSL under Apache
Previously, a site using SSL required a dedicated IP address, as the hostname was not transmitted by browsers until after the handshake was completed. The problem with this was that the server could not know which site’s certificate to use in the handshake. Modern browsers, however, will pass the hostname, and thus permit the use of name based virtual hosting with SSL.
Secure (https) connections are typically established on port 443, and Apache typically uses mod_ssl for an SSL connection. Common locations for SSL directives include: /etc/httpd/conf/httpd.conf
or a file in /etc/httpd/conf.d
(in my case, /etc/httpd/conf.d/ssl.conf
)
ssl.conf
already contains default options that will provide a starting point, just ensure that the server is listening on port 443 and has a corresponding NameVirtualHost
directive:
Listen 443 NameVirtualHost *:443
The ssl.conf file includes a VirtualHost
block – you will need to remove it if you already have virtualhosts setup for each site.
Since I use Webmin as my hosting panel, virtual hosts are added to the file /etc/httpd/conf.d/hosts.conf
Note: my setup uses Nginx as a reverse-proxy with apache running in the background; apache is setup to use fastcgi with suexec for php, certain configuration options, below, are specific to this setup.
I created a new file /etc/httpd/conf.d/hosts.ssl.conf
and copied and modified the relevant virtualhost block from the file created by webmin, yielding the following:
<VirtualHost *:443> SuexecUserGroup "#UID" "#GID" ServerName www.domain.com DocumentRoot /var/www/html/domain.com/public_html ErrorLog /var/log/virtualmin/domain.com_ssl_error_log CustomLog /var/log/virtualmin/domain.com_ssl_access_log "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b" ScriptAlias /cgi-bin/ /var/www/html/domain.com/cgi-bin/ DirectoryIndex index.html index.htm index.php index.php4 index.php5 SSLEngine on SSLProtocol all -SSLv2 SSLCipherSuite ALL:!ADH:!NULL:!EXPORT:!SSLv2:RC4+RSA:+HIGH SSLCertificateFile /etc/pki/tls/certs/www.domain.com.crt SSLCertificateKeyFile /etc/pki/tls/private/www.domain.com.key SSLCertificateChainFile /etc/pki/tls/certs/sub.class1.server.startcom.pem SSLCACertificateFile /etc/pki/tls/certs/startcom.pem SetEnvIf User-Agent ".*MSIE.*" nokeepalive ssl-unclean-shutdown <Directory /var/www/html/domain.com/public_html> Options -Indexes +IncludesNOEXEC +FollowSymLinks allow from all AllowOverride All </Directory> <Directory /var/www/html/domain.com/cgi-bin> allow from all </Directory> </VirtualHost>
The list of encryption ciphers that the server will permit are governed by the SSLCipherSuite
line. You can view the corresponding list, by running it through OpenSSL:
openssl ciphers -v 'ALL:!ADH:!NULL:!EXPORT:!SSLv2:RC4+RSA:+HIGH'
(In this case, we take the complete list (ALL), remove anonymous Diffie-Hellman (ADH), remove ciphers without encryption (NULL), remove export strength ciphers (EXPORT), remove ciphers that are only valid for SSLv2, include RC4 ciphers using RSA key exchange, and prioritize the high strength ciphers.)
Update: It is worth noting that Ephemeral Diffie-Hellman (EDH or DHE) is computationally intensive and comes at the cost of a significant performance decrease. This can be mitigated by adding !kEDH
to your ciphers line (as per this article.
The first time I tried this, I got the following error, on starting apache:
[warn] RSA server certificate is a CA certificate (BasicConstraints: CA == TRUE !?) [warn] RSA server certificate CommonName (CN) ‘…’ does NOT match server name!?
You can find the common name (CN) the certificate was issued for by running:
openssl x509 -in /etc/pki/tls/certs/www.domain.com.crt -noout -subject
The ServerName directive must exactly match (including/excluding the www) the CN value from the output above.
A point of mention – on AWS instances, you must open port 443 (TCP) on your security group in order to be able to connect (opening the port using iptables is not sufficient) – otherwise, you will simply get connection timed out errors (without anything logged by the server).
Another notable point is that if you are using a CDN (e.g. Cloudfront), or a service such as Amazon’s S3, and you access files using a CNAME, https will not work. The service has a wildcard certificate issued for subdomains of the service (e.g. abc.cloudfront.net), your CNAME (e.g. media.thatsgeeky.com) is not a subdomain of the service, and therefore, trying to access a file will yield a certificate domain mismatch.
At this point you should be able to access either the SSL or non-SSL version of your site by varying the protocol (http/https). There is a good chance however that you will have some unencrypted items on your page. To the extent possible, replacing full paths with relative paths will likely help reduce the number of insecure items on your page. A quick search of the source code for anything starting with http:// might be a good place to start.
SSL Logins under WordPress
While the above items are generally applicable, the rest of this article is specific to WordPress.
WordPress has two configuration constants that can be defined to either force SSL logins, or force the entire admin area to be accessed only via SSL. In both cases, the relevant line is to be added to wp-config.php:
To force SSL logins add: define('FORCE_SSL_LOGIN', true);
For SSL admin add: define('FORCE_SSL_ADMIN', true);
The FORCE_SSL_LOGIN
will only change the form action on the login page to point to the SSL (https) page. That is, the submitted information will go to an encrypted page, but the user remains on unencrypted pages. This will prevent your username/password from being transmitted in clear-text. (This is the option that I decided to implement).
The FORCE_SSL_ADMIN
will change the link to the login page to point to the SSL version, and will redirecti all admin pages to an SSL version. There is a good chance however, that pages will contain some unencrypted content.
Note: using site_url()
instead of get_option('siteurl')
will provide links (e.g. to stylesheets) with the correct protocol (http/https).
Note: in Chrome, if you load a secure page that has been incorrectly setup, refreshing the page after fixing the problem is insufficient to update the security state (e.g. get a red crossed out https). You will need to restart the browser to display the updated security state (e.g.. green https).
Redirecting Users to the Non-SSL Site
Since there is little purpose in using the secure site for most pages, it may be desirable to redirect visitors to the non-SSL site, and only allow SSL logins. To do so, add the following to the VirtualHost block above (before the SSL directives) and restart apache:
<IfModule mod_rewrite.c> RewriteEngine On RewriteRule !wp-login(.*) - [C] RewriteRule ^/(.*) http://www.domain.com/$1 [QSA,L] </IfModule>
Now, the only page that can be accessed via SSL is wp-login.php. Note however, that if the page is loaded, it will display insecure elements (if FORCE_SSL_ADMIN
was used, the page does not display any insecure elements – all file sources are changed from http to https).
You can confirm that login occurred via SSL, by checking your server logs:
[08/Jan/2011:18:27:34 -0500] xx.xxx.x.xx TLSv1 DHE-RSA-CAMELLIA256-SHA "POST /wp-login.php HTTP/1.1" response_size
You can also test an SSL connection, and view the cipher used by running:
openssl s_client -host HOSTNAME -port 443
Alternatively, on the client side, you can use a browser plugin such as Live HTTP Headers to verify that data was posted to a secure version of wp-login. Most browsers will also display the details of an SSL connection (for instance, Chrome displays the key-exchange cipher (e.g. AES-256), the message authentication (e.g. SHA1), and the encryption cipher (e.g. RSA).
Thanks for such an accurate and well written post. Saved me a lot of hair pulling, and I was pretty close before seeing this. An inch can be like a mile tho. I didn’t get the rewrite back to non-ssl working yet, but am looking more closely now. (i want to get it for anything that is not wp-login.php and not wp-admin)
Glad to hear it helped, I have two server blocks on my setup – one for SSL and one for everything else – the SSL block accepts the one page I want to be SSL (wp-login.php) and redirects everything else:
Hope that helps.
I just checked back to your site — hadn’t noticed your reply at the time (March), but thanks. I am revisiting rewrite rules in general for the need to handle WPMU subsites, meaning wp-login.php will have any variety of subsite prefix. That needs to be handled as a variable in the rules. I hope to do other selective rewrites to https (such as if the URL contains “private”). Thanks again.