This is a Nuts & Bolts Article – Tips & Tricks for Developers

Create and install SSL certificates with ease – a Capistrano plugin (revised)

by Torsten Bühl

How do I generate the private key file again? Or what is the correct chaining order? Installing, or renewing SSL certificates on the web server has some pitfalls. And most of us don’t do regularly, so it’s easy to forget the process.

That’s why I created a small Capistrano 3 plugin.

This is a revised version of my 4 year old article on the topic. It got a cleanup, some extra functionality, and finally works with Capistrano 3.

What it does

The recipe helps with four common tasks:

  • Generate the private key and CSR (Certificate Signature Request) files. You need them to purchase your SSL certificate.
  • Create blank certificate files following the naming convention for easy copy & paste of the files you get from your CA (certificate authority).
  • Create a chained certificate file. No need to remember the right order of the certificate chain anymore. It also creates a chained certificate with the private key included. Monit for example expects this variation.
  • Upload the certificates to your server and set the correct permissions.

To keep things simple, there are a few conventions:

  • The local certificate folder is APP_ROOT/config/certs/ (configurable)
  • The filename for the intermediate certificate file is intermediate-ca.crt. Or rapidssl-intermediate-ca.crt when you set the ssl_certificate_authority variable to rapidssl.
  • The filename for the domain certificate file is #{ssl_domain}.crt or #{ssl_certificate_authority}-#{ssl_domain}.crt. So for example exceptiontrap.com-rapidssl.crt when you set both variables.

To make it even simpler, ssl_domain and ssl_remote_dir have default values, too. ssl_certificate_authority defaults to nil.

Installation & Configuration

First, grab the Rake task.

# lib/capistrano/tasks/ssl.rake
namespace :load do
  task :defaults do
    set :ssl_domain, -> { fetch(:domain) || fetch(:application) }
    set :ssl_certificate_authority, nil
    set :ssl_remote_dir, "/var/certs"
    set :ssl_local_dir, -> { File.join("config", "certs") }
  end
end

namespace :ssl do
  desc "Upload certificates to the server"
  task :upload do
    on roles(:web) do
      execute :mkdir, '-p', fetch(:ssl_remote_dir)

      upload_certificate certificate_filename
      upload_certificate certificate_key_filename
      upload_certificate chained_certificate_filename
      upload_certificate chained_with_key_certificate_filename
    end
  end

  desc "Create the chained certificates"
  task :chain do
    # Chained certificate
    generate_chained_certificate(certificate_filename, intermediate_certificate_filename, chained_certificate_filename)
    # Chained with private key certificate
    generate_chained_certificate(certificate_key_filename, chained_certificate_filename, chained_with_key_certificate_filename)
  end

  desc "Generate private key and CSR"
  task :generate_key do
    create_local_certificate_dir
    generate_private_key_and_csr_files
  end

  desc "Create blank certificate files"
  task :generate_blanks do
    create_local_certificate_dir
    generate_blank_certificate_files
  end
end

def create_local_certificate_dir
  run_locally do
    execute :mkdir, '-p', fetch(:ssl_local_dir)
  end
end

# Upload a certificate to the remote server
def upload_certificate(filename)
  destination = File.join(fetch(:ssl_remote_dir), filename)

  upload!(certificate_file_for(filename), destination)
  execute :chown, 'root', destination
  execute :chmod, '600', destination
end

# Chains the certificates to a new file
def generate_chained_certificate(certificate1, certificate2, chained_certificate)
  sh "sed -i '' -e '$a\\' #{certificate_file_for(certificate1)}" # Add newline to file unless there is one
  sh "cat #{certificate_file_for(certificate1)} #{certificate_file_for(certificate2)} > #{certificate_file_for(chained_certificate)}"
end

def generate_private_key_and_csr_files
  sh "openssl req -nodes -newkey rsa:2048 -sha256 -keyout #{certificate_file_for(certificate_key_filename)} -out #{certificate_file_for(certificate_csr_filename)}"
end

def generate_blank_certificate_files
  sh "touch #{certificate_file_for(certificate_filename)}"
  sh "touch #{certificate_file_for(intermediate_certificate_filename)}"
