Software Developer & Founder Torsten Bühl

Hi, I'm Torsten Bühl. I am the founder of Foodlane, Company Signal, and Exceptiontrap (acquired by Scout APM). I strive to build simple and beautiful software products that people love to use. Learn more about them here

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

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 webserver has some pitfalls. And most of us don’t do it regularly, so it’s easy to forget the process.

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

What it does

The recipe helps with four common tasks:

  • Generate the private key and CSR (Certificate Signature Request) files. You need them to buy your SSL certificate.
  • Create blank certificate files following the naming convention. So you can copy & paste 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.

The Code

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 setting 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. We use it to set default values. These can be overwritten in deploy.rb, or the stage files like production.rb. Everything you set here is accessible 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 every task that’s loaded after will have access to these methods. If you’re not careful, you could end up calling the wrong method.

Unfortunately, I didn’t find a solution to prevent this from happening. 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 – its 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 seem 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

Any feedback? Just ping me at @tbuehl