Deploying SSL keys securely with Ansible

Deploying SSL keys securely with Ansible


Last summer, Stu wrote a piece about Ansible – a beatiful and simple automation tool with easy to read configuration files. When I started at Red Badger I joined Stu on a project that already had an Ansible setup. Even though I've never really used it before, Ansible was quite easy to pick up and make the necessary changes. But then came the need to deploy SSL certificates and their respective private keys.

The basic setup is easy enough: you need to upload a few files and add a bit of web server configuration (Nginx in this case). The problem is the provisioning configuration is kept in the git repository next to the source code, including all the necessary files and templates, specifically the SSL keys. Yes, the private keys. Not a very secure solution, since a whole lot of people have read access to the repository.

We could use passphrase protected, triple DES encrypted keys, but then someone would have to input the passphrase every time Nginx was started and all the work setting up a continuous delivery pipeline would be lost.

The goal was storing the certificates and keys in the git repository encrypted, upload them, still encrypted, over a secure channel to the server, place them into a tightly restricted directory and only then decrypt them, all that without extra tools.

I found a few Ansible SSL setups online, but none of them did what I wanted. Most just ignored the problem, some didn't, but used tools like git-encrypt to encrypt the keys. So I set out to create my own setup, with blackjack and hoo... security.

OpenSSL to the rescue

As I said, I wanted to use no extra encryption tools, because I'd have to deal with the extra setup and make sure they are actually secure and trustworthy. Fortunately, OpenSSL itself lets you take a passphrase protected key and generate an unprotected one and vice versa.

If you have a key you want to protect with a passphrase, you'd do it like this:

$ openssl rsa -des -in insecure.key -out secure.key

Reverse of that (a non-interactive version) would be

$ openssl rsa -in secure.key -out insecure.key -passin pass:{your_pass_phrase_here}

Great, that should work. We'll just protect the key up front, and then run the second command from Ansible after upload. Right? Well, there is still one big problem: the passphrase. Encrypting the keys is pointless, if you keep the passphrase in clear text in an Ansible playbook right next to the encrypted file.

Fortunately, Ansible has a neat trick up it's sleeve for just this situation: vars_prompt. Ansible lets you use variables inside playbooks and templates to abstract out the bits of configuration that are actually specific to your setup. The variables come from different sources, one of which can be vars_prompt – a special option in a playbook that prompts the user for input before the playbook is run. You can read all about it in Ansible's documentation.

Great! That allows us to store the passphrase in a secure place – the user's head. It means extracting the SSL setup into a separate playbook, so everything else can still be run without knowing the passphrase, but that is just a small price to pay.

The code

The playbook is quite simple

---
- hosts: webservers
  user: ""
  roles:
    - role: https
  vars_prompt:
    - name: ssl_passphrase
      prompt: "Enter SSL Certificate Passphrase"
      private: false

Line by line it reads: apply to hosts in a group webservers, using a user defined in the inventory file, assign these machines a role https and then the aforementioned vars_prompt, which will prompt the user to "Enter SSL Certificate Passphrase" and make the result accessible as ssl_passphrase.

The role has two task files: nginx.yml and ssl.yml. Here is the nginx.yml:

---
- name: Add SSL virtual hosts
  template: src=nginx-ssl-vhost.conf dest=/etc/nginx/sites-available/{{item.hostname}}_ssl
  with_items: ssl_virtual_hosts
  sudo: yes
  notify: restart nginx
  tags: nginx

- name: Enable SSL virtual hosts
  file: state=link
        src=/etc/nginx/sites-available/{{item.hostname}}_ssl
        path=/etc/nginx/sites-enabled/{{item.hostname}}_ssl
        owner=nginx
  with_items: ssl_virtual_hosts
  sudo: yes
  notify: restart nginx
  tags: nginx

First, it uses a template to create a virtual host configuration for each virtualhost in a list ssl_virtual_hosts defined in a group_vars file. Then it enables the created virtualhosts by creating a symlink (which is the way Nginx handles enabling and disabling virtualhosts on Ubuntu).

The relevant bit of the vars file looks something like this:

---
ssl_virtual_hosts:
  - hostname: secure.example.com
    port: 3000
    certificate: /etc/ssl/certs/secure.example.com.pem
    key: /etc/ssl/private/secure.example.com.key

and the configuration template contains the following:

server {
  listen 443;
  server_name {{item.hostname}};

  ssl on;
  ssl_certificate {{item.certificate}};
  ssl_certificate_key {{item.key}};

  location / {
    proxy_pass http://127.0.0.1:{{item.port}};
    proxy_set_header X-Real-IP $remote_addr;
  }
}

The virtual host we've defined is a simple proxy to a (node.js) server running on port 3000.

The ssl.yml is a bit more interesting:

---
- name: ssl-certs group
  group: name=ssl-cert state=present
  sudo: yes
  tags: ssl

- name: Make sure nginx user is in ssl-cert
  user: name=nginx groups=www-data,ssl-cert
  sudo: yes
  tags: nginx

- name: ssl certs dir
  file: path=/etc/ssl/certs mode=755 state=directory owner=root
  sudo: yes
  tags: ssl

- name: ssl private dir
  file: path=/etc/ssl/private mode=700 state=directory owner=root
  sudo: yes
  tags: ssl

- name: copy the certificate
  copy: src={{item.certificate_src}} dest={{item.certificate_dest}} mode=644 group=ssl-cert
  with_items: ssl_certificates
  sudo: yes
  tags: ssl
  notify: restart nginx

- name: copy the key
  copy: src={{item.key_src}} dest={{item.key_dest}} mode=640 group=ssl-cert
  with_items: ssl_certificates
  sudo: yes
  tags: ssl

- name: strip ssl keys
  command: openssl rsa -in {{item.key_dest}} -out {{item.key_stripped}} -passin pass:{{item.key_password}} creates={{item.key_stripped}}
  sudo: yes
  with_items: ssl_certificates
  tags: ssl
  notify: restart nginx

First, we create a group with access to SSL certificates, add the nginx user to the group and make sure permissions are set up correctly on the /etc/ssl/private and /etc/ssl/certs directories. Then we upload the certificate and encrypted key and finally, we strip the key using the command introduced above. For all the steps we use variables defined in a group_vars file:

---
ssl_certificates:
  - certificate_src: secure.example.com.pem
    certificate_dest: /etc/ssl/certs/secure.example.com.pem
    key_src: secure.example.com.protected.key
    key_dest: /etc/ssl/private/secure.example.com.protected.key
    key_stripped: /etc/ssl/private/secure_example.com.key
    key_password: ""

Notice especially the last line, which sets the key_password for this certificate to the prompt variable defined in the playbook.

When you run the playbook by issuing

$ ansible-playbook -i inventory deploy_ssl.yml

Ansible first asks for the SSL password and then sets everything up securely. And that's it!

What first seemed like a problem that will come down to a choice between security and automation ended up being a pretty simple setup that achieves both and all thanks to Ansible.

Taking it one step further, you could set up a key storage server from which the SSL key would get downloaded directly to the production server over SFTP (using a password from a user prompt to access the key storage), preventing a possible brute force attack on the encrypted key stored in git.

I really hope this helps someone strugling with a similar problem and at the same time shows how fun, simple and great Ansible is. It is definitely worth learning. Once you do, automate everything and get better sleep knowing your servers are expendable and your setup is secure.

Similar posts

Are you looking to build a digital capability?