Category Archives: LetsEncrypt

Automating LetsEncrypt certificate generation with Ansible for AWS Instances

LetsEncrypt is a free certificate provider and myriads of tools and technologies available to automate its certificate generation. Here, we use Ansible, letsencrypt/dehydrated client to generate certificate for AWS instance configured as 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
  tags: inventory

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: gatewayed
    ansible_ssh_host: "{{ item.private_ip_address }}"
    ansible_user: centos
  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 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” and we can do that via http based or dns-01 challenge. We will be using dns-01 based challenge as its straighforward with AWS Route53, and dehydrated client can automate that.

lets install 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 dehydrated 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 }}"

create 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 certificate and apply:

we are off to generate the certificate if everything went well so far:

- 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, replace these variables with your aws credentials.

It should generate the certificate and private key in the target location. Now we have to combine 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 > mybahmni.org.pem
    chdir: /etc/dehydrated/certs/mybahmni.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/mybahmni.org/mybahmni.org.pem

Hit your application and observe that your app is up and running with https.