Skip to content

Commit 6d5ddbe

Browse files
author
Miguel Medinilla
committed
Add managed certificate support
1 parent a132384 commit 6d5ddbe

File tree

6 files changed

+465
-0
lines changed

6 files changed

+465
-0
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ required repositories configured.
6060
| `haproxy_decision_spoa_releases` | see defaults | Mapping keyed by SPOA name (`decision`, `coraza`, `cookie_guard`) that exposes per-OS package URLs (`rh_package_url`, `debian_package_url`, or `package_urls.*`) plus optional checksum settings (`use_checksum`, `checksums_url`, `checksums`). Override entries to point at your own builds. |
6161
| `haproxy_decision_haproxy_package` | `haproxy` | (Debian/Ubuntu) Package name used with `apt`. Override if you need a specific NEVRA. |
6262
| `haproxy_decision_manage_config` | `true` | When `true` the role renders `haproxy.cfg` from `templates/haproxy.cfg.j2`. |
63+
| `haproxy_decision_manage_certificates` | `false` | When `true` the role assembles HAProxy-ready PEM bundles under `haproxy_decision_certificate_dir`. |
64+
| `haproxy_decision_certificates` | `[]` | List of TLS certificate definitions pulling from Certbot or other sources; each entry can point at `fullchain` and `privkey` paths or a pre-built bundle. |
65+
| `haproxy_decision_certificate_bootstrap_enabled` | `true` | Controls whether the role generates a short-lived self-signed bundle when referenced certificate files are missing so HAProxy can start. |
66+
| `haproxy_decision_manage_certbot_hook` | `false` | Deploy a Certbot deploy-hook script that rebuilds managed PEM bundles and reloads HAProxy immediately after renewal. |
67+
| `haproxy_decision_certbot_hook_path` | `/etc/letsencrypt/renewal-hooks/deploy/haproxy-decision-certificates.sh` | Destination of the managed deploy-hook script. |
68+
| `haproxy_decision_certbot_hook_owner` / `_group` / `_mode` | see defaults | Ownership and permissions applied to the deploy-hook script. |
69+
| `haproxy_decision_certbot_hook_reload_command` | `systemctl reload haproxy` | Command executed by the hook after regenerating PEM bundles. |
6370
| `haproxy_decision_global_settings` / `haproxy_decision_defaults_settings` | see defaults | Lists of directives written to the `global` and `defaults` sections. |
6471
| `haproxy_decision_listeners`, `haproxy_decision_frontends`, `haproxy_decision_backends` | `[]` | Optional lists of sections appended to the generated configuration. |
6572
| `haproxy_decision_spoas` | see defaults | Dictionary describing each SPOA daemon. Set `enabled: true` to activate one, adjust service/backend data, and rely on `haproxy_decision_spoa_releases` for download metadata when installing from GitHub releases. |

defaults/main.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,24 @@ haproxy_decision_runtime_dir_mode: "0750"
5050
haproxy_decision_binary_path: /usr/sbin/haproxy
5151
haproxy_decision_setcap_net_bind_service: true
5252

53+
haproxy_decision_manage_certificates: false
54+
haproxy_decision_certificate_dir: "{{ haproxy_decision_config_dir }}/certs"
55+
haproxy_decision_certificate_dir_mode: "0750"
56+
haproxy_decision_certificate_owner: root
57+
haproxy_decision_certificate_group: haproxy
58+
haproxy_decision_certificate_mode: "0640"
59+
haproxy_decision_certificates: []
60+
haproxy_decision_certificate_bootstrap_enabled: true
61+
haproxy_decision_certificate_bootstrap_valid_days: 3
62+
haproxy_decision_certificate_bootstrap_key_type: rsa
63+
haproxy_decision_certificate_bootstrap_key_bits: 2048
64+
haproxy_decision_manage_certbot_hook: false
65+
haproxy_decision_certbot_hook_path: /etc/letsencrypt/renewal-hooks/deploy/haproxy-decision-certificates.sh
66+
haproxy_decision_certbot_hook_owner: root
67+
haproxy_decision_certbot_hook_group: root
68+
haproxy_decision_certbot_hook_mode: "0750"
69+
haproxy_decision_certbot_hook_reload_command: "systemctl reload {{ haproxy_decision_service_name }}"
70+
5371
haproxy_decision_global_settings:
5472
- "log /dev/log local0"
5573
- "log /dev/log local1 notice"

