Automating LetsEncrypt Certificates With Ansible for AWS Instances
Learn how to more conveniently make your AWS instance safer by automatically generating LetsEncrypt certificates.
Join the DZone community and get the full member experience.
Join For FreeLetsEncrypt is a free certificate provider and myriads of tools and technologies available to automate its certificate generation. Here, we use Ansible and and letsencrypt/dehydrated client to generate certificates for an AWS instance configured as a proxy server using HAProxy.
Create Ansible Inventory for Proxy Server
Lets start with retrieving AWS instance that has HAProxy installed already and create a dynamic inventory. HAProxy instance is spun up with a tag "Role: proxy-server" and we can use the same tag to filter retrieve the proxy instance.
All the tasks described here are assumed to have appropriate aws_access_key, aws_secret_key and region setup properly and omitted here to avoid repetition.
- ec2_remote_facts:
filters:
instance-state-name: running
"tag:role": proxy-server
register: proxy_instances
Now, populate the proxy server IP address in the dynamic inventory.
- name: Add proxy to dynamic host group
add_host:
name: "{{ item.tags.Name }}"
groupname: groupName
ansible_ssh_host: "{{ item.private_ip_address }}"
ansible_user: userName
with_items: "{{ proxy_instances.instances }}"
tags: inventory
Install LetsEncrypt/Dehydrated Client
LetsEncrypt requires us to prove that we own the domain for which we request a certificate. Say we own a domain, "myexample.org," in the public zone, "myexample.org," in AWS Route53. Now we have to prove to LetsEncrypt that we own the domain "myexample.org." Fortunately, we can do that via HTTP-based or dns-01 challenges. We will be using a dns-01-based challenge, as it's pretty straightforward with AWS Route53, and the dehydrated client can automate that.
Let's install the dehydrated client from GitHub:
- name: Clone dehydrated repo into configured directory.
delegate_to: "{{ item.tags.Name }}"
become: yes
git:
repo: "{{ dehydrated_repo }}"
dest: "{{ dehydrated_dir }}"
version: "{{ dehydrated_version }}"
update: "{{ dehydrated_keep_updated }}"
tags: dehydrated
with_items: "{{ proxy_instances.instances }}"
We will make it executable:
- name: Ensure dehydrated is executable.
delegate_to: "{{ item.tags.Name }}"
become: yes
file:
path: "{{ dehydrated_dir }}/dehydrated"
mode: 0755
tags: dehydrated
with_items: "{{ proxy_instances.instances }}"
And we'll create a directory for the dehydrated supporting files:
- name: create directory for dehydrated files.
delegate_to: "{{ item.tags.Name }}"
become: yes
file: path=/etc/dehydrated state=directory mode=0755
with_items: "{{ proxy_instances.instances }}"
Configure letsencrypt/dehydrated client:
we need domains.txt file which contains entry for the domain and subdomain for which we are requesting certificate. Please note that LetsEncrypt CA does not support wild card certificate at the moment of this writing. So we have to include all the subdomain names in domains.txt so that every subdomain name can be included in the same certificate itself.
In our case we are generating certificate for main domain "example.org" and a subdomain "app1.example.org". So, our domains.txt looks as follows:
example.org app1.example.org
Next, download the dns-01 hook from github at https://gist.github.com/joshgarnett/02920846fea35f738d3370fd991bb0e0 to automate the dns challenge for our domain. This hook is a ruby based so pre-install the proxy instance with ruby.
Finally, Create a dehydrated config file to specify required configuration values for dehydrated client to work as we wanted.
Config file contains url to LetsEncrypt certificate API, challenge type, ruby hook for the challenge and certificate admin email id:
CA="https://acme-v01.api.letsencrypt.org/directory"
CHALLENGETYPE="dns-01"
HOOK="${BASEDIR}/lets-encrypt-route53.rb"
CONTACT_EMAIL="admin@example.org"
Now copy all the supporting files in target instance
- name: copy dehydrated files
delegate_to: "{{ item[1].tags.Name }}"
become: yes
copy: src={{ item[0] }} dest=/etc/dehydrated owner=root mode=0775
with_nested:
- ['files/config', 'files/domains.txt', 'files/lets-encrypt-route53.rb' ]
- "{{ proxy_instances.instances }}"
Generate Certificates and Apply
If everything went well so far, we'll get started generating a certificate:
- name: Generate certificate
delegate_to: "{{ item.tags.Name }}"
become: yes
environment:
AWS_REGION: "{{ aws_region }}"
AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
shell: "/opt/dehydrated/dehydrated -c"
register: certificates_resp
with_items: "{{ proxy_instances.instances }}"
We supply AWS credentials via environment variables for our DNS hook to work. So, replace these variables with your AWS credentials.
It should generate the certificate and private key in the target location. Now we have to combine the fullchain certificate and private key file into a new file as "example.org.pem" and assign it to the HAProxy.
- name: combine certificate and priv key into main cert
delegate_to: "{{ item.tags.Name }}"
become: yes
shell:
cmd: cat fullchain.pem privkey.pem > example.org.pem
chdir: /etc/dehydrated/certs/example.org
tags: cert
with_items: "{{ proxy_instances.instances }}"
We can assign the certificate to HAProxy as follows:
frontend https_443_frontend
bind *:443 ssl crt /etc/dehydrated/certs/example.org/example.org.pem
Hit your application and observe that your app is up and running with HTTPS.
Opinions expressed by DZone contributors are their own.
Comments