Create and install SSL certificates with ease – a Capistrano plugin (revised)
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
. Orrapidssl-intermediate-ca.crt
when you set thessl_certificate_authority
variable torapidssl
. - The filename for the domain certificate file is
#{ssl_domain}.crt
or#{ssl_certificate_authority}-#{ssl_domain}.crt
. So for exampleexceptiontrap.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
cap production ssl:generate_key
to generate the private key and CSR.- Order your certificate.
cap production ssl:create_blanks
to create empty certificate files for easy copy & paste.- Copy your Domain Certificate to e.g.
exceptiontrap.com.crt
- Copy the CA’s Intermediate Certificate to e.g.
intermediate-ca.crt
cap production ssl:chain
to create the chained certificates.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