tasks/certificate-item.yml

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
---
2+
- name: Determine HAProxy certificate label
3+
ansible.builtin.set_fact:
4+
haproxy_decision_cert_label: >-
5+
{{
6+
[
7+
haproxy_decision_certificate.filename,
8+
haproxy_decision_certificate.name,
9+
(haproxy_decision_certificate.dest | default('', true) | basename),
10+
((haproxy_decision_certificate.domains | default([''])) | first),
11+
'haproxy'
12+
]
13+
| select('string')
14+
| select('truthy')
15+
| list
16+
| first
17+
}}
18+
tags:
19+
- haproxy-decision
20+
- haproxy-decision-config
21+
- haproxy-decision-certificates
22+
23+
- name: Calculate HAProxy certificate basename
24+
ansible.builtin.set_fact:
25+
haproxy_decision_cert_basename: "{{ haproxy_decision_cert_label | regex_replace('[^A-Za-z0-9_.-]', '_') }}"
26+
tags:
27+
- haproxy-decision
28+
- haproxy-decision-config
29+
- haproxy-decision-certificates
30+
31+
- name: Calculate HAProxy certificate defaults
32+
ansible.builtin.set_fact:
33+
haproxy_decision_cert_dest_path: "{{ haproxy_decision_certificate.dest | default(haproxy_decision_certificate_dir ~ '/' ~ haproxy_decision_cert_basename ~ '.pem') }}"
34+
haproxy_decision_cert_owner: "{{ haproxy_decision_certificate.owner | default(haproxy_decision_certificate_owner) }}"
35+
haproxy_decision_cert_group: "{{ haproxy_decision_certificate.group | default(haproxy_decision_certificate_group) }}"
36+
haproxy_decision_cert_mode: "{{ haproxy_decision_certificate.mode | default(haproxy_decision_certificate_mode) }}"
37+
haproxy_decision_cert_bootstrap_enabled: "{{ haproxy_decision_certificate.bootstrap_self_signed | default(haproxy_decision_certificate_bootstrap_enabled) }}"
38+
haproxy_decision_cert_bootstrap_valid_days: "{{ haproxy_decision_certificate.bootstrap_valid_days | default(haproxy_decision_certificate_bootstrap_valid_days) }}"
39+
haproxy_decision_cert_bootstrap_key_type: "{{ haproxy_decision_certificate.bootstrap_key_type | default(haproxy_decision_certificate_bootstrap_key_type) }}"
40+
haproxy_decision_cert_bootstrap_key_bits: "{{ haproxy_decision_certificate.bootstrap_key_bits | default(haproxy_decision_certificate_bootstrap_key_bits) }}"
41+
haproxy_decision_cert_fullchain_path: "{{ haproxy_decision_certificate.fullchain_path | default(haproxy_decision_certificate.cert_path | default('', true)) }}"
42+
haproxy_decision_cert_privkey_path: "{{ haproxy_decision_certificate.privkey_path | default(haproxy_decision_certificate.key_path | default('', true)) }}"
43+
tags:
44+
- haproxy-decision
45+
- haproxy-decision-config
46+
- haproxy-decision-certificates
47+
48+
- name: Calculate HAProxy certificate source filenames
49+
ansible.builtin.set_fact:
50+
haproxy_decision_cert_fullchain_filename: "{{ (haproxy_decision_cert_fullchain_path | basename) if (haproxy_decision_cert_fullchain_path | length > 0) else '' }}"
51+
haproxy_decision_cert_privkey_filename: "{{ (haproxy_decision_cert_privkey_path | basename) if (haproxy_decision_cert_privkey_path | length > 0) else '' }}"
52+
tags:
53+
- haproxy-decision
54+
- haproxy-decision-config
55+
- haproxy-decision-certificates
56+
57+
- name: Determine HAProxy certificate common name
58+
ansible.builtin.set_fact:
59+
haproxy_decision_cert_common_name: "{{ haproxy_decision_certificate.bootstrap_common_name | default((haproxy_decision_certificate.domains | default([''])) | first | default(haproxy_decision_cert_label)) }}"
60+
tags:
61+
- haproxy-decision
62+
- haproxy-decision-config
63+
- haproxy-decision-certificates
64+
65+
- name: Determine HAProxy certificate bootstrap subject
66+
ansible.builtin.set_fact:
67+
haproxy_decision_cert_bootstrap_subject: "{{ haproxy_decision_certificate.bootstrap_subject | default('/CN=' ~ haproxy_decision_cert_common_name) }}"
68+
tags:
69+
- haproxy-decision
70+
- haproxy-decision-config
71+
- haproxy-decision-certificates
72+
73+
- name: Derive certificate lineage directory
74+
ansible.builtin.set_fact:
75+
haproxy_decision_cert_lineage_dir: "{{ haproxy_decision_certificate.lineage_path }}"
76+
when: haproxy_decision_certificate.lineage_path is defined
77+
tags:
78+
- haproxy-decision
79+
- haproxy-decision-config
80+
- haproxy-decision-certificates
81+
82+
- name: Derive certificate lineage directory from fullchain path
83+
ansible.builtin.set_fact:
84+
haproxy_decision_cert_lineage_dir: "{{ haproxy_decision_cert_fullchain_path | dirname }}"
85+
when:
86+
- haproxy_decision_cert_lineage_dir is not defined
87+
- haproxy_decision_cert_fullchain_path | length > 0
88+
tags:
89+
- haproxy-decision
90+
- haproxy-decision-config
91+
- haproxy-decision-certificates
92+
93+
- name: Inspect existing HAProxy certificate bundle
94+
ansible.builtin.stat:
95+
path: "{{ haproxy_decision_cert_dest_path }}"
96+
register: haproxy_decision_cert_dest_stat
97+
tags:
98+
- haproxy-decision
99+
- haproxy-decision-config
100+
- haproxy-decision-certificates
101+
102+
- name: Inspect combined certificate source
103+
ansible.builtin.stat:
104+
path: "{{ haproxy_decision_certificate.combined_path }}"
105+
register: haproxy_decision_cert_combined_stat
106+
when: haproxy_decision_certificate.combined_path is defined
107+
tags:
108+
- haproxy-decision
109+
- haproxy-decision-config
110+
- haproxy-decision-certificates
111+
112+
- name: Inspect certificate chain source
113+
ansible.builtin.stat:
114+
path: "{{ haproxy_decision_cert_fullchain_path }}"
115+
register: haproxy_decision_cert_fullchain_stat
116+
when: haproxy_decision_cert_fullchain_path | length > 0
117+
tags:
118+
- haproxy-decision
119+
- haproxy-decision-config
120+
- haproxy-decision-certificates
121+
122+
- name: Inspect private key source
123+
ansible.builtin.stat:
124+
path: "{{ haproxy_decision_cert_privkey_path }}"
125+
register: haproxy_decision_cert_privkey_stat
126+
when: haproxy_decision_cert_privkey_path | length > 0
127+
tags:
128+
- haproxy-decision
129+
- haproxy-decision-config
130+
- haproxy-decision-certificates
131+
132+
- name: Track certificate mapping for Certbot deploy hook
133+
ansible.builtin.set_fact:
134+
haproxy_decision_certbot_hook_entries: >-
135+
{{
136+
(haproxy_decision_certbot_hook_entries | default({})) |
137+
combine(
138+
{
139+
haproxy_decision_cert_lineage_dir: (
140+
(haproxy_decision_certbot_hook_entries | default({})).get(haproxy_decision_cert_lineage_dir, [])
141+
+ [ {
142+
'dest': haproxy_decision_cert_dest_path,
143+
'owner': haproxy_decision_cert_owner,
144+
'group': haproxy_decision_cert_group,
145+
'mode': haproxy_decision_cert_mode,
146+
'fullchain': haproxy_decision_cert_fullchain_filename,
147+
'privkey': haproxy_decision_cert_privkey_filename
148+
} ]
149+
)
150+
},
151+
recursive=True
152+
)
153+
}}
154+
when:
155+
- haproxy_decision_manage_certbot_hook | bool
156+
- (haproxy_decision_cert_lineage_dir | default('', true)) | length > 0
157+
- haproxy_decision_cert_fullchain_filename | length > 0
158+
- haproxy_decision_cert_privkey_filename | length > 0
159+
tags:
160+
- haproxy-decision
161+
- haproxy-decision-config
162+
- haproxy-decision-certificates
163+
164+
- name: Detect availability of real certificate artifacts
165+
ansible.builtin.set_fact:
166+
haproxy_decision_real_cert_available: "{{ (
167+
(haproxy_decision_certificate.combined_path is defined)
168+
and (haproxy_decision_cert_combined_stat.stat.exists | default(false))
169+
) or (
170+
(haproxy_decision_cert_fullchain_path | length > 0)
171+
and (haproxy_decision_cert_privkey_path | length > 0)
172+
and (haproxy_decision_cert_fullchain_stat.stat.exists | default(false))
173+
and (haproxy_decision_cert_privkey_stat.stat.exists | default(false))
174+
) }}"
175+
tags:
176+
- haproxy-decision
177+
- haproxy-decision-config
178+
- haproxy-decision-certificates
179+
180+
- name: Install HAProxy certificate bundle from combined source
181+
ansible.builtin.copy:
182+
src: "{{ haproxy_decision_certificate.combined_path }}"
183+
dest: "{{ haproxy_decision_cert_dest_path }}"
184+
owner: "{{ haproxy_decision_cert_owner }}"
185+
group: "{{ haproxy_decision_cert_group }}"
186+
mode: "{{ haproxy_decision_cert_mode }}"
187+
remote_src: true
188+
when:
189+
- haproxy_decision_certificate.combined_path is defined
190+
- haproxy_decision_cert_combined_stat.stat.exists | default(false)
191+
notify: reload haproxy
192+
tags:
193+
- haproxy-decision
194+
- haproxy-decision-config
195+
- haproxy-decision-certificates
196+
197+
- name: Assemble HAProxy certificate bundle from separate files
198+
no_log: true
199+
when:
200+
- haproxy_decision_certificate.combined_path is not defined
201+
- haproxy_decision_cert_fullchain_path | length > 0
202+
- haproxy_decision_cert_privkey_path | length > 0
203+
- haproxy_decision_cert_fullchain_stat.stat.exists | default(false)
204+
- haproxy_decision_cert_privkey_stat.stat.exists | default(false)
205+
tags:
206+
- haproxy-decision
207+
- haproxy-decision-config
208+
- haproxy-decision-certificates
209+
block:
210+
- name: Read certificate chain content
211+
ansible.builtin.slurp:
212+
src: "{{ haproxy_decision_cert_fullchain_path }}"
213+
register: haproxy_decision_cert_fullchain_content
214+
215+
- name: Read certificate private key content
216+
ansible.builtin.slurp:
217+
src: "{{ haproxy_decision_cert_privkey_path }}"
218+
register: haproxy_decision_cert_privkey_content
219+
220+
- name: Deploy assembled HAProxy certificate bundle
221+
ansible.builtin.copy:
222+
dest: "{{ haproxy_decision_cert_dest_path }}"
223+
content: "{{ (haproxy_decision_cert_fullchain_content.content | b64decode | regex_replace('\n*$', ''))
224+
~ '\n'
225+
~ (haproxy_decision_cert_privkey_content.content | b64decode | regex_replace('^\n*', '')) }}"
226+
owner: "{{ haproxy_decision_cert_owner }}"
227+
group: "{{ haproxy_decision_cert_group }}"
228+
mode: "{{ haproxy_decision_cert_mode }}"
229+
notify: reload haproxy
230+
231+
- name: Refresh HAProxy certificate bundle facts
232+
ansible.builtin.stat:
233+
path: "{{ haproxy_decision_cert_dest_path }}"
234+
register: haproxy_decision_cert_dest_stat
235+
tags:
236+
- haproxy-decision
237+
- haproxy-decision-config
238+
- haproxy-decision-certificates
239+
240+
- name: Generate bootstrap certificate for HAProxy
241+
when:
242+
- not haproxy_decision_real_cert_available
243+
- haproxy_decision_cert_bootstrap_enabled | bool
244+
- not haproxy_decision_cert_dest_stat.stat.exists | default(false)
245+
tags:
246+
- haproxy-decision
247+
- haproxy-decision-config
248+
- haproxy-decision-certificates
249+
block:
250+
- name: Ensure OpenSSL is present for bootstrap certificate generation
251+
ansible.builtin.package:
252+
name: openssl
253+
state: present
254+
255+
- name: Create temporary directory for bootstrap certificate
256+
ansible.builtin.tempfile:
257+
state: directory
258+
suffix: haproxy-cert
259+
register: haproxy_decision_cert_bootstrap_tmp
260+
261+
- name: Generate bootstrap self-signed certificate
262+
ansible.builtin.command: >
263+
openssl req -x509 -nodes
264+
-newkey {{ haproxy_decision_cert_bootstrap_key_type }}:{{ haproxy_decision_cert_bootstrap_key_bits }}
265+
-keyout {{ haproxy_decision_cert_bootstrap_tmp.path }}/privkey.pem
266+
-out {{ haproxy_decision_cert_bootstrap_tmp.path }}/fullchain.pem
267+
-days {{ haproxy_decision_cert_bootstrap_valid_days }}
268+
-subj "{{ haproxy_decision_cert_bootstrap_subject }}"
269+
args:
270+
creates: "{{ haproxy_decision_cert_bootstrap_tmp.path }}/fullchain.pem"
271+
no_log: true
272+
273+
- name: Install bootstrap certificate bundle
274+
ansible.builtin.shell: |
275+
set -euo pipefail
276+
cat "{{ haproxy_decision_cert_bootstrap_tmp.path }}/fullchain.pem" "{{ haproxy_decision_cert_bootstrap_tmp.path }}/privkey.pem" > "{{ haproxy_decision_cert_dest_path }}"
277+
args:
278+
executable: /bin/bash
279+
no_log: true
280+
notify: reload haproxy
281+
282+
- name: Set permissions on bootstrap certificate bundle
283+
ansible.builtin.file:
284+
path: "{{ haproxy_decision_cert_dest_path }}"
285+
owner: "{{ haproxy_decision_cert_owner }}"
286+
group: "{{ haproxy_decision_cert_group }}"
287+
mode: "{{ haproxy_decision_cert_mode }}"
288+
289+
- name: Remove temporary bootstrap directory
290+
ansible.builtin.file:
291+
path: "{{ haproxy_decision_cert_bootstrap_tmp.path }}"
292+
state: absent
293+
no_log: true
294+
295+
- name: Warn about placeholder certificate bundle in use
296+
ansible.builtin.debug:
297+
msg: >-
298+
Using existing HAProxy certificate bundle at {{ haproxy_decision_cert_dest_path }} because
299+
referenced certificate files are not available yet.
300+
verbosity: 0
301+
when:
302+
- not haproxy_decision_real_cert_available
303+
- haproxy_decision_cert_dest_stat.stat.exists | default(false)
304+
- haproxy_decision_cert_bootstrap_enabled | bool
305+
tags:
306+
- haproxy-decision
307+
- haproxy-decision-config
308+
- haproxy-decision-certificates
309+
310+
- name: Abort when certificate artifacts are missing
311+
ansible.builtin.fail:
312+
msg: >-
313+
Unable to build HAProxy certificate bundle at {{ haproxy_decision_cert_dest_path }}.
314+
Provide either a combined_path or both fullchain_path and privkey_path (cert_path/key_path),
315+
or enable bootstrap_self_signed for this certificate definition.
316+
when:
317+
- not haproxy_decision_real_cert_available
318+
- not haproxy_decision_cert_bootstrap_enabled | bool
319+
- not haproxy_decision_cert_dest_stat.stat.exists | default(false)
320+
tags:
321+
- haproxy-decision
322+
- haproxy-decision-config
323+
- haproxy-decision-certificates

0 commit comments

Comments
 (0)