Deploying Rails Apps with Capistrano without root or sudo Privileges

In an effort to prepare for my presentation on Rails Deployment with Capistrano and Phusion Passenger at the next Bmore on Rails ruby users meetup, I'm writing a series of blog posts to help illustrate some concepts. This represents the second installment of the series. Better setup for environments in rails addressed the set of structural changes that I make to any fresh Rails app. This post will focus on some general principles and useful security considerations to take into account when deploying Rails apps with Capistrano.

The primary point of this post is this: You don't need to deploy using root. And you don't need to grant sudo access to the user used for deployment.

Our primary deployment setup is either a single or two-box solution (web server, asset server, database server spread across two). We generally use MySQL for the backend, and Phusion Passenger to serve Rails. We deploy to either Ubuntu Server (Hardy 7.10) or CentOS 5. We also generally disallow root ssh access.

First of all, it's important to categorize tasks into two types: privileged tasks and unprivileged tasks. The nice part about a rails app is that, for the most part, it's pretty self-sufficient, and rarely ever needs to venture out of its own tree in the filesystem. This means that we can get away with deploying with an unprivileged account. There are, however, certain 'setup' tasks that likely need to be executed with root/privileged access. Fortunately, all of our privileged tasks can be performed before we ever deploy the app!

Privileged Tasks
For our baseline deployment, this includes the following:

  1. Install any necessary software (Ruby, RubyGems, ImageMagick, MySQL, etc.)

  2. Create the Rails app structure. For us, we create the following structure:

/var/vhosts/myapp
  /releases
  /shared
    /content

The default Capistrano setup task performs similar functionality. The only additional folder here is /shared/content. We use this folder to hold all of our uploaded assets (mostly via the File Column plugin). Then, on successive deployments, we set up symbolic links from the public directory up to this shared folder. This allows these assets to live outside of the context of a specific release.

  1. Create a log directory at the system level: /var/log/myapp.

  2. Create a symlink from the apache config directory (generally /etc/apache2/sites-enabled on Ubuntu, /etc/httpd/conf.d on CentOS) to /var/vhosts/myapp/shared/passenger.conf. Note that at this stage, passenger.conf does not exist. This is ok, as our cold deployment will address this, and each successive deployment will exploit this. This passenger.conf file will actually just be another symlink out to the current release. What this allows us is the ability to alter our apache/passenger configuration for this app on successive deployments. The apache config directories will not be visible to non-privileged users, and thus, we won't be able to update those symlinks using a nonprivileged account.

  3. Create (if it doesn't already exist) a deploy user, and assign it to the same group that apache runs (www-data on Ubuntu, apache on CentOS).

$> adduser --group www-data deploy
  1. chown the app root (/var/vhosts/myapp) and log directory (/var/log/myapp), and give both user/group write permissions (775, 774, or 770). Apache's user will be able to write to these directories by virtue of them being owned by the group.
$> chown 775 deploy:www-data -R /var/vhosts/myapp /var/log/myapp
  1. Create your database, and grant all privileges to a non-root user.
mysql> CREATE DATABASE myapp;
mysql> GRANT ALL PRIVILEGES ON myapp.* TO 'myappuser'@localhost IDENTIFIED BY 'asecretpassword';
  1. Install Passenger and the GemInstaller gem (as long as we keep our geminstaller.yml file up to date, we can ensure that our server will use those exact gem versions).

In order to execute these tasks, you're going to need root access. Note, however, that it is ill-advised to ever include your root password in your deploy script (lest someone accidentally commits it to the repo). One way to handle this is to implement some/all of this functionality in cap deploy:setup. You can then temporarily run this with the root user, but no password (this only works if root can ssh in), which will force you to enter your password. I like this approach, because at this point, we'll never need the root password again. Put it in a lockbox and leave it alone! Your other option (if root can't login), is just to login with the unprivileged account, su -, then perform these steps manually. Similarly, you can temporarily grant your deploy user unrestricted sudo access temporarily (but don't forget to undo this!) in the sudoers file. Either way, these are one time steps, and it's really not much of a hassle if you know what you're doing.

Unprivileged Tasks
Everything else you'll ever have to do (unless you're adding a new feature that requires installing other software, etc.) with Capistrano can now be completed with the unprivileged deploy user that we created in step 5 above.

At this stage, the only difference between a cold deployment and a successive deployment is the fact that your app was never running in the first place. In essence, it's the difference between a hard restart of apache and a soft restart. All other steps are the same:

  1. create a new release (with the updated code)

  2. update the current and filecolumn symlinks (these were the symlinks I mentioned above that point out to the shared/content directory)

  3. ensure that all of our necessary gems are installed (reading geminstaller.yml and executing geminstaller if necessary) -- more on this in the next post

  4. run any pending migrations

  5. update /var/vhosts/myapp/shared/passenger.conf to point to the config snippet in the latest release

  6. restart apache (hard if cold deploy or if the apache config is updated, soft otherwise)

We overwrite more or less all of the default Capistrano recipes. Actual code will be released probably just after the presentation (Tuesday, Jun 10, 2008, 7:00 PM at Medical Decision Logic, 1216 E. Baltimore Street, Baltimore, MD 21202), as I'm still tweaking things here and there.

The important take home note is this: yeah, it's nice to be able to just pull out your Capistrano recipes and build an app on a brand new server from scratch with a few command line calls. However, it is borderline impossible to securely pull this off. The line between server setup and application deployment becomes blurred when you try to put this together. The server setup process, by nature, requires root/privileged access. Incremental deployment, however, does not require this level of privilege. Capistrano was not designed to build a server from scratch. Rather, a better approach is to develop a server image that you will use for all of your client app servers.

My next post will elaborate (with more code samples, particularly recipe snippets and some capistrano/rails extensions I've been working on) on much of what was covered here. Additionally, I'll go into further detail regarding other ways to make maintaining your production apps easier with Capistrano.