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/;
}