Clever-clever automated certificate download

Introduction #

A client of mine needs some proper automation on his server. It’s a modest little container-based thing that runs a LAMP stack which powers a few tens of websites. I faced the prospect, for each website that was added to the server, of making the necessary changes to the vhosts configuration, adding sites to the SAN certificate they have, and so on. The manual way of doing this, would be something like:

This is quite a bit of stuff that needs to be remembered every time, so it’s a perfect candidate for automation.

It turns out that this client’s certificate provider, GoDaddy, exposes a well-documented REST API to the rest of the internet. One of the services provided by this API is the ability to interact with certificates

Accessing the API #

To access the API, you need access keys. GoDaddy has a page for that (login required), Click on “Create New API Key” to…create a new API key. Choose “Production” for the environment and add a name that makes sense. The next step shows the API key and secret. I dealt with these as follows.

Add the API key as an environment variable #

In ~/.bashrc, ~/.bash_profile, or wherever you store such things, add a line like this:

GODADDY_API_KEY=922495jklhh_notarealkey;lkfjskdl

Store the API secret securely #

It’s possible to also place the secret in an unencrypted file in the same way, but probably not recommended. I chose to store it using pass. I won’t cover that now, because my friend Mike has already done that quite capably
here.

Something like this should work:

echo "09ujnotarealsecretno9vsd | pass insert godaddy/secret --echo

To make this accessible from the environment, add a line like this in your ~/.bashrc or similar.

GODADDY_SECRET=$(pass show godaddy/secret)

Don’t forget to source ~/.bashrc to read in these environment variables!

Actually accessing the API :) #

The GoDaddy site gives a sample command on its getting started page. It uses curl to send and receive data from the API. To modify it to fit the example above and to access the certificates endpoint, we need:

curl -X GET -H"Authorization: sso-key $GODADDY_API_KEY:$GODADDY_SECRET" https://api.godaddy.com/v1/certificates/

This will give a load of JSON output, which won’t make a lot of sense to begin with. I recommend jq. It’s great for parsing json, but I often find myself resorting to the documentation and a few excellent articles to remember how to use it. To get a list of (godaddy’s internal designation) certificate IDs, the correct incantation is:

curl -X GET -H"Authorization: sso-key $GODADDY_API_KEY:$GODADDY_SECRET" https://api.godaddy.com/v1/certificates/ | jq '.[].certificateId'

The output of that will be something like this:

"rcvqyobnotarealcertidbohikjhkoxx"
"clyszfnnotarealcertidmuqrxacfyym"
"bqolmhdnotarealcertidhu0uxgwixxc"
"uhovvs58notarealcertidnz9n5okxhu"
"tjrltnotarealcertidoyht8vblxmauz"
"jga0o5g5notarealcertidtdwmusq0zp"
"pjghdcv1tpe0h67notarealcertidoxz"
"onotarealcertid6qcnlpd2iplv9shwi"

In this case, I determined that the certificate ID was the first in that list (FIXME: that can probably be automated). Now, we can use a slightly
different request to dig into the info available from the API about that particular cert.

There are also actions that can be called for each certificate ID, of particular interest for this is download/. This produces output like this:

{
  "serialNumber" : "100c0000000000000000",
  "certificateThumbprint" : "45xxx1",
  "pems" : {
    "certificate" : "-----BEGIN CERTIFICATE-----\....xxxx....\n-----END CERTIFICATE-----\n",
    "intermediate" : "-----BEGIN CERTIFICATE-----\n....xxxx....\n-----END CERTIFICATE-----",
    "root" : "-----BEGIN CERTIFICATE-----\n....xxxx....\n-----END CERTIFICATE-----",
    "cross" : null
  }
}

Obviously, the TLS data has been obfuscated above. But the point is….there’s TLS data available.

Enter Ansible #

My automation tool of choice at the moment is Ansible. So, that’s what I went for. Initially, I was going to run some ansible code locally to i) download the certs ii) unzip them iii) go and make a cup of tea iv) etc. etc. But as we now seem to have access to the raw data, why not do that operation on the server itself? We can use ansible to make the client’s webserver contact the godaddy API and use the data it receives to write the certificate files. No messy copying around of files, sounds like a win-win situation.

Ansible has a uri module, which can be used in much the same way we used curl earlier

We can use it to access the API as described earlier, then use the data it receives about the cert as a variable.

The actual bit of code is surprisingly compact:

- name: Grab the certs from the api 
  uri:
    url: "{{ godaddy_api_url_root }}/{{ cert_id }}/download" 
    headers: 
      Authorization: sso-key {{ godaddy_api_key }}:{{ godaddy_secret }}
  register: godaddy_cert_download_response

The {{ godaddy_api_key }} and {{ godaddy_secret }} Ansible variables are passed from the shell to Ansible like this:

godaddy_api_key: "{{ lookup('env', 'GODADDY_API_KEY') }}"
godaddy_secret: "{{ lookup('pipe', 'pass show tokens/godaddy') }}"

See here and here for info about those to lookup plugins…

So now, we have an Ansible variable called {{ godaddy_cert_download_response }} containing the JSON response from the API. We can use normal variable notation to get to this data, as Ansible can use it directly. So, .e.g to get to the certificate data, we would need {{ godaddy_cert_download_response.json.pems.certificate }}

Of course, lookup('env') 'GODADDY_SECRET would also work, but I wanted to showcase the lookup('pipe' pass ...) method, as Mike’s blog post mentioned…

The final piece of the puzzle is to use the template module to place the data in the files that the webserver expects:

- name: Template the bundle file 
  template: 
    src: apache-bundle.j2 
    dest: "{{ godaddy_cert_root_location }}/san-cert-bundle.crt" 
    owner: root 
    group: root 
    mode: 0644 
    backup: yes 
  notify: restart apache2 

- name: Template the certificate file 
  template: 
    src: apache-cert.j2 
    dest: "{{ godaddy_cert_root_location }}/san-cert.crt" 
    owner: root 
    group: root 
    mode: 0644 
    backup: yes 
  notify: restart apache2 

apache-bundle.j2:

{{ godaddy_cert_download_response.json.pems.root }}
{{ godaddy_cert_download_response.json.pems.intermediate }}

apache-cert.j2:

 {{ godaddy_cert_download_response.json.pems.root }}

This worked a treat. The files were written correctly. The server contacted the API on its own, using credentials safely stored and securely transmitted from the Ansible control machine. Yay!

FIXME: There is obviously more that can be done here. For instance, we could generate a CSR on the server, and use that to request a cert update automatically, then place that new cert on the server all in one go, etc. WRiting a test for the role wouldn’t hurt either, of course..

Another blog post on that later, if you’re lucky :)

Ansible code here, feel free to submit PRs, etc.

 
4
Kudos
 
4
Kudos

Now read this

DigitalOcean Vagrantfile

I was a bit besotted with being able to develop ansible code using an on-premise Vsphere cluster as a substrate. As I was developing a single-machine ELK stack for testing, I needed something a bit more beefy than virtualbox on my poor... Continue →