Centralising Certificate Management - Part 3
What's in This Post
This is the final post about centralising your certificate management. In part 2 I explained how to use the ACME Controller role in order to generate challenges and fetch the resulting certificates for Let's Encrypt.
Now that we have those certificates on our ACME controller, we need to deploy them to our edge systems and restart any services that require it.
Certificate Deploy Playbook
Here is a simple playbook that will search the directories where the ACME Controller role stored its new certificates and copy those to each host as required.
The previous post showed that my acme_hosts group contains host1.example.com and host2.example.com.
A group variable file for acme_hosts:
1# group_vars/acme_hosts.yml
2# src vars define where certificates are found on the acme controller
3src_root_dir: "/etc/acme_controller/"
4src_cert_sub_dir: "certs"
5src_priv_sub_dir: "priv"
6# dst vars define where certificates will be copied on the host.
7# You applications will have their config point to these locations.
8dst_cert_sub_dir: "certs"
9dst_priv_sub_dir: "priv"
10default_dst_root_dir: "/etc/certificates/"
A host variable file for host1:
1# host_vars/host1.example.com.yml
2 domains:
3 - { name: example.com, services: ['postfix', 'nginx'] }
4 - { name: example.biz, services: ['postfix', 'nginx'] }
My playbook is run on the acme controller host. Alternatively you could use delegate_to
to specify your acme controller:
1# acme_deploy.yml
2 - name: Deploy ACME certificates to target hosts
3 hosts: acme_hosts #
4 become: yes
5 vars:
6 services: {}
7 tasks:
8 - name: move files to destination and register restart if changed
9 include_tasks: acme_cert_copy.yml
10 loop: "{{domains}}"
11 loop_control:
12 loop_var: domain
13 label: domain.name
14 handlers:
15 - name: General service reload
16 service:
17 name: "{{item.key}}"
18 state: "{{item.value}}"
19 loop: "{{services|d({})|dict2items}}"
20 listen: "restart services"
The included file:
1# acme_cert_copy.yml
2- name: Create Certificate Directory
3 file:
4 path: "{{default_dst_root_dir}}{{domain.name}}/{{item}}"
5 state: directory
6 mode: 0755
7 vars:
8 new_dirs:
9 - "{{dst_cert_sub_dir}}"
10 - "{{dst_priv_sub_dir}}"
11 loop: "{{new_dirs}}"
12- name: Check that source files exist.
13 local_action:
14 module: stat
15 path: "{{item}}"
16 register: cert_files
17 loop:
18 - "{{src_root_dir}}{{domain.name}}/{{src_cert_sub_dir}}/{{domain.name}}.fullchain"
19 - "{{src_root_dir}}{{domain.name}}/{{src_priv_sub_dir}}/private.key"
20- name: Copy files and register services
21 block:
22 - name: Copy domain certificate
23 copy:
24 src: "{{src_root_dir}}{{domain.name}}/{{src_cert_sub_dir}}/{{domain.name}}.fullchain"
25 dest: "{{default_dst_root_dir}}{{domain.name}}/{{dst_cert_sub_dir}}/{{domain.name}}.fullchain"
26 backup: yes
27 force: yes
28 mode: 0644
29 register: domain_cert
30 notify: "restart services"
31 - name: Copy private key
32 copy:
33 src: "{{src_root_dir}}{{domain.name}}/{{src_priv_sub_dir}}/private.key"
34 dest: "{{default_dst_root_dir}}{{domain.name}}/{{dst_priv_sub_dir}}/private.key"
35 backup: yes
36 force: yes
37 mode: 0644
38 - name: Register services to reload
39 set_fact:
40 services : "{{ services|default({}) |combine({item: 'reloaded'}) }}"
41 loop: "{{domain.services}}"
42 when: domain_cert is changed
43 when:
44 - cert_files.results[0].stat.exists
45 - cert_files.results[1].stat.exists
Why is only 1 certificate file copied?
Although the ACME controller role generated the fullchain certificate, a CA certficate and the server certificate, only the fullchain is copied over. In my case all services I use, requre the fullchain file which contains both the certificate for my issuer (Let's Encrypt's intermediate Certificate) and my server certificate. You can always modify the playbook above to copy the additional certificates to each host.