May 6, 2017

Request Let’s Encrypt Certificates with Ansible

Since version 2.2, Ansible comes with the letsencrypt module. This allows you to request certificates from letsencrypt. Unfortunately, I’ve found the documentation to be a bit lacking, so I want to document what I’ve done to get this working. If your distribution doesn’t have Ansible 2.2 yet, you can very easily install it using pip:

pip install --user ansible

As always, this is not a copy & paste tutorial, but a collection of ideas that you have to adjust to your environment.

First of all, you need an account key. The Ansible documentation doesn’t really tell you how you have to create that, so here is an Ansible task that does just that:

- name: Create Lets Encrypt Account Key
  command: "openssl genrsa -out {{ letsencrypt_account_key }}"
  args:
      creates: "{{ letsencrypt_account_key }}"

You also may want to make sure that the key is protected from unauthorized access:

- file:
    path: "{{ letsencrypt_account_key }}"
    owner: root
    group: root
    mode: 0600

Now we can start with the certificates. For this, it is important to know that the host to which the playbook is applied handles the communication with Let’s Encrypt. This means that it is not sufficent if the host on which you run Ansible can make outbound connections to the Let’s Encrypt servers. You have to adjust your firewall to allow outbound connections from the host the playbook is being applied to to the Let’s Encrypt servers. I usually run a small proxy server (for example, tinyproxy for such outgoing connections. Ansible will respect the http_proxy and https_proxy environment variables.

sslvhost.yml will handle key generation and requesting the certificates from Let’s Encrypt. We call this tasklist for each certificate we want to request. This is also a great place for the proxy environment variables, if you use one:

- name: Generate Certificates for SSL
  include: sslvhost.yml
  vars:
    domain: "example.com"
    key_file: "{{ letsencrypt_key_dir }}/example.com-key.pem"
    req_file: "{{ letsencrypt_cert_dir }}/example.com-req.pem"
    cert_file: "{{ letsencrypt_cert_dir }}/example.com-cert.pem"
  environment:
      http_proxy: http://httpproxy:8080/
      https_proxy: http://httpproxy:8080/

In sslvhost.yml we first create a private key and make sure it has appropriate permissions:

- name: "Generate HTTPS Private Key"
  command: "openssl genrsa -out {{ key_file }} {{ rsa_key_size }}"
  args:
      creates: "{{ key_file }}"

- file:
    path: "{{ key_file }}"
    mode: 0600
    owner: root
    group: root

Now, we need a Certificate Signing Request (CSR). By default, OpenSSL prompts the user for some values when doing so. For Let’s Encrypt, everything except the Common Name is unimportant. Thus we can just supply some dummy values:

- name: Generate CSR
  command: "openssl req -new 
      -key {{ key_file }} 
      -out {{ req_file }}
      -nodes -subj '/C=US/ST=SomeState/L=City/O=Org/CN={{ domain }}'"
  args:
      creates: "{{ req_file }}"

- file:
    path: "{{ req_file }}"
    mode: 0600
    owner: root
    group: root

Now we can request the challenge from Let’s Encrypt, deploy the challenge and retrieve the certificate:

- name: Requesting Lets Encrypt Challenge
  letsencrypt:
    account_email: "{{ letsencrypt_account_email }}"
    account_key: "{{ letsencrypt_account_key }}"
    csr: "{{ req_file }}"
    dest: "{{ cert_file }}"
  register: challenge

- copy:
    dest: "{{ challenge_file_name }}"
    content: "{{ challenge_file_content }}"
    mode: 0644
  when: challenge|changed 
  vars:
      challenge_file_name: "{{ letsencrypt_challenge_dir }}/{{ challenge['challenge_data'][domain]['http-01']['resource'] }}"
      challenge_file_content: "{{ challenge['challenge_data'][domain]['http-01']['resource_value'] }}"

- name: Responding To Challenge and Fetching Certificate
  letsencrypt:
    account_email: "{{ letsencrypt_account_email }}"
    account_key: "{{ letsencrypt_account_key }}"
    csr: "{{ req_file }}"
    dest: "{{ cert_file }}"
    data: "{{ challenge }}"

Of course, this assumes that letsencrypt_challenge_dir is being served by your webserver. Note that challenges are deployed to {{ letsencrypt_challenge_dir }}/.well-known/acme-challenge. For nginx, the following line should do the job:

location /.well-known/acme-challenge/ {
    alias {{ letsencrypt_challenge_dir }}/.well-known/acme-challenge/;
}

Powered by Hugo & Kiss.