Setting up SSL Login on WordPress

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:

  1. Go to Tools > Options > Advanced > Encryption
  2. Click View Certificates
  3. 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).

By cyberx86

Just a random guy who dabbles with assorted technologies yet works in a completely unrelated field.

4 comments

  1. 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)

    1. 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:

      server {
              listen *:443 ssl;
              ssl_certificate /path/to/my.crt;
              ssl_certificate_key /path/to/my.key;
              server_name thatsgeeky.com www.thatsgeeky.com;
              root   /path/to/public_html;
              error_log /path/to/error.log;
              access_log /path/to/access.log main;
      
              location = /wp-login.php {
                    include /etc/nginx/fastcgi_params;
                    fastcgi_pass 127.0.0.1:XXXX;
              }
      
              location / { //keep the path intact when changing to non-SSL
                    if ($http_referer !~ ^(https://www.thatsgeeky.com/) ){
                           rewrite ^ http://www.thatsgeeky.com$request_uri permanent;
                    }
              }
              location = / { //for site root
                    rewrite ^ http://www.thatsgeeky.com permanent;
              }
      }
      

      Hope that helps.

      1. 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.

Leave a comment

Your email address will not be published. Required fields are marked *