All,

I've seen several threads talking about SSL encryption for Drupal logins, but haven't found one outlining a complete and viable solution. I can't say that I've got a silver bullet for SSL logins (far from it), but I think I've got it working on my system. I'd like to share my setup in hopes that other people might derive related solutions. Of course, I'd also like feedback: I've been using Linux and Apache for less than a year, Drupal for only a few months, and I'm not even confident that I've actually implemented secure logins. I'd greatly appreciate any sanity checks, suggestions, criticisms, or best practices that people want to offer.

This gist of my approach uses mod_rewrite to redirect users into secure "https://" pages when they're submitting sensitive information, and automatically bring them back to plain "http://" pages when they resume less critical activities. This involves the normal Apache tricks, plus (at least in my case) running parallel http and https Drupal sites from a common codebase and database, and also moving Drupal's clean url rules out of the default .htaccess file.

With that said, I have taken the time to compose a "formal-ish" write up of my setup. If this works for people, I can keep updating it as I learn more, as Drupal changes, etc. Here we go...

1) Goals

  • I wanted SSL encryption for transactions involving logins, passwords, and personal information; I did not want to host the entire site in an encrypted (https) space
  • In addition to logins, I wanted a solution that encrypted administrative pages where logins and passwords can be set or changed.
  • I wanted the solution to work with multisite Drupal installations (specifically unique sites with unique hostnames and databases)
  • I wanted the solution to work with and without Drupal's clean URLs

2) Assumptions

  • Apache is up and running with mod_ssl and mod_rewrite installed, enabled, and configured
  • Apache is listening for traffic on both http and https ports (usually 80 and 443; different in my case, described below)
  • You have write access to Apache's httpd.conf (I'm self-hosting; I can't speak to hosted situations)

3) Complications
Half the machinations that follow wouldn't be necessary--or would be greatly simplified--if I had unrestricted inbound traffic to my server. As it is...

  • My ISP blocks incoming traffic to port 80, so:
    • Normal http traffic (port 80) is redirected through dyndns.org and arrives to my server as "http://myhostname.dyn.mydomain.com:8080"
    • Encrypted https traffic (port 443) is not blocked and thus not redirected; it arrives to my server as "https://myhostname.dyn.mydomain.com"
      • Note that the visible ":[port]" isn't required here: the server expects--and can get--incoming traffic on the default https port

4) Test Setup

5) Drupal Configuration


Login Block
Drupal's default login block is at odds with SSL logins. In the default setup, the login block appears on every page/node of a site; unless the entire site is served under https, login and password information submitted via the login block gets sent in plain text. To protect login and password information, we need to limit the locations from which user can submit this information. Specifically, we want to force logins through "~/?q=user" (or "~/user" with clean urls), which gives us a limited chunk of the site to route through https.

An easy solution is to remove the login block (administer > blocks) and create a static "Login" link pointing to "~/?q=user" (Drupal's menu.module is good for this). My solution was to borrow some php for a state-aware login link (see http://drupal.org/node/17272). Just dump the php into a custom block (administer > blocks > add block), set the input type to "PHP code," and it should be good. Either way, users are now required to visit "~/?q=user" in order to login (or change their personal information, or request a new password, for that matter).

Sites Setup
As mentioned, we're setting up two sites under a single codebase: "firsthost.~" and "secondhost.~"; we'll say that "firsthost.~" is our default site. To get around my port redirect, I ended up establishing four Drupal sites: "firsthost.~" and "secondhost.~" each has one site to handle http and one site to handle https. The paired http/https sites point to the same database (and thus the same content, configuration, users, etc.) while specifying different base urls. I did this so both http and https connections could reach the same database, despite the fact that they use different urls (one with ":[port]" and one without).

I built my "drupal/sites/" directory as follows:

drupal/sites/default/
drupal/sites/firsthost.dyn.mydomain.com/
drupal/sites/secondhost.dyn.mydomain.com:8080/
drupal/sites/secondhost.dyn.mydomain.com/

If you're not redirecting around port 80, you might be able to consolidate the structure (i.e. use only "sites/default/" and "sites/secondhost.dyn.mydomain.com/"). In that case, I think you'd have to specify a "$base_url" (see below) without the "http://" part, thus allowing the site to function with both http and https requests. I don't know if that'll actually work with Drupal, but it's probably worth a try.

Regardless, each site's directory contains a "settings.php" file. I configured the files as follows (relevant info only; the files are identical in every other regard):

  • drupal/sites/default/settings.php
    $db_url = 'mysql://firstSQLuser:firstSQLpassword@localhost/firstDatabaseName';
    $base_url = 'http://firsthost.dyn.mydomain.com:8080';
    
  • drupal/sites/firsthost.dyn.mydomain.com/settings.php
    $db_url = 'mysql://firstSQLuser:firstSQLpassword@localhost/firstDatabaseName';
    $base_url = 'https://firsthost.dyn.mydomain.com';
    
  • drupal/sites/secondhost.dyn.mydomain.com:8080/settings.php
    $db_url = 'mysql://secondSQLuser:secondSQLpassword@localhost/secondDatabaseName';
    $base_url = 'http://secondhost.dyn.mydomain.com:8080';
    
  • drupal/sites/secondhost.dyn.mydomain.com/settings.php
    $db_url = 'mysql://secondSQLuser:secondSQLpassword@localhost/secondDatabaseName';
    $base_url = 'https://secondhost.dyn.mydomain.com';
    

6) Apache Configuration
Nothing terribly exciting here. I configured one Apache virtual host for http traffic on port 8080 and a second virtual host for https traffic on port 443. "firsthost.~" and "secondhost.~" are each mapped into both the http and https virtual hosts. Once again, this might be simpler without the port redirect (everything could be contained within one virtual host, I believe).

As it stands, here's the good stuff from httpd.conf:

#listen for traffic where we expect it
Listen 8080
Listen 443

#setup the virtual host for http traffic on port 8080
<VirtualHost *:8080>
		
		#use our default host.domain name as the ServerName
        ServerName firsthost.dyn.mydomain.com

		#alias other host.domain names into the same space
        ServerAlias secondhost.dyn.mydomain.com

		#point everything to the Drupal codebase
        DocumentRoot /var/www/drupal

		#not sure if this block is necessary in the present context
        <Directory />
                Options FollowSymLinks
                AllowOverride None
        </Directory>

		#setup the Drupal codebase directory
        <Directory /var/www/drupal>
				#note: we don't want MultiViews because it doesn't play well with Drupal's clean urls
                Options Indexes FollowSymLinks
				#note: we want AllowOverride All so Drupal's .htaccess can work its magic	
                AllowOverride All
                Order allow,deny
                allow from all
		</Directory>
</VirtualHost>                               

#setup the virtual host for https traffic on port 443
#note: this vHost is _identical_ to the one specified above with two exceptions:
   # 1) it points to port 443 via "VirtualHost *:443" (below)
   # 2) it includes directives for SSL
<VirtualHost *:443>
		
		#use our default host.domain name as the ServerName
        ServerName firsthost.dyn.mydomain.com

		#alias other host.domain names into the same space
        ServerAlias secondhost.dyn.mydomain.com

		#point everything to the Drupal codebase
        DocumentRoot /var/www/drupal

		#SSL directives; you'll want a more complete set, as these are just placeholders
        SSLEngine on
        SSLCertificateFile /etc/apache2/ssl/mydomain.pem

		#not sure if this block is necessary in the present context
        <Directory />
                Options FollowSymLinks
                AllowOverride None
        </Directory>

		#setup the Drupal codebase directory
        <Directory /var/www/drupal>
				#note: we don't want MultiViews because it doesn't play well with Drupal's clean urls
                Options Indexes FollowSymLinks
				#note: we want AllowOverride All so Drupal's .htaccess can work its magic	
                AllowOverride All
                Order allow,deny
                allow from all
        </Directory>
</VirtualHost>                               

7) Status Check
Restart Apache, fire up a web browser, and have a look around.

You should get plain-vanilla http access at:
http://firsthost.dyn.mydomain.com:8080
http://secondhost.dyn.mydomain.com:8080

You should get encrypted https access at:
https://firsthost.dyn.mydomain.com
https://secondhost.dyn.mydomain.com

You should see the same "firsthost.~" site/content at these two:
http://firsthost.dyn.mydomain.com:8080
https://firsthost.dyn.mydomain.com

And, you should see the same "secondhost.~" site/content at these two:
http://secondhost.dyn.mydomain.com:8080
https://secondhost.dyn.mydomain.com

If you're working through this without the port redirect, just check both your host.domain names, varying "http://" and "https://" in the url.

If something has gone awry at this point, scour the Drupal forums for multisite setups (a likely candidate) and the the Apache lists for virtual host setups (always fun).

8) mod_rewrite Mayhem
Here's where things get a little fuzzy and my eyes start rolling back into my head. If I'm new to Apache and Drupal, I'm an absolute child in the face of mod_rewrite: working out what follows involved a lot of trial and error, hunting-and-pecking, and outright guesswork. But, it _almost_ works like it's supposed to.