end

# Get the full path of a certificate file
def certificate_file_for(filename)
  File.join(fetch(:ssl_local_dir), filename)
end

# Filenames of the different certificates
def certificate_base_filename
  [fetch(:ssl_domain), fetch(:ssl_certificate_authority)].compact.join('-')
end

def certificate_filename
  [certificate_base_filename, 'crt'].join('.')
end

def certificate_csr_filename
  [certificate_base_filename, 'csr'].join('.')
end

def certificate_key_filename
  [certificate_base_filename, 'key'].join('.')
end

def intermediate_certificate_filename
  [fetch(:ssl_certificate_authority), 'intermediate', 'ca.crt'].compact.join('-')
end

def chained_certificate_filename
  [certificate_base_filename, 'chain.crt'].join('-')
end

def chained_with_key_certificate_filename
  [certificate_base_filename, 'chain', 'with', 'key.crt'].join('-')
end

Then configure it for your stages where needed.

# config/deploy/production.rb
set :ssl_domain, "exceptiontrap.com" # your domain (optional, defaults to domain or application)

# optional
set :ssl_certificate_authority, "rapidssl" # name of the CA
set :ssl_remote_dir, "/var/certs" # remote certificate folder
set :ssl_local_dir, "/config/certs" # local certificate folder

All of these variables are optional, but I would recommend to set at least ssl_domain if you haven’t set domain anywhere else.

Please Note: We shouldn't store the private key in source control. So to be safe, just add the whole local certificates folder to your .gitignore file.

Implementation details

The defaults task within the load namespace is automatically executed by Capistrano, and is used to set default values. These can be overwritten in deploy.rb, or the stage files like production.rb. Everything you set here, can be accessed via fetch.

The upload! method that we use in the ssl:upload task is only visible when called within an on roles block. Which actually makes sense, because we usually want to upload the file to a specific server. In this case the web server, not the database server.

To execute commands locally, we can either use execute within a run_locally block (see create_local_certificate_dir), or use the sh method with a string parameter.

Gotcha

You should be aware that all methods defined in a Capistrano Rake task will end up in the global namespace. So another task that's loaded after this task will have access to for example certificate_base_filename. If you're not careful, this will end up in unwanted side effects like calling the wrong method.

Unfortunately I didn't find a good solution. Even extracting the methods into a module, and load it in the task will make them accessible for all other tasks. Also defining the methods within the Rake namespace has no effect – it's solely purpose is to namespace the task names.

What you can do, is putting them into their own class, or include the module within a task block. But both seems a bit odd to me. That's why I named the methods extra-expressive, like certificate_base_filename.

How to use it

  1. cap production ssl:generate_key to generate the private key and CSR.
  2. Order your certificate.
  3. cap production ssl:create_blanks to create empty certificate files for easy copy & paste.
  4. Copy your Domain Certificate to e.g. exceptiontrap.com.crt
  5. Copy the CA’s Intermediate Certificate to e.g. intermediate-ca.crt
  6. cap production ssl:chain to create the chained certificates.
  7. cap production ssl:upload to upload the certificates to your web server.

nginx, Apache & Monit setup

Following our conventions, here are quick examples of how to choose the correct files for nginx, Apache, and Monit.

nginx

ssl_certificate      /var/certs/exceptiontrap.com-chain.crt;
ssl_certificate_key  /var/certs/exceptiontrap.com.key;

Apache

SSLCertificateFile    /var/certs/exceptiontrap.com.crt;
SSLCertificateKeyFile /var/certs/exceptiontrap.com.key;
SSLCertificateChainFile /var/certs/exceptiontrap.com-chain.crt;

Monit

SET HTTPD PORT 2812
    WITH SSL {
        PEMFILE:  /var/certs/exceptiontrap.com-chain-with-key.crt
    }

Let me know

Did this work for you or do you use another approach? Just ping me at @tbuehl

This is a Nuts & Bolts Series post – join the mailing list below to get more tips & tricks.


← Back to Overview

Try our simple and powerful
application error tracking