Begin main content

Stunnel + Thin + Rails for the world's easiest ssl hosting

These days I do most quick and dirty web stuff with Ruby on Rails, and any open source web stuff I install I prefer Rails based code for easy hacking. Every so often you want to host something for internal or other small-volume usage, but it has to be SSL. Traditionally this is a pain in the backside, especially with Rails. Back in the day I did everything with OpenACS or plain AOLServer. This meant I had to install a bunch of stuff by hand (although debian packages are now available), but once it was done it was a simple config tweak to change the number of threads and/or add ssl etc. With Rails it's kind of the opposite. Getting things up and running is incredibly easy, but scaling it or adding ssl usually means Apache, or Nginx, or some other (relatively) heavy server plus other stuff. Doing this all as a non-super user is even more painful.

For a client I wanted to setup a Redmine instance for a wiki and browsing internal git repositories. It's a Rails 2.x app. Initial setup is easy. Using the latest trunk version of Redmine (which supports Ruby 1.9 and uses bundler) it's a simple matter of the following steps (which assume pretty much nothing is installed except base libraries like sqlite3):

  • checking out the svn trunk

svn co svn co http://redmine.rubyforge.org/svn/trunk redmine
cd redmine

  • installing rvm, setting up the right ruby & gem set

bash -s stable < <(curl -s https://raw.github.com/wayneeseguin/rvm/master/binscripts/rvm-installer)
. ~/.bashrc
rvm install ruby-1.9.3
rvm use ruby-1.9.3@redmine --create
rvm install bundler

  • installing bundler and using it to prepare all the ruby gems (I am using sqlite and don't have mysql libraries installed)

gem install bundler
bundle install --without=mysql

You now have Redmine up and running, via the test webrick server, on port 3000. But that's no good for two reasons, one we obviously don't have ssl, and two webrick is a single process (and ruby is always a single processor thread) so it's not very scalable.

A nice simple, and yet fast, way to scale Ruby http servers is Thin. It's Ruby and C. It's only http though, so we need to put ssl in front some how. A great way of doing this is stunnel, which can tunnel just about anything to anything when it comes to ssl. So we run a couple of Thin instances, and stunner to proxy it to ssl. The great thing is we can arrange for the communication between the two to be via Unix sockets so we don't even need to reserve localhost TCP ports so we can scale the number of instances without checking with anyone else.

Here are my steps for Thin and stunnel:

  • Install Thin:

gem install thin

  • Install stunnel:

# get the latest version of stunner from http://www.stunnel.org/?page=downloads
tar zxf stunnel-4.xx.tar.gz
cd stunnel-4.xx
mkdir ~/usr 
./configure --prefix=~/usr
make
make install

  • You need a .pem certificate for stunnel. If you don't have a real certificate, you can create a self-signed one like so:

openssl req -new -x509 -days 365 -nodes -config stunnel.cnf -out stunnel.pem -keyout my_stunnel.pem

  • where the stunnel.cnf file is in the source distribution. Answer the questions appropriately.
  • stunnel needs a config file, something like this (in this case for three thin instances)

pid =
cert = /path/to/my_stunnel.pem

[redmine]
accept = 12345
connect = /path/to/my_rails_app/tmp/thin.0.sock

connect = /path/to/my_rails_app/tmp/thin.1.sock

connect = /path/to/my_rails_app/tmp/thin.2.sock

  • start up stunnel

~/usr/bin/stunnel /path/to/my/stunnel.conf

  • start up your rails app with Thin

thin start -s3 --socket /path/to/my_rails_app/tmp/thin.sock -e production

And that's it, your site running on https port 12345! All this has not required root access at any point. If you want to listen on a low port, eg. 443, the only thing that needs to run as root is stunnel.

But don't break out the champagne yet - we have a problem. Your rails app knows the hostname and port thanks to the Host: header, but it has no idea that it's running behind stunnel and is therefore ssl. This is only a problem in the occasions where a full url is needed, and the primary case is for http redirect headers. I take care of this by rewriting the Location header in application_controller.rb:

after_filter :force_https_for_redirects
def force_https_for_redirects
if response.headers.has_key?("Location")
response.headers["Location"].sub!(/^http:/, 'https:')
end
end

You may have other cases that need fixing, like constructing urls for emails. Note that I wouldn't recommend mucking about with the X-Forwarded-Proto patches for stunnel. I just don't trust third party patches for security software, and in any case I'm not sure it would work with using Unix sockets for the intermediary transport.

Update: Here's how to monkey patch ActionController::UrlWriter::url_for() so your emails will have correct links. In Rails 2 (in my case for redmine), I did it in environment.rb - for Rails 3 I think you could do it in application.rb, but it doesn't really matter where you do it as long as it gets compiled after ActionController::UrlWriter is loaded.

module ActionController
module UrlWriter
alias :original_url_for:url_for
def url_for(options = {})
options[:host] = 'efserv.com'unless options[:host]
options[:protocol] = 'https' unless options[:protocol]
options[:port] = 7022 unless options[:port]

original_url_for(options)
end
end
end

Another update: There's another patch you need to make in the redmine code itself, in app/controllers/application_controller.rb, make the following change to the require_login method, adding in the :protocol => 'https://'. This is required since it doesn't use url_for to generate the redirect url.

respond_to do |format|
    format.html { redirect_to :protocol => 'https://', :controller => "account", :action => "login", :back_url => url }
    format.atom { redirect_to :protocol => 'https://', :controller => "account", :action => "login", :back_url => url }

09:12 PM, 20 Mar 2012 by Mark Aufflick Permalink

Add comment