.htaccess
I couldn't get my "http to https" mod_rewrite rules to work with the clean url rewrite rules in Drupal's .htaccess file. It should be possible, but I'm not the one to figure it out. So, I moved the clean url rules out of .htaccess and directly into Apache's httpd.conf. This isn't necessarily bad: the mod_rewrite docs suggest that rewrite rules in .htaccess offer significantly poorer performance than those contained in the server configuration file (see "API Phases" at http://httpd.apache.org/docs/1.3/mod/mod_rewrite.html).

I commented out the entire mod_rewrite section of "/var/www/drupal/.htaccess":

# Various rewrite rules.
#<IfModule mod_rewrite.c>
#  RewriteEngine on
  # Modify the RewriteBase if you are using Drupal in a subdirectory and
  # the rewrite rules are not working properly.
  #RewriteBase /drupal

  # Rewrite old-style URLs of the form 'node.php?id=x'.
  #RewriteCond %{REQUEST_FILENAME} !-f
  #RewriteCond %{REQUEST_FILENAME} !-d
  #RewriteCond %{QUERY_STRING} ^id=([^&]+)$
  #RewriteRule node.php index.php?q=node/view/%1 [L]

  # Rewrite old-style URLs of the form 'module.php?mod=x'.
  #RewriteCond %{REQUEST_FILENAME} !-f
  #RewriteCond %{REQUEST_FILENAME} !-d
  #RewriteCond %{QUERY_STRING} ^mod=([^&]+)$
  #RewriteRule module.php index.php?q=%1 [L]

  # Rewrite current-style URLs of the form 'index.php?q=x'.
  #  RewriteCond %{REQUEST_FILENAME} !-f
  #  RewriteCond %{REQUEST_FILENAME} !-d
  #  RewriteRule ^(.*)$ index.php?q=$1 [L,QSA]
#</IfModule>  

In a Drupal 4.6.3 install, only first three and last five lines need attention: everything else should already be disabled by default.

Turning back to Apache's "httpd.conf" (from above, but I removed content we're not presently concerned with):

<VirtualHost *:8080>
		#setup the Drupal codebase directory
        <Directory /var/www/drupal>
				#note: we don't want MultiViews because it doesn't play well with Drupal's clean urls
                Options Indexes FollowSymLinks
				#note: we want AllowOverride All so Drupal's .htaccess can work its magic	
                AllowOverride All
                Order allow,deny
                allow from all
                
                #enable mod_rewrite for this context
                RewriteEngine on

				#the following lines do the clean urls;
				#they are taken directly from Drupal's default .htaccess
                RewriteCond %{REQUEST_FILENAME} !-f
                RewriteCond %{REQUEST_FILENAME} !-d
                RewriteRule ^(.*)$ index.php?q=$1 [QSA]

				#the following lines say:
				# if the requested resource begins with "/user" or "/admin" (clean urls)
                RewriteCond %{REQUEST_URI} ^/(user|admin) [OR]
                # _or_ if the query string begins with "q=user" or "q=admin" (standard urls)
                RewriteCond %{QUERY_STRING} ^q=(user|admin)
                # we want https, so rewrite and redirect the entire url ("^(.*)$"), 
                # prepending "https://" and appending the above-matched resource or string
                # to the base server name 
                RewriteRule ^(.*)$ https://%{SERVER_NAME}/$1 [L,R=301]
		</Directory>
</VirtualHost>                               

<VirtualHost *:443>		
		#setup the Drupal codebase directory
        <Directory /var/www/drupal>
				#note: we don't want MultiViews because it doesn't play well with Drupal's clean urls
                Options Indexes FollowSymLinks
				#note: we want AllowOverride All so Drupal's .htaccess can work its magic	
                AllowOverride All
                Order allow,deny
                allow from all

                #enable mod_rewrite for this context
                RewriteEngine on

				#the following lines do the clean urls;
				#they are taken directly from Drupal's default .htaccess
                RewriteCond %{REQUEST_FILENAME} !-f
                RewriteCond %{REQUEST_FILENAME} !-d
                RewriteRule ^(.*)$ index.php?q=$1 [QSA]
                
				#the following lines say:
				# if the requested resource doesn't begin with "/user" or "/admin" (clean urls)                
                RewriteCond %{REQUEST_URI} !^/(user|admin)
                # _and_ if the query string doesn't begin with "q=user" or "q=admin" (standard urls)
                RewriteCond %{QUERY_STRING} !^q=(user|admin)
                # we're leaving https, so rewrite and redirect the entire url ("^(.*)$"), 
                # prepending "http://" and appending my non-standard http port 
                # and the above-matched resource or string to the base server name 
                RewriteRule (.*) http://%{SERVER_NAME}:8080/$1 [L,R=301]
        </Directory>
</VirtualHost>                               

Huh? The sensitive pages we want to encrypt exist as "~/user" (or "~/?q=user") and "~/admin" (or "~/q=admin"); we don't want or need to encrypt other sections of the sites. So, we're making mod_rewrite

  1. redirect to https when users request "~/user" or "~/admin" pages,
  2. stay within https as long as users are working with "~/user" or "~/admin" pages, and
  3. return to standard http as soon as users leave the "~/user" or "~/admin" pages.

Because it checks against both clean urls (i.e. "~/user") and standard urls (i.e. "~/?q=user"), it should keep working even if you toggle on/off Drupal's clean urls option.

And, much to my own surprise, this whole thing actually works.

When I arrive at either "firsthost.~" or "secondhost.~", I'm placed in a standard, unencrypted "http://" space. I can browse through content nodes, jump back to the front page, etc., and the site is sent via http. When I click the login link, I'm sent to "https://~/user/login" where I can enter my username and password; after login, I'm returned to the still-encrypted "https://~/user/1"; when I click back to a content node, everything again comes from "http://"; when I click into the administration pages, I'm back in the "https://"; and so forth and so on.

Unless I'm fundamentally misunderstanding something about Apache, Drupal, and/or SSL--which is entirely possible--I've got encrypted login and administrative pages working for a multisite, multidatabase Drupal install... with or without clean urls. I'm happy.

9) Problems
Actually, I'm mostly happy.

There is one nagging, relatively minor glitch with this setup when used with Drupal's clean urls: the first (and only the first) page accessed when redirecting from "http://" to "https://" (or vice versa) is displayed with a standard url.

For example, if I click on "Login" from "http://firsthost.dyn.mydomain.com:8080/node/2" (clean), I arrive at "https://firsthost.dyn.mydomain.com/?q=user/login" (standard). However, if I then click "Register" from "~/?q=user/login" (standard), I'm taken to "https://firsthost.dyn.mydomain.com/user/register" (clean). Every subsequent page accessed in the https space will display clean urls. The same thing happens when leaving the https space: the first "http://" page is displayed with standard urls, every subsequent page accessed in the http space (as long as I don't jump back to https) gets clean urls.

I believe there must be a relatively simple fix for this in the rewrite rules, but I have not been able to figure it out.

10) What's Next?
Provided I haven't created any dangerous security holes in Apache or Drupal, and provided this setup is actually doing what I think it's doing (providing SSL encrypted login and administration pages), I'm hoping that I've presented some useful ideas for people wanting secure Drupal logins. However, everything in this write up is specific to my (rather irritatingly specific) setup.

Thinking about a more general tutorial for the Drupal community, a few questions come to mind:

  • How specifically could this be used on a server that doesn't require my port redirect? Do you need two separate Drupal sites (i.e. http and https, with settings.php files for each), or can you get away with a single "$base_url=" that omits the "http://" part of the url? Would omitting the "http://" from "$base_url=" still let Drupal respond to both http and https requests? Can both the http (port 80) and https (port 443) directives be folded into a single VirtualHost block in Apache's httpd.conf? Concrete answers to these and other questions, with an accompanying write up, would probably be a lot more useful to the Drupal community than what I've presented here.
  • Is there any way to make all of this work through Drupal's .htaccess file? That'd be a boon for people who aren't self-hosting their sites. Or, if it can't all be done through .htaccess, what specifically would hosting providers need to do so most of this could happen in .htaccess?
  • Are there common exceptions to "~/user" and "~/admin" where sensitive (personal, login and password, etc.) information is transmitted via Drupal? How about with modules that embed other applications into Drupal (e.g the gallery.module for embedding Gallery2)? I know, for example, that gallery.module syncs user/password information between Drupal and Gallery: does any of that happen in plain text sent to and returned from "outside" the server's firewall? If exceptions do exist, can they be addressed in a manner similar to what I've suggested here?
  • How--for the love of man, how?!--can we fix the problem causing the first page after moving from http to https to be displayed with a standard (not clean) url?

    Cheers,

    --MW

Comments

dimmer’s picture

Thanks a lot for contributing. I'm currently trying to find a good solution to secure important information on drupal. After reading your guide (okay, skimming) I thought that it should be easier than this. Isn't this something that the drupal developers would want to be working on? An SSL module would be sweet.

There's also a cool article with a couple of neat hacks here: http://civicspacelabs.org/home/node/14096 that you might be interested in.

kayfish’s picture

Excellent work. I'm new to drupal and have been trying to figure out exactly the same thing, only to be frustrated.

Secure login and secure handling of personal information is absolutely essential. It seems people have been discussing it for almost 2 years now in other threads. Unfortunately, I still don't see a single, cohesive, working solution.

For drupal to gain wide acceptance, this really needs to be figured out by folks who know what they are doing regarding security (unlike me).

oxblood’s picture

Wouldn't your Rewrite rules potentially cause looping? For some reason when I configure my Drupal (4.6.5) with clean URL, I am able to direct the user appropriately to https version but then I constantly get warning message regarding nonsecure items on the page. On top of that, I am giving base_url https protocol base on the port being accessed as it is suggested here ( http://civicspacelabs.org/home/node/14096 ) and from what I have checked, all the images/css, they do have https://site/path/to/image.jpg for example accordingly. So why aren't they being transferred via secure connection? Maybe the second Rewrite rule for SSL virtualhost is the cluprit because it's forcing everything to be thrown back to http since they are not "/user|/admin".

I wish there was a way I could get a hold of the original poster.

moshe weitzman’s picture

some of this is not right anymore. the port number comes at the beginning of the sites directory name now.

boaz_r’s picture

Just tested this setup. Works like a charm. THANKS!

My 2 bits: I don't like the login link on every page and I'd rather have some page dedicated to login and simple menu entry for getting to that page. If you prefer it this way as well, you can do it like this:

  • Create some content which will serve as the login page. I created a "page" for that (content).
  • Configure the user login block to display itself only on certain addresses, and put the newly added node (from the above step) as the address there (admin-> site building -> blocks -> configure).
  • Create a menu item for this newly added node (page added in 1sy step) and place it in a hierarchy that looks good for you (site building-> menus)
  • Now, this page will needs its own handling in the rewrite rules. Wisely applying the rules given in this article, even without good knowledge of mod_rewrite, does the trick. In my case, the new page was at node/6:
    In the port 80 virtual host section
           <IfModule mod_rewrite.c>
            RewriteEngine on
            #the following lines do the clean urls;
            #they are taken directly from Drupal's default .htaccess
            RewriteCond %{REQUEST_FILENAME} !-f
            RewriteCond %{REQUEST_FILENAME} !-d
            RewriteRule ^(.*)$ index.php?q=$1 [QSA,L]
            # the following lines say:
            # if the requested resource begins with "/user" or "/admin" (clean urls)
            RewriteCond %{REQUEST_URI} ^/(user|admin) [OR]
            # _or_ if the query string begins with "q=user" or "q=admin" (standard urls)
            RewriteCond %{QUERY_STRING} ^q=(user|admin) [OR]
            # or if its node/6 (our login page, BR)
            RewriteCond %{REQUEST_URI} ^/(node/6) [OR]
            # same for non clean URLs
            RewriteCond %{QUERY_STRING} ^q=(node/6)
            # we want https, so rewrite and redirect the entire url ("^(.*)$"),
            # prepending "https://" and appending the above-matched resource or string
            # to the base server name
            RewriteRule ^(.*)$ https://%{SERVER_NAME}/$1 [L,R=301]
         </IfModule>
    

    In the SSL virtual host section

    <IfModule mod_rewrite.c>
                    RewriteEngine on
                    # the following lines do the clean urls;
                    # they are taken directly from Drupal's default .htaccess
                    RewriteCond %{REQUEST_FILENAME} !-f
                    RewriteCond %{REQUEST_FILENAME} !-d
                    RewriteRule ^(.*)$ index.php?q=$1 [QSA]
    
                    # the following lines say:
                    # if the requested resource doesn't begin with "/user" or "/admin" (clean urls)
                    RewriteCond %{REQUEST_URI} !^/(user|admin)
                    # _and_ if the query string doesn't begin with "q=user" or "q=admin" (standard urls)
                    RewriteCond %{QUERY_STRING} !^q=(user|admin)
                    # and its not node/6 (our login page, BR)
                    RewriteCond %{REQUEST_URI} !^/(node/6)
                    # and not node/6 in non clean URLs
                    RewriteCond %{QUERY_STRING} !^q=(node/6)
                    # we're leaving https, so rewrite and redirect the entire url ("^(.*)$"),
                    # prepending "http://" and appending my non-standard http port
                    # and the above-matched resource or string to the base server name
                    RewriteRule (.*) http://%{SERVER_NAME}/$1 [L,R=301]
            </IfModule>
    

Enjoy! :